octane 0.1.10 → 0.1.11

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.
@@ -12,13 +12,18 @@ import { createRequire } from 'node:module';
12
12
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
13
13
  import { parseModule } from '@tsrx/core';
14
14
  import { compile, hasOnlyLowerableNullishExits, isVoidJsxCodeBlockFunction } from './compile.js';
15
- import { addSourceMapNeedles, composeSourceMaps } from './compile-universal.js';
15
+ import {
16
+ addSourceMapNeedles,
17
+ composeSourceMaps,
18
+ validateRendererModuleSource,
19
+ } from './compile-universal.js';
16
20
  import {
17
21
  hydrateBoundaryPathFromId,
18
22
  prepareHydrateBoundaries,
19
23
  prepareServerHydrateBoundaries,
20
24
  } from './hydrate-boundaries.js';
21
25
  import { normalizeRendererConfig, resolveRendererForFile } from './renderers.js';
26
+ import { normalizeUniversalRuntime } from './universal-runtime.js';
22
27
  import {
23
28
  analyzeNativeChangeDiagnostics,
24
29
  formatCompileDiagnostic,
@@ -370,6 +375,7 @@ class OctaneBundlerCompiler {
370
375
  hmr: normalizeHmrDialect(options.hmr),
371
376
  dev: options.dev,
372
377
  profile: options.profile === true,
378
+ universalRuntime: normalizeUniversalRuntime(options.universalRuntime),
373
379
  };
374
380
  this.renderers = normalizeRendererConfig(options.renderers);
375
381
  // Ownership gate for mixed-toolchain projects (e.g. a React app hosting
@@ -830,6 +836,9 @@ class OctaneBundlerCompiler {
830
836
  // of both HMR and dev hydration diagnostics. Server transforms stay byte-for-
831
837
  // byte identical even when a shared client/server bundler configuration opts in.
832
838
  const profile = environment === 'client' && (options.profile ?? this.defaults.profile) === true;
839
+ const universalRuntime = normalizeUniversalRuntime(
840
+ options.universalRuntime ?? this.defaults.universalRuntime,
841
+ );
833
842
  const filename = this._canonicalModuleId(file);
834
843
  const clientOnlyImports =
835
844
  environment === 'server' && Array.isArray(options.clientOnlyImports)
@@ -854,6 +863,21 @@ class OctaneBundlerCompiler {
854
863
  // The directive is a build-time ownership signal only — never ship it.
855
864
  // Blanking (not deleting) keeps positions stable for source maps.
856
865
  const source = directive === null ? code : stripDirective(code, directive);
866
+ const plainHelperSource =
867
+ (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts');
868
+ if (
869
+ plainHelperSource &&
870
+ renderer.target === 'universal' &&
871
+ renderer.validation !== undefined &&
872
+ this._isProjectOwnedSource(file) &&
873
+ !this.exclude.some((path) => file.includes(path)) &&
874
+ !hostOwned
875
+ ) {
876
+ // Renderer rules also own the runtime assumptions of their project-local
877
+ // helper modules. Validate those assumptions without claiming their output:
878
+ // the existing hook-slot/pass-through branch below remains authoritative.
879
+ validateRendererModuleSource(source, filename, renderer);
880
+ }
857
881
  if (fullCompile) {
858
882
  const profileFilename = profile ? this._profileModuleId(file, collected) : undefined;
859
883
  const clientReference =
@@ -908,6 +932,7 @@ class OctaneBundlerCompiler {
908
932
  dev,
909
933
  profile,
910
934
  profileFilename,
935
+ ...(universalRuntime === undefined ? null : { universalRuntime }),
911
936
  // Keep the established DOM compiler call byte-for-byte equivalent. A
912
937
  // renderer descriptor is an orthogonal compiler input only for the
913
938
  // universal branch selected at this template boundary.
@@ -934,6 +959,7 @@ class OctaneBundlerCompiler {
934
959
  diagnostics: out.diagnostics,
935
960
  kind: 'compile',
936
961
  renderer,
962
+ ...(out.universalRuntime === undefined ? null : { universalRuntime: out.universalRuntime }),
937
963
  ...(clientReference === null ? null : { clientReference }),
938
964
  ...(environment === 'client' && options.collectVoidComponentExports === true
939
965
  ? { voidComponentExports: findVoidComponentExports(compileSource, filename) }
@@ -6,6 +6,7 @@ import {
6
6
  import {
7
7
  lowerUniversalRendererRegion,
8
8
  originsFromSourceMap,
9
+ rendererValidationImportReferences,
9
10
  sourceMapFromOrigins,
10
11
  } from './compile-universal.js';
11
12
  import { parseModule } from '@tsrx/core';
@@ -499,6 +500,7 @@ function collectRuntimeCallNames(node, runtime) {
499
500
  function collectLocalSpecializationInfo(source, filename) {
500
501
  const ast = parseModule(source, filename);
501
502
  const components = new Map();
503
+ const exported = new Set();
502
504
  const runtime = { direct: new Map(), namespaces: new Set(), moduleBindings: new Set() };
503
505
  for (const statement of ast.body ?? []) {
504
506
  if (statement.type === 'ImportDeclaration') {
@@ -529,8 +531,93 @@ function collectLocalSpecializationInfo(source, filename) {
529
531
  for (const statement of ast.body ?? []) {
530
532
  const component = localComponentDeclaration(statement, runtime);
531
533
  if (component !== null) components.set(component.name, component);
534
+ if (statement.type === 'ExportNamedDeclaration' && statement.source == null) {
535
+ if (component !== null) exported.add(component.name);
536
+ for (const specifier of statement.specifiers ?? []) {
537
+ if (specifier.local?.name) exported.add(specifier.local.name);
538
+ }
539
+ }
540
+ if (statement.type === 'ExportDefaultDeclaration') {
541
+ if (component !== null) exported.add(component.name);
542
+ if (statement.declaration?.type === 'Identifier') exported.add(statement.declaration.name);
543
+ }
544
+ }
545
+ return { ast, components, exported, runtime };
546
+ }
547
+
548
+ function includedOriginalPredicate(origins, sourceLength) {
549
+ const included = new Uint8Array(sourceLength + 1);
550
+ for (const origin of origins) {
551
+ if (origin >= 0 && origin < sourceLength) included[origin] = 1;
552
+ }
553
+ const prefix = new Uint32Array(sourceLength + 1);
554
+ for (let index = 0; index < sourceLength; index++) {
555
+ prefix[index + 1] = prefix[index] + included[index];
556
+ }
557
+ return (node) => {
558
+ if (typeof node?.start !== 'number' || typeof node?.end !== 'number') return false;
559
+ const start = Math.max(0, Math.min(sourceLength, node.start));
560
+ const end = Math.max(start, Math.min(sourceLength, node.end));
561
+ return prefix[end] !== prefix[start];
562
+ };
563
+ }
564
+
565
+ function owningComponentForReference(components, node) {
566
+ for (const component of components.values()) {
567
+ if (component.declaration.start <= node.start && node.end <= component.declaration.end) {
568
+ return component.name;
569
+ }
570
+ }
571
+ return null;
572
+ }
573
+
574
+ function ownerReachableComponents(sourceLength, origins, localSpecializations) {
575
+ const localNames = new Set(localSpecializations.components.keys());
576
+ const isIncluded = includedOriginalPredicate(origins, sourceLength);
577
+ const dependencies = new Map();
578
+ const reachable = new Set(
579
+ [...localSpecializations.exported].filter((name) => localNames.has(name)),
580
+ );
581
+ const record = (node, name) => {
582
+ if (!localNames.has(name) || !isIncluded(node)) return;
583
+ const owner = owningComponentForReference(localSpecializations.components, node);
584
+ if (owner === null) {
585
+ reachable.add(name);
586
+ return;
587
+ }
588
+ let references = dependencies.get(owner);
589
+ if (references === undefined) dependencies.set(owner, (references = new Set()));
590
+ references.add(name);
591
+ };
592
+ walkScopedReferences(localSpecializations.ast, {
593
+ call: record,
594
+ tag: record,
595
+ });
596
+ const queue = [...reachable];
597
+ while (queue.length > 0) {
598
+ for (const dependency of dependencies.get(queue.shift()) ?? []) {
599
+ if (reachable.has(dependency)) continue;
600
+ reachable.add(dependency);
601
+ queue.push(dependency);
602
+ }
532
603
  }
533
- return { components, runtime };
604
+ return reachable;
605
+ }
606
+
607
+ function maskOriginalRanges(origins, sourceLength, ranges) {
608
+ if (ranges.length === 0) return origins;
609
+ const masked = new Uint8Array(sourceLength);
610
+ for (const range of ranges) {
611
+ const start = Math.max(0, Math.min(sourceLength, range.start));
612
+ const end = Math.max(start, Math.min(sourceLength, range.end));
613
+ masked.fill(1, start, end);
614
+ }
615
+ const output = new Int32Array(origins);
616
+ for (let index = 0; index < output.length; index++) {
617
+ const origin = output[index];
618
+ if (origin >= 0 && origin < sourceLength && masked[origin] === 1) output[index] = -1;
619
+ }
620
+ return output;
534
621
  }
535
622
 
536
623
  function remapText(code, relativeOrigins, input) {
@@ -683,6 +770,7 @@ function specializeLocalComponents(value, kind, index, state) {
683
770
  queue.push(reference);
684
771
  }
685
772
  }
773
+ for (const name of selected) state.specializedLocalNames.add(name);
686
774
 
687
775
  const prefix = `__octaneRendererRegion${index}`;
688
776
  const cloneNames = new Map();
@@ -812,8 +900,12 @@ function lowerBoundaryNode(node, ownerRenderer, state) {
812
900
  profileFilename: state.profileFilename,
813
901
  regionOrigins: specialization.region.origins,
814
902
  runtimeImports: specialization.runtimeImports,
903
+ universalRuntime: state.universalRuntime,
815
904
  },
816
905
  );
906
+ for (const reference of lowered.validationImportReferences) {
907
+ state.childValidationImportReferences.set(`${reference.start}:${reference.end}`, reference);
908
+ }
817
909
  state.preludes.push({ code: lowered.prelude, origins: lowered.preludeOrigins });
818
910
  state.universalUnits.push(lowered.metadata);
819
911
  for (const mapping of lowered.mappings) {
@@ -859,6 +951,7 @@ export function prepareRendererBoundaryRegions(source, filename, ownerRenderer,
859
951
  if (tree === null || tree.roots.length === 0) return null;
860
952
 
861
953
  const state = {
954
+ childValidationImportReferences: new Map(),
862
955
  domRegions: [],
863
956
  filename,
864
957
  hmr: options?.hmr === true ? 'vite' : options?.hmr || false,
@@ -870,6 +963,8 @@ export function prepareRendererBoundaryRegions(source, filename, ownerRenderer,
870
963
  profileFilename: options?.profileFilename,
871
964
  rendererRegistry,
872
965
  source,
966
+ specializedLocalNames: new Set(),
967
+ universalRuntime: options?.universalRuntime,
873
968
  universalUnits: [],
874
969
  };
875
970
  const replacements = [];
@@ -879,6 +974,7 @@ export function prepareRendererBoundaryRegions(source, filename, ownerRenderer,
879
974
  }
880
975
 
881
976
  let transformed = applySourceReplacements(source, 0, source.length, replacements);
977
+ const ownerSourceLength = transformed.code.length;
882
978
  if (state.preludes.length > 0) {
883
979
  transformed = concatMapped(
884
980
  transformed,
@@ -892,6 +988,38 @@ export function prepareRendererBoundaryRegions(source, filename, ownerRenderer,
892
988
  transformed = expandDomRendererRegionsMapped(transformed, state.domRegions);
893
989
  state.domRegions = [];
894
990
  }
991
+ let validationOrigins = transformed.origins;
992
+ if (ownerRenderer.validation !== undefined && state.preludes.length > 0) {
993
+ validationOrigins = new Int32Array(transformed.origins.length);
994
+ validationOrigins.fill(-1);
995
+ validationOrigins.set(transformed.origins.subarray(0, ownerSourceLength));
996
+ }
997
+ if (ownerRenderer.validation !== undefined && state.specializedLocalNames.size > 0) {
998
+ const reachable = ownerReachableComponents(
999
+ source.length,
1000
+ validationOrigins,
1001
+ state.localSpecializations,
1002
+ );
1003
+ const childOnlyDeclarations = [...state.specializedLocalNames]
1004
+ .filter((name) => !reachable.has(name))
1005
+ .map((name) => state.localSpecializations.components.get(name)?.declaration)
1006
+ .filter(Boolean);
1007
+ validationOrigins = maskOriginalRanges(validationOrigins, source.length, childOnlyDeclarations);
1008
+ }
1009
+ if (
1010
+ (ownerRenderer.validation?.forbiddenImports?.length ?? 0) > 0 &&
1011
+ state.childValidationImportReferences.size > 0
1012
+ ) {
1013
+ const ownerReferences = new Set(
1014
+ rendererValidationImportReferences(source, filename, validationOrigins).map(
1015
+ (reference) => `${reference.start}:${reference.end}`,
1016
+ ),
1017
+ );
1018
+ const childOnlyImports = [...state.childValidationImportReferences]
1019
+ .filter(([key]) => !ownerReferences.has(key))
1020
+ .map(([, reference]) => reference);
1021
+ validationOrigins = maskOriginalRanges(validationOrigins, source.length, childOnlyImports);
1022
+ }
895
1023
  return Object.freeze({
896
1024
  analysis: tree.analysis,
897
1025
  domRegions: Object.freeze(state.domRegions),
@@ -901,6 +1029,7 @@ export function prepareRendererBoundaryRegions(source, filename, ownerRenderer,
901
1029
  // scoped-style hashes after a region must be restamped from authored
902
1030
  // coordinates (compile.js).
903
1031
  origins: transformed.origins,
1032
+ ...(ownerRenderer.validation === undefined ? {} : { validationOrigins }),
904
1033
  source: transformed.code,
905
1034
  universalUnits: Object.freeze(state.universalUnits),
906
1035
  });