Skip to content

Commit 03aa134

Browse files
committed
chore: Introduce CTHelpers in ts-transformers serving imported symbols.
As a part of the "pretransform" step, we replace the CTS enable directive with a wildcard import for "commontools" module that is used when injecting new functionality via transformations.
1 parent 92506df commit 03aa134

File tree

72 files changed

+686
-820
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+686
-820
lines changed

packages/runner/src/harness/engine.ts

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as RuntimeModules from "./runtime-modules.ts";
2222
import { IRuntime } from "../runtime.ts";
2323
import * as merkleReference from "merkle-reference";
2424
import { StaticCache } from "@commontools/static";
25+
import { pretransformProgram } from "./pretransform.ts";
2526

2627
const RUNTIME_ENGINE_CONSOLE_HOOK = "RUNTIME_ENGINE_CONSOLE_HOOK";
2728
const INJECTED_SCRIPT =
@@ -147,7 +148,7 @@ export class Engine extends EventTarget implements Harness {
147148
> {
148149
const id = options.identifier ?? computeId(program);
149150
const filename = options.filename ?? `${id}.js`;
150-
const mappedProgram = mapPrefixProgramFiles(program, id);
151+
const mappedProgram = pretransformProgram(program, id);
151152
const resolver = new EngineProgramResolver(
152153
mappedProgram,
153154
this.ctRuntime.staticCache,
@@ -250,32 +251,3 @@ function computeId(program: Program): string {
250251
];
251252
return merkleReference.refer(source).toString();
252253
}
253-
254-
// Adds `id` as a prefix to all files in the program.
255-
// Injects a new entry at root `/index.ts` to re-export
256-
// the entry contents because otherwise `typescript`
257-
// flattens the output, eliding the common prefix.
258-
function mapPrefixProgramFiles(program: RuntimeProgram, id: string): Program {
259-
const main = program.main;
260-
const exportNameds = `export * from "${prefix(main, id)}";`;
261-
const exportDefault = `export { default } from "${prefix(main, id)}";`;
262-
const hasDefault = !program.mainExport || program.mainExport === "default";
263-
const files = [
264-
...program.files.map((source) => ({
265-
name: prefix(source.name, id),
266-
contents: source.contents,
267-
})),
268-
{
269-
name: `/index.ts`,
270-
contents: `${exportNameds}${hasDefault ? `\n${exportDefault}` : ""}`,
271-
},
272-
];
273-
return {
274-
main: `/index.ts`,
275-
files,
276-
};
277-
}
278-
279-
function prefix(filename: string, id: string): string {
280-
return `/${id}${filename}`;
281-
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { transformCtDirective } from "@commontools/ts-transformers";
2+
import { RuntimeProgram } from "./types.ts";
3+
4+
export function pretransformProgram(
5+
program: RuntimeProgram,
6+
id: string,
7+
): RuntimeProgram {
8+
program = transformInjectHelperModule(program);
9+
program = transformProgramWithPrefix(program, id);
10+
return program;
11+
}
12+
13+
// For each source file in the program, replace
14+
// a `/// <cts-enable />` directive line with an
15+
// internal import statement for use by the AST transformer
16+
// to provide access to helpers like `derive`, etc.
17+
export function transformInjectHelperModule(
18+
program: RuntimeProgram,
19+
): RuntimeProgram {
20+
return {
21+
main: program.main,
22+
files: program.files.map((source) => ({
23+
name: source.name,
24+
contents: transformCtDirective(source.contents),
25+
})),
26+
mainExport: program.mainExport,
27+
};
28+
}
29+
30+
// Adds `id` as a prefix to all files in the program.
31+
// Injects a new entry at root `/index.ts` to re-export
32+
// the entry contents because otherwise `typescript`
33+
// flattens the output, eliding the common prefix.
34+
export function transformProgramWithPrefix(
35+
program: RuntimeProgram,
36+
id: string,
37+
): RuntimeProgram {
38+
const main = program.main;
39+
const exportNameds = `export * from "${prefix(main, id)}";`;
40+
const exportDefault = `export { default } from "${prefix(main, id)}";`;
41+
const hasDefault = !program.mainExport || program.mainExport === "default";
42+
const files = [
43+
...program.files.map((source) => ({
44+
name: prefix(source.name, id),
45+
contents: source.contents,
46+
})),
47+
{
48+
name: `/index.ts`,
49+
contents: `${exportNameds}${hasDefault ? `\n${exportDefault}` : ""}`,
50+
},
51+
];
52+
return {
53+
main: `/index.ts`,
54+
files,
55+
};
56+
}
57+
58+
function prefix(filename: string, id: string): string {
59+
return `/${id}${filename}`;
60+
}

packages/ts-transformers/src/core/context.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import ts from "typescript";
2-
import { ImportRequirements } from "./imports.ts";
32
import {
43
DiagnosticInput,
54
TransformationDiagnostic,
65
TransformationOptions,
76
} from "./transformers.ts";
7+
import { CTHelpers } from "./ct-helpers.ts";
88

99
const DEFAULT_OPTIONS: TransformationOptions = {
1010
mode: "transform",
@@ -16,7 +16,6 @@ export interface TransformationContextConfig {
1616
sourceFile: ts.SourceFile;
1717
tsContext: ts.TransformationContext;
1818
options?: TransformationOptions;
19-
imports?: ImportRequirements;
2019
}
2120

2221
export class TransformationContext {
@@ -25,7 +24,7 @@ export class TransformationContext {
2524
readonly factory: ts.NodeFactory;
2625
readonly sourceFile: ts.SourceFile;
2726
readonly options: TransformationOptions;
28-
readonly imports: ImportRequirements;
27+
readonly ctHelpers: CTHelpers;
2928
readonly diagnostics: TransformationDiagnostic[] = [];
3029
readonly tsContext: ts.TransformationContext;
3130
#typeCache = new Map<ts.Node, ts.Type>();
@@ -36,7 +35,10 @@ export class TransformationContext {
3635
this.tsContext = config.tsContext;
3736
this.factory = config.tsContext.factory;
3837
this.sourceFile = config.sourceFile;
39-
this.imports = config.imports ?? new ImportRequirements();
38+
this.ctHelpers = new CTHelpers({
39+
factory: this.factory,
40+
sourceFile: this.sourceFile,
41+
});
4042
this.options = {
4143
...DEFAULT_OPTIONS,
4244
...config.options,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import ts from "typescript";
2+
import { TransformationContext } from "./mod.ts";
3+
4+
export const CT_HELPERS_IDENTIFIER = "__ctHelpers";
5+
6+
const CT_HELPERS_SPECIFIER = "commontools";
7+
8+
const HELPERS_STMT =
9+
`import * as ${CT_HELPERS_IDENTIFIER} from "${CT_HELPERS_SPECIFIER}";`;
10+
11+
const HELPERS_USED_STMT = `${CT_HELPERS_IDENTIFIER}.NAME; // <internals>`;
12+
13+
export class CTHelpers {
14+
#sourceFile: ts.SourceFile;
15+
#factory: ts.NodeFactory;
16+
#helperIdent?: ts.Identifier;
17+
18+
constructor(params: Pick<TransformationContext, "sourceFile" | "factory">) {
19+
this.#sourceFile = params.sourceFile;
20+
this.#factory = params.factory;
21+
22+
for (const stmt of this.#sourceFile.statements) {
23+
const symbol = getCTHelpersIdentifier(stmt);
24+
if (symbol) {
25+
this.#helperIdent = symbol;
26+
break;
27+
}
28+
}
29+
}
30+
31+
sourceHasHelpers(): boolean {
32+
return !!this.#helperIdent;
33+
}
34+
35+
// Returns an PropertyAccessExpression of the requested
36+
// helper name e.g. `(__ctHelpers.derive)`.
37+
getHelperExpr(
38+
name: string,
39+
): ts.PropertyAccessExpression {
40+
if (!this.sourceHasHelpers()) {
41+
throw new Error("Source file does not contain helpers.");
42+
}
43+
return this.#factory.createPropertyAccessExpression(
44+
this.#helperIdent!,
45+
name,
46+
);
47+
}
48+
49+
// Returns an QualifiedName of the requested
50+
// helper name e.g. `__ctHelpers.JSONSchema`.
51+
getHelperQualified(
52+
name: string,
53+
): ts.QualifiedName {
54+
if (!this.sourceHasHelpers()) {
55+
throw new Error("Source file does not contain helpers.");
56+
}
57+
return this.#factory.createQualifiedName(
58+
this.#helperIdent!,
59+
name,
60+
);
61+
}
62+
}
63+
64+
// Replace a `/// <cts-enable />` directive line with an
65+
// internal import statement for use by the AST transformer
66+
// to provide access to helpers like `derive`, etc.
67+
// This operates on strings, and to be used outside of
68+
// the TypeScript transformer pipeline, since symbol binding
69+
// occurs before transformers run.
70+
//
71+
// We must also inject usage of the module before the AST transformer
72+
// pipeline, otherwise the binding fails, and the helper module
73+
// is not available in the compiled JS.
74+
//
75+
// Source maps are derived from this transformation.
76+
// Take care in maintaining source lines from its input.
77+
//
78+
// This injected statement enables subsequent transformations.
79+
export function transformCtDirective(
80+
source: string,
81+
): string {
82+
// Throw when this symbol name is already in use
83+
// for some reason.
84+
if (source.indexOf(CT_HELPERS_IDENTIFIER) !== -1) {
85+
throw new Error(
86+
`Source cannot contain reserved '${CT_HELPERS_IDENTIFIER}' symbol.`,
87+
);
88+
}
89+
const lines = source.split("\n");
90+
if (!lines[0] || !isCTSEnabled(lines[0])) {
91+
return source;
92+
}
93+
return [
94+
HELPERS_STMT,
95+
...lines.slice(1),
96+
HELPERS_USED_STMT,
97+
].join("\n");
98+
}
99+
100+
function isCTSEnabled(line: string) {
101+
return /^\/\/\/\s*<cts-enable\s*\/>/m.test(line);
102+
}
103+
104+
function getCTHelpersIdentifier(
105+
statement: ts.Statement,
106+
): ts.Identifier | undefined {
107+
if (!ts.isImportDeclaration(statement)) return;
108+
const { importClause, moduleSpecifier } = statement;
109+
110+
// Check specifier is "commontools"
111+
if (!ts.isStringLiteral(moduleSpecifier)) return;
112+
if (moduleSpecifier.text !== CT_HELPERS_SPECIFIER) return;
113+
114+
// Check it is a namespace import `* as __ctHelpers`
115+
if (!importClause || !ts.isImportClause(importClause)) return;
116+
const { namedBindings } = importClause;
117+
if (!namedBindings || !ts.isNamespaceImport(namedBindings)) return;
118+
if (namedBindings.name.getText() !== CT_HELPERS_IDENTIFIER) return;
119+
return namedBindings.name;
120+
}

packages/ts-transformers/src/core/cts-directive.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)