@vesk/compiler 0.2.43 → 0.2.44
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/client-codegen.d.ts +8 -0
- package/dist/client-codegen.d.ts.map +1 -1
- package/dist/client-codegen.js +15 -1
- package/dist/errors.d.ts +14 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +30 -9
- package/dist/ir-generator.d.ts.map +1 -1
- package/dist/ir-generator.js +171 -8
- package/dist/server-render.d.ts.map +1 -1
- package/dist/server-render.js +129 -54
- package/dist/ssr-store.d.ts +60 -0
- package/dist/ssr-store.d.ts.map +1 -1
- package/dist/ssr-store.js +157 -6
- package/dist/vsk-collision.d.ts +44 -0
- package/dist/vsk-collision.d.ts.map +1 -0
- package/dist/vsk-collision.js +73 -0
- package/package.json +2 -2
package/dist/client-codegen.d.ts
CHANGED
|
@@ -73,6 +73,14 @@ export declare function compileClientBoth(source: string, _componentName: string
|
|
|
73
73
|
exported: string;
|
|
74
74
|
}>;
|
|
75
75
|
reexportPaths: string[];
|
|
76
|
+
componentNames: string[];
|
|
76
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* The component names a `.vsk` file declares. Used by the bundle to spot
|
|
80
|
+
* same-name declarations across files, which collide on the single global
|
|
81
|
+
* registry. Prefer the free `componentNames` from `compileClientBoth`; this
|
|
82
|
+
* exists for call sites that already have the source but no IR to hand.
|
|
83
|
+
*/
|
|
84
|
+
export declare function vskComponentNames(source: string, sourcePath?: string): string[];
|
|
77
85
|
export { compileClient as compile, isStaticIR };
|
|
78
86
|
//# sourceMappingURL=client-codegen.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-codegen.d.ts","sourceRoot":"","sources":["../src/client-codegen.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAuB,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAGL,cAAc,EAQd,gBAAgB,EASjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAkGxD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;CAC1B;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI1D;AAiDD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAgJlI;AAID,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAU5E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,EAAE,CAuB9F;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAY5F;AAoBD,qDAAqD;AACrD,MAAM,MAAM,SAAS,GAAG;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,SAAS,CAQtE;AAi7DD,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAe3C;AAmJD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAuED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,CAAC,EAAE,SAAS,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAOpQ;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,UAAU,CAAC,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"client-codegen.d.ts","sourceRoot":"","sources":["../src/client-codegen.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAuB,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAGL,cAAc,EAQd,gBAAgB,EASjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAkGxD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;CAC1B;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI1D;AAiDD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAgJlI;AAID,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAU5E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,EAAE,CAuB9F;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAY5F;AAoBD,qDAAqD;AACrD,MAAM,MAAM,SAAS,GAAG;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,SAAS,CAQtE;AAi7DD,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAe3C;AAmJD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAuED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,CAAC,EAAE,SAAS,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAOpQ;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,UAAU,CAAC,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAC;IAAC,cAAc,EAAE,MAAM,EAAE,CAAA;CAAE,CAyC5J;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAG/E;AAED,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC"}
|
package/dist/client-codegen.js
CHANGED
|
@@ -2833,13 +2833,27 @@ export function compileClientBoth(source, _componentName, sourcePath, opts) {
|
|
|
2833
2833
|
? collectVskReexportPaths(ir.reexportSources, sourcePath)
|
|
2834
2834
|
: [];
|
|
2835
2835
|
if (opts?.skipHyd)
|
|
2836
|
-
return { comp, hyd: '', name, aliases, reexportPaths };
|
|
2836
|
+
return { comp, hyd: '', name, aliases, reexportPaths, componentNames: ir.components.map((c) => c.name) };
|
|
2837
2837
|
return {
|
|
2838
2838
|
comp,
|
|
2839
2839
|
hyd: emitClientFromIR(irHyd, { forceClient: true, hydrate: true, includeTopLevel: false, nameAllocator: alloc, markerless: opts?.markerless !== false }),
|
|
2840
2840
|
name,
|
|
2841
2841
|
aliases,
|
|
2842
2842
|
reexportPaths,
|
|
2843
|
+
// Every component NAME this file registers, not just the resolved one —
|
|
2844
|
+
// the bundle needs the full list to spot same-name declarations across
|
|
2845
|
+
// files (see findVskComponentCollisions).
|
|
2846
|
+
componentNames: ir.components.map((c) => c.name),
|
|
2843
2847
|
};
|
|
2844
2848
|
}
|
|
2849
|
+
/**
|
|
2850
|
+
* The component names a `.vsk` file declares. Used by the bundle to spot
|
|
2851
|
+
* same-name declarations across files, which collide on the single global
|
|
2852
|
+
* registry. Prefer the free `componentNames` from `compileClientBoth`; this
|
|
2853
|
+
* exists for call sites that already have the source but no IR to hand.
|
|
2854
|
+
*/
|
|
2855
|
+
export function vskComponentNames(source, sourcePath) {
|
|
2856
|
+
const ast = parse(source, sourcePath ? { filename: sourcePath } : {});
|
|
2857
|
+
return generateIR(ast, source, sourcePath).components.map((c) => c.name);
|
|
2858
|
+
}
|
|
2845
2859
|
export { compileClient as compile, isStaticIR };
|
package/dist/errors.d.ts
CHANGED
|
@@ -22,7 +22,20 @@ export declare class VeskError extends Error {
|
|
|
22
22
|
code?: string;
|
|
23
23
|
constructor(message: string, opts?: VeskErrorOptions);
|
|
24
24
|
static notFound(name: string, candidates?: string[], context?: VeskErrorOptions): VeskError;
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A `.vsk` namespace is resolved at the JSX-tag level: `<ns.Icon />` looks
|
|
27
|
+
* the component up in the registry by its exported name, exactly like
|
|
28
|
+
* `import { Icon }`, and no module object is ever built. The two forms that
|
|
29
|
+
* genuinely need a real module object are reported here.
|
|
30
|
+
*
|
|
31
|
+
* - `nested` — `<ns.Sub.Icon />`. A `.vsk` module's exports are flat
|
|
32
|
+
* component names, so there is no object to walk into.
|
|
33
|
+
* - `value` — `ns.max` read as a value. Component tags resolve; plain value
|
|
34
|
+
* reads do not, because nothing binds `ns` at runtime.
|
|
35
|
+
*/
|
|
36
|
+
static vskNamespaceMember(context?: VeskErrorOptions & {
|
|
37
|
+
form?: 'nested' | 'value';
|
|
38
|
+
}): VeskError;
|
|
26
39
|
static classDecl(context?: VeskErrorOptions): VeskError;
|
|
27
40
|
static serverBlockInClient(compName: string, context?: VeskErrorOptions): VeskError;
|
|
28
41
|
static clientBlockInServer(compName: string, context?: VeskErrorOptions): VeskError;
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAoBxD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,WAAW,GAAE,MAAU,GAAG,MAAM,GAAG,IAAI,CAWrG;AAqCD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,KAAK,SAAI,GAAG,MAAM,CAkBrG;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAgBD,qBAAa,SAAU,SAAQ,KAAK;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEF,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB;IAYxD,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IA8BnG,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAoBxD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,WAAW,GAAE,MAAU,GAAG,MAAM,GAAG,IAAI,CAWrG;AAqCD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,KAAK,SAAI,GAAG,MAAM,CAkBrG;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAgBD,qBAAa,SAAU,SAAQ,KAAK;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEF,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB;IAYxD,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IA8BnG;;;;;;;;;;OAUG;IACH,MAAM,CAAC,kBAAkB,CAAC,OAAO,GAAE,gBAAgB,GAAG;QAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAA;KAAO,GAAG,SAAS;IA6BpG,MAAM,CAAC,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAoB3D,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAmBvF,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAmBvF,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAO3G,MAAM,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAYvG,MAAM,CAAC,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAmB/G,MAAM,CAAC,cAAc,CAAC,OAAO,GAAE,gBAAgB,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,SAAS;IAmBpF,QAAQ,IAAI,MAAM;IA0BlB,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAclC"}
|
package/dist/errors.js
CHANGED
|
@@ -150,19 +150,40 @@ export class VeskError extends Error {
|
|
|
150
150
|
: '',
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
/**
|
|
154
|
+
* A `.vsk` namespace is resolved at the JSX-tag level: `<ns.Icon />` looks
|
|
155
|
+
* the component up in the registry by its exported name, exactly like
|
|
156
|
+
* `import { Icon }`, and no module object is ever built. The two forms that
|
|
157
|
+
* genuinely need a real module object are reported here.
|
|
158
|
+
*
|
|
159
|
+
* - `nested` — `<ns.Sub.Icon />`. A `.vsk` module's exports are flat
|
|
160
|
+
* component names, so there is no object to walk into.
|
|
161
|
+
* - `value` — `ns.max` read as a value. Component tags resolve; plain value
|
|
162
|
+
* reads do not, because nothing binds `ns` at runtime.
|
|
163
|
+
*/
|
|
164
|
+
static vskNamespaceMember(context = {}) {
|
|
165
|
+
const nested = context.form === 'nested';
|
|
166
|
+
return new VeskError(nested
|
|
167
|
+
? 'A `.vsk` namespace is flat, so `<ns.Sub.Icon />` cannot be resolved.'
|
|
168
|
+
: 'A `.vsk` namespace resolves component tags only, so `ns.value` is not available.', {
|
|
155
169
|
...context,
|
|
156
170
|
code: context.code || 'V0410',
|
|
157
|
-
suggestions:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
171
|
+
suggestions: nested
|
|
172
|
+
? [
|
|
173
|
+
"Import the component by name: import { Icon } from './icons.vsk'",
|
|
174
|
+
"Import from the module that owns Sub: import * as Sub from './sub.vsk'",
|
|
175
|
+
]
|
|
176
|
+
: [
|
|
177
|
+
"Import the value by name: import { MAX } from './constants.vsk'",
|
|
178
|
+
'Move the value into a .ts module and import it from there — those are real ES modules with live bindings.',
|
|
179
|
+
],
|
|
161
180
|
nextSteps: [
|
|
162
|
-
'A `.vsk`
|
|
163
|
-
|
|
181
|
+
'A `.vsk` tag resolves by component name in the global registry, not by module exports, so `import * as ns` + `<ns.Icon />` needs no module object.',
|
|
182
|
+
nested
|
|
183
|
+
? 'Re-export with a flat name (`export { Icon }`) or import the component directly.'
|
|
184
|
+
: 'Re-export the value under a flat name and import it directly, or keep the value in a .ts module.',
|
|
164
185
|
],
|
|
165
|
-
tip: '
|
|
186
|
+
tip: '`<ns.Icon />` works; only nested paths and value reads need a real module namespace.',
|
|
166
187
|
});
|
|
167
188
|
}
|
|
168
189
|
static classDecl(context = {}) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAsBP,MAAM,uBAAuB,CAAC;AA2T/B,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,cAAc,CAAC,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkCnL;
|
|
1
|
+
{"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAsBP,MAAM,uBAAuB,CAAC;AA2T/B,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,cAAc,CAAC,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkCnL;AA6nCD,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAgS9E"}
|
package/dist/ir-generator.js
CHANGED
|
@@ -407,6 +407,26 @@ function jsxTagRootName(nameNode) {
|
|
|
407
407
|
node = node.object;
|
|
408
408
|
return node && node.type === 'JSXIdentifier' ? node.name : '';
|
|
409
409
|
}
|
|
410
|
+
/** The final property of a dotted JSX tag: `NS.A.B` → `B`. */
|
|
411
|
+
function jsxTagMemberName(nameNode) {
|
|
412
|
+
let node = nameNode;
|
|
413
|
+
while (node && node.type === 'JSXMemberExpression') {
|
|
414
|
+
node = node.property;
|
|
415
|
+
if (node && node.type === 'JSXIdentifier')
|
|
416
|
+
return node.name;
|
|
417
|
+
}
|
|
418
|
+
return '';
|
|
419
|
+
}
|
|
420
|
+
/** How many member hops a dotted tag has: `NS.A` → 1, `NS.A.B` → 2. */
|
|
421
|
+
function jsxTagDepth(nameNode) {
|
|
422
|
+
let depth = 0;
|
|
423
|
+
let node = nameNode;
|
|
424
|
+
while (node && node.type === 'JSXMemberExpression') {
|
|
425
|
+
depth++;
|
|
426
|
+
node = node.object;
|
|
427
|
+
}
|
|
428
|
+
return depth;
|
|
429
|
+
}
|
|
410
430
|
// Set for the duration of one `generateIR` call so `processJSXElement` — which
|
|
411
431
|
// is reached from eleven call sites and would otherwise need a context
|
|
412
432
|
// parameter threaded through all of them — can reject `<ns.Tag />` for a
|
|
@@ -414,24 +434,154 @@ function jsxTagRootName(nameNode) {
|
|
|
414
434
|
let currentVskNamespaceLocals = null;
|
|
415
435
|
let currentSourceFile;
|
|
416
436
|
/**
|
|
417
|
-
* Local names bound by `import * as ns from './x.vsk'
|
|
418
|
-
*
|
|
419
|
-
*
|
|
437
|
+
* Local names bound by `import * as ns from './x.vsk'` that are NOT shadowed by
|
|
438
|
+
* any other binding in the file. A `.vsk` namespace is only special when the
|
|
439
|
+
* name still refers to the import: if a parameter or a local shadows it, the
|
|
440
|
+
* tag is an ordinary member expression on a real object and must keep the
|
|
441
|
+
* `calleeExpr` path. Names the file re-binds are therefore dropped here, which
|
|
442
|
+
* also keeps the value-position check from rejecting valid shadowed code.
|
|
420
443
|
*/
|
|
421
444
|
function collectVskNamespaceLocals(ast) {
|
|
422
|
-
const
|
|
445
|
+
const namespaceLocals = new Map();
|
|
423
446
|
for (const node of (ast.body || [])) {
|
|
424
447
|
if (node.type !== 'ImportDeclaration')
|
|
425
448
|
continue;
|
|
426
449
|
if (typeof node.source?.value !== 'string' || !node.source.value.endsWith('.vsk'))
|
|
427
450
|
continue;
|
|
428
451
|
for (const spec of node.specifiers || []) {
|
|
429
|
-
if (spec.type === 'ImportNamespaceSpecifier' && spec.local?.name)
|
|
430
|
-
|
|
452
|
+
if (spec.type === 'ImportNamespaceSpecifier' && spec.local?.name) {
|
|
453
|
+
namespaceLocals.set(spec.local.name, { path: node.source.value, shadowed: false });
|
|
454
|
+
}
|
|
431
455
|
}
|
|
432
456
|
}
|
|
457
|
+
if (namespaceLocals.size === 0)
|
|
458
|
+
return new Set();
|
|
459
|
+
markShadowedBindings(ast, namespaceLocals);
|
|
460
|
+
return new Set([...namespaceLocals].filter(([, v]) => !v.shadowed).map(([k]) => k));
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Flags every namespace local that the file also binds by some other means.
|
|
464
|
+
* The namespace import itself is the only declaration that does NOT shadow —
|
|
465
|
+
* everything else (var/let/const, function and class declarations, any
|
|
466
|
+
* parameter list, catch bindings) means the name is a plain local from here on.
|
|
467
|
+
*/
|
|
468
|
+
function markShadowedBindings(node, namespaceLocals) {
|
|
469
|
+
if (!node || typeof node !== 'object')
|
|
470
|
+
return;
|
|
471
|
+
if (Array.isArray(node)) {
|
|
472
|
+
for (const child of node)
|
|
473
|
+
markShadowedBindings(child, namespaceLocals);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (typeof node.type === 'string') {
|
|
477
|
+
if (node.type === 'ImportNamespaceSpecifier')
|
|
478
|
+
return; // the import itself
|
|
479
|
+
for (const name of declaredBindingNames(node)) {
|
|
480
|
+
const entry = namespaceLocals.get(name);
|
|
481
|
+
if (entry)
|
|
482
|
+
entry.shadowed = true;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
for (const key of Object.keys(node)) {
|
|
486
|
+
if (key === 'loc' || key === 'range' || key === 'start' || key === 'end')
|
|
487
|
+
continue;
|
|
488
|
+
markShadowedBindings(node[key], namespaceLocals);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/** Names a single binding-bearing node introduces. */
|
|
492
|
+
function declaredBindingNames(node) {
|
|
493
|
+
const out = [];
|
|
494
|
+
const pushPattern = (id) => {
|
|
495
|
+
if (!id)
|
|
496
|
+
return;
|
|
497
|
+
if (id.type === 'Identifier') {
|
|
498
|
+
out.push(id.name);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (id.type === 'ObjectPattern') {
|
|
502
|
+
for (const prop of id.properties || []) {
|
|
503
|
+
if (prop.type === 'RestElement')
|
|
504
|
+
pushPattern(prop.argument);
|
|
505
|
+
else
|
|
506
|
+
pushPattern(prop.value);
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (id.type === 'ArrayPattern') {
|
|
511
|
+
for (const el of id.elements || [])
|
|
512
|
+
if (el)
|
|
513
|
+
pushPattern(el.type === 'RestElement' ? el.argument : el);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (id.type === 'AssignmentPattern')
|
|
517
|
+
pushPattern(id.left);
|
|
518
|
+
if (id.type === 'RestElement')
|
|
519
|
+
pushPattern(id.argument);
|
|
520
|
+
};
|
|
521
|
+
switch (node.type) {
|
|
522
|
+
case 'VariableDeclarator':
|
|
523
|
+
pushPattern(node.id);
|
|
524
|
+
break;
|
|
525
|
+
case 'FunctionDeclaration':
|
|
526
|
+
case 'FunctionExpression':
|
|
527
|
+
case 'ArrowFunctionExpression':
|
|
528
|
+
if (node.id)
|
|
529
|
+
pushPattern(node.id);
|
|
530
|
+
for (const param of node.params || [])
|
|
531
|
+
pushPattern(param);
|
|
532
|
+
break;
|
|
533
|
+
case 'ClassDeclaration':
|
|
534
|
+
case 'ClassExpression':
|
|
535
|
+
if (node.id)
|
|
536
|
+
pushPattern(node.id);
|
|
537
|
+
break;
|
|
538
|
+
case 'CatchClause':
|
|
539
|
+
pushPattern(node.param);
|
|
540
|
+
break;
|
|
541
|
+
default:
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
433
544
|
return out;
|
|
434
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Rejects reading a member off a `.vsk` namespace in *value* position. A
|
|
548
|
+
* namespace is resolved only for component tags, so `ns.max` in an expression
|
|
549
|
+
* has nothing bound to it at runtime — say so here instead of failing later
|
|
550
|
+
* with a bare `ReferenceError: ns is not defined`.
|
|
551
|
+
*/
|
|
552
|
+
function assertNoVskNamespaceValue(source, expr) {
|
|
553
|
+
if (!currentVskNamespaceLocals || currentVskNamespaceLocals.size === 0)
|
|
554
|
+
return;
|
|
555
|
+
const hit = findNamespaceValueUse(expr, currentVskNamespaceLocals);
|
|
556
|
+
if (!hit)
|
|
557
|
+
return;
|
|
558
|
+
const { line, column } = offsetToLineCol(source, hit.start ?? 0);
|
|
559
|
+
throw VeskError.vskNamespaceMember({ file: currentSourceFile, line, column, code: codeFrame(source, line, column), form: 'value' });
|
|
560
|
+
}
|
|
561
|
+
function findNamespaceValueUse(node, locals) {
|
|
562
|
+
if (!node || typeof node !== 'object')
|
|
563
|
+
return null;
|
|
564
|
+
if (Array.isArray(node)) {
|
|
565
|
+
for (const child of node) {
|
|
566
|
+
const hit = findNamespaceValueUse(child, locals);
|
|
567
|
+
if (hit)
|
|
568
|
+
return hit;
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
if (typeof node.type !== 'string')
|
|
573
|
+
return null;
|
|
574
|
+
if (node.type === 'MemberExpression' && node.object?.type === 'Identifier' && locals.has(node.object.name))
|
|
575
|
+
return node;
|
|
576
|
+
for (const key of Object.keys(node)) {
|
|
577
|
+
if (key === 'loc' || key === 'range' || key === 'start' || key === 'end')
|
|
578
|
+
continue;
|
|
579
|
+
const hit = findNamespaceValueUse(node[key], locals);
|
|
580
|
+
if (hit)
|
|
581
|
+
return hit;
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
435
585
|
function isHTMLTag(name) {
|
|
436
586
|
return name.length > 0 && name[0] === name[0].toLowerCase();
|
|
437
587
|
}
|
|
@@ -443,6 +593,7 @@ function isMapCall(expr) {
|
|
|
443
593
|
expr.arguments[0].type === 'ArrowFunctionExpression');
|
|
444
594
|
}
|
|
445
595
|
function toExpression(source, expr) {
|
|
596
|
+
assertNoVskNamespaceValue(source, expr);
|
|
446
597
|
return new Expression(getSource(source, expr), [], expr, source);
|
|
447
598
|
}
|
|
448
599
|
function processAttribute(source, attr) {
|
|
@@ -920,12 +1071,24 @@ function processJSXElement(source, element) {
|
|
|
920
1071
|
// registry lookup by dotted string.
|
|
921
1072
|
if (nameNode && nameNode.type === 'JSXMemberExpression') {
|
|
922
1073
|
const root = jsxTagRootName(nameNode);
|
|
923
|
-
|
|
1074
|
+
const isVskNamespace = !!currentVskNamespaceLocals && currentVskNamespaceLocals.has(root);
|
|
1075
|
+
if (isVskNamespace && jsxTagDepth(nameNode) > 1) {
|
|
1076
|
+
// A `.vsk` module's exports are flat component names, so there is no
|
|
1077
|
+
// nested namespace object to walk into.
|
|
924
1078
|
const { line, column } = offsetToLineCol(source, nameNode.start ?? 0);
|
|
925
|
-
throw VeskError.
|
|
1079
|
+
throw VeskError.vskNamespaceMember({ file: currentSourceFile, line, column, code: codeFrame(source, line, column), form: 'nested' });
|
|
926
1080
|
}
|
|
927
1081
|
const { props, spreadProps, slots } = extractProps(source, element);
|
|
928
1082
|
const children = selfClosing ? [] : processJSXChildren(source, element.children || []);
|
|
1083
|
+
if (isVskNamespace) {
|
|
1084
|
+
// `<ns.Icon />` after `import * as ns from './lib.vsk'` resolves to the
|
|
1085
|
+
// same registry entry `import { Icon }` would, keyed by the EXPORTED name.
|
|
1086
|
+
// Deliberately carries no `calleeExpr`: both codegen paths must take the
|
|
1087
|
+
// registry branch so `<ns.Icon />` keeps the "component was not found"
|
|
1088
|
+
// guard and hydrates like any other compiled component. The namespace
|
|
1089
|
+
// object itself is never built.
|
|
1090
|
+
return [new ComponentCall(jsxTagMemberName(nameNode), props, [...children, ...slots.map((s) => new PropSlot(s.name, s.nodes))], spreadProps, element.start)];
|
|
1091
|
+
}
|
|
929
1092
|
return [new ComponentCall(tagName, props, [...children, ...slots.map((s) => new PropSlot(s.name, s.nodes))], spreadProps, element.start, getSource(source, nameNode))];
|
|
930
1093
|
}
|
|
931
1094
|
if (!isHTMLTag(tagName) && selfClosing) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-render.d.ts","sourceRoot":"","sources":["../src/server-render.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,SAAS,EAAE,eAAe,EAAqB,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"server-render.d.ts","sourceRoot":"","sources":["../src/server-render.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,SAAS,EAAE,eAAe,EAAqB,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAuCvJ,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,iBAAiB,CAKhG;AA6DD,wBAAgB,MAAM,CACpB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACnC,QAAQ,GAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAa,EAC3C,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACpC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA0C1B;AAED,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACnC,QAAQ,GAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAa,EAC3C,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACpC,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsF9C;AAoED,KAAK,gBAAgB,GAAG,OAAO,aAAa,EAAE,UAAU,CAAC;AAEzD,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,gBAAgB,EAAE,GAAG,SAAS,GAAG,IAAI,EAC9C,GAAG,CAAC,EAAE,OAAO,aAAa,EAAE,mBAAmB,GAC9C,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,gBAAgB,EAAE,GAAG,SAAS,GAAG,IAAI,EAC9C,GAAG,CAAC,EAAE,OAAO,aAAa,EAAE,mBAAmB,GAC9C,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,iBAAiB,EAC1B,GAAG,CAAC,EAAE,OAAO,aAAa,EAAE,mBAAmB,GAC9C,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED,wBAAsB,GAAG,CACvB,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,EACtB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAO,GACnI,OAAO,CAAC,SAAS,CAAC,CA6EpB;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EACpD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,QAAQ,CAAC,EAAE,kBAAkB,GAAG,IAAI,GACnC,MAAM,EAAE,CAuBV;AAED,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACnC,QAAQ,GAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAa,EAC3C,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAiHjB;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACnC,QAAQ,GAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAa,EAC3C,OAAO,GAAE,eAAoB,GAC5B,cAAc,CAAC,MAAM,CAAC,CAkJxB"}
|
package/dist/server-render.js
CHANGED
|
@@ -10,8 +10,9 @@ import { buildComponentMap } from '@vesk/compiler/src/server-jsgen';
|
|
|
10
10
|
import { transformTopLevelForActions } from '@vesk/compiler/src/actions';
|
|
11
11
|
import { collectVskImportPaths, collectVskReexportPaths, vskRegistryAliases, applyVskRegistryAliases } from '@vesk/compiler/src/vsk-imports';
|
|
12
12
|
import { inlineMdImportsFrom, guessProjectRoots } from '@vesk/compiler/src/md-inline';
|
|
13
|
-
import { withSsrStore, ssrSink } from '@vesk/compiler/src/ssr-store';
|
|
13
|
+
import { withSsrStore, withSsrStoreOf, adoptSsrStore, ssrSink, keepSsrSlot, dropSsrSlot, isSsrSlotLive } from '@vesk/compiler/src/ssr-store';
|
|
14
14
|
import { applyLocalModuleImports } from '@vesk/compiler/src/module-imports';
|
|
15
|
+
import { VskComponentOwners } from '@vesk/compiler/src/vsk-collision';
|
|
15
16
|
// Registry-hoisted sub components are invoked with the caller's `__vesk`,
|
|
16
17
|
// but each component's scope declaration destructures its DEFINING file's
|
|
17
18
|
// top-level bindings. When two `.vsk` files declare the same top-level name
|
|
@@ -22,9 +23,12 @@ function scopedVesk(fn, fallback) {
|
|
|
22
23
|
return fn.__veskScope || fallback;
|
|
23
24
|
}
|
|
24
25
|
export function compileFile(source, options) {
|
|
25
|
-
|
|
26
|
+
const owners = new VskComponentOwners();
|
|
27
|
+
const result = compileFileInternal(source, options?.sourcePath, new Set(), owners);
|
|
28
|
+
owners.report('SSR');
|
|
29
|
+
return result;
|
|
26
30
|
}
|
|
27
|
-
function compileFileInternal(source, sourcePath, seenImportFiles) {
|
|
31
|
+
function compileFileInternal(source, sourcePath, seenImportFiles, owners) {
|
|
28
32
|
if (sourcePath) {
|
|
29
33
|
const dir = dirname(sourcePath);
|
|
30
34
|
source = inlineMdImportsFrom(source, sourcePath, guessProjectRoots(dir));
|
|
@@ -33,6 +37,12 @@ function compileFileInternal(source, sourcePath, seenImportFiles) {
|
|
|
33
37
|
const ir = generateIR(ast, source, sourcePath);
|
|
34
38
|
const componentMap = buildComponentMap(ir, true);
|
|
35
39
|
const ownComponentNames = ir.components.map((c) => c.name);
|
|
40
|
+
// Every component shares one registry, so a name declared in two files
|
|
41
|
+
// resolves to one of them. SSR keeps the FIRST, the client keeps the LAST —
|
|
42
|
+
// report it rather than letting the two sides render different components.
|
|
43
|
+
if (sourcePath && owners)
|
|
44
|
+
for (const n of ownComponentNames)
|
|
45
|
+
owners.claim(n, sourcePath);
|
|
36
46
|
const __vesk = loadRuntimeImports(ir.imports);
|
|
37
47
|
applyLocalModuleImports(__vesk, ir.imports, sourcePath);
|
|
38
48
|
if (sourcePath) {
|
|
@@ -55,7 +65,7 @@ function compileFileInternal(source, sourcePath, seenImportFiles) {
|
|
|
55
65
|
catch {
|
|
56
66
|
continue;
|
|
57
67
|
}
|
|
58
|
-
const sub = compileFileInternal(importedSrc, importPath, seenImportFiles);
|
|
68
|
+
const sub = compileFileInternal(importedSrc, importPath, seenImportFiles, owners);
|
|
59
69
|
for (const [name, fn] of sub.componentMap) {
|
|
60
70
|
if (!componentMap.has(name))
|
|
61
71
|
componentMap.set(name, fn);
|
|
@@ -95,32 +105,39 @@ export function render(source, componentName, props = {}, registry = new Map(),
|
|
|
95
105
|
const targetComp = compiled.ir.components.find((c) => c.name === componentName);
|
|
96
106
|
const isAsyncComp = !!(targetComp && (targetComp.isAsync || targetComp.ssrAwait));
|
|
97
107
|
globalThis.__vsk_ssr = true;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
108
|
+
// Resolve the token INSIDE the render scope. The global is an AsyncLocalStorage
|
|
109
|
+
// accessor (see ssr-store.ts), so outside a scope it reads undefined and any
|
|
110
|
+
// assignment is dropped — reading it first would leave renderToken undefined
|
|
111
|
+
// and silently skip the settle/cleanup below.
|
|
112
|
+
return withSsrStore(() => {
|
|
113
|
+
if (!globalThis.__vsk_ssr_token)
|
|
114
|
+
globalThis.__vsk_ssr_token = Math.random().toString(36).slice(2);
|
|
115
|
+
const renderToken = globalThis.__vsk_ssr_token;
|
|
116
|
+
keepSsrSlot(renderToken);
|
|
117
|
+
if (isAsyncComp) {
|
|
118
|
+
return (async () => {
|
|
119
|
+
let bodyHtml;
|
|
120
|
+
try {
|
|
121
|
+
bodyHtml = await renderFn(props, fullRegistry, scopedVesk(renderFn, __vesk));
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
delete globalThis.__vsk_ssr;
|
|
125
|
+
await settleSsrPromises(renderToken);
|
|
126
|
+
clearSsrCells(renderToken);
|
|
127
|
+
}
|
|
128
|
+
return bodyHtml;
|
|
129
|
+
})();
|
|
130
|
+
}
|
|
131
|
+
let bodyHtml;
|
|
132
|
+
try {
|
|
133
|
+
bodyHtml = renderFn(props, fullRegistry, scopedVesk(renderFn, __vesk));
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
delete globalThis.__vsk_ssr;
|
|
137
|
+
clearSsrCells(renderToken);
|
|
138
|
+
}
|
|
139
|
+
return bodyHtml;
|
|
140
|
+
});
|
|
124
141
|
}
|
|
125
142
|
export function renderPage(source, componentName, props = {}, registry = new Map(), options = {}) {
|
|
126
143
|
resetVskState(!!options.hydrate, options.markerless !== false);
|
|
@@ -151,9 +168,10 @@ export function renderPage(source, componentName, props = {}, registry = new Map
|
|
|
151
168
|
if (!globalThis.__vsk_ssr_token)
|
|
152
169
|
globalThis.__vsk_ssr_token = Math.random().toString(36).slice(2);
|
|
153
170
|
const renderToken = globalThis.__vsk_ssr_token;
|
|
171
|
+
keepSsrSlot(renderToken);
|
|
154
172
|
pruneSsrDataSlots();
|
|
155
173
|
if (targetComp && (targetComp.isAsync || targetComp.ssrAwait)) {
|
|
156
|
-
return
|
|
174
|
+
return (async () => {
|
|
157
175
|
let bodyHtml;
|
|
158
176
|
try {
|
|
159
177
|
bodyHtml = await renderFn(ssrProps, fullRegistry, scopedVesk(renderFn, __vesk));
|
|
@@ -172,11 +190,11 @@ export function renderPage(source, componentName, props = {}, registry = new Map
|
|
|
172
190
|
head: renderHeadHtml(targetComp, ssrProps),
|
|
173
191
|
props: ssrProps
|
|
174
192
|
};
|
|
175
|
-
})()
|
|
193
|
+
})();
|
|
176
194
|
}
|
|
177
195
|
let bodyHtml;
|
|
178
196
|
try {
|
|
179
|
-
bodyHtml =
|
|
197
|
+
bodyHtml = renderFn(ssrProps, fullRegistry, scopedVesk(renderFn, __vesk));
|
|
180
198
|
}
|
|
181
199
|
finally {
|
|
182
200
|
delete globalThis.__vsk_ssr;
|
|
@@ -186,11 +204,11 @@ export function renderPage(source, componentName, props = {}, registry = new Map
|
|
|
186
204
|
if (pending && pending.length > 0) {
|
|
187
205
|
// A sync component still started server resources (useFetch): settle them
|
|
188
206
|
// before returning so the handoff serializes real data, not loading state.
|
|
189
|
-
return
|
|
207
|
+
return (async () => {
|
|
190
208
|
await settleSsrPromises(renderToken);
|
|
191
209
|
clearSsrCells(renderToken);
|
|
192
210
|
return { body: bodyHtml, head: headHtml, props: ssrProps };
|
|
193
|
-
})()
|
|
211
|
+
})();
|
|
194
212
|
}
|
|
195
213
|
clearSsrCells(renderToken);
|
|
196
214
|
return { body: bodyHtml, head: headHtml, props: ssrProps };
|
|
@@ -217,6 +235,10 @@ function clearSsrCells(token) {
|
|
|
217
235
|
return;
|
|
218
236
|
delete globalThis[`__vsk_ssr_promises_${token}`];
|
|
219
237
|
delete globalThis[`__vsk_ssr_failures_${token}`];
|
|
238
|
+
// This render is done with the token: release the slot so the reaper below
|
|
239
|
+
// can collect paths that never run renderFullPage (a bare renderPage/render
|
|
240
|
+
// leaves its data slot behind for the full-page merge).
|
|
241
|
+
dropSsrSlot(token);
|
|
220
242
|
const cells = globalThis.__vsk_ssr_cells;
|
|
221
243
|
if (!cells || !(cells instanceof Map))
|
|
222
244
|
return;
|
|
@@ -233,24 +255,50 @@ function clearSsrCells(token) {
|
|
|
233
255
|
async function settleSsrPromises(token) {
|
|
234
256
|
if (!token)
|
|
235
257
|
return;
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
await
|
|
258
|
+
const key = `__vsk_ssr_promises_${token}`;
|
|
259
|
+
// Drain until the queue stops growing. The runtime only ever pushes onto
|
|
260
|
+
// this array, and settling one promise can register another: an async
|
|
261
|
+
// component body re-runs after its previous await and starts the next
|
|
262
|
+
// fetch. A single allSettled over a snapshot returns before that lands, so
|
|
263
|
+
// the render serializes an empty data slot and ships without its ssr-data
|
|
264
|
+
// script — the client then refetches what SSR already had.
|
|
265
|
+
for (let round = 0; round < 10; round++) {
|
|
266
|
+
const pending = globalThis[key];
|
|
267
|
+
if (!pending || pending.length === 0)
|
|
268
|
+
return;
|
|
269
|
+
const before = pending.length;
|
|
270
|
+
await Promise.allSettled(pending.slice());
|
|
271
|
+
const after = globalThis[key];
|
|
272
|
+
if (!after || after.length <= before)
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
240
275
|
}
|
|
241
276
|
/**
|
|
242
277
|
* Bound the per-token SSR data slots against abandonment (dev-partial
|
|
243
278
|
* renderPage / render() that never runs renderFullPage). Slots are keyed by
|
|
244
|
-
*
|
|
279
|
+
* per-request tokens, so deleted-slot accounting by token can't be cleaned by
|
|
245
280
|
* an owning render; cap the map instead. RenderFullPage/RenderPageStream delete
|
|
246
281
|
* their own slot + token in every path, so this only guards the untracked ones.
|
|
282
|
+
*
|
|
283
|
+
* Live renders are exempt. Slots are only reapable once no render owns them
|
|
284
|
+
* (see isSsrSlotLive) — a cap that ignored ownership would delete a
|
|
285
|
+
* *concurrent* request's handoff, which is the empty-ssr-data bug again, just
|
|
286
|
+
* triggered by load rather than by interleaving.
|
|
247
287
|
*/
|
|
248
288
|
function pruneSsrDataSlots() {
|
|
249
289
|
const g = globalThis;
|
|
250
|
-
const
|
|
251
|
-
|
|
290
|
+
const prefix = '__vsk_ssr_data_';
|
|
291
|
+
const stale = [];
|
|
292
|
+
for (const k of Object.keys(g)) {
|
|
293
|
+
if (typeof k !== 'string' || !k.startsWith(prefix))
|
|
294
|
+
continue;
|
|
295
|
+
if (isSsrSlotLive(k.slice(prefix.length)))
|
|
296
|
+
continue;
|
|
297
|
+
stale.push(k);
|
|
298
|
+
}
|
|
299
|
+
if (stale.length <= 40)
|
|
252
300
|
return;
|
|
253
|
-
|
|
301
|
+
stale.sort().slice(0, stale.length - 40).forEach((k) => {
|
|
254
302
|
delete g[k];
|
|
255
303
|
});
|
|
256
304
|
}
|
|
@@ -411,6 +459,7 @@ export async function renderFullPage(source, componentName, props = {}, registry
|
|
|
411
459
|
if (!globalThis.__vsk_ssr_token)
|
|
412
460
|
globalThis.__vsk_ssr_token = Math.random().toString(36).slice(2);
|
|
413
461
|
const renderToken = globalThis.__vsk_ssr_token;
|
|
462
|
+
keepSsrSlot(renderToken);
|
|
414
463
|
pruneSsrDataSlots();
|
|
415
464
|
// Start this render's handoff store fresh: setSsrData mirrors into
|
|
416
465
|
// globalThis.__vsk_ssr_data (see resource.ts) because the AsyncLocalStorage
|
|
@@ -505,12 +554,24 @@ ${dataScriptBlock}${clientScript}</body>
|
|
|
505
554
|
finally {
|
|
506
555
|
delete globalThis.__vsk_ssr;
|
|
507
556
|
delete globalThis[`__vsk_ssr_data_${renderToken}`];
|
|
508
|
-
|
|
557
|
+
// The token lives in the AsyncLocalStorage store now (see ssr-store.ts);
|
|
558
|
+
// it goes away with the scope. Deleting the global would destroy the
|
|
559
|
+
// request-scoped accessor that generated render code reads.
|
|
560
|
+
dropSsrSlot(renderToken);
|
|
509
561
|
pruneSsrDataSlots();
|
|
510
562
|
}
|
|
511
563
|
});
|
|
512
564
|
}
|
|
513
565
|
export function renderPageStream(source, componentName, props = {}, registry = new Map(), options = {}) {
|
|
566
|
+
// One store for the whole stream. `next()` is driven by the consumer, so a
|
|
567
|
+
// per-`next()` scope would mint a new token every chunk, orphan the slots
|
|
568
|
+
// written by earlier chunks, and leave the tail merge reading an untouched
|
|
569
|
+
// slot (the page then streams with no ssr-data script). Adopting the ambient
|
|
570
|
+
// store keeps the chunks, the promise tracker and that merge on the request's
|
|
571
|
+
// single token; every pull below runs through `withSsrStoreOf(streamStore)`.
|
|
572
|
+
const streamStore = adoptSsrStore();
|
|
573
|
+
keepSsrSlot(streamStore.token);
|
|
574
|
+
const streamToken = streamStore.token;
|
|
514
575
|
async function* raw() {
|
|
515
576
|
const cached = options.cached || undefined;
|
|
516
577
|
const ir = cached ? cached.ir : generateIR(parse(source, options.sourcePath ? { filename: options.sourcePath } : {}), source, options.sourcePath);
|
|
@@ -584,9 +645,7 @@ export function renderPageStream(source, componentName, props = {}, registry = n
|
|
|
584
645
|
globalThis.__vsk_ssr = true;
|
|
585
646
|
// Fresh handoff store per render; see renderFullPage note.
|
|
586
647
|
globalThis.__vsk_ssr_data = {};
|
|
587
|
-
|
|
588
|
-
globalThis.__vsk_ssr_token = Math.random().toString(36).slice(2);
|
|
589
|
-
const renderToken = globalThis.__vsk_ssr_token;
|
|
648
|
+
const renderToken = streamToken;
|
|
590
649
|
pruneSsrDataSlots();
|
|
591
650
|
let bodyHtml;
|
|
592
651
|
try {
|
|
@@ -607,7 +666,7 @@ export function renderPageStream(source, componentName, props = {}, registry = n
|
|
|
607
666
|
console.error(`[render-trace] renderPageStream comp=${componentName} keys=${Object.keys(ssrData).join(',') || '∅'} slot=${Object.keys(slot).join(',') || '∅'} sink=${Object.keys(ssrSink.snapshot()).join(',') || '∅'}`);
|
|
608
667
|
}
|
|
609
668
|
delete globalThis[`__vsk_ssr_data_${renderToken}`];
|
|
610
|
-
|
|
669
|
+
dropSsrSlot(renderToken);
|
|
611
670
|
pruneSsrDataSlots();
|
|
612
671
|
const dataScripts = buildDataScripts(ssrProps, ssrData, options.externalDataScript);
|
|
613
672
|
const dataScriptBlock = dataScripts.length > 0 ? '\n' + dataScripts.join('\n') : '';
|
|
@@ -619,13 +678,29 @@ export function renderPageStream(source, componentName, props = {}, registry = n
|
|
|
619
678
|
yield `\n</div>${dataScriptBlock}${clientScript}</body>\n</html>\n`;
|
|
620
679
|
}
|
|
621
680
|
const gen = raw();
|
|
681
|
+
// Drive every pull in the stream's own store. `gen.next()` is called by the
|
|
682
|
+
// consumer, so it runs in the consumer's context — without this the render
|
|
683
|
+
// would see whatever token the caller's request happens to hold (or none),
|
|
684
|
+
// while the merge above reads `streamToken`.
|
|
622
685
|
async function* scoped() {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
686
|
+
try {
|
|
687
|
+
let result;
|
|
688
|
+
do {
|
|
689
|
+
result = (await withSsrStoreOf(streamStore, () => gen.next()));
|
|
690
|
+
if (!result.done)
|
|
691
|
+
yield result.value;
|
|
692
|
+
} while (!result.done);
|
|
693
|
+
}
|
|
694
|
+
finally {
|
|
695
|
+
// Release the claim `raw()`'s own clearSsrCells already balanced, then
|
|
696
|
+
// sweep anything the stream left behind.
|
|
697
|
+
dropSsrSlot(streamToken);
|
|
698
|
+
delete globalThis[`__vsk_ssr_data_${streamToken}`];
|
|
699
|
+
delete globalThis[`__vsk_ssr_promises_${streamToken}`];
|
|
700
|
+
delete globalThis[`__vsk_ssr_failures_${streamToken}`];
|
|
701
|
+
delete globalThis.__vsk_ssr_data;
|
|
702
|
+
pruneSsrDataSlots();
|
|
703
|
+
}
|
|
629
704
|
}
|
|
630
705
|
return scoped();
|
|
631
706
|
}
|
package/dist/ssr-store.d.ts
CHANGED
|
@@ -1,4 +1,64 @@
|
|
|
1
1
|
import type { SsrDataSink } from '@vesk/runtime/src/resource';
|
|
2
|
+
export interface SsrStore {
|
|
3
|
+
/** Per-request token. Keyed data slots hang off this, so it must be unique
|
|
4
|
+
* per concurrent render — a shared token lets a finishing render delete a
|
|
5
|
+
* stranger's data slot. */
|
|
6
|
+
token?: string;
|
|
7
|
+
/** Handoff data for the current request. */
|
|
8
|
+
data: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A store that is not yet attached to a scope. Callers that drive several
|
|
12
|
+
* scopes (streaming renders) own it and hand it to `withSsrStoreOf` so the
|
|
13
|
+
* token and sink survive across the boundary.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createSsrStore(): SsrStore;
|
|
16
|
+
/**
|
|
17
|
+
* The store for the current request: the ambient one when a scope is already
|
|
18
|
+
* open, otherwise a fresh caller-owned one. Callers that outlive a single
|
|
19
|
+
* `withSsrStore` (streaming renders drive chunks from the consumer, long after
|
|
20
|
+
* the scope's promise resolved) use this so their work still lands on the
|
|
21
|
+
* request's token instead of minting a second one.
|
|
22
|
+
*/
|
|
23
|
+
export declare function adoptSsrStore(): SsrStore;
|
|
24
|
+
/**
|
|
25
|
+
* Run `fn` in a request scope, reusing the ambient store when one is open.
|
|
26
|
+
*
|
|
27
|
+
* The scope owns a claim on its token for its whole lifetime, which is what
|
|
28
|
+
* keeps a request's data slot reap-exempt between its stages. A request runs
|
|
29
|
+
* `renderPage` (page) -> `renderPage` (each nested layout) -> `renderFullPage`
|
|
30
|
+
* (document), and each of those claims and releases the slot on entry/exit.
|
|
31
|
+
* Without the scope-level claim the token is briefly unowned between stages, so
|
|
32
|
+
* with more than ~40 renders in flight a concurrent reaper sees the slot as
|
|
33
|
+
* abandoned and deletes the data the document render still has to serialize.
|
|
34
|
+
*/
|
|
2
35
|
export declare function withSsrStore<T>(fn: () => T): T;
|
|
36
|
+
/**
|
|
37
|
+
* Run `fn` in a scope backed by a caller-owned store, so the token and the sink
|
|
38
|
+
* survive across `yield`s. Streaming renders need this: each `next()` is driven
|
|
39
|
+
* by the consumer, not by the generator, so a per-`next()` `withSsrStore` would
|
|
40
|
+
* mint a new token every chunk and every data slot written by an earlier chunk
|
|
41
|
+
* would be orphaned.
|
|
42
|
+
*/
|
|
43
|
+
export declare function withSsrStoreOf<T>(store: SsrStore, fn: () => T): T;
|
|
44
|
+
/** The current request's token, or undefined outside a render scope. */
|
|
45
|
+
export declare function currentSsrToken(): string | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Mint a fresh token for the current scope. Used by the dev servers' data-nav
|
|
48
|
+
* path, which renders several routes within one request and must not let an
|
|
49
|
+
* earlier route's slot settle into a later one.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resetSsrToken(): string;
|
|
52
|
+
/** Claim a token's data slot so the reaper never deletes it mid-render. */
|
|
53
|
+
export declare function keepSsrSlot(token: string | undefined): void;
|
|
54
|
+
/** Release one claim on a token's data slot. */
|
|
55
|
+
export declare function dropSsrSlot(token: string | undefined): void;
|
|
56
|
+
/**
|
|
57
|
+
* True while a render still owns this token's slot. Slots are keyed by
|
|
58
|
+
* per-request tokens, so a cap-based reaper that ignores ownership deletes a
|
|
59
|
+
* *concurrent* request's handoff and the second page ships with no ssr-data
|
|
60
|
+
* script — the original bug, just triggered by load instead of interleaving.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isSsrSlotLive(token: string): boolean;
|
|
3
63
|
export declare const ssrSink: SsrDataSink;
|
|
4
64
|
//# sourceMappingURL=ssr-store.d.ts.map
|
package/dist/ssr-store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-store.d.ts","sourceRoot":"","sources":["../src/ssr-store.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAI9D,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"ssr-store.d.ts","sourceRoot":"","sources":["../src/ssr-store.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAI9D,MAAM,WAAW,QAAQ;IACvB;;gCAE4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAkBD;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,QAAQ,CAEzC;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,IAAI,QAAQ,CAExC;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAuB9C;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEjE;AAED,wEAAwE;AACxE,wBAAgB,eAAe,IAAI,MAAM,GAAG,SAAS,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAYtC;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAG3D;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAM3D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,eAAO,MAAM,OAAO,EAAE,WAgBrB,CAAC"}
|
package/dist/ssr-store.js
CHANGED
|
@@ -3,30 +3,181 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
3
3
|
// checkout the compiler typechecks BEFORE @vesk/runtime's dist exists, and
|
|
4
4
|
// resolving types through the barrel then fails.
|
|
5
5
|
import { setSsrSink } from '@vesk/runtime/src/index-server';
|
|
6
|
+
const TOKEN_KEY = '__vsk_ssr_token';
|
|
6
7
|
const storage = new AsyncLocalStorage();
|
|
8
|
+
/**
|
|
9
|
+
* Tokens whose `__vsk_ssr_data_<token>` slot a live render still owns, with a
|
|
10
|
+
* refcount. A request runs `renderPage` (page) -> `renderPage` (each nested
|
|
11
|
+
* layout) -> `renderFullPage` (document) inside ONE store, and each of those
|
|
12
|
+
* claims the slot on entry and releases it on exit. A plain Set would let the
|
|
13
|
+
* first inner release revoke the claim the document render still depends on,
|
|
14
|
+
* so the reaper could reap a live request's handoff. Counts balance instead.
|
|
15
|
+
*/
|
|
16
|
+
const liveSlots = new Map();
|
|
17
|
+
function newSsrToken() {
|
|
18
|
+
return Math.random().toString(36).slice(2);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A store that is not yet attached to a scope. Callers that drive several
|
|
22
|
+
* scopes (streaming renders) own it and hand it to `withSsrStoreOf` so the
|
|
23
|
+
* token and sink survive across the boundary.
|
|
24
|
+
*/
|
|
25
|
+
export function createSsrStore() {
|
|
26
|
+
return { token: newSsrToken(), data: {} };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The store for the current request: the ambient one when a scope is already
|
|
30
|
+
* open, otherwise a fresh caller-owned one. Callers that outlive a single
|
|
31
|
+
* `withSsrStore` (streaming renders drive chunks from the consumer, long after
|
|
32
|
+
* the scope's promise resolved) use this so their work still lands on the
|
|
33
|
+
* request's token instead of minting a second one.
|
|
34
|
+
*/
|
|
35
|
+
export function adoptSsrStore() {
|
|
36
|
+
return storage.getStore() ?? createSsrStore();
|
|
37
|
+
}
|
|
38
|
+
function isThenable(value) {
|
|
39
|
+
return typeof value?.then === 'function';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run `fn` in a request scope, reusing the ambient store when one is open.
|
|
43
|
+
*
|
|
44
|
+
* The scope owns a claim on its token for its whole lifetime, which is what
|
|
45
|
+
* keeps a request's data slot reap-exempt between its stages. A request runs
|
|
46
|
+
* `renderPage` (page) -> `renderPage` (each nested layout) -> `renderFullPage`
|
|
47
|
+
* (document), and each of those claims and releases the slot on entry/exit.
|
|
48
|
+
* Without the scope-level claim the token is briefly unowned between stages, so
|
|
49
|
+
* with more than ~40 renders in flight a concurrent reaper sees the slot as
|
|
50
|
+
* abandoned and deletes the data the document render still has to serialize.
|
|
51
|
+
*/
|
|
7
52
|
export function withSsrStore(fn) {
|
|
8
53
|
const existing = storage.getStore();
|
|
9
54
|
if (existing)
|
|
10
55
|
return fn();
|
|
11
|
-
|
|
56
|
+
const store = createSsrStore();
|
|
57
|
+
keepSsrSlot(store.token);
|
|
58
|
+
let result;
|
|
59
|
+
try {
|
|
60
|
+
result = storage.run(store, fn);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
dropSsrSlot(store.token);
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
// A streaming render hands back a Response while its body is still pending;
|
|
67
|
+
// releasing on promise settlement is exactly right for both cases, because a
|
|
68
|
+
// render that outlives the scope claims the token itself (renderPageStream).
|
|
69
|
+
if (isThenable(result)) {
|
|
70
|
+
return result.then((value) => { dropSsrSlot(store.token); return value; }, (err) => { dropSsrSlot(store.token); throw err; });
|
|
71
|
+
}
|
|
72
|
+
dropSsrSlot(store.token);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Run `fn` in a scope backed by a caller-owned store, so the token and the sink
|
|
77
|
+
* survive across `yield`s. Streaming renders need this: each `next()` is driven
|
|
78
|
+
* by the consumer, not by the generator, so a per-`next()` `withSsrStore` would
|
|
79
|
+
* mint a new token every chunk and every data slot written by an earlier chunk
|
|
80
|
+
* would be orphaned.
|
|
81
|
+
*/
|
|
82
|
+
export function withSsrStoreOf(store, fn) {
|
|
83
|
+
return storage.run(store, fn);
|
|
84
|
+
}
|
|
85
|
+
/** The current request's token, or undefined outside a render scope. */
|
|
86
|
+
export function currentSsrToken() {
|
|
87
|
+
return storage.getStore()?.token;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Mint a fresh token for the current scope. Used by the dev servers' data-nav
|
|
91
|
+
* path, which renders several routes within one request and must not let an
|
|
92
|
+
* earlier route's slot settle into a later one.
|
|
93
|
+
*/
|
|
94
|
+
export function resetSsrToken() {
|
|
95
|
+
const store = storage.getStore();
|
|
96
|
+
const token = newSsrToken();
|
|
97
|
+
if (store) {
|
|
98
|
+
if (store.token)
|
|
99
|
+
liveSlots.delete(store.token);
|
|
100
|
+
store.token = token;
|
|
101
|
+
}
|
|
102
|
+
// The caller owns the request from here, so hold the claim for the whole
|
|
103
|
+
// scope: nested page/layout renders each add and remove their own count, and
|
|
104
|
+
// this one keeps the slot reap-exempt until the caller is done with it.
|
|
105
|
+
liveSlots.set(token, 1);
|
|
106
|
+
return token;
|
|
107
|
+
}
|
|
108
|
+
/** Claim a token's data slot so the reaper never deletes it mid-render. */
|
|
109
|
+
export function keepSsrSlot(token) {
|
|
110
|
+
if (!token)
|
|
111
|
+
return;
|
|
112
|
+
liveSlots.set(token, (liveSlots.get(token) ?? 0) + 1);
|
|
113
|
+
}
|
|
114
|
+
/** Release one claim on a token's data slot. */
|
|
115
|
+
export function dropSsrSlot(token) {
|
|
116
|
+
if (!token)
|
|
117
|
+
return;
|
|
118
|
+
const count = liveSlots.get(token);
|
|
119
|
+
if (count === undefined)
|
|
120
|
+
return;
|
|
121
|
+
if (count <= 1)
|
|
122
|
+
liveSlots.delete(token);
|
|
123
|
+
else
|
|
124
|
+
liveSlots.set(token, count - 1);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* True while a render still owns this token's slot. Slots are keyed by
|
|
128
|
+
* per-request tokens, so a cap-based reaper that ignores ownership deletes a
|
|
129
|
+
* *concurrent* request's handoff and the second page ships with no ssr-data
|
|
130
|
+
* script — the original bug, just triggered by load instead of interleaving.
|
|
131
|
+
*/
|
|
132
|
+
export function isSsrSlotLive(token) {
|
|
133
|
+
return liveSlots.has(token);
|
|
12
134
|
}
|
|
13
135
|
export const ssrSink = {
|
|
14
136
|
set: (key, value) => {
|
|
15
137
|
const store = storage.getStore();
|
|
16
138
|
if (store)
|
|
17
|
-
store[key] = value;
|
|
139
|
+
store.data[key] = value;
|
|
18
140
|
},
|
|
19
141
|
get: (key) => {
|
|
20
142
|
const store = storage.getStore();
|
|
21
|
-
return store ? store[key] : undefined;
|
|
143
|
+
return store ? store.data[key] : undefined;
|
|
22
144
|
},
|
|
23
|
-
snapshot: () => ({ ...(storage.getStore() ?? {}) }),
|
|
145
|
+
snapshot: () => ({ ...(storage.getStore()?.data ?? {}) }),
|
|
24
146
|
clear: () => {
|
|
25
147
|
const store = storage.getStore();
|
|
26
148
|
if (store) {
|
|
27
|
-
for (const key of Object.keys(store))
|
|
28
|
-
delete store[key];
|
|
149
|
+
for (const key of Object.keys(store.data))
|
|
150
|
+
delete store.data[key];
|
|
29
151
|
}
|
|
30
152
|
},
|
|
31
153
|
};
|
|
32
154
|
setSsrSink(ssrSink);
|
|
155
|
+
// The generated server code reads `globalThis.__vsk_ssr_token` synchronously
|
|
156
|
+
// at the top of every component render, and the runtime's promise tracker and
|
|
157
|
+
// data writers read it the same way. Back that global with the ALS store so
|
|
158
|
+
// those readers become request-scoped instead of process-global. Before this,
|
|
159
|
+
// two concurrent renders adopted the same token and whichever finished first
|
|
160
|
+
// deleted the other's data slot, so the second page shipped with an empty
|
|
161
|
+
// handoff and the client refetched what SSR already had.
|
|
162
|
+
//
|
|
163
|
+
// Outside a render scope the accessor reads/writes nothing, which is what
|
|
164
|
+
// keeps a stray write from leaking into the next request. Bundles that never
|
|
165
|
+
// load this module keep the plain global and the previous behaviour.
|
|
166
|
+
//
|
|
167
|
+
// NEVER `delete globalThis.__vsk_ssr_token`. This is a configurable accessor,
|
|
168
|
+
// so `delete` removes the property definition outright — it does not route
|
|
169
|
+
// through the setter — and the process silently falls back to a
|
|
170
|
+
// process-global token, reintroducing the race for every later request. Clear
|
|
171
|
+
// a token by assigning through `globalThis` inside a scope, or with
|
|
172
|
+
// `resetSsrToken`.
|
|
173
|
+
Object.defineProperty(globalThis, TOKEN_KEY, {
|
|
174
|
+
configurable: true,
|
|
175
|
+
get() {
|
|
176
|
+
return storage.getStore()?.token;
|
|
177
|
+
},
|
|
178
|
+
set(value) {
|
|
179
|
+
const store = storage.getStore();
|
|
180
|
+
if (store)
|
|
181
|
+
store.token = value;
|
|
182
|
+
},
|
|
183
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-name component declarations across `.vsk` files.
|
|
3
|
+
*
|
|
4
|
+
* `.vsk` components are not module-local bindings — every component a bundle
|
|
5
|
+
* compiles is registered in ONE global registry under its declared name
|
|
6
|
+
* (`__components["Helper"]`, `__registry.get("Helper")`). Two files that each
|
|
7
|
+
* declare `component Helper` therefore land on the same key, and which one wins
|
|
8
|
+
* depends on the order the bundle graph happened to walk. There is no type
|
|
9
|
+
* error and no runtime error: the page just renders somebody else's component.
|
|
10
|
+
*
|
|
11
|
+
* That is the failure mode worth reporting. It is *not* the same as importing a
|
|
12
|
+
* non-exported component: making `export` load-bearing would not prevent a
|
|
13
|
+
* collision, because two files can both `export component Helper` and still
|
|
14
|
+
* overwrite one key. So this check looks only at declaration names, never at
|
|
15
|
+
* the registry (the registry holds no ownership metadata and cannot say which
|
|
16
|
+
* file a symbol came from).
|
|
17
|
+
*
|
|
18
|
+
* Deliberately a WARNING, not an error. The loose name-keyed registry is
|
|
19
|
+
* long-standing and load-bearing, and a hard error here would break builds that
|
|
20
|
+
* render correctly today.
|
|
21
|
+
*/
|
|
22
|
+
export interface VskComponentCollision {
|
|
23
|
+
name: string;
|
|
24
|
+
files: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare class VskComponentOwners {
|
|
27
|
+
private owners;
|
|
28
|
+
/** Records that `file` declares a component called `name`. */
|
|
29
|
+
claim(name: string, file: string): void;
|
|
30
|
+
/** Names declared by more than one file, each with its declaring files. */
|
|
31
|
+
collisions(): VskComponentCollision[];
|
|
32
|
+
/**
|
|
33
|
+
* Warns once per colliding name. Returns the message per collision so a
|
|
34
|
+
* caller (or a test) can assert on the text without capturing stderr.
|
|
35
|
+
*
|
|
36
|
+
* Deduped by name for the life of the process: the SSR path compiles one
|
|
37
|
+
* file graph per `compileFile` call, so without this a single build would
|
|
38
|
+
* repeat the same warning once per entry page.
|
|
39
|
+
*/
|
|
40
|
+
report(context: string): string[];
|
|
41
|
+
}
|
|
42
|
+
/** Test hook: forget which collisions have been reported. */
|
|
43
|
+
export declare function resetVskCollisionReports(): void;
|
|
44
|
+
//# sourceMappingURL=vsk-collision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vsk-collision.d.ts","sourceRoot":"","sources":["../src/vsk-collision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAkC;IAEhD,8DAA8D;IAC9D,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IASvC,2EAA2E;IAC3E,UAAU,IAAI,qBAAqB,EAAE;IAQrC;;;;;;;OAOG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE;CAelC;AAQD,6DAA6D;AAC7D,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-name component declarations across `.vsk` files.
|
|
3
|
+
*
|
|
4
|
+
* `.vsk` components are not module-local bindings — every component a bundle
|
|
5
|
+
* compiles is registered in ONE global registry under its declared name
|
|
6
|
+
* (`__components["Helper"]`, `__registry.get("Helper")`). Two files that each
|
|
7
|
+
* declare `component Helper` therefore land on the same key, and which one wins
|
|
8
|
+
* depends on the order the bundle graph happened to walk. There is no type
|
|
9
|
+
* error and no runtime error: the page just renders somebody else's component.
|
|
10
|
+
*
|
|
11
|
+
* That is the failure mode worth reporting. It is *not* the same as importing a
|
|
12
|
+
* non-exported component: making `export` load-bearing would not prevent a
|
|
13
|
+
* collision, because two files can both `export component Helper` and still
|
|
14
|
+
* overwrite one key. So this check looks only at declaration names, never at
|
|
15
|
+
* the registry (the registry holds no ownership metadata and cannot say which
|
|
16
|
+
* file a symbol came from).
|
|
17
|
+
*
|
|
18
|
+
* Deliberately a WARNING, not an error. The loose name-keyed registry is
|
|
19
|
+
* long-standing and load-bearing, and a hard error here would break builds that
|
|
20
|
+
* render correctly today.
|
|
21
|
+
*/
|
|
22
|
+
export class VskComponentOwners {
|
|
23
|
+
owners = new Map();
|
|
24
|
+
/** Records that `file` declares a component called `name`. */
|
|
25
|
+
claim(name, file) {
|
|
26
|
+
let set = this.owners.get(name);
|
|
27
|
+
if (!set) {
|
|
28
|
+
set = new Set();
|
|
29
|
+
this.owners.set(name, set);
|
|
30
|
+
}
|
|
31
|
+
set.add(file);
|
|
32
|
+
}
|
|
33
|
+
/** Names declared by more than one file, each with its declaring files. */
|
|
34
|
+
collisions() {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const [name, files] of this.owners) {
|
|
37
|
+
if (files.size > 1)
|
|
38
|
+
out.push({ name, files: [...files] });
|
|
39
|
+
}
|
|
40
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Warns once per colliding name. Returns the message per collision so a
|
|
44
|
+
* caller (or a test) can assert on the text without capturing stderr.
|
|
45
|
+
*
|
|
46
|
+
* Deduped by name for the life of the process: the SSR path compiles one
|
|
47
|
+
* file graph per `compileFile` call, so without this a single build would
|
|
48
|
+
* repeat the same warning once per entry page.
|
|
49
|
+
*/
|
|
50
|
+
report(context) {
|
|
51
|
+
const collisions = this.collisions();
|
|
52
|
+
const fresh = [];
|
|
53
|
+
for (const { name, files } of collisions) {
|
|
54
|
+
if (reportedCollisions.has(name))
|
|
55
|
+
continue;
|
|
56
|
+
reportedCollisions.add(name);
|
|
57
|
+
fresh.push(`${name}: ${files.join(', ')}`);
|
|
58
|
+
console.warn(`[vesk] ${context}: component "${name}" is declared in ${files.length} files and resolves to only one of them — `
|
|
59
|
+
+ `${files.join(', ')}. Which one wins depends on compile order, and the client and server registries pick differently, `
|
|
60
|
+
+ `so this can render a different component in the browser than in the SSR HTML. Rename one of them.`);
|
|
61
|
+
}
|
|
62
|
+
return fresh;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Names already warned about. The client bundle and the SSR path each compile
|
|
67
|
+
* overlapping file graphs, so a per-instance set would double-report.
|
|
68
|
+
*/
|
|
69
|
+
const reportedCollisions = new Set();
|
|
70
|
+
/** Test hook: forget which collisions have been reported. */
|
|
71
|
+
export function resetVskCollisionReports() {
|
|
72
|
+
reportedCollisions.clear();
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vesk/compiler",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.44",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vesk compiler — lexer, parser, semantic analysis, IR, codegen",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@sveltejs/acorn-typescript": "^1.0.0",
|
|
23
|
-
"@vesk/types": "^0.2.
|
|
23
|
+
"@vesk/types": "^0.2.44",
|
|
24
24
|
"acorn": "^8.15.0",
|
|
25
25
|
"esrap": "^2.3.0",
|
|
26
26
|
"is-reference": "^3.0.0",
|