octane 0.1.19 → 0.1.21
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/compiler/bundler.js +38 -0
- package/dist/compiler/compile-universal.js +11 -1
- package/dist/compiler/compile.js +141 -49
- package/dist/compiler/slot-hooks.js +3 -1
- package/dist/compiler/strong-mode.js +1064 -0
- package/dist/compiler/vite.d.ts +7 -0
- package/dist/compiler/vite.js +5 -0
- package/dist/compiler/volar.js +27 -1
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +583 -33
- package/dist/testing.d.ts +8 -0
- package/dist/testing.js +14 -0
- package/package.json +5 -1
package/dist/compiler/bundler.js
CHANGED
|
@@ -31,6 +31,7 @@ import { findLeadingJsxImportSourcePragma } from './pragma.js';
|
|
|
31
31
|
import { normalizeUniversalRuntime } from './universal-runtime.js';
|
|
32
32
|
import { formatCompileDiagnostic } from './native-change-diagnostics.js';
|
|
33
33
|
import { findVoidComponentImports, findVoidRootImports, slotHooks } from './slot-hooks.js';
|
|
34
|
+
import { assertStrongMode } from './strong-mode.js';
|
|
34
35
|
import {
|
|
35
36
|
assertNoLiveClientOnlyImports,
|
|
36
37
|
createClientOnlyServerStub,
|
|
@@ -306,6 +307,9 @@ export function findVoidComponentExports(source, id) {
|
|
|
306
307
|
|
|
307
308
|
class OctaneBundlerCompiler {
|
|
308
309
|
constructor(options) {
|
|
310
|
+
if (options.strong !== undefined && typeof options.strong !== 'boolean') {
|
|
311
|
+
throw new TypeError('Octane compiler `strong` must be a boolean when provided.');
|
|
312
|
+
}
|
|
309
313
|
this.root = nodePath.resolve(options.root ?? process.cwd());
|
|
310
314
|
try {
|
|
311
315
|
this.realRoot = nodeFs.realpathSync(this.root);
|
|
@@ -318,6 +322,7 @@ class OctaneBundlerCompiler {
|
|
|
318
322
|
hmr: normalizeHmrDialect(options.hmr),
|
|
319
323
|
dev: options.dev,
|
|
320
324
|
profile: options.profile === true,
|
|
325
|
+
strong: options.strong === true,
|
|
321
326
|
universalRuntime: normalizeUniversalRuntime(options.universalRuntime),
|
|
322
327
|
};
|
|
323
328
|
this.renderers = normalizeRendererConfig(options.renderers);
|
|
@@ -449,6 +454,22 @@ class OctaneBundlerCompiler {
|
|
|
449
454
|
return isPathInside(this.root, absoluteFile) || isPathInside(this.realRoot, absoluteFile);
|
|
450
455
|
}
|
|
451
456
|
|
|
457
|
+
_hasApplicationStrongPolicy(file, collected) {
|
|
458
|
+
if (!this._isProjectOwnedSource(file)) return false;
|
|
459
|
+
const absoluteFile = nodePath.isAbsolute(file)
|
|
460
|
+
? nodePath.resolve(file)
|
|
461
|
+
: nodePath.resolve(this.root, file);
|
|
462
|
+
const application = this._nearestOctanePackageRule(this.root);
|
|
463
|
+
const source = this._nearestOctanePackageRule(nodePath.dirname(absoluteFile));
|
|
464
|
+
addMetadata(collected, application);
|
|
465
|
+
addMetadata(collected, source);
|
|
466
|
+
return (
|
|
467
|
+
application.rule === null ||
|
|
468
|
+
source.rule === null ||
|
|
469
|
+
application.rule.root === source.rule.root
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
452
473
|
_isInstalledOctaneSource(file, collected) {
|
|
453
474
|
// Project-owned TS/JS/TSX is always eligible. A linked package is commonly
|
|
454
475
|
// handed to bundlers as its real path, without a node_modules segment, so
|
|
@@ -816,6 +837,13 @@ class OctaneBundlerCompiler {
|
|
|
816
837
|
// of both HMR and dev hydration diagnostics. Server transforms stay byte-for-
|
|
817
838
|
// byte identical even when a shared client/server bundler configuration opts in.
|
|
818
839
|
const profile = environment === 'client' && (options.profile ?? this.defaults.profile) === true;
|
|
840
|
+
// An application's global policy never leaks into installed or linked
|
|
841
|
+
// compatibility packages, including workspace packages nested inside the
|
|
842
|
+
// project root. Modules may still opt themselves in with their own
|
|
843
|
+
// directive, which the authored-source compiler resolves separately.
|
|
844
|
+
const strong =
|
|
845
|
+
(options.strong ?? this.defaults.strong) === true &&
|
|
846
|
+
this._hasApplicationStrongPolicy(file, collected);
|
|
819
847
|
const universalRuntime = normalizeUniversalRuntime(
|
|
820
848
|
options.universalRuntime ?? this.defaults.universalRuntime,
|
|
821
849
|
);
|
|
@@ -892,6 +920,7 @@ class OctaneBundlerCompiler {
|
|
|
892
920
|
dev,
|
|
893
921
|
profile,
|
|
894
922
|
profileFilename,
|
|
923
|
+
...(strong ? { strong: true } : null),
|
|
895
924
|
...(universalRuntime === undefined ? null : { universalRuntime }),
|
|
896
925
|
// Keep the established DOM compiler call byte-for-byte equivalent. A
|
|
897
926
|
// renderer descriptor is an orthogonal compiler input only for the
|
|
@@ -971,6 +1000,14 @@ class OctaneBundlerCompiler {
|
|
|
971
1000
|
return this._passThrough(code, collected);
|
|
972
1001
|
}
|
|
973
1002
|
if (this._hasManualHookSlots(file, collected)) {
|
|
1003
|
+
// Hand-slotted bindings still own their authored policy. Opting one
|
|
1004
|
+
// module in must not require changing its established slot ABI.
|
|
1005
|
+
if (strong || code.includes('use strong')) {
|
|
1006
|
+
const authoredSource = code;
|
|
1007
|
+
assertStrongMode(parseModule(authoredSource, filename), authoredSource, filename, {
|
|
1008
|
+
strong,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
974
1011
|
return this._passThrough(code, collected);
|
|
975
1012
|
}
|
|
976
1013
|
const profileFilename = profile ? this._profileModuleId(file, collected) : undefined;
|
|
@@ -981,6 +1018,7 @@ class OctaneBundlerCompiler {
|
|
|
981
1018
|
hmr: !!hmr,
|
|
982
1019
|
profile,
|
|
983
1020
|
profileFilename,
|
|
1021
|
+
...(strong ? { strong: true } : null),
|
|
984
1022
|
...(specializeVoidRoot
|
|
985
1023
|
? {
|
|
986
1024
|
isVoidComponentImport: options.isVoidComponentImport,
|
|
@@ -3141,7 +3141,17 @@ function hmrHandoffStatements(state, hot, origin) {
|
|
|
3141
3141
|
for (const component of state.hmrComponents) {
|
|
3142
3142
|
const componentOrigin = component.origin ?? origin;
|
|
3143
3143
|
const existing = b.member(hmrComponentStore(hot, componentOrigin), component.name);
|
|
3144
|
-
|
|
3144
|
+
// Webpack/rspack leave `hot.data` undefined until a previous instance of
|
|
3145
|
+
// the module has disposed, so first evaluation must guard the bag itself.
|
|
3146
|
+
const test = b.logical(
|
|
3147
|
+
'&&',
|
|
3148
|
+
b.logical(
|
|
3149
|
+
'&&',
|
|
3150
|
+
memberPath(hot, ['data'], componentOrigin),
|
|
3151
|
+
hmrComponentStore(hot, componentOrigin),
|
|
3152
|
+
),
|
|
3153
|
+
existing,
|
|
3154
|
+
);
|
|
3145
3155
|
const update = b.stmt(
|
|
3146
3156
|
b.call(
|
|
3147
3157
|
b.member(b.member(existing, b.id(state.helpers.hmrSymbol), true), 'update'),
|
package/dist/compiler/compile.js
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
import { assertNoLiveClientOnlyImports } from './client-only-server.js';
|
|
57
57
|
import { nsForChildren, nsForSelf } from './jsx-namespace.js';
|
|
58
58
|
import { analyzeNativeChangeDiagnostics } from './native-change-diagnostics.js';
|
|
59
|
+
import { assertStrongMode } from './strong-mode.js';
|
|
59
60
|
import { assertUniversalRuntimeTarget, normalizeUniversalRuntime } from './universal-runtime.js';
|
|
60
61
|
|
|
61
62
|
// DOM truth tables shared with the client/server runtimes (via constants.ts) —
|
|
@@ -3994,11 +3995,22 @@ function isConditionalJsx(node) {
|
|
|
3994
3995
|
);
|
|
3995
3996
|
}
|
|
3996
3997
|
|
|
3997
|
-
/** Wrap
|
|
3998
|
+
/** Wrap a ternary arm as a BlockStatement body, so makeIfCall can consume it. */
|
|
3998
3999
|
function wrapAsBlockStmt(node) {
|
|
3999
4000
|
if (!node) return null;
|
|
4000
4001
|
// null / Literal(null) / Literal(false) → no branch
|
|
4001
4002
|
if (node.type === 'Literal' && (node.value === null || node.value === false)) return null;
|
|
4003
|
+
if (!isJsxLike(node)) {
|
|
4004
|
+
// A non-JSX arm (`{cond ? xs.map(…) : <Jsx/>}`, a string, a variable
|
|
4005
|
+
// holding an element, a nested ternary) is the branch's render VALUE. As
|
|
4006
|
+
// a bare statement the branch body would evaluate-and-discard it, so wrap
|
|
4007
|
+
// it as the authored-equivalent `<>{expr}</>` — the branch renders the
|
|
4008
|
+
// value through the fragment's child hole. The hole sits at the
|
|
4009
|
+
// fragment's ROOT, a value position on both compilers: a nested ternary
|
|
4010
|
+
// stays a value hole here (it is NOT re-claimed into a nested ifBlock),
|
|
4011
|
+
// which is why the server claim requires host-child position too.
|
|
4012
|
+
node = inheritOriginLoc(b.jsx_fragment([b.jsx_expression_container(node, node)]), node);
|
|
4013
|
+
}
|
|
4002
4014
|
return b.block([node]);
|
|
4003
4015
|
}
|
|
4004
4016
|
|
|
@@ -5653,7 +5665,7 @@ function instrumentProfileComponents(ast, ctx) {
|
|
|
5653
5665
|
* Compile a .tsrx source string into JS targeting `octane`.
|
|
5654
5666
|
* @param {string} source
|
|
5655
5667
|
* @param {string} filename
|
|
5656
|
-
* @param {{ hmr?: boolean | 'vite' | 'webpack', mode?: 'client' | 'server', dev?: boolean, profile?: boolean, profileFilename?: string, autoMemo?: boolean, inlineHookMemo?: boolean, dataCallbackHooks?: readonly string[], renderer?: { id: string, module: string, target: 'dom' | 'universal', server?: string }, rendererBoundaries?: Readonly<Record<string, Readonly<Record<string, { ownerRenderer: string, childRenderer: string, prop: string, server?: string }>>>>, rendererRegistry?: Readonly<Record<string, { module: string, target: 'dom' | 'universal', server?: string }>>, clientOnlyImports?: readonly unknown[], __hydratePrepared?: boolean, __hydrateBoundaryModule?: boolean, __nativeChangeDiagnostics?: readonly unknown[], __nativeChangeAnalysis?: { diagnostics: readonly unknown[], classifications: Map<number, string> } }} [options] —
|
|
5668
|
+
* @param {{ hmr?: boolean | 'vite' | 'webpack', mode?: 'client' | 'server', dev?: boolean, strong?: boolean, profile?: boolean, profileFilename?: string, autoMemo?: boolean, inlineHookMemo?: boolean, dataCallbackHooks?: readonly string[], renderer?: { id: string, module: string, target: 'dom' | 'universal', server?: string }, rendererBoundaries?: Readonly<Record<string, Readonly<Record<string, { ownerRenderer: string, childRenderer: string, prop: string, server?: string }>>>>, rendererRegistry?: Readonly<Record<string, { module: string, target: 'dom' | 'universal', server?: string }>>, clientOnlyImports?: readonly unknown[], __hydratePrepared?: boolean, __hydrateBoundaryModule?: boolean, __nativeChangeDiagnostics?: readonly unknown[], __nativeChangeAnalysis?: { diagnostics: readonly unknown[], classifications: Map<number, string> } }} [options] —
|
|
5657
5669
|
* `dev: true` emits client hydration source-location metadata (per-component
|
|
5658
5670
|
* `__s.locs`/`__s.locFile`) and, in server mode, source-located native-element
|
|
5659
5671
|
* scopes for invalid HTML nesting diagnostics. Both are strictly gated so
|
|
@@ -5705,6 +5717,7 @@ function compileAuthored(source, filename, options, bundlerMetadata) {
|
|
|
5705
5717
|
const analyzedAst = parseModule(source, cleanFilename);
|
|
5706
5718
|
analyzeTsrx(analyzedAst, cleanFilename);
|
|
5707
5719
|
adoptParserAst(analyzedAst);
|
|
5720
|
+
assertStrongMode(analyzedAst, source, cleanFilename, options);
|
|
5708
5721
|
if (bundlerMetadata !== null) bundlerMetadata.hydrateAst = analyzedAst;
|
|
5709
5722
|
return compileInternal(source, filename, options, analyzedAst, mode, bundlerMetadata);
|
|
5710
5723
|
}
|
|
@@ -7480,7 +7493,15 @@ function ssrCompileBodyWithMapTemps(
|
|
|
7480
7493
|
((!!(node.body && node.body.type === 'JSXCodeBlock') && !returnedOutput) ||
|
|
7481
7494
|
returnedFragmentRoot) &&
|
|
7482
7495
|
inheritSoleCompRoot(bodyNodes, ctx);
|
|
7496
|
+
// Body/sub ROOTS (including fragment-root children) are not host-element
|
|
7497
|
+
// children: the client routes rich holes there through the de-opt value
|
|
7498
|
+
// path (a portal has no host to stamp — see the emitNodeHtml root branch),
|
|
7499
|
+
// so the ternary claim must stay off until an element's own children walk
|
|
7500
|
+
// (ssrEmitElement) turns the flag on.
|
|
7501
|
+
const prevHostChildPos = ctx._ssrHostChildPos;
|
|
7502
|
+
ctx._ssrHostChildPos = false;
|
|
7483
7503
|
const htmlExpr = ssrEmitNodes(bodyNodes, ctx, name, inlinedSubs, parentNs, cssHash, componentNs);
|
|
7504
|
+
ctx._ssrHostChildPos = prevHostChildPos;
|
|
7484
7505
|
ctx._ssrInheritRoot = prevInheritRoot;
|
|
7485
7506
|
ctx._returnedFragmentTemplate = prevReturnedFragmentTemplate;
|
|
7486
7507
|
ctx._tsxValuePos = prevValuePos;
|
|
@@ -8585,6 +8606,11 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
|
|
|
8585
8606
|
// pre/textarea/listing: the parser eats a '\n' right after the opening tag —
|
|
8586
8607
|
// the first text part must protect a leading newline (see ssrEmitNodes).
|
|
8587
8608
|
const nlGuardFirst = tag === 'pre' || tag === 'textarea' || tag === 'listing';
|
|
8609
|
+
// These children sit directly under a host element — the one position
|
|
8610
|
+
// where the client claims `{cond ? A : B}` holes (emitElementHtml). Root
|
|
8611
|
+
// positions reset this in ssrCompileBody.
|
|
8612
|
+
const prevHostChildPos = ctx._ssrHostChildPos;
|
|
8613
|
+
ctx._ssrHostChildPos = true;
|
|
8588
8614
|
childrenExpr = ssrEmitNodes(
|
|
8589
8615
|
normChildren,
|
|
8590
8616
|
ctx,
|
|
@@ -8595,6 +8621,7 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
|
|
|
8595
8621
|
childComponentNs,
|
|
8596
8622
|
nlGuardFirst,
|
|
8597
8623
|
);
|
|
8624
|
+
ctx._ssrHostChildPos = prevHostChildPos;
|
|
8598
8625
|
}
|
|
8599
8626
|
// `children=` and spread-held children are content props, not attributes.
|
|
8600
8627
|
// With no nested JSX children, the last present writer renders as the host's
|
|
@@ -8923,18 +8950,35 @@ function ssrCompileSub(
|
|
|
8923
8950
|
) {
|
|
8924
8951
|
const fnName = `${baseName}$${ctx.nextHelperId++}`;
|
|
8925
8952
|
const synth = { params: paramNodes || [], body: bodyStmts };
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8953
|
+
// Returned-tree mirror subs (`__sfragment`): extractFragment folds EVERY
|
|
8954
|
+
// expression hole in the mirrored tree into a descriptor value hole on the
|
|
8955
|
+
// client (`props.hN` + childTextHole), even holes sitting inside host
|
|
8956
|
+
// elements. Mark them so ssrEmitTsrxExpression keeps `{cond ? A : B}` on
|
|
8957
|
+
// ssrChild throughout the mirror, matching that fold. Directive-arm subs
|
|
8958
|
+
// reset the flag through this same save/restore, so an @if INSIDE a mirror
|
|
8959
|
+
// claims again, exactly like the client. (Component `__schildren` subs are
|
|
8960
|
+
// NOT folded wholesale — only their ROOT holes are the children-as-props
|
|
8961
|
+
// value, which `_ssrHostChildPos` already leaves unclaimed — while a hole
|
|
8962
|
+
// inside a host element within children claims like any template child.)
|
|
8963
|
+
const prevFoldedExprHoles = ctx._ssrFoldedExprHoles;
|
|
8964
|
+
ctx._ssrFoldedExprHoles = baseName === '__sfragment';
|
|
8965
|
+
let fn;
|
|
8966
|
+
try {
|
|
8967
|
+
fn = ssrCompileBody(
|
|
8968
|
+
synth,
|
|
8969
|
+
ctx,
|
|
8970
|
+
fnName,
|
|
8971
|
+
cssHash,
|
|
8972
|
+
[],
|
|
8973
|
+
parentNs || 'html',
|
|
8974
|
+
false,
|
|
8975
|
+
componentNs,
|
|
8976
|
+
returnedFragmentTemplate,
|
|
8977
|
+
returnedFragmentRoot,
|
|
8978
|
+
);
|
|
8979
|
+
} finally {
|
|
8980
|
+
ctx._ssrFoldedExprHoles = prevFoldedExprHoles;
|
|
8981
|
+
}
|
|
8938
8982
|
return { fnName, fn };
|
|
8939
8983
|
}
|
|
8940
8984
|
|
|
@@ -8942,10 +8986,19 @@ function ssrEmitIf(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs)
|
|
|
8942
8986
|
// rewriteHookCalls: key any `use(thenable)` in the @if test (it bypasses the
|
|
8943
8987
|
// setup rewrite, so without a stable key it collides with sibling/body use()).
|
|
8944
8988
|
const testExpr = rewriteHookCalls(node.test, ctx, name);
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8989
|
+
// A null consequent (`{cond ? null : <Jsx/>}` lowered by wrapAsBlockStmt)
|
|
8990
|
+
// means "no then branch". It must emit like a MISSING @else — plain '' with
|
|
8991
|
+
// no inner arm range — because the hydrating client renders that branch with
|
|
8992
|
+
// a null body and adopts an empty slot range.
|
|
8993
|
+
const thenStmts = node.consequent
|
|
8994
|
+
? node.consequent.type === 'BlockStatement'
|
|
8995
|
+
? node.consequent.body
|
|
8996
|
+
: [node.consequent]
|
|
8997
|
+
: null;
|
|
8998
|
+
const thenSub = thenStmts
|
|
8999
|
+
? ssrCompileSub(thenStmts, ctx, '__sif', [], cssHash, parentNs, componentNs)
|
|
9000
|
+
: null;
|
|
9001
|
+
if (thenSub) inlinedSubs.push(thenSub.fn);
|
|
8949
9002
|
let elseCall = ssrHtmlTemplate([], node, ctx);
|
|
8950
9003
|
let elseFnName = null;
|
|
8951
9004
|
if (node.alternate) {
|
|
@@ -8966,26 +9019,33 @@ function ssrEmitIf(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs)
|
|
|
8966
9019
|
ctx.runtimeNeeded.add('ssrBlock');
|
|
8967
9020
|
ctx.runtimeNeeded.add('ssrControl');
|
|
8968
9021
|
ctx.runtimeNeeded.add('ssrArm');
|
|
8969
|
-
registerDirectiveOrigin(ctx, node, [
|
|
9022
|
+
registerDirectiveOrigin(ctx, node, [
|
|
9023
|
+
'_$ssrControl',
|
|
9024
|
+
'_$ssrArm',
|
|
9025
|
+
thenSub ? thenSub.fnName : null,
|
|
9026
|
+
elseFnName,
|
|
9027
|
+
]);
|
|
8970
9028
|
// Nested ranges: the OUTER ssrBlock is the if-slot; the INNER one wraps the
|
|
8971
9029
|
// taken branch's content. The client adopts BOTH on hydration (slot = outer,
|
|
8972
9030
|
// branch = inner) so no comment markers are inserted — byte-for-byte, exactly
|
|
8973
9031
|
// like @for. The not-taken arm emits no inner range (just `''`).
|
|
8974
|
-
const thenInner =
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
9032
|
+
const thenInner = thenSub
|
|
9033
|
+
? ssrCall(
|
|
9034
|
+
'ssrArm',
|
|
9035
|
+
[
|
|
9036
|
+
b.literal('then', '"then"'),
|
|
9037
|
+
ssrThunk(
|
|
9038
|
+
ssrCall(
|
|
9039
|
+
'ssrBlock',
|
|
9040
|
+
[ssrSubCall(thenSub.fnName, [b.id('undefined')], node.consequent)],
|
|
9041
|
+
node.consequent,
|
|
9042
|
+
),
|
|
9043
|
+
node.consequent,
|
|
9044
|
+
),
|
|
9045
|
+
],
|
|
8984
9046
|
node.consequent,
|
|
8985
|
-
)
|
|
8986
|
-
],
|
|
8987
|
-
node.consequent,
|
|
8988
|
-
);
|
|
9047
|
+
)
|
|
9048
|
+
: ssrHtmlTemplate([], node, ctx);
|
|
8989
9049
|
const elseInner = node.alternate
|
|
8990
9050
|
? ssrCall(
|
|
8991
9051
|
'ssrArm',
|
|
@@ -9439,6 +9499,30 @@ function ssrEmitTsrxExpression(node, ctx, name, inlinedSubs, parentNs, cssHash,
|
|
|
9439
9499
|
ctx.runtimeNeeded.add('ssrPortal');
|
|
9440
9500
|
return ssrCall('ssrPortal', [], node);
|
|
9441
9501
|
}
|
|
9502
|
+
if (
|
|
9503
|
+
node.returnedJsxValue !== true &&
|
|
9504
|
+
ctx._tsxValuePos !== true &&
|
|
9505
|
+
ctx._ssrFoldedExprHoles !== true &&
|
|
9506
|
+
ctx._ssrHostChildPos === true &&
|
|
9507
|
+
isConditionalJsx(expr)
|
|
9508
|
+
) {
|
|
9509
|
+
// Mirror the client's claim EXACTLY: only emitElementHtml's children
|
|
9510
|
+
// walk lowers `{cond ? A : B}` with a JSX arm to an @if, so both sides
|
|
9511
|
+
// compile each arm as an ordinary arm body and the hydration shapes
|
|
9512
|
+
// (control key, arm ranges, a keyed `.map` claimed inside a value arm)
|
|
9513
|
+
// agree by construction. Everywhere else the hole is a VALUE on the
|
|
9514
|
+
// client and must stay on ssrChild here: returned `.tsx` trees
|
|
9515
|
+
// (`_tsxValuePos`), returned-tree mirrors whose holes extractFragment
|
|
9516
|
+
// folds to `props.hN` (`_ssrFoldedExprHoles`), and every non-host-child
|
|
9517
|
+
// position — body/arm/fragment roots and component-children roots,
|
|
9518
|
+
// where a rich hole rides the de-opt value path because a portal there
|
|
9519
|
+
// has no host to stamp (`_ssrHostChildPos`).
|
|
9520
|
+
const asIf = {
|
|
9521
|
+
...b.if(expr.test, wrapAsBlockStmt(expr.consequent), wrapAsBlockStmt(expr.alternate)),
|
|
9522
|
+
loc: expr.loc, // same devLoc/control-key position as the client's claim
|
|
9523
|
+
};
|
|
9524
|
+
return ssrEmitIf(asIf, ctx, name, inlinedSubs, parentNs, cssHash, componentNs);
|
|
9525
|
+
}
|
|
9442
9526
|
ctx.runtimeNeeded.add('ssrChild');
|
|
9443
9527
|
// rewriteHookCalls first (key any `use(thenable)` in the hole — it bypasses the
|
|
9444
9528
|
// setup rewrite), then rewriteJsxValues (lower nested JSX to createElement).
|
|
@@ -20167,10 +20251,16 @@ function hoistBodyHelper(
|
|
|
20167
20251
|
// ===========================================================================
|
|
20168
20252
|
|
|
20169
20253
|
function makeIfCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
|
|
20170
|
-
// node.test, node.consequent (BlockStatement | Element), node.alternate (BlockStatement | IfStatement | null)
|
|
20171
|
-
|
|
20172
|
-
|
|
20173
|
-
|
|
20254
|
+
// node.test, node.consequent (BlockStatement | Element | null), node.alternate (BlockStatement | IfStatement | null)
|
|
20255
|
+
// A null consequent (`{cond ? null : <Jsx/>}` lowered by wrapAsBlockStmt)
|
|
20256
|
+
// means "no then branch" — same contract as the null alternate: the helper
|
|
20257
|
+
// slot compiles to `null` and the runtime renders an empty branch.
|
|
20258
|
+
|
|
20259
|
+
const thenStmts = node.consequent
|
|
20260
|
+
? node.consequent.type === 'BlockStatement'
|
|
20261
|
+
? node.consequent.body
|
|
20262
|
+
: [node.consequent]
|
|
20263
|
+
: null;
|
|
20174
20264
|
const elseStmts = node.alternate
|
|
20175
20265
|
? node.alternate.type === 'BlockStatement'
|
|
20176
20266
|
? node.alternate.body
|
|
@@ -20178,20 +20268,22 @@ function makeIfCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
|
|
|
20178
20268
|
: null;
|
|
20179
20269
|
// Phase 2: one shared env tuple for both branches (see unionEnv).
|
|
20180
20270
|
const envNames = unionEnv(ctx, [
|
|
20181
|
-
{ stmts: thenStmts, params: [] },
|
|
20271
|
+
thenStmts && { stmts: thenStmts, params: [] },
|
|
20182
20272
|
elseStmts && { stmts: elseStmts, params: [] },
|
|
20183
20273
|
]);
|
|
20184
|
-
const thenHelperName =
|
|
20185
|
-
|
|
20186
|
-
|
|
20187
|
-
|
|
20188
|
-
|
|
20189
|
-
|
|
20190
|
-
|
|
20191
|
-
|
|
20192
|
-
|
|
20193
|
-
|
|
20194
|
-
|
|
20274
|
+
const thenHelperName = thenStmts
|
|
20275
|
+
? hoistBodyHelper(
|
|
20276
|
+
ctx,
|
|
20277
|
+
inlinedSubs,
|
|
20278
|
+
'__then',
|
|
20279
|
+
thenStmts,
|
|
20280
|
+
[],
|
|
20281
|
+
parentNs,
|
|
20282
|
+
cssHash,
|
|
20283
|
+
envNames,
|
|
20284
|
+
directiveKeywordOrigin(ctx, node),
|
|
20285
|
+
)
|
|
20286
|
+
: null;
|
|
20195
20287
|
|
|
20196
20288
|
let elseHelperName = null;
|
|
20197
20289
|
if (elseStmts) {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { parseModule } from '@tsrx/core';
|
|
18
18
|
import { HOOK_NAMES, hookSlotHash } from './compile.js';
|
|
19
19
|
import { annotateHookCalls } from './hook-deps.js';
|
|
20
|
+
import { assertStrongMode } from './strong-mode.js';
|
|
20
21
|
|
|
21
22
|
// Build a cheap import-presence gate. Precise call identity is annotated by the
|
|
22
23
|
// lexical scope analysis in analyzeHookDependencies below; this gate only avoids
|
|
@@ -990,7 +991,7 @@ function walk(node, owner, st) {
|
|
|
990
991
|
*
|
|
991
992
|
* @param {string} source raw module text
|
|
992
993
|
* @param {string} id module id (embedded in the stable Symbol.for key)
|
|
993
|
-
* @param {{ environment?: 'client' | 'server', hmr?: boolean, profile?: boolean, profileFilename?: string, isVoidComponentImport?: (request: string, imported: string) => boolean }} [options] `hmr: true` (dev serve) emits
|
|
994
|
+
* @param {{ environment?: 'client' | 'server', strong?: boolean, hmr?: boolean, profile?: boolean, profileFilename?: string, isVoidComponentImport?: (request: string, imported: string) => boolean }} [options] `hmr: true` (dev serve) emits
|
|
994
995
|
* `Symbol.for(stableKey)` so a re-imported module resolves the same hook
|
|
995
996
|
* slots (state survives HMR); off (ordinary prod builds and SSR) emits
|
|
996
997
|
* runtime-ranged Symbols. Profiling retains short described Symbols because
|
|
@@ -1010,6 +1011,7 @@ export function slotHooks(source, id, options) {
|
|
|
1010
1011
|
} catch {
|
|
1011
1012
|
return null; // let the normal pipeline surface the parse error
|
|
1012
1013
|
}
|
|
1014
|
+
assertStrongMode(ast, source, id, options);
|
|
1013
1015
|
const importInfo = octaneHookLocals(ast);
|
|
1014
1016
|
const canSpecializeRoot =
|
|
1015
1017
|
!options?.hmr &&
|