@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/src/load-config.js
CHANGED
|
@@ -1,95 +1,64 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/**
|
|
3
|
-
* Shared utility for loading and resolving octane.config.ts.
|
|
4
|
-
*
|
|
5
|
-
* `resolveOctaneConfig` is the single source of truth for all config
|
|
6
|
-
* validation and default values. Every consumer should receive a
|
|
7
|
-
* `ResolvedOctaneConfig` rather than applying ad-hoc defaults.
|
|
8
|
-
*
|
|
9
|
-
* `loadOctaneConfig` is the single entry point for loading the config
|
|
10
|
-
* file. It accepts an optional Vite dev server — when provided the
|
|
11
|
-
* config is loaded via `ssrLoadModule` (no temp server overhead,
|
|
12
|
-
* HMR-aware). Otherwise a temporary Vite server is spun up, used to
|
|
13
|
-
* transpile the TypeScript config, and immediately shut down.
|
|
14
|
-
*
|
|
15
|
-
* Used by the Vite plugin (during dev + build), the preview CLI script,
|
|
16
|
-
* and the generated production server entry.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/** @import { OctaneConfigOptions, ResolvedOctaneConfig } from '@octanejs/vite-plugin' */
|
|
20
2
|
|
|
21
|
-
import path from 'node:path';
|
|
22
3
|
import fs from 'node:fs';
|
|
23
|
-
import { compile } from 'octane/compiler';
|
|
24
|
-
import { resolveOctaneConfig } from './resolve-config.js';
|
|
25
4
|
|
|
26
|
-
|
|
5
|
+
import {
|
|
6
|
+
getOctaneConfigPath,
|
|
7
|
+
loadOctaneConfig as loadCoreOctaneConfig,
|
|
8
|
+
loadOctaneConfigWithMetadata as loadCoreOctaneConfigWithMetadata,
|
|
9
|
+
octaneConfigExists,
|
|
10
|
+
resolveOctaneConfig,
|
|
11
|
+
} from '@octanejs/app-core/config-loader';
|
|
12
|
+
import { compile } from 'octane/compiler';
|
|
27
13
|
|
|
28
|
-
|
|
29
|
-
// compiler imports) so the production server bundle can include it without
|
|
30
|
-
// dragging the toolchain along. Re-exported here for existing importers.
|
|
31
|
-
export { resolveOctaneConfig } from './resolve-config.js';
|
|
14
|
+
export { getOctaneConfigPath, octaneConfigExists, resolveOctaneConfig };
|
|
32
15
|
|
|
33
16
|
/**
|
|
34
|
-
*
|
|
17
|
+
* Vite compatibility facade over app-core's bundler-neutral config loader.
|
|
18
|
+
* A live dev server is adapted to the neutral module-runner contract. Build
|
|
19
|
+
* and preview calls use a temporary Vite module runner so lazy config imports
|
|
20
|
+
* keep Vite's transform semantics without making app-core depend on Vite.
|
|
35
21
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* @
|
|
22
|
+
* @param {string} projectRoot
|
|
23
|
+
* @param {{
|
|
24
|
+
* vite?: import('vite').ViteDevServer,
|
|
25
|
+
* moduleRunner?: import('@octanejs/app-core').ConfigModuleRunner | import('@octanejs/app-core').ConfigModuleRunner['loadModule'],
|
|
26
|
+
* requireAdapter?: boolean,
|
|
27
|
+
* configFile?: string,
|
|
28
|
+
* cacheDir?: string,
|
|
29
|
+
* }} [options]
|
|
40
30
|
*/
|
|
41
|
-
export function
|
|
42
|
-
return
|
|
31
|
+
export async function loadOctaneConfig(projectRoot, options = {}) {
|
|
32
|
+
return withDefaultViteRunner(projectRoot, options, loadCoreOctaneConfig);
|
|
43
33
|
}
|
|
44
34
|
|
|
45
35
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
36
|
+
* @param {string} projectRoot
|
|
37
|
+
* @param {{
|
|
38
|
+
* vite?: import('vite').ViteDevServer,
|
|
39
|
+
* moduleRunner?: import('@octanejs/app-core').ConfigModuleRunner | import('@octanejs/app-core').ConfigModuleRunner['loadModule'],
|
|
40
|
+
* requireAdapter?: boolean,
|
|
41
|
+
* configFile?: string,
|
|
42
|
+
* cacheDir?: string,
|
|
43
|
+
* }} [options]
|
|
53
44
|
*/
|
|
54
|
-
export function
|
|
55
|
-
return
|
|
45
|
+
export async function loadOctaneConfigWithMetadata(projectRoot, options = {}) {
|
|
46
|
+
return withDefaultViteRunner(projectRoot, options, loadCoreOctaneConfigWithMetadata);
|
|
56
47
|
}
|
|
57
48
|
|
|
58
49
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* When no dev server is available (build / preview), a temporary Vite server
|
|
66
|
-
* is created in middleware mode, used to transpile the config, then shut down.
|
|
67
|
-
*
|
|
68
|
-
* Throws if the config file does not exist or is invalid.
|
|
69
|
-
*
|
|
70
|
-
* @param {string} projectRoot - Absolute path to the project root
|
|
71
|
-
* @param {{ vite?: import('vite').ViteDevServer, requireAdapter?: boolean }} [options]
|
|
72
|
-
* @returns {Promise<ResolvedOctaneConfig>}
|
|
50
|
+
* @template T
|
|
51
|
+
* @param {string} projectRoot
|
|
52
|
+
* @param {Record<string, any>} options
|
|
53
|
+
* @param {(root: string, options: any) => Promise<T>} loader
|
|
54
|
+
* @returns {Promise<T>}
|
|
73
55
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!fs.existsSync(configPath)) {
|
|
79
|
-
throw new Error(`[@octanejs/vite-plugin] octane.config.ts not found in ${projectRoot}`);
|
|
56
|
+
async function withDefaultViteRunner(projectRoot, options, loader) {
|
|
57
|
+
if (options.vite || options.moduleRunner) {
|
|
58
|
+
return loader(projectRoot, withViteModuleRunner(options));
|
|
80
59
|
}
|
|
81
60
|
|
|
82
|
-
// When a running Vite dev server is available, use it directly.
|
|
83
|
-
if (vite) {
|
|
84
|
-
const configModule = await vite.ssrLoadModule(configPath);
|
|
85
|
-
return resolveOctaneConfig(configModule.default, { requireAdapter });
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Otherwise spin up a temporary Vite server (build / preview).
|
|
89
|
-
// The temp server only transpiles octane.config.ts (plain TypeScript) —
|
|
90
|
-
// no .tsrx compilation plugin is needed beyond config-referenced helpers.
|
|
91
61
|
const { createServer } = await import('vite');
|
|
92
|
-
|
|
93
62
|
const tempVite = await createServer({
|
|
94
63
|
root: projectRoot,
|
|
95
64
|
configFile: false,
|
|
@@ -99,12 +68,9 @@ export async function loadOctaneConfig(projectRoot, options = {}) {
|
|
|
99
68
|
{
|
|
100
69
|
name: 'octane-config-tsrx-loader',
|
|
101
70
|
transform(source, id) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return compile(source,
|
|
105
|
-
mode: 'server',
|
|
106
|
-
hmr: false,
|
|
107
|
-
});
|
|
71
|
+
const file = id.split('?')[0];
|
|
72
|
+
if (!file.endsWith('.tsrx')) return null;
|
|
73
|
+
return compile(source, file, { mode: 'server', hmr: false });
|
|
108
74
|
},
|
|
109
75
|
},
|
|
110
76
|
],
|
|
@@ -112,9 +78,66 @@ export async function loadOctaneConfig(projectRoot, options = {}) {
|
|
|
112
78
|
});
|
|
113
79
|
|
|
114
80
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
81
|
+
return await loader(projectRoot, {
|
|
82
|
+
...options,
|
|
83
|
+
moduleRunner: viteConfigModuleRunner(tempVite),
|
|
84
|
+
});
|
|
117
85
|
} finally {
|
|
118
86
|
await tempVite.close();
|
|
119
87
|
}
|
|
120
88
|
}
|
|
89
|
+
|
|
90
|
+
/** @param {Record<string, any>} options */
|
|
91
|
+
function withViteModuleRunner(options) {
|
|
92
|
+
if (!options.vite || options.moduleRunner) return options;
|
|
93
|
+
const { vite, ...rest } = options;
|
|
94
|
+
return {
|
|
95
|
+
...rest,
|
|
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];
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
package/src/project-codegen.js
CHANGED
|
@@ -1,218 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// and its public exports added to the stub.
|
|
12
|
-
export const SERVER_ONLY_ADAPTER_IDS = new Set([
|
|
13
|
-
'@ripple-ts/adapter-node',
|
|
14
|
-
'@ripple-ts/adapter-bun',
|
|
15
|
-
'@ripple-ts/adapter-vercel',
|
|
16
|
-
'@octanejs/adapter-vercel',
|
|
17
|
-
]);
|
|
18
|
-
|
|
19
|
-
/** @type {Map<string, string>} */
|
|
20
|
-
const generated_file_cache = new Map();
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The browser stand-in shared by every SERVER_ONLY_ADAPTER_IDS package — it
|
|
24
|
-
* must export the UNION of their public names, each failing loudly on use
|
|
25
|
-
* (never at import, so merely reaching the module keeps the app alive).
|
|
26
|
-
* @returns {string}
|
|
27
|
-
*/
|
|
28
|
-
export function create_adapter_browser_stub_source() {
|
|
29
|
-
return `export const runtime = undefined;
|
|
30
|
-
export function serve() {
|
|
31
|
-
throw new Error('[octane] Server adapters cannot run in the browser.');
|
|
32
|
-
}
|
|
33
|
-
export function nodeRequestToWebRequest() {
|
|
34
|
-
throw new Error('[octane] Node request helpers cannot run in the browser.');
|
|
35
|
-
}
|
|
36
|
-
export function webResponseToNodeResponse() {
|
|
37
|
-
throw new Error('[octane] Node response helpers cannot run in the browser.');
|
|
38
|
-
}
|
|
39
|
-
export function vercel() {
|
|
40
|
-
throw new Error('[octane] Deploy adapters cannot run in the browser.');
|
|
41
|
-
}
|
|
42
|
-
export function adapt() {
|
|
43
|
-
throw new Error('[octane] Deploy adapters cannot run in the browser.');
|
|
44
|
-
}
|
|
45
|
-
`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @param {ResolvedConfig} viteConfig
|
|
50
|
-
* @returns {string}
|
|
51
|
-
*/
|
|
52
|
-
export function get_project_generated_dir(viteConfig) {
|
|
53
|
-
return path.join(viteConfig.cacheDir, 'project');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* @param {ResolvedConfig} viteConfig
|
|
58
|
-
* @param {string} name
|
|
59
|
-
* @param {string} source
|
|
60
|
-
* @returns {string}
|
|
61
|
-
*/
|
|
62
|
-
export function write_project_generated_file(viteConfig, name, source) {
|
|
63
|
-
const dir = get_project_generated_dir(viteConfig);
|
|
64
|
-
const file = path.join(dir, name);
|
|
65
|
-
|
|
66
|
-
if (generated_file_cache.get(file) === source && fs.existsSync(file)) {
|
|
67
|
-
return file;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
-
if (!fs.existsSync(file) || fs.readFileSync(file, 'utf-8') !== source) {
|
|
72
|
-
fs.writeFileSync(file, source);
|
|
73
|
-
}
|
|
74
|
-
generated_file_cache.set(file, source);
|
|
75
|
-
return file;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Generate the client hydration entry (served at virtual:octane-hydrate).
|
|
80
|
-
*
|
|
81
|
-
* CONFIG-FREE: it does NOT import octane.config.ts. Importing the config into
|
|
82
|
-
* the browser would drag the plugin (and the server adapter) — with their
|
|
83
|
-
* `node:fs` imports — into the client graph and throw at module-eval. Instead
|
|
84
|
-
* the server serializes everything needed into #__octane_data ({ entry,
|
|
85
|
-
* exportName, layout, params, url, preHydrate }), and this entry
|
|
86
|
-
* dynamic-imports the page/layout from there.
|
|
87
|
-
*
|
|
88
|
-
* `staticEntries` (production builds) lists every module path the server can
|
|
89
|
-
* name in #__octane_data — page entries, layouts, and the preHydrate hook.
|
|
90
|
-
* Each becomes a STATIC `() => import('/src/…')` in a lookup map, so Rollup
|
|
91
|
-
* sees, chunks, and hashes them; the runtime falls back to the hidden dynamic
|
|
92
|
-
* import only for paths outside the map (the dev case, where the map is empty
|
|
93
|
-
* and Vite serves any module by URL).
|
|
94
|
-
*
|
|
95
|
-
* octane specifics:
|
|
96
|
-
* - `import { hydrateRoot } from 'octane'` (NO `mount`).
|
|
97
|
-
* - `hydrateRoot(container, body, props)` signature (container FIRST, React-18
|
|
98
|
-
* shape) — no `{ target, props }` wrapper.
|
|
99
|
-
* - The layout `children` is a ComponentBody `(s) => Page(s, { params })`,
|
|
100
|
-
* NOT a 0-arg thunk: octane's `childSlot` invokes a bare function child
|
|
101
|
-
* as a ComponentBody (block first arg, `{}` props), so page data rides the
|
|
102
|
-
* closure — mirroring the server `createLayoutWrapper`.
|
|
103
|
-
* - hydrateRoot() itself locates/consumes the <script data-octane-suspense>
|
|
104
|
-
* seed inside #root, so the entry does nothing special for suspense.
|
|
105
|
-
* - `preHydrate` (config `router.preHydrate`, a Vite-root module path) is
|
|
106
|
-
* imported and its default export awaited BEFORE hydrateRoot — the hook an
|
|
107
|
-
* app-level client router uses to commit its match tree so the first
|
|
108
|
-
* hydration pass adopts the same resolved tree the server rendered.
|
|
109
|
-
*
|
|
110
|
-
* `getComponentExport` mirrors routes.js `get_component_export` (route named
|
|
111
|
-
* export > default > first PascalCase) so server and client pick the SAME
|
|
112
|
-
* component.
|
|
113
|
-
*
|
|
114
|
-
* @param {{ configPath?: string, staticEntries?: string[] }} [options]
|
|
115
|
-
* @returns {string}
|
|
116
|
-
*/
|
|
117
|
-
export function create_client_entry_source(options = {}) {
|
|
118
|
-
const staticEntries = [...new Set(options.staticEntries ?? [])];
|
|
119
|
-
const static_map_lines = staticEntries
|
|
120
|
-
.map((entry) => ` ${JSON.stringify(entry)}: () => import(${JSON.stringify(entry)}),`)
|
|
121
|
-
.join('\n');
|
|
122
|
-
|
|
123
|
-
return `// Auto-generated by @octanejs/vite-plugin.
|
|
124
|
-
// This file is written to Vite's cacheDir/project folder.
|
|
125
|
-
|
|
126
|
-
import { hydrateRoot } from 'octane';
|
|
127
|
-
|
|
128
|
-
// Static import map (production): every module the server may name in
|
|
129
|
-
// #__octane_data, as bundle-analyzable dynamic imports. Empty in dev.
|
|
130
|
-
const routeModules = {
|
|
131
|
-
${static_map_lines}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// Dynamic import Vite's import-analysis can NOT see: its rewrite appends
|
|
135
|
-
// '?import' to variable dynamic imports, and a queried URL evaluates as a
|
|
136
|
-
// SECOND browser module instance — so the page (or the preHydrate hook) would
|
|
137
|
-
// no longer share module singletons with statically-imported copies of the
|
|
138
|
-
// same files (e.g. a client router the app also imports directly).
|
|
139
|
-
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
140
|
-
|
|
141
|
-
function importModule(path) {
|
|
142
|
-
const loader = routeModules[path];
|
|
143
|
-
return loader ? loader() : dynamicImport(path);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function getComponentExport(module, exportName) {
|
|
147
|
-
// Explicit export name requires an exact match; do NOT fall back, so a
|
|
148
|
-
// typo'd route renders nothing rather than the wrong component.
|
|
149
|
-
if (exportName) return typeof module[exportName] === 'function' ? module[exportName] : undefined;
|
|
150
|
-
if (typeof module.default === 'function') return module.default;
|
|
151
|
-
return Object.entries(module).find(([key, value]) => typeof value === 'function' && /^[A-Z]/.test(key))?.[1];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
(async () => {
|
|
155
|
-
try {
|
|
156
|
-
const el = document.getElementById('__octane_data');
|
|
157
|
-
const target = document.getElementById('root');
|
|
158
|
-
if (!el || !target) {
|
|
159
|
-
console.error('[octane] Unable to hydrate: missing #__octane_data or #root.');
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const data = JSON.parse(el.textContent || '{}'); // { entry, exportName, layout, params, url, preHydrate }
|
|
163
|
-
if (!data.entry) {
|
|
164
|
-
console.error('[octane] Unable to hydrate: no route entry in #__octane_data.');
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const pageMod = await importModule(data.entry);
|
|
169
|
-
const Component = getComponentExport(pageMod, data.exportName ?? undefined);
|
|
170
|
-
if (!Component) {
|
|
171
|
-
console.error('[octane] Unable to hydrate: no component export for', data.entry);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const params = data.params;
|
|
176
|
-
const url = data.url;
|
|
177
|
-
|
|
178
|
-
// Run the app's pre-hydrate hook (config \`router.preHydrate\`) before the
|
|
179
|
-
// first hydration render — e.g. a client router committing its match tree
|
|
180
|
-
// so hydration adopts the same resolved tree the server rendered.
|
|
181
|
-
if (data.preHydrate) {
|
|
182
|
-
const preMod = await importModule(data.preHydrate);
|
|
183
|
-
const hook = preMod.default;
|
|
184
|
-
if (typeof hook === 'function') await hook({ url, params });
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Props mirror the server render exactly: { params, url }.
|
|
188
|
-
if (data.layout) {
|
|
189
|
-
const layoutMod = await importModule(data.layout);
|
|
190
|
-
const Layout = getComponentExport(layoutMod);
|
|
191
|
-
if (Layout) {
|
|
192
|
-
// children is a ComponentBody closing over the page props; octane's
|
|
193
|
-
// childSlot invokes a function child PROPS-FIRST as \`({}, block, extra)\`,
|
|
194
|
-
// so we ignore the empty props and render the page with its real
|
|
195
|
-
// \`{ params, url }\`, threading the scope + extra — mirroring the server
|
|
196
|
-
// createLayoutWrapper so the markers line up.
|
|
197
|
-
const children = (_props, scope, extra) => Component({ params, url }, scope, extra);
|
|
198
|
-
hydrateRoot(target, Layout, { params, url, children });
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
hydrateRoot(target, Component, { params, url });
|
|
204
|
-
} catch (error) {
|
|
205
|
-
console.error('[octane] Failed to bootstrap client hydration.', error);
|
|
206
|
-
}
|
|
207
|
-
})();
|
|
208
|
-
`;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* @param {string} filename
|
|
213
|
-
* @param {string} root
|
|
214
|
-
* @returns {string}
|
|
215
|
-
*/
|
|
216
|
-
export function to_vite_root_import(filename, root) {
|
|
217
|
-
return '/' + path.relative(root, filename).split(path.sep).join('/');
|
|
218
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
RESOLVED_ADAPTER_BROWSER_STUB_ID,
|
|
3
|
+
SERVER_ONLY_ADAPTER_IDS,
|
|
4
|
+
create_adapter_browser_stub_source,
|
|
5
|
+
create_client_entry_source,
|
|
6
|
+
get_project_generated_dir,
|
|
7
|
+
normalize_module_reference,
|
|
8
|
+
to_vite_root_import,
|
|
9
|
+
write_project_generated_file,
|
|
10
|
+
} from '@octanejs/app-core/codegen';
|
package/src/resolve-config.js
CHANGED
|
@@ -1,171 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Config validation + defaults — `resolveOctaneConfig` and its validators.
|
|
4
|
-
*
|
|
5
|
-
* Kept in a module with NO heavy imports (no vite, no octane/compiler) because
|
|
6
|
-
* it is part of the PRODUCTION server bundle's graph: the generated server
|
|
7
|
-
* entry re-resolves octane.config.ts through it at boot, and the whole
|
|
8
|
-
* `@octanejs/vite-plugin/production` graph is bundled into dist/server/entry.js.
|
|
9
|
-
* The file-loading half (`loadOctaneConfig`, which spins up Vite) lives in
|
|
10
|
-
* `load-config.js` and re-exports everything here.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** @import { OctaneConfigOptions, ResolvedOctaneConfig } from '@octanejs/vite-plugin' */
|
|
14
|
-
|
|
15
|
-
import { DEFAULT_OUTDIR } from './constants.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* @param {unknown} route
|
|
19
|
-
* @returns {void}
|
|
20
|
-
*/
|
|
21
|
-
function validate_render_route(route) {
|
|
22
|
-
if (
|
|
23
|
-
!route ||
|
|
24
|
-
typeof route !== 'object' ||
|
|
25
|
-
/** @type {{ type?: unknown }} */ (route).type !== 'render'
|
|
26
|
-
) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const render_route = /** @type {{ entry?: unknown, layout?: unknown }} */ (route);
|
|
31
|
-
const has_entry =
|
|
32
|
-
typeof render_route.entry === 'string' ||
|
|
33
|
-
(Array.isArray(render_route.entry) &&
|
|
34
|
-
render_route.entry.length === 2 &&
|
|
35
|
-
typeof render_route.entry[0] === 'string' &&
|
|
36
|
-
typeof render_route.entry[1] === 'string');
|
|
37
|
-
|
|
38
|
-
if (!has_entry) {
|
|
39
|
-
throw new Error('[@octanejs/vite-plugin] RenderRoute requires a string/tuple `entry`.');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (render_route.layout !== undefined && typeof render_route.layout !== 'string') {
|
|
43
|
-
throw new Error('[@octanejs/vite-plugin] RenderRoute `layout` must be a string path.');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const status = /** @type {{ status?: unknown }} */ (route).status;
|
|
47
|
-
if (status !== undefined && (typeof status !== 'number' || !Number.isInteger(status))) {
|
|
48
|
-
throw new Error('[@octanejs/vite-plugin] RenderRoute `status` must be an integer.');
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @param {unknown} rootBoundary
|
|
54
|
-
* @returns {void}
|
|
55
|
-
*/
|
|
56
|
-
function validate_root_boundary(rootBoundary) {
|
|
57
|
-
if (rootBoundary === undefined) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (!rootBoundary || typeof rootBoundary !== 'object') {
|
|
61
|
-
throw new Error('[@octanejs/vite-plugin] rootBoundary must be an object when provided.');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const boundary = /** @type {{ pending?: unknown, catch?: unknown }} */ (rootBoundary);
|
|
65
|
-
if (boundary.pending !== undefined && typeof boundary.pending !== 'function') {
|
|
66
|
-
throw new Error('[@octanejs/vite-plugin] rootBoundary.pending must be a component function.');
|
|
67
|
-
}
|
|
68
|
-
if (boundary.catch !== undefined && typeof boundary.catch !== 'function') {
|
|
69
|
-
throw new Error('[@octanejs/vite-plugin] rootBoundary.catch must be a component function.');
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Validate a raw octane config and apply all defaults.
|
|
75
|
-
*
|
|
76
|
-
* After this function returns every optional field carries its default
|
|
77
|
-
* value so callers never need to use `??` / `||` fallbacks.
|
|
78
|
-
*
|
|
79
|
-
* The function is idempotent — passing an already-resolved config
|
|
80
|
-
* through it again is safe and produces the same result.
|
|
81
|
-
*
|
|
82
|
-
* @param {OctaneConfigOptions} raw - The user-provided config (from octane.config.ts)
|
|
83
|
-
* @param {{ requireAdapter?: boolean }} [options]
|
|
84
|
-
* @returns {ResolvedOctaneConfig}
|
|
85
|
-
*/
|
|
86
|
-
export function resolveOctaneConfig(raw, options = {}) {
|
|
87
|
-
const { requireAdapter = false } = options;
|
|
88
|
-
|
|
89
|
-
// ------------------------------------------------------------------
|
|
90
|
-
// Validate
|
|
91
|
-
// ------------------------------------------------------------------
|
|
92
|
-
if (!raw) {
|
|
93
|
-
throw new Error(
|
|
94
|
-
'[@octanejs/vite-plugin] octane.config.ts must export a default config object.',
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (requireAdapter && !raw.adapter) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
'[@octanejs/vite-plugin] This build requires an `adapter` in octane.config.ts. ' +
|
|
101
|
-
'Install an adapter package (e.g. @octanejs/adapter-vercel) and set the `adapter` property.',
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (raw.adapter !== undefined) {
|
|
106
|
-
if (typeof raw.adapter !== 'object' || raw.adapter === null) {
|
|
107
|
-
throw new Error(
|
|
108
|
-
'[@octanejs/vite-plugin] adapter must be an adapter object (e.g. `adapter: vercel()`).',
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
if (raw.adapter.adapt !== undefined && typeof raw.adapter.adapt !== 'function') {
|
|
112
|
-
throw new Error('[@octanejs/vite-plugin] adapter.adapt must be a function.');
|
|
113
|
-
}
|
|
114
|
-
if (raw.adapter.serve !== undefined && typeof raw.adapter.serve !== 'function') {
|
|
115
|
-
throw new Error('[@octanejs/vite-plugin] adapter.serve must be a function.');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (raw.router?.routes !== undefined && !Array.isArray(raw.router.routes)) {
|
|
120
|
-
throw new Error('[@octanejs/vite-plugin] router.routes must be an array.');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (raw.router?.preHydrate !== undefined) {
|
|
124
|
-
// A Vite-root module path: the client hydrate entry dynamic-imports it in
|
|
125
|
-
// the browser, so it must be root-absolute ('/src/…'), not relative or fs.
|
|
126
|
-
if (typeof raw.router.preHydrate !== 'string' || !raw.router.preHydrate.startsWith('/')) {
|
|
127
|
-
throw new Error(
|
|
128
|
-
"[@octanejs/vite-plugin] router.preHydrate must be a Vite-root module path (e.g. '/src/pre-hydrate.ts').",
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
for (const route of raw.router?.routes ?? []) {
|
|
134
|
-
validate_render_route(route);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
validate_root_boundary(raw.rootBoundary);
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
raw.server?.render !== undefined &&
|
|
141
|
-
raw.server.render !== 'streaming' &&
|
|
142
|
-
raw.server.render !== 'buffered'
|
|
143
|
-
) {
|
|
144
|
-
throw new Error("[@octanejs/vite-plugin] server.render must be 'streaming' or 'buffered'.");
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ------------------------------------------------------------------
|
|
148
|
-
// Apply defaults
|
|
149
|
-
// ------------------------------------------------------------------
|
|
150
|
-
return {
|
|
151
|
-
build: {
|
|
152
|
-
outDir: raw.build?.outDir ?? DEFAULT_OUTDIR,
|
|
153
|
-
minify: raw.build?.minify,
|
|
154
|
-
target: raw.build?.target,
|
|
155
|
-
},
|
|
156
|
-
adapter: raw.adapter,
|
|
157
|
-
router: {
|
|
158
|
-
routes: raw.router?.routes ?? [],
|
|
159
|
-
preHydrate: raw.router?.preHydrate,
|
|
160
|
-
},
|
|
161
|
-
rootBoundary: raw.rootBoundary ?? {},
|
|
162
|
-
middlewares: raw.middlewares ?? [],
|
|
163
|
-
platform: {
|
|
164
|
-
env: raw.platform?.env ?? {},
|
|
165
|
-
},
|
|
166
|
-
server: {
|
|
167
|
-
trustProxy: raw.server?.trustProxy ?? false,
|
|
168
|
-
render: raw.server?.render ?? 'streaming',
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
}
|
|
1
|
+
export { resolveOctaneConfig } from '@octanejs/app-core/config';
|