@prisma-next/ts-render 0.11.0 → 0.12.0

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/dist/index.d.mts CHANGED
@@ -40,12 +40,20 @@ declare function jsonToTsSource(value: unknown): string;
40
40
  * `"default"` emits `import a from "m"`. `attributes`, if provided, emits an
41
41
  * import attributes clause (`with { type: "json" }`) verbatim — required for
42
42
  * JSON module imports in the rendered scaffolds.
43
+ *
44
+ * `alias`, when present and different from `symbol`, renders `symbol as alias`.
45
+ * `typeOnly` marks the symbol as a type import: when every symbol contributed
46
+ * for a module is `typeOnly`, the whole statement collapses to
47
+ * `import type { … } from "m"`; when a module mixes value and type symbols, the
48
+ * type-only ones carry a per-specifier `type` prefix (`import { type T, val }`).
43
49
  */
44
50
  interface ImportRequirement {
45
51
  readonly moduleSpecifier: string;
46
52
  readonly symbol: string;
47
53
  readonly kind?: 'named' | 'default';
48
54
  readonly attributes?: Readonly<Record<string, string>>;
55
+ readonly alias?: string;
56
+ readonly typeOnly?: boolean;
49
57
  }
50
58
  /**
51
59
  * Abstract base class for any IR node that can be emitted as a TypeScript
@@ -69,8 +77,15 @@ declare abstract class TsExpression {
69
77
  * The emitter invariants:
70
78
  *
71
79
  * - **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";`).
80
+ * emitted sorted; a single default symbol is combined onto the same line
81
+ * when attributes agree (`import def, { a, b } from "m";`). Aliased symbols
82
+ * render `symbol as alias`. When every symbol for a module is `typeOnly`,
83
+ * the statement collapses to `import type { … }`; a module mixing value
84
+ * and type symbols prefixes the type-only ones (`import { type T, v }`).
85
+ * Exception: a fully type-only statement that has both a default and one or
86
+ * more named bindings splits to two lines (`import type D from "m";` then
87
+ * `import type { N } from "m";`) because TypeScript rejects
88
+ * `import type D, { N } from "m"` (TS1363).
74
89
  * - **At most one default symbol per module.** Two conflicting default
75
90
  * symbols on the same specifier throw — the user's renderer can't
76
91
  * guess which one they meant.
@@ -78,8 +93,17 @@ declare abstract class TsExpression {
78
93
  * module specifier must carry the same (or no) `attributes` map.
79
94
  * Divergent attribute maps throw — they can't collapse to one line
80
95
  * and there's no user-resolvable recovery at this layer.
96
+ * - **Distinct (symbol, alias) pairs are distinct bindings.** TypeScript
97
+ * permits importing the same export under multiple local names, so
98
+ * `{ A }` + `{ A as B }` renders as `import { A, A as B } from "m"` and
99
+ * `{ A as B }` + `{ A as C }` renders as `import { A as B, A as C } from "m"`.
100
+ * Truly identical `(symbol, alias)` pairs still collapse to one binding,
101
+ * merging `typeOnly` by AND.
81
102
  * - **Deterministic ordering.** Modules are emitted sorted by specifier;
82
- * within a module, named symbols are emitted sorted alphabetically.
103
+ * within a module, named bindings are emitted sorted by `(symbol, alias)`
104
+ * using JavaScript code-unit comparison, with the un-aliased form (no
105
+ * alias) treated as alias `""` so it sorts before any aliased form of the
106
+ * same symbol.
83
107
  *
84
108
  * Returns a string containing one import line per module, joined by `\n`
85
109
  * (no trailing newline). An empty requirement list returns `""`.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/json-to-ts-source.ts","../src/ts-expression.ts","../src/render-imports.ts"],"mappings":";;AAiBA;;;;;AACA;;;;;AAUA;;;;;KAXY,SAAA,+CAAwD,SAAA,KAAc,UAAA;AAAA,KACtE,UAAA;EAAA,UAAyB,GAAA,WAAc,SAAA;AAAA;;;;;;;;;iBAUnC,cAAA,CAAe,KAAA;;;;AAX/B;;;;;AACA;;;;;UCPiB,iBAAA;EAAA,SACN,eAAA;EAAA,SACA,MAAA;EAAA,SACA,IAAA;EAAA,SACA,UAAA,GAAa,QAAA,CAAS,MAAA;AAAA;;;AAJjC;;;;;;uBAesB,YAAA;EAAA,SACX,gBAAA,CAAA;EAAA,SACA,kBAAA,CAAA,YAA+B,iBAAA;AAAA;;;ADX1C;;;;;AACA;;;;;AAUA;;;;;;;;ACjBA;;;;;ADMA,iBEQgB,aAAA,CAAc,YAAA,WAAuB,iBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/json-to-ts-source.ts","../src/ts-expression.ts","../src/render-imports.ts"],"mappings":";;AAiBA;;;;AAA4F;AAC5F;;;;AAA4D;AAU5D;;;;AAA6C;KAXjC,SAAA,+CAAwD,SAAA,KAAc,UAAU;AAAA,KAChF,UAAA;EAAA,UAAyB,GAAA,WAAc,SAAS;AAAA;;;;;;;;;iBAU5C,cAAA,CAAe,KAAc;;;;AAX7C;;;;AAA4F;AAC5F;;;;AAA4D;AAU5D;;;;AAA6C;;UCX5B,iBAAA;EAAA,SACN,eAAA;EAAA,SACA,MAAA;EAAA,SACA,IAAA;EAAA,SACA,UAAA,GAAa,QAAQ,CAAC,MAAA;EAAA,SACtB,KAAA;EAAA,SACA,QAAA;AAAA;;;;;;;;AAAQ;uBAWG,YAAA;EAAA,SACX,gBAAA,CAAA;EAAA,SACA,kBAAA,CAAA,YAA+B,iBAAiB;AAAA;;;ADnB3D;;;;AAA4F;AAC5F;;;;AAA4D;AAU5D;;;;AAA6C;;;;ACX7C;;;;;;;;;;;;;AAMmB;AAWnB;;;;;;;ADjBA,iBEwBgB,aAAA,CAAc,YAA0C,WAAnB,iBAAiB"}
package/dist/index.mjs CHANGED
@@ -43,8 +43,15 @@ function renderKey(key) {
43
43
  * The emitter invariants:
44
44
  *
45
45
  * - **One line per module specifier.** Named imports are aggregated and
46
- * emitted sorted alphabetically; a single default symbol is combined
47
- * onto the same line when attributes agree (`import def, { a, b } from "m";`).
46
+ * emitted sorted; a single default symbol is combined onto the same line
47
+ * when attributes agree (`import def, { a, b } from "m";`). Aliased symbols
48
+ * render `symbol as alias`. When every symbol for a module is `typeOnly`,
49
+ * the statement collapses to `import type { … }`; a module mixing value
50
+ * and type symbols prefixes the type-only ones (`import { type T, v }`).
51
+ * Exception: a fully type-only statement that has both a default and one or
52
+ * more named bindings splits to two lines (`import type D from "m";` then
53
+ * `import type { N } from "m";`) because TypeScript rejects
54
+ * `import type D, { N } from "m"` (TS1363).
48
55
  * - **At most one default symbol per module.** Two conflicting default
49
56
  * symbols on the same specifier throw — the user's renderer can't
50
57
  * guess which one they meant.
@@ -52,8 +59,17 @@ function renderKey(key) {
52
59
  * module specifier must carry the same (or no) `attributes` map.
53
60
  * Divergent attribute maps throw — they can't collapse to one line
54
61
  * and there's no user-resolvable recovery at this layer.
62
+ * - **Distinct (symbol, alias) pairs are distinct bindings.** TypeScript
63
+ * permits importing the same export under multiple local names, so
64
+ * `{ A }` + `{ A as B }` renders as `import { A, A as B } from "m"` and
65
+ * `{ A as B }` + `{ A as C }` renders as `import { A as B, A as C } from "m"`.
66
+ * Truly identical `(symbol, alias)` pairs still collapse to one binding,
67
+ * merging `typeOnly` by AND.
55
68
  * - **Deterministic ordering.** Modules are emitted sorted by specifier;
56
- * within a module, named symbols are emitted sorted alphabetically.
69
+ * within a module, named bindings are emitted sorted by `(symbol, alias)`
70
+ * using JavaScript code-unit comparison, with the un-aliased form (no
71
+ * alias) treated as alias `""` so it sorts before any aliased form of the
72
+ * same symbol.
57
73
  *
58
74
  * Returns a string containing one import line per module, joined by `\n`
59
75
  * (no trailing newline). An empty requirement list returns `""`.
@@ -67,8 +83,9 @@ function aggregateByModule(requirements) {
67
83
  let group = byModule.get(req.moduleSpecifier);
68
84
  if (!group) {
69
85
  group = {
70
- named: /* @__PURE__ */ new Set(),
86
+ named: /* @__PURE__ */ new Map(),
71
87
  defaultSymbol: null,
88
+ defaultTypeOnly: true,
72
89
  attributes: null,
73
90
  attributesSet: false
74
91
  };
@@ -79,10 +96,23 @@ function aggregateByModule(requirements) {
79
96
  return byModule;
80
97
  }
81
98
  function mergeRequirementIntoGroup(req, group) {
82
- if ((req.kind ?? "named") === "default") {
99
+ const kind = req.kind ?? "named";
100
+ const typeOnly = req.typeOnly === true;
101
+ if (kind === "default") {
83
102
  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.`);
84
103
  group.defaultSymbol = req.symbol;
85
- } else group.named.add(req.symbol);
104
+ group.defaultTypeOnly = group.defaultTypeOnly && typeOnly;
105
+ } else {
106
+ const alias = req.alias && req.alias !== req.symbol ? req.alias : null;
107
+ const key = namedBindingKey(req.symbol, alias);
108
+ const existing = group.named.get(key);
109
+ if (existing) existing.typeOnly = existing.typeOnly && typeOnly;
110
+ else group.named.set(key, {
111
+ symbol: req.symbol,
112
+ alias,
113
+ typeOnly
114
+ });
115
+ }
86
116
  mergeAttributes(req, group);
87
117
  }
88
118
  function mergeAttributes(req, group) {
@@ -112,15 +142,46 @@ function stringifyAttributes(attrs) {
112
142
  return `{ ${Object.entries(attrs).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(", ")} }`;
113
143
  }
114
144
  function renderModuleImport(moduleSpecifier, group) {
115
- return `import ${buildImportClause(group)} from '${moduleSpecifier}'${buildAttributesClause(group.attributes)};`;
145
+ const typeOnlyStatement = isStatementTypeOnly(group);
146
+ const attrs = buildAttributesClause(group.attributes);
147
+ const hasDefault = group.defaultSymbol !== null;
148
+ const hasNamed = group.named.size > 0;
149
+ if (typeOnlyStatement && hasDefault && hasNamed) return `${`import type ${group.defaultSymbol} from '${moduleSpecifier}'${attrs};`}\n${`import type { ${renderNamedBindingsList(group, true)} } from '${moduleSpecifier}'${attrs};`}`;
150
+ return `${typeOnlyStatement ? "import type" : "import"} ${buildImportClause(group, typeOnlyStatement)} from '${moduleSpecifier}'${attrs};`;
151
+ }
152
+ function isStatementTypeOnly(group) {
153
+ const hasDefault = group.defaultSymbol !== null;
154
+ const hasNamed = group.named.size > 0;
155
+ if (!hasDefault && !hasNamed) return false;
156
+ if (hasDefault && !group.defaultTypeOnly) return false;
157
+ for (const binding of group.named.values()) if (!binding.typeOnly) return false;
158
+ return true;
116
159
  }
117
- function buildImportClause(group) {
118
- const named = [...group.named].sort();
119
- const hasNamed = named.length > 0;
160
+ function buildImportClause(group, statementTypeOnly) {
161
+ const hasNamed = group.named.size > 0;
120
162
  const hasDefault = group.defaultSymbol !== null;
121
- if (hasDefault && hasNamed) return `${group.defaultSymbol}, { ${named.join(", ")} }`;
163
+ const namedClause = hasNamed ? renderNamedBindingsList(group, statementTypeOnly) : "";
164
+ if (hasDefault && hasNamed) return `${group.defaultSymbol}, { ${namedClause} }`;
122
165
  if (hasDefault) return group.defaultSymbol;
123
- return `{ ${named.join(", ")} }`;
166
+ return `{ ${namedClause} }`;
167
+ }
168
+ function renderNamedBindingsList(group, statementTypeOnly) {
169
+ return [...group.named.values()].sort(compareNamedBindings).map((binding) => renderNamedBinding(binding, statementTypeOnly)).join(", ");
170
+ }
171
+ function compareNamedBindings(a, b) {
172
+ if (a.symbol !== b.symbol) return a.symbol < b.symbol ? -1 : 1;
173
+ const aAlias = a.alias ?? "";
174
+ const bAlias = b.alias ?? "";
175
+ if (aAlias === bAlias) return 0;
176
+ return aAlias < bAlias ? -1 : 1;
177
+ }
178
+ function namedBindingKey(symbol, alias) {
179
+ return `${symbol}\x00${alias ?? ""}`;
180
+ }
181
+ function renderNamedBinding(binding, statementTypeOnly) {
182
+ const prefix = !statementTypeOnly && binding.typeOnly ? "type " : "";
183
+ const aliasClause = binding.alias !== null ? ` as ${binding.alias}` : "";
184
+ return `${prefix}${binding.symbol}${aliasClause}`;
124
185
  }
125
186
  function buildAttributesClause(attrs) {
126
187
  if (attrs === null) return "";
@@ -1 +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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"}
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; a single default symbol is combined onto the same line\n * when attributes agree (`import def, { a, b } from \"m\";`). Aliased symbols\n * render `symbol as alias`. When every symbol for a module is `typeOnly`,\n * the statement collapses to `import type { … }`; a module mixing value\n * and type symbols prefixes the type-only ones (`import { type T, v }`).\n * Exception: a fully type-only statement that has both a default and one or\n * more named bindings splits to two lines (`import type D from \"m\";` then\n * `import type { N } from \"m\";`) because TypeScript rejects\n * `import type D, { N } from \"m\"` (TS1363).\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 * - **Distinct (symbol, alias) pairs are distinct bindings.** TypeScript\n * permits importing the same export under multiple local names, so\n * `{ A }` + `{ A as B }` renders as `import { A, A as B } from \"m\"` and\n * `{ A as B }` + `{ A as C }` renders as `import { A as B, A as C } from \"m\"`.\n * Truly identical `(symbol, alias)` pairs still collapse to one binding,\n * merging `typeOnly` by AND.\n * - **Deterministic ordering.** Modules are emitted sorted by specifier;\n * within a module, named bindings are emitted sorted by `(symbol, alias)`\n * using JavaScript code-unit comparison, with the un-aliased form (no\n * alias) treated as alias `\"\"` so it sorts before any aliased form of the\n * same symbol.\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 NamedBinding {\n symbol: string;\n alias: string | null;\n typeOnly: boolean;\n}\n\ninterface ModuleImportGroup {\n readonly named: Map<string, NamedBinding>;\n defaultSymbol: string | null;\n defaultTypeOnly: boolean;\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 = {\n named: new Map(),\n defaultSymbol: null,\n defaultTypeOnly: true,\n attributes: null,\n attributesSet: false,\n };\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 const typeOnly = req.typeOnly === true;\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 group.defaultTypeOnly = group.defaultTypeOnly && typeOnly;\n } else {\n const alias = req.alias && req.alias !== req.symbol ? req.alias : null;\n const key = namedBindingKey(req.symbol, alias);\n const existing = group.named.get(key);\n if (existing) {\n existing.typeOnly = existing.typeOnly && typeOnly;\n } else {\n group.named.set(key, { symbol: req.symbol, alias, typeOnly });\n }\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 typeOnlyStatement = isStatementTypeOnly(group);\n const attrs = buildAttributesClause(group.attributes);\n const hasDefault = group.defaultSymbol !== null;\n const hasNamed = group.named.size > 0;\n if (typeOnlyStatement && hasDefault && hasNamed) {\n const defaultLine = `import type ${group.defaultSymbol} from '${moduleSpecifier}'${attrs};`;\n const namedClause = renderNamedBindingsList(group, true);\n const namedLine = `import type { ${namedClause} } from '${moduleSpecifier}'${attrs};`;\n return `${defaultLine}\\n${namedLine}`;\n }\n const keyword = typeOnlyStatement ? 'import type' : 'import';\n const clause = buildImportClause(group, typeOnlyStatement);\n return `${keyword} ${clause} from '${moduleSpecifier}'${attrs};`;\n}\n\nfunction isStatementTypeOnly(group: ModuleImportGroup): boolean {\n const hasDefault = group.defaultSymbol !== null;\n const hasNamed = group.named.size > 0;\n if (!hasDefault && !hasNamed) return false;\n if (hasDefault && !group.defaultTypeOnly) return false;\n for (const binding of group.named.values()) {\n if (!binding.typeOnly) return false;\n }\n return true;\n}\n\nfunction buildImportClause(group: ModuleImportGroup, statementTypeOnly: boolean): string {\n const hasNamed = group.named.size > 0;\n const hasDefault = group.defaultSymbol !== null;\n const namedClause = hasNamed ? renderNamedBindingsList(group, statementTypeOnly) : '';\n if (hasDefault && hasNamed) {\n return `${group.defaultSymbol}, { ${namedClause} }`;\n }\n if (hasDefault) {\n return group.defaultSymbol as string;\n }\n return `{ ${namedClause} }`;\n}\n\nfunction renderNamedBindingsList(group: ModuleImportGroup, statementTypeOnly: boolean): string {\n return [...group.named.values()]\n .sort(compareNamedBindings)\n .map((binding) => renderNamedBinding(binding, statementTypeOnly))\n .join(', ');\n}\n\nfunction compareNamedBindings(a: NamedBinding, b: NamedBinding): number {\n if (a.symbol !== b.symbol) return a.symbol < b.symbol ? -1 : 1;\n const aAlias = a.alias ?? '';\n const bAlias = b.alias ?? '';\n if (aAlias === bAlias) return 0;\n return aAlias < bAlias ? -1 : 1;\n}\n\nfunction namedBindingKey(symbol: string, alias: string | null): string {\n return `${symbol}\\x00${alias ?? ''}`;\n}\n\nfunction renderNamedBinding(binding: NamedBinding, statementTypeOnly: boolean): string {\n const prefix = !statementTypeOnly && binding.typeOnly ? 'type ' : '';\n const aliasClause = binding.alias !== null ? ` as ${binding.alias}` : '';\n return `${prefix}${binding.symbol}${aliasClause}`;\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 *\n * `alias`, when present and different from `symbol`, renders `symbol as alias`.\n * `typeOnly` marks the symbol as a type import: when every symbol contributed\n * for a module is `typeOnly`, the whole statement collapses to\n * `import type { … } from \"m\"`; when a module mixes value and type symbols, the\n * type-only ones carry a per-specifier `type` prefix (`import { type T, val }`).\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 readonly alias?: string;\n readonly typeOnly?: boolean;\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 CHANGED
@@ -1,30 +1,38 @@
1
1
  {
2
2
  "name": "@prisma-next/ts-render",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "description": "TypeScript source-text rendering utilities: JSON-to-TS literal printer and abstract expression base class",
8
8
  "dependencies": {},
9
9
  "devDependencies": {
10
- "@prisma-next/tsconfig": "0.11.0",
11
- "@prisma-next/tsdown": "0.11.0",
10
+ "@prisma-next/tsconfig": "0.12.0",
11
+ "@prisma-next/tsdown": "0.12.0",
12
12
  "tsdown": "0.22.0",
13
13
  "typescript": "5.9.3",
14
14
  "vitest": "4.1.6"
15
15
  },
16
+ "peerDependencies": {
17
+ "typescript": ">=5.9"
18
+ },
19
+ "peerDependenciesMeta": {
20
+ "typescript": {
21
+ "optional": true
22
+ }
23
+ },
16
24
  "files": [
17
25
  "dist",
18
26
  "src"
19
27
  ],
20
- "engines": {
21
- "node": ">=20"
22
- },
28
+ "types": "./dist/index.d.mts",
23
29
  "exports": {
24
30
  ".": "./dist/index.mjs",
25
31
  "./package.json": "./package.json"
26
32
  },
27
- "types": "./dist/index.d.mts",
33
+ "engines": {
34
+ "node": ">=24"
35
+ },
28
36
  "repository": {
29
37
  "type": "git",
30
38
  "url": "https://github.com/prisma/prisma-next.git",
@@ -8,8 +8,15 @@ import type { ImportRequirement } from './ts-expression';
8
8
  * The emitter invariants:
9
9
  *
10
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";`).
11
+ * emitted sorted; a single default symbol is combined onto the same line
12
+ * when attributes agree (`import def, { a, b } from "m";`). Aliased symbols
13
+ * render `symbol as alias`. When every symbol for a module is `typeOnly`,
14
+ * the statement collapses to `import type { … }`; a module mixing value
15
+ * and type symbols prefixes the type-only ones (`import { type T, v }`).
16
+ * Exception: a fully type-only statement that has both a default and one or
17
+ * more named bindings splits to two lines (`import type D from "m";` then
18
+ * `import type { N } from "m";`) because TypeScript rejects
19
+ * `import type D, { N } from "m"` (TS1363).
13
20
  * - **At most one default symbol per module.** Two conflicting default
14
21
  * symbols on the same specifier throw — the user's renderer can't
15
22
  * guess which one they meant.
@@ -17,8 +24,17 @@ import type { ImportRequirement } from './ts-expression';
17
24
  * module specifier must carry the same (or no) `attributes` map.
18
25
  * Divergent attribute maps throw — they can't collapse to one line
19
26
  * and there's no user-resolvable recovery at this layer.
27
+ * - **Distinct (symbol, alias) pairs are distinct bindings.** TypeScript
28
+ * permits importing the same export under multiple local names, so
29
+ * `{ A }` + `{ A as B }` renders as `import { A, A as B } from "m"` and
30
+ * `{ A as B }` + `{ A as C }` renders as `import { A as B, A as C } from "m"`.
31
+ * Truly identical `(symbol, alias)` pairs still collapse to one binding,
32
+ * merging `typeOnly` by AND.
20
33
  * - **Deterministic ordering.** Modules are emitted sorted by specifier;
21
- * within a module, named symbols are emitted sorted alphabetically.
34
+ * within a module, named bindings are emitted sorted by `(symbol, alias)`
35
+ * using JavaScript code-unit comparison, with the un-aliased form (no
36
+ * alias) treated as alias `""` so it sorts before any aliased form of the
37
+ * same symbol.
22
38
  *
23
39
  * Returns a string containing one import line per module, joined by `\n`
24
40
  * (no trailing newline). An empty requirement list returns `""`.
@@ -31,9 +47,16 @@ export function renderImports(requirements: readonly ImportRequirement[]): strin
31
47
  .join('\n');
32
48
  }
33
49
 
50
+ interface NamedBinding {
51
+ symbol: string;
52
+ alias: string | null;
53
+ typeOnly: boolean;
54
+ }
55
+
34
56
  interface ModuleImportGroup {
35
- readonly named: Set<string>;
57
+ readonly named: Map<string, NamedBinding>;
36
58
  defaultSymbol: string | null;
59
+ defaultTypeOnly: boolean;
37
60
  attributes: Readonly<Record<string, string>> | null;
38
61
  attributesSet: boolean;
39
62
  }
@@ -45,7 +68,13 @@ function aggregateByModule(
45
68
  for (const req of requirements) {
46
69
  let group = byModule.get(req.moduleSpecifier);
47
70
  if (!group) {
48
- group = { named: new Set(), defaultSymbol: null, attributes: null, attributesSet: false };
71
+ group = {
72
+ named: new Map(),
73
+ defaultSymbol: null,
74
+ defaultTypeOnly: true,
75
+ attributes: null,
76
+ attributesSet: false,
77
+ };
49
78
  byModule.set(req.moduleSpecifier, group);
50
79
  }
51
80
  mergeRequirementIntoGroup(req, group);
@@ -55,6 +84,7 @@ function aggregateByModule(
55
84
 
56
85
  function mergeRequirementIntoGroup(req: ImportRequirement, group: ModuleImportGroup): void {
57
86
  const kind = req.kind ?? 'named';
87
+ const typeOnly = req.typeOnly === true;
58
88
  if (kind === 'default') {
59
89
  if (group.defaultSymbol !== null && group.defaultSymbol !== req.symbol) {
60
90
  throw new Error(
@@ -63,8 +93,16 @@ function mergeRequirementIntoGroup(req: ImportRequirement, group: ModuleImportGr
63
93
  );
64
94
  }
65
95
  group.defaultSymbol = req.symbol;
96
+ group.defaultTypeOnly = group.defaultTypeOnly && typeOnly;
66
97
  } else {
67
- group.named.add(req.symbol);
98
+ const alias = req.alias && req.alias !== req.symbol ? req.alias : null;
99
+ const key = namedBindingKey(req.symbol, alias);
100
+ const existing = group.named.get(key);
101
+ if (existing) {
102
+ existing.typeOnly = existing.typeOnly && typeOnly;
103
+ } else {
104
+ group.named.set(key, { symbol: req.symbol, alias, typeOnly });
105
+ }
68
106
  }
69
107
  mergeAttributes(req, group);
70
108
  }
@@ -110,22 +148,68 @@ function stringifyAttributes(attrs: Readonly<Record<string, string>> | null): st
110
148
  }
111
149
 
112
150
  function renderModuleImport(moduleSpecifier: string, group: ModuleImportGroup): string {
113
- const clause = buildImportClause(group);
151
+ const typeOnlyStatement = isStatementTypeOnly(group);
114
152
  const attrs = buildAttributesClause(group.attributes);
115
- return `import ${clause} from '${moduleSpecifier}'${attrs};`;
153
+ const hasDefault = group.defaultSymbol !== null;
154
+ const hasNamed = group.named.size > 0;
155
+ if (typeOnlyStatement && hasDefault && hasNamed) {
156
+ const defaultLine = `import type ${group.defaultSymbol} from '${moduleSpecifier}'${attrs};`;
157
+ const namedClause = renderNamedBindingsList(group, true);
158
+ const namedLine = `import type { ${namedClause} } from '${moduleSpecifier}'${attrs};`;
159
+ return `${defaultLine}\n${namedLine}`;
160
+ }
161
+ const keyword = typeOnlyStatement ? 'import type' : 'import';
162
+ const clause = buildImportClause(group, typeOnlyStatement);
163
+ return `${keyword} ${clause} from '${moduleSpecifier}'${attrs};`;
164
+ }
165
+
166
+ function isStatementTypeOnly(group: ModuleImportGroup): boolean {
167
+ const hasDefault = group.defaultSymbol !== null;
168
+ const hasNamed = group.named.size > 0;
169
+ if (!hasDefault && !hasNamed) return false;
170
+ if (hasDefault && !group.defaultTypeOnly) return false;
171
+ for (const binding of group.named.values()) {
172
+ if (!binding.typeOnly) return false;
173
+ }
174
+ return true;
116
175
  }
117
176
 
118
- function buildImportClause(group: ModuleImportGroup): string {
119
- const named = [...group.named].sort();
120
- const hasNamed = named.length > 0;
177
+ function buildImportClause(group: ModuleImportGroup, statementTypeOnly: boolean): string {
178
+ const hasNamed = group.named.size > 0;
121
179
  const hasDefault = group.defaultSymbol !== null;
180
+ const namedClause = hasNamed ? renderNamedBindingsList(group, statementTypeOnly) : '';
122
181
  if (hasDefault && hasNamed) {
123
- return `${group.defaultSymbol}, { ${named.join(', ')} }`;
182
+ return `${group.defaultSymbol}, { ${namedClause} }`;
124
183
  }
125
184
  if (hasDefault) {
126
185
  return group.defaultSymbol as string;
127
186
  }
128
- return `{ ${named.join(', ')} }`;
187
+ return `{ ${namedClause} }`;
188
+ }
189
+
190
+ function renderNamedBindingsList(group: ModuleImportGroup, statementTypeOnly: boolean): string {
191
+ return [...group.named.values()]
192
+ .sort(compareNamedBindings)
193
+ .map((binding) => renderNamedBinding(binding, statementTypeOnly))
194
+ .join(', ');
195
+ }
196
+
197
+ function compareNamedBindings(a: NamedBinding, b: NamedBinding): number {
198
+ if (a.symbol !== b.symbol) return a.symbol < b.symbol ? -1 : 1;
199
+ const aAlias = a.alias ?? '';
200
+ const bAlias = b.alias ?? '';
201
+ if (aAlias === bAlias) return 0;
202
+ return aAlias < bAlias ? -1 : 1;
203
+ }
204
+
205
+ function namedBindingKey(symbol: string, alias: string | null): string {
206
+ return `${symbol}\x00${alias ?? ''}`;
207
+ }
208
+
209
+ function renderNamedBinding(binding: NamedBinding, statementTypeOnly: boolean): string {
210
+ const prefix = !statementTypeOnly && binding.typeOnly ? 'type ' : '';
211
+ const aliasClause = binding.alias !== null ? ` as ${binding.alias}` : '';
212
+ return `${prefix}${binding.symbol}${aliasClause}`;
129
213
  }
130
214
 
131
215
  function buildAttributesClause(attrs: Readonly<Record<string, string>> | null): string {
@@ -8,12 +8,20 @@
8
8
  * `"default"` emits `import a from "m"`. `attributes`, if provided, emits an
9
9
  * import attributes clause (`with { type: "json" }`) verbatim — required for
10
10
  * JSON module imports in the rendered scaffolds.
11
+ *
12
+ * `alias`, when present and different from `symbol`, renders `symbol as alias`.
13
+ * `typeOnly` marks the symbol as a type import: when every symbol contributed
14
+ * for a module is `typeOnly`, the whole statement collapses to
15
+ * `import type { … } from "m"`; when a module mixes value and type symbols, the
16
+ * type-only ones carry a per-specifier `type` prefix (`import { type T, val }`).
11
17
  */
12
18
  export interface ImportRequirement {
13
19
  readonly moduleSpecifier: string;
14
20
  readonly symbol: string;
15
21
  readonly kind?: 'named' | 'default';
16
22
  readonly attributes?: Readonly<Record<string, string>>;
23
+ readonly alias?: string;
24
+ readonly typeOnly?: boolean;
17
25
  }
18
26
 
19
27
  /**