@@ -4,6 +4,8 @@ import * as commander from "commander";
44import * as fs from "fs" ;
55import * as path from "path" ;
66import * as ts from "typescript" ;
7+ import { createMacro , MacroError , MacroHandler , Options as MacroOptions } from "babel-plugin-macros" ;
8+ import { ordinal } from "./util/ordinal" ;
79
810// Default format to use for `format` option
911const defaultFormat = "ts"
@@ -326,3 +328,114 @@ export function main() {
326328 fs . writeFileSync ( outPath , generatedCode ) ;
327329 }
328330}
331+
332+ const macroHandler : MacroHandler = params => {
333+ const callPaths = params . references [ "makeCheckers" ] ;
334+
335+ // Bail out if no calls in this file
336+ if ( ! callPaths || ! callPaths . length ) {
337+ return ;
338+ }
339+
340+ const { babel, state : { filename} } = params
341+
342+ // Rename any bindings to `t` in any parent scope of any call
343+ for ( const callPath of callPaths ) {
344+ let scope = callPath . scope ;
345+ while ( true ) {
346+ if ( scope . hasBinding ( "t" ) ) {
347+ scope . rename ( "t" ) ;
348+ }
349+ if ( ! scope . parent || ( scope . parent === scope ) ) {
350+ break ;
351+ }
352+ scope = scope . parent ;
353+ }
354+ }
355+
356+ // Add `import * as t from 'ts-interface-checker'` statement
357+ const firstStatementPath = callPaths [ 0 ] . findParent ( path => path . isProgram ( ) ) . get ( "body.0" ) as babel . NodePath ;
358+ firstStatementPath . insertBefore (
359+ babel . types . importDeclaration (
360+ [ babel . types . importNamespaceSpecifier ( babel . types . identifier ( "t" ) ) ] ,
361+ babel . types . stringLiteral ( "ts-interface-checker" ) ,
362+ )
363+ ) ;
364+
365+ // Get the user config passed to us by babel-plugin-macros, for use as default options
366+ // Note: `config` property is missing in `babelPluginMacros.MacroParams` type definition
367+ const defaultOptions = ( ( params as any ) . config || { } ) as ICompilerOptions
368+
369+ callPaths . forEach ( ( { parentPath} , callIndex ) => {
370+ // Determine compiler parameters
371+ const getArgValue = getGetArgValue ( callIndex , parentPath ) ;
372+ const file = path . resolve ( filename , ".." , getArgValue ( 0 ) || path . basename ( filename ) ) ;
373+ const options = { ...defaultOptions , ...( getArgValue ( 1 ) || { } ) , format : "js:cjs" } ;
374+
375+ // Compile
376+ let compiled : string | undefined ;
377+ try {
378+ compiled = Compiler . compile ( file , options ) ;
379+ } catch ( error ) {
380+ throw macroError ( `${ error . name } : ${ error . message } ` )
381+ }
382+
383+ // Get the compiled type suite as AST node
384+ const parsed = parse ( compiled ) ! ;
385+ if ( parsed . type !== 'File' ) throw macroInternalError ( ) ;
386+ if ( parsed . program . body [ 1 ] . type !== 'ExpressionStatement' ) throw macroInternalError ( ) ;
387+ if ( parsed . program . body [ 1 ] . expression . type !== 'AssignmentExpression' ) throw macroInternalError ( ) ;
388+ const typeSuiteNode = parsed . program . body [ 1 ] . expression . right ;
389+
390+ // Build checker suite expression using type suite
391+ const checkerSuiteNode = babel . types . callExpression (
392+ babel . types . memberExpression (
393+ babel . types . identifier ( "t" ) ,
394+ babel . types . identifier ( "createCheckers" ) ,
395+ ) ,
396+ [ typeSuiteNode ] ,
397+ ) ;
398+
399+ // Replace call with checker suite expression
400+ parentPath . replaceWith ( checkerSuiteNode ) ;
401+ } )
402+
403+ function parse ( code : string ) {
404+ return babel . parse ( code , { configFile : false } ) ;
405+ }
406+ function getGetArgValue ( callIndex : number , callExpressionPath : babel . NodePath ) {
407+ const argPaths = callExpressionPath . get ( "arguments" ) ;
408+ if ( ! Array . isArray ( argPaths ) ) throw macroInternalError ( )
409+ return ( argIndex : number ) : any => {
410+ const argPath = argPaths [ argIndex ] ;
411+ if ( ! argPath ) {
412+ return null ;
413+ }
414+ const { confident, value } = argPath . evaluate ( ) ;
415+ if ( ! confident ) {
416+ /**
417+ * TODO: Could not get following line to work:
418+ * const lineSuffix = argPath.node.loc ? ` on line ${argPath.node.loc.start.line}` : ""
419+ * Line number displayed is for the intermediary js produced by typescript.
420+ * Even with `inputSourceMap: true`, Babel doesn't seem to parse inline sourcemaps in input.
421+ * Maybe babel-plugin-macros doesn't support "input -> TS -> babel -> output" pipeline?
422+ * Or maybe I'm doing that pipeline wrong?
423+ */
424+ throw macroError ( `Unable to evaluate ${ ordinal ( argIndex + 1 ) } argument to ${ ordinal ( callIndex + 1 ) } call to makeCheckers()` )
425+ }
426+ return value
427+ }
428+ }
429+ }
430+
431+ function macroError ( message : string ) : MacroError {
432+ return new MacroError ( `ts-interface-builder/macro: ${ message } ` )
433+ }
434+
435+ function macroInternalError ( message ?: string ) : MacroError {
436+ return macroError ( `Internal Error: ${ message || 'Check stack trace' } ` )
437+ }
438+
439+ const macroParams : MacroOptions = { configName : "ts-interface-builder" } ;
440+
441+ export const macro = ( ) => createMacro ( macroHandler , macroParams ) ;
0 commit comments