Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions lib/macro.ts
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
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);
3 changes: 3 additions & 0 deletions macro.d.ts
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
1 change: 1 addition & 0 deletions macro.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require("./dist/macro.js").macro()
17 changes: 14 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -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",
Expand All @@ -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"
}
}
3 changes: 3 additions & 0 deletions test/fixtures/macro-error-compiling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {makeCheckers} from "../../macro";

makeCheckers("./ignore-index-signature.ts");
4 changes: 4 additions & 0 deletions test/fixtures/macro-error-evaluating-arguments.ts
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'));
20 changes: 20 additions & 0 deletions test/fixtures/macro-locals.js
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) {
// shows function t is renamed and argument t is not
return t;
}

void _t2;
19 changes: 19 additions & 0 deletions test/fixtures/macro-locals.ts
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
24 changes: 24 additions & 0 deletions test/fixtures/macro-options.js
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"
})
});
9 changes: 9 additions & 0 deletions test/fixtures/macro-options.ts
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});
64 changes: 64 additions & 0 deletions test/test_macro.ts
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!;
}
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"declaration": true
},
"files": [
"lib/index.ts"
"lib/index.ts",
"lib/macro.ts"
]
}