@octanejs/rspack-plugin 0.1.48 → 0.1.49

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.49",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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.7"
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,6 @@ 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 }),
123
124
  ...(options.exclude === undefined ? null : { exclude: options.exclude }),
124
125
  ...(compilerOptions.renderers === undefined
125
126
  ? null
@@ -135,6 +136,41 @@ export default function octaneLoader(source, inputSourceMap) {
135
136
  });
136
137
  const id = realModuleId(this.resource ?? this.resourcePath);
137
138
  const authoredSource = String(source);
139
+ const textTypeCandidate =
140
+ options.textTypes !== undefined && this.mode === 'production' && !dev && this.hot !== true;
141
+ let textTypeFilename;
142
+ if (textTypeCandidate) {
143
+ const file = cleanModuleId(id);
144
+ if (
145
+ /(?:\.tsrx|\.tsx)$/i.test(file) &&
146
+ existsSync(file) &&
147
+ compiler._isProjectOwnedSource(file)
148
+ ) {
149
+ const canonical = compiler._canonicalModuleId(file);
150
+ const pragmaOwned =
151
+ file.endsWith('.tsx') && compiler._pragmaClaimsOwnership(authoredSource);
152
+ if (compiler._passesOwnershipGate(file, canonical, pragmaOwned)) {
153
+ const renderer = resolveRendererForFile(compiler.renderers, canonical);
154
+ if (
155
+ renderer.target === 'dom' &&
156
+ !(environment === 'server' && renderer.server === 'client-only')
157
+ ) {
158
+ textTypeFilename = canonical;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ if (textTypeFilename !== undefined) {
164
+ // An imported type can change while this module's source stays the same.
165
+ // Rspack's persistent module cache has no dependency edges to every TS
166
+ // Program input. Production watches stay syntax-only but must not persist
167
+ // their generic output for a later one-shot typed build either.
168
+ this.cacheable?.(false);
169
+ }
170
+ const textTypes =
171
+ textTypeFilename !== undefined &&
172
+ this._compiler?.watchMode !== true &&
173
+ this._compiler?.options?.watch !== true;
138
174
  const cssModuleConstants =
139
175
  this[CSS_MODULE_CONTEXT_KEY]?.enabled === true
140
176
  ? prepareCssModuleConstants(this, compiler, authoredSource, id, {
@@ -143,7 +179,7 @@ export default function octaneLoader(source, inputSourceMap) {
143
179
  dev,
144
180
  })
145
181
  : null;
146
- const finish = (clientOnlyImports, isDescriptorChildrenImport, callback) => {
182
+ const finish = (clientOnlyImports, isDescriptorChildrenImport, textTypeFacts, callback) => {
147
183
  try {
148
184
  const result = compiler.transform(authoredSource, id, {
149
185
  environment,
@@ -152,6 +188,7 @@ export default function octaneLoader(source, inputSourceMap) {
152
188
  profile,
153
189
  ...(clientOnlyImports.length > 0 ? { clientOnlyImports } : null),
154
190
  ...(isDescriptorChildrenImport === null ? null : { isDescriptorChildrenImport }),
191
+ ...(textTypeFacts === undefined ? null : { textTypeFacts }),
155
192
  ...cssModuleConstants?.transformOptions,
156
193
  });
157
194
 
@@ -206,18 +243,33 @@ export default function octaneLoader(source, inputSourceMap) {
206
243
  /\.(?:tsrx|tsx)$/.test(cleanModuleId(id)) &&
207
244
  /<\s*[A-Z_$]/.test(authoredSource) &&
208
245
  authoredSource.includes('import');
209
- if (requests.length > 0 || mayUseDescriptorImports) {
246
+ if (requests.length > 0 || mayUseDescriptorImports || textTypes) {
210
247
  const asyncCallback = this.async?.() ?? callback;
211
248
  Promise.all([
212
249
  requests.length > 0 ? resolveClientOnlyImports(this, compiler, authoredSource, id) : [],
213
250
  mayUseDescriptorImports ? loadDescriptorChildrenImports(this, authoredSource, id) : null,
251
+ textTypes
252
+ ? textTypeFactsForLoader(
253
+ this._compiler,
254
+ options.textTypes.tsconfig,
255
+ compilerOptions.renderers,
256
+ cleanModuleId(id),
257
+ authoredSource,
258
+ ).then((facts) => ({
259
+ ...facts,
260
+ // The neutral compiler uses a project-relative module identity;
261
+ // the checker necessarily reads the real filesystem filename.
262
+ filename: textTypeFilename,
263
+ }))
264
+ : undefined,
214
265
  ]).then(
215
- ([imports, descriptorImport]) => finish(imports, descriptorImport, asyncCallback),
266
+ ([imports, descriptorImport, textTypeFacts]) =>
267
+ finish(imports, descriptorImport, textTypeFacts, asyncCallback),
216
268
  (error) => asyncCallback(error instanceof Error ? error : new Error(String(error))),
217
269
  );
218
270
  return;
219
271
  }
220
- finish([], null, callback);
272
+ finish([], null, undefined, callback);
221
273
  } catch (error) {
222
274
  this.callback(error instanceof Error ? error : new Error(String(error)));
223
275
  }
package/src/plugin.js CHANGED
@@ -14,6 +14,11 @@ import {
14
14
  inferRspackEnvironment,
15
15
  normalizePluginOptions,
16
16
  } from './shared.js';
17
+ import {
18
+ disposeTextTypeCompiler,
19
+ invalidateTextTypeCompiler,
20
+ registerTextTypeCompiler,
21
+ } from './text-types.js';
17
22
 
18
23
  const PLUGIN_NAME = 'OctaneRspackPlugin';
19
24
  const PROFILE_DEFINE = '__OCTANE_PROFILE_ENABLED__';
@@ -177,7 +182,6 @@ function createDiscoveryCompiler(options, root, profile, specialization) {
177
182
  root,
178
183
  profile,
179
184
  ...(options.strong === undefined ? null : { strong: options.strong }),
180
- ...(options.nativeReads === undefined ? null : { nativeReads: options.nativeReads }),
181
185
  ...(options.exclude === undefined ? null : { exclude: options.exclude }),
182
186
  ...(renderers === undefined ? null : { renderers }),
183
187
  ...(universalRuntime === undefined ? null : { universalRuntime }),
@@ -360,6 +364,16 @@ export class OctaneRspackPlugin {
360
364
  compiler.options.mode === 'production' &&
361
365
  !dev &&
362
366
  !hotModuleReplacement;
367
+ // Loader worker pools cannot share one TypeScript Program. Watch and HMR
368
+ // use syntax-only compilation, including production-mode watch builds.
369
+ const textTypes =
370
+ this.options.textTypes !== undefined &&
371
+ compiler.options.mode === 'production' &&
372
+ !dev &&
373
+ !hotModuleReplacement;
374
+ const tsconfig = textTypes
375
+ ? realRoot(resolve(root, this.options.textTypes.tsconfig))
376
+ : undefined;
363
377
  assertProfilingDefineAvailable(compiler, profile);
364
378
  saltPersistentCacheVersion(compiler, {
365
379
  root,
@@ -368,7 +382,6 @@ export class OctaneRspackPlugin {
368
382
  dev,
369
383
  profile,
370
384
  strong: this.options.strong === true,
371
- nativeReads: this.options.nativeReads === true,
372
385
  exclude: this.options.exclude ?? [],
373
386
  renderers: this.options.renderers?.signature,
374
387
  runtime: this.options.runtime,
@@ -379,7 +392,14 @@ export class OctaneRspackPlugin {
379
392
  requireDirective: this.options.requireDirective === true,
380
393
  transpile: this.options.transpile !== false,
381
394
  cssModuleConstants,
395
+ textTypes: tsconfig,
382
396
  });
397
+ if (tsconfig !== undefined) {
398
+ registerTextTypeCompiler(compiler, tsconfig, root);
399
+ compiler.hooks.run?.tap(PLUGIN_NAME, () => invalidateTextTypeCompiler(compiler));
400
+ compiler.hooks.watchRun?.tap(PLUGIN_NAME, () => invalidateTextTypeCompiler(compiler));
401
+ compiler.hooks.shutdown?.tap(PLUGIN_NAME, () => disposeTextTypeCompiler(compiler));
402
+ }
383
403
  installProfilingDefine(compiler, profile);
384
404
  if (cssModuleConstants) {
385
405
  installCssModuleConstants(compiler, {
@@ -409,9 +429,6 @@ export class OctaneRspackPlugin {
409
429
  environment,
410
430
  profile,
411
431
  ...(this.options.strong === undefined ? null : { strong: this.options.strong }),
412
- ...(this.options.nativeReads === undefined
413
- ? null
414
- : { nativeReads: this.options.nativeReads }),
415
432
  ...(this.options.hmr === undefined ? null : { hmr: this.options.hmr }),
416
433
  ...(this.options.dev === undefined ? null : { dev: this.options.dev }),
417
434
  ...(this.options.exclude === undefined ? null : { exclude: this.options.exclude }),
@@ -425,13 +442,14 @@ export class OctaneRspackPlugin {
425
442
  ...(this.options.requireDirective === undefined
426
443
  ? null
427
444
  : { requireDirective: this.options.requireDirective }),
445
+ ...(tsconfig === undefined ? null : { textTypes: { tsconfig } }),
428
446
  };
429
447
  compiler.options.module.rules.push({
430
448
  test: OCTANE_RULE,
431
449
  type: 'javascript/auto',
432
450
  enforce: 'pre',
433
451
  use:
434
- this.options.parallel === false
452
+ this.options.parallel === false || textTypes
435
453
  ? [{ loader: loaderPath, options: loaderOptions }]
436
454
  : [
437
455
  { loader: finalizeLoaderPath, options: loaderOptions },
package/src/shared.js CHANGED
@@ -41,12 +41,12 @@ const LOADER_OPTION_KEYS = new Set([
41
41
  'dev',
42
42
  'profile',
43
43
  'strong',
44
- 'nativeReads',
45
44
  'exclude',
46
45
  'renderers',
47
46
  'requireDirective',
48
47
  'universalRuntime',
49
48
  'layerSpecializations',
49
+ 'textTypes',
50
50
  ]);
51
51
  const PLUGIN_OPTION_KEYS = new Set([
52
52
  ...LOADER_OPTION_KEYS,
@@ -209,7 +209,6 @@ function normalizeOptions(value, plugin) {
209
209
  assertBooleanOption(options, 'dev');
210
210
  assertBooleanOption(options, 'profile');
211
211
  assertBooleanOption(options, 'strong');
212
- assertBooleanOption(options, 'nativeReads');
213
212
  assertBooleanOption(options, 'requireDirective');
214
213
  if (
215
214
  options.exclude !== undefined &&
@@ -218,6 +217,21 @@ function normalizeOptions(value, plugin) {
218
217
  throw new TypeError('@octanejs/rspack-plugin: `exclude` must be an array of path strings.');
219
218
  }
220
219
  if (plugin) assertBooleanOption(options, 'transpile');
220
+ if (options.textTypes !== undefined) {
221
+ if (
222
+ options.textTypes === null ||
223
+ typeof options.textTypes !== 'object' ||
224
+ Array.isArray(options.textTypes) ||
225
+ Object.keys(options.textTypes).some((key) => key !== 'tsconfig') ||
226
+ typeof options.textTypes.tsconfig !== 'string' ||
227
+ options.textTypes.tsconfig.trim() !== options.textTypes.tsconfig ||
228
+ !options.textTypes.tsconfig
229
+ ) {
230
+ throw new TypeError(
231
+ '@octanejs/rspack-plugin: `textTypes` must be an object with a non-empty `tsconfig` path.',
232
+ );
233
+ }
234
+ }
221
235
  if (
222
236
  plugin &&
223
237
  options.cssModuleConstants !== undefined &&
@@ -241,7 +255,6 @@ function normalizeOptions(value, plugin) {
241
255
  ...(options.dev === undefined ? null : { dev: options.dev }),
242
256
  ...(options.profile === undefined ? null : { profile: options.profile }),
243
257
  ...(options.strong === undefined ? null : { strong: options.strong }),
244
- ...(options.nativeReads === undefined ? null : { nativeReads: options.nativeReads }),
245
258
  ...(options.exclude === undefined ? null : { exclude: [...options.exclude] }),
246
259
  ...(renderers === undefined ? null : { renderers }),
247
260
  ...(universalRuntime === undefined ? null : { universalRuntime }),
@@ -249,6 +262,9 @@ function normalizeOptions(value, plugin) {
249
262
  ...(options.requireDirective === undefined
250
263
  ? null
251
264
  : { requireDirective: options.requireDirective }),
265
+ ...(options.textTypes === undefined
266
+ ? null
267
+ : { textTypes: Object.freeze({ tsconfig: options.textTypes.tsconfig }) }),
252
268
  ...(plugin && parallel !== undefined ? { parallel } : null),
253
269
  ...(plugin && options.transpile !== undefined ? { transpile: options.transpile } : null),
254
270
  ...(plugin && options.runtime !== undefined ? { runtime: options.runtime } : null),
@@ -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
@@ -144,8 +144,6 @@ 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;
149
147
  /**
150
148
  * Path fragments excluded from the plain `.ts`/`.js` hook-slot pass. With
151
149
  * `requireDirective`, excluded paths are exempt from Octane ownership
@@ -177,6 +175,17 @@ export interface OctaneRspackLoaderOptions {
177
175
  }
178
176
 
179
177
  export interface OctaneRspackPluginOptions extends OctaneRspackLoaderOptions {
178
+ /**
179
+ * @experimental Prove primitive DOM text children from the TypeScript project
180
+ * in one-shot production builds. Relative paths resolve from the plugin root.
181
+ * Requires the optional TypeScript peer. Uses one main-thread checker per
182
+ * compiler, and typed modules bypass Rspack's persistent module cache so
183
+ * imported type edits are observed on the next build. Watch/HMR and
184
+ * development builds retain syntax-only classification. Configure the same
185
+ * tsconfig and renderers for browser and server compilation.
186
+ * @default undefined
187
+ */
188
+ textTypes?: { tsconfig: string };
180
189
  /**
181
190
  * @experimental Fold proven CSS-module strings in one-shot production builds.
182
191
  * `true` accepts only pure, initialized named ESM string exports. A provider