|
| 1 | +// @ts-ignore |
1 | 2 | import * as path from "path";
|
2 |
| -import { MacroHandler, MacroError, createMacro } from "babel-plugin-macros"; |
3 |
| -import { Compiler, ICompilerOptions } from "./index"; |
4 |
| - |
| 3 | +import { createMacro, MacroHandler } from "babel-plugin-macros"; |
| 4 | +import { NodePath, types } from "@babel/core"; // typescript types ONLY |
| 5 | +import { ICompilerOptions } from "./index"; |
| 6 | +import { getCallPaths } from "./macro/getCallPaths"; |
| 7 | +import { RequirementRegistry } from "./macro/RequirementRegistry"; |
| 8 | +import { getGetArgValue } from "./macro/getGetArgValue"; |
| 9 | +import { compileTypeSuite, ICompilerArgs } from "./macro/compileTypeSuite"; |
| 10 | +import { macroInternalError } from "./macro/errors"; |
| 11 | + |
| 12 | +const tsInterfaceCheckerIdentifier = "t"; |
| 13 | +const onceIdentifier = "once"; |
| 14 | + |
| 15 | +/** |
| 16 | + * This function is called for each file that imports the macro module. |
| 17 | + * `params.references` is an object where each key is the name of a variable imported from the macro module, |
| 18 | + * and each value is an array of references to that that variable. |
| 19 | + * Said references come in the form of Babel `NodePath`s, |
| 20 | + * which have AST (Abstract Syntax Tree) data and methods for manipulating it. |
| 21 | + * For more info: https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/author.md#function-api |
| 22 | + */ |
5 | 23 | const macroHandler: MacroHandler = (params) => {
|
6 |
| - const callPaths = params.references["makeCheckers"]; |
7 |
| - |
8 |
| - // Bail out if no calls in this file |
9 |
| - if (!callPaths || !callPaths.length) { |
| 24 | + const { references, babel, state } = params; |
| 25 | + const callPaths = getCallPaths(references); |
| 26 | + const somePath = callPaths.getTypeSuite[0] || callPaths.getCheckers[0]; |
| 27 | + if (!somePath) { |
10 | 28 | return;
|
11 | 29 | }
|
| 30 | + const programPath = somePath.findParent((path) => path.isProgram()); |
| 31 | + |
| 32 | + const registry = new RequirementRegistry(); |
| 33 | + const toReplace = [ |
| 34 | + ...callPaths.getTypeSuite.map((callPath, index) => { |
| 35 | + const compilerArgs = getCompilerArgs(callPath, "getTypeSuite", index); |
| 36 | + const typeSuiteId = registry.requireTypeSuite(compilerArgs); |
| 37 | + return { callPath, id: typeSuiteId }; |
| 38 | + }), |
| 39 | + ...callPaths.getCheckers.map((callPath, index) => { |
| 40 | + const compilerArgs = getCompilerArgs(callPath, "getCheckers", index); |
| 41 | + const checkerSuiteId = registry.requireCheckerSuite(compilerArgs); |
| 42 | + return { callPath, id: checkerSuiteId }; |
| 43 | + }), |
| 44 | + ]; |
| 45 | + |
| 46 | + // Begin mutations |
| 47 | + |
| 48 | + programPath.scope.rename(tsInterfaceCheckerIdentifier); |
| 49 | + programPath.scope.rename(onceIdentifier); |
| 50 | + toReplace.forEach(({ callPath, id }) => { |
| 51 | + scopeRenameRecursive(callPath.scope, id); |
| 52 | + }); |
12 | 53 |
|
13 |
| - const { |
14 |
| - babel, |
15 |
| - state: { filename }, |
16 |
| - } = params; |
17 |
| - |
18 |
| - // Rename any bindings to `t` in any parent scope of any call |
19 |
| - for (const callPath of callPaths) { |
20 |
| - let scope = callPath.scope; |
21 |
| - while (true) { |
22 |
| - if (scope.hasBinding("t")) { |
23 |
| - scope.rename("t"); |
24 |
| - } |
25 |
| - if (!scope.parent || scope.parent === scope) { |
26 |
| - break; |
27 |
| - } |
28 |
| - scope = scope.parent; |
| 54 | + const toPrepend = ` |
| 55 | + import * as ${tsInterfaceCheckerIdentifier} from "ts-interface-checker"; |
| 56 | + function ${onceIdentifier}(fn) { |
| 57 | + var result; |
| 58 | + return function () { |
| 59 | + return result || (result = fn()); |
| 60 | + }; |
29 | 61 | }
|
30 |
| - } |
| 62 | + ${registry.typeSuites |
| 63 | + .map( |
| 64 | + ({ compilerArgs, id }) => ` |
| 65 | + var ${id} = ${onceIdentifier}(function(){ |
| 66 | + return ${compileTypeSuite(compilerArgs)} |
| 67 | + }); |
| 68 | + ` |
| 69 | + ) |
| 70 | + .join("")} |
| 71 | + ${registry.checkerSuites |
| 72 | + .map( |
| 73 | + ({ typeSuiteId, id }) => ` |
| 74 | + var ${id} = ${onceIdentifier}(function(){ |
| 75 | + return ${tsInterfaceCheckerIdentifier}.createCheckers(${typeSuiteId}()); |
| 76 | + }); |
| 77 | + ` |
| 78 | + ) |
| 79 | + .join("")} |
| 80 | + `; |
| 81 | + parseStatements(toPrepend).reverse().forEach(prependProgramStatement); |
| 82 | + |
| 83 | + const { identifier, callExpression } = babel.types; |
| 84 | + toReplace.forEach(({ callPath, id }) => { |
| 85 | + callPath.replaceWith(callExpression(identifier(id), [])); |
| 86 | + }); |
| 87 | + |
| 88 | + // Done mutations (only helper functions below) |
| 89 | + |
| 90 | + function getCompilerArgs( |
| 91 | + callPath: NodePath<types.CallExpression>, |
| 92 | + functionName: string, |
| 93 | + callIndex: number |
| 94 | + ): ICompilerArgs { |
| 95 | + const callDescription = `${functionName} call ${callIndex + 1}`; |
| 96 | + const getArgValue = getGetArgValue(callPath, callDescription); |
| 97 | + |
| 98 | + const basename = getArgValue(0) || path.basename(state.filename); |
| 99 | + const file = path.resolve(state.filename, "..", basename); |
31 | 100 |
|
32 |
| - // Add `import * as t from 'ts-interface-checker'` statement |
33 |
| - const firstStatementPath = callPaths[0] |
34 |
| - .findParent((path) => path.isProgram()) |
35 |
| - .get("body.0") as babel.NodePath; |
36 |
| - firstStatementPath.insertBefore( |
37 |
| - babel.types.importDeclaration( |
38 |
| - [babel.types.importNamespaceSpecifier(babel.types.identifier("t"))], |
39 |
| - babel.types.stringLiteral("ts-interface-checker") |
40 |
| - ) |
41 |
| - ); |
42 |
| - |
43 |
| - // Get the user config passed to us by babel-plugin-macros, for use as default options |
44 |
| - // Note: `config` property is missing in `babelPluginMacros.MacroParams` type definition |
45 |
| - const defaultOptions = ((params as any).config || {}) as ICompilerOptions; |
46 |
| - |
47 |
| - callPaths.forEach(({ parentPath }, callIndex) => { |
48 |
| - // Determine compiler parameters |
49 |
| - const getArgValue = getGetArgValue(callIndex, parentPath); |
50 |
| - const file = path.resolve( |
51 |
| - filename, |
52 |
| - "..", |
53 |
| - getArgValue(0) || path.basename(filename) |
54 |
| - ); |
| 101 | + // Get the user config passed to us by babel-plugin-macros, for use as default options |
| 102 | + // Note: `config` property is missing in `babelPluginMacros.MacroParams` type definition |
| 103 | + const defaultOptions = (params as any).config; |
55 | 104 | const options = {
|
56 |
| - ...defaultOptions, |
| 105 | + ...(defaultOptions || {}), |
57 | 106 | ...(getArgValue(1) || {}),
|
58 | 107 | format: "js:cjs",
|
59 |
| - }; |
60 |
| - |
61 |
| - // Compile |
62 |
| - let compiled: string | undefined; |
63 |
| - try { |
64 |
| - compiled = Compiler.compile(file, options); |
65 |
| - } catch (error) { |
66 |
| - throw macroError(callIndex, `${error.name}: ${error.message}`); |
67 |
| - } |
| 108 | + } as ICompilerOptions; |
68 | 109 |
|
69 |
| - // Get the compiled type suite as AST node |
70 |
| - const parsed = parse(compiled)!; |
71 |
| - if (parsed.type !== "File") throw macroInternalError(); |
72 |
| - if (parsed.program.body[1].type !== "ExpressionStatement") |
73 |
| - throw macroInternalError(); |
74 |
| - if (parsed.program.body[1].expression.type !== "AssignmentExpression") |
75 |
| - throw macroInternalError(); |
76 |
| - const typeSuiteNode = parsed.program.body[1].expression.right; |
77 |
| - |
78 |
| - // Build checker suite expression using type suite |
79 |
| - const checkerSuiteNode = babel.types.callExpression( |
80 |
| - babel.types.memberExpression( |
81 |
| - babel.types.identifier("t"), |
82 |
| - babel.types.identifier("createCheckers") |
83 |
| - ), |
84 |
| - [typeSuiteNode] |
85 |
| - ); |
86 |
| - |
87 |
| - // Replace call with checker suite expression |
88 |
| - parentPath.replaceWith(checkerSuiteNode); |
89 |
| - }); |
| 110 | + return [file, options]; |
| 111 | + } |
90 | 112 |
|
91 |
| - function parse(code: string) { |
92 |
| - return babel.parse(code, { configFile: false }); |
| 113 | + function scopeRenameRecursive(scope: NodePath["scope"], oldName: string) { |
| 114 | + scope.rename(oldName); |
| 115 | + if (scope.parent) { |
| 116 | + scopeRenameRecursive(scope.parent, oldName); |
| 117 | + } |
93 | 118 | }
|
94 | 119 |
|
95 |
| - function getGetArgValue( |
96 |
| - callIndex: number, |
97 |
| - callExpressionPath: babel.NodePath |
98 |
| - ) { |
99 |
| - const argPaths = callExpressionPath.get("arguments"); |
100 |
| - if (!Array.isArray(argPaths)) throw macroInternalError(); |
101 |
| - return (argIndex: number): any => { |
102 |
| - const argPath = argPaths[argIndex]; |
103 |
| - if (!argPath) { |
104 |
| - return null; |
105 |
| - } |
106 |
| - const { confident, value } = argPath.evaluate(); |
107 |
| - if (!confident) { |
108 |
| - /** |
109 |
| - * TODO: Could not get following line to work: |
110 |
| - * const lineSuffix = argPath.node.loc ? ` on line ${argPath.node.loc.start.line}` : "" |
111 |
| - * Line number displayed is for the intermediary js produced by typescript. |
112 |
| - * Even with `inputSourceMap: true`, Babel doesn't seem to parse inline sourcemaps in input. |
113 |
| - * Maybe babel-plugin-macros doesn't support "input -> TS -> babel -> output" pipeline? |
114 |
| - * Or maybe I'm doing that pipeline wrong? |
115 |
| - */ |
116 |
| - throw macroError( |
117 |
| - callIndex, |
118 |
| - `Unable to evaluate argument ${argIndex + 1}` |
119 |
| - ); |
120 |
| - } |
121 |
| - return value; |
122 |
| - }; |
| 120 | + function parseStatements(code: string) { |
| 121 | + const parsed = babel.parse(code, { configFile: false }); |
| 122 | + if (!parsed || parsed.type !== "File") throw macroInternalError(); |
| 123 | + return parsed.program.body; |
123 | 124 | }
|
124 |
| -}; |
125 | 125 |
|
126 |
| -function macroError(callIndex: number, message: string): MacroError { |
127 |
| - return new MacroError( |
128 |
| - `ts-interface-builder/macro: makeCheckers call ${callIndex + 1}: ${message}` |
129 |
| - ); |
130 |
| -} |
131 |
| - |
132 |
| -function macroInternalError(message?: string): MacroError { |
133 |
| - return new MacroError( |
134 |
| - `ts-interface-builder/macro: Internal Error: ${ |
135 |
| - message || "Check stack trace" |
136 |
| - }` |
137 |
| - ); |
138 |
| -} |
| 126 | + function prependProgramStatement(statement: types.Statement) { |
| 127 | + (programPath.get("body.0") as NodePath).insertBefore(statement); |
| 128 | + } |
| 129 | +}; |
139 | 130 |
|
140 | 131 | const macroParams = { configName: "ts-interface-builder" };
|
141 | 132 |
|
|
0 commit comments