@octanejs/vite-plugin 0.1.5 → 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 +17 -4
- package/src/client-assets.js +81 -0
- package/src/config-entry.js +15 -0
- package/src/constants.js +1 -3
- package/src/index.js +229 -71
- package/src/load-config.js +103 -80
- package/src/project-codegen.js +10 -218
- package/src/resolve-config.js +1 -171
- package/src/routes.js +1 -133
- package/src/server/component-wrappers.js +5 -56
- package/src/server/html-stream.js +1 -0
- package/src/server/html-template.js +9 -0
- package/src/server/middleware.js +1 -127
- package/src/server/node-http.js +1 -188
- package/src/server/production.js +1 -286
- package/src/server/render-route.js +66 -41
- package/src/server/router.js +1 -123
- package/src/server/server-route.js +1 -47
- package/src/server/virtual-entry.js +1 -184
- package/types/index.d.ts +52 -260
- package/types/node.d.ts +1 -31
- package/types/production.d.ts +1 -71
- package/CHANGELOG.md +0 -254
- package/tests/_fixtures/app/index.html +0 -11
- package/tests/_fixtures/app/octane.config.ts +0 -14
- package/tests/_fixtures/app/package.json +0 -7
- package/tests/_fixtures/app/src/Layout.tsrx +0 -6
- package/tests/_fixtures/app/src/Page.tsrx +0 -17
- package/tests/_fixtures/app/vite.config.ts +0 -12
- package/tests/handler.test.ts +0 -134
- package/tests/plugin.test.ts +0 -155
- package/tests/production.test.ts +0 -157
- package/tsconfig.typecheck.json +0 -18
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
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": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"description": "Vite integration for Octane (SPA compilation plus optional routing, SSR, and hydration)",
|
|
7
10
|
"author": {
|
|
8
11
|
"name": "Dominic Gannaway",
|
|
9
12
|
"email": "dg@domgan.com"
|
|
@@ -16,6 +19,10 @@
|
|
|
16
19
|
"url": "git+https://github.com/octanejs/octane.git",
|
|
17
20
|
"directory": "packages/vite-plugin-octane"
|
|
18
21
|
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"types"
|
|
25
|
+
],
|
|
19
26
|
"module": "src/index.js",
|
|
20
27
|
"main": "src/index.js",
|
|
21
28
|
"bin": {
|
|
@@ -40,11 +47,17 @@
|
|
|
40
47
|
},
|
|
41
48
|
"dependencies": {
|
|
42
49
|
"@ripple-ts/adapter": "^0.3.86",
|
|
43
|
-
"
|
|
50
|
+
"@octanejs/app-core": "0.0.5"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"vite": "^8.0.16",
|
|
54
|
+
"octane": "0.1.9"
|
|
44
55
|
},
|
|
45
56
|
"devDependencies": {
|
|
46
57
|
"@types/node": "^24.3.0",
|
|
58
|
+
"playwright": "^1.61.0",
|
|
47
59
|
"type-fest": "^5.6.0",
|
|
48
|
-
"vite": "^8.0.16"
|
|
60
|
+
"vite": "^8.0.16",
|
|
61
|
+
"octane": "0.1.9"
|
|
49
62
|
}
|
|
50
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/config-entry.js
CHANGED
|
@@ -13,6 +13,21 @@
|
|
|
13
13
|
|
|
14
14
|
export { RenderRoute, ServerRoute } from './routes.js';
|
|
15
15
|
export { resolveOctaneConfig } from './resolve-config.js';
|
|
16
|
+
export { OCTANE_NONCE_STATE_KEY } from './constants.js';
|
|
17
|
+
export {
|
|
18
|
+
DEFAULT_OUTDIR,
|
|
19
|
+
ENTRY_FILENAME,
|
|
20
|
+
compose,
|
|
21
|
+
createContext,
|
|
22
|
+
createRouter,
|
|
23
|
+
get_component_export,
|
|
24
|
+
get_route_entry_export_name,
|
|
25
|
+
get_route_entry_id,
|
|
26
|
+
get_route_entry_path,
|
|
27
|
+
handleServerRoute,
|
|
28
|
+
is_rpc_request,
|
|
29
|
+
runMiddlewareChain,
|
|
30
|
+
} from '@octanejs/app-core';
|
|
16
31
|
|
|
17
32
|
// Mirrors src/index.js — enforce types / DX only.
|
|
18
33
|
export function defineConfig(/** @type {any} */ options) {
|
package/src/constants.js
CHANGED
package/src/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
9
10
|
|
|
10
11
|
import { octane as octaneCompiler } from 'octane/compiler/vite';
|
|
11
12
|
|
|
@@ -13,12 +14,14 @@ import { createRouter } from './server/router.js';
|
|
|
13
14
|
import { createContext, runMiddlewareChain } from './server/middleware.js';
|
|
14
15
|
import { handleRenderRoute } from './server/render-route.js';
|
|
15
16
|
import { handleServerRoute } from './server/server-route.js';
|
|
17
|
+
import { HYDRATION_NONCE_PLACEHOLDER, injectHydrationEntry } from './server/html-template.js';
|
|
16
18
|
import { generateServerEntry } from './server/virtual-entry.js';
|
|
17
19
|
import { nodeRequestToWebRequest, sendWebResponse } from './server/node-http.js';
|
|
18
20
|
import { ENTRY_FILENAME } from './constants.js';
|
|
19
21
|
import {
|
|
20
22
|
getOctaneConfigPath,
|
|
21
23
|
loadOctaneConfig,
|
|
24
|
+
loadOctaneConfigWithMetadata,
|
|
22
25
|
resolveOctaneConfig,
|
|
23
26
|
octaneConfigExists,
|
|
24
27
|
} from './load-config.js';
|
|
@@ -30,6 +33,7 @@ import {
|
|
|
30
33
|
to_vite_root_import,
|
|
31
34
|
write_project_generated_file,
|
|
32
35
|
} from './project-codegen.js';
|
|
36
|
+
import { createClientAssetMap } from './client-assets.js';
|
|
33
37
|
|
|
34
38
|
import { patch_global_fetch, is_rpc_request, handle_rpc_request } from '@ripple-ts/adapter/rpc';
|
|
35
39
|
|
|
@@ -37,16 +41,35 @@ import { get_route_entry_path } from './routes.js';
|
|
|
37
41
|
|
|
38
42
|
// Re-export route classes + config helpers (public API surface).
|
|
39
43
|
export { RenderRoute, ServerRoute } from './routes.js';
|
|
44
|
+
export { OCTANE_NONCE_STATE_KEY } from './constants.js';
|
|
45
|
+
export {
|
|
46
|
+
DEFAULT_OUTDIR,
|
|
47
|
+
ENTRY_FILENAME,
|
|
48
|
+
compose,
|
|
49
|
+
createContext,
|
|
50
|
+
createRouter,
|
|
51
|
+
get_component_export,
|
|
52
|
+
get_route_entry_export_name,
|
|
53
|
+
get_route_entry_id,
|
|
54
|
+
get_route_entry_path,
|
|
55
|
+
handleServerRoute,
|
|
56
|
+
is_rpc_request,
|
|
57
|
+
runMiddlewareChain,
|
|
58
|
+
} from '@octanejs/app-core';
|
|
40
59
|
export {
|
|
41
60
|
getOctaneConfigPath,
|
|
42
61
|
loadOctaneConfig,
|
|
62
|
+
loadOctaneConfigWithMetadata,
|
|
43
63
|
resolveOctaneConfig,
|
|
44
64
|
octaneConfigExists,
|
|
45
65
|
} from './load-config.js';
|
|
46
66
|
|
|
47
67
|
const VIRTUAL_HYDRATE_ID = 'virtual:octane-hydrate';
|
|
48
68
|
const RESOLVED_VIRTUAL_HYDRATE_ID = '\0virtual:octane-hydrate';
|
|
49
|
-
const
|
|
69
|
+
const requireFromPlugin = createRequire(import.meta.url);
|
|
70
|
+
// Mirrors octane/compiler/vite's full-compiler surface. Keeping this list in
|
|
71
|
+
// sync is especially important for production `module server` discovery.
|
|
72
|
+
const OCTANE_EXTENSIONS = ['.tsrx', '.tsx'];
|
|
50
73
|
|
|
51
74
|
/**
|
|
52
75
|
* @param {string} file_name
|
|
@@ -147,9 +170,36 @@ function has_route_config(config) {
|
|
|
147
170
|
}
|
|
148
171
|
|
|
149
172
|
/**
|
|
150
|
-
*
|
|
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
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The recommended Octane Vite integration. With no octane.config.ts it behaves
|
|
199
|
+
* as a compiler plugin inside a normal Vite SPA; configured routes activate the
|
|
200
|
+
* metaframework layer.
|
|
151
201
|
*
|
|
152
|
-
* Returns an ARRAY: `[octaneCompiler(
|
|
202
|
+
* Returns an ARRAY: `[octaneCompiler(options), metaPlugin]`. The first element is
|
|
153
203
|
* octane/compiler's transform plugin — it owns ALL `.tsrx` compilation, picking
|
|
154
204
|
* client vs server mode per-module from Vite's SSR signal (so the SAME file
|
|
155
205
|
* compiles to a DOM-clone client body for the browser and to an HTML-building
|
|
@@ -164,7 +214,7 @@ function has_route_config(config) {
|
|
|
164
214
|
* node builtins external) exporting `handler`/`nodeHandler` and auto-booting
|
|
165
215
|
* under `node`. See server/virtual-entry.js and server/production.js.
|
|
166
216
|
*
|
|
167
|
-
* @param {{ hmr?: boolean, exclude?: string[] }} [inlineOptions]
|
|
217
|
+
* @param {{ hmr?: boolean, profile?: boolean, exclude?: string[], requireDirective?: boolean, renderers?: import('@octanejs/app-core').ExperimentalRendererConfigOptions }} [inlineOptions]
|
|
168
218
|
* @returns {Plugin[]}
|
|
169
219
|
*/
|
|
170
220
|
export function octane(inlineOptions = {}) {
|
|
@@ -180,10 +230,40 @@ export function octane(inlineOptions = {}) {
|
|
|
180
230
|
let isBuild = false;
|
|
181
231
|
/** @type {boolean} Is this the SSR sub-build closeBundle launches? */
|
|
182
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();
|
|
183
242
|
/** @type {ResolvedOctaneConfig | null} Config loaded for the build (config hook, reused in closeBundle) */
|
|
184
243
|
let buildOctaneConfig = null;
|
|
185
244
|
/** @type {string[]} Module paths the generated client entry maps statically (build only) */
|
|
186
245
|
let staticEntries = [];
|
|
246
|
+
/** @type {Set<string>} Vite-root paths of modules containing `module server` */
|
|
247
|
+
const serverModuleModules = new Set();
|
|
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
|
+
}
|
|
187
267
|
|
|
188
268
|
/** @type {Plugin} */
|
|
189
269
|
const metaPlugin = {
|
|
@@ -196,33 +276,31 @@ export function octane(inlineOptions = {}) {
|
|
|
196
276
|
async config(userConfig, env) {
|
|
197
277
|
isBuild = env?.command === 'build';
|
|
198
278
|
isSSRBuild = !!userConfig.build?.ssr;
|
|
279
|
+
const projectRoot = userConfig.root ? path.resolve(userConfig.root) : process.cwd();
|
|
280
|
+
const hasOctaneConfig = octaneConfigExists(projectRoot);
|
|
199
281
|
|
|
200
282
|
const exclude = userConfig.optimizeDeps?.exclude || [];
|
|
201
283
|
const base = {
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
284
|
+
// A zero-config project is a normal Vite SPA: leave appType unset so
|
|
285
|
+
// Vite retains its HTML transform and history fallback. An Octane app
|
|
286
|
+
// config opts into framework routing, where SSR owns navigation and the
|
|
287
|
+
// SPA fallback must not mask unmatched routes. Respect an explicit user
|
|
288
|
+
// appType, and leave `vite preview` alone — production SSR is previewed
|
|
205
289
|
// with `octane-preview` (it serves dist/server), not `vite preview`.
|
|
206
|
-
...(userConfig.appType === undefined && !env?.isPreview
|
|
290
|
+
...(hasOctaneConfig && userConfig.appType === undefined && !env?.isPreview
|
|
207
291
|
? { appType: /** @type {const} */ ('custom') }
|
|
208
292
|
: {}),
|
|
209
293
|
optimizeDeps: {
|
|
210
294
|
exclude: [
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
...new Set([
|
|
214
|
-
...exclude,
|
|
215
|
-
'octane',
|
|
216
|
-
'octane/compiler',
|
|
217
|
-
'@octanejs/tanstack-query',
|
|
218
|
-
...SERVER_ONLY_ADAPTER_IDS,
|
|
219
|
-
]),
|
|
295
|
+
// The compiler plugin has already added every manifest-discovered
|
|
296
|
+
// raw Octane dependency to `exclude`; preserve that list here.
|
|
297
|
+
...new Set([...exclude, 'octane', 'octane/compiler', ...SERVER_ONLY_ADAPTER_IDS]),
|
|
220
298
|
],
|
|
221
299
|
},
|
|
222
|
-
//
|
|
223
|
-
//
|
|
300
|
+
// Raw binding packages are supplied recursively by the compiler plugin;
|
|
301
|
+
// these core entrypoints remain explicit metaframework dependencies.
|
|
224
302
|
ssr: {
|
|
225
|
-
noExternal: ['octane', 'octane/compiler'
|
|
303
|
+
noExternal: ['octane', 'octane/compiler'],
|
|
226
304
|
},
|
|
227
305
|
};
|
|
228
306
|
|
|
@@ -230,8 +308,7 @@ export function octane(inlineOptions = {}) {
|
|
|
230
308
|
// build.ssr, so it skips this): route the client bundle to
|
|
231
309
|
// `{outDir}/client` and emit a manifest for the server's asset map.
|
|
232
310
|
if (isBuild && !isSSRBuild) {
|
|
233
|
-
|
|
234
|
-
if (octaneConfigExists(projectRoot)) {
|
|
311
|
+
if (hasOctaneConfig) {
|
|
235
312
|
buildOctaneConfig = await loadOctaneConfig(projectRoot);
|
|
236
313
|
if (has_route_config(buildOctaneConfig)) {
|
|
237
314
|
if (!fs.existsSync(path.join(projectRoot, 'index.html'))) {
|
|
@@ -251,7 +328,28 @@ export function octane(inlineOptions = {}) {
|
|
|
251
328
|
if (buildOctaneConfig.build.target !== undefined) {
|
|
252
329
|
buildConfig.target = buildOctaneConfig.build.target;
|
|
253
330
|
}
|
|
254
|
-
|
|
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
|
+
};
|
|
255
353
|
}
|
|
256
354
|
}
|
|
257
355
|
}
|
|
@@ -267,12 +365,8 @@ export function octane(inlineOptions = {}) {
|
|
|
267
365
|
*/
|
|
268
366
|
buildStart() {
|
|
269
367
|
if (!isBuild || isSSRBuild || !has_route_config(buildOctaneConfig)) return;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
.filter((r) => r.type === 'render')
|
|
273
|
-
.flatMap((r) => [get_route_entry_path(/** @type {RenderRoute} */ (r).entry), r.layout]);
|
|
274
|
-
if (cfg.router.preHydrate) entries.push(cfg.router.preHydrate);
|
|
275
|
-
staticEntries = [...new Set(entries.filter((e) => typeof e === 'string'))];
|
|
368
|
+
serverModuleModules.clear();
|
|
369
|
+
staticEntries = collect_hydrate_module_paths(buildOctaneConfig);
|
|
276
370
|
},
|
|
277
371
|
|
|
278
372
|
async configResolved(resolvedConfig) {
|
|
@@ -296,16 +390,29 @@ export function octane(inlineOptions = {}) {
|
|
|
296
390
|
return create_adapter_browser_stub_source();
|
|
297
391
|
}
|
|
298
392
|
if (id === RESOLVED_VIRTUAL_HYDRATE_ID) {
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
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
|
+
}
|
|
303
410
|
const file = write_project_generated_file(
|
|
304
411
|
config,
|
|
305
412
|
'client-entry.js',
|
|
306
413
|
create_client_entry_source({
|
|
307
414
|
configPath: to_vite_root_import(getOctaneConfigPath(root), root),
|
|
308
|
-
staticEntries,
|
|
415
|
+
staticEntries: entries,
|
|
309
416
|
}),
|
|
310
417
|
);
|
|
311
418
|
return fs.readFileSync(file, 'utf-8');
|
|
@@ -313,6 +420,22 @@ export function octane(inlineOptions = {}) {
|
|
|
313
420
|
return null;
|
|
314
421
|
},
|
|
315
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Observe the compiler's client output after the pre-transform. A generated
|
|
425
|
+
* __serverRpc call is an exact, syntax-level signal that this source owns a
|
|
426
|
+
* `module server`; production uses the collected paths as static SSR imports.
|
|
427
|
+
*/
|
|
428
|
+
transform(code, id, options) {
|
|
429
|
+
if (!isBuild || options?.ssr || !is_octane_module_path(id.split('?')[0])) return null;
|
|
430
|
+
if (!code.includes('_$__serverRpc(')) return null;
|
|
431
|
+
const file = id.split('?')[0];
|
|
432
|
+
const relative = path.relative(root, file);
|
|
433
|
+
const isWithinRoot =
|
|
434
|
+
relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative);
|
|
435
|
+
serverModuleModules.add(isWithinRoot ? '/' + relative.split(path.sep).join('/') : file);
|
|
436
|
+
return null;
|
|
437
|
+
},
|
|
438
|
+
|
|
316
439
|
/**
|
|
317
440
|
* Dev SSR middleware. Registered as a pre-hook (no return) so it runs
|
|
318
441
|
* BEFORE Vite's HTML fallback. Config is loaded lazily on first request
|
|
@@ -321,6 +444,9 @@ export function octane(inlineOptions = {}) {
|
|
|
321
444
|
* @param {ViteDevServer} vite
|
|
322
445
|
*/
|
|
323
446
|
configureServer(vite) {
|
|
447
|
+
if (rendererConfigWatchFiles.size > 0) {
|
|
448
|
+
vite.watcher.add([...rendererConfigWatchFiles]);
|
|
449
|
+
}
|
|
324
450
|
/** @type {Promise<void> | null} */
|
|
325
451
|
let initPromise = null;
|
|
326
452
|
/** @type {number} */
|
|
@@ -493,6 +619,13 @@ export function octane(inlineOptions = {}) {
|
|
|
493
619
|
order: 'pre',
|
|
494
620
|
async handler({ file, modules, server }) {
|
|
495
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
|
+
}
|
|
496
629
|
if (modules.length > 0 && modules.every((m) => m.isSelfAccepting)) return;
|
|
497
630
|
if (!is_octane_module_path(file)) return;
|
|
498
631
|
|
|
@@ -521,8 +654,7 @@ export function octane(inlineOptions = {}) {
|
|
|
521
654
|
order: 'pre',
|
|
522
655
|
handler(html) {
|
|
523
656
|
if (!isBuild || isSSRBuild || !has_route_config(buildOctaneConfig)) return html;
|
|
524
|
-
|
|
525
|
-
return html.replace('</body>', `${hydrationScript}\n</body>`);
|
|
657
|
+
return injectHydrationEntry(html, VIRTUAL_HYDRATE_ID, HYDRATION_NONCE_PLACEHOLDER);
|
|
526
658
|
},
|
|
527
659
|
},
|
|
528
660
|
|
|
@@ -550,7 +682,7 @@ export function octane(inlineOptions = {}) {
|
|
|
550
682
|
// tags the production server emits for the matched route).
|
|
551
683
|
// ------------------------------------------------------------------
|
|
552
684
|
const manifestPath = path.join(clientOutDir, '.vite', 'manifest.json');
|
|
553
|
-
/** @type {Record<string, { file: string, css?: string[], imports?: string[] }>} */
|
|
685
|
+
/** @type {Record<string, { file: string, src?: string, css?: string[], imports?: string[], dynamicImports?: string[] }>} */
|
|
554
686
|
let clientManifest = {};
|
|
555
687
|
if (fs.existsSync(manifestPath)) {
|
|
556
688
|
clientManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
@@ -560,35 +692,7 @@ export function octane(inlineOptions = {}) {
|
|
|
560
692
|
);
|
|
561
693
|
}
|
|
562
694
|
|
|
563
|
-
|
|
564
|
-
* All CSS a manifest entry needs, transitively (cycle-safe).
|
|
565
|
-
* @param {string} key
|
|
566
|
-
* @param {Set<string>} [visited]
|
|
567
|
-
* @returns {string[]}
|
|
568
|
-
*/
|
|
569
|
-
const collectCss = (key, visited = new Set()) => {
|
|
570
|
-
if (visited.has(key)) return [];
|
|
571
|
-
visited.add(key);
|
|
572
|
-
const entry = clientManifest[key];
|
|
573
|
-
if (!entry) return [];
|
|
574
|
-
const css = [...(entry.css || [])];
|
|
575
|
-
for (const imp of entry.imports || []) css.push(...collectCss(imp, visited));
|
|
576
|
-
return css;
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
/** @type {Record<string, { js: string, css: string[] }>} */
|
|
580
|
-
const clientAssetMap = {};
|
|
581
|
-
for (const moduleId of staticEntries) {
|
|
582
|
-
// Manifest keys are root-relative without the leading slash.
|
|
583
|
-
const manifestKey = moduleId.startsWith('/') ? moduleId.slice(1) : moduleId;
|
|
584
|
-
const manifestEntry = clientManifest[manifestKey];
|
|
585
|
-
if (manifestEntry) {
|
|
586
|
-
clientAssetMap[moduleId] = {
|
|
587
|
-
js: manifestEntry.file,
|
|
588
|
-
css: [...new Set(collectCss(manifestKey))],
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
}
|
|
695
|
+
const clientAssetMap = createClientAssetMap(clientManifest, staticEntries);
|
|
592
696
|
|
|
593
697
|
// The manifest was only needed here; leaving .vite/ in dist/client would
|
|
594
698
|
// publish source file paths through the static server.
|
|
@@ -605,7 +709,14 @@ export function octane(inlineOptions = {}) {
|
|
|
605
709
|
generateServerEntry({
|
|
606
710
|
routes: cfg.router.routes,
|
|
607
711
|
octaneConfigPath: getOctaneConfigPath(root),
|
|
712
|
+
rootBoundary: cfg.rootBoundary,
|
|
713
|
+
rpcModulePaths: [...serverModuleModules],
|
|
608
714
|
clientAssetMap,
|
|
715
|
+
// The virtual entry has no filesystem importer, so resolve app-core
|
|
716
|
+
// from this package before handing source to Vite. This also works
|
|
717
|
+
// when app-core is nested under the plugin by a package manager.
|
|
718
|
+
productionModuleId: requireFromPlugin.resolve('@octanejs/app-core/production'),
|
|
719
|
+
nodeModuleId: requireFromPlugin.resolve('@octanejs/app-core/node'),
|
|
609
720
|
}),
|
|
610
721
|
);
|
|
611
722
|
|
|
@@ -700,13 +811,60 @@ export function octane(inlineOptions = {}) {
|
|
|
700
811
|
// compiler's `.ts`/`.js` hook-slotting pass must skip. Hand-slot-forwarding
|
|
701
812
|
// bindings should not need it: they declare
|
|
702
813
|
// `"octane": { "hookSlots": { "manual": ["src"] } }` in their own package.json and the
|
|
703
|
-
// compiler plugin skips
|
|
704
|
-
//
|
|
814
|
+
// compiler plugin skips those directories via a nearest-manifest lookup.
|
|
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
|
+
*/
|
|
705
825
|
const compilerOptions = {};
|
|
706
826
|
if (inlineOptions.hmr !== undefined) compilerOptions.hmr = inlineOptions.hmr;
|
|
827
|
+
if (inlineOptions.profile !== undefined) compilerOptions.profile = inlineOptions.profile;
|
|
707
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
|
+
}
|
|
708
866
|
// The compiler plugin is untyped JS (its `enforce` infers as `string`).
|
|
709
|
-
return [
|
|
867
|
+
return [compilerPlugin, metaPlugin];
|
|
710
868
|
}
|
|
711
869
|
|
|
712
870
|
// Mainly to enforce types / DX.
|