diff --git a/packages/adapter-vite/__tests__/compiler-source-map-v3.test.ts b/packages/adapter-vite/__tests__/compiler-source-map-v3.test.ts
new file mode 100644
index 000000000..f5383676a
--- /dev/null
+++ b/packages/adapter-vite/__tests__/compiler-source-map-v3.test.ts
@@ -0,0 +1,438 @@
+/**
+ * @openelement/adapter-vite — real Source Map v3 emission for compiled
+ * elements (#1210, A10.2).
+ *
+ * These tests consume the compiler's source map through a STANDARD source-map
+ * consumer (@jridgewell/trace-mapping, already in the dependency tree) instead
+ * of asserting snapshot equality of the mappings string. Every assertion
+ * resolves a position in the generated module back to an independently
+ * computed file/line/column in the authored TSX source.
+ */
+
+import { assert, assertEquals, assertNotEquals } from '@std/assert';
+import { eachMapping, originalPositionFor, TraceMap } from 'npm:@jridgewell/trace-mapping@0.3.31';
+import type { Plugin } from 'vite';
+import {
+ CompiledElementError,
+ compileElementProgram,
+} from '../src/internal/compiler/semantic-core/compile.ts';
+import { compileElementModule } from '../src/internal/compiler/plugin.ts';
+import { createOpenPlugin } from '../src/plugin.ts';
+
+const FILE = '/project/app/components/map-fixture.tsx';
+
+/**
+ * Fixture grammar (one construct per line where possible so positions are
+ * independently computable):
+ * - decorator, verbatim import, island-config passthrough
+ * - plain/computed/multiline-initializer/boolean/array properties
+ * - two methods sharing an identical `this.count++;` line (one method carries
+ * the same line twice)
+ * - attr sink, JSX text part, prop sink, bool sink, method handler
+ * - two arrow handlers with identical `this.count++;` bodies (the MANDATORY
+ * duplicate-location vector)
+ * - a conditional (when) region and a keyed each region at nested tree paths
+ */
+const SOURCE = `import { computed, element, OpenElement, property } from '@openelement/element';
+import { defineIslandConfig as island } from '@openelement/app';
+export const openElement = island({ hydrate: 'load', ssr: true, dsd: false });
+
+@element('oe-map-fixture')
+export class MapFixture extends OpenElement {
+ @property({ reflect: true }) count = 0;
+ @property({ reflect: false }) label = 'ready';
+ @property({ reflect: false, attribute: false }) doubled = computed(() => this.count * 2);
+ @property({ reflect: false }) config = {
+ step: 1,
+ parity: 'even',
+ };
+ @property({ reflect: false }) busy = false;
+ @property({ reflect: false }) items: Array<{ id: string; text: string }> = [];
+
+ bump(): void {
+ this.count++;
+ }
+ bumpTwice(): void {
+ this.count++;
+ this.count++;
+ }
+
+ render() {
+ return (
+
+
Count: {this.count}
+
+
+
+
+ {this.count > 0 ?
positive
:
zero
}
+
{this.items.map((item) => - {item.text}
)}
+
+ );
+ }
+}
+`;
+
+/** 1-based line / 0-based column of the nth (1-based) occurrence of needle. */
+function positionOf(
+ haystack: string,
+ needle: string,
+ occurrence = 1,
+): { line: number; column: number } {
+ let from = 0;
+ let offset = -1;
+ for (let index = 0; index < occurrence; index++) {
+ offset = haystack.indexOf(needle, from);
+ assert(offset >= 0, `needle occurrence ${occurrence} not found: ${needle}`);
+ from = offset + needle.length;
+ }
+ const before = haystack.slice(0, offset);
+ return {
+ line: before.split('\n').length,
+ column: offset - (before.lastIndexOf('\n') + 1),
+ };
+}
+
+function decodeInlineMap(code: string): Record {
+ const marker = '//# sourceMappingURL=data:application/json;base64,';
+ const line = code.split('\n').find((candidate) => candidate.startsWith(marker));
+ assert(line, 'generated code must embed an inline base64 source map');
+ return JSON.parse(
+ new TextDecoder().decode(
+ Uint8Array.from(atob(line.slice(marker.length)), (c) => c.charCodeAt(0)),
+ ),
+ );
+}
+
+function traceOf(map: unknown): TraceMap {
+ return new TraceMap(map as ConstructorParameters[0]);
+}
+
+/** Resolve the nth occurrence of needle in the generated code to the source. */
+function resolve(
+ trace: TraceMap,
+ code: string,
+ needle: string,
+ occurrence = 1,
+ columnNeedle?: string,
+) {
+ const position = positionOf(code, needle, occurrence);
+ const lineText = code.split('\n')[position.line - 1];
+ // Segments sit at the first mapped token of a line: query at the column of
+ // columnNeedle when given, otherwise at the needle's first non-space char.
+ const column = columnNeedle !== undefined
+ ? lineText.indexOf(columnNeedle)
+ : position.column + (needle.length - needle.trimStart().length);
+ return originalPositionFor(trace, { line: position.line, column });
+}
+
+/** Independently computed authored-source position of a needle. */
+function expectSource(needle: string, occurrence = 1, columnNeedle?: string) {
+ const position = positionOf(SOURCE, needle, occurrence);
+ return {
+ source: FILE,
+ line: position.line,
+ column: columnNeedle === undefined
+ ? position.column
+ : SOURCE.split('\n')[position.line - 1].indexOf(columnNeedle),
+ };
+}
+
+function assertResolves(
+ trace: TraceMap,
+ code: string,
+ needle: string,
+ occurrence: number,
+ expected: { source: string; line: number; column: number },
+ columnNeedle?: string,
+) {
+ const resolved = resolve(trace, code, needle, occurrence, columnNeedle);
+ assertEquals(
+ { source: resolved.source, line: resolved.line, column: resolved.column },
+ expected,
+ `generated "${needle}" (occurrence ${occurrence}) must resolve to ${expected.source}:${expected.line}:${expected.column}`,
+ );
+}
+
+Deno.test('A10.2 compiler emits a REAL Source Map v3 (VLQ line+column segments)', () => {
+ const { code, map, program } = compileElementProgram(SOURCE, FILE);
+
+ // The old substitute (mappings: '') is gone; a standard consumer decodes
+ // real segments from both the returned map and the inline artifact map.
+ assertEquals(typeof map.mappings, 'string');
+ assertNotEquals(map.mappings, '', 'mappings must carry real VLQ segments');
+ assertEquals(map.version, 3);
+ assertEquals(map.sources, [FILE]);
+ assertEquals(map.sourcesContent, [SOURCE]);
+ assertEquals(decodeInlineMap(code), map as unknown as Record);
+
+ // x_openElement survives only as SUPPLEMENTARY metadata next to real
+ // segments, deep-equal to the Part Program's provenance records (#1209).
+ assertEquals(map.x_openElement, program.sourceMap);
+
+ const trace = traceOf(map);
+ const decoded: Array = [];
+ eachMapping(trace, (mapping) => decoded.push(mapping));
+ assert(decoded.length >= 30, `expected a dense segment table, decoded ${decoded.length}`);
+});
+
+Deno.test('A10.2 standard consumer resolves every program source record to its authored span', () => {
+ const { code, map, program } = compileElementProgram(SOURCE, FILE);
+ const trace = traceOf(map);
+
+ // Locate each record's serialized entry inside the embedded program JSON
+ // (records serialize in order under the "records" key) and resolve that
+ // generated position through the generic consumer. This sweeps decorators'
+ // program payload, properties, JSX elements, text parts, attribute sinks,
+ // boolean/property sinks, handlers, conditional and keyed regions at nested
+ // tree paths — everything the compiler records provenance for.
+ let cursor = code.indexOf('"records": [');
+ assert(cursor >= 0, 'generated code must embed the program source records');
+ for (const record of program.sourceMap.records) {
+ const idNeedle = `"id": ${JSON.stringify(record.id)}`;
+ const offset = code.indexOf(idNeedle, cursor);
+ assert(offset >= 0, `generated program JSON must carry record ${record.id}`);
+ cursor = offset + idNeedle.length;
+ const before = code.slice(0, offset);
+ const line = before.split('\n').length;
+ const column = offset - (before.lastIndexOf('\n') + 1);
+ const resolved = originalPositionFor(trace, { line, column });
+ assertEquals(
+ { source: resolved.source, line: resolved.line, column: resolved.column },
+ {
+ source: FILE,
+ line: record.source.start.line,
+ column: record.source.start.column - 1,
+ },
+ `record ${record.id} must resolve to its authored source span start`,
+ );
+ }
+});
+
+Deno.test('A10.2 module scaffolding resolves to authored constructs', () => {
+ const { code, map } = compileElementProgram(SOURCE, FILE);
+ const trace = traceOf(map);
+
+ // Verbatim/rewritten imports resolve to the authored import statements.
+ assertResolves(trace, code, `import { computed, OpenElement } from '@openelement/element';`, 1, {
+ source: FILE,
+ line: 1,
+ column: 0,
+ });
+ assertResolves(
+ trace,
+ code,
+ `import { defineIslandConfig as island } from '@openelement/app';`,
+ 1,
+ {
+ source: FILE,
+ line: 2,
+ column: 0,
+ },
+ );
+ // Island-config passthrough is copied verbatim.
+ assertResolves(
+ trace,
+ code,
+ `export const openElement = island(`,
+ 1,
+ expectSource(`export const openElement = island(`),
+ );
+ // The embedded program payload resolves to the render() that produced it;
+ // the serialized tag resolves to the @element decorator.
+ assertResolves(trace, code, 'const __partProgram = {', 1, expectSource('render() {'));
+ assertResolves(
+ trace,
+ code,
+ '"tag": "oe-map-fixture"',
+ 1,
+ expectSource("@element('oe-map-fixture')"),
+ );
+ // Generated helper/scaffold consts resolve to the class they describe.
+ assertResolves(
+ trace,
+ code,
+ 'const __compiledProperties = ',
+ 1,
+ expectSource('MapFixture extends'),
+ );
+ assertResolves(trace, code, 'const __compiledProps = {', 1, expectSource('MapFixture extends'));
+ // The generated class declaration resolves to the authored class name.
+ assertResolves(
+ trace,
+ code,
+ 'export class MapFixture extends OpenElement {',
+ 1,
+ expectSource('export class MapFixture extends OpenElement {', 1, 'MapFixture'),
+ 'MapFixture',
+ );
+});
+
+Deno.test('A10.2 properties, computed fields, methods and multiline initializers resolve', () => {
+ const { code, map } = compileElementProgram(SOURCE, FILE);
+ const trace = traceOf(map);
+
+ // Field declarations resolve to the authored field name (column-exact).
+ assertResolves(trace, code, ' count = 0;', 1, expectSource('count = 0;'));
+ assertResolves(trace, code, ` label = 'ready';`, 1, expectSource(`label = 'ready';`));
+ // The same fields inside the generated __compiledProps helper resolve too.
+ assertResolves(trace, code, ' count: { type: Number', 1, expectSource('count = 0;'));
+ // Computed field factory resolves to the authored computed() initializer.
+ assertResolves(
+ trace,
+ code,
+ 'doubled: (__s) => computed(() => __s.count.value * 2),',
+ 1,
+ expectSource('computed(() => this.count * 2)'),
+ '(__s)',
+ );
+ // Methods resolve to the authored method start.
+ assertResolves(trace, code, ' bump(): void {', 1, expectSource('bump(): void {'));
+ // Multiline initializer: continuation lines in BOTH generated copies (the
+ // __compiledProps default and the class field) resolve to the authored
+ // continuation line/column.
+ assertResolves(trace, code, ' step: 1,', 1, expectSource('step: 1,'));
+ assertResolves(trace, code, ' step: 1,', 2, expectSource('step: 1,'));
+ assertResolves(trace, code, ` parity: 'even',`, 2, expectSource(`parity: 'even',`));
+});
+
+Deno.test('A10.2 identical repeated source lines map to their DISTINCT authored locations', () => {
+ const { code, map } = compileElementProgram(SOURCE, FILE);
+ const trace = traceOf(map);
+
+ // Three identical method-body lines resolve to three distinct authored lines.
+ const first = resolve(trace, code, 'this.count++;', 1);
+ const second = resolve(trace, code, 'this.count++;', 2);
+ const third = resolve(trace, code, 'this.count++;', 3);
+ assertEquals(
+ { source: first.source, line: first.line, column: first.column },
+ expectSource('this.count++;', 1),
+ );
+ assertEquals(
+ { source: second.source, line: second.line, column: second.column },
+ expectSource('this.count++;', 2),
+ );
+ assertEquals(
+ { source: third.source, line: third.line, column: third.column },
+ expectSource('this.count++;', 3),
+ );
+ assertNotEquals(first.line, second.line);
+ assertNotEquals(second.line, third.line);
+});
+
+Deno.test('A10.2 MANDATORY: two generated `this.count++;` event handlers map to two distinct arrows', () => {
+ const { code, map } = compileElementProgram(SOURCE, FILE);
+ const trace = traceOf(map);
+
+ // The two generated handler lines are byte-identical; each must map back to
+ // ITS OWN authored arrow function (line AND column), not first-match-wins.
+ const first = resolve(trace, code, '__compiledEvent0(): void { this.count++; }', 1);
+ const second = resolve(trace, code, '__compiledEvent1(): void { this.count++; }', 1);
+ const firstArrow = positionOf(SOURCE, '() => this.count++', 1);
+ const secondArrow = positionOf(SOURCE, '() => this.count++', 2);
+ assertEquals({ source: first.source, line: first.line, column: first.column }, {
+ source: FILE,
+ line: firstArrow.line,
+ column: firstArrow.column,
+ });
+ assertEquals({ source: second.source, line: second.line, column: second.column }, {
+ source: FILE,
+ line: secondArrow.line,
+ column: secondArrow.column,
+ });
+ assertNotEquals(
+ first.line,
+ second.line,
+ 'duplicate handler bodies must not collapse to one line',
+ );
+});
+
+Deno.test('A10.2 segments carry original identifier names where the compiler knows them', () => {
+ const { code, map } = compileElementProgram(SOURCE, FILE);
+ const trace = traceOf(map);
+ assert(map.names.includes('count'), 'names table must carry authored identifiers');
+ assert(map.names.includes('bump'), 'names table must carry authored method names');
+ const resolved = resolve(trace, code, ' count = 0;', 1);
+ assertEquals(resolved.name, 'count');
+});
+
+Deno.test('A10.2 diagnostics keep pointing at authored positions', () => {
+ const nested = `import { element, OpenElement, property } from '@openelement/element';
+@element('oe-nested-region')
+export class NestedRegion extends OpenElement {
+ @property({ reflect: true }) count = 0;
+ render() {
+ return (
+
+ {this.count > 0 ?
{this.count > 1 ? many : one}
:
zero
}
+
+ );
+ }
+}
+`;
+ const file = '/project/app/components/nested-region.tsx';
+ let caught: unknown;
+ try {
+ compileElementProgram(nested, file);
+ } catch (error) {
+ caught = error;
+ }
+ assert(caught instanceof CompiledElementError, 'nested regions must fail closed');
+ const diagnostic = caught.diagnostics[0];
+ assertEquals(diagnostic.code, 'OEC9012');
+ const expected = positionOf(nested, '{this.count > 1 ?');
+ assertEquals(diagnostic.file, file);
+ assertEquals(diagnostic.line, expected.line);
+ assertEquals(diagnostic.character, expected.column + 1);
+});
+
+Deno.test('A10.2 Vite boundary: open:core hands the real map to Vite without a double map story', () => {
+ // compileElementModule (the Vite-bound entrypoint) returns the real map.
+ const result = compileElementModule(SOURCE, FILE);
+ assert(result, 'fixture must be admitted by the compiler gate');
+ assertNotEquals(result.map.mappings, '');
+ assertEquals(result.map.x_openElement, result.program.sourceMap);
+ const trace = traceOf(result.map);
+ const resolved = resolve(trace, result.code, '__compiledEvent1(): void { this.count++; }', 1);
+ const secondArrow = positionOf(SOURCE, '() => this.count++', 2);
+ assertEquals({ source: resolved.source, line: resolved.line, column: resolved.column }, {
+ source: FILE,
+ line: secondArrow.line,
+ column: secondArrow.column,
+ });
+
+ // The open:core transform returns the real map object as its `map` output
+ // (Vite composes downstream) and strips the inline comment from the served
+ // code so there is exactly one map story at the plugin boundary.
+ const core = createOpenPlugin().find((plugin: Plugin) => plugin.name === 'open:core');
+ assert(core, 'open:core plugin must be registered');
+ const transform = core.transform as unknown as (
+ this: { error(message: string): never },
+ code: string,
+ id: string,
+ ) => { code: string; map?: { mappings: string } } | string | null;
+ const transformed = transform.call(
+ {
+ error(message: string): never {
+ throw new Error(message);
+ },
+ },
+ SOURCE,
+ FILE,
+ );
+ assert(transformed !== null && typeof transformed === 'object', 'open:core must return code+map');
+ assertEquals(
+ transformed.code.includes('sourceMappingURL'),
+ false,
+ 'no inline map comment may survive the boundary',
+ );
+ assert(transformed.map, 'open:core must return the real map object');
+ assertNotEquals(transformed.map!.mappings, '');
+ const boundaryTrace = traceOf(transformed.map);
+ const viaPlugin = resolve(boundaryTrace, transformed.code, ' count = 0;', 1);
+ assertEquals({ source: viaPlugin.source, line: viaPlugin.line, column: viaPlugin.column }, {
+ source: FILE,
+ line: positionOf(SOURCE, 'count = 0;').line,
+ column: positionOf(SOURCE, 'count = 0;').column,
+ });
+});
diff --git a/packages/adapter-vite/__tests__/v044-delivery/island-delivery.test.ts b/packages/adapter-vite/__tests__/v044-delivery/island-delivery.test.ts
index 49de050cc..f522f7ed8 100644
--- a/packages/adapter-vite/__tests__/v044-delivery/island-delivery.test.ts
+++ b/packages/adapter-vite/__tests__/v044-delivery/island-delivery.test.ts
@@ -1,10 +1,7 @@
import { assertEquals, assertStringIncludes, assertThrows } from '@std/assert';
import { join } from 'node:path';
import { buildCriticalHeadExtras } from '../../src/internal/ssg/critical-assets.ts';
-import {
- compiledElementPlugin,
- createCompiledElementSourceMap,
-} from '../../src/internal/compiler/plugin.ts';
+import { compiledElementPlugin, compileElementModule } from '../../src/internal/compiler/plugin.ts';
import { generateClientEntry } from '../../src/internal/ssg/entry-client-codegen.ts';
import { createIslandScheduler } from '../../src/internal/ssg/island-scheduler.ts';
import { readIslandConfig } from '../../src/internal/ssg/island-scanner.ts';
@@ -204,22 +201,21 @@ Deno.test('v0.44 SSR admission expands one capability declaration per delivered
});
Deno.test('v0.44 compiler source records pass through the Vite source map', () => {
- const records = {
- version: 1,
- file: '/src/clock.tsx',
- records: [{
- id: 'root',
- kind: 'root',
- source: { start: { offset: 0, line: 1, column: 1 }, end: { offset: 1, line: 1, column: 2 } },
- }],
- };
- const map = createCompiledElementSourceMap(
- 'const source = true;',
- 'const generated = true;',
- '/src/clock.tsx',
- { sourceMap: records },
- );
- assertEquals(map.x_openElement, records);
+ // A10.2 (#1210): the map returned for Vite composition is the core's real
+ // Source Map v3; the program's v1 provenance records ride along only as
+ // supplementary x_openElement metadata.
+ const source = [
+ "import { element, OpenElement, property } from '@openelement/element';",
+ "@element('oe-clock')",
+ 'export class Clock extends OpenElement {',
+ ' @property({ reflect: true }) count = 0;',
+ ' render() { return {this.count}
; }',
+ '}',
+ ].join('\n');
+ const result = compileElementModule(source, '/src/clock.tsx');
+ assertEquals(result?.map.x_openElement, result?.program.sourceMap);
+ assertEquals(result?.map.sources, ['/src/clock.tsx']);
+ assertEquals(result?.map.sourcesContent, [source]);
});
Deno.test('v0.44 compiler hook transforms once and classifies HMR shape changes', async () => {
diff --git a/packages/adapter-vite/src/internal/compiler/plugin.ts b/packages/adapter-vite/src/internal/compiler/plugin.ts
index 669896d72..ae32d804a 100644
--- a/packages/adapter-vite/src/internal/compiler/plugin.ts
+++ b/packages/adapter-vite/src/internal/compiler/plugin.ts
@@ -31,17 +31,6 @@ import { analyzeModuleSemantics } from './semantic-core/module-analysis.ts';
export const COMPILED_ELEMENT_MARKER = '@element(';
-interface CompiledElementSourceMap {
- version: 3;
- file: string;
- sources: string[];
- sourcesContent: string[];
- names: string[];
- mappings: string;
- /** Compiler-owned Part Program source records carried through Vite. */
- x_openElement?: unknown;
-}
-
/**
* Cheap first stage only — NOT a recognizer. The substring match exists to
* keep plain modules off the AST path and may false-positive (string literals
@@ -84,56 +73,15 @@ export function compileElementModule(code: string, id: string): CompileElementRe
return compileElementProgram(code, id);
}
-function encodeVlq(value: number): string {
- const base64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
- let current = value < 0 ? ((-value) << 1) | 1 : value << 1;
- let encoded = '';
- do {
- let digit = current & 31;
- current >>>= 5;
- if (current > 0) digit |= 32;
- encoded += base64[digit];
- } while (current > 0);
- return encoded;
-}
-
/**
- * Emit a small source map for generated compiler modules. Generated program
- * data has no source position of its own, while copied fields/methods can be
- * mapped back to their source line. Unmapped scaffolding points at line 1 so
- * Vite and stack consumers still have a valid, named source rather than an
- * absent map.
+ * Strip the inline map comment from a compiled module at the Vite boundary.
+ * The core artifact embeds its real Source Map v3 inline for standalone
+ * consumers; the Vite transform returns that same map as its `map` output so
+ * Vite composes it with the rest of the pipeline — leaving the comment in the
+ * served code would create a second, conflicting map story (#1210).
*/
-export function createCompiledElementSourceMap(
- source: string,
- generated: string,
- id: string,
- program?: unknown,
-): CompiledElementSourceMap {
- const sourceLines = source.split(/\r?\n/);
- let previousOriginalLine = 0;
- const mappings = generated.split('\n').map((line) => {
- const text = line.trim();
- const match = text.length > 0
- ? sourceLines.findIndex((sourceLine) => sourceLine.trim() === text)
- : -1;
- const originalLine = match >= 0 ? match : 0;
- const mapping = `AA${encodeVlq(originalLine - previousOriginalLine)}A`;
- previousOriginalLine = originalLine;
- return mapping;
- }).join(';');
- const artifactSourceMap = program && typeof program === 'object'
- ? (program as { sourceMap?: unknown }).sourceMap
- : undefined;
- return {
- version: 3,
- file: id,
- sources: [id],
- sourcesContent: [source],
- names: [],
- mappings,
- ...(artifactSourceMap === undefined ? {} : { x_openElement: artifactSourceMap }),
- };
+export function stripInlineSourceMapComment(code: string): string {
+ return code.replace(/\n\/\/# sourceMappingURL=data:application\/json;base64,[^\n]*(?=\n?$)/, '');
}
export function compiledElementPlugin(): Plugin {
diff --git a/packages/adapter-vite/src/internal/compiler/semantic-core/compile.ts b/packages/adapter-vite/src/internal/compiler/semantic-core/compile.ts
index 3c143b41c..284fcaff8 100644
--- a/packages/adapter-vite/src/internal/compiler/semantic-core/compile.ts
+++ b/packages/adapter-vite/src/internal/compiler/semantic-core/compile.ts
@@ -20,6 +20,7 @@ import {
isCompileTimeOnlyImport,
type ModuleIntrinsicBindings,
} from './module-analysis.ts';
+import { type CompiledElementSourceMap, SourceMapSegmentBuilder } from './source-map.ts';
import {
type CompiledElementMetadata,
type PartProgramV1,
@@ -49,6 +50,13 @@ export class CompiledElementError extends CompilerDiagnosticError {
export interface CompileElementResult {
code: string;
+ /**
+ * Real Source Map v3 for the emitted module (VLQ line+column segments
+ * derived from the compiler's span records and emission provenance). The
+ * same map is embedded as the module's inline map; the Vite shell returns it
+ * as its `map` output for downstream composition (#1210).
+ */
+ map: CompiledElementSourceMap;
program: PartProgramV1;
}
@@ -71,7 +79,7 @@ interface CompiledField {
* source-signal dependencies and emits a `__computedFields` factory over the
* instance's signal record; no field initializer runs on the generated class.
*/
- computed?: { deps: string[]; factoryText: string };
+ computed?: { deps: string[]; factoryText: string; body: ts.Expression };
}
interface GeneratedHandler {
@@ -318,7 +326,7 @@ function parseComputedInitializer(
plainFieldNames: ReadonlySet,
computedFieldNames: ReadonlySet,
fail: (node: ts.Node, code: string, message: string) => never,
-): { deps: string[]; factoryText: string } | null {
+): { deps: string[]; factoryText: string; body: ts.Expression } | null {
const value = unwrapExpression(initializer);
if (!ts.isCallExpression(value)) return null;
const resolution = intrinsics.resolveIntrinsic(value.expression, 'computed');
@@ -394,7 +402,7 @@ function parseComputedInitializer(
`__s.${replacement.name}.value` +
bodyText.slice(replacement.end - offset);
}
- return { deps, factoryText: `(__s) => ${resolution.localName}(() => ${bodyText})` };
+ return { deps, factoryText: `(__s) => ${resolution.localName}(() => ${bodyText})`, body };
}
function inferPropertyType(
@@ -1268,11 +1276,13 @@ function propertyFields(
methods: ts.MethodDeclaration[];
render: ts.MethodDeclaration;
stylesText?: string;
+ stylesNode?: ts.Expression;
} {
const fields: CompiledField[] = [];
const methods: ts.MethodDeclaration[] = [];
let render: ts.MethodDeclaration | null = null;
let stylesText: string | undefined;
+ let stylesNode: ts.Expression | undefined;
const names = new Set();
const propertyAttributeNames = new Set();
for (const member of classNode.members) {
@@ -1297,6 +1307,7 @@ function propertyFields(
fail(member, 'OEC9005', 'compiled classes may declare static styles only once');
}
stylesText = member.initializer.getText(sf);
+ stylesNode = member.initializer;
continue;
}
const accessibilityModifiers = modifiers.filter((modifier) =>
@@ -1531,7 +1542,7 @@ function propertyFields(
);
}
if (!render) fail(classNode, 'OEC9007', 'compiled classes must declare render()');
- return { fields, methods, render, stylesText };
+ return { fields, methods, render, stylesText, stylesNode };
}
function isDeclareStatement(statement: ts.Statement): boolean {
@@ -1659,25 +1670,6 @@ function encodeBase64(value: string): string {
return btoa(binary);
}
-function runtimePropsText(fields: CompiledField[]): string[] {
- const lines = ['const __compiledProps = {'];
- for (const field of fields) {
- if (field.computed) {
- lines.push(
- ` ${field.name}: { type: Object, default: undefined, reflect: false, attribute: false },`,
- );
- continue;
- }
- const attribute = field.attribute === null ? 'false' : JSON.stringify(field.attribute);
- lines.push(
- ` ${field.name}: { type: ${field.typeConstructor}, default: ${field.initializerText}, ` +
- `reflect: ${field.reflect}, attribute: ${attribute} },`,
- );
- }
- lines.push('};');
- return lines;
-}
-
function generatedHandlerText(handler: GeneratedHandler): string {
const action = handler.action;
if (action.kind === 'method') return ` ${handler.name}(): void { this.${action.name}(); }`;
@@ -1716,7 +1708,7 @@ export function compileElementProgram(source: string, fileName: string): Compile
// analysis — no spelling-based recognizer survives in the compiler.
const intrinsics = createModuleIntrinsicBindings(sf);
- const passthroughStatements: string[] = [];
+ const passthroughStatements: ts.Statement[] = [];
for (const statement of sf.statements) {
if (
ts.isImportDeclaration(statement) || ts.isClassDeclaration(statement) ||
@@ -1726,7 +1718,7 @@ export function compileElementProgram(source: string, fileName: string): Compile
// alpha.8: the island delivery policy is the one runtime statement a
// compiled module may carry; it is validated and copied verbatim below.
if (isIslandConfigStatement(statement, intrinsics)) {
- passthroughStatements.push(statement.getText(sf));
+ passthroughStatements.push(statement);
continue;
}
fail(
@@ -1898,7 +1890,12 @@ export function compileElementProgram(source: string, fileName: string): Compile
const openElementLocalName = heritageResolution.localName!;
if (!classNode.name) fail(classNode, 'OEC9003', 'compiled classes must be named');
const className = classNode.name.text;
- const { fields, methods, render, stylesText } = propertyFields(sf, classNode, intrinsics, fail);
+ const { fields, methods, render, stylesText, stylesNode } = propertyFields(
+ sf,
+ classNode,
+ intrinsics,
+ fail,
+ );
const methodNames = methods.map((method) => (method.name as ts.Identifier).text);
const lowering = new Lowering(sf, fields, methodNames);
const renderStatements = render.body?.statements ?? [];
@@ -1977,31 +1974,210 @@ export function compileElementProgram(source: string, fileName: string): Compile
const propertiesJson = JSON.stringify(metadata.properties, null, 2);
const metadataJson = JSON.stringify(metadata, null, 2);
const observedJson = JSON.stringify(metadata.observedAttributes, null, 2);
- const memberLines: string[] = [
- ' static __partProgram = __partProgram;',
- ' static __compiledProperties = __compiledProperties;',
- ' static __elementMetadata = __elementMetadata;',
- ' static props = __compiledProps;',
- ' static observedAttributes = __observedAttributes;',
- ];
- if (delegatesFocus) memberLines.push(' static delegatesFocus = true;');
- if (formAssociated) memberLines.push(' static formAssociated = true;');
+
+ // Emission provenance (#1210, ADR-0148): the semantic core owns both the
+ // original source spans and where each copied/derived construct lands in the
+ // generated module. Every such line records a real Source Map v3 segment
+ // (VLQ line+column, names where known); pure scaffolding stays unmapped so
+ // consumers fall through to the nearest real construct. The emitted module
+ // text is unchanged by this bookkeeping.
+ const segments = new SourceMapSegmentBuilder();
+ const codeLines: string[] = [];
+ const nodePosition = (node: ts.Node): { line: number; column: number } => {
+ const position = sf.getLineAndCharacterOfPosition(node.getStart(sf));
+ return { line: position.line + 1, column: position.character };
+ };
+ const mapLineAt = (
+ generatedLine: number,
+ generatedColumn: number,
+ node: ts.Node,
+ name?: string,
+ ): void => {
+ const position = nodePosition(node);
+ segments.add({
+ generatedLine,
+ generatedColumn,
+ sourceLine: position.line,
+ sourceColumn: position.column,
+ ...(name === undefined ? {} : { name }),
+ });
+ };
+ /**
+ * Continuation lines of a verbatim-copied block: text line i is authored
+ * line (start + i) verbatim, so generated column (prefix + whitespace) maps
+ * to authored column (whitespace) — the first non-whitespace character.
+ */
+ const mapContinuationLines = (
+ text: string,
+ firstGeneratedLine: number,
+ node: ts.Node,
+ prefix: number,
+ ): void => {
+ const position = nodePosition(node);
+ const lines = text.split('\n');
+ for (let index = 1; index < lines.length; index++) {
+ const whitespace = /^\s*/.exec(lines[index])![0].length;
+ segments.add({
+ generatedLine: firstGeneratedLine + index,
+ generatedColumn: prefix + whitespace,
+ sourceLine: position.line + index,
+ sourceColumn: whitespace,
+ });
+ }
+ };
+ /** Push a line or block: multiline text always splits so codeLines.length tracks emitted lines. */
+ const push = (text: string): void => {
+ for (const line of text.split('\n')) codeLines.push(line);
+ };
+ /** Push a verbatim copy of a node's text (optionally line-prefixed). */
+ const pushVerbatim = (text: string, node: ts.Node, prefix = '', name?: string): void => {
+ const firstGeneratedLine = codeLines.length + 1;
+ push(text.split('\n').map((line) => prefix + line).join('\n'));
+ mapLineAt(firstGeneratedLine, prefix.length, node, name);
+ mapContinuationLines(text, firstGeneratedLine, node, prefix.length);
+ };
+ /** Push one synthesized line embedding a verbatim value, mapping both. */
+ const pushDerivedLine = (
+ head: string,
+ valueText: string,
+ tail: string,
+ nameNode: ts.Node,
+ nameColumn: number,
+ name: string,
+ valueNode: ts.Node | undefined,
+ ): void => {
+ const generatedLine = codeLines.length + 1;
+ push(`${head}${valueText}${tail}`);
+ mapLineAt(generatedLine, nameColumn, nameNode, name);
+ if (valueNode !== undefined) {
+ mapLineAt(generatedLine, head.length, valueNode);
+ mapContinuationLines(valueText, generatedLine, valueNode, 0);
+ }
+ };
+ /** Push a synthesized block whose first line traces to `node`. */
+ const pushDerivedBlock = (text: string, node: ts.Node, name?: string): void => {
+ const firstGeneratedLine = codeLines.length + 1;
+ push(text);
+ mapLineAt(firstGeneratedLine, 0, node, name);
+ };
+
+ push('// ');
+ for (const statement of sf.statements) {
+ if (!ts.isImportDeclaration(statement)) continue;
+ const rewritten = rewriteImportForGeneratedModule(sf, statement);
+ // No OpenElement import injection (#1209): heritage provenance is required,
+ // so the canonical binding (possibly aliased) always exists in the source
+ // and is carried by the copied imports above.
+ if (rewritten !== null) pushVerbatim(rewritten, statement);
+ }
+ push('');
+ for (const statement of passthroughStatements) pushVerbatim(statement.getText(sf), statement);
+ if (passthroughStatements.length > 0) push('');
+
+ // The serialized program payload derives from the render() JSX; each source
+ // record's own serialized entry maps to its authored span below.
+ const programStartLine = codeLines.length + 1;
+ pushDerivedBlock(`const __partProgram = ${programJson};`, render);
+ const programJsonPosition = (offset: number): { line: number; column: number } => {
+ const before = programJson.slice(0, offset);
+ return {
+ line: programStartLine + before.split('\n').length - 1,
+ column: offset - (before.lastIndexOf('\n') + 1),
+ };
+ };
+ const tagOffset = programJson.indexOf(`"tag": ${JSON.stringify(tag)}`);
+ if (tagOffset >= 0) {
+ // The compiled tag payload traces to the @element decorator application.
+ const at = programJsonPosition(tagOffset);
+ mapLineAt(at.line, at.column, decorator);
+ }
+ let recordsCursor = programJson.indexOf('"records": [');
+ if (recordsCursor >= 0) {
+ for (const record of program.sourceMap.records) {
+ const needle = `"id": ${JSON.stringify(record.id)}`;
+ const offset = programJson.indexOf(needle, recordsCursor);
+ if (offset < 0) continue; // records always serialize in order; defensive
+ recordsCursor = offset + needle.length;
+ const at = programJsonPosition(offset);
+ segments.add({
+ generatedLine: at.line,
+ generatedColumn: at.column,
+ sourceLine: record.source.start.line,
+ sourceColumn: record.source.start.column - 1,
+ });
+ }
+ }
+ push('');
+ pushDerivedBlock(`const __compiledProperties = ${propertiesJson};`, classNode.name!);
+ push('');
+ pushDerivedBlock(`const __elementMetadata = ${metadataJson};`, classNode.name!);
+ push('');
+ pushDerivedBlock(`const __observedAttributes = ${observedJson};`, classNode.name!);
+ push('');
+
+ pushDerivedBlock('const __compiledProps = {', classNode.name!);
+ for (const field of fields) {
+ if (field.computed) {
+ const computedLine = codeLines.length + 1;
+ push(
+ ` ${field.name}: { type: Object, default: undefined, reflect: false, attribute: false },`,
+ );
+ mapLineAt(computedLine, 2, field.node.name, field.name);
+ continue;
+ }
+ const attribute = field.attribute === null ? 'false' : JSON.stringify(field.attribute);
+ pushDerivedLine(
+ ` ${field.name}: { type: ${field.typeConstructor}, default: `,
+ field.initializerText,
+ `, reflect: ${field.reflect}, attribute: ${attribute} },`,
+ field.node.name,
+ 2,
+ field.name,
+ field.node.initializer,
+ );
+ }
+ push('};');
+ push('');
+
+ const classLine = `export ${
+ isDefaultExport ? 'default ' : ''
+ }class ${className} extends ${openElementLocalName} {`;
+ push(classLine);
+ mapLineAt(codeLines.length, classLine.indexOf(className), classNode.name!, className);
+ push(
+ [
+ ' static __partProgram = __partProgram;',
+ ' static __compiledProperties = __compiledProperties;',
+ ' static __elementMetadata = __elementMetadata;',
+ ' static props = __compiledProps;',
+ ' static observedAttributes = __observedAttributes;',
+ ].join('\n'),
+ );
+ if (delegatesFocus) push(' static delegatesFocus = true;');
+ if (formAssociated) push(' static formAssociated = true;');
const computedFields = fields.filter((field) => field.computed);
if (computedFields.length > 0) {
// Derived-signal factories: each builds the field's read-only computed
// over the instance's plain property signals (facade + renderDsd run the
// same factories, so server output and client claim read one value set).
- memberLines.push(' static __computedFields = {');
+ push(' static __computedFields = {');
for (const field of computedFields) {
- memberLines.push(` ${field.name}: ${field.computed!.factoryText},`);
+ const factoryLine = codeLines.length + 1;
+ push(` ${field.name}: ${field.computed!.factoryText},`);
+ mapLineAt(factoryLine, 4, field.node.name, field.name);
+ mapLineAt(factoryLine, 4 + field.name.length + 2, field.node.initializer!);
+ mapContinuationLines(field.computed!.factoryText, factoryLine, field.computed!.body, 0);
}
- memberLines.push(' };');
+ push(' };');
}
- if (stylesText !== undefined) {
+ if (stylesText !== undefined && stylesNode !== undefined) {
// Copied verbatim: the facade reads static styles into the compiled style
// scope (adoptedStyleSheets on shadow roots, a document-head sink on light
// roots); the serializer inlines them as the marked DSD