@octanejs/adapter-vercel 0.0.1
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/LICENSE +21 -0
- package/package.json +32 -0
- package/src/adapt.js +216 -0
- package/src/index.js +42 -0
- package/tests/adapt.test.ts +100 -0
- package/tsconfig.typecheck.json +18 -0
- package/types/index.d.ts +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominic Gannaway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/adapter-vercel",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Vercel adapter for octane apps (Build Output API v3)",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Dominic Gannaway",
|
|
9
|
+
"email": "dg@domgan.com"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
17
|
+
"directory": "packages/adapter-vercel"
|
|
18
|
+
},
|
|
19
|
+
"module": "src/index.js",
|
|
20
|
+
"main": "src/index.js",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./types/index.d.ts",
|
|
24
|
+
"import": "./src/index.js",
|
|
25
|
+
"default": "./src/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^24.3.0",
|
|
30
|
+
"@octanejs/vite-plugin": "0.1.3"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/adapt.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Vercel Build Output API v3 emitter.
|
|
4
|
+
*
|
|
5
|
+
* Restructures an octane production build (`dist/client` + `dist/server`,
|
|
6
|
+
* produced by @octanejs/vite-plugin) into `.vercel/output/`:
|
|
7
|
+
*
|
|
8
|
+
* static/ ← dist/client verbatim (hashed assets; the build
|
|
9
|
+
* already moved index.html — the SSR template —
|
|
10
|
+
* to dist/server, so nothing shadows SSR at '/')
|
|
11
|
+
* functions/index.func/ ← dist/server verbatim + a generated index.js that
|
|
12
|
+
* wraps the entry's fetch `handler` in Vercel's
|
|
13
|
+
* Web-Standard function shape
|
|
14
|
+
* config.json ← routing: immutable asset headers → filesystem →
|
|
15
|
+
* catch-all to the function
|
|
16
|
+
*
|
|
17
|
+
* Modeled on @ripple-ts/adapter-vercel (itself after @sveltejs/adapter-vercel),
|
|
18
|
+
* with one octane simplification: the server entry is SELF-CONTAINED (the
|
|
19
|
+
* plugin bundles the app + octane with `ssr.noExternal: true`), so there is no
|
|
20
|
+
* dependency tracing — the function is dist/server plus one wrapper file.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** @import { VercelAdapterOptions, VercelConfig, VercelRoute } from '@octanejs/adapter-vercel' */
|
|
24
|
+
/** @import { AdaptContext } from '@octanejs/vite-plugin' */
|
|
25
|
+
|
|
26
|
+
import { existsSync, mkdirSync, cpSync, writeFileSync, rmSync } from 'node:fs';
|
|
27
|
+
import { join, dirname } from 'node:path';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
|
|
30
|
+
const VERCEL_OUTPUT_DIR = '.vercel/output';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The adapter's own version (for .vc-config.json `framework`). MUST stay lazy:
|
|
34
|
+
* octane.config.ts is compiled into the server bundle, so this module's scope
|
|
35
|
+
* executes when the deployed function boots — where `../package.json` does not
|
|
36
|
+
* exist. adapt() itself only ever runs at build time from the real package.
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function get_adapter_version() {
|
|
40
|
+
try {
|
|
41
|
+
const require = createRequire(import.meta.url);
|
|
42
|
+
return require('../package.json').version;
|
|
43
|
+
} catch {
|
|
44
|
+
return '0.0.0';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Default Node.js runtime for the function, from the build machine's major.
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
function get_default_runtime() {
|
|
53
|
+
const major = Number(process.version.slice(1).split('.')[0]);
|
|
54
|
+
const valid = [20, 22, 24];
|
|
55
|
+
if (!valid.includes(major)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`[@octanejs/adapter-vercel] Unsupported Node.js version: ${process.version}. ` +
|
|
58
|
+
`Build with Node ${valid.join('/')} or set serverless.runtime explicitly.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return `nodejs${major}.x`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} file_path
|
|
66
|
+
* @param {string} data
|
|
67
|
+
*/
|
|
68
|
+
function write(file_path, data) {
|
|
69
|
+
mkdirSync(dirname(file_path), { recursive: true });
|
|
70
|
+
writeFileSync(file_path, data);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The serverless function entry: Vercel's Web-Standard shape
|
|
75
|
+
* (`export default { fetch(Request) => Response }`) over the built handler.
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
function generate_handler_source() {
|
|
79
|
+
return `// Auto-generated by @octanejs/adapter-vercel — the Vercel function entry.
|
|
80
|
+
// The imported handler is the self-contained octane SSR bundle's fetch handler.
|
|
81
|
+
import { handler } from './entry.js';
|
|
82
|
+
|
|
83
|
+
export default {
|
|
84
|
+
async fetch(request) {
|
|
85
|
+
try {
|
|
86
|
+
return await handler(request);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('[octane] Serverless handler error:', error);
|
|
89
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build Output API v3 config.json.
|
|
98
|
+
*
|
|
99
|
+
* @param {VercelAdapterOptions} options
|
|
100
|
+
* @returns {VercelConfig}
|
|
101
|
+
*/
|
|
102
|
+
function generate_vercel_config(options) {
|
|
103
|
+
const { cleanUrls = true, trailingSlash, images, headers = [], redirects = [] } = options;
|
|
104
|
+
|
|
105
|
+
/** @type {VercelRoute[]} */
|
|
106
|
+
const routes = [];
|
|
107
|
+
|
|
108
|
+
for (const redirect of redirects) {
|
|
109
|
+
routes.push({
|
|
110
|
+
src: redirect.source,
|
|
111
|
+
headers: { Location: redirect.destination },
|
|
112
|
+
status: redirect.permanent ? 308 : 307,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Hashed build assets are immutable by construction.
|
|
117
|
+
routes.push({
|
|
118
|
+
src: '/assets/.+',
|
|
119
|
+
headers: { 'Cache-Control': 'public, max-age=31536000, immutable' },
|
|
120
|
+
continue: true,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
for (const header of headers) {
|
|
124
|
+
routes.push({
|
|
125
|
+
src: header.source,
|
|
126
|
+
headers: Object.fromEntries(header.headers.map((h) => [h.key, h.value])),
|
|
127
|
+
continue: true,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Static files win first; everything else — every page, including the
|
|
132
|
+
// catch-all 404 route — is server-rendered by the function.
|
|
133
|
+
routes.push({ handle: 'filesystem' });
|
|
134
|
+
routes.push({ src: '/.*', dest: '/index' });
|
|
135
|
+
|
|
136
|
+
/** @type {VercelConfig} */
|
|
137
|
+
const config = { version: 3, routes };
|
|
138
|
+
if (cleanUrls !== undefined) config.cleanUrls = cleanUrls;
|
|
139
|
+
if (trailingSlash !== undefined) config.trailingSlash = trailingSlash;
|
|
140
|
+
if (images) config.images = images;
|
|
141
|
+
return config;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Emit `.vercel/output` from a completed octane build.
|
|
146
|
+
*
|
|
147
|
+
* Callable two ways: by @octanejs/vite-plugin's closeBundle (which passes its
|
|
148
|
+
* {@link AdaptContext}) or directly with explicit dirs.
|
|
149
|
+
*
|
|
150
|
+
* @param {AdaptContext} ctx
|
|
151
|
+
* @param {VercelAdapterOptions} [options]
|
|
152
|
+
* @returns {Promise<void>}
|
|
153
|
+
*/
|
|
154
|
+
export async function adapt(ctx, options = {}) {
|
|
155
|
+
const { root, clientDir, serverDir } = ctx;
|
|
156
|
+
const log = ctx.log ?? ((/** @type {string} */ msg) => console.log(msg));
|
|
157
|
+
|
|
158
|
+
const server_entry = join(serverDir, 'entry.js');
|
|
159
|
+
if (!existsSync(clientDir)) {
|
|
160
|
+
throw new Error(`[@octanejs/adapter-vercel] Client build output not found at ${clientDir}.`);
|
|
161
|
+
}
|
|
162
|
+
if (!existsSync(server_entry)) {
|
|
163
|
+
throw new Error(`[@octanejs/adapter-vercel] Server entry not found at ${server_entry}.`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const output_dir = join(root, VERCEL_OUTPUT_DIR);
|
|
167
|
+
rmSync(output_dir, { recursive: true, force: true });
|
|
168
|
+
mkdirSync(output_dir, { recursive: true });
|
|
169
|
+
|
|
170
|
+
// 1. Static assets — dist/client verbatim (no index.html in it: the build
|
|
171
|
+
// moved the SSR template to dist/server).
|
|
172
|
+
const static_dir = join(output_dir, 'static');
|
|
173
|
+
mkdirSync(static_dir, { recursive: true });
|
|
174
|
+
cpSync(clientDir, static_dir, { recursive: true });
|
|
175
|
+
|
|
176
|
+
// 2. The serverless function — dist/server verbatim (self-contained entry +
|
|
177
|
+
// the SSR template + any dynamic-import chunks) plus the wrapper.
|
|
178
|
+
const func_dir = join(output_dir, 'functions', 'index.func');
|
|
179
|
+
mkdirSync(func_dir, { recursive: true });
|
|
180
|
+
cpSync(serverDir, func_dir, { recursive: true });
|
|
181
|
+
write(join(func_dir, 'index.js'), generate_handler_source());
|
|
182
|
+
write(join(func_dir, 'package.json'), JSON.stringify({ type: 'module' }));
|
|
183
|
+
|
|
184
|
+
const runtime = options.serverless?.runtime ?? get_default_runtime();
|
|
185
|
+
/** @type {Record<string, unknown>} */
|
|
186
|
+
const vc_config = {
|
|
187
|
+
runtime,
|
|
188
|
+
handler: 'index.js',
|
|
189
|
+
launcherType: 'Nodejs',
|
|
190
|
+
experimentalResponseStreaming: true,
|
|
191
|
+
framework: { slug: 'octane', version: get_adapter_version() },
|
|
192
|
+
};
|
|
193
|
+
if (options.serverless?.regions) vc_config.regions = options.serverless.regions;
|
|
194
|
+
if (options.serverless?.memory) vc_config.memory = options.serverless.memory;
|
|
195
|
+
if (options.serverless?.maxDuration) vc_config.maxDuration = options.serverless.maxDuration;
|
|
196
|
+
|
|
197
|
+
// ISR: cache the function's response at the edge, revalidate after
|
|
198
|
+
// `expiration` seconds (false = never expire).
|
|
199
|
+
if (options.isr) {
|
|
200
|
+
/** @type {Record<string, unknown>} */
|
|
201
|
+
const prerender = { expiration: options.isr.expiration };
|
|
202
|
+
if (options.isr.bypassToken) prerender.bypassToken = options.isr.bypassToken;
|
|
203
|
+
if (options.isr.allowQuery !== undefined) prerender.allowQuery = options.isr.allowQuery;
|
|
204
|
+
vc_config.prerender = prerender;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
write(join(func_dir, '.vc-config.json'), JSON.stringify(vc_config, null, '\t'));
|
|
208
|
+
|
|
209
|
+
// 3. Routing config.
|
|
210
|
+
write(
|
|
211
|
+
join(output_dir, 'config.json'),
|
|
212
|
+
JSON.stringify(generate_vercel_config(options), null, '\t'),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
log(`[@octanejs/adapter-vercel] Build Output written to ${VERCEL_OUTPUT_DIR} (${runtime})`);
|
|
216
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @octanejs/adapter-vercel — Vercel adapter for octane apps.
|
|
4
|
+
*
|
|
5
|
+
* Usage (octane.config.ts):
|
|
6
|
+
*
|
|
7
|
+
* import { vercel } from '@octanejs/adapter-vercel';
|
|
8
|
+
* export default defineConfig({
|
|
9
|
+
* adapter: vercel(),
|
|
10
|
+
* router: { … },
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* `vercel(options)` returns the plugin's adapter contract: after `vite build`
|
|
14
|
+
* produces both bundles, @octanejs/vite-plugin calls `adapt(ctx)` and this
|
|
15
|
+
* adapter restructures them into Vercel's Build Output API v3 under
|
|
16
|
+
* `.vercel/output/` — no vercel.json rewrites, no hand-written api/ function.
|
|
17
|
+
* The same contract shape (`{ name, adapt }`) is what a Cloudflare/Netlify
|
|
18
|
+
* adapter implements to target other platforms.
|
|
19
|
+
*
|
|
20
|
+
* No `serve`/`runtime` overrides are provided: Vercel functions run on Node,
|
|
21
|
+
* and the generated server entry's built-in Node defaults (AsyncLocalStorage
|
|
22
|
+
* async context, sha-256 hash) are exactly right — `octane-preview` keeps
|
|
23
|
+
* serving dist/server locally, untouched by the adapter.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** @import { VercelAdapterOptions } from '@octanejs/adapter-vercel' */
|
|
27
|
+
/** @import { OctaneAdapter } from '@octanejs/vite-plugin' */
|
|
28
|
+
|
|
29
|
+
import { adapt } from './adapt.js';
|
|
30
|
+
|
|
31
|
+
export { adapt };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {VercelAdapterOptions} [options]
|
|
35
|
+
* @returns {OctaneAdapter}
|
|
36
|
+
*/
|
|
37
|
+
export function vercel(options = {}) {
|
|
38
|
+
return {
|
|
39
|
+
name: 'vercel',
|
|
40
|
+
adapt: (ctx) => adapt(ctx, options),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
//
|
|
3
|
+
// adapt() emitter test — feeds a fake completed octane build (the dist/client +
|
|
4
|
+
// dist/server layout @octanejs/vite-plugin produces) through adapt() and
|
|
5
|
+
// asserts the emitted `.vercel/output` is valid Build Output API v3: static
|
|
6
|
+
// assets, one self-contained Node function, and the routing config. The
|
|
7
|
+
// emitted function entry is then actually imported and invoked. The end-to-end
|
|
8
|
+
// wiring (closeBundle → adapt via `adapter: vercel()`) is covered by the
|
|
9
|
+
// website's ssr-smoke test, which runs the real `vite build`.
|
|
10
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
14
|
+
import { adapt } from '../src/index.js';
|
|
15
|
+
|
|
16
|
+
// The tests dir doubles as the fake project root: the fake build goes in
|
|
17
|
+
// `dist/` and adapt() writes `.vercel/output/` next to it — both gitignored.
|
|
18
|
+
const root = fileURLToPath(new URL('.', import.meta.url));
|
|
19
|
+
const clientDir = path.join(root, 'dist/client');
|
|
20
|
+
const serverDir = path.join(root, 'dist/server');
|
|
21
|
+
const outputDir = path.join(root, '.vercel/output');
|
|
22
|
+
const funcDir = path.join(outputDir, 'functions/index.func');
|
|
23
|
+
|
|
24
|
+
// A stand-in for the plugin's self-contained server bundle: same shape (fetch
|
|
25
|
+
// `handler` export next to the SSR template), no dependencies.
|
|
26
|
+
const FAKE_ENTRY = `export const handler = async (req) => new Response('ok:' + new URL(req.url).pathname);\n`;
|
|
27
|
+
|
|
28
|
+
function write(file: string, data: string) {
|
|
29
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
30
|
+
fs.writeFileSync(file, data);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
beforeAll(async () => {
|
|
34
|
+
fs.rmSync(path.join(root, 'dist'), { recursive: true, force: true });
|
|
35
|
+
fs.rmSync(path.join(root, '.vercel'), { recursive: true, force: true });
|
|
36
|
+
|
|
37
|
+
write(path.join(clientDir, 'assets/app-abc.js'), 'console.log("app");\n');
|
|
38
|
+
write(path.join(serverDir, 'entry.js'), FAKE_ENTRY);
|
|
39
|
+
write(path.join(serverDir, 'index.html'), '<!doctype html><html><!--ssr-body--></html>\n');
|
|
40
|
+
|
|
41
|
+
await adapt({ root, outDir: 'dist', clientDir, serverDir, log: () => {} });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterAll(() => {
|
|
45
|
+
fs.rmSync(path.join(root, 'dist'), { recursive: true, force: true });
|
|
46
|
+
fs.rmSync(path.join(root, '.vercel'), { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('adapt()', () => {
|
|
50
|
+
it('emits Build Output API v3 routing config', () => {
|
|
51
|
+
const config = JSON.parse(fs.readFileSync(path.join(outputDir, 'config.json'), 'utf-8'));
|
|
52
|
+
expect(config.version).toBe(3);
|
|
53
|
+
|
|
54
|
+
// Static files first, then EVERYTHING else (including the 404 catch-all
|
|
55
|
+
// route) goes to the SSR function.
|
|
56
|
+
const filesystemIndex = config.routes.findIndex(
|
|
57
|
+
(r: Record<string, unknown>) => r.handle === 'filesystem',
|
|
58
|
+
);
|
|
59
|
+
const catchAllIndex = config.routes.findIndex(
|
|
60
|
+
(r: Record<string, unknown>) => r.src === '/.*' && r.dest === '/index',
|
|
61
|
+
);
|
|
62
|
+
expect(filesystemIndex).toBeGreaterThanOrEqual(0);
|
|
63
|
+
expect(catchAllIndex).toBe(filesystemIndex + 1);
|
|
64
|
+
|
|
65
|
+
// Hashed assets are immutable, applied BEFORE the filesystem handler.
|
|
66
|
+
const assetsRoute = config.routes.find((r: Record<string, unknown>) => r.src === '/assets/.+');
|
|
67
|
+
expect(assetsRoute).toMatchObject({
|
|
68
|
+
headers: { 'Cache-Control': 'public, max-age=31536000, immutable' },
|
|
69
|
+
continue: true,
|
|
70
|
+
});
|
|
71
|
+
expect(config.routes.indexOf(assetsRoute)).toBeLessThan(filesystemIndex);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('copies the client bundle to static/ (no index.html to shadow SSR)', () => {
|
|
75
|
+
expect(fs.existsSync(path.join(outputDir, 'static/assets/app-abc.js'))).toBe(true);
|
|
76
|
+
expect(fs.existsSync(path.join(outputDir, 'static/index.html'))).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('emits the serverless function: dist/server verbatim + wrapper + config', () => {
|
|
80
|
+
for (const file of ['index.js', 'entry.js', 'index.html', '.vc-config.json', 'package.json']) {
|
|
81
|
+
expect(fs.existsSync(path.join(funcDir, file)), file).toBe(true);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const vcConfig = JSON.parse(fs.readFileSync(path.join(funcDir, '.vc-config.json'), 'utf-8'));
|
|
85
|
+
expect(vcConfig.handler).toBe('index.js');
|
|
86
|
+
expect(vcConfig.launcherType).toBe('Nodejs');
|
|
87
|
+
expect(vcConfig.runtime).toMatch(/^nodejs(20|22|24)\.x$/);
|
|
88
|
+
|
|
89
|
+
// The wrapper imports as ESM — the function dir must say so.
|
|
90
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(funcDir, 'package.json'), 'utf-8'));
|
|
91
|
+
expect(pkg.type).toBe('module');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('emitted function entry serves requests through the bundled handler', async () => {
|
|
95
|
+
const mod = await import(pathToFileURL(path.join(funcDir, 'index.js')).href);
|
|
96
|
+
const response = await mod.default.fetch(new Request('http://x/hello'));
|
|
97
|
+
expect(response.status).toBe(200);
|
|
98
|
+
expect(await response.text()).toBe('ok:/hello');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "nodenext",
|
|
4
|
+
"moduleResolution": "nodenext",
|
|
5
|
+
"target": "es2022",
|
|
6
|
+
"lib": ["esnext", "dom", "dom.iterable"],
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"types": ["node"],
|
|
13
|
+
"allowJs": true,
|
|
14
|
+
"checkJs": false
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*", "types/**/*", "tests/**/*.ts"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { OctaneAdapter, AdaptContext } from '@octanejs/vite-plugin';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Adapter options
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
/** Serverless function configuration (functions/index.func/.vc-config.json). */
|
|
8
|
+
export interface ServerlessConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Node.js runtime version for the function.
|
|
11
|
+
* @default auto-detected from the build machine's Node major (20/22/24)
|
|
12
|
+
*/
|
|
13
|
+
runtime?: string;
|
|
14
|
+
/** Regions to deploy the function to (e.g. ['iad1']). */
|
|
15
|
+
regions?: string[];
|
|
16
|
+
/** Maximum execution duration in seconds. */
|
|
17
|
+
maxDuration?: number;
|
|
18
|
+
/** Memory in MB allocated to the function. */
|
|
19
|
+
memory?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Incremental Static Regeneration — edge-cache the SSR response. */
|
|
23
|
+
export interface ISRConfig {
|
|
24
|
+
/** Seconds before the cached response regenerates; `false` = never expires. */
|
|
25
|
+
expiration: number | false;
|
|
26
|
+
/** Token bypassing the cache (`__prerender_bypass` cookie / revalidate header). */
|
|
27
|
+
bypassToken?: string;
|
|
28
|
+
/** Query params that are part of the cache key. */
|
|
29
|
+
allowQuery?: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface VercelAdapterOptions {
|
|
33
|
+
serverless?: ServerlessConfig;
|
|
34
|
+
isr?: ISRConfig;
|
|
35
|
+
/** @default true */
|
|
36
|
+
cleanUrls?: boolean;
|
|
37
|
+
trailingSlash?: boolean;
|
|
38
|
+
/** Vercel image optimization config (passed through to config.json). */
|
|
39
|
+
images?: Record<string, unknown>;
|
|
40
|
+
/** Extra response headers, matched by regex source. */
|
|
41
|
+
headers?: Array<{ source: string; headers: Array<{ key: string; value: string }> }>;
|
|
42
|
+
/** Redirects, matched by regex source. */
|
|
43
|
+
redirects?: Array<{ source: string; destination: string; permanent?: boolean }>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Build Output API v3 shapes (what adapt() writes)
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
export interface VercelRoute {
|
|
51
|
+
src?: string;
|
|
52
|
+
dest?: string;
|
|
53
|
+
headers?: Record<string, string>;
|
|
54
|
+
status?: number;
|
|
55
|
+
continue?: boolean;
|
|
56
|
+
handle?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface VercelConfig {
|
|
60
|
+
version: 3;
|
|
61
|
+
routes: VercelRoute[];
|
|
62
|
+
cleanUrls?: boolean;
|
|
63
|
+
trailingSlash?: boolean;
|
|
64
|
+
images?: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Entry points
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The octane.config.ts adapter: @octanejs/vite-plugin runs its `adapt()` after
|
|
73
|
+
* `vite build` produces dist/client + dist/server, emitting `.vercel/output/`
|
|
74
|
+
* (Build Output API v3).
|
|
75
|
+
*/
|
|
76
|
+
export function vercel(options?: VercelAdapterOptions): OctaneAdapter;
|
|
77
|
+
|
|
78
|
+
/** The emitter itself — callable directly with an explicit build context. */
|
|
79
|
+
export function adapt(ctx: AdaptContext, options?: VercelAdapterOptions): Promise<void>;
|