@octanejs/rspack-plugin 0.1.47 → 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/README.md +12 -0
- package/package.json +2 -2
- package/src/descriptor-children.js +116 -0
- package/src/loader.js +79 -16
- package/src/plugin.js +24 -6
- package/src/shared.js +19 -3
- package/src/text-types.js +62 -0
- package/types/index.d.ts +11 -2
package/README.md
CHANGED
|
@@ -196,6 +196,18 @@ export default {
|
|
|
196
196
|
|
|
197
197
|
The class plugin is recommended unless another integration owns those concerns.
|
|
198
198
|
|
|
199
|
+
## Scoped CSS
|
|
200
|
+
|
|
201
|
+
Scoped `<style>` blocks compile to `injectStyle(hash, css)` calls — one per
|
|
202
|
+
style scope, in lexical order — emitted as module-level statements in client
|
|
203
|
+
compilations and inside the component body per request in server
|
|
204
|
+
compilations, where the render collects them into the response's
|
|
205
|
+
`<style data-octane>` tags. A theme (`export const theme = <style>…</style>`)
|
|
206
|
+
is an ordinary module value, so importing it injects its sheet ahead of the
|
|
207
|
+
importer's own scopes. There is no virtual CSS module and no CSS HMR path:
|
|
208
|
+
editing a block re-evaluates the module like any other source change. Neither
|
|
209
|
+
the plugin nor the loader needs a CSS rule for this.
|
|
210
|
+
|
|
199
211
|
## App-level metadata
|
|
200
212
|
|
|
201
213
|
Transformed Rspack modules receive a serializable `buildInfo.octane` record
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/rspack-plugin",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
57
|
+
"octane": "0.2.7"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"test": "vitest run --config vitest.config.js"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
cleanModuleId,
|
|
5
|
+
findDescriptorChildrenExports,
|
|
6
|
+
findDescriptorChildrenImports,
|
|
7
|
+
} from 'octane/compiler/bundler';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A loader runs before Rspack publishes outgoing module-graph edges. Resolve
|
|
11
|
+
* each JSX import with its ESM resolver, then prove the named export from the
|
|
12
|
+
* resolved authored source. Every file in the proof becomes a loader dependency
|
|
13
|
+
* so changing a marker or any barrel invalidates its compiled consumers.
|
|
14
|
+
*/
|
|
15
|
+
export async function loadDescriptorChildrenImports(context, source, id) {
|
|
16
|
+
if (typeof context.getResolve !== 'function' || !/<\s*[A-Z_$]/.test(source)) return null;
|
|
17
|
+
const imports = findDescriptorChildrenImports(source, id).filter(
|
|
18
|
+
(candidate) => candidate.local !== undefined && candidate.request !== 'octane',
|
|
19
|
+
);
|
|
20
|
+
if (imports.length === 0) return null;
|
|
21
|
+
const resolver = context.getResolve({ dependencyType: 'esm' });
|
|
22
|
+
const analyses = new Map();
|
|
23
|
+
const classifications = new Map();
|
|
24
|
+
const proven = new Set();
|
|
25
|
+
|
|
26
|
+
async function analyze(resolvedId) {
|
|
27
|
+
const filename = cleanModuleId(resolvedId);
|
|
28
|
+
if (!isAbsolute(filename) || !/\.(?:[cm]?[jt]sx?|tsrx)$/.test(filename)) return null;
|
|
29
|
+
let analysis = analyses.get(filename);
|
|
30
|
+
if (analysis === undefined) {
|
|
31
|
+
context.addDependency?.(filename);
|
|
32
|
+
analysis = readFile(filename, 'utf8')
|
|
33
|
+
.then((code) => {
|
|
34
|
+
// An ordinary source with neither the marker's name nor an
|
|
35
|
+
// import/re-export pair cannot prove any binding. Keep that
|
|
36
|
+
// common case out of the parser entirely.
|
|
37
|
+
const mayReexport = code.includes('export') && code.includes('from');
|
|
38
|
+
return {
|
|
39
|
+
exports: code.includes('descriptorChildren')
|
|
40
|
+
? new Set(findDescriptorChildrenExports(code, filename))
|
|
41
|
+
: new Set(),
|
|
42
|
+
code: mayReexport ? code : null,
|
|
43
|
+
reexports: mayReexport ? undefined : [],
|
|
44
|
+
};
|
|
45
|
+
})
|
|
46
|
+
.catch((error) => {
|
|
47
|
+
if (error?.code === 'ENOENT') context.addMissingDependency?.(filename);
|
|
48
|
+
return null;
|
|
49
|
+
});
|
|
50
|
+
analyses.set(filename, analysis);
|
|
51
|
+
}
|
|
52
|
+
return analysis;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function resolveImport(request, importer) {
|
|
56
|
+
try {
|
|
57
|
+
return await resolver(dirname(cleanModuleId(importer)), request);
|
|
58
|
+
} catch {
|
|
59
|
+
// Let Rspack's module factory report unresolved imports with its own
|
|
60
|
+
// issuer trace; no export can be proven from a missing dependency.
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function isMarked(resolvedId, imported, ancestors = new Set()) {
|
|
66
|
+
const key = `${resolvedId}\0${imported}`;
|
|
67
|
+
if (ancestors.has(key)) return false;
|
|
68
|
+
let classification = ancestors.size === 0 ? classifications.get(key) : undefined;
|
|
69
|
+
if (classification === undefined) {
|
|
70
|
+
classification = (async () => {
|
|
71
|
+
const analysis = await analyze(resolvedId);
|
|
72
|
+
if (analysis === null) return false;
|
|
73
|
+
if (analysis.exports.has(imported)) return true;
|
|
74
|
+
const reexports = (analysis.reexports ??=
|
|
75
|
+
analysis.code !== null
|
|
76
|
+
? findDescriptorChildrenImports(analysis.code, cleanModuleId(resolvedId)).filter(
|
|
77
|
+
(candidate) => candidate.exported !== undefined,
|
|
78
|
+
)
|
|
79
|
+
: []);
|
|
80
|
+
const nextAncestors = new Set(ancestors);
|
|
81
|
+
nextAncestors.add(key);
|
|
82
|
+
for (const candidate of reexports) {
|
|
83
|
+
if (candidate.exported !== imported) continue;
|
|
84
|
+
const target = await resolveImport(candidate.request, resolvedId);
|
|
85
|
+
if (typeof target === 'string' && target !== resolvedId) {
|
|
86
|
+
if (await isMarked(target, candidate.imported, nextAncestors)) return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
})();
|
|
91
|
+
// A result inside a cycle can depend on the starting point. Only root
|
|
92
|
+
// classifications are safe to share across independent import paths.
|
|
93
|
+
if (ancestors.size === 0) classifications.set(key, classification);
|
|
94
|
+
}
|
|
95
|
+
return classification;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const byRequest = new Map();
|
|
99
|
+
for (const candidate of imports) {
|
|
100
|
+
const values = byRequest.get(candidate.request) ?? [];
|
|
101
|
+
values.push(candidate);
|
|
102
|
+
byRequest.set(candidate.request, values);
|
|
103
|
+
}
|
|
104
|
+
await Promise.all(
|
|
105
|
+
[...byRequest].map(async ([request, candidates]) => {
|
|
106
|
+
const target = await resolveImport(request, id);
|
|
107
|
+
if (typeof target !== 'string' || target === id) return;
|
|
108
|
+
await Promise.all(
|
|
109
|
+
candidates.map(async ({ imported }) => {
|
|
110
|
+
if (await isMarked(target, imported)) proven.add(`${request}\0${imported}`);
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
return proven.size === 0 ? null : (request, imported) => proven.has(`${request}\0${imported}`);
|
|
116
|
+
}
|
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,
|
|
@@ -13,6 +14,8 @@ import {
|
|
|
13
14
|
normalizeLoaderOptions,
|
|
14
15
|
selectLayerCompilerOptions,
|
|
15
16
|
} from './shared.js';
|
|
17
|
+
import { loadDescriptorChildrenImports } from './descriptor-children.js';
|
|
18
|
+
import { textTypeFactsForLoader } from './text-types.js';
|
|
16
19
|
|
|
17
20
|
function realRoot(path) {
|
|
18
21
|
try {
|
|
@@ -118,7 +121,6 @@ export default function octaneLoader(source, inputSourceMap) {
|
|
|
118
121
|
root,
|
|
119
122
|
profile,
|
|
120
123
|
...(options.strong === undefined ? null : { strong: options.strong }),
|
|
121
|
-
...(options.nativeReads === undefined ? null : { nativeReads: options.nativeReads }),
|
|
122
124
|
...(options.exclude === undefined ? null : { exclude: options.exclude }),
|
|
123
125
|
...(compilerOptions.renderers === undefined
|
|
124
126
|
? null
|
|
@@ -134,6 +136,41 @@ export default function octaneLoader(source, inputSourceMap) {
|
|
|
134
136
|
});
|
|
135
137
|
const id = realModuleId(this.resource ?? this.resourcePath);
|
|
136
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;
|
|
137
174
|
const cssModuleConstants =
|
|
138
175
|
this[CSS_MODULE_CONTEXT_KEY]?.enabled === true
|
|
139
176
|
? prepareCssModuleConstants(this, compiler, authoredSource, id, {
|
|
@@ -142,7 +179,7 @@ export default function octaneLoader(source, inputSourceMap) {
|
|
|
142
179
|
dev,
|
|
143
180
|
})
|
|
144
181
|
: null;
|
|
145
|
-
const finish = (clientOnlyImports, callback) => {
|
|
182
|
+
const finish = (clientOnlyImports, isDescriptorChildrenImport, textTypeFacts, callback) => {
|
|
146
183
|
try {
|
|
147
184
|
const result = compiler.transform(authoredSource, id, {
|
|
148
185
|
environment,
|
|
@@ -150,6 +187,8 @@ export default function octaneLoader(source, inputSourceMap) {
|
|
|
150
187
|
dev,
|
|
151
188
|
profile,
|
|
152
189
|
...(clientOnlyImports.length > 0 ? { clientOnlyImports } : null),
|
|
190
|
+
...(isDescriptorChildrenImport === null ? null : { isDescriptorChildrenImport }),
|
|
191
|
+
...(textTypeFacts === undefined ? null : { textTypeFacts }),
|
|
153
192
|
...cssModuleConstants?.transformOptions,
|
|
154
193
|
});
|
|
155
194
|
|
|
@@ -191,22 +230,46 @@ export default function octaneLoader(source, inputSourceMap) {
|
|
|
191
230
|
environment === 'server' && typeof compiler.clientReferenceForFile === 'function'
|
|
192
231
|
? compiler.clientReferenceForFile(id)
|
|
193
232
|
: null;
|
|
194
|
-
|
|
233
|
+
const needsServerImports =
|
|
195
234
|
environment === 'server' &&
|
|
196
235
|
currentReference === null &&
|
|
197
|
-
typeof this.getResolve === 'function'
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
236
|
+
typeof this.getResolve === 'function';
|
|
237
|
+
const requests = needsServerImports
|
|
238
|
+
? compiler.findServerImportRequests(authoredSource, id)
|
|
239
|
+
: [];
|
|
240
|
+
const mayUseDescriptorImports =
|
|
241
|
+
typeof this.getResolve === 'function' &&
|
|
242
|
+
(environment !== 'server' || currentReference === null) &&
|
|
243
|
+
/\.(?:tsrx|tsx)$/.test(cleanModuleId(id)) &&
|
|
244
|
+
/<\s*[A-Z_$]/.test(authoredSource) &&
|
|
245
|
+
authoredSource.includes('import');
|
|
246
|
+
if (requests.length > 0 || mayUseDescriptorImports || textTypes) {
|
|
247
|
+
const asyncCallback = this.async?.() ?? callback;
|
|
248
|
+
Promise.all([
|
|
249
|
+
requests.length > 0 ? resolveClientOnlyImports(this, compiler, authoredSource, id) : [],
|
|
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,
|
|
265
|
+
]).then(
|
|
266
|
+
([imports, descriptorImport, textTypeFacts]) =>
|
|
267
|
+
finish(imports, descriptorImport, textTypeFacts, asyncCallback),
|
|
268
|
+
(error) => asyncCallback(error instanceof Error ? error : new Error(String(error))),
|
|
269
|
+
);
|
|
270
|
+
return;
|
|
208
271
|
}
|
|
209
|
-
finish([], callback);
|
|
272
|
+
finish([], null, undefined, callback);
|
|
210
273
|
} catch (error) {
|
|
211
274
|
this.callback(error instanceof Error ? error : new Error(String(error)));
|
|
212
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
|