Skip to content

Commit 54a70dc

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 54a70dc

File tree

17 files changed

+263
-415
lines changed

17 files changed

+263
-415
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: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
}
61+
62+
function isCTSEnabled(line: string) {
63+
return /^\/\/\/\s*<cts-enable\s*\/>/m.test(line);
64+
}

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: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
export class CTHelpers {
12+
#sourceFile: ts.SourceFile;
13+
#factory: ts.NodeFactory;
14+
#helperIdent?: ts.Identifier;
15+
16+
constructor(params: Pick<TransformationContext, "sourceFile" | "factory">) {
17+
this.#sourceFile = params.sourceFile;
18+
this.#factory = params.factory;
19+
20+
for (const stmt of this.#sourceFile.statements) {
21+
const symbol = getCTHelpersIdentifier(stmt);
22+
if (symbol) {
23+
this.#helperIdent = symbol;
24+
break;
25+
}
26+
}
27+
}
28+
29+
sourceHasHelpers(): boolean {
30+
return !!this.#helperIdent;
31+
}
32+
33+
// Returns an PropertyAccessExpression of the requested
34+
// helper name e.g. `(__ctHelpers.derive)`.
35+
getHelperExpr(
36+
name: string,
37+
): ts.PropertyAccessExpression {
38+
if (!this.sourceHasHelpers()) {
39+
throw new Error("Source file does not contain helpers.");
40+
}
41+
return this.#factory.createPropertyAccessExpression(
42+
this.#helperIdent!,
43+
name,
44+
);
45+
}
46+
47+
// Returns an QualifiedName of the requested
48+
// helper name e.g. `__ctHelpers.JSONSchema`.
49+
getHelperQualified(
50+
name: string,
51+
): ts.QualifiedName {
52+
if (!this.sourceHasHelpers()) {
53+
throw new Error("Source file does not contain helpers.");
54+
}
55+
return this.#factory.createQualifiedName(
56+
this.#helperIdent!,
57+
name,
58+
);
59+
}
60+
}
61+
62+
// Replace a `/// <cts-enable />` directive line with an
63+
// internal import statement for use by the AST transformer
64+
// to provide access to helpers like `derive`, etc.
65+
// This operates on strings, and to be used outside of
66+
// the TypeScript transformer pipeline, since symbol binding
67+
// occurs before transformers run.
68+
//
69+
// This injected statement enables subsequent transformations.
70+
export function transformCtDirective(
71+
source: string,
72+
): string {
73+
// Throw when this symbol name is already in use
74+
// for some reason.
75+
if (source.indexOf(CT_HELPERS_IDENTIFIER) !== -1) {
76+
throw new Error(
77+
`Source cannot contain reserved '${CT_HELPERS_IDENTIFIER}' symbol.`,
78+
);
79+
}
80+
const lines = source.split("\n");
81+
if (!lines[0] || !isCTSEnabled(lines[0])) {
82+
return source;
83+
}
84+
return [
85+
HELPERS_STMT,
86+
...lines.slice(1),
87+
].join("\n");
88+
}
89+
90+
function isCTSEnabled(line: string) {
91+
return /^\/\/\/\s*<cts-enable\s*\/>/m.test(line);
92+
}
93+
94+
function getCTHelpersIdentifier(
95+
statement: ts.Statement,
96+
): ts.Identifier | undefined {
97+
if (!ts.isImportDeclaration(statement)) return;
98+
const { importClause, moduleSpecifier } = statement;
99+
100+
// Check specifier is "commontools"
101+
if (!ts.isStringLiteral(moduleSpecifier)) return;
102+
if (moduleSpecifier.getText() !== `"${CT_HELPERS_SPECIFIER}"`) return;
103+
104+
// Check it is a namespace import `* as __ctHelpers`
105+
if (!importClause || !ts.isImportClause(importClause)) return;
106+
const { namedBindings } = importClause;
107+
if (!namedBindings || !ts.isNamespaceImport(namedBindings)) return;
108+
if (namedBindings.name.getText() !== CT_HELPERS_IDENTIFIER) return;
109+
return namedBindings.name;
110+
}

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

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

0 commit comments

Comments
 (0)