Skip to content

Commit 86cb1d6

Browse files
committed
Add babel macro, closes #22
1 parent 0508a1a commit 86cb1d6

File tree

12 files changed

+308
-2
lines changed

12 files changed

+308
-2
lines changed

lib/index.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import * as commander from "commander";
44
import * as fs from "fs";
55
import * as path from "path";
66
import * 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
911
const 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);

lib/util/ordinal.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export function ordinal (number: number): string {
2+
let result = String(number)
3+
const n = Math.abs(number)
4+
const cent = n % 100
5+
const dec = n % 10
6+
if (cent >= 10 && cent <= 20) {
7+
result += 'th'
8+
} else if (dec === 1) {
9+
result += 'st'
10+
} else if (dec === 2) {
11+
result += 'nd'
12+
} else if (dec === 3) {
13+
result += 'rd'
14+
} else {
15+
result += 'th'
16+
}
17+
return result
18+
}

macro.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ICompilerOptions } from "."
2+
import { ICheckerSuite } from "ts-interface-checker"
3+
export declare function makeCheckers (modulePath?: string, options?: ICompilerOptions): ICheckerSuite

macro.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require("./dist/index.js").macro()

package.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@
2020
"type",
2121
"validate",
2222
"validator",
23-
"check"
23+
"check",
24+
"babel-plugin-macros"
2425
],
2526
"author": "Dmitry S, Grist Labs",
27+
"contributors": [
28+
"Matthew Francis Brunetti <[email protected]> (https://github.com/zenflow)"
29+
],
2630
"license": "Apache-2.0",
2731
"repository": {
2832
"type": "git",
@@ -33,19 +37,25 @@
3337
},
3438
"files": [
3539
"dist",
36-
"bin"
40+
"bin",
41+
"macro.js",
42+
"macro.d.ts"
3743
],
3844
"dependencies": {
45+
"babel-plugin-macros": "^2.8.0",
3946
"commander": "^2.12.2",
4047
"fs-extra": "^4.0.3",
4148
"typescript": "^3.0.0"
4249
},
4350
"devDependencies": {
51+
"@babel/core": "^7.10.5",
52+
"@types/babel-plugin-macros": "^2.8.2",
4453
"@types/fs-extra": "^4.0.5",
4554
"@types/mocha": "^5.2.7",
4655
"@types/node": "^8.0.57",
4756
"fs-extra": "^4.0.3",
4857
"mocha": "^6.2.0",
58+
"ts-interface-checker": "^0.1.12",
4959
"ts-node": "^4.0.1"
5060
}
5161
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {makeCheckers} from "../../macro";
2+
3+
makeCheckers("./ignore-index-signature.ts");
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {join} from "path"
2+
import {makeCheckers} from "../../macro";
3+
4+
makeCheckers(join(__dirname, 'foo.ts'));

test/fixtures/macro-locals.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as t from "ts-interface-checker";
2+
export function checkLocalInterface(input) {
3+
// shows that t is renamed
4+
var _t = t.createCheckers({
5+
LocalInterface: t.iface([], {
6+
"foo": "number"
7+
})
8+
});
9+
10+
_t.LocalInterface.check(input);
11+
12+
return input;
13+
}
14+
15+
function _t2(t) {
16+
// shows function t is renamed and argument t is not
17+
return t;
18+
}
19+
20+
void _t2;

test/fixtures/macro-locals.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {makeCheckers} from "../../macro";
2+
3+
interface LocalInterface {
4+
foo: number;
5+
}
6+
7+
export function checkLocalInterface(input: any): LocalInterface {
8+
// shows that t is renamed
9+
const t = makeCheckers(undefined, {inlineImports: false});
10+
t.LocalInterface.check(input);
11+
return input as LocalInterface;
12+
}
13+
14+
function t(t: any) {
15+
// shows function t is renamed and argument t is not
16+
return t;
17+
}
18+
19+
void t

test/fixtures/macro-options.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as t from "ts-interface-checker";
2+
// Note: default options defined in babel plugin options in ../test_macro.ts
3+
var dir = '.';
4+
var file = dir + "/imports-parent.ts";
5+
export var checkersUsingDefaultOptions = t.createCheckers({
6+
TypeA: t.iface([], {}),
7+
TypeB: t.iface([], {}),
8+
TypeC: t.iface([], {}),
9+
TypeD: t.iface([], {}),
10+
TypeAll: t.iface([], {
11+
"a": "TypeA",
12+
"b": "TypeB",
13+
"c": "TypeC",
14+
"d": "TypeD"
15+
})
16+
});
17+
export var checkersUsingInlineOptions = t.createCheckers({
18+
TypeAll: t.iface([], {
19+
"a": "TypeA",
20+
"b": "TypeB",
21+
"c": "TypeC",
22+
"d": "TypeD"
23+
})
24+
});

0 commit comments

Comments
 (0)