@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.
@@ -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
- const OCTANE_EXTENSION_PATTERN = /\.tsrx$/;
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
- // Validation + defaults live in resolve-config.js (a module with no vite /
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
- * Return the absolute path to octane.config.ts for the given project root.
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
- * This is the single source of truth for the config file name / location.
37
- *
38
- * @param {string} projectRoot - Absolute path to the project root
39
- * @returns {string}
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 getOctaneConfigPath(projectRoot) {
42
- return path.join(projectRoot, 'octane.config.ts');
31
+ export async function loadOctaneConfig(projectRoot, options = {}) {
32
+ return withDefaultViteRunner(projectRoot, options, loadCoreOctaneConfig);
43
33
  }
44
34
 
45
35
  /**
46
- * Check whether a octane.config.ts file exists in the given root.
47
- *
48
- * Use this before calling `loadOctaneConfig` when the absence of a
49
- * config is a valid state (e.g. the Vite plugin running in SPA mode).
50
- *
51
- * @param {string} projectRoot - Absolute path to the project root
52
- * @returns {boolean}
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 octaneConfigExists(projectRoot) {
55
- return fs.existsSync(getOctaneConfigPath(projectRoot));
45
+ export async function loadOctaneConfigWithMetadata(projectRoot, options = {}) {
46
+ return withDefaultViteRunner(projectRoot, options, loadCoreOctaneConfigWithMetadata);
56
47
  }
57
48
 
58
49
  /**
59
- * Load octane.config.ts, validate, and apply defaults via `resolveOctaneConfig`.
60
- *
61
- * When a Vite dev server is provided via `options.vite`, the config is loaded
62
- * through its `ssrLoadModule` avoiding the cost of spinning up a temporary
63
- * server and enabling HMR-aware reloads.
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
- export async function loadOctaneConfig(projectRoot, options = {}) {
75
- const { vite, requireAdapter } = options;
76
- const configPath = getOctaneConfigPath(projectRoot);
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
- if (!OCTANE_EXTENSION_PATTERN.test(id)) return null;
103
- const filename = id.replace(projectRoot, '');
104
- return compile(source, filename, {
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
- const configModule = await tempVite.ssrLoadModule(configPath);
116
- return resolveOctaneConfig(configModule.default, { requireAdapter });
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
+ }
@@ -1,218 +1,10 @@
1
- // @ts-check
2
- /** @import { ResolvedConfig } from 'vite' */
3
-
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
-
7
- export const RESOLVED_ADAPTER_BROWSER_STUB_ID = '\0octane:adapter-browser-stub';
8
- // Server-only deploy/adapter packages: client-side imports of these specifiers
9
- // resolve to the browser stub below instead of the real module (whose graph
10
- // pulls node builtins). Every published adapter package MUST be listed here,
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';
@@ -1,171 +1 @@
1
- // @ts-check
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';