-
Notifications
You must be signed in to change notification settings - Fork 28
Add babel macro #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add babel macro #24
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
86cb1d6
Add babel macro, closes #22
zenflow c5a22c1
Add "test-only" package script
zenflow 470a556
In macro tests, use `assert.throws()` instead of local `getError()`
zenflow 95b43eb
Improve error reporting and avoid using `ordinal`
zenflow ed8633d
Move macro to separate entrypoint & move babel-plugin-macros to dev deps
zenflow 561c7d9
Lots of improvements
zenflow 113aa5e
Improve inline documentation
zenflow 204650d
Fix processing of js:cjs format output
zenflow f8bd4a1
Add comments about processing of js:cjs format output
zenflow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
// @ts-ignore | ||
import * as path from "path"; | ||
import { createMacro, MacroHandler } from "babel-plugin-macros"; | ||
import { NodePath, types } from "@babel/core"; // typescript types ONLY | ||
import { ICompilerOptions } from "./index"; | ||
import { getCallPaths } from "./macro/getCallPaths"; | ||
import { RequirementRegistry } from "./macro/RequirementRegistry"; | ||
import { getGetArgValue } from "./macro/getGetArgValue"; | ||
import { compileTypeSuite, ICompilerArgs } from "./macro/compileTypeSuite"; | ||
import { macroInternalError } from "./macro/errors"; | ||
|
||
const tsInterfaceCheckerIdentifier = "t"; | ||
const onceIdentifier = "once"; | ||
|
||
/** | ||
* This macro handler is called for each file that imports the macro module. | ||
* `params.references` is an object where each key is the name of a variable imported from the macro module, | ||
* and each value is an array of references to that that variable. | ||
* Said references come in the form of Babel `NodePath`s, | ||
* which have AST (Abstract Syntax Tree) data and methods for manipulating it. | ||
* For more info: https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/author.md#function-api | ||
* | ||
* This macro handler needs to replace each call to `getTypeSuite` or `getCheckers` | ||
* with the code that fulfills that function's behavior as documented in `macro.d.ts` (in root of repo). | ||
*/ | ||
const macroHandler: MacroHandler = (params) => { | ||
const { references, babel, state } = params; | ||
const callPaths = getCallPaths(references); | ||
const somePath = callPaths.getTypeSuite[0] || callPaths.getCheckers[0]; | ||
if (!somePath) { | ||
return; | ||
} | ||
const programPath = somePath.findParent((path) => path.isProgram()); | ||
|
||
const registry = new RequirementRegistry(); | ||
const toReplace = [ | ||
...callPaths.getTypeSuite.map((callPath, index) => { | ||
const compilerArgs = getCompilerArgs(callPath, "getTypeSuite", index); | ||
const typeSuiteId = registry.requireTypeSuite(compilerArgs); | ||
return { callPath, id: typeSuiteId }; | ||
}), | ||
...callPaths.getCheckers.map((callPath, index) => { | ||
const compilerArgs = getCompilerArgs(callPath, "getCheckers", index); | ||
const checkerSuiteId = registry.requireCheckerSuite(compilerArgs); | ||
return { callPath, id: checkerSuiteId }; | ||
}), | ||
]; | ||
|
||
// Begin mutations | ||
|
||
programPath.scope.rename(tsInterfaceCheckerIdentifier); | ||
programPath.scope.rename(onceIdentifier); | ||
toReplace.forEach(({ callPath, id }) => { | ||
scopeRenameRecursive(callPath.scope, id); | ||
}); | ||
|
||
const toPrepend = ` | ||
import * as ${tsInterfaceCheckerIdentifier} from "ts-interface-checker"; | ||
function ${onceIdentifier}(fn) { | ||
var result; | ||
return function () { | ||
return result || (result = fn()); | ||
}; | ||
} | ||
${registry.typeSuites | ||
.map( | ||
({ compilerArgs, id }) => ` | ||
var ${id} = ${onceIdentifier}(function(){ | ||
return ${compileTypeSuite(compilerArgs)}; | ||
}); | ||
` | ||
) | ||
.join("")} | ||
${registry.checkerSuites | ||
.map( | ||
({ typeSuiteId, id }) => ` | ||
var ${id} = ${onceIdentifier}(function(){ | ||
return ${tsInterfaceCheckerIdentifier}.createCheckers(${typeSuiteId}()); | ||
}); | ||
` | ||
) | ||
.join("")} | ||
`; | ||
parseStatements(toPrepend).reverse().forEach(prependProgramStatement); | ||
|
||
const { identifier, callExpression } = babel.types; | ||
toReplace.forEach(({ callPath, id }) => { | ||
callPath.replaceWith(callExpression(identifier(id), [])); | ||
}); | ||
|
||
// Done mutations (only helper functions below) | ||
|
||
function getCompilerArgs( | ||
callPath: NodePath<types.CallExpression>, | ||
functionName: string, | ||
callIndex: number | ||
): ICompilerArgs { | ||
const callDescription = `${functionName} call ${callIndex + 1}`; | ||
const getArgValue = getGetArgValue(callPath, callDescription); | ||
|
||
const basename = getArgValue(0) || path.basename(state.filename); | ||
const file = path.resolve(state.filename, "..", basename); | ||
|
||
// Get the user config passed to us by babel-plugin-macros, for use as default options | ||
// Note: `config` property is missing in `babelPluginMacros.MacroParams` type definition | ||
const defaultOptions = (params as any).config; | ||
const options = { | ||
...(defaultOptions || {}), | ||
...(getArgValue(1) || {}), | ||
format: "js:cjs", | ||
} as ICompilerOptions; | ||
|
||
return [file, options]; | ||
} | ||
|
||
function scopeRenameRecursive(scope: NodePath["scope"], oldName: string) { | ||
scope.rename(oldName); | ||
if (scope.parent) { | ||
scopeRenameRecursive(scope.parent, oldName); | ||
} | ||
} | ||
|
||
function parseStatements(code: string) { | ||
const parsed = babel.parse(code, { configFile: false }); | ||
if (!parsed || parsed.type !== "File") throw macroInternalError(); | ||
return parsed.program.body; | ||
} | ||
|
||
function prependProgramStatement(statement: types.Statement) { | ||
(programPath.get("body.0") as NodePath).insertBefore(statement); | ||
} | ||
}; | ||
|
||
const macroParams = { configName: "ts-interface-builder" }; | ||
|
||
export const macro = () => createMacro(macroHandler, macroParams); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// @ts-ignore | ||
import { isDeepStrictEqual } from "util"; | ||
import { ICompilerArgs } from "./compileTypeSuite"; | ||
|
||
export interface IRequiredTypeSuite { | ||
compilerArgs: ICompilerArgs; | ||
id: string; | ||
} | ||
|
||
export interface IRequiredCheckerSuite { | ||
typeSuiteId: string; | ||
id: string; | ||
} | ||
|
||
export class RequirementRegistry { | ||
public typeSuites: IRequiredTypeSuite[] = []; | ||
public checkerSuites: IRequiredCheckerSuite[] = []; | ||
|
||
public requireTypeSuite(compilerArgs: ICompilerArgs): string { | ||
let index = this.typeSuites.findIndex((typeSuite) => | ||
isDeepStrictEqual(typeSuite.compilerArgs, compilerArgs) | ||
); | ||
if (index === -1) { | ||
index = this.typeSuites.length; | ||
this.typeSuites.push({ | ||
compilerArgs, | ||
id: `typeSuite${index}`, | ||
}); | ||
} | ||
return this.typeSuites[index].id; | ||
} | ||
|
||
public requireCheckerSuite(compilerArgs: ICompilerArgs): string { | ||
const typeSuiteId = this.requireTypeSuite(compilerArgs); | ||
let index = this.checkerSuites.findIndex( | ||
(checkerSuite) => checkerSuite.typeSuiteId === typeSuiteId | ||
); | ||
if (index === -1) { | ||
index = this.checkerSuites.length; | ||
this.checkerSuites.push({ | ||
typeSuiteId, | ||
id: `checkerSuite${index}`, | ||
}); | ||
} | ||
return this.checkerSuites[index].id; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { Compiler, ICompilerOptions } from "../index"; | ||
import { macroError, macroInternalError } from "./errors"; | ||
|
||
export type ICompilerArgs = [string, ICompilerOptions]; | ||
|
||
export function compileTypeSuite(args: ICompilerArgs): string { | ||
let compiled: string | undefined; | ||
const [file, options] = args; | ||
const optionsString = JSON.stringify(options); | ||
const context = `compiling file ${file} with options ${optionsString}`; | ||
try { | ||
compiled = Compiler.compile(file, options); | ||
} catch (error) { | ||
throw macroError(`Error ${context}: ${error.name}: ${error.message}`); | ||
} | ||
/* | ||
Here we have `compiled` in "js:cjs" format. | ||
From this string we need to extract the type suite expression that is exported. | ||
The format is expected to have only two statements: | ||
1. a cjs-style import statement which defines `t`, e.g. `const t = require("ts-interface-checker")` | ||
2. beginning on 3rd line, a cjs-style export statement that starts with `module.exports = ` and ends with `;\n` | ||
*/ | ||
const exportStatement = compiled.split("\n").slice(2).join("\n"); | ||
dsagal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const prefix = "module.exports = "; | ||
const postfix = ";\n"; | ||
if ( | ||
!exportStatement.startsWith(prefix) || | ||
!exportStatement.endsWith(postfix) | ||
) { | ||
throw macroInternalError( | ||
`Unexpected output format from Compiler (${context})` | ||
); | ||
} | ||
return exportStatement.slice(prefix.length, -postfix.length); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import {MacroError} from "babel-plugin-macros"; | ||
|
||
export function macroError(message: string): MacroError { | ||
return new MacroError(`ts-interface-builder/macro: ${message}`); | ||
} | ||
|
||
export function macroInternalError(message?: string): MacroError { | ||
return macroError(`Internal Error: ${message || "Check stack trace"}`); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { References } from "babel-plugin-macros"; | ||
import { NodePath, types } from "@babel/core"; // typescript types ONLY | ||
import { macroError } from "./errors"; | ||
|
||
export function getCallPaths({ | ||
getTypeSuite = [], | ||
getCheckers = [], | ||
...rest | ||
}: References) { | ||
const restKeys = Object.keys(rest); | ||
if (restKeys.length) { | ||
throw macroError( | ||
`Reference(s) to unknown export(s): ${restKeys.join(", ")}` | ||
); | ||
} | ||
const callPaths = { | ||
getTypeSuite: [] as NodePath<types.CallExpression>[], | ||
getCheckers: [] as NodePath<types.CallExpression>[], | ||
}; | ||
getTypeSuite.forEach((path, index) => { | ||
if (!path.parentPath.isCallExpression()) { | ||
throw macroError( | ||
`Reference ${index + 1} to getTypeSuite not used for a call expression` | ||
); | ||
} | ||
callPaths.getTypeSuite.push(path.parentPath); | ||
}); | ||
getCheckers.forEach((path, index) => { | ||
if (!path.parentPath.isCallExpression()) { | ||
throw macroError( | ||
`Reference ${index + 1} to getCheckers not used for a call expression` | ||
); | ||
} | ||
callPaths.getCheckers.push(path.parentPath); | ||
}); | ||
return callPaths; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { NodePath, types } from "@babel/core"; // typescript types ONLY | ||
import { macroError, macroInternalError } from "./errors"; | ||
|
||
export function getGetArgValue( | ||
callPath: NodePath<types.CallExpression>, | ||
callDescription: string | ||
) { | ||
const argPaths = callPath.get("arguments"); | ||
if (!Array.isArray(argPaths)) throw macroInternalError(); | ||
return (argIndex: number): any => { | ||
const argPath = argPaths[argIndex]; | ||
if (!argPath) { | ||
return null; | ||
} | ||
const { confident, value } = argPath.evaluate(); | ||
if (!confident) { | ||
/** | ||
* TODO: Could not get following line to work: | ||
* const lineSuffix = argPath.node.loc ? ` on line ${argPath.node.loc.start.line}` : "" | ||
* Line number displayed is for the intermediary js produced by typescript. | ||
* Even with `inputSourceMap: true`, Babel doesn't seem to parse inline sourcemaps in input. | ||
* Maybe babel-plugin-macros doesn't support "input -> TS -> babel -> output" pipeline? | ||
* Or maybe I'm doing that pipeline wrong? | ||
*/ | ||
throw macroError( | ||
`Unable to evaluate argument ${argIndex + 1} of ${callDescription}` | ||
); | ||
} | ||
return value; | ||
}; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { ICompilerOptions } from "." | ||
import { ICheckerSuite, ITypeSuite } from "ts-interface-checker" | ||
|
||
/** | ||
* Returns a type suite compiled from the given module with the given compiler options | ||
* @param modulePath - Relative path to the target module (defaults to the module in which the function is called) | ||
* @param options - Compiler options | ||
*/ | ||
export declare function getTypeSuite (modulePath?: string, options?: ICompilerOptions): ITypeSuite | ||
|
||
/** | ||
* Returns a checker suite created from a type suite compiled from the given module with the given compiler options | ||
* @param modulePath - Relative path to the target module (defaults to the module in which the function is called) | ||
* @param options - Compiler options | ||
*/ | ||
export declare function getCheckers (modulePath?: string, options?: ICompilerOptions): ICheckerSuite |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require("./dist/macro.js").macro() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,8 @@ | |
"scripts": { | ||
"build": "tsc", | ||
"watch": "tsc -w", | ||
"test": "tsc && mocha 'test/*.ts'", | ||
"test-only": "mocha 'test/*.ts'", | ||
"test": "npm run build && npm run test-only", | ||
"prepack": "npm run test" | ||
}, | ||
"keywords": [ | ||
|
@@ -20,9 +21,13 @@ | |
"type", | ||
"validate", | ||
"validator", | ||
"check" | ||
"check", | ||
"babel-plugin-macros" | ||
], | ||
"author": "Dmitry S, Grist Labs", | ||
"contributors": [ | ||
"Matthew Francis Brunetti <[email protected]> (https://github.com/zenflow)" | ||
], | ||
"license": "Apache-2.0", | ||
"repository": { | ||
"type": "git", | ||
|
@@ -33,19 +38,25 @@ | |
}, | ||
"files": [ | ||
"dist", | ||
"bin" | ||
"bin", | ||
"macro.js", | ||
"macro.d.ts" | ||
], | ||
"dependencies": { | ||
"commander": "^2.12.2", | ||
"fs-extra": "^4.0.3", | ||
"typescript": "^3.0.0" | ||
}, | ||
"devDependencies": { | ||
"@babel/core": "^7.10.5", | ||
"@types/babel-plugin-macros": "^2.8.2", | ||
"@types/fs-extra": "^4.0.5", | ||
"@types/mocha": "^5.2.7", | ||
"@types/node": "^8.0.57", | ||
"babel-plugin-macros": "^2.8.0", | ||
"fs-extra": "^4.0.3", | ||
"mocha": "^6.2.0", | ||
"ts-interface-checker": "^0.1.12", | ||
"ts-node": "^4.0.1" | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { getCheckers } from "../../macro"; | ||
|
||
getCheckers("./ignore-index-signature.ts"); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { join } from "path"; | ||
import { getCheckers } from "../../macro"; | ||
|
||
getCheckers(join(__dirname, "foo.ts")); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { getCheckers } from "../../macro"; | ||
|
||
const foo = getCheckers; | ||
|
||
foo("foo.ts"); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.