@octanejs/vite-plugin 0.1.6 → 0.1.9
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 +5 -4
- package/src/client-assets.js +81 -0
- package/src/index.js +159 -51
- package/src/load-config.js +48 -5
- package/types/index.d.ts +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/vite-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -47,16 +47,17 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@ripple-ts/adapter": "^0.3.86",
|
|
50
|
-
"@octanejs/app-core": "0.0.
|
|
50
|
+
"@octanejs/app-core": "0.0.5"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"vite": "^8.0.16",
|
|
54
|
-
"octane": "0.1.
|
|
54
|
+
"octane": "0.1.9"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "^24.3.0",
|
|
58
|
+
"playwright": "^1.61.0",
|
|
58
59
|
"type-fest": "^5.6.0",
|
|
59
60
|
"vite": "^8.0.16",
|
|
60
|
-
"octane": "0.1.
|
|
61
|
+
"octane": "0.1.9"
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { HYDRATE_QUERY_PARAM } from 'octane/compiler/bundler';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {{
|
|
6
|
+
* file: string,
|
|
7
|
+
* src?: string,
|
|
8
|
+
* css?: string[],
|
|
9
|
+
* imports?: string[],
|
|
10
|
+
* dynamicImports?: string[],
|
|
11
|
+
* }} ViteManifestEntry
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** @param {string | undefined} id */
|
|
15
|
+
function isDeferredHydrationId(id) {
|
|
16
|
+
if (!id) return false;
|
|
17
|
+
const queryStart = id.indexOf('?');
|
|
18
|
+
if (queryStart === -1) return false;
|
|
19
|
+
const hashStart = id.indexOf('#', queryStart);
|
|
20
|
+
const query = id.slice(queryStart + 1, hashStart === -1 ? undefined : hashStart);
|
|
21
|
+
return new URLSearchParams(query).has(HYDRATE_QUERY_PARAM);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the route asset map consumed by the production server.
|
|
26
|
+
*
|
|
27
|
+
* A normal dynamic import stays lazy in both channels. Compiler-generated
|
|
28
|
+
* `?octane-hydrate=` imports are different: their JavaScript remains deferred,
|
|
29
|
+
* but their CSS must be present while the server-rendered boundary is inert.
|
|
30
|
+
* Once inside one of those branches, collect CSS through the whole async
|
|
31
|
+
* descendant graph so nested Hydrate/lazy components cannot flash unstyled.
|
|
32
|
+
*
|
|
33
|
+
* @param {Record<string, ViteManifestEntry>} manifest
|
|
34
|
+
* @param {string[]} moduleIds
|
|
35
|
+
* @returns {Record<string, { js: string, css: string[] }>}
|
|
36
|
+
*/
|
|
37
|
+
export function createClientAssetMap(manifest, moduleIds) {
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} key
|
|
40
|
+
* @param {boolean} deferredHydrationBranch
|
|
41
|
+
* @param {Set<string>} visited
|
|
42
|
+
* @returns {string[]}
|
|
43
|
+
*/
|
|
44
|
+
function collectCss(key, deferredHydrationBranch, visited) {
|
|
45
|
+
const visitKey = `${deferredHydrationBranch ? 'deferred' : 'eager'}:${key}`;
|
|
46
|
+
if (visited.has(visitKey)) return [];
|
|
47
|
+
visited.add(visitKey);
|
|
48
|
+
const entry = manifest[key];
|
|
49
|
+
if (!entry) return [];
|
|
50
|
+
|
|
51
|
+
const css = [...(entry.css || [])];
|
|
52
|
+
for (const imported of entry.imports || []) {
|
|
53
|
+
css.push(...collectCss(imported, deferredHydrationBranch, visited));
|
|
54
|
+
}
|
|
55
|
+
for (const imported of entry.dynamicImports || []) {
|
|
56
|
+
const importedEntry = manifest[imported];
|
|
57
|
+
const entersDeferredHydration =
|
|
58
|
+
deferredHydrationBranch ||
|
|
59
|
+
isDeferredHydrationId(imported) ||
|
|
60
|
+
isDeferredHydrationId(importedEntry?.src);
|
|
61
|
+
if (entersDeferredHydration) {
|
|
62
|
+
css.push(...collectCss(imported, true, visited));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return css;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @type {Record<string, { js: string, css: string[] }>} */
|
|
69
|
+
const assets = {};
|
|
70
|
+
for (const moduleId of moduleIds) {
|
|
71
|
+
// Vite manifest keys are root-relative without the leading slash.
|
|
72
|
+
const manifestKey = moduleId.startsWith('/') ? moduleId.slice(1) : moduleId;
|
|
73
|
+
const entry = manifest[manifestKey];
|
|
74
|
+
if (!entry) continue;
|
|
75
|
+
assets[moduleId] = {
|
|
76
|
+
js: entry.file,
|
|
77
|
+
css: [...new Set(collectCss(manifestKey, false, new Set()))],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return assets;
|
|
81
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** @import {Plugin, ResolvedConfig, ViteDevServer, UserConfig} from 'vite' */
|
|
3
|
-
/** @import {OctaneConfigOptions, ResolvedOctaneConfig, RenderRoute} from '@octanejs/vite-plugin' */
|
|
2
|
+
/** @import {Plugin, RenderBuiltAssetUrl, ResolvedConfig, ViteDevServer, UserConfig} from 'vite' */
|
|
3
|
+
/** @import {LoadedOctaneConfig, OctaneConfigOptions, ResolvedOctaneConfig, RenderRoute} from '@octanejs/vite-plugin' */
|
|
4
4
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
to_vite_root_import,
|
|
34
34
|
write_project_generated_file,
|
|
35
35
|
} from './project-codegen.js';
|
|
36
|
+
import { createClientAssetMap } from './client-assets.js';
|
|
36
37
|
|
|
37
38
|
import { patch_global_fetch, is_rpc_request, handle_rpc_request } from '@ripple-ts/adapter/rpc';
|
|
38
39
|
|
|
@@ -168,12 +169,37 @@ function has_route_config(config) {
|
|
|
168
169
|
return (config?.router.routes.length ?? 0) > 0;
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Every module path the server can name in #__octane_data — page entries,
|
|
174
|
+
* layouts, the preHydrate hook, and root boundaries. The generated hydrate
|
|
175
|
+
* entry maps each as a LITERAL `() => import('/src/…')`. Production needs the
|
|
176
|
+
* map so Rollup chunks and hashes the modules; dev needs it so the imports go
|
|
177
|
+
* through Vite's import analysis and share URL identity with every other
|
|
178
|
+
* importer (see the hydrate-entry load hook).
|
|
179
|
+
*
|
|
180
|
+
* @param {ResolvedOctaneConfig | null} config
|
|
181
|
+
* @returns {string[]}
|
|
182
|
+
*/
|
|
183
|
+
function collect_hydrate_module_paths(config) {
|
|
184
|
+
if (!has_route_config(config)) return [];
|
|
185
|
+
const cfg = /** @type {ResolvedOctaneConfig} */ (config);
|
|
186
|
+
const entries = cfg.router.routes
|
|
187
|
+
.filter((r) => r.type === 'render')
|
|
188
|
+
.flatMap((r) => [get_route_entry_path(/** @type {RenderRoute} */ (r).entry), r.layout]);
|
|
189
|
+
if (cfg.router.preHydrate) entries.push(cfg.router.preHydrate);
|
|
190
|
+
entries.push(
|
|
191
|
+
get_route_entry_path(cfg.rootBoundary.pending),
|
|
192
|
+
get_route_entry_path(cfg.rootBoundary.catch),
|
|
193
|
+
);
|
|
194
|
+
return [...new Set(entries.filter((e) => typeof e === 'string'))];
|
|
195
|
+
}
|
|
196
|
+
|
|
171
197
|
/**
|
|
172
198
|
* The recommended Octane Vite integration. With no octane.config.ts it behaves
|
|
173
199
|
* as a compiler plugin inside a normal Vite SPA; configured routes activate the
|
|
174
200
|
* metaframework layer.
|
|
175
201
|
*
|
|
176
|
-
* Returns an ARRAY: `[octaneCompiler(
|
|
202
|
+
* Returns an ARRAY: `[octaneCompiler(options), metaPlugin]`. The first element is
|
|
177
203
|
* octane/compiler's transform plugin — it owns ALL `.tsrx` compilation, picking
|
|
178
204
|
* client vs server mode per-module from Vite's SSR signal (so the SAME file
|
|
179
205
|
* compiles to a DOM-clone client body for the browser and to an HTML-building
|
|
@@ -188,7 +214,7 @@ function has_route_config(config) {
|
|
|
188
214
|
* node builtins external) exporting `handler`/`nodeHandler` and auto-booting
|
|
189
215
|
* under `node`. See server/virtual-entry.js and server/production.js.
|
|
190
216
|
*
|
|
191
|
-
* @param {{ hmr?: boolean, exclude?: string[] }} [inlineOptions]
|
|
217
|
+
* @param {{ hmr?: boolean, profile?: boolean, exclude?: string[], requireDirective?: boolean, renderers?: import('@octanejs/app-core').ExperimentalRendererConfigOptions }} [inlineOptions]
|
|
192
218
|
* @returns {Plugin[]}
|
|
193
219
|
*/
|
|
194
220
|
export function octane(inlineOptions = {}) {
|
|
@@ -204,6 +230,15 @@ export function octane(inlineOptions = {}) {
|
|
|
204
230
|
let isBuild = false;
|
|
205
231
|
/** @type {boolean} Is this the SSR sub-build closeBundle launches? */
|
|
206
232
|
let isSSRBuild = false;
|
|
233
|
+
/**
|
|
234
|
+
* Config dependencies that select compiler renderers. A change requires a
|
|
235
|
+
* server restart because the neutral compiler snapshots normalized renderer
|
|
236
|
+
* metadata before the first module transform.
|
|
237
|
+
* @type {Set<string>}
|
|
238
|
+
*/
|
|
239
|
+
const rendererConfigWatchFiles = new Set();
|
|
240
|
+
/** @type {Map<string, Promise<LoadedOctaneConfig | null>>} */
|
|
241
|
+
const startupConfigLoads = new Map();
|
|
207
242
|
/** @type {ResolvedOctaneConfig | null} Config loaded for the build (config hook, reused in closeBundle) */
|
|
208
243
|
let buildOctaneConfig = null;
|
|
209
244
|
/** @type {string[]} Module paths the generated client entry maps statically (build only) */
|
|
@@ -211,6 +246,25 @@ export function octane(inlineOptions = {}) {
|
|
|
211
246
|
/** @type {Set<string>} Vite-root paths of modules containing `module server` */
|
|
212
247
|
const serverModuleModules = new Set();
|
|
213
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Load declarative app config early enough for the compiler plugin's own
|
|
251
|
+
* `config` hook. Cache per project root for the paired compiler/meta hooks;
|
|
252
|
+
* a dev-server restart constructs a fresh plugin instance and fresh snapshot.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} projectRoot
|
|
255
|
+
* @returns {Promise<LoadedOctaneConfig | null>}
|
|
256
|
+
*/
|
|
257
|
+
function loadStartupConfig(projectRoot) {
|
|
258
|
+
const resolvedRoot = path.resolve(projectRoot);
|
|
259
|
+
let load = startupConfigLoads.get(resolvedRoot);
|
|
260
|
+
if (load !== undefined) return load;
|
|
261
|
+
load = octaneConfigExists(resolvedRoot)
|
|
262
|
+
? loadOctaneConfigWithMetadata(resolvedRoot)
|
|
263
|
+
: Promise.resolve(null);
|
|
264
|
+
startupConfigLoads.set(resolvedRoot, load);
|
|
265
|
+
return load;
|
|
266
|
+
}
|
|
267
|
+
|
|
214
268
|
/** @type {Plugin} */
|
|
215
269
|
const metaPlugin = {
|
|
216
270
|
name: '@octanejs/vite-plugin',
|
|
@@ -274,7 +328,28 @@ export function octane(inlineOptions = {}) {
|
|
|
274
328
|
if (buildOctaneConfig.build.target !== undefined) {
|
|
275
329
|
buildConfig.target = buildOctaneConfig.build.target;
|
|
276
330
|
}
|
|
277
|
-
|
|
331
|
+
const userRenderBuiltUrl = userConfig.experimental?.renderBuiltUrl;
|
|
332
|
+
/** @type {RenderBuiltAssetUrl} */
|
|
333
|
+
const renderBuiltUrl = (filename, context) => {
|
|
334
|
+
const userResult = userRenderBuiltUrl?.(filename, context);
|
|
335
|
+
if (userResult !== undefined) return userResult;
|
|
336
|
+
|
|
337
|
+
// Vite's production module-preload helper otherwise resolves its
|
|
338
|
+
// root-relative dependency URLs through document.baseURI. Generate
|
|
339
|
+
// module-relative JS asset URLs so an authored <base> cannot redirect
|
|
340
|
+
// route, layout, or pre-hydrate chunk preloads off the app origin.
|
|
341
|
+
if (!context.ssr && context.type === 'asset' && context.hostType === 'js') {
|
|
342
|
+
return { relative: true };
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
return {
|
|
346
|
+
...base,
|
|
347
|
+
build: buildConfig,
|
|
348
|
+
experimental: {
|
|
349
|
+
...userConfig.experimental,
|
|
350
|
+
renderBuiltUrl,
|
|
351
|
+
},
|
|
352
|
+
};
|
|
278
353
|
}
|
|
279
354
|
}
|
|
280
355
|
}
|
|
@@ -291,16 +366,7 @@ export function octane(inlineOptions = {}) {
|
|
|
291
366
|
buildStart() {
|
|
292
367
|
if (!isBuild || isSSRBuild || !has_route_config(buildOctaneConfig)) return;
|
|
293
368
|
serverModuleModules.clear();
|
|
294
|
-
|
|
295
|
-
const entries = cfg.router.routes
|
|
296
|
-
.filter((r) => r.type === 'render')
|
|
297
|
-
.flatMap((r) => [get_route_entry_path(/** @type {RenderRoute} */ (r).entry), r.layout]);
|
|
298
|
-
if (cfg.router.preHydrate) entries.push(cfg.router.preHydrate);
|
|
299
|
-
entries.push(
|
|
300
|
-
get_route_entry_path(cfg.rootBoundary.pending),
|
|
301
|
-
get_route_entry_path(cfg.rootBoundary.catch),
|
|
302
|
-
);
|
|
303
|
-
staticEntries = [...new Set(entries.filter((e) => typeof e === 'string'))];
|
|
369
|
+
staticEntries = collect_hydrate_module_paths(buildOctaneConfig);
|
|
304
370
|
},
|
|
305
371
|
|
|
306
372
|
async configResolved(resolvedConfig) {
|
|
@@ -324,16 +390,29 @@ export function octane(inlineOptions = {}) {
|
|
|
324
390
|
return create_adapter_browser_stub_source();
|
|
325
391
|
}
|
|
326
392
|
if (id === RESOLVED_VIRTUAL_HYDRATE_ID) {
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
393
|
+
// Production builds pass the routes' module paths (collected in
|
|
394
|
+
// buildStart) so Rollup bundles them. Dev ALSO needs the literal map —
|
|
395
|
+
// not for chunking (dev serves any module by URL) but for MODULE
|
|
396
|
+
// IDENTITY on a hot server: the codegen's fallback `dynamicImport(path)`
|
|
397
|
+
// is hidden from Vite's import analysis, so it fetches the BARE url
|
|
398
|
+
// while the page's own import chain fetches the analyzed url (`?import`
|
|
399
|
+
// for non-JS extensions, `?t=` stamps after an HMR invalidation). Two
|
|
400
|
+
// urls = two browser module instances — e.g. two app-router singletons,
|
|
401
|
+
// where preHydrate commits matches on one and the page renders the
|
|
402
|
+
// empty other, breaking hydration on every reload until the dev server
|
|
403
|
+
// restarts. Literal `import('/src/…')` entries go through import
|
|
404
|
+
// analysis and share url identity with every other importer.
|
|
405
|
+
let entries = staticEntries;
|
|
406
|
+
if (!isBuild) {
|
|
407
|
+
const loaded = octaneConfig ?? (await loadStartupConfig(root))?.config ?? null;
|
|
408
|
+
entries = collect_hydrate_module_paths(loaded);
|
|
409
|
+
}
|
|
331
410
|
const file = write_project_generated_file(
|
|
332
411
|
config,
|
|
333
412
|
'client-entry.js',
|
|
334
413
|
create_client_entry_source({
|
|
335
414
|
configPath: to_vite_root_import(getOctaneConfigPath(root), root),
|
|
336
|
-
staticEntries,
|
|
415
|
+
staticEntries: entries,
|
|
337
416
|
}),
|
|
338
417
|
);
|
|
339
418
|
return fs.readFileSync(file, 'utf-8');
|
|
@@ -365,6 +444,9 @@ export function octane(inlineOptions = {}) {
|
|
|
365
444
|
* @param {ViteDevServer} vite
|
|
366
445
|
*/
|
|
367
446
|
configureServer(vite) {
|
|
447
|
+
if (rendererConfigWatchFiles.size > 0) {
|
|
448
|
+
vite.watcher.add([...rendererConfigWatchFiles]);
|
|
449
|
+
}
|
|
368
450
|
/** @type {Promise<void> | null} */
|
|
369
451
|
let initPromise = null;
|
|
370
452
|
/** @type {number} */
|
|
@@ -537,6 +619,13 @@ export function octane(inlineOptions = {}) {
|
|
|
537
619
|
order: 'pre',
|
|
538
620
|
async handler({ file, modules, server }) {
|
|
539
621
|
if (this.environment.name !== 'client') return;
|
|
622
|
+
if (rendererConfigWatchFiles.has(path.resolve(file))) {
|
|
623
|
+
// Renderer rules and boundary metadata are immutable inputs to every
|
|
624
|
+
// compiler environment. Rebuild the plugin/compiler snapshot instead
|
|
625
|
+
// of letting later transforms observe a mixture of old and new config.
|
|
626
|
+
await server.restart();
|
|
627
|
+
return [];
|
|
628
|
+
}
|
|
540
629
|
if (modules.length > 0 && modules.every((m) => m.isSelfAccepting)) return;
|
|
541
630
|
if (!is_octane_module_path(file)) return;
|
|
542
631
|
|
|
@@ -593,7 +682,7 @@ export function octane(inlineOptions = {}) {
|
|
|
593
682
|
// tags the production server emits for the matched route).
|
|
594
683
|
// ------------------------------------------------------------------
|
|
595
684
|
const manifestPath = path.join(clientOutDir, '.vite', 'manifest.json');
|
|
596
|
-
/** @type {Record<string, { file: string, css?: string[], imports?: string[] }>} */
|
|
685
|
+
/** @type {Record<string, { file: string, src?: string, css?: string[], imports?: string[], dynamicImports?: string[] }>} */
|
|
597
686
|
let clientManifest = {};
|
|
598
687
|
if (fs.existsSync(manifestPath)) {
|
|
599
688
|
clientManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
@@ -603,35 +692,7 @@ export function octane(inlineOptions = {}) {
|
|
|
603
692
|
);
|
|
604
693
|
}
|
|
605
694
|
|
|
606
|
-
|
|
607
|
-
* All CSS a manifest entry needs, transitively (cycle-safe).
|
|
608
|
-
* @param {string} key
|
|
609
|
-
* @param {Set<string>} [visited]
|
|
610
|
-
* @returns {string[]}
|
|
611
|
-
*/
|
|
612
|
-
const collectCss = (key, visited = new Set()) => {
|
|
613
|
-
if (visited.has(key)) return [];
|
|
614
|
-
visited.add(key);
|
|
615
|
-
const entry = clientManifest[key];
|
|
616
|
-
if (!entry) return [];
|
|
617
|
-
const css = [...(entry.css || [])];
|
|
618
|
-
for (const imp of entry.imports || []) css.push(...collectCss(imp, visited));
|
|
619
|
-
return css;
|
|
620
|
-
};
|
|
621
|
-
|
|
622
|
-
/** @type {Record<string, { js: string, css: string[] }>} */
|
|
623
|
-
const clientAssetMap = {};
|
|
624
|
-
for (const moduleId of staticEntries) {
|
|
625
|
-
// Manifest keys are root-relative without the leading slash.
|
|
626
|
-
const manifestKey = moduleId.startsWith('/') ? moduleId.slice(1) : moduleId;
|
|
627
|
-
const manifestEntry = clientManifest[manifestKey];
|
|
628
|
-
if (manifestEntry) {
|
|
629
|
-
clientAssetMap[moduleId] = {
|
|
630
|
-
js: manifestEntry.file,
|
|
631
|
-
css: [...new Set(collectCss(manifestKey))],
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
}
|
|
695
|
+
const clientAssetMap = createClientAssetMap(clientManifest, staticEntries);
|
|
635
696
|
|
|
636
697
|
// The manifest was only needed here; leaving .vite/ in dist/client would
|
|
637
698
|
// publish source file paths through the static server.
|
|
@@ -752,11 +813,58 @@ export function octane(inlineOptions = {}) {
|
|
|
752
813
|
// `"octane": { "hookSlots": { "manual": ["src"] } }` in their own package.json and the
|
|
753
814
|
// compiler plugin skips those directories via a nearest-manifest lookup.
|
|
754
815
|
// Other installed raw-source Octane packages are transformed automatically.
|
|
816
|
+
/**
|
|
817
|
+
* @type {{
|
|
818
|
+
* hmr?: boolean,
|
|
819
|
+
* profile?: boolean,
|
|
820
|
+
* exclude?: string[],
|
|
821
|
+
* requireDirective?: boolean,
|
|
822
|
+
* renderers?: import('@octanejs/app-core').ExperimentalRendererConfigOptions,
|
|
823
|
+
* }}
|
|
824
|
+
*/
|
|
755
825
|
const compilerOptions = {};
|
|
756
826
|
if (inlineOptions.hmr !== undefined) compilerOptions.hmr = inlineOptions.hmr;
|
|
827
|
+
if (inlineOptions.profile !== undefined) compilerOptions.profile = inlineOptions.profile;
|
|
757
828
|
if (inlineOptions.exclude !== undefined) compilerOptions.exclude = inlineOptions.exclude;
|
|
829
|
+
if (inlineOptions.requireDirective !== undefined) {
|
|
830
|
+
compilerOptions.requireDirective = inlineOptions.requireDirective;
|
|
831
|
+
}
|
|
832
|
+
if (inlineOptions.renderers !== undefined) compilerOptions.renderers = inlineOptions.renderers;
|
|
833
|
+
const compilerPlugin = /** @type {Plugin} */ (octaneCompiler(compilerOptions));
|
|
834
|
+
const compilerConfigHook = compilerPlugin.config;
|
|
835
|
+
if (typeof compilerConfigHook === 'function') {
|
|
836
|
+
compilerPlugin.config = function compilerConfigWithAppRenderers(userConfig, env) {
|
|
837
|
+
const projectRoot = userConfig.root ? path.resolve(userConfig.root) : process.cwd();
|
|
838
|
+
// Inline renderer metadata is an explicit full override. Preserve the
|
|
839
|
+
// synchronous no-config/inline path used by compiler-only SPA projects.
|
|
840
|
+
if (inlineOptions.renderers !== undefined) {
|
|
841
|
+
rendererConfigWatchFiles.clear();
|
|
842
|
+
return compilerConfigHook.call(this, userConfig, env);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const configPath = getOctaneConfigPath(projectRoot);
|
|
846
|
+
if (!octaneConfigExists(projectRoot)) {
|
|
847
|
+
delete compilerOptions.renderers;
|
|
848
|
+
rendererConfigWatchFiles.clear();
|
|
849
|
+
// A newly-created octane.config.ts can introduce renderer rules. Watch
|
|
850
|
+
// the missing path so dev restarts into the configured compiler.
|
|
851
|
+
rendererConfigWatchFiles.add(path.resolve(configPath));
|
|
852
|
+
return compilerConfigHook.call(this, userConfig, env);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return loadStartupConfig(projectRoot).then((loaded) => {
|
|
856
|
+
const config = /** @type {LoadedOctaneConfig} */ (loaded);
|
|
857
|
+
compilerOptions.renderers = config.config.compiler.renderers;
|
|
858
|
+
rendererConfigWatchFiles.clear();
|
|
859
|
+
for (const file of [...config.dependencies, ...config.missingDependencies]) {
|
|
860
|
+
rendererConfigWatchFiles.add(path.resolve(file));
|
|
861
|
+
}
|
|
862
|
+
return compilerConfigHook.call(this, userConfig, env);
|
|
863
|
+
});
|
|
864
|
+
};
|
|
865
|
+
}
|
|
758
866
|
// The compiler plugin is untyped JS (its `enforce` infers as `string`).
|
|
759
|
-
return [
|
|
867
|
+
return [compilerPlugin, metaPlugin];
|
|
760
868
|
}
|
|
761
869
|
|
|
762
870
|
// Mainly to enforce types / DX.
|
package/src/load-config.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
|
|
3
5
|
import {
|
|
4
6
|
getOctaneConfigPath,
|
|
5
7
|
loadOctaneConfig as loadCoreOctaneConfig,
|
|
@@ -78,9 +80,7 @@ async function withDefaultViteRunner(projectRoot, options, loader) {
|
|
|
78
80
|
try {
|
|
79
81
|
return await loader(projectRoot, {
|
|
80
82
|
...options,
|
|
81
|
-
moduleRunner:
|
|
82
|
-
loadModule: (/** @type {string} */ id) => tempVite.ssrLoadModule(id),
|
|
83
|
-
},
|
|
83
|
+
moduleRunner: viteConfigModuleRunner(tempVite),
|
|
84
84
|
});
|
|
85
85
|
} finally {
|
|
86
86
|
await tempVite.close();
|
|
@@ -93,8 +93,51 @@ function withViteModuleRunner(options) {
|
|
|
93
93
|
const { vite, ...rest } = options;
|
|
94
94
|
return {
|
|
95
95
|
...rest,
|
|
96
|
-
moduleRunner:
|
|
97
|
-
|
|
96
|
+
moduleRunner: viteConfigModuleRunner(vite),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Adapt Vite's SSR runner and module graph to app-core's config-loader
|
|
102
|
+
* contract. Config evaluation itself is not enough: integrations also need
|
|
103
|
+
* the transitive file set so edits to imported renderer rules/boundary tables
|
|
104
|
+
* invalidate the compiler snapshot.
|
|
105
|
+
*
|
|
106
|
+
* @param {import('vite').ViteDevServer} vite
|
|
107
|
+
* @returns {import('@octanejs/app-core').ConfigModuleRunner}
|
|
108
|
+
*/
|
|
109
|
+
function viteConfigModuleRunner(vite) {
|
|
110
|
+
return {
|
|
111
|
+
loadModule: (/** @type {string} */ id) => vite.ssrLoadModule(id),
|
|
112
|
+
getDependencies(id) {
|
|
113
|
+
const graph = vite.environments.ssr.moduleGraph;
|
|
114
|
+
const roots = new Set();
|
|
115
|
+
const candidates = new Set([id]);
|
|
116
|
+
try {
|
|
117
|
+
// Vite canonicalizes graph IDs through realpath. Preserve the
|
|
118
|
+
// config loader's lexical path in its own metadata, but use both
|
|
119
|
+
// forms to find the root (notably /var -> /private/var on macOS).
|
|
120
|
+
candidates.add(fs.realpathSync(id));
|
|
121
|
+
} catch {
|
|
122
|
+
// The config loader reports the useful missing-file error.
|
|
123
|
+
}
|
|
124
|
+
for (const candidate of candidates) {
|
|
125
|
+
const byId = graph.getModuleById(candidate);
|
|
126
|
+
if (byId) roots.add(byId);
|
|
127
|
+
for (const module of graph.getModulesByFile(candidate) ?? []) roots.add(module);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
const dependencies = new Set();
|
|
132
|
+
/** @param {import('vite').EnvironmentModuleNode} module */
|
|
133
|
+
function visit(module) {
|
|
134
|
+
if (seen.has(module)) return;
|
|
135
|
+
seen.add(module);
|
|
136
|
+
if (module.file) dependencies.add(module.file);
|
|
137
|
+
for (const imported of module.importedModules) visit(imported);
|
|
138
|
+
}
|
|
139
|
+
for (const root of roots) visit(root);
|
|
140
|
+
return [...dependencies];
|
|
98
141
|
},
|
|
99
142
|
};
|
|
100
143
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Plugin, ViteDevServer } from 'vite';
|
|
2
2
|
import type {
|
|
3
3
|
ConfigModuleRunner,
|
|
4
|
+
ExperimentalRendererConfigOptions,
|
|
4
5
|
LoadedOctaneConfig,
|
|
5
6
|
OctaneConfigOptions,
|
|
6
7
|
ResolvedOctaneConfig,
|
|
@@ -11,11 +12,32 @@ export * from '@octanejs/app-core';
|
|
|
11
12
|
export interface OctanePluginOptions {
|
|
12
13
|
/** Override the client HMR default (on in serve mode, off for SSR). */
|
|
13
14
|
hmr?: boolean;
|
|
15
|
+
/** Enable component profiling in client transforms. */
|
|
16
|
+
profile?: boolean;
|
|
14
17
|
/**
|
|
15
18
|
* Path fragments the compiler's plain `.ts`/`.js` hook-slotting pass must
|
|
16
19
|
* skip. Prefer package manifest `octane.hookSlots.manual` declarations.
|
|
20
|
+
* With `requireDirective`, excluded paths are exempt from Octane ownership
|
|
21
|
+
* entirely — including `.tsrx`/`.tsx` — for projects routing those paths
|
|
22
|
+
* through a different tsrx compiler (e.g. `@tsrx/react`).
|
|
17
23
|
*/
|
|
18
24
|
exclude?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Mixed-toolchain ownership gate: when `true`, Octane compiles only
|
|
27
|
+
* project modules that declare `'use octane'` in their directive prologue.
|
|
28
|
+
* Undirected project `.tsx`/`.ts`/`.js` pass through to the host
|
|
29
|
+
* framework's own pipeline (e.g. React's JSX transform); an undirected
|
|
30
|
+
* project `.tsrx` is a build error. Installed and linked packages keep
|
|
31
|
+
* their Octane package-manifest decision. The directive is always
|
|
32
|
+
* tolerated and stripped from compiled output, even when this is off.
|
|
33
|
+
* @default false
|
|
34
|
+
*/
|
|
35
|
+
requireDirective?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* @experimental Full renderer-config override. When omitted, the compiler
|
|
38
|
+
* reads `compiler.renderers` from `octane.config.ts` before transforming modules.
|
|
39
|
+
*/
|
|
40
|
+
renderers?: ExperimentalRendererConfigOptions;
|
|
19
41
|
}
|
|
20
42
|
|
|
21
43
|
/** The Octane compiler plugin plus Vite app/metaframework integration. */
|