@prisma-next/ts-render 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # @prisma-next/ts-render
2
+
3
+ > **Internal package.** This package is an implementation detail of [`prisma-next`](https://www.npmjs.com/package/prisma-next)
4
+ > and is published only to support its runtime. Its API is unstable and may change
5
+ > without notice. Do not depend on this package directly; install `prisma-next` instead.
6
+
7
+ TypeScript source-text rendering utilities shared by any Prisma Next component that has to emit hand-editable `.ts` files — today the Postgres and Mongo migration-authoring surfaces, later any new target that ships a migration-authoring experience.
8
+
9
+ ## Overview
10
+
11
+ Two small pieces:
12
+
13
+ - **`TsExpression`** — abstract base class for any node that renders as a TypeScript expression in a generated source file. Subclasses supply `renderTypeScript(): string` and `importRequirements(): readonly ImportRequirement[]`. Hierarchical so that composite nodes (e.g. a `DataTransformCall` containing slot expressions) can recurse into their children.
14
+ - **`jsonToTsSource(value)`** — pure JSON-to-TypeScript-source printer. Accepts `unknown` for ergonomics with structural types whose fields happen to be JSON-compatible, and throws at runtime on anything that is not a JSON primitive / array / object.
15
+
16
+ ## Codec → TS pipeline
17
+
18
+ `jsonToTsSource` is deliberately the **second** stage of a two-stage pipeline:
19
+
20
+ ```text
21
+ jsValue → codec.encodeJson → JsonValue → jsonToTsSource → TS source text
22
+ ```
23
+
24
+ Stage 1 (`codec.encodeJson`) is a codec responsibility — date serialization, opaque domain types (vector, bigint, uuid), JSON canonicalization. Stage 2 (this module) is a pure printer that must never grow type-specific branches. To render a non-JSON JS value (`Date`, `Vector`, `BigInt`, `Buffer`, …), encode it through the relevant codec's `encodeJson` first.
25
+
26
+ ## Usage
27
+
28
+ ```ts
29
+ import { type ImportRequirement, TsExpression, jsonToTsSource } from '@prisma-next/ts-render';
30
+
31
+ class CreateTableCall extends TsExpression {
32
+ constructor(
33
+ readonly schema: string,
34
+ readonly table: string,
35
+ readonly columns: ReadonlyArray<{ name: string; typeSql: string; nullable: boolean }>,
36
+ ) {
37
+ super();
38
+ }
39
+
40
+ override renderTypeScript(): string {
41
+ return `createTable(${jsonToTsSource(this.schema)}, ${jsonToTsSource(this.table)}, ${jsonToTsSource(this.columns)})`;
42
+ }
43
+
44
+ override importRequirements(): readonly ImportRequirement[] {
45
+ return [{ moduleSpecifier: '@prisma-next/target-postgres/migration', symbol: 'createTable' }];
46
+ }
47
+ }
48
+ ```
@@ -0,0 +1,90 @@
1
+ //#region src/json-to-ts-source.d.ts
2
+ /**
3
+ * Pure JSON-to-TypeScript-source printer.
4
+ *
5
+ * This module is the second stage of the codec → TS pipeline:
6
+ *
7
+ * jsValue → codec.encodeJson → JsonValue → jsonToTsSource → TS source text
8
+ *
9
+ * Stage 1 (`codec.encodeJson`) is a codec responsibility — date serialization,
10
+ * opaque domain types (vector, bigint, uuid), JSON canonicalization. Stage 2
11
+ * (this module) is a pure JSON-to-TS printer that must never grow type-specific
12
+ * branches.
13
+ *
14
+ * To render a non-JSON JS value (Date, Vector, BigInt, Buffer, …), encode it
15
+ * through the relevant codec's `encodeJson` first. Adding special cases to
16
+ * this file is not the answer — that's what codecs are for.
17
+ */
18
+ type JsonValue = string | number | boolean | null | readonly JsonValue[] | JsonObject;
19
+ type JsonObject = {
20
+ readonly [key: string]: JsonValue | undefined;
21
+ };
22
+ /**
23
+ * Render a JSON-compatible value as a TypeScript source-text literal.
24
+ *
25
+ * Accepts `unknown` for ergonomics with structural types (e.g. `ColumnSpec`,
26
+ * `ForeignKeySpec`) whose fields are all JSON-compatible but whose interfaces
27
+ * lack the index signature TypeScript requires for `JsonObject` assignability.
28
+ * Non-JSON values (Date, Symbol, Function, etc.) throw at runtime.
29
+ */
30
+ declare function jsonToTsSource(value: unknown): string;
31
+ //#endregion
32
+ //#region src/ts-expression.d.ts
33
+ /**
34
+ * Declarative contribution to the `import` block of a rendered TypeScript
35
+ * source file. Each node in an IR declares which symbols it needs from which
36
+ * modules; the top-level renderer deduplicates across nodes and emits one
37
+ * `import { a, b, c } from "…"` line per module.
38
+ *
39
+ * `kind` defaults to `"named"` (e.g. `import { a } from "m"`). Setting it to
40
+ * `"default"` emits `import a from "m"`. `attributes`, if provided, emits an
41
+ * import attributes clause (`with { type: "json" }`) verbatim — required for
42
+ * JSON module imports in the rendered scaffolds.
43
+ */
44
+ interface ImportRequirement {
45
+ readonly moduleSpecifier: string;
46
+ readonly symbol: string;
47
+ readonly kind?: 'named' | 'default';
48
+ readonly attributes?: Readonly<Record<string, string>>;
49
+ }
50
+ /**
51
+ * Abstract base class for any IR node that can be emitted as a TypeScript
52
+ * expression and declare its own import requirements.
53
+ *
54
+ * A top-level renderer walks an array of these polymorphically, concatenates
55
+ * `renderTypeScript()` results, and aggregates `importRequirements()` into a
56
+ * deduplicated import block.
57
+ */
58
+ declare abstract class TsExpression {
59
+ abstract renderTypeScript(): string;
60
+ abstract importRequirements(): readonly ImportRequirement[];
61
+ }
62
+ //#endregion
63
+ //#region src/render-imports.d.ts
64
+ /**
65
+ * Render an aggregated `import` block from a flat list of
66
+ * `ImportRequirement`s. Each target's migration renderer collects
67
+ * requirements polymorphically from its call nodes and pipes them here.
68
+ *
69
+ * The emitter invariants:
70
+ *
71
+ * - **One line per module specifier.** Named imports are aggregated and
72
+ * emitted sorted alphabetically; a single default symbol is combined
73
+ * onto the same line when attributes agree (`import def, { a, b } from "m";`).
74
+ * - **At most one default symbol per module.** Two conflicting default
75
+ * symbols on the same specifier throw — the user's renderer can't
76
+ * guess which one they meant.
77
+ * - **Attribute unanimity per module.** All requirements for the same
78
+ * module specifier must carry the same (or no) `attributes` map.
79
+ * Divergent attribute maps throw — they can't collapse to one line
80
+ * and there's no user-resolvable recovery at this layer.
81
+ * - **Deterministic ordering.** Modules are emitted sorted by specifier;
82
+ * within a module, named symbols are emitted sorted alphabetically.
83
+ *
84
+ * Returns a string containing one import line per module, joined by `\n`
85
+ * (no trailing newline). An empty requirement list returns `""`.
86
+ */
87
+ declare function renderImports(requirements: readonly ImportRequirement[]): string;
88
+ //#endregion
89
+ export { type ImportRequirement, type JsonObject, type JsonValue, TsExpression, jsonToTsSource, renderImports };
90
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/json-to-ts-source.ts","../src/ts-expression.ts","../src/render-imports.ts"],"sourcesContent":[],"mappings":";;AAiBA;AACA;AAUA;;;;ACjBA;AAeA;;;;ACDA;;;;KFRY,SAAA,+CAAwD,cAAc;KACtE,UAAA;0BAAuC;;;;;;;;;;iBAUnC,cAAA;;;;AAXhB;AACA;AAUA;;;;ACjBA;AAeA;;;UAfiB,iBAAA;ECcD,SAAA,eAAa,EAAA,MAAwB;;;wBDV7B,SAAS;;;;;;;;;;uBAWX,YAAA;;0CAEoB;;;;ADX1C;AACA;AAUA;;;;ACjBA;AAeA;;;;ACDA;;;;;;;;;;;;iBAAgB,aAAA,wBAAqC"}
package/dist/index.mjs ADDED
@@ -0,0 +1,147 @@
1
+ //#region src/json-to-ts-source.ts
2
+ /**
3
+ * Render a JSON-compatible value as a TypeScript source-text literal.
4
+ *
5
+ * Accepts `unknown` for ergonomics with structural types (e.g. `ColumnSpec`,
6
+ * `ForeignKeySpec`) whose fields are all JSON-compatible but whose interfaces
7
+ * lack the index signature TypeScript requires for `JsonObject` assignability.
8
+ * Non-JSON values (Date, Symbol, Function, etc.) throw at runtime.
9
+ */
10
+ function jsonToTsSource(value) {
11
+ if (value === void 0) return "undefined";
12
+ if (value === null) return "null";
13
+ if (typeof value === "string") return JSON.stringify(value);
14
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
15
+ if (Array.isArray(value)) {
16
+ if (value.length === 0) return "[]";
17
+ const items = value.map((v) => jsonToTsSource(v));
18
+ const singleLine = `[${items.join(", ")}]`;
19
+ if (singleLine.length <= 80) return singleLine;
20
+ return `[\n${items.map((i) => ` ${i}`).join(",\n")},\n]`;
21
+ }
22
+ if (typeof value === "object") {
23
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0);
24
+ if (entries.length === 0) return "{}";
25
+ const items = entries.map(([k, v]) => `${renderKey(k)}: ${jsonToTsSource(v)}`);
26
+ const singleLine = `{ ${items.join(", ")} }`;
27
+ if (singleLine.length <= 80) return singleLine;
28
+ return `{\n${items.map((i) => ` ${i}`).join(",\n")},\n}`;
29
+ }
30
+ throw new Error(`jsonToTsSource: unsupported value type "${typeof value}"`);
31
+ }
32
+ function renderKey(key) {
33
+ if (key === "__proto__") return JSON.stringify(key);
34
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
35
+ }
36
+
37
+ //#endregion
38
+ //#region src/render-imports.ts
39
+ /**
40
+ * Render an aggregated `import` block from a flat list of
41
+ * `ImportRequirement`s. Each target's migration renderer collects
42
+ * requirements polymorphically from its call nodes and pipes them here.
43
+ *
44
+ * The emitter invariants:
45
+ *
46
+ * - **One line per module specifier.** Named imports are aggregated and
47
+ * emitted sorted alphabetically; a single default symbol is combined
48
+ * onto the same line when attributes agree (`import def, { a, b } from "m";`).
49
+ * - **At most one default symbol per module.** Two conflicting default
50
+ * symbols on the same specifier throw — the user's renderer can't
51
+ * guess which one they meant.
52
+ * - **Attribute unanimity per module.** All requirements for the same
53
+ * module specifier must carry the same (or no) `attributes` map.
54
+ * Divergent attribute maps throw — they can't collapse to one line
55
+ * and there's no user-resolvable recovery at this layer.
56
+ * - **Deterministic ordering.** Modules are emitted sorted by specifier;
57
+ * within a module, named symbols are emitted sorted alphabetically.
58
+ *
59
+ * Returns a string containing one import line per module, joined by `\n`
60
+ * (no trailing newline). An empty requirement list returns `""`.
61
+ */
62
+ function renderImports(requirements) {
63
+ return [...aggregateByModule(requirements).entries()].sort(([a], [b]) => a.localeCompare(b)).map(([moduleSpecifier, group]) => renderModuleImport(moduleSpecifier, group)).join("\n");
64
+ }
65
+ function aggregateByModule(requirements) {
66
+ const byModule = /* @__PURE__ */ new Map();
67
+ for (const req of requirements) {
68
+ let group = byModule.get(req.moduleSpecifier);
69
+ if (!group) {
70
+ group = {
71
+ named: /* @__PURE__ */ new Set(),
72
+ defaultSymbol: null,
73
+ attributes: null,
74
+ attributesSet: false
75
+ };
76
+ byModule.set(req.moduleSpecifier, group);
77
+ }
78
+ mergeRequirementIntoGroup(req, group);
79
+ }
80
+ return byModule;
81
+ }
82
+ function mergeRequirementIntoGroup(req, group) {
83
+ if ((req.kind ?? "named") === "default") {
84
+ if (group.defaultSymbol !== null && group.defaultSymbol !== req.symbol) throw new Error(`Conflicting default imports for module "${req.moduleSpecifier}": "${group.defaultSymbol}" and "${req.symbol}". Only one default symbol is allowed per module.`);
85
+ group.defaultSymbol = req.symbol;
86
+ } else group.named.add(req.symbol);
87
+ mergeAttributes(req, group);
88
+ }
89
+ function mergeAttributes(req, group) {
90
+ const incoming = req.attributes ?? null;
91
+ if (!group.attributesSet) {
92
+ group.attributes = incoming;
93
+ group.attributesSet = true;
94
+ return;
95
+ }
96
+ if (!attributesEqual(group.attributes, incoming)) throw new Error(`Conflicting import attributes for module "${req.moduleSpecifier}": ${stringifyAttributes(group.attributes)} vs ${stringifyAttributes(incoming)}.`);
97
+ }
98
+ function attributesEqual(a, b) {
99
+ if (a === b) return true;
100
+ if (a === null || b === null) return false;
101
+ const aKeys = Object.keys(a).sort();
102
+ const bKeys = Object.keys(b).sort();
103
+ if (aKeys.length !== bKeys.length) return false;
104
+ for (let i = 0; i < aKeys.length; i++) {
105
+ const key = aKeys[i];
106
+ if (key !== bKeys[i]) return false;
107
+ if (a[key] !== b[key]) return false;
108
+ }
109
+ return true;
110
+ }
111
+ function stringifyAttributes(attrs) {
112
+ if (attrs === null) return "(none)";
113
+ return `{ ${Object.entries(attrs).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(", ")} }`;
114
+ }
115
+ function renderModuleImport(moduleSpecifier, group) {
116
+ return `import ${buildImportClause(group)} from '${moduleSpecifier}'${buildAttributesClause(group.attributes)};`;
117
+ }
118
+ function buildImportClause(group) {
119
+ const named = [...group.named].sort();
120
+ const hasNamed = named.length > 0;
121
+ const hasDefault = group.defaultSymbol !== null;
122
+ if (hasDefault && hasNamed) return `${group.defaultSymbol}, { ${named.join(", ")} }`;
123
+ if (hasDefault) return group.defaultSymbol;
124
+ return `{ ${named.join(", ")} }`;
125
+ }
126
+ function buildAttributesClause(attrs) {
127
+ if (attrs === null) return "";
128
+ const entries = Object.entries(attrs).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}: ${JSON.stringify(v)}`);
129
+ if (entries.length === 0) return "";
130
+ return ` with { ${entries.join(", ")} }`;
131
+ }
132
+
133
+ //#endregion
134
+ //#region src/ts-expression.ts
135
+ /**
136
+ * Abstract base class for any IR node that can be emitted as a TypeScript
137
+ * expression and declare its own import requirements.
138
+ *
139
+ * A top-level renderer walks an array of these polymorphically, concatenates
140
+ * `renderTypeScript()` results, and aggregates `importRequirements()` into a
141
+ * deduplicated import block.
142
+ */
143
+ var TsExpression = class {};
144
+
145
+ //#endregion
146
+ export { TsExpression, jsonToTsSource, renderImports };
147
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/json-to-ts-source.ts","../src/render-imports.ts","../src/ts-expression.ts"],"sourcesContent":["/**\n * Pure JSON-to-TypeScript-source printer.\n *\n * This module is the second stage of the codec → TS pipeline:\n *\n * jsValue → codec.encodeJson → JsonValue → jsonToTsSource → TS source text\n *\n * Stage 1 (`codec.encodeJson`) is a codec responsibility — date serialization,\n * opaque domain types (vector, bigint, uuid), JSON canonicalization. Stage 2\n * (this module) is a pure JSON-to-TS printer that must never grow type-specific\n * branches.\n *\n * To render a non-JSON JS value (Date, Vector, BigInt, Buffer, …), encode it\n * through the relevant codec's `encodeJson` first. Adding special cases to\n * this file is not the answer — that's what codecs are for.\n */\n\nexport type JsonValue = string | number | boolean | null | readonly JsonValue[] | JsonObject;\nexport type JsonObject = { readonly [key: string]: JsonValue | undefined };\n\n/**\n * Render a JSON-compatible value as a TypeScript source-text literal.\n *\n * Accepts `unknown` for ergonomics with structural types (e.g. `ColumnSpec`,\n * `ForeignKeySpec`) whose fields are all JSON-compatible but whose interfaces\n * lack the index signature TypeScript requires for `JsonObject` assignability.\n * Non-JSON values (Date, Symbol, Function, etc.) throw at runtime.\n */\nexport function jsonToTsSource(value: unknown): string {\n if (value === undefined) return 'undefined';\n if (value === null) return 'null';\n if (typeof value === 'string') return JSON.stringify(value);\n if (typeof value === 'number' || typeof value === 'boolean') return String(value);\n if (Array.isArray(value)) {\n if (value.length === 0) return '[]';\n const items = value.map((v: unknown) => jsonToTsSource(v));\n const singleLine = `[${items.join(', ')}]`;\n if (singleLine.length <= 80) return singleLine;\n return `[\\n${items.map((i) => ` ${i}`).join(',\\n')},\\n]`;\n }\n if (typeof value === 'object') {\n const entries = Object.entries(value).filter(([, v]) => v !== undefined);\n if (entries.length === 0) return '{}';\n const items = entries.map(([k, v]) => `${renderKey(k)}: ${jsonToTsSource(v)}`);\n const singleLine = `{ ${items.join(', ')} }`;\n if (singleLine.length <= 80) return singleLine;\n return `{\\n${items.map((i) => ` ${i}`).join(',\\n')},\\n}`;\n }\n throw new Error(`jsonToTsSource: unsupported value type \"${typeof value}\"`);\n}\n\nfunction renderKey(key: string): string {\n if (key === '__proto__') return JSON.stringify(key);\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);\n}\n","import type { ImportRequirement } from './ts-expression';\n\n/**\n * Render an aggregated `import` block from a flat list of\n * `ImportRequirement`s. Each target's migration renderer collects\n * requirements polymorphically from its call nodes and pipes them here.\n *\n * The emitter invariants:\n *\n * - **One line per module specifier.** Named imports are aggregated and\n * emitted sorted alphabetically; a single default symbol is combined\n * onto the same line when attributes agree (`import def, { a, b } from \"m\";`).\n * - **At most one default symbol per module.** Two conflicting default\n * symbols on the same specifier throw — the user's renderer can't\n * guess which one they meant.\n * - **Attribute unanimity per module.** All requirements for the same\n * module specifier must carry the same (or no) `attributes` map.\n * Divergent attribute maps throw — they can't collapse to one line\n * and there's no user-resolvable recovery at this layer.\n * - **Deterministic ordering.** Modules are emitted sorted by specifier;\n * within a module, named symbols are emitted sorted alphabetically.\n *\n * Returns a string containing one import line per module, joined by `\\n`\n * (no trailing newline). An empty requirement list returns `\"\"`.\n */\nexport function renderImports(requirements: readonly ImportRequirement[]): string {\n const byModule = aggregateByModule(requirements);\n const entries = [...byModule.entries()].sort(([a], [b]) => a.localeCompare(b));\n return entries\n .map(([moduleSpecifier, group]) => renderModuleImport(moduleSpecifier, group))\n .join('\\n');\n}\n\ninterface ModuleImportGroup {\n readonly named: Set<string>;\n defaultSymbol: string | null;\n attributes: Readonly<Record<string, string>> | null;\n attributesSet: boolean;\n}\n\nfunction aggregateByModule(\n requirements: readonly ImportRequirement[],\n): Map<string, ModuleImportGroup> {\n const byModule = new Map<string, ModuleImportGroup>();\n for (const req of requirements) {\n let group = byModule.get(req.moduleSpecifier);\n if (!group) {\n group = { named: new Set(), defaultSymbol: null, attributes: null, attributesSet: false };\n byModule.set(req.moduleSpecifier, group);\n }\n mergeRequirementIntoGroup(req, group);\n }\n return byModule;\n}\n\nfunction mergeRequirementIntoGroup(req: ImportRequirement, group: ModuleImportGroup): void {\n const kind = req.kind ?? 'named';\n if (kind === 'default') {\n if (group.defaultSymbol !== null && group.defaultSymbol !== req.symbol) {\n throw new Error(\n `Conflicting default imports for module \"${req.moduleSpecifier}\": ` +\n `\"${group.defaultSymbol}\" and \"${req.symbol}\". Only one default symbol is allowed per module.`,\n );\n }\n group.defaultSymbol = req.symbol;\n } else {\n group.named.add(req.symbol);\n }\n mergeAttributes(req, group);\n}\n\nfunction mergeAttributes(req: ImportRequirement, group: ModuleImportGroup): void {\n const incoming = req.attributes ?? null;\n if (!group.attributesSet) {\n group.attributes = incoming;\n group.attributesSet = true;\n return;\n }\n if (!attributesEqual(group.attributes, incoming)) {\n throw new Error(\n `Conflicting import attributes for module \"${req.moduleSpecifier}\": ` +\n `${stringifyAttributes(group.attributes)} vs ${stringifyAttributes(incoming)}.`,\n );\n }\n}\n\nfunction attributesEqual(\n a: Readonly<Record<string, string>> | null,\n b: Readonly<Record<string, string>> | null,\n): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n const aKeys = Object.keys(a).sort();\n const bKeys = Object.keys(b).sort();\n if (aKeys.length !== bKeys.length) return false;\n for (let i = 0; i < aKeys.length; i++) {\n const key = aKeys[i];\n if (key !== bKeys[i]) return false;\n if (a[key as string] !== b[key as string]) return false;\n }\n return true;\n}\n\nfunction stringifyAttributes(attrs: Readonly<Record<string, string>> | null): string {\n if (attrs === null) return '(none)';\n const entries = Object.entries(attrs)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}: ${JSON.stringify(v)}`);\n return `{ ${entries.join(', ')} }`;\n}\n\nfunction renderModuleImport(moduleSpecifier: string, group: ModuleImportGroup): string {\n const clause = buildImportClause(group);\n const attrs = buildAttributesClause(group.attributes);\n return `import ${clause} from '${moduleSpecifier}'${attrs};`;\n}\n\nfunction buildImportClause(group: ModuleImportGroup): string {\n const named = [...group.named].sort();\n const hasNamed = named.length > 0;\n const hasDefault = group.defaultSymbol !== null;\n if (hasDefault && hasNamed) {\n return `${group.defaultSymbol}, { ${named.join(', ')} }`;\n }\n if (hasDefault) {\n return group.defaultSymbol as string;\n }\n return `{ ${named.join(', ')} }`;\n}\n\nfunction buildAttributesClause(attrs: Readonly<Record<string, string>> | null): string {\n if (attrs === null) return '';\n const entries = Object.entries(attrs)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}: ${JSON.stringify(v)}`);\n if (entries.length === 0) return '';\n return ` with { ${entries.join(', ')} }`;\n}\n","/**\n * Declarative contribution to the `import` block of a rendered TypeScript\n * source file. Each node in an IR declares which symbols it needs from which\n * modules; the top-level renderer deduplicates across nodes and emits one\n * `import { a, b, c } from \"…\"` line per module.\n *\n * `kind` defaults to `\"named\"` (e.g. `import { a } from \"m\"`). Setting it to\n * `\"default\"` emits `import a from \"m\"`. `attributes`, if provided, emits an\n * import attributes clause (`with { type: \"json\" }`) verbatim — required for\n * JSON module imports in the rendered scaffolds.\n */\nexport interface ImportRequirement {\n readonly moduleSpecifier: string;\n readonly symbol: string;\n readonly kind?: 'named' | 'default';\n readonly attributes?: Readonly<Record<string, string>>;\n}\n\n/**\n * Abstract base class for any IR node that can be emitted as a TypeScript\n * expression and declare its own import requirements.\n *\n * A top-level renderer walks an array of these polymorphically, concatenates\n * `renderTypeScript()` results, and aggregates `importRequirements()` into a\n * deduplicated import block.\n */\nexport abstract class TsExpression {\n abstract renderTypeScript(): string;\n abstract importRequirements(): readonly ImportRequirement[];\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@prisma-next/ts-render",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "TypeScript source-text rendering utilities: JSON-to-TS literal printer and abstract expression base class",
7
+ "scripts": {
8
+ "build": "tsdown",
9
+ "test": "vitest run",
10
+ "test:coverage": "vitest run --coverage",
11
+ "typecheck": "tsc --noEmit",
12
+ "lint": "biome check . --error-on-warnings",
13
+ "lint:fix": "biome check --write .",
14
+ "lint:fix:unsafe": "biome check --write --unsafe .",
15
+ "clean": "rm -rf dist dist-tsc dist-tsc-prod coverage .tmp-output"
16
+ },
17
+ "dependencies": {},
18
+ "devDependencies": {
19
+ "@prisma-next/tsconfig": "workspace:*",
20
+ "@prisma-next/tsdown": "workspace:*",
21
+ "tsdown": "catalog:",
22
+ "typescript": "catalog:",
23
+ "vitest": "catalog:"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "exports": {
33
+ ".": "./dist/index.mjs",
34
+ "./package.json": "./package.json"
35
+ },
36
+ "main": "./dist/index.mjs",
37
+ "module": "./dist/index.mjs",
38
+ "types": "./dist/index.d.mts",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/prisma/prisma-next.git",
42
+ "directory": "packages/1-framework/1-core/ts-render"
43
+ }
44
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { type JsonObject, type JsonValue, jsonToTsSource } from './json-to-ts-source';
2
+ export { renderImports } from './render-imports';
3
+ export { type ImportRequirement, TsExpression } from './ts-expression';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Pure JSON-to-TypeScript-source printer.
3
+ *
4
+ * This module is the second stage of the codec → TS pipeline:
5
+ *
6
+ * jsValue → codec.encodeJson → JsonValue → jsonToTsSource → TS source text
7
+ *
8
+ * Stage 1 (`codec.encodeJson`) is a codec responsibility — date serialization,
9
+ * opaque domain types (vector, bigint, uuid), JSON canonicalization. Stage 2
10
+ * (this module) is a pure JSON-to-TS printer that must never grow type-specific
11
+ * branches.
12
+ *
13
+ * To render a non-JSON JS value (Date, Vector, BigInt, Buffer, …), encode it
14
+ * through the relevant codec's `encodeJson` first. Adding special cases to
15
+ * this file is not the answer — that's what codecs are for.
16
+ */
17
+
18
+ export type JsonValue = string | number | boolean | null | readonly JsonValue[] | JsonObject;
19
+ export type JsonObject = { readonly [key: string]: JsonValue | undefined };
20
+
21
+ /**
22
+ * Render a JSON-compatible value as a TypeScript source-text literal.
23
+ *
24
+ * Accepts `unknown` for ergonomics with structural types (e.g. `ColumnSpec`,
25
+ * `ForeignKeySpec`) whose fields are all JSON-compatible but whose interfaces
26
+ * lack the index signature TypeScript requires for `JsonObject` assignability.
27
+ * Non-JSON values (Date, Symbol, Function, etc.) throw at runtime.
28
+ */
29
+ export function jsonToTsSource(value: unknown): string {
30
+ if (value === undefined) return 'undefined';
31
+ if (value === null) return 'null';
32
+ if (typeof value === 'string') return JSON.stringify(value);
33
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
34
+ if (Array.isArray(value)) {
35
+ if (value.length === 0) return '[]';
36
+ const items = value.map((v: unknown) => jsonToTsSource(v));
37
+ const singleLine = `[${items.join(', ')}]`;
38
+ if (singleLine.length <= 80) return singleLine;
39
+ return `[\n${items.map((i) => ` ${i}`).join(',\n')},\n]`;
40
+ }
41
+ if (typeof value === 'object') {
42
+ const entries = Object.entries(value).filter(([, v]) => v !== undefined);
43
+ if (entries.length === 0) return '{}';
44
+ const items = entries.map(([k, v]) => `${renderKey(k)}: ${jsonToTsSource(v)}`);
45
+ const singleLine = `{ ${items.join(', ')} }`;
46
+ if (singleLine.length <= 80) return singleLine;
47
+ return `{\n${items.map((i) => ` ${i}`).join(',\n')},\n}`;
48
+ }
49
+ throw new Error(`jsonToTsSource: unsupported value type "${typeof value}"`);
50
+ }
51
+
52
+ function renderKey(key: string): string {
53
+ if (key === '__proto__') return JSON.stringify(key);
54
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
55
+ }
@@ -0,0 +1,138 @@
1
+ import type { ImportRequirement } from './ts-expression';
2
+
3
+ /**
4
+ * Render an aggregated `import` block from a flat list of
5
+ * `ImportRequirement`s. Each target's migration renderer collects
6
+ * requirements polymorphically from its call nodes and pipes them here.
7
+ *
8
+ * The emitter invariants:
9
+ *
10
+ * - **One line per module specifier.** Named imports are aggregated and
11
+ * emitted sorted alphabetically; a single default symbol is combined
12
+ * onto the same line when attributes agree (`import def, { a, b } from "m";`).
13
+ * - **At most one default symbol per module.** Two conflicting default
14
+ * symbols on the same specifier throw — the user's renderer can't
15
+ * guess which one they meant.
16
+ * - **Attribute unanimity per module.** All requirements for the same
17
+ * module specifier must carry the same (or no) `attributes` map.
18
+ * Divergent attribute maps throw — they can't collapse to one line
19
+ * and there's no user-resolvable recovery at this layer.
20
+ * - **Deterministic ordering.** Modules are emitted sorted by specifier;
21
+ * within a module, named symbols are emitted sorted alphabetically.
22
+ *
23
+ * Returns a string containing one import line per module, joined by `\n`
24
+ * (no trailing newline). An empty requirement list returns `""`.
25
+ */
26
+ export function renderImports(requirements: readonly ImportRequirement[]): string {
27
+ const byModule = aggregateByModule(requirements);
28
+ const entries = [...byModule.entries()].sort(([a], [b]) => a.localeCompare(b));
29
+ return entries
30
+ .map(([moduleSpecifier, group]) => renderModuleImport(moduleSpecifier, group))
31
+ .join('\n');
32
+ }
33
+
34
+ interface ModuleImportGroup {
35
+ readonly named: Set<string>;
36
+ defaultSymbol: string | null;
37
+ attributes: Readonly<Record<string, string>> | null;
38
+ attributesSet: boolean;
39
+ }
40
+
41
+ function aggregateByModule(
42
+ requirements: readonly ImportRequirement[],
43
+ ): Map<string, ModuleImportGroup> {
44
+ const byModule = new Map<string, ModuleImportGroup>();
45
+ for (const req of requirements) {
46
+ let group = byModule.get(req.moduleSpecifier);
47
+ if (!group) {
48
+ group = { named: new Set(), defaultSymbol: null, attributes: null, attributesSet: false };
49
+ byModule.set(req.moduleSpecifier, group);
50
+ }
51
+ mergeRequirementIntoGroup(req, group);
52
+ }
53
+ return byModule;
54
+ }
55
+
56
+ function mergeRequirementIntoGroup(req: ImportRequirement, group: ModuleImportGroup): void {
57
+ const kind = req.kind ?? 'named';
58
+ if (kind === 'default') {
59
+ if (group.defaultSymbol !== null && group.defaultSymbol !== req.symbol) {
60
+ throw new Error(
61
+ `Conflicting default imports for module "${req.moduleSpecifier}": ` +
62
+ `"${group.defaultSymbol}" and "${req.symbol}". Only one default symbol is allowed per module.`,
63
+ );
64
+ }
65
+ group.defaultSymbol = req.symbol;
66
+ } else {
67
+ group.named.add(req.symbol);
68
+ }
69
+ mergeAttributes(req, group);
70
+ }
71
+
72
+ function mergeAttributes(req: ImportRequirement, group: ModuleImportGroup): void {
73
+ const incoming = req.attributes ?? null;
74
+ if (!group.attributesSet) {
75
+ group.attributes = incoming;
76
+ group.attributesSet = true;
77
+ return;
78
+ }
79
+ if (!attributesEqual(group.attributes, incoming)) {
80
+ throw new Error(
81
+ `Conflicting import attributes for module "${req.moduleSpecifier}": ` +
82
+ `${stringifyAttributes(group.attributes)} vs ${stringifyAttributes(incoming)}.`,
83
+ );
84
+ }
85
+ }
86
+
87
+ function attributesEqual(
88
+ a: Readonly<Record<string, string>> | null,
89
+ b: Readonly<Record<string, string>> | null,
90
+ ): boolean {
91
+ if (a === b) return true;
92
+ if (a === null || b === null) return false;
93
+ const aKeys = Object.keys(a).sort();
94
+ const bKeys = Object.keys(b).sort();
95
+ if (aKeys.length !== bKeys.length) return false;
96
+ for (let i = 0; i < aKeys.length; i++) {
97
+ const key = aKeys[i];
98
+ if (key !== bKeys[i]) return false;
99
+ if (a[key as string] !== b[key as string]) return false;
100
+ }
101
+ return true;
102
+ }
103
+
104
+ function stringifyAttributes(attrs: Readonly<Record<string, string>> | null): string {
105
+ if (attrs === null) return '(none)';
106
+ const entries = Object.entries(attrs)
107
+ .sort(([a], [b]) => a.localeCompare(b))
108
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`);
109
+ return `{ ${entries.join(', ')} }`;
110
+ }
111
+
112
+ function renderModuleImport(moduleSpecifier: string, group: ModuleImportGroup): string {
113
+ const clause = buildImportClause(group);
114
+ const attrs = buildAttributesClause(group.attributes);
115
+ return `import ${clause} from '${moduleSpecifier}'${attrs};`;
116
+ }
117
+
118
+ function buildImportClause(group: ModuleImportGroup): string {
119
+ const named = [...group.named].sort();
120
+ const hasNamed = named.length > 0;
121
+ const hasDefault = group.defaultSymbol !== null;
122
+ if (hasDefault && hasNamed) {
123
+ return `${group.defaultSymbol}, { ${named.join(', ')} }`;
124
+ }
125
+ if (hasDefault) {
126
+ return group.defaultSymbol as string;
127
+ }
128
+ return `{ ${named.join(', ')} }`;
129
+ }
130
+
131
+ function buildAttributesClause(attrs: Readonly<Record<string, string>> | null): string {
132
+ if (attrs === null) return '';
133
+ const entries = Object.entries(attrs)
134
+ .sort(([a], [b]) => a.localeCompare(b))
135
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`);
136
+ if (entries.length === 0) return '';
137
+ return ` with { ${entries.join(', ')} }`;
138
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Declarative contribution to the `import` block of a rendered TypeScript
3
+ * source file. Each node in an IR declares which symbols it needs from which
4
+ * modules; the top-level renderer deduplicates across nodes and emits one
5
+ * `import { a, b, c } from "…"` line per module.
6
+ *
7
+ * `kind` defaults to `"named"` (e.g. `import { a } from "m"`). Setting it to
8
+ * `"default"` emits `import a from "m"`. `attributes`, if provided, emits an
9
+ * import attributes clause (`with { type: "json" }`) verbatim — required for
10
+ * JSON module imports in the rendered scaffolds.
11
+ */
12
+ export interface ImportRequirement {
13
+ readonly moduleSpecifier: string;
14
+ readonly symbol: string;
15
+ readonly kind?: 'named' | 'default';
16
+ readonly attributes?: Readonly<Record<string, string>>;
17
+ }
18
+
19
+ /**
20
+ * Abstract base class for any IR node that can be emitted as a TypeScript
21
+ * expression and declare its own import requirements.
22
+ *
23
+ * A top-level renderer walks an array of these polymorphically, concatenates
24
+ * `renderTypeScript()` results, and aggregates `importRequirements()` into a
25
+ * deduplicated import block.
26
+ */
27
+ export abstract class TsExpression {
28
+ abstract renderTypeScript(): string;
29
+ abstract importRequirements(): readonly ImportRequirement[];
30
+ }