octane 0.1.19 → 0.1.20

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.
@@ -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
- const test = b.logical('&&', hmrComponentStore(hot, componentOrigin), existing);
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'),
@@ -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) —
@@ -5653,7 +5654,7 @@ function instrumentProfileComponents(ast, ctx) {
5653
5654
  * Compile a .tsrx source string into JS targeting `octane`.
5654
5655
  * @param {string} source
5655
5656
  * @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] —
5657
+ * @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
5658
  * `dev: true` emits client hydration source-location metadata (per-component
5658
5659
  * `__s.locs`/`__s.locFile`) and, in server mode, source-located native-element
5659
5660
  * scopes for invalid HTML nesting diagnostics. Both are strictly gated so
@@ -5705,6 +5706,7 @@ function compileAuthored(source, filename, options, bundlerMetadata) {
5705
5706
  const analyzedAst = parseModule(source, cleanFilename);
5706
5707
  analyzeTsrx(analyzedAst, cleanFilename);
5707
5708
  adoptParserAst(analyzedAst);
5709
+ assertStrongMode(analyzedAst, source, cleanFilename, options);
5708
5710
  if (bundlerMetadata !== null) bundlerMetadata.hydrateAst = analyzedAst;
5709
5711
  return compileInternal(source, filename, options, analyzedAst, mode, bundlerMetadata);
5710
5712
  }
@@ -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 &&