-
Notifications
You must be signed in to change notification settings - Fork 30
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 5 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,142 @@ | ||
import * as path from "path"; | ||
import { MacroHandler, MacroError, createMacro } from "babel-plugin-macros"; | ||
import { Compiler, ICompilerOptions } from "./index"; | ||
|
||
const macroHandler: MacroHandler = (params) => { | ||
const callPaths = params.references["makeCheckers"]; | ||
|
||
// Bail out if no calls in this file | ||
if (!callPaths || !callPaths.length) { | ||
return; | ||
} | ||
|
||
const { | ||
babel, | ||
state: { filename }, | ||
} = params; | ||
|
||
// Rename any bindings to `t` in any parent scope of any call | ||
zenflow marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
for (const callPath of callPaths) { | ||
let scope = callPath.scope; | ||
while (true) { | ||
if (scope.hasBinding("t")) { | ||
scope.rename("t"); | ||
} | ||
if (!scope.parent || scope.parent === scope) { | ||
break; | ||
} | ||
scope = scope.parent; | ||
} | ||
} | ||
|
||
// Add `import * as t from 'ts-interface-checker'` statement | ||
const firstStatementPath = callPaths[0] | ||
.findParent((path) => path.isProgram()) | ||
.get("body.0") as babel.NodePath; | ||
firstStatementPath.insertBefore( | ||
babel.types.importDeclaration( | ||
[babel.types.importNamespaceSpecifier(babel.types.identifier("t"))], | ||
babel.types.stringLiteral("ts-interface-checker") | ||
) | ||
); | ||
|
||
// 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 || {}) as ICompilerOptions; | ||
|
||
callPaths.forEach(({ parentPath }, callIndex) => { | ||
// Determine compiler parameters | ||
const getArgValue = getGetArgValue(callIndex, parentPath); | ||
const file = path.resolve( | ||
filename, | ||
"..", | ||
getArgValue(0) || path.basename(filename) | ||
); | ||
const options = { | ||
...defaultOptions, | ||
...(getArgValue(1) || {}), | ||
format: "js:cjs", | ||
}; | ||
|
||
// Compile | ||
let compiled: string | undefined; | ||
try { | ||
compiled = Compiler.compile(file, options); | ||
} catch (error) { | ||
throw macroError(callIndex, `${error.name}: ${error.message}`); | ||
} | ||
|
||
// Get the compiled type suite as AST node | ||
const parsed = parse(compiled)!; | ||
if (parsed.type !== "File") throw macroInternalError(); | ||
if (parsed.program.body[1].type !== "ExpressionStatement") | ||
throw macroInternalError(); | ||
if (parsed.program.body[1].expression.type !== "AssignmentExpression") | ||
throw macroInternalError(); | ||
const typeSuiteNode = parsed.program.body[1].expression.right; | ||
|
||
// Build checker suite expression using type suite | ||
const checkerSuiteNode = babel.types.callExpression( | ||
babel.types.memberExpression( | ||
babel.types.identifier("t"), | ||
babel.types.identifier("createCheckers") | ||
), | ||
[typeSuiteNode] | ||
); | ||
|
||
// Replace call with checker suite expression | ||
parentPath.replaceWith(checkerSuiteNode); | ||
}); | ||
|
||
function parse(code: string) { | ||
return babel.parse(code, { configFile: false }); | ||
} | ||
|
||
function getGetArgValue( | ||
callIndex: number, | ||
callExpressionPath: babel.NodePath | ||
) { | ||
const argPaths = callExpressionPath.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( | ||
callIndex, | ||
`Unable to evaluate argument ${argIndex + 1}` | ||
); | ||
} | ||
return value; | ||
}; | ||
} | ||
}; | ||
|
||
function macroError(callIndex: number, message: string): MacroError { | ||
return new MacroError( | ||
`ts-interface-builder/macro: makeCheckers call ${callIndex + 1}: ${message}` | ||
); | ||
} | ||
|
||
function macroInternalError(message?: string): MacroError { | ||
return new MacroError( | ||
`ts-interface-builder/macro: Internal Error: ${ | ||
message || "Check stack trace" | ||
}` | ||
); | ||
} | ||
|
||
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,3 @@ | ||
import { ICompilerOptions } from "." | ||
import { ICheckerSuite } from "ts-interface-checker" | ||
export declare function makeCheckers (modulePath?: string, options?: ICompilerOptions): ICheckerSuite | ||
zenflow marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
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 {makeCheckers} from "../../macro"; | ||
|
||
makeCheckers("./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 {makeCheckers} from "../../macro"; | ||
|
||
makeCheckers(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,20 @@ | ||
import * as t from "ts-interface-checker"; | ||
export function checkLocalInterface(input) { | ||
// shows that t is renamed | ||
var _t = t.createCheckers({ | ||
LocalInterface: t.iface([], { | ||
"foo": "number" | ||
}) | ||
}); | ||
|
||
_t.LocalInterface.check(input); | ||
|
||
return input; | ||
} | ||
|
||
function _t2(t) { | ||
zenflow marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
// shows function t is renamed and argument t is not | ||
return t; | ||
} | ||
|
||
void _t2; |
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,19 @@ | ||
import {makeCheckers} from "../../macro"; | ||
|
||
interface LocalInterface { | ||
foo: number; | ||
} | ||
|
||
export function checkLocalInterface(input: any): LocalInterface { | ||
// shows that t is renamed | ||
const t = makeCheckers(undefined, {inlineImports: false}); | ||
t.LocalInterface.check(input); | ||
return input as LocalInterface; | ||
} | ||
|
||
function t(t: any) { | ||
// shows function t is renamed and argument t is not | ||
return t; | ||
} | ||
|
||
void t |
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,24 @@ | ||
import * as t from "ts-interface-checker"; | ||
// Note: default options defined in babel plugin options in ../test_macro.ts | ||
var dir = '.'; | ||
var file = dir + "/imports-parent.ts"; | ||
export var checkersUsingDefaultOptions = t.createCheckers({ | ||
TypeA: t.iface([], {}), | ||
TypeB: t.iface([], {}), | ||
TypeC: t.iface([], {}), | ||
TypeD: t.iface([], {}), | ||
TypeAll: t.iface([], { | ||
"a": "TypeA", | ||
"b": "TypeB", | ||
"c": "TypeC", | ||
"d": "TypeD" | ||
}) | ||
}); | ||
export var checkersUsingInlineOptions = t.createCheckers({ | ||
TypeAll: t.iface([], { | ||
"a": "TypeA", | ||
"b": "TypeB", | ||
"c": "TypeC", | ||
"d": "TypeD" | ||
}) | ||
}); |
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 {makeCheckers} from "../../macro"; | ||
// Note: default options defined in babel plugin options in ../test_macro.ts | ||
|
||
const dir = '.' | ||
const file = `${dir}/imports-parent.ts` | ||
|
||
export const checkersUsingDefaultOptions = makeCheckers(file); | ||
|
||
export const checkersUsingInlineOptions = makeCheckers(file, {inlineImports: false}); |
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,64 @@ | ||
import * as assert from "assert"; | ||
import {readFile} from "fs-extra"; | ||
import {join} from "path"; | ||
import * as ts from "typescript"; | ||
import * as babel from "@babel/core"; | ||
import * as macroPlugin from "babel-plugin-macros"; | ||
|
||
const fixtures = join(__dirname, "fixtures"); | ||
|
||
describe("ts-interface-builder/macro", () => { | ||
it("locals", async function () { | ||
this.timeout(5000) | ||
const file = join(fixtures, "macro-locals.ts"); | ||
const output = babelCompile(tsCompile(await readFile(file, {encoding: "utf8"})), file); | ||
const expected = (await readFile(join(fixtures, "macro-locals.js"), { encoding: "utf8" })).trim(); | ||
assert.equal(output, expected); | ||
}); | ||
it("options", async function () { | ||
this.timeout(5000) | ||
const file = join(fixtures, "macro-options.ts"); | ||
const output = babelCompile(tsCompile(await readFile(file, {encoding: "utf8"})), file); | ||
const expected = (await readFile(join(fixtures, "macro-options.js"), { encoding: "utf8" })).trim(); | ||
assert.equal(output, expected); | ||
}); | ||
it("error evaluating arguments", async function () { | ||
const file = join(fixtures, "macro-error-evaluating-arguments.ts"); | ||
const tsOutput = tsCompile(await readFile(file, {encoding: "utf8"})); | ||
assert.throws(() => babelCompile(tsOutput, file), { | ||
name: "MacroError", | ||
message: `${file}: ts-interface-builder/macro: makeCheckers call 1: Unable to evaluate argument 1`, | ||
} as any); | ||
}); | ||
it("error compiling", async function () { | ||
this.timeout(5000) | ||
const file = join(fixtures, "macro-error-compiling.ts"); | ||
const tsOutput = tsCompile(await readFile(file, {encoding: "utf8"})); | ||
assert.throws(() => babelCompile(tsOutput, file), { | ||
name: "MacroError", | ||
message: `${file}: ts-interface-builder/macro: makeCheckers call 1: Error: Node IndexSignature not supported by ts-interface-builder: [extra: string]: any;`, | ||
} as any); | ||
}); | ||
}); | ||
|
||
function tsCompile (code: string): string { | ||
const compilerOptions: ts.CompilerOptions = { | ||
module: ts.ModuleKind.ES2015, | ||
inlineSourceMap: true, | ||
}; | ||
const {outputText, diagnostics} = ts.transpileModule(code, {compilerOptions}); | ||
if (diagnostics && diagnostics.length) { | ||
throw new Error('Got diagnostic errors: '+ JSON.stringify(diagnostics, null, 2)); | ||
} | ||
return outputText; | ||
} | ||
|
||
function babelCompile (code: string, filename: string): string { | ||
return babel.transform(code, { | ||
babelrc: false, | ||
plugins: [[macroPlugin, {"ts-interface-builder": {inlineImports: true}}]], | ||
filename, | ||
// Note: Type definitions for @babel/core's TransformOptions.inputSourceMap is wrong; see https://babeljs.io/docs/en/options#source-map-options | ||
inputSourceMap: true as any, | ||
})!.code!; | ||
} |
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 |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
"declaration": true | ||
}, | ||
"files": [ | ||
"lib/index.ts" | ||
"lib/index.ts", | ||
"lib/macro.ts" | ||
] | ||
} |
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.