@octanejs/vite-plugin 0.1.5 → 0.1.6
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 +16 -4
- package/src/config-entry.js +15 -0
- package/src/constants.js +1 -3
- package/src/index.js +74 -24
- package/src/load-config.js +61 -81
- 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 +30 -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/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';
|
package/src/routes.js
CHANGED
|
@@ -1,133 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* @typedef {import('@octanejs/vite-plugin').Context} Context
|
|
4
|
-
* @typedef {import('@octanejs/vite-plugin').Middleware} Middleware
|
|
5
|
-
* @typedef {import('@octanejs/vite-plugin').RenderRouteOptions} RenderRouteOptions
|
|
6
|
-
* @typedef {import('@octanejs/vite-plugin').ServerRouteOptions} ServerRouteOptions
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @typedef {string | readonly [string, string]} RenderRouteEntry
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @param {RenderRouteEntry | undefined} entry
|
|
15
|
-
* @returns {string | undefined}
|
|
16
|
-
*/
|
|
17
|
-
export function get_route_entry_path(entry) {
|
|
18
|
-
return typeof entry === 'string' ? entry : entry?.[1];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param {RenderRouteEntry | undefined} entry
|
|
23
|
-
* @returns {string | undefined}
|
|
24
|
-
*/
|
|
25
|
-
export function get_route_entry_export_name(entry) {
|
|
26
|
-
return typeof entry === 'string' ? undefined : entry?.[0];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @param {RenderRouteEntry | undefined} entry
|
|
31
|
-
* @returns {string | undefined}
|
|
32
|
-
*/
|
|
33
|
-
export function get_route_entry_id(entry) {
|
|
34
|
-
const path = get_route_entry_path(entry);
|
|
35
|
-
const export_name = get_route_entry_export_name(entry);
|
|
36
|
-
return path && export_name ? `${path}#${export_name}` : path;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @param {Record<string, unknown>} module
|
|
41
|
-
* @param {string | undefined} export_name
|
|
42
|
-
* @returns {Function | null}
|
|
43
|
-
*/
|
|
44
|
-
export function get_component_export(module, export_name) {
|
|
45
|
-
// When an explicit export name is given, require an exact match. Do NOT fall
|
|
46
|
-
// back to default/first-PascalCase — a typo'd route tuple should fail loudly
|
|
47
|
-
// rather than silently render the wrong component.
|
|
48
|
-
if (export_name) {
|
|
49
|
-
return typeof module[export_name] === 'function' ? module[export_name] : null;
|
|
50
|
-
}
|
|
51
|
-
if (typeof module.default === 'function') {
|
|
52
|
-
return module.default;
|
|
53
|
-
}
|
|
54
|
-
for (const [key, value] of Object.entries(module)) {
|
|
55
|
-
if (typeof value === 'function' && /^[A-Z]/.test(key)) {
|
|
56
|
-
return value;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Route for rendering octane components with SSR
|
|
64
|
-
*/
|
|
65
|
-
export class RenderRoute {
|
|
66
|
-
/** @type {'render'} */
|
|
67
|
-
type = 'render';
|
|
68
|
-
|
|
69
|
-
/** @type {string} */
|
|
70
|
-
path;
|
|
71
|
-
|
|
72
|
-
// Non-optional: the constructor throws without one (matches types/index.d.ts).
|
|
73
|
-
/** @type {RenderRouteEntry} */
|
|
74
|
-
entry;
|
|
75
|
-
|
|
76
|
-
/** @type {string | undefined} */
|
|
77
|
-
layout;
|
|
78
|
-
|
|
79
|
-
/** @type {Middleware[]} */
|
|
80
|
-
before;
|
|
81
|
-
|
|
82
|
-
/** @type {number | undefined} */
|
|
83
|
-
status;
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* @param {RenderRouteOptions} options
|
|
87
|
-
*/
|
|
88
|
-
constructor(options) {
|
|
89
|
-
if (!options.entry) {
|
|
90
|
-
throw new Error('RenderRoute requires an `entry`.');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
this.path = options.path;
|
|
94
|
-
this.entry = options.entry;
|
|
95
|
-
this.layout = options.layout;
|
|
96
|
-
this.before = options.before ?? [];
|
|
97
|
-
this.status = options.status;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Route for API endpoints (returns Response directly)
|
|
103
|
-
*/
|
|
104
|
-
export class ServerRoute {
|
|
105
|
-
/** @type {'server'} */
|
|
106
|
-
type = 'server';
|
|
107
|
-
|
|
108
|
-
/** @type {string} */
|
|
109
|
-
path;
|
|
110
|
-
|
|
111
|
-
/** @type {string[]} */
|
|
112
|
-
methods;
|
|
113
|
-
|
|
114
|
-
/** @type {(context: Context) => Response | Promise<Response>} */
|
|
115
|
-
handler;
|
|
116
|
-
|
|
117
|
-
/** @type {Middleware[]} */
|
|
118
|
-
before;
|
|
119
|
-
|
|
120
|
-
/** @type {Middleware[]} */
|
|
121
|
-
after;
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* @param {ServerRouteOptions} options
|
|
125
|
-
*/
|
|
126
|
-
constructor(options) {
|
|
127
|
-
this.path = options.path;
|
|
128
|
-
this.methods = options.methods ?? ['GET'];
|
|
129
|
-
this.handler = options.handler;
|
|
130
|
-
this.before = options.before ?? [];
|
|
131
|
-
this.after = options.after ?? [];
|
|
132
|
-
}
|
|
133
|
-
}
|
|
1
|
+
export * from '@octanejs/app-core/routes';
|
|
@@ -1,56 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* `(props, scope, extra) => string`. `render(Component, props)` invokes the ROOT
|
|
7
|
-
* directly as `Component(props, rootScope, undefined)` and does NOT wrap it in
|
|
8
|
-
* block markers — and `hydrateRoot()` adopts the container's FIRST CHILD as the
|
|
9
|
-
* root's own node. So the wrapper must call the top-level component DIRECTLY
|
|
10
|
-
* (wrapping it in `ssrComponent` would add an extra `<!--[-->…<!--]-->` layer that
|
|
11
|
-
* `clone()` then mis-adopts on hydrate).
|
|
12
|
-
*
|
|
13
|
-
* Only the layout's `{children}` is a nested hole: the compiled layout emits
|
|
14
|
-
* `ssrChild(props.children, scope)`, and `ssrChild` invokes a FUNCTION child as
|
|
15
|
-
* `children({}, scope, undefined)` wrapped in one `<!--[-->…<!--]-->` range. So
|
|
16
|
-
* `children` is a ComponentBody that calls the page directly, and any page data
|
|
17
|
-
* (params) rides its CLOSURE — `ssrChild` supplies only `{}`. The client
|
|
18
|
-
* `childSlot` applies the identical rule (bare function = ComponentBody, `{}`
|
|
19
|
-
* props, one marker range), so server markers and client adoption line up.
|
|
20
|
-
*
|
|
21
|
-
* @typedef {(props?: any, scope?: any, extra?: any) => string} ServerComponent
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Wrap a page component, baking in its route props.
|
|
26
|
-
*
|
|
27
|
-
* @param {ServerComponent} Page
|
|
28
|
-
* @param {Record<string, unknown>} pageProps
|
|
29
|
-
* @returns {ServerComponent}
|
|
30
|
-
*/
|
|
31
|
-
export function createPropsWrapper(Page, pageProps) {
|
|
32
|
-
return function Root(_props, scope) {
|
|
33
|
-
return Page(pageProps, scope, undefined);
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Compose a layout with a page: the layout's `{children}` renders the page.
|
|
39
|
-
*
|
|
40
|
-
* @param {ServerComponent} Layout
|
|
41
|
-
* @param {ServerComponent} Page
|
|
42
|
-
* @param {Record<string, unknown>} pageProps
|
|
43
|
-
* @returns {ServerComponent}
|
|
44
|
-
*/
|
|
45
|
-
export function createLayoutWrapper(Layout, Page, pageProps) {
|
|
46
|
-
return function Root(_props, scope) {
|
|
47
|
-
// `children` is a ComponentBody closing over pageProps; the layout's
|
|
48
|
-
// `{children}` hole runs it via ssrChild (which supplies `{}` props and
|
|
49
|
-
// wraps the output in one block range), so the page still gets its real
|
|
50
|
-
// route props through the closure. PROPS-FIRST: childSlot/ssrChild call it
|
|
51
|
-
// as `({}, scope, extra)`, so the page's real props are passed explicitly.
|
|
52
|
-
const children = (/** @type {any} */ _cprops, /** @type {any} */ cscope) =>
|
|
53
|
-
Page(pageProps, cscope, undefined);
|
|
54
|
-
return Layout({ ...pageProps, children }, scope, undefined);
|
|
55
|
-
};
|
|
56
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
createLayoutWrapper,
|
|
3
|
+
createPropsWrapper,
|
|
4
|
+
createRootBoundaryWrapper,
|
|
5
|
+
} from '@octanejs/app-core/production';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { composeHtmlStream } from '@octanejs/app-core/html';
|