@octanejs/vite-plugin 0.1.3 → 0.1.4
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/CHANGELOG.md +73 -1
- package/package.json +7 -2
- package/src/bin/preview.js +20 -5
- package/src/config-entry.js +31 -0
- package/src/constants.js +1 -0
- package/src/index.js +362 -73
- package/src/load-config.js +6 -126
- package/src/project-codegen.js +75 -15
- package/src/resolve-config.js +171 -0
- package/src/routes.js +7 -1
- package/src/server/component-wrappers.js +1 -0
- package/src/server/middleware.js +1 -0
- package/src/server/node-http.js +188 -0
- package/src/server/production.js +276 -16
- package/src/server/render-route.js +86 -37
- package/src/server/router.js +1 -0
- package/src/server/server-route.js +1 -0
- package/src/server/virtual-entry.js +176 -7
- package/tests/_fixtures/app/index.html +11 -0
- package/tests/_fixtures/app/octane.config.ts +14 -0
- package/tests/_fixtures/app/package.json +7 -0
- package/tests/_fixtures/app/src/Layout.tsrx +6 -0
- package/tests/_fixtures/app/src/Page.tsrx +17 -0
- package/tests/_fixtures/app/vite.config.ts +12 -0
- package/tests/handler.test.ts +134 -0
- package/tests/plugin.test.ts +155 -0
- package/tests/production.test.ts +157 -0
- package/tsconfig.typecheck.json +2 -2
- package/types/index.d.ts +102 -13
- package/types/node.d.ts +31 -0
- package/types/production.d.ts +34 -21
|
@@ -1,15 +1,184 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
/**
|
|
2
3
|
* Production server-entry generator.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* `generateServerEntry` emits the module the SSR sub-build (closeBundle in
|
|
6
|
+
* src/index.js) uses as its Rollup input. The generated module statically
|
|
7
|
+
* imports every RenderRoute entry/layout module (compiled in server mode by
|
|
8
|
+
* the octane plugin the sub-build inherits from the app's vite.config) plus
|
|
9
|
+
* octane.config.ts itself, wires them into `createHandler`, and:
|
|
10
|
+
*
|
|
11
|
+
* - exports `handler` — the Web fetch handler `(Request) => Promise<Response>`
|
|
12
|
+
* - exports `nodeHandler` — a Node `(req, res)` wrapper for serverless
|
|
13
|
+
* platforms (e.g. a Vercel Node function does
|
|
14
|
+
* `export { nodeHandler as default } from '../dist/server/entry.js'`)
|
|
15
|
+
* - auto-boots when run directly (`node dist/server/entry.js`): the
|
|
16
|
+
* adapter's `serve()` when configured, else the built-in Node server
|
|
17
|
+
* (static dist/client assets + the handler).
|
|
18
|
+
*
|
|
19
|
+
* It is unused in dev (dev SSR loads modules through `vite.ssrLoadModule`).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** @import { Route } from '@octanejs/vite-plugin' */
|
|
23
|
+
/** @import { ClientAssetEntry } from '../../types/production.d.ts' */
|
|
24
|
+
|
|
25
|
+
import { get_route_entry_path } from '../routes.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} ServerEntryOptions
|
|
29
|
+
* @property {Route[]} routes - Route definitions from octane.config.ts
|
|
30
|
+
* @property {string} octaneConfigPath - Absolute path to octane.config.ts
|
|
31
|
+
* @property {Record<string, ClientAssetEntry>} [clientAssetMap] - Route entry path → built client asset paths
|
|
7
32
|
*/
|
|
8
33
|
|
|
9
34
|
/**
|
|
10
|
-
* @param {
|
|
11
|
-
* @returns {
|
|
35
|
+
* @param {ServerEntryOptions} options
|
|
36
|
+
* @returns {string} The generated JavaScript module source
|
|
37
|
+
*/
|
|
38
|
+
export function generateServerEntry(options) {
|
|
39
|
+
const { routes, octaneConfigPath, clientAssetMap = {} } = options;
|
|
40
|
+
|
|
41
|
+
// Unique page-entry and layout module paths (multiple routes may share both).
|
|
42
|
+
/** @type {Map<string, string>} module path → import variable name */
|
|
43
|
+
const page_imports = new Map();
|
|
44
|
+
/** @type {Map<string, string>} */
|
|
45
|
+
const layout_imports = new Map();
|
|
46
|
+
|
|
47
|
+
for (const route of routes) {
|
|
48
|
+
if (route.type !== 'render') continue;
|
|
49
|
+
const entryPath = get_route_entry_path(route.entry);
|
|
50
|
+
if (entryPath && !page_imports.has(entryPath)) {
|
|
51
|
+
page_imports.set(entryPath, `_page_${page_imports.size}`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof route.layout === 'string' && !layout_imports.has(route.layout)) {
|
|
54
|
+
layout_imports.set(route.layout, `_layout_${layout_imports.size}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const import_lines = [];
|
|
59
|
+
for (const [modulePath, varName] of page_imports) {
|
|
60
|
+
import_lines.push(`import * as ${varName} from ${JSON.stringify(modulePath)};`);
|
|
61
|
+
}
|
|
62
|
+
for (const [modulePath, varName] of layout_imports) {
|
|
63
|
+
import_lines.push(`import * as ${varName} from ${JSON.stringify(modulePath)};`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The manifest maps MODULE PATHS to module namespaces; createHandler picks
|
|
67
|
+
// the export per-route with the same `get_component_export` dev uses.
|
|
68
|
+
const component_entries = [...page_imports]
|
|
69
|
+
.map(([modulePath, varName]) => `\t${JSON.stringify(modulePath)}: ${varName},`)
|
|
70
|
+
.join('\n');
|
|
71
|
+
const layout_entries = [...layout_imports]
|
|
72
|
+
.map(([modulePath, varName]) => `\t${JSON.stringify(modulePath)}: ${varName},`)
|
|
73
|
+
.join('\n');
|
|
74
|
+
|
|
75
|
+
return `\
|
|
76
|
+
// Auto-generated by @octanejs/vite-plugin — the production server entry.
|
|
77
|
+
// Do not edit; regenerated on every build.
|
|
78
|
+
|
|
79
|
+
import { readFileSync } from 'node:fs';
|
|
80
|
+
import { createHash } from 'node:crypto';
|
|
81
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
82
|
+
import { fileURLToPath } from 'node:url';
|
|
83
|
+
import { dirname, join, resolve } from 'node:path';
|
|
84
|
+
|
|
85
|
+
import { renderToReadableStream, executeServerFunction } from 'octane/server';
|
|
86
|
+
import { prerender } from 'octane/static';
|
|
87
|
+
import { createHandler, resolveOctaneConfig } from '@octanejs/vite-plugin/production';
|
|
88
|
+
import { createNodeServer, nodeRequestToWebRequest, sendWebResponse } from '@octanejs/vite-plugin/node';
|
|
89
|
+
|
|
90
|
+
// The app config — bundled (the sub-build aliases '@octanejs/vite-plugin' to
|
|
91
|
+
// its config-surface facade, so this does not drag the compiler in).
|
|
92
|
+
import _rawOctaneConfig from ${JSON.stringify(octaneConfigPath)};
|
|
93
|
+
|
|
94
|
+
${import_lines.join('\n')}
|
|
95
|
+
|
|
96
|
+
const octaneConfig = resolveOctaneConfig(_rawOctaneConfig);
|
|
97
|
+
|
|
98
|
+
// Platform primitives: the adapter's when configured, else Node defaults.
|
|
99
|
+
// (hash mirrors the compiler's module-server hashing: sha-256 hex, 8 chars.)
|
|
100
|
+
const runtime = octaneConfig.adapter?.runtime ?? {
|
|
101
|
+
hash: (str) => createHash('sha256').update(str).digest('hex').slice(0, 8),
|
|
102
|
+
createAsyncContext: () => {
|
|
103
|
+
const als = new AsyncLocalStorage();
|
|
104
|
+
return { run: (store, fn) => als.run(store, fn), getStore: () => als.getStore() };
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
109
|
+
// The HTML template is the BUILT client index.html (hashed hydrate script and
|
|
110
|
+
// asset links already in place), moved next to this entry by the build.
|
|
111
|
+
const htmlTemplate = readFileSync(join(__dirname, './index.html'), 'utf-8');
|
|
112
|
+
|
|
113
|
+
const components = {
|
|
114
|
+
${component_entries}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const layouts = {
|
|
118
|
+
${layout_entries}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const clientAssets = ${JSON.stringify(clientAssetMap, null, '\t')};
|
|
122
|
+
|
|
123
|
+
export const handler = createHandler(
|
|
124
|
+
{
|
|
125
|
+
routes: octaneConfig.router.routes,
|
|
126
|
+
components,
|
|
127
|
+
layouts,
|
|
128
|
+
middlewares: octaneConfig.middlewares,
|
|
129
|
+
trustProxy: octaneConfig.server.trustProxy,
|
|
130
|
+
render: octaneConfig.server.render,
|
|
131
|
+
rootBoundary: octaneConfig.rootBoundary,
|
|
132
|
+
preHydrate: octaneConfig.router.preHydrate ?? null,
|
|
133
|
+
rpcModules: {},
|
|
134
|
+
runtime,
|
|
135
|
+
clientAssets,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
renderToReadableStream,
|
|
139
|
+
prerender,
|
|
140
|
+
htmlTemplate,
|
|
141
|
+
executeServerFunction,
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Node-style (req, res) wrapper — for serverless platforms whose functions
|
|
147
|
+
* speak Node HTTP (e.g. Vercel's Node runtime).
|
|
12
148
|
*/
|
|
13
|
-
export function
|
|
14
|
-
|
|
149
|
+
export async function nodeHandler(req, res) {
|
|
150
|
+
try {
|
|
151
|
+
const response = await handler(nodeRequestToWebRequest(req));
|
|
152
|
+
await sendWebResponse(res, response);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('[@octanejs/vite-plugin] Request error:', error);
|
|
155
|
+
if (!res.headersSent) {
|
|
156
|
+
res.statusCode = 500;
|
|
157
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
158
|
+
}
|
|
159
|
+
res.end('Internal Server Error');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Auto-boot when run directly (node dist/server/entry.js); stay quiet when
|
|
164
|
+
// imported by a serverless wrapper.
|
|
165
|
+
const isMainModule =
|
|
166
|
+
typeof process !== 'undefined' &&
|
|
167
|
+
process.argv[1] &&
|
|
168
|
+
fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
169
|
+
|
|
170
|
+
if (isMainModule) {
|
|
171
|
+
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
|
172
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
173
|
+
console.error('[@octanejs/vite-plugin] Invalid PORT value:', process.env.PORT);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
const staticDir = join(__dirname, '../client');
|
|
177
|
+
const server = octaneConfig.adapter?.serve
|
|
178
|
+
? octaneConfig.adapter.serve(handler, { static: { dir: staticDir } })
|
|
179
|
+
: createNodeServer(handler, { staticDir });
|
|
180
|
+
server.listen(port);
|
|
181
|
+
console.log('[@octanejs/vite-plugin] Production server listening on port ' + port);
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
15
184
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig, RenderRoute } from '@octanejs/vite-plugin';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
router: {
|
|
5
|
+
routes: [
|
|
6
|
+
new RenderRoute({ path: '/', entry: ['Page', '/src/Page.tsrx'], layout: '/src/Layout.tsrx' }),
|
|
7
|
+
new RenderRoute({
|
|
8
|
+
path: '/pages/:slug',
|
|
9
|
+
entry: ['Page', '/src/Page.tsrx'],
|
|
10
|
+
layout: '/src/Layout.tsrx',
|
|
11
|
+
}),
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useState } from 'octane';
|
|
2
|
+
|
|
3
|
+
// A page with a dynamic text hole, a param read, and an event handler — enough
|
|
4
|
+
// to prove the server bundle renders hydratable markup (markers + scoped
|
|
5
|
+
// state) identical to dev SSR.
|
|
6
|
+
export function Page(props: { params: Record<string, string>; url: string }) @{
|
|
7
|
+
const [count] = useState(1);
|
|
8
|
+
<main>
|
|
9
|
+
<h1>
|
|
10
|
+
{'Fixture page ' + (props.params.slug ?? 'home')}
|
|
11
|
+
</h1>
|
|
12
|
+
<p class="url">{props.url}</p>
|
|
13
|
+
<p class="count">
|
|
14
|
+
{'Count: ' + count}
|
|
15
|
+
</p>
|
|
16
|
+
</main>
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import { octane } from '@octanejs/vite-plugin';
|
|
3
|
+
|
|
4
|
+
// SSR module graphs (dev + server build) must see the server runtime for bare
|
|
5
|
+
// 'octane' imports from non-compiled sources (none here, but keeps the fixture
|
|
6
|
+
// shaped like a real app).
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [octane()],
|
|
9
|
+
ssr: {
|
|
10
|
+
noExternal: [/^octane($|\/)/],
|
|
11
|
+
},
|
|
12
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// createHandler unit tests — the production handler's pure assembly decisions
|
|
2
|
+
// (template splitting, hydration payload shape, status, preload tags, the
|
|
3
|
+
// buffered fallback, server routes, 404) with stub renderers. The real-render
|
|
4
|
+
// byte-compat path is covered end-to-end in production.test.ts.
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { createHandler } from '../src/server/production.js';
|
|
7
|
+
import { RenderRoute, ServerRoute } from '../src/routes.js';
|
|
8
|
+
|
|
9
|
+
const TEMPLATE = `<!doctype html>
|
|
10
|
+
<html><head><!--ssr-head--></head><body><div id="root"><!--ssr-body--></div>
|
|
11
|
+
<script type="module" src="/assets/hydrate-abc.js"></script></body></html>`;
|
|
12
|
+
|
|
13
|
+
function Page() {
|
|
14
|
+
return '<main>page</main>';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function streamOf(text: string): ReadableStream<Uint8Array> {
|
|
18
|
+
return new ReadableStream({
|
|
19
|
+
start(controller) {
|
|
20
|
+
controller.enqueue(new TextEncoder().encode(text));
|
|
21
|
+
controller.close();
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const baseDeps = {
|
|
27
|
+
renderToReadableStream: async () =>
|
|
28
|
+
streamOf('<style data-octane="x">.x{}</style><main>page</main>'),
|
|
29
|
+
prerender: async () => ({
|
|
30
|
+
html: '<main>page</main>',
|
|
31
|
+
css: '<style data-octane="x">.x{}</style>',
|
|
32
|
+
}),
|
|
33
|
+
htmlTemplate: TEMPLATE,
|
|
34
|
+
executeServerFunction: async () => '',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function makeManifest(overrides: Record<string, unknown> = {}) {
|
|
38
|
+
const routes = [
|
|
39
|
+
new RenderRoute({ path: '/', entry: ['Page', '/src/Page.tsrx'] }),
|
|
40
|
+
new RenderRoute({ path: '/*splat', entry: ['Page', '/src/Page.tsrx'], status: 404 }),
|
|
41
|
+
new ServerRoute({
|
|
42
|
+
path: '/api/ping',
|
|
43
|
+
handler: () => new Response('pong', { status: 200 }),
|
|
44
|
+
}),
|
|
45
|
+
];
|
|
46
|
+
return {
|
|
47
|
+
routes,
|
|
48
|
+
components: { '/src/Page.tsrx': { Page } },
|
|
49
|
+
layouts: {},
|
|
50
|
+
middlewares: [],
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('createHandler', () => {
|
|
56
|
+
it('streams template prefix → render stream → suffix with the dev-shaped data script', async () => {
|
|
57
|
+
const handler = createHandler(makeManifest() as any, baseDeps as any);
|
|
58
|
+
const response = await handler(new Request('http://localhost/?q=1'));
|
|
59
|
+
expect(response.status).toBe(200);
|
|
60
|
+
const html = await response.text();
|
|
61
|
+
|
|
62
|
+
// Assembly: styles + markup inside #root, template suffix intact.
|
|
63
|
+
expect(html).toContain(
|
|
64
|
+
'<div id="root"><style data-octane="x">.x{}</style><main>page</main></div>',
|
|
65
|
+
);
|
|
66
|
+
expect(html).toContain('src="/assets/hydrate-abc.js"');
|
|
67
|
+
|
|
68
|
+
// The payload matches dev render-route.js: same keys, same order.
|
|
69
|
+
const payload = html.match(/__octane_data" type="application\/json">(.*?)<\/script>/s)![1];
|
|
70
|
+
expect(payload).toBe(
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
entry: '/src/Page.tsrx',
|
|
73
|
+
exportName: 'Page',
|
|
74
|
+
layout: null,
|
|
75
|
+
routeIndex: 0,
|
|
76
|
+
params: {},
|
|
77
|
+
url: '/?q=1',
|
|
78
|
+
preHydrate: null,
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('uses the RenderRoute status (catch-all 404) and route params', async () => {
|
|
84
|
+
const handler = createHandler(makeManifest() as any, baseDeps as any);
|
|
85
|
+
const response = await handler(new Request('http://localhost/not/a/page'));
|
|
86
|
+
expect(response.status).toBe(404);
|
|
87
|
+
const html = await response.text();
|
|
88
|
+
expect(html).toContain('"params":{"splat":"not/a/page"}');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("render: 'buffered' awaits prerender and sends one document (css leads the body)", async () => {
|
|
92
|
+
let streamed = false;
|
|
93
|
+
const deps = {
|
|
94
|
+
...baseDeps,
|
|
95
|
+
renderToReadableStream: async () => {
|
|
96
|
+
streamed = true;
|
|
97
|
+
return streamOf('');
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const handler = createHandler(makeManifest({ render: 'buffered' }) as any, deps as any);
|
|
101
|
+
const html = await (await handler(new Request('http://localhost/'))).text();
|
|
102
|
+
expect(streamed).toBe(false);
|
|
103
|
+
expect(html).toContain(
|
|
104
|
+
'<div id="root"><style data-octane="x">.x{}</style><main>page</main></div>',
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('emits stylesheet + modulepreload tags for the matched entry from clientAssets', async () => {
|
|
109
|
+
const handler = createHandler(
|
|
110
|
+
makeManifest({
|
|
111
|
+
clientAssets: {
|
|
112
|
+
'/src/Page.tsrx': { js: 'assets/Page-123.js', css: ['assets/Page-123.css'] },
|
|
113
|
+
},
|
|
114
|
+
}) as any,
|
|
115
|
+
baseDeps as any,
|
|
116
|
+
);
|
|
117
|
+
const html = await (await handler(new Request('http://localhost/'))).text();
|
|
118
|
+
expect(html).toContain('<link rel="stylesheet" href="/assets/Page-123.css">');
|
|
119
|
+
expect(html).toContain('<link rel="modulepreload" href="/assets/Page-123.js">');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('serves ServerRoutes and 404s unmatched paths when no catch-all exists', async () => {
|
|
123
|
+
const manifest = makeManifest();
|
|
124
|
+
(manifest.routes as unknown[]).splice(1, 1); // drop the catch-all
|
|
125
|
+
const handler = createHandler(manifest as any, baseDeps as any);
|
|
126
|
+
|
|
127
|
+
const api = await handler(new Request('http://localhost/api/ping'));
|
|
128
|
+
expect(api.status).toBe(200);
|
|
129
|
+
expect(await api.text()).toBe('pong');
|
|
130
|
+
|
|
131
|
+
const missing = await handler(new Request('http://localhost/nope'));
|
|
132
|
+
expect(missing.status).toBe(404);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// @octanejs/vite-plugin unit tests — the pure decision logic the website
|
|
2
|
+
// surfaced gaps in: the dev-middleware's Vite-owned URL filter, `octane()`
|
|
3
|
+
// option forwarding to the bundled compiler, the appType default, and the
|
|
4
|
+
// config resolution of `router.preHydrate` / RenderRoute `status`.
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { dirname } from 'node:path';
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { octane, isViteOwnedUrl, resolveOctaneConfig, RenderRoute } from '../src/index.js';
|
|
9
|
+
import { RESOLVED_ADAPTER_BROWSER_STUB_ID } from '../src/project-codegen.js';
|
|
10
|
+
|
|
11
|
+
function url(u: string): URL {
|
|
12
|
+
return new URL(u, 'http://localhost');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Use this package as the fake Vite root: '/src/index.js' and '/types/index.d.ts'
|
|
16
|
+
// are real files under it, '/docs/v2.0' etc. are not.
|
|
17
|
+
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
18
|
+
|
|
19
|
+
describe('isViteOwnedUrl', () => {
|
|
20
|
+
it('skips Vite-internal namespaces and module/asset requests', () => {
|
|
21
|
+
expect(isViteOwnedUrl(url('/@vite/client'))).toBe(true);
|
|
22
|
+
expect(isViteOwnedUrl(url('/@id/virtual:octane-hydrate'))).toBe(true);
|
|
23
|
+
expect(isViteOwnedUrl(url('/@fs/Users/x/repo/src/main.ts'))).toBe(true);
|
|
24
|
+
expect(isViteOwnedUrl(url('/__open-in-editor?file=x'))).toBe(true);
|
|
25
|
+
expect(isViteOwnedUrl(url('/node_modules/.vite/deps/chunk.js'))).toBe(true);
|
|
26
|
+
expect(isViteOwnedUrl(url('/src/main.ts'))).toBe(true);
|
|
27
|
+
expect(isViteOwnedUrl(url('/src/app/App.tsrx?v=abc123'))).toBe(true);
|
|
28
|
+
expect(isViteOwnedUrl(url('/favicon.svg'))).toBe(true);
|
|
29
|
+
expect(isViteOwnedUrl(url('/src/worker?worker'))).toBe(true);
|
|
30
|
+
expect(isViteOwnedUrl(url('/src/logo?url'))).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('keeps page navigations', () => {
|
|
34
|
+
expect(isViteOwnedUrl(url('/'))).toBe(false);
|
|
35
|
+
expect(isViteOwnedUrl(url('/docs'))).toBe(false);
|
|
36
|
+
expect(isViteOwnedUrl(url('/docs/quick-start'))).toBe(false);
|
|
37
|
+
expect(isViteOwnedUrl(url('/definitely/not/a/page'))).toBe(false);
|
|
38
|
+
expect(isViteOwnedUrl(url('/search?q=octane'))).toBe(false);
|
|
39
|
+
// A bare dotfile segment is not an extension.
|
|
40
|
+
expect(isViteOwnedUrl(url('/.well-known'))).toBe(false);
|
|
41
|
+
// VALUED params matching a marker name are app query strings — Vite's
|
|
42
|
+
// transform markers are always bare (`?url`, `?raw`, `&import`).
|
|
43
|
+
expect(isViteOwnedUrl(url('/docs?url=https://example.com'))).toBe(false);
|
|
44
|
+
expect(isViteOwnedUrl(url('/page?raw=1'))).toBe(false);
|
|
45
|
+
expect(isViteOwnedUrl(url('/jobs?worker=nurse'))).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('with fileRoots, an extension only counts when a real file backs it', () => {
|
|
49
|
+
// Real files under the root → Vite's.
|
|
50
|
+
expect(isViteOwnedUrl(url('/src/index.js'), [PKG_ROOT])).toBe(true);
|
|
51
|
+
expect(isViteOwnedUrl(url('/types/index.d.ts?v=abc'), [PKG_ROOT])).toBe(true);
|
|
52
|
+
// Dotted PAGE urls → SSR them.
|
|
53
|
+
expect(isViteOwnedUrl(url('/docs/v2.0'), [PKG_ROOT])).toBe(false);
|
|
54
|
+
expect(isViteOwnedUrl(url('/users/jane.doe'), [PKG_ROOT])).toBe(false);
|
|
55
|
+
// A missing asset also SSRs (the app's 404 page beats a bare dev 404).
|
|
56
|
+
expect(isViteOwnedUrl(url('/missing.png'), [PKG_ROOT])).toBe(false);
|
|
57
|
+
// Multiple roots (root + publicDir).
|
|
58
|
+
expect(isViteOwnedUrl(url('/src/index.js'), ['/nowhere', PKG_ROOT])).toBe(true);
|
|
59
|
+
// Non-file checks are unaffected by fileRoots.
|
|
60
|
+
expect(isViteOwnedUrl(url('/@vite/client'), [PKG_ROOT])).toBe(true);
|
|
61
|
+
expect(isViteOwnedUrl(url('/src/worker?worker'), [PKG_ROOT])).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('octane() plugin factory', () => {
|
|
66
|
+
it('forwards `exclude` to the bundled compiler (hook-slotting skip)', () => {
|
|
67
|
+
const [compiler] = octane({ exclude: ['/packages/tanstack-router/src/'] });
|
|
68
|
+
const code =
|
|
69
|
+
"import { useState } from 'octane';\nexport function useThing() { return useState(0); }\n";
|
|
70
|
+
const transform = compiler.transform as (code: string, id: string) => unknown;
|
|
71
|
+
|
|
72
|
+
// A pnpm-symlinked binding source resolves to /packages/*/src — excluded,
|
|
73
|
+
// so the hand-slot-forwarding file passes through untouched.
|
|
74
|
+
expect(transform.call({}, code, '/repo/packages/tanstack-router/src/useRouter.ts')).toBeNull();
|
|
75
|
+
// App code is still slotted.
|
|
76
|
+
expect(transform.call({}, code, '/repo/src/useThing.ts')).not.toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("defaults appType to 'custom' for dev, but respects the user and `vite preview`", async () => {
|
|
80
|
+
const [, meta] = octane();
|
|
81
|
+
const config = meta.config as (
|
|
82
|
+
userConfig: object,
|
|
83
|
+
env: object,
|
|
84
|
+
) => Promise<{ appType?: string }>;
|
|
85
|
+
|
|
86
|
+
expect((await config({}, { command: 'serve', mode: 'development' })).appType).toBe('custom');
|
|
87
|
+
// An explicit user appType wins.
|
|
88
|
+
expect(
|
|
89
|
+
(await config({ appType: 'spa' }, { command: 'serve', mode: 'development' })).appType,
|
|
90
|
+
).toBe(undefined);
|
|
91
|
+
// `vite preview` serves static files with Vite's own fallback; the
|
|
92
|
+
// production SSR build is previewed with `octane-preview` instead.
|
|
93
|
+
expect(
|
|
94
|
+
(await config({}, { command: 'serve', mode: 'production', isPreview: true })).appType,
|
|
95
|
+
).toBe(undefined);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('resolveOctaneConfig', () => {
|
|
100
|
+
const entry = ['App', '/src/App.tsrx'] as const;
|
|
101
|
+
|
|
102
|
+
it('passes router.preHydrate through and validates its shape', () => {
|
|
103
|
+
const resolved = resolveOctaneConfig({
|
|
104
|
+
router: {
|
|
105
|
+
routes: [new RenderRoute({ path: '/', entry })],
|
|
106
|
+
preHydrate: '/src/pre-hydrate.ts',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
expect(resolved.router.preHydrate).toBe('/src/pre-hydrate.ts');
|
|
110
|
+
// Absent stays absent.
|
|
111
|
+
expect(resolveOctaneConfig({}).router.preHydrate).toBe(undefined);
|
|
112
|
+
// Must be a Vite-root path the browser can dynamic-import.
|
|
113
|
+
expect(() =>
|
|
114
|
+
resolveOctaneConfig({ router: { routes: [], preHydrate: 'src/pre-hydrate.ts' } }),
|
|
115
|
+
).toThrow(/preHydrate/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('accepts an integer RenderRoute status and rejects anything else', () => {
|
|
119
|
+
const route = new RenderRoute({ path: '/*splat', entry, status: 404 });
|
|
120
|
+
expect(route.status).toBe(404);
|
|
121
|
+
expect(resolveOctaneConfig({ router: { routes: [route] } }).router.routes[0]).toBe(route);
|
|
122
|
+
expect(() =>
|
|
123
|
+
resolveOctaneConfig({
|
|
124
|
+
router: { routes: [new RenderRoute({ path: '/', entry, status: 4.04 })] },
|
|
125
|
+
}),
|
|
126
|
+
).toThrow(/status/);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('server-only adapter browser stub', () => {
|
|
131
|
+
const [, meta] = octane();
|
|
132
|
+
const resolveId = meta.resolveId as (
|
|
133
|
+
id: string,
|
|
134
|
+
importer?: string,
|
|
135
|
+
options?: { ssr?: boolean },
|
|
136
|
+
) => Promise<string | null>;
|
|
137
|
+
const load = meta.load as (id: string) => Promise<string | undefined>;
|
|
138
|
+
|
|
139
|
+
it('client-side imports of adapter packages resolve to the stub, server gets the real one', async () => {
|
|
140
|
+
for (const id of ['@octanejs/adapter-vercel', '@ripple-ts/adapter-node']) {
|
|
141
|
+
expect(await resolveId(id, undefined, { ssr: false })).toBe(RESOLVED_ADAPTER_BROWSER_STUB_ID);
|
|
142
|
+
}
|
|
143
|
+
expect(await resolveId('@octanejs/adapter-vercel', undefined, { ssr: true })).toBe(null);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('the stub covers the octane adapter surface with no node builtins', async () => {
|
|
147
|
+
const source = (await load(RESOLVED_ADAPTER_BROWSER_STUB_ID)) as string;
|
|
148
|
+
// The union of the listed adapters' public names — a client import of any
|
|
149
|
+
// of them must resolve (and only throw on USE).
|
|
150
|
+
for (const name of ['vercel', 'adapt', 'serve']) {
|
|
151
|
+
expect(source).toContain(`export function ${name}`);
|
|
152
|
+
}
|
|
153
|
+
expect(source).not.toContain('node:');
|
|
154
|
+
});
|
|
155
|
+
});
|