@octanejs/rspack-plugin 0.1.48 → 0.1.50

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/rspack-plugin",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@rspack/core": "^2.0.0",
48
- "octane": "^0.1.51 || ^0.2.0"
48
+ "octane": "^0.2.13"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@rspack/core": "^2.1.4",
@@ -54,7 +54,7 @@
54
54
  "css-loader": "6.11.0",
55
55
  "jsdom": "^30.0.1",
56
56
  "vitest": "^4.1.10",
57
- "octane": "0.2.4"
57
+ "octane": "0.2.13"
58
58
  },
59
59
  "scripts": {
60
60
  "test": "vitest run --config vitest.config.js"
package/src/loader.js CHANGED
@@ -1,7 +1,8 @@
1
- import { realpathSync } from 'node:fs';
1
+ import { existsSync, realpathSync } from 'node:fs';
2
2
  import { dirname, isAbsolute, resolve } from 'node:path';
3
3
  import remapping from '@jridgewell/remapping';
4
4
  import { canonicalModuleId, cleanModuleId, createOctaneCompiler } from 'octane/compiler/bundler';
5
+ import { resolveRendererForFile } from 'octane/compiler/renderers';
5
6
  import {
6
7
  clearCssModuleBuildInfo,
7
8
  CSS_MODULE_CONTEXT_KEY,
@@ -14,6 +15,7 @@ import {
14
15
  selectLayerCompilerOptions,
15
16
  } from './shared.js';
16
17
  import { loadDescriptorChildrenImports } from './descriptor-children.js';
18
+ import { textTypeFactsForLoader } from './text-types.js';
17
19
 
18
20
  function realRoot(path) {
19
21
  try {
@@ -119,7 +121,9 @@ export default function octaneLoader(source, inputSourceMap) {
119
121
  root,
120
122
  profile,
121
123
  ...(options.strong === undefined ? null : { strong: options.strong }),
122
- ...(options.nativeReads === undefined ? null : { nativeReads: options.nativeReads }),
124
+ ...(options.knownAttributeSpreads === undefined
125
+ ? null
126
+ : { knownAttributeSpreads: options.knownAttributeSpreads }),
123
127
  ...(options.exclude === undefined ? null : { exclude: options.exclude }),
124
128
  ...(compilerOptions.renderers === undefined
125
129
  ? null
@@ -135,6 +139,41 @@ export default function octaneLoader(source, inputSourceMap) {
135
139
  });
136
140
  const id = realModuleId(this.resource ?? this.resourcePath);
137
141
  const authoredSource = String(source);
142
+ const textTypeCandidate =
143
+ options.textTypes !== undefined && this.mode === 'production' && !dev && this.hot !== true;
144
+ let textTypeFilename;
145
+ if (textTypeCandidate) {
146
+ const file = cleanModuleId(id);
147
+ if (
148
+ /(?:\.tsrx|\.tsx)$/i.test(file) &&
149
+ existsSync(file) &&
150
+ compiler._isProjectOwnedSource(file)
151
+ ) {
152
+ const canonical = compiler._canonicalModuleId(file);
153
+ const pragmaOwned =
154
+ file.endsWith('.tsx') && compiler._pragmaClaimsOwnership(authoredSource);
155
+ if (compiler._passesOwnershipGate(file, canonical, pragmaOwned)) {
156
+ const renderer = resolveRendererForFile(compiler.renderers, canonical);
157
+ if (
158
+ renderer.target === 'dom' &&
159
+ !(environment === 'server' && renderer.server === 'client-only')
160
+ ) {
161
+ textTypeFilename = canonical;
162
+ }
163
+ }
164
+ }
165
+ }
166
+ if (textTypeFilename !== undefined) {
167
+ // An imported type can change while this module's source stays the same.
168
+ // Rspack's persistent module cache has no dependency edges to every TS
169
+ // Program input. Production watches stay syntax-only but must not persist
170
+ // their generic output for a later one-shot typed build either.
171
+ this.cacheable?.(false);
172
+ }
173
+ const textTypes =
174
+ textTypeFilename !== undefined &&
175
+ this._compiler?.watchMode !== true &&
176
+ this._compiler?.options?.watch !== true;
138
177
  const cssModuleConstants =
139
178
  this[CSS_MODULE_CONTEXT_KEY]?.enabled === true
140
179
  ? prepareCssModuleConstants(this, compiler, authoredSource, id, {
@@ -143,7 +182,7 @@ export default function octaneLoader(source, inputSourceMap) {
143
182
  dev,
144
183
  })
145
184
  : null;
146
- const finish = (clientOnlyImports, isDescriptorChildrenImport, callback) => {
185
+ const finish = (clientOnlyImports, isDescriptorChildrenImport, textTypeFacts, callback) => {
147
186
  try {
148
187
  const result = compiler.transform(authoredSource, id, {
149
188
  environment,
@@ -152,6 +191,7 @@ export default function octaneLoader(source, inputSourceMap) {
152
191
  profile,
153
192
  ...(clientOnlyImports.length > 0 ? { clientOnlyImports } : null),
154
193
  ...(isDescriptorChildrenImport === null ? null : { isDescriptorChildrenImport }),
194
+ ...(textTypeFacts === undefined ? null : { textTypeFacts }),
155
195
  ...cssModuleConstants?.transformOptions,
156
196
  });
157
197
 
@@ -168,11 +208,16 @@ export default function octaneLoader(source, inputSourceMap) {
168
208
  }
169
209
  setBuildInfo(this._module, {
170
210
  canonicalId: canonicalModuleId(id, root),
211
+ resourceQuery: id.slice(cleanModuleId(id).length),
171
212
  transformKind: result.kind,
213
+ ...(result.streamedSignals === true ? { streamedSignals: true } : null),
172
214
  serverRpc:
173
215
  result.kind === 'compile' &&
174
216
  (result.code.includes('_$__serverRpc(') ||
175
217
  result.code.includes('export const _$_server_$_')),
218
+ ...(Array.isArray(result.independentWidgets) && result.independentWidgets.length > 0
219
+ ? { independentWidgets: result.independentWidgets }
220
+ : null),
176
221
  ...(result.universalRuntime === undefined
177
222
  ? null
178
223
  : { universalRuntime: result.universalRuntime }),
@@ -206,18 +251,33 @@ export default function octaneLoader(source, inputSourceMap) {
206
251
  /\.(?:tsrx|tsx)$/.test(cleanModuleId(id)) &&
207
252
  /<\s*[A-Z_$]/.test(authoredSource) &&
208
253
  authoredSource.includes('import');
209
- if (requests.length > 0 || mayUseDescriptorImports) {
254
+ if (requests.length > 0 || mayUseDescriptorImports || textTypes) {
210
255
  const asyncCallback = this.async?.() ?? callback;
211
256
  Promise.all([
212
257
  requests.length > 0 ? resolveClientOnlyImports(this, compiler, authoredSource, id) : [],
213
258
  mayUseDescriptorImports ? loadDescriptorChildrenImports(this, authoredSource, id) : null,
259
+ textTypes
260
+ ? textTypeFactsForLoader(
261
+ this._compiler,
262
+ options.textTypes.tsconfig,
263
+ compilerOptions.renderers,
264
+ cleanModuleId(id),
265
+ authoredSource,
266
+ ).then((facts) => ({
267
+ ...facts,
268
+ // The neutral compiler uses a project-relative module identity;
269
+ // the checker necessarily reads the real filesystem filename.
270
+ filename: textTypeFilename,
271
+ }))
272
+ : undefined,
214
273
  ]).then(
215
- ([imports, descriptorImport]) => finish(imports, descriptorImport, asyncCallback),
274
+ ([imports, descriptorImport, textTypeFacts]) =>
275
+ finish(imports, descriptorImport, textTypeFacts, asyncCallback),
216
276
  (error) => asyncCallback(error instanceof Error ? error : new Error(String(error))),
217
277
  );
218
278
  return;
219
279
  }
220
- finish([], null, callback);
280
+ finish([], null, undefined, callback);
221
281
  } catch (error) {
222
282
  this.callback(error instanceof Error ? error : new Error(String(error)));
223
283
  }
package/src/plugin.js CHANGED
@@ -5,15 +5,22 @@ import { dirname, isAbsolute, join, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import {
7
7
  CLIENT_REFERENCE_MANIFEST_FILENAME,
8
+ INDEPENDENT_HYDRATION_MANIFEST_FILENAME,
8
9
  createClientReferenceManifest,
9
10
  createOctaneCompiler,
10
11
  } from 'octane/compiler/bundler';
11
12
  import { installCssModuleConstants } from './css-module-constants.js';
13
+ import { createStreamedSignalHmrRuntimeModule } from './streamed-signals-hmr.js';
12
14
  import {
13
15
  getOctaneRspackBuildInfo,
14
16
  inferRspackEnvironment,
15
17
  normalizePluginOptions,
16
18
  } from './shared.js';
19
+ import {
20
+ disposeTextTypeCompiler,
21
+ invalidateTextTypeCompiler,
22
+ registerTextTypeCompiler,
23
+ } from './text-types.js';
17
24
 
18
25
  const PLUGIN_NAME = 'OctaneRspackPlugin';
19
26
  const PROFILE_DEFINE = '__OCTANE_PROFILE_ENABLED__';
@@ -177,7 +184,9 @@ function createDiscoveryCompiler(options, root, profile, specialization) {
177
184
  root,
178
185
  profile,
179
186
  ...(options.strong === undefined ? null : { strong: options.strong }),
180
- ...(options.nativeReads === undefined ? null : { nativeReads: options.nativeReads }),
187
+ ...(options.knownAttributeSpreads === undefined
188
+ ? null
189
+ : { knownAttributeSpreads: options.knownAttributeSpreads }),
181
190
  ...(options.exclude === undefined ? null : { exclude: options.exclude }),
182
191
  ...(renderers === undefined ? null : { renderers }),
183
192
  ...(universalRuntime === undefined ? null : { universalRuntime }),
@@ -230,16 +239,17 @@ function visitClientReferenceModules(
230
239
  inheritedChunks,
231
240
  visit,
232
241
  seen = new Set(),
242
+ executableModule = module,
233
243
  ) {
234
244
  if (!module || seen.has(module)) return;
235
245
  seen.add(module);
236
246
  const chunks = moduleChunks(compilation, module, inheritedChunks);
237
- visit(module, chunks);
247
+ visit(module, chunks, executableModule);
238
248
  for (const child of iterable(module.modules)) {
239
- visitClientReferenceModules(compilation, child, chunks, visit, seen);
249
+ visitClientReferenceModules(compilation, child, chunks, visit, seen, module);
240
250
  }
241
251
  if (module.rootModule) {
242
- visitClientReferenceModules(compilation, module.rootModule, chunks, visit, seen);
252
+ visitClientReferenceModules(compilation, module.rootModule, chunks, visit, seen, module);
243
253
  }
244
254
  }
245
255
 
@@ -275,6 +285,158 @@ function emitClientReferenceManifest(compiler, compilation) {
275
285
  );
276
286
  }
277
287
 
288
+ function emitIndependentHydrationManifest(compiler, compilation, mode) {
289
+ const modules = collectIndependentHydrationModules(compilation);
290
+ const widgets = {};
291
+ for (const { template, target } of independentHydrationTargets(modules)) {
292
+ const activationChunks = new Set(target.chunks);
293
+ for (const chunk of target.chunks) {
294
+ for (const referenced of iterable(chunk?.getAllReferencedChunks?.())) {
295
+ activationChunks.add(referenced);
296
+ }
297
+ }
298
+ const files = [...activationChunks]
299
+ .flatMap((chunk) => [...iterable(chunk.files), ...iterable(chunk.auxiliaryFiles)])
300
+ .map(String);
301
+ widgets[template.boundaryId] = {
302
+ version: 1,
303
+ boundaryId: template.boundaryId,
304
+ // Rspack chunks are runtime payloads, not native module namespaces. The
305
+ // initial runtime module below owns the actual chunk-load + require closure.
306
+ moduleId: template.boundaryId,
307
+ exportName: template.exportName,
308
+ captureSchema: template.captureSchema,
309
+ hookSeed: template.hookSeed,
310
+ idSeed: template.idSeed,
311
+ signalSites: template.signalSites,
312
+ styles: [...new Set(files.filter((file) => /\.css(?:\?|$)/.test(file)))].sort(),
313
+ parentDependencies: false,
314
+ };
315
+ }
316
+ const records = Object.fromEntries(
317
+ Object.entries(widgets).sort(([left], [right]) => left.localeCompare(right)),
318
+ );
319
+ // Match the native __webpack_hash__ value captured by the executing entry.
320
+ // A widget-schema hash does not fence changes elsewhere in its client graph.
321
+ const buildId = compilation.hash;
322
+ if (typeof buildId !== 'string' || !buildId) {
323
+ throw new Error('Octane client build has no completed compilation hash.');
324
+ }
325
+ const independentHydration = Object.keys(records).length !== 0;
326
+ compilation.emitAsset(
327
+ 'octane-client-build.json',
328
+ new compiler.webpack.sources.RawSource(
329
+ JSON.stringify(
330
+ { version: 1, buildId, mode, capabilities: { independentHydration } },
331
+ null,
332
+ 2,
333
+ ) + '\n',
334
+ ),
335
+ );
336
+ if (!independentHydration) return;
337
+ compilation.emitAsset(
338
+ INDEPENDENT_HYDRATION_MANIFEST_FILENAME,
339
+ new compiler.webpack.sources.RawSource(
340
+ JSON.stringify({ version: 1, buildId, widgets: records }, null, 2) + '\n',
341
+ ),
342
+ );
343
+ }
344
+
345
+ function collectIndependentHydrationModules(compilation) {
346
+ const modules = [];
347
+ for (const topLevelModule of iterable(compilation.modules)) {
348
+ visitClientReferenceModules(
349
+ compilation,
350
+ topLevelModule,
351
+ [],
352
+ (module, chunks, executableModule) => {
353
+ const info = getOctaneRspackBuildInfo(module);
354
+ if (info !== null) modules.push({ module, executableModule, info, chunks });
355
+ },
356
+ new Set(),
357
+ );
358
+ }
359
+ return modules;
360
+ }
361
+
362
+ function independentHydrationTargets(modules) {
363
+ const targets = [];
364
+ for (const { info } of modules) {
365
+ for (const template of info.independentWidgets ?? []) {
366
+ const query = template.request.slice(template.request.indexOf('?'));
367
+ const candidates = modules.filter(
368
+ (candidate) =>
369
+ candidate.info.canonicalId === template.moduleId &&
370
+ candidate.info.resourceQuery?.includes(query),
371
+ );
372
+ const target =
373
+ candidates.find(
374
+ (candidate) =>
375
+ candidate.executableModule != null && candidate.executableModule !== candidate.module,
376
+ ) ?? candidates[0];
377
+ if (target === undefined) {
378
+ throw new Error(
379
+ `Octane independent Hydrate ${JSON.stringify(template.boundaryId)} has no emitted activation chunk.`,
380
+ );
381
+ }
382
+ targets.push({ template, target });
383
+ }
384
+ }
385
+ return targets;
386
+ }
387
+
388
+ function independentHydrationRuntimeModule(compiler, compilation) {
389
+ const { RuntimeGlobals, RuntimeModule } = compiler.webpack;
390
+ return class OctaneIndependentHydrationRuntimeModule extends RuntimeModule {
391
+ constructor() {
392
+ super('octane independent hydration loaders');
393
+ this.fullHash = true;
394
+ }
395
+
396
+ generate() {
397
+ const registrations = [];
398
+ const seen = new Set();
399
+ for (const { template, target } of independentHydrationTargets(
400
+ collectIndependentHydrationModules(compilation),
401
+ )) {
402
+ if (seen.has(template.boundaryId)) continue;
403
+ seen.add(template.boundaryId);
404
+ const candidates = [target.executableModule, target.module, target.module?.rootModule];
405
+ const executable = candidates.find(
406
+ (module) => module != null && compilation.chunkGraph.getModuleId(module) != null,
407
+ );
408
+ if (executable === undefined) {
409
+ throw new Error(
410
+ `Octane independent Hydrate ${JSON.stringify(template.boundaryId)} has no executable Rspack module.`,
411
+ );
412
+ }
413
+ const moduleId = compilation.chunkGraph.getModuleId(executable);
414
+ const activationChunks = new Set(target.chunks);
415
+ for (const chunk of target.chunks) {
416
+ for (const referenced of iterable(chunk?.getAllReferencedChunks?.())) {
417
+ activationChunks.add(referenced);
418
+ }
419
+ }
420
+ const chunkIds = [...activationChunks]
421
+ .filter((chunk) => chunk !== this.chunk && chunk?.id != null)
422
+ .map((chunk) => chunk.id)
423
+ .sort((left, right) => String(left).localeCompare(String(right)));
424
+ const load =
425
+ chunkIds.length === 0
426
+ ? 'Promise.resolve()'
427
+ : `Promise.all(${JSON.stringify(chunkIds)}.map(${RuntimeGlobals.ensureChunk}))`;
428
+ registrations.push(
429
+ `${RuntimeGlobals.global}.__OCTANE_INDEPENDENT_MODULES__[${JSON.stringify(template.boundaryId)}] = function() { return ${load}.then(function() { return ${RuntimeGlobals.require}(${JSON.stringify(moduleId)}); }); };`,
430
+ );
431
+ }
432
+ return [
433
+ `${RuntimeGlobals.global}.__OCTANE_INDEPENDENT_MODULES__ ||= Object.create(null);`,
434
+ ...registrations,
435
+ ].join('\n');
436
+ }
437
+ };
438
+ }
439
+
278
440
  function defineMatchesBoolean(value, expected) {
279
441
  return value === expected || value === JSON.stringify(expected);
280
442
  }
@@ -360,6 +522,16 @@ export class OctaneRspackPlugin {
360
522
  compiler.options.mode === 'production' &&
361
523
  !dev &&
362
524
  !hotModuleReplacement;
525
+ // Loader worker pools cannot share one TypeScript Program. Watch and HMR
526
+ // use syntax-only compilation, including production-mode watch builds.
527
+ const textTypes =
528
+ this.options.textTypes !== undefined &&
529
+ compiler.options.mode === 'production' &&
530
+ !dev &&
531
+ !hotModuleReplacement;
532
+ const tsconfig = textTypes
533
+ ? realRoot(resolve(root, this.options.textTypes.tsconfig))
534
+ : undefined;
363
535
  assertProfilingDefineAvailable(compiler, profile);
364
536
  saltPersistentCacheVersion(compiler, {
365
537
  root,
@@ -368,7 +540,7 @@ export class OctaneRspackPlugin {
368
540
  dev,
369
541
  profile,
370
542
  strong: this.options.strong === true,
371
- nativeReads: this.options.nativeReads === true,
543
+ knownAttributeSpreads: this.options.knownAttributeSpreads,
372
544
  exclude: this.options.exclude ?? [],
373
545
  renderers: this.options.renderers?.signature,
374
546
  runtime: this.options.runtime,
@@ -379,7 +551,14 @@ export class OctaneRspackPlugin {
379
551
  requireDirective: this.options.requireDirective === true,
380
552
  transpile: this.options.transpile !== false,
381
553
  cssModuleConstants,
554
+ textTypes: tsconfig,
382
555
  });
556
+ if (tsconfig !== undefined) {
557
+ registerTextTypeCompiler(compiler, tsconfig, root);
558
+ compiler.hooks.run?.tap(PLUGIN_NAME, () => invalidateTextTypeCompiler(compiler));
559
+ compiler.hooks.watchRun?.tap(PLUGIN_NAME, () => invalidateTextTypeCompiler(compiler));
560
+ compiler.hooks.shutdown?.tap(PLUGIN_NAME, () => disposeTextTypeCompiler(compiler));
561
+ }
383
562
  installProfilingDefine(compiler, profile);
384
563
  if (cssModuleConstants) {
385
564
  installCssModuleConstants(compiler, {
@@ -409,9 +588,9 @@ export class OctaneRspackPlugin {
409
588
  environment,
410
589
  profile,
411
590
  ...(this.options.strong === undefined ? null : { strong: this.options.strong }),
412
- ...(this.options.nativeReads === undefined
591
+ ...(this.options.knownAttributeSpreads === undefined
413
592
  ? null
414
- : { nativeReads: this.options.nativeReads }),
593
+ : { knownAttributeSpreads: this.options.knownAttributeSpreads }),
415
594
  ...(this.options.hmr === undefined ? null : { hmr: this.options.hmr }),
416
595
  ...(this.options.dev === undefined ? null : { dev: this.options.dev }),
417
596
  ...(this.options.exclude === undefined ? null : { exclude: this.options.exclude }),
@@ -425,13 +604,14 @@ export class OctaneRspackPlugin {
425
604
  ...(this.options.requireDirective === undefined
426
605
  ? null
427
606
  : { requireDirective: this.options.requireDirective }),
607
+ ...(tsconfig === undefined ? null : { textTypes: { tsconfig } }),
428
608
  };
429
609
  compiler.options.module.rules.push({
430
610
  test: OCTANE_RULE,
431
611
  type: 'javascript/auto',
432
612
  enforce: 'pre',
433
613
  use:
434
- this.options.parallel === false
614
+ this.options.parallel === false || textTypes
435
615
  ? [{ loader: loaderPath, options: loaderOptions }]
436
616
  : [
437
617
  { loader: finalizeLoaderPath, options: loaderOptions },
@@ -497,12 +677,56 @@ export class OctaneRspackPlugin {
497
677
  addDependencies(compilation.fileDependencies, current.dependencies);
498
678
  addDependencies(compilation.missingDependencies, current.missingDependencies);
499
679
  if (environment === 'client') {
680
+ const IndependentHydrationRuntimeModule = independentHydrationRuntimeModule(
681
+ compiler,
682
+ compilation,
683
+ );
684
+ compilation.hooks.additionalTreeRuntimeRequirements?.tap(
685
+ PLUGIN_NAME,
686
+ (chunk, runtimeRequirements) => {
687
+ if (!chunk.hasRuntime?.()) return;
688
+ const modules = collectIndependentHydrationModules(compilation);
689
+ const featureModules = modules.filter(
690
+ ({ info }) =>
691
+ info.streamedSignals === true || (info.independentWidgets?.length ?? 0) > 0,
692
+ );
693
+ if (
694
+ featureModules.length > 0 &&
695
+ compiler.options.plugins.some(
696
+ (plugin) => plugin instanceof compiler.webpack.HotModuleReplacementPlugin,
697
+ )
698
+ ) {
699
+ runtimeRequirements.add(compiler.webpack.RuntimeGlobals.interceptModuleExecution);
700
+ runtimeRequirements.add(compiler.webpack.RuntimeGlobals.moduleCache);
701
+ runtimeRequirements.add(compiler.webpack.RuntimeGlobals.global);
702
+ compilation.addRuntimeModule(
703
+ chunk,
704
+ createStreamedSignalHmrRuntimeModule(compiler, compilation, featureModules),
705
+ );
706
+ }
707
+ if (independentHydrationTargets(modules).length === 0) {
708
+ return;
709
+ }
710
+ runtimeRequirements.add(compiler.webpack.RuntimeGlobals.ensureChunk);
711
+ runtimeRequirements.add(compiler.webpack.RuntimeGlobals.global);
712
+ runtimeRequirements.add(compiler.webpack.RuntimeGlobals.require);
713
+ compilation.addRuntimeModule(chunk, new IndependentHydrationRuntimeModule());
714
+ },
715
+ );
500
716
  compilation.hooks.processAssets.tap(
501
717
  {
502
718
  name: PLUGIN_NAME,
503
719
  stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT,
504
720
  },
505
- () => emitClientReferenceManifest(compiler, compilation),
721
+ () => {
722
+ emitClientReferenceManifest(compiler, compilation);
723
+ emitIndependentHydrationManifest(
724
+ compiler,
725
+ compilation,
726
+ this.options.clientBuildMode ??
727
+ (compiler.options.mode === 'development' ? 'development' : 'production'),
728
+ );
729
+ },
506
730
  );
507
731
  }
508
732
  });
package/src/shared.js CHANGED
@@ -41,12 +41,13 @@ const LOADER_OPTION_KEYS = new Set([
41
41
  'dev',
42
42
  'profile',
43
43
  'strong',
44
- 'nativeReads',
44
+ 'knownAttributeSpreads',
45
45
  'exclude',
46
46
  'renderers',
47
47
  'requireDirective',
48
48
  'universalRuntime',
49
49
  'layerSpecializations',
50
+ 'textTypes',
50
51
  ]);
51
52
  const PLUGIN_OPTION_KEYS = new Set([
52
53
  ...LOADER_OPTION_KEYS,
@@ -54,6 +55,7 @@ const PLUGIN_OPTION_KEYS = new Set([
54
55
  'runtime',
55
56
  'transpile',
56
57
  'cssModuleConstants',
58
+ 'clientBuildMode',
57
59
  ]);
58
60
  const LAYER_SPECIALIZATION_KEYS = new Set(['runtime', 'renderers', 'universalRuntime']);
59
61
 
@@ -198,6 +200,16 @@ function normalizeOptions(value, plugin) {
198
200
  throw new TypeError('@octanejs/rspack-plugin: `root` must be a path string.');
199
201
  }
200
202
  if (plugin) normalizeRuntimeRequest(options.runtime);
203
+ if (
204
+ plugin &&
205
+ options.clientBuildMode !== undefined &&
206
+ options.clientBuildMode !== 'production' &&
207
+ options.clientBuildMode !== 'development'
208
+ ) {
209
+ throw new TypeError(
210
+ '@octanejs/rspack-plugin: `clientBuildMode` must be "production" or "development".',
211
+ );
212
+ }
201
213
  if (
202
214
  options.environment !== undefined &&
203
215
  options.environment !== 'client' &&
@@ -209,7 +221,6 @@ function normalizeOptions(value, plugin) {
209
221
  assertBooleanOption(options, 'dev');
210
222
  assertBooleanOption(options, 'profile');
211
223
  assertBooleanOption(options, 'strong');
212
- assertBooleanOption(options, 'nativeReads');
213
224
  assertBooleanOption(options, 'requireDirective');
214
225
  if (
215
226
  options.exclude !== undefined &&
@@ -218,6 +229,21 @@ function normalizeOptions(value, plugin) {
218
229
  throw new TypeError('@octanejs/rspack-plugin: `exclude` must be an array of path strings.');
219
230
  }
220
231
  if (plugin) assertBooleanOption(options, 'transpile');
232
+ if (options.textTypes !== undefined) {
233
+ if (
234
+ options.textTypes === null ||
235
+ typeof options.textTypes !== 'object' ||
236
+ Array.isArray(options.textTypes) ||
237
+ Object.keys(options.textTypes).some((key) => key !== 'tsconfig') ||
238
+ typeof options.textTypes.tsconfig !== 'string' ||
239
+ options.textTypes.tsconfig.trim() !== options.textTypes.tsconfig ||
240
+ !options.textTypes.tsconfig
241
+ ) {
242
+ throw new TypeError(
243
+ '@octanejs/rspack-plugin: `textTypes` must be an object with a non-empty `tsconfig` path.',
244
+ );
245
+ }
246
+ }
221
247
  if (
222
248
  plugin &&
223
249
  options.cssModuleConstants !== undefined &&
@@ -241,7 +267,9 @@ function normalizeOptions(value, plugin) {
241
267
  ...(options.dev === undefined ? null : { dev: options.dev }),
242
268
  ...(options.profile === undefined ? null : { profile: options.profile }),
243
269
  ...(options.strong === undefined ? null : { strong: options.strong }),
244
- ...(options.nativeReads === undefined ? null : { nativeReads: options.nativeReads }),
270
+ ...(options.knownAttributeSpreads === undefined
271
+ ? null
272
+ : { knownAttributeSpreads: options.knownAttributeSpreads }),
245
273
  ...(options.exclude === undefined ? null : { exclude: [...options.exclude] }),
246
274
  ...(renderers === undefined ? null : { renderers }),
247
275
  ...(universalRuntime === undefined ? null : { universalRuntime }),
@@ -249,7 +277,13 @@ function normalizeOptions(value, plugin) {
249
277
  ...(options.requireDirective === undefined
250
278
  ? null
251
279
  : { requireDirective: options.requireDirective }),
280
+ ...(options.textTypes === undefined
281
+ ? null
282
+ : { textTypes: Object.freeze({ tsconfig: options.textTypes.tsconfig }) }),
252
283
  ...(plugin && parallel !== undefined ? { parallel } : null),
284
+ ...(plugin && options.clientBuildMode !== undefined
285
+ ? { clientBuildMode: options.clientBuildMode }
286
+ : null),
253
287
  ...(plugin && options.transpile !== undefined ? { transpile: options.transpile } : null),
254
288
  ...(plugin && options.runtime !== undefined ? { runtime: options.runtime } : null),
255
289
  ...(plugin && options.cssModuleConstants !== undefined
@@ -291,6 +325,8 @@ export function getOctaneRspackBuildInfo(module) {
291
325
  value.transformKind === 'slots' ||
292
326
  value.transformKind === 'client-only-stub') &&
293
327
  typeof value.serverRpc === 'boolean' &&
328
+ (value.resourceQuery === undefined || typeof value.resourceQuery === 'string') &&
329
+ (value.independentWidgets === undefined || Array.isArray(value.independentWidgets)) &&
294
330
  (value.clientReference === undefined || nestedReferenceValid) &&
295
331
  (value.universalRuntime === undefined || universalRuntimeValid)
296
332
  ) {
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Fence feature-bearing documents through Rspack's native module-execution
3
+ * interceptor, before authored factories run. No compiled source is rewritten.
4
+ * @param {import('@rspack/core').Compiler} compiler
5
+ * @param {import('@rspack/core').Compilation} compilation
6
+ * @param {Array<{ module: import('@rspack/core').Module, executableModule?: import('@rspack/core').Module }>} modules
7
+ */
8
+ export function createStreamedSignalHmrRuntimeModule(compiler, compilation, modules) {
9
+ const { RuntimeGlobals, RuntimeModule } = compiler.webpack;
10
+ return new (class extends RuntimeModule {
11
+ constructor() {
12
+ super('octane streamed signal HMR fence', RuntimeModule.STAGE_TRIGGER);
13
+ }
14
+
15
+ generate() {
16
+ const ids = new Set();
17
+ for (const { module, executableModule } of modules) {
18
+ for (const candidate of [executableModule, module, module?.rootModule]) {
19
+ const id = candidate == null ? null : compilation.chunkGraph.getModuleId(candidate);
20
+ if (id != null) {
21
+ ids.add(String(id));
22
+ break;
23
+ }
24
+ }
25
+ }
26
+ return `
27
+ var octaneSignalModules = new Set(${JSON.stringify([...ids].sort())});
28
+ var octaneSignalFenceInstalled = false;
29
+ var octaneSignalGenerationChanged = false;
30
+ var octaneSignalModuleExecuted = false;
31
+ function octaneReloadSignalDocument() {
32
+ ${RuntimeGlobals.global}.location?.reload?.();
33
+ throw new Error('[octane] The streamed signal client build changed. Reload this document.');
34
+ }
35
+ function octaneTrackSignalGeneration(hot) {
36
+ if (!hot || octaneSignalFenceInstalled) return;
37
+ octaneSignalFenceInstalled = true;
38
+ if (hot.status() !== 'idle') octaneSignalGenerationChanged = true;
39
+ hot.addStatusHandler(function(status) {
40
+ if (status === 'apply') octaneSignalGenerationChanged = true;
41
+ if (octaneSignalModuleExecuted && (status === 'check' || status === 'dispose' || status === 'apply')) octaneReloadSignalDocument();
42
+ });
43
+ }
44
+ // A newly introduced runtime may itself arrive during hot application. The
45
+ // entry's cached hot module supplies that state before any feature executes.
46
+ for (var octaneCachedId in ${RuntimeGlobals.moduleCache}) {
47
+ octaneTrackSignalGeneration(${RuntimeGlobals.moduleCache}[octaneCachedId].hot);
48
+ if (octaneSignalFenceInstalled) break;
49
+ }
50
+ ${RuntimeGlobals.interceptModuleExecution}.push(function(options) {
51
+ var hot = options.module.hot;
52
+ octaneTrackSignalGeneration(hot);
53
+ if (!hot || !octaneSignalModules.has(String(options.id))) return;
54
+ if (octaneSignalGenerationChanged || hot.status() !== 'idle') octaneReloadSignalDocument();
55
+ octaneSignalModuleExecuted = true;
56
+ });`;
57
+ }
58
+ })();
59
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Type analysis is an opt-in production build concern. Keep TypeScript out of
3
+ * the ordinary loader's module graph and retain one project per compiler and
4
+ * renderer configuration, rather than one TypeScript Program per module.
5
+ */
6
+ // Rspack loads the plugin from its application's module graph and the loader
7
+ // through its loader runner. Those can instantiate this ESM module separately
8
+ // (for example under Vitest), even though both receive the same compiler.
9
+ // Keep the project on that compiler under a process-wide symbol instead of in
10
+ // module-local state. Typed builds use the main-thread loader.
11
+ const COMPILER_TEXT_TYPES = Symbol.for('@octanejs/rspack-plugin/text-types/v1');
12
+
13
+ function compilerState(compiler) {
14
+ return compiler?.[COMPILER_TEXT_TYPES];
15
+ }
16
+
17
+ export function registerTextTypeCompiler(compiler, tsconfig, root) {
18
+ if (compilerState(compiler)) {
19
+ throw new Error('@octanejs/rspack-plugin: only one text type project may own a compiler.');
20
+ }
21
+ Object.defineProperty(compiler, COMPILER_TEXT_TYPES, {
22
+ value: { tsconfig, root, projects: new Map(), closed: false },
23
+ configurable: true,
24
+ });
25
+ }
26
+
27
+ export function invalidateTextTypeCompiler(compiler) {
28
+ const state = compilerState(compiler);
29
+ if (!state || state.closed) return;
30
+ for (const project of state.projects.values()) project.invalidate();
31
+ }
32
+
33
+ export function disposeTextTypeCompiler(compiler) {
34
+ const state = compilerState(compiler);
35
+ if (!state) return;
36
+ state.closed = true;
37
+ for (const project of state.projects.values()) project.dispose();
38
+ state.projects.clear();
39
+ delete compiler[COMPILER_TEXT_TYPES];
40
+ }
41
+
42
+ export async function textTypeFactsForLoader(compiler, tsconfig, renderers, filename, source) {
43
+ const state = compilerState(compiler);
44
+ if (!state || state.tsconfig !== tsconfig || state.closed) {
45
+ throw new Error(
46
+ '@octanejs/rspack-plugin: `textTypes` requires the OctaneRspackPlugin on the same compiler.',
47
+ );
48
+ }
49
+ // The optional TypeScript peer and Volar adapter are loaded only by an opted-in
50
+ // production compiler. import() also shares its module initialization across
51
+ // concurrently built modules without retaining a checker in loader options.
52
+ const { createTextTypeProject } = await import('octane/compiler/typescript');
53
+ if (state.closed)
54
+ throw new Error('@octanejs/rspack-plugin: compiler closed during type analysis.');
55
+ const key = renderers?.signature ?? 'dom';
56
+ let project = state.projects.get(key);
57
+ if (!project) {
58
+ project = createTextTypeProject({ tsconfig, root: state.root, renderers });
59
+ state.projects.set(key, project);
60
+ }
61
+ return project.snapshot(filename, source);
62
+ }
package/types/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Compiler, RspackPluginInstance } from '@rspack/core';
2
- import type { OctaneCssModuleConstants } from 'octane/compiler';
2
+ import type { KnownAttributeSpread, OctaneCssModuleConstants } from 'octane/compiler';
3
3
 
4
4
  export type { OctaneCssModuleConstants } from 'octane/compiler';
5
5
 
@@ -144,8 +144,8 @@ export interface OctaneRspackLoaderOptions {
144
144
  * @default false
145
145
  */
146
146
  strong?: boolean;
147
- /** Experimental native signal reads in DOM client/server render scopes. */
148
- nativeReads?: boolean;
147
+ /** Fixed native-attribute shapes for trusted imported spread helpers. */
148
+ knownAttributeSpreads?: readonly KnownAttributeSpread[];
149
149
  /**
150
150
  * Path fragments excluded from the plain `.ts`/`.js` hook-slot pass. With
151
151
  * `requireDirective`, excluded paths are exempt from Octane ownership
@@ -177,6 +177,19 @@ export interface OctaneRspackLoaderOptions {
177
177
  }
178
178
 
179
179
  export interface OctaneRspackPluginOptions extends OctaneRspackLoaderOptions {
180
+ /** Build intent for SSR metadata; independent of optimization/HMR settings. */
181
+ clientBuildMode?: 'production' | 'development';
182
+ /**
183
+ * @experimental Prove primitive DOM text children from the TypeScript project
184
+ * in one-shot production builds. Relative paths resolve from the plugin root.
185
+ * Requires the optional TypeScript peer. Uses one main-thread checker per
186
+ * compiler, and typed modules bypass Rspack's persistent module cache so
187
+ * imported type edits are observed on the next build. Watch/HMR and
188
+ * development builds retain syntax-only classification. Configure the same
189
+ * tsconfig and renderers for browser and server compilation.
190
+ * @default undefined
191
+ */
192
+ textTypes?: { tsconfig: string };
180
193
  /**
181
194
  * @experimental Fold proven CSS-module strings in one-shot production builds.
182
195
  * `true` accepts only pure, initialized named ESM string exports. A provider
@@ -217,8 +230,25 @@ export interface OctaneRspackPluginOptions extends OctaneRspackLoaderOptions {
217
230
 
218
231
  export interface OctaneRspackBuildInfo {
219
232
  canonicalId: string;
233
+ /** Resource query retained separately from the canonical source identity. */
234
+ resourceQuery?: string;
220
235
  transformKind: 'compile' | 'slots' | 'client-only-stub';
236
+ streamedSignals?: true;
221
237
  serverRpc: boolean;
238
+ /** Strict-independent Hydrate templates completed into the emitted client manifest. */
239
+ independentWidgets?: readonly {
240
+ readonly version: 1;
241
+ readonly boundaryId: string;
242
+ readonly moduleId: string;
243
+ readonly exportName: string;
244
+ readonly request: string;
245
+ readonly captureSchema: readonly { readonly name: string; readonly type: 'json' }[];
246
+ readonly hookSeed: number;
247
+ readonly idSeed: number;
248
+ readonly signalSites: readonly string[];
249
+ readonly styles: readonly string[];
250
+ readonly parentDependencies: false;
251
+ }[];
222
252
  /** Universal host runtime/thread identity, when this module was specialized. */
223
253
  universalRuntime?: OctaneUniversalRuntimeOptions;
224
254
  /** Stable identity shared by the client compile and its inert server stub. */