@invinite-org/chartlang-compiler 1.0.1 → 1.1.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +3 -0
  3. package/dist/analysis/extractCapabilities.d.ts +5 -1
  4. package/dist/analysis/extractCapabilities.d.ts.map +1 -1
  5. package/dist/analysis/extractCapabilities.js +6 -2
  6. package/dist/analysis/extractCapabilities.js.map +1 -1
  7. package/dist/analysis/extractDependencyGraph.d.ts +160 -0
  8. package/dist/analysis/extractDependencyGraph.d.ts.map +1 -0
  9. package/dist/analysis/extractDependencyGraph.js +690 -0
  10. package/dist/analysis/extractDependencyGraph.js.map +1 -0
  11. package/dist/analysis/extractInputs.d.ts +5 -1
  12. package/dist/analysis/extractInputs.d.ts.map +1 -1
  13. package/dist/analysis/extractInputs.js +6 -2
  14. package/dist/analysis/extractInputs.js.map +1 -1
  15. package/dist/analysis/extractMaxLookback.d.ts +6 -1
  16. package/dist/analysis/extractMaxLookback.d.ts.map +1 -1
  17. package/dist/analysis/extractMaxLookback.js +10 -5
  18. package/dist/analysis/extractMaxLookback.js.map +1 -1
  19. package/dist/analysis/forbiddenConstructs.d.ts.map +1 -1
  20. package/dist/analysis/forbiddenConstructs.js +3 -0
  21. package/dist/analysis/forbiddenConstructs.js.map +1 -1
  22. package/dist/analysis/index.d.ts +3 -1
  23. package/dist/analysis/index.d.ts.map +1 -1
  24. package/dist/analysis/index.js +1 -0
  25. package/dist/analysis/index.js.map +1 -1
  26. package/dist/analysis/structuralChecks.d.ts +64 -8
  27. package/dist/analysis/structuralChecks.d.ts.map +1 -1
  28. package/dist/analysis/structuralChecks.js +111 -22
  29. package/dist/analysis/structuralChecks.js.map +1 -1
  30. package/dist/api.d.ts +40 -0
  31. package/dist/api.d.ts.map +1 -1
  32. package/dist/api.js +484 -35
  33. package/dist/api.js.map +1 -1
  34. package/dist/bundle.d.ts +90 -3
  35. package/dist/bundle.d.ts.map +1 -1
  36. package/dist/bundle.js +59 -5
  37. package/dist/bundle.js.map +1 -1
  38. package/dist/dependency/index.d.ts +3 -0
  39. package/dist/dependency/index.d.ts.map +1 -0
  40. package/dist/dependency/index.js +4 -0
  41. package/dist/dependency/index.js.map +1 -0
  42. package/dist/dependency/resolveProducer.d.ts +183 -0
  43. package/dist/dependency/resolveProducer.d.ts.map +1 -0
  44. package/dist/dependency/resolveProducer.js +256 -0
  45. package/dist/dependency/resolveProducer.js.map +1 -0
  46. package/dist/diagnostics.d.ts +6 -2
  47. package/dist/diagnostics.d.ts.map +1 -1
  48. package/dist/diagnostics.js.map +1 -1
  49. package/dist/index.d.ts +3 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/manifest.d.ts +8 -1
  54. package/dist/manifest.d.ts.map +1 -1
  55. package/dist/manifest.js +27 -0
  56. package/dist/manifest.js.map +1 -1
  57. package/dist/program.d.ts +1 -0
  58. package/dist/program.d.ts.map +1 -1
  59. package/dist/program.js +80 -4
  60. package/dist/program.js.map +1 -1
  61. package/dist/transformers/callsiteIdInjection.d.ts +2 -1
  62. package/dist/transformers/callsiteIdInjection.d.ts.map +1 -1
  63. package/dist/transformers/callsiteIdInjection.js +15 -0
  64. package/dist/transformers/callsiteIdInjection.js.map +1 -1
  65. package/dist/transformers/index.d.ts +2 -0
  66. package/dist/transformers/index.d.ts.map +1 -1
  67. package/dist/transformers/index.js +1 -0
  68. package/dist/transformers/index.js.map +1 -1
  69. package/dist/transformers/plotKindFromCallsite.d.ts +43 -0
  70. package/dist/transformers/plotKindFromCallsite.d.ts.map +1 -0
  71. package/dist/transformers/plotKindFromCallsite.js +103 -0
  72. package/dist/transformers/plotKindFromCallsite.js.map +1 -0
  73. package/dist/transformers/rewriteDependencyAccessors.d.ts +65 -0
  74. package/dist/transformers/rewriteDependencyAccessors.d.ts.map +1 -0
  75. package/dist/transformers/rewriteDependencyAccessors.js +204 -0
  76. package/dist/transformers/rewriteDependencyAccessors.js.map +1 -0
  77. package/dist/typesEmit.d.ts +14 -11
  78. package/dist/typesEmit.d.ts.map +1 -1
  79. package/dist/typesEmit.js +91 -7
  80. package/dist/typesEmit.js.map +1 -1
  81. package/package.json +2 -2
package/dist/api.js CHANGED
@@ -2,15 +2,17 @@
2
2
  // See the LICENSE file in the repo root for full license text.
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
5
- import { isAbsolute, join, relative, resolve as resolvePath } from "node:path";
5
+ import { dirname, isAbsolute, join, relative, resolve as resolvePath } from "node:path";
6
6
  import { STATEFUL_PRIMITIVES_BY_NAME } from "@invinite-org/chartlang-core";
7
7
  import ts from "typescript";
8
- import { extractAlertConditions, extractCapabilities, extractInputs, extractMaxLookback, extractRequestedIntervals, extractRequiresIntervals, runForbiddenConstructs, runStatefulCallInLoop, runStructuralChecks, validateLowerTfIntervals, } from "./analysis/index.js";
9
- import { bundleModule, formatManifestAssignment } from "./bundle.js";
8
+ import { extractAlertConditions, extractCapabilities, extractDependencyGraph, extractInputs, extractMaxLookback, extractRequestedIntervals, extractRequiresIntervals, runForbiddenConstructs, runStatefulCallInLoop, runStructuralChecks, validateLowerTfIntervals, } from "./analysis/index.js";
9
+ import { bundleModule, formatDependenciesAssignment, formatManifestAssignment } from "./bundle.js";
10
+ import { createProducerResolver, } from "./dependency/index.js";
10
11
  import { mapTsDiagnostic } from "./diagnostics.js";
11
12
  import { buildManifest } from "./manifest.js";
12
13
  import { createProgramForSource } from "./program.js";
13
14
  import { injectCallsiteIds } from "./transformers/callsiteIdInjection.js";
15
+ import { rewriteDependencyAccessors } from "./transformers/rewriteDependencyAccessors.js";
14
16
  import { emitTypes } from "./typesEmit.js";
15
17
  /**
16
18
  * Run the Phase-1 compiler pipeline against an in-memory script source.
@@ -36,7 +38,11 @@ import { emitTypes } from "./typesEmit.js";
36
38
  */
37
39
  export function transformAndAnalyse(source, opts) {
38
40
  const sourcePath = opts.sourcePath;
39
- const { program, sourceFile, checker } = createProgramForSource(source, { sourcePath });
41
+ const chartImports = preScanChartImports(source, sourcePath);
42
+ const { program, sourceFile, checker } = createProgramForSource(source, {
43
+ sourcePath,
44
+ chartImports,
45
+ });
40
46
  const structural = runStructuralChecks(sourceFile, checker, sourcePath);
41
47
  const forbidden = runForbiddenConstructs(sourceFile, sourcePath);
42
48
  const statefulInLoop = runStatefulCallInLoop(sourceFile, checker, sourcePath, STATEFUL_PRIMITIVES_BY_NAME);
@@ -50,11 +56,28 @@ export function transformAndAnalyse(source, opts) {
50
56
  .getSemanticDiagnostics(sourceFile)
51
57
  .filter((d) => d.file?.fileName === sourceFile.fileName)
52
58
  .map((d) => mapTsDiagnostic(d, sourcePath));
59
+ const depGraph = extractDependencyGraph(sourceFile, checker, sourcePath, structural.bindings, opts.resolveProducer === undefined
60
+ ? () => null
61
+ : (modSpec, expName) => {
62
+ const snap = opts.resolveProducer?.(modSpec, expName);
63
+ /* v8 ignore next */
64
+ if (snap === undefined || snap === null)
65
+ return null;
66
+ return Object.freeze({
67
+ name: snap.name,
68
+ outputs: Object.freeze(snap.outputs.map((o) => Object.freeze({
69
+ title: o.title,
70
+ kind: o.kind,
71
+ }))),
72
+ inputs: snap.inputs,
73
+ });
74
+ });
53
75
  const earlyDiagnostics = [
54
76
  ...semanticDiagnostics,
55
77
  ...structural.diagnostics,
56
78
  ...forbidden,
57
79
  ...statefulInLoop,
80
+ ...depGraph.diagnostics,
58
81
  ];
59
82
  const hasError = earlyDiagnostics.some((d) => d.severity === "error");
60
83
  if (hasError) {
@@ -74,41 +97,79 @@ export function transformAndAnalyse(source, opts) {
74
97
  diagnostics: Object.freeze(earlyDiagnostics.slice()),
75
98
  });
76
99
  }
77
- const injection = injectCallsiteIds(sourceFile, checker, {
100
+ const rewrite = rewriteDependencyAccessors(sourceFile, depGraph, sourcePath);
101
+ const rewrittenSource = rewrite.transformed;
102
+ const plotSlots = [];
103
+ const injection = injectCallsiteIds(rewrittenSource, checker, {
78
104
  sourcePath,
79
105
  statefulByName: STATEFUL_PRIMITIVES_BY_NAME,
106
+ plotSlots,
80
107
  });
81
- const capabilities = extractCapabilities(sourceFile, checker, structural.kind);
82
- const lookback = extractMaxLookback(sourceFile, checker, sourcePath);
83
- const inputs = extractInputs(sourceFile, checker, sourcePath);
84
108
  const alertConditions = extractAlertConditions(sourceFile, checker, sourcePath);
85
109
  const intervalDiagnostics = [];
86
- const requestedIntervalsFromCalls = extractRequestedIntervals(sourceFile, checker, inputs.inputs, intervalDiagnostics, sourcePath);
87
- const requiresIntervals = extractRequiresIntervals(sourceFile, checker, intervalDiagnostics, sourcePath);
88
110
  const lowerTfDiagnostics = validateLowerTfIntervals(sourceFile, checker, sourcePath, opts.declaredIntervals ?? []);
89
- const requestedIntervals = Array.from(new Set([...requestedIntervalsFromCalls, ...requiresIntervals])).sort();
90
111
  const { requiresIntervals: structuralRequiresIntervals, ...structuralOverrides } = structural.overrides;
91
112
  void structuralRequiresIntervals;
92
- const manifest = buildManifest({
93
- name: structural.name,
94
- kind: structural.kind,
95
- capabilities,
96
- requestedIntervals,
97
- userPickableInterval: inputs.userPickableInterval,
98
- seriesCapacities: lookback.seriesCapacities,
99
- maxLookback: lookback.maxLookback,
100
- inputs: inputs.inputs,
101
- ...structuralOverrides,
102
- ...(requiresIntervals.length === 0 ? {} : { requiresIntervals }),
103
- ...(alertConditions.alertConditions.length === 0
104
- ? {}
105
- : { alertConditions: alertConditions.alertConditions }),
106
- });
113
+ // The structural pass guarantees a single default-export drawn
114
+ // entry before we reach this code (errors short-circuit above);
115
+ // we surface the default here for manifest assembly.
116
+ const defaultDrawn = depGraph.drawn.find((d) => d.exportName === "default");
117
+ /* v8 ignore start */
118
+ if (defaultDrawn === undefined) {
119
+ // The structural pass guarantees a default-export drawn entry
120
+ // before we reach this code (errors short-circuit above).
121
+ throw new Error("internal: depGraph.drawn missing default entry");
122
+ }
123
+ /* v8 ignore stop */
124
+ const isMultiExport = depGraph.drawn.length > 1;
125
+ // File-level extractions (single-export back-compat path) — full source
126
+ // walk so existing single-script manifests stay byte-identical.
127
+ const fileCapabilities = extractCapabilities(sourceFile, checker, structural.kind);
128
+ const fileLookback = extractMaxLookback(sourceFile, checker, sourcePath);
129
+ const fileInputs = extractInputs(sourceFile, checker, sourcePath);
130
+ const fileRequestedIntervalsFromCalls = extractRequestedIntervals(sourceFile, checker, fileInputs.inputs, intervalDiagnostics, sourcePath);
131
+ const fileRequiresIntervals = extractRequiresIntervals(sourceFile, checker, intervalDiagnostics, sourcePath);
132
+ const fileRequestedIntervals = Array.from(new Set([...fileRequestedIntervalsFromCalls, ...fileRequiresIntervals])).sort();
133
+ const namedManifests = [];
134
+ if (isMultiExport) {
135
+ for (const drawn of depGraph.drawn) {
136
+ if (drawn.exportName === "default")
137
+ continue;
138
+ namedManifests.push(buildDrawnManifest(drawn, depGraph, sourceFile, checker, sourcePath, structural.kind, structuralOverrides, alertConditions.alertConditions));
139
+ }
140
+ }
141
+ const defaultDependencies = buildDependencyDeclarations(defaultDrawn, depGraph, sourcePath);
142
+ const defaultOutputs = defaultDrawn.outputs.length === 0 ? undefined : defaultDrawn.outputs;
143
+ const manifest = isMultiExport
144
+ ? buildDrawnManifest(defaultDrawn, depGraph, sourceFile, checker, sourcePath, structural.kind, structuralOverrides, alertConditions.alertConditions, namedManifests, plotSlots)
145
+ : buildManifest({
146
+ name: structural.name,
147
+ kind: structural.kind,
148
+ capabilities: fileCapabilities,
149
+ requestedIntervals: fileRequestedIntervals,
150
+ userPickableInterval: fileInputs.userPickableInterval,
151
+ seriesCapacities: fileLookback.seriesCapacities,
152
+ maxLookback: fileLookback.maxLookback,
153
+ inputs: fileInputs.inputs,
154
+ ...structuralOverrides,
155
+ ...(fileRequiresIntervals.length === 0
156
+ ? {}
157
+ : { requiresIntervals: fileRequiresIntervals }),
158
+ ...(alertConditions.alertConditions.length === 0
159
+ ? {}
160
+ : { alertConditions: alertConditions.alertConditions }),
161
+ ...(defaultDependencies === undefined || defaultDependencies.length === 0
162
+ ? {}
163
+ : { dependencies: defaultDependencies }),
164
+ ...(defaultOutputs === undefined ? {} : { outputs: defaultOutputs }),
165
+ ...(plotSlots.length === 0 ? {} : { plots: plotSlots }),
166
+ });
107
167
  const allDiagnostics = [
108
168
  ...earlyDiagnostics,
109
169
  ...injection.diagnostics,
110
- ...lookback.diagnostics,
111
- ...inputs.diagnostics,
170
+ ...rewrite.diagnostics,
171
+ ...fileLookback.diagnostics,
172
+ ...fileInputs.diagnostics,
112
173
  ...alertConditions.diagnostics,
113
174
  ...intervalDiagnostics,
114
175
  ...lowerTfDiagnostics,
@@ -117,8 +178,68 @@ export function transformAndAnalyse(source, opts) {
117
178
  transformed: injection.transformed,
118
179
  manifest,
119
180
  diagnostics: Object.freeze(allDiagnostics.slice()),
181
+ ...(isMultiExport ? { siblings: Object.freeze(namedManifests.slice()) } : {}),
120
182
  });
121
183
  }
184
+ function buildDrawnManifest(drawn, depGraph, sourceFile, checker, sourcePath, kind, structuralOverrides, sharedAlertConditions, siblings, plotSlots) {
185
+ const intervalDiagnostics = [];
186
+ const scope = drawn.defineCall;
187
+ const capabilities = extractCapabilities(sourceFile, checker, kind, scope);
188
+ const lookback = extractMaxLookback(sourceFile, checker, sourcePath, scope);
189
+ const inputs = extractInputs(sourceFile, checker, sourcePath, scope);
190
+ const requestedFromCalls = extractRequestedIntervals(sourceFile, checker, inputs.inputs, intervalDiagnostics, sourcePath);
191
+ const requiresIntervalsScoped = extractRequiresIntervals(sourceFile, checker, intervalDiagnostics, sourcePath);
192
+ const requestedIntervals = Array.from(new Set([...requestedFromCalls, ...requiresIntervalsScoped])).sort();
193
+ const dependencies = buildDependencyDeclarations(drawn, depGraph, sourcePath);
194
+ const outputs = drawn.outputs.length === 0 ? undefined : drawn.outputs;
195
+ const isDefault = drawn.exportName === "default";
196
+ const nameForManifest = isDefault ? readDefineCallName(drawn.defineCall) : drawn.bindingName;
197
+ return buildManifest({
198
+ name: nameForManifest,
199
+ kind,
200
+ capabilities,
201
+ requestedIntervals,
202
+ userPickableInterval: inputs.userPickableInterval,
203
+ seriesCapacities: lookback.seriesCapacities,
204
+ maxLookback: lookback.maxLookback,
205
+ inputs: inputs.inputs,
206
+ ...structuralOverrides,
207
+ /* v8 ignore start */
208
+ ...(requiresIntervalsScoped.length === 0
209
+ ? {}
210
+ : { requiresIntervals: requiresIntervalsScoped }),
211
+ ...(sharedAlertConditions.length === 0 ? {} : { alertConditions: sharedAlertConditions }),
212
+ /* v8 ignore stop */
213
+ ...(dependencies === undefined || dependencies.length === 0 ? {} : { dependencies }),
214
+ ...(outputs === undefined ? {} : { outputs }),
215
+ ...(plotSlots === undefined || plotSlots.length === 0 ? {} : { plots: plotSlots }),
216
+ exportName: drawn.exportName,
217
+ isDrawn: true,
218
+ ...(siblings !== undefined && siblings.length > 0 ? { siblings } : {}),
219
+ });
220
+ }
221
+ function readDefineCallName(defineCall) {
222
+ const arg = defineCall.arguments[0];
223
+ /* v8 ignore next 3 */
224
+ if (arg === undefined || !ts.isObjectLiteralExpression(arg)) {
225
+ return "";
226
+ }
227
+ for (const property of arg.properties) {
228
+ /* v8 ignore next */
229
+ if (!ts.isPropertyAssignment(property))
230
+ continue;
231
+ const name = property.name;
232
+ /* v8 ignore next */
233
+ if (!ts.isIdentifier(name) || name.text !== "name")
234
+ continue;
235
+ const initializer = property.initializer;
236
+ if (ts.isStringLiteral(initializer))
237
+ return initializer.text;
238
+ /* v8 ignore next */
239
+ }
240
+ /* v8 ignore next 2 */
241
+ return "";
242
+ }
122
243
  /**
123
244
  * Error thrown by `compile` / `compileFile` / `compileProject` when any
124
245
  * compilation produces an error-severity diagnostic. Carries the full
@@ -166,26 +287,122 @@ const PRINTER = ts.createPrinter({
166
287
  */
167
288
  export async function compile(source, opts) {
168
289
  const sourcePath = opts.sourcePath ?? "script.chart.ts";
169
- const result = transformAndAnalyse(source, {
290
+ const resolveProducer = opts.resolveProducer ?? createDefaultProducerResolver(sourcePath, opts);
291
+ // Pre-scan the consumer's source for `import X from "./Y.chart"`
292
+ // statements + resolve them in parallel. Each producer's resolved
293
+ // snapshot feeds the sync lookup passed to `transformAndAnalyse`;
294
+ // the same snapshots become the bundler's `inlinedProducers`.
295
+ const preScan = preScanChartImports(source, sourcePath);
296
+ const resolved = await Promise.all(preScan.map(async (specifier) => {
297
+ const compiled = await resolveProducer(specifier, sourcePath);
298
+ return { specifier, compiled };
299
+ }));
300
+ const resolvedBySpecifier = new Map();
301
+ for (const entry of resolved) {
302
+ resolvedBySpecifier.set(entry.specifier, entry.compiled);
303
+ }
304
+ const transformOpts = {
170
305
  sourcePath,
306
+ /* v8 ignore next 3 */
171
307
  ...(opts.declaredIntervals === undefined
172
308
  ? {}
173
309
  : { declaredIntervals: opts.declaredIntervals }),
174
- });
310
+ resolveProducer: (modSpec, expName) => {
311
+ const compiled = resolvedBySpecifier.get(modSpec);
312
+ /* v8 ignore next 3 */
313
+ if (compiled === undefined || compiled === null) {
314
+ return null;
315
+ }
316
+ const manifest = compiled.drawnByExportName.get(expName);
317
+ /* v8 ignore next 3 */
318
+ if (manifest === undefined) {
319
+ return null;
320
+ }
321
+ /* v8 ignore next */
322
+ const outputs = manifest.outputs ?? [];
323
+ return Object.freeze({
324
+ name: manifest.name,
325
+ outputs: Object.freeze(outputs.map((o) => Object.freeze({ title: o.title, kind: o.kind }))),
326
+ inputs: Object.fromEntries(Object.entries(manifest.inputs).map(([key, descriptor]) => [
327
+ key,
328
+ descriptor,
329
+ ])),
330
+ });
331
+ },
332
+ };
333
+ const result = transformAndAnalyse(source, transformOpts);
175
334
  const errors = result.diagnostics.filter((d) => d.severity === "error");
176
335
  if (errors.length > 0) {
177
336
  throw new CompileError(Object.freeze(errors.slice()));
178
337
  }
179
- const transformedSource = PRINTER.printFile(result.transformed);
338
+ const printedSource = PRINTER.printFile(result.transformed);
180
339
  const sourcemap = opts.sourcemap ?? false;
340
+ // Walk every direct dep's transitive producer tree to build a
341
+ // topologically-ordered list (leaves first). Dedup by hash so a
342
+ // producer reached via two paths inlines exactly once.
343
+ const orderedProducers = [];
344
+ const seenHashes = new Set();
345
+ const collectTransitive = (p) => {
346
+ if (seenHashes.has(p.hash))
347
+ return;
348
+ for (const nested of p.transitiveProducers)
349
+ collectTransitive(nested);
350
+ seenHashes.add(p.hash);
351
+ orderedProducers.push(p);
352
+ };
353
+ for (const { compiled } of resolved) {
354
+ /* v8 ignore next */
355
+ if (compiled === null)
356
+ continue;
357
+ collectTransitive(compiled);
358
+ }
359
+ // Lower each cross-file `import <name> from "./X.chart"` line in
360
+ // the consumer's source to `const <name> = __producer_<hash>__default;`
361
+ // so the inlined producer's local binding feeds the rest of the
362
+ // consumer's body. Imports of non-resolved producers stay as-is so
363
+ // esbuild surfaces the unresolved-import error.
364
+ const specifierToHash = new Map();
365
+ for (const entry of resolved) {
366
+ /* v8 ignore next */
367
+ if (entry.compiled === null)
368
+ continue;
369
+ specifierToHash.set(entry.specifier, entry.compiled.hash);
370
+ }
371
+ const consumerSourceWithRewrittenImports = rewriteConsumerChartImports(printedSource, specifierToHash);
372
+ // §22.10 indicator-composition: when the default manifest declares
373
+ // private deps, append a hidden `export const __dependencies = [...]`
374
+ // BEFORE handing the source to esbuild so the bundler sees every
375
+ // alias binding referenced from the export graph. Pre-bundle inclusion
376
+ // is load-bearing — appending after `bundleModule` would let esbuild's
377
+ // tree-shaker drop aliases declared via `const trend = baseTrend;`
378
+ // (cross-file aliases reduce to a bare reference after the §22.10
379
+ // `withInputs` chain rewrite, which esbuild treats as side-effect-free
380
+ // and DCE-eligible).
381
+ const defaultDeps = result.manifest.dependencies ?? [];
382
+ const depsAssignment = formatDependenciesAssignment(defaultDeps.map((d) => ({
383
+ localId: d.localId,
384
+ bindingExpression: d.localId,
385
+ ...(Object.keys(d.effectiveInputs).length === 0
386
+ ? {}
387
+ : { effectiveInputs: d.effectiveInputs }),
388
+ })));
389
+ const transformedSource = `${consumerSourceWithRewrittenImports}\n${depsAssignment}`;
390
+ const inlinedProducers = orderedProducers.map((p) => ({
391
+ hash: p.hash,
392
+ rewrittenSource: p.rewrittenSource,
393
+ }));
181
394
  const bundle = await bundleModule({
182
395
  transformedSource,
183
396
  sourcePath,
184
397
  sourcemap,
185
398
  minify: opts.minify ?? false,
399
+ ...(inlinedProducers.length === 0 ? {} : { inlinedProducers }),
186
400
  });
187
- const moduleSource = `${bundle.moduleSource}${formatManifestAssignment(result.manifest)}`;
188
- const types = emitTypes({ manifest: result.manifest, sourcePath });
401
+ const sidecar = result.siblings === undefined
402
+ ? result.manifest
403
+ : Object.freeze([result.manifest, ...result.siblings]);
404
+ const moduleSource = `${bundle.moduleSource}${formatManifestAssignment(sidecar)}`;
405
+ const types = emitTypes({ manifest: sidecar, sourcePath });
189
406
  if (bundle.sourcemap !== undefined) {
190
407
  return Object.freeze({
191
408
  moduleSource,
@@ -253,8 +470,11 @@ export async function compileFile(path, opts) {
253
470
  const jsPath = `${base}.chart.js`;
254
471
  const manifestPath = `${base}.chart.manifest.json`;
255
472
  const dtsPath = `${base}.chart.d.ts`;
473
+ const sidecar = result.manifest.siblings === undefined
474
+ ? result.manifest
475
+ : Object.freeze([result.manifest, ...result.manifest.siblings]);
256
476
  await writeAtomic(jsPath, result.moduleSource);
257
- await writeAtomic(manifestPath, JSON.stringify(result.manifest, null, 4));
477
+ await writeAtomic(manifestPath, JSON.stringify(sidecar, null, 4));
258
478
  await writeAtomic(dtsPath, result.types);
259
479
  if ((opts.sourcemap === true || opts.sourcemap === "external") &&
260
480
  result.sourcemap !== undefined) {
@@ -318,10 +538,22 @@ export async function walkChartFiles(rootDir) {
318
538
  */
319
539
  export async function compileProject(rootDir, opts) {
320
540
  const files = await walkChartFiles(rootDir);
541
+ let absoluteRoot;
542
+ if (isAbsolute(rootDir)) {
543
+ absoluteRoot = rootDir;
544
+ /* v8 ignore next 3 */
545
+ }
546
+ else {
547
+ absoluteRoot = resolvePath(process.cwd(), rootDir);
548
+ }
549
+ const sharedResolver = opts.resolveProducer ??
550
+ createProducerResolver({ rootDir: absoluteRoot }, (source, producerSourcePath) => compileProducerArtefacts(source, producerSourcePath, sharedResolver));
321
551
  const compiled = await Promise.all(files.map((file) => compileFile(file, {
322
552
  ...opts,
323
553
  write: false,
324
554
  sourcePath: toPosixRelative(process.cwd(), file),
555
+ resolveProducer: sharedResolver,
556
+ rootDir: absoluteRoot,
325
557
  })));
326
558
  return Object.freeze(compiled);
327
559
  }
@@ -336,8 +568,49 @@ async function readDirEntries(dir) {
336
568
  isFile: entry.isFile(),
337
569
  }));
338
570
  }
571
+ function buildDependencyDeclarations(drawn, depGraph, sourcePath) {
572
+ if (drawn.consumes.length === 0)
573
+ return Object.freeze([]);
574
+ const declarations = [];
575
+ for (const consume of drawn.consumes) {
576
+ declarations.push(buildOneDependencyDeclaration(consume, depGraph, sourcePath));
577
+ }
578
+ return Object.freeze(declarations);
579
+ }
580
+ function buildOneDependencyDeclaration(consume, depGraph, sourcePath) {
581
+ const ref = consume.producerRef;
582
+ const outputsCopy = consume.outputs.map((o) => Object.freeze({ title: o.title, kind: o.kind }));
583
+ /* v8 ignore next */
584
+ if (ref.kind !== "same-file")
585
+ return buildCrossFileDeclaration(consume, ref, outputsCopy);
586
+ const isDrawn = depGraph.drawn.some((d) => d.bindingName === ref.bindingName && d.exportName !== "default");
587
+ const exportName = depGraph.drawn.find((d) => d.bindingName === ref.bindingName)?.exportName ??
588
+ ref.bindingName;
589
+ return Object.freeze({
590
+ localId: consume.localId,
591
+ producerName: ref.bindingName,
592
+ producerSourcePath: sourcePath,
593
+ producerExportName: exportName,
594
+ effectiveInputs: Object.freeze({ ...consume.effectiveInputs }),
595
+ outputs: Object.freeze(outputsCopy),
596
+ isDrawn,
597
+ });
598
+ }
599
+ /* v8 ignore start */
600
+ function buildCrossFileDeclaration(consume, ref, outputsCopy) {
601
+ return Object.freeze({
602
+ localId: consume.localId,
603
+ producerName: ref.exportName,
604
+ producerSourcePath: ref.sourcePath,
605
+ producerExportName: ref.exportName,
606
+ effectiveInputs: Object.freeze({ ...consume.effectiveInputs }),
607
+ outputs: Object.freeze(outputsCopy),
608
+ isDrawn: false,
609
+ });
610
+ }
611
+ /* v8 ignore stop */
339
612
  function stripWriteFlag(opts) {
340
- const { apiVersion, sourcePath, sourcemap, minify, target, declaredIntervals } = opts;
613
+ const { apiVersion, sourcePath, sourcemap, minify, target, declaredIntervals, resolveProducer, rootDir, } = opts;
341
614
  const out = { apiVersion };
342
615
  if (sourcePath !== undefined)
343
616
  out.sourcePath = sourcePath;
@@ -349,6 +622,182 @@ function stripWriteFlag(opts) {
349
622
  out.target = target;
350
623
  if (declaredIntervals !== undefined)
351
624
  out.declaredIntervals = declaredIntervals;
625
+ if (resolveProducer !== undefined)
626
+ out.resolveProducer = resolveProducer;
627
+ if (rootDir !== undefined)
628
+ out.rootDir = rootDir;
352
629
  return out;
353
630
  }
631
+ /**
632
+ * Lower each cross-file `import <name> from "./X.chart"` line in the
633
+ * consumer's printed TS source to `const <name> = __producer_<hash>__default;`
634
+ * so the inlined producer's local binding wires into the consumer's body.
635
+ * Imports of unresolved specifiers (no entry in `specifierToHash`) stay
636
+ * as-is so esbuild surfaces the resolution failure.
637
+ *
638
+ * Phase 1 only supports the default-import form — named imports remain
639
+ * untouched and will be addressed when same-file named-export composition
640
+ * crosses file boundaries.
641
+ *
642
+ * @since 0.7
643
+ */
644
+ function rewriteConsumerChartImports(source, specifierToHash) {
645
+ if (specifierToHash.size === 0)
646
+ return source;
647
+ return source.replace(/^\s*import\s+([A-Za-z_$][A-Za-z0-9_$]*)\s+from\s+(['"])([^'"]+)\2;\s*$/gm, (match, name, _quote, specifier) => {
648
+ const hash = specifierToHash.get(specifier);
649
+ /* v8 ignore next */
650
+ if (hash === undefined)
651
+ return match;
652
+ return `const ${name} = __producer_${hash}__default;`;
653
+ });
654
+ }
655
+ /**
656
+ * Pre-scan a `.chart.ts` source for `.chart.ts` / `.chart` import
657
+ * specifiers using a one-pass AST walk. Returns a deduplicated array of
658
+ * specifiers in source-declaration order. The list feeds `compile`'s
659
+ * async resolver before `transformAndAnalyse` runs so the analysis pass
660
+ * can resolve cross-file producer snapshots synchronously.
661
+ *
662
+ * @since 0.7
663
+ */
664
+ function preScanChartImports(source, sourcePath) {
665
+ const sourceFile = ts.createSourceFile(sourcePath, source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TS);
666
+ const specifiers = [];
667
+ const seen = new Set();
668
+ for (const statement of sourceFile.statements) {
669
+ if (!ts.isImportDeclaration(statement))
670
+ continue;
671
+ const specifier = statement.moduleSpecifier;
672
+ /* v8 ignore next 3 */
673
+ if (!ts.isStringLiteral(specifier)) {
674
+ continue;
675
+ }
676
+ const text = specifier.text;
677
+ if (!text.endsWith(".chart") && !text.endsWith(".chart.ts"))
678
+ continue;
679
+ /* v8 ignore next 3 */
680
+ if (seen.has(text)) {
681
+ continue;
682
+ }
683
+ seen.add(text);
684
+ specifiers.push(text);
685
+ }
686
+ return Object.freeze(specifiers);
687
+ }
688
+ /**
689
+ * Build a per-`compile` cross-file resolver rooted at the consumer's
690
+ * directory. `compileProject` overrides this by sharing a single
691
+ * resolver across every file so the inline-once invariant holds.
692
+ *
693
+ * @since 0.7
694
+ */
695
+ function createDefaultProducerResolver(sourcePath, opts) {
696
+ let absSourcePath;
697
+ /* v8 ignore next 3 */
698
+ if (isAbsolute(sourcePath)) {
699
+ absSourcePath = sourcePath;
700
+ }
701
+ else {
702
+ absSourcePath = resolvePath(process.cwd(), sourcePath);
703
+ }
704
+ const rootDir = opts.rootDir ?? dirname(absSourcePath);
705
+ const resolver = createProducerResolver({ rootDir }, (source, producerSourcePath) => compileProducerArtefacts(source, producerSourcePath, resolver));
706
+ return resolver;
707
+ }
708
+ /**
709
+ * Compile one producer file for the resolver. Pre-scans its imports,
710
+ * resolves them recursively (so transitive producers populate the
711
+ * cache before we hand control back), runs the producer's own
712
+ * `compile` + `transformAndAnalyse`, and returns the artefacts the
713
+ * resolver wraps into a {@link ProducerCompiled} snapshot.
714
+ *
715
+ * @since 0.7
716
+ */
717
+ async function compileProducerArtefacts(source, producerSourcePath, resolver) {
718
+ try {
719
+ // Pre-resolve the producer's own cross-file deps so the
720
+ // recursive `compile` can pull the matching snapshot from the
721
+ // shared resolver's cache instead of compiling twice.
722
+ const preScan = preScanChartImports(source, producerSourcePath);
723
+ const nested = await Promise.all(preScan.map(async (specifier) => ({
724
+ specifier,
725
+ compiled: await resolver(specifier, producerSourcePath),
726
+ })));
727
+ const result = await compile(source, {
728
+ apiVersion: 1,
729
+ sourcePath: producerSourcePath,
730
+ resolveProducer: resolver,
731
+ });
732
+ const transformAndAnalyseResult = transformAndAnalyse(source, {
733
+ sourcePath: producerSourcePath,
734
+ resolveProducer: buildSyncSnapshotResolver(nested),
735
+ });
736
+ const transformedSource = PRINTER.printFile(transformAndAnalyseResult.transformed);
737
+ const transitiveProducers = [];
738
+ const specifierToHash = new Map();
739
+ for (const { specifier, compiled } of nested) {
740
+ /* v8 ignore next */
741
+ if (compiled === null)
742
+ continue;
743
+ transitiveProducers.push(compiled);
744
+ specifierToHash.set(specifier, compiled.hash);
745
+ }
746
+ let siblings;
747
+ if (transformAndAnalyseResult.siblings === undefined) {
748
+ siblings = Object.freeze([]);
749
+ /* v8 ignore next 3 */
750
+ }
751
+ else {
752
+ siblings = transformAndAnalyseResult.siblings;
753
+ }
754
+ return Object.freeze({
755
+ moduleSource: result.moduleSource,
756
+ transformedSource,
757
+ manifest: result.manifest,
758
+ siblings,
759
+ transitiveProducers: Object.freeze(transitiveProducers),
760
+ specifierToHash,
761
+ });
762
+ /* v8 ignore next 3 */
763
+ }
764
+ catch {
765
+ return null;
766
+ }
767
+ }
768
+ /**
769
+ * Build a sync snapshot resolver from a pre-resolved list of cross-file
770
+ * producers. Returns the producer manifest's `ProducerSnapshot` shape
771
+ * `transformAndAnalyse` calls when walking consumer-side
772
+ * `<binding>.output("title")` references.
773
+ */
774
+ function buildSyncSnapshotResolver(nested) {
775
+ const bySpecifier = new Map();
776
+ for (const entry of nested) {
777
+ if (entry.compiled !== null)
778
+ bySpecifier.set(entry.specifier, entry.compiled);
779
+ }
780
+ return (modSpec, expName) => {
781
+ const compiled = bySpecifier.get(modSpec);
782
+ /* v8 ignore next 3 */
783
+ if (compiled === undefined) {
784
+ return null;
785
+ }
786
+ const manifest = compiled.drawnByExportName.get(expName);
787
+ /* v8 ignore next 3 */
788
+ if (manifest === undefined) {
789
+ return null;
790
+ }
791
+ /* v8 ignore next */
792
+ const outputs = manifest.outputs ?? [];
793
+ return Object.freeze({
794
+ name: manifest.name,
795
+ outputs: Object.freeze(outputs.map((o) => Object.freeze({ title: o.title, kind: o.kind }))),
796
+ inputs: Object.fromEntries(Object.entries(manifest.inputs).map(([key, descriptor]) => [
797
+ key,
798
+ descriptor,
799
+ ])),
800
+ });
801
+ };
802
+ }
354
803
  //# sourceMappingURL=api.js.map