@octanejs/adapter-vercel 0.0.1 → 0.0.3
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 +12 -2
- package/src/adapt.js +42 -17
- package/src/index.js +3 -3
- package/types/index.d.ts +5 -5
- package/tests/adapt.test.ts +0 -100
- package/tsconfig.typecheck.json +0 -18
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/adapter-vercel",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
6
9
|
"description": "Vercel adapter for octane apps (Build Output API v3)",
|
|
7
10
|
"author": {
|
|
8
11
|
"name": "Dominic Gannaway",
|
|
@@ -16,6 +19,10 @@
|
|
|
16
19
|
"url": "git+https://github.com/octanejs/octane.git",
|
|
17
20
|
"directory": "packages/adapter-vercel"
|
|
18
21
|
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"types"
|
|
25
|
+
],
|
|
19
26
|
"module": "src/index.js",
|
|
20
27
|
"main": "src/index.js",
|
|
21
28
|
"exports": {
|
|
@@ -25,8 +32,11 @@
|
|
|
25
32
|
"default": "./src/index.js"
|
|
26
33
|
}
|
|
27
34
|
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@octanejs/app-core": "0.0.2"
|
|
37
|
+
},
|
|
28
38
|
"devDependencies": {
|
|
29
39
|
"@types/node": "^24.3.0",
|
|
30
|
-
"@octanejs/
|
|
40
|
+
"@octanejs/app-core": "0.0.2"
|
|
31
41
|
}
|
|
32
42
|
}
|
package/src/adapt.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Vercel Build Output API v3 emitter.
|
|
4
4
|
*
|
|
5
5
|
* Restructures an octane production build (`dist/client` + `dist/server`,
|
|
6
|
-
* produced by
|
|
6
|
+
* produced by an Octane app integration) into `.vercel/output/`:
|
|
7
7
|
*
|
|
8
8
|
* static/ ← dist/client verbatim (hashed assets; the build
|
|
9
9
|
* already moved index.html — the SSR template —
|
|
@@ -21,13 +21,15 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
/** @import { VercelAdapterOptions, VercelConfig, VercelRoute } from '@octanejs/adapter-vercel' */
|
|
24
|
-
/** @import { AdaptContext } from '@octanejs/
|
|
24
|
+
/** @import { AdaptContext } from '@octanejs/app-core' */
|
|
25
25
|
|
|
26
26
|
import { existsSync, mkdirSync, cpSync, writeFileSync, rmSync } from 'node:fs';
|
|
27
27
|
import { join, dirname } from 'node:path';
|
|
28
28
|
import { createRequire } from 'node:module';
|
|
29
29
|
|
|
30
30
|
const VERCEL_OUTPUT_DIR = '.vercel/output';
|
|
31
|
+
const VALID_NODE_MAJORS = [22, 24];
|
|
32
|
+
const VALID_NODE_RUNTIMES = new Set(VALID_NODE_MAJORS.map((major) => `nodejs${major}.x`));
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* The adapter's own version (for .vc-config.json `framework`). MUST stay lazy:
|
|
@@ -51,16 +53,32 @@ function get_adapter_version() {
|
|
|
51
53
|
*/
|
|
52
54
|
function get_default_runtime() {
|
|
53
55
|
const major = Number(process.version.slice(1).split('.')[0]);
|
|
54
|
-
|
|
55
|
-
if (!valid.includes(major)) {
|
|
56
|
+
if (!VALID_NODE_MAJORS.includes(major)) {
|
|
56
57
|
throw new Error(
|
|
57
58
|
`[@octanejs/adapter-vercel] Unsupported Node.js version: ${process.version}. ` +
|
|
58
|
-
`Build with Node ${
|
|
59
|
+
`Build with Node ${VALID_NODE_MAJORS.join('/')} or set serverless.runtime to ` +
|
|
60
|
+
'nodejs22.x/nodejs24.x explicitly.',
|
|
59
61
|
);
|
|
60
62
|
}
|
|
61
63
|
return `nodejs${major}.x`;
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Reject deployment runtimes outside Octane's supported Node.js baseline.
|
|
68
|
+
* An explicit runtime still lets a newer build machine target Node 22 or 24.
|
|
69
|
+
* @param {string} runtime
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function validate_runtime(runtime) {
|
|
73
|
+
if (!VALID_NODE_RUNTIMES.has(runtime)) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`[@octanejs/adapter-vercel] Unsupported serverless runtime: ${runtime}. ` +
|
|
76
|
+
'Use nodejs22.x or nodejs24.x.',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return runtime;
|
|
80
|
+
}
|
|
81
|
+
|
|
64
82
|
/**
|
|
65
83
|
* @param {string} file_path
|
|
66
84
|
* @param {string} data
|
|
@@ -113,12 +131,15 @@ function generate_vercel_config(options) {
|
|
|
113
131
|
});
|
|
114
132
|
}
|
|
115
133
|
|
|
116
|
-
// Hashed build assets are immutable by construction.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
// Hashed build assets are immutable by construction. Vite emits them under
|
|
135
|
+
// /assets, while Rsbuild uses /static by default.
|
|
136
|
+
for (const assetDirectory of ['assets', 'static']) {
|
|
137
|
+
routes.push({
|
|
138
|
+
src: `/${assetDirectory}/.+`,
|
|
139
|
+
headers: { 'Cache-Control': 'public, max-age=31536000, immutable' },
|
|
140
|
+
continue: true,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
122
143
|
|
|
123
144
|
for (const header of headers) {
|
|
124
145
|
routes.push({
|
|
@@ -144,7 +165,7 @@ function generate_vercel_config(options) {
|
|
|
144
165
|
/**
|
|
145
166
|
* Emit `.vercel/output` from a completed octane build.
|
|
146
167
|
*
|
|
147
|
-
* Callable two ways: by
|
|
168
|
+
* Callable two ways: by an app integration's post-build hook (which passes its
|
|
148
169
|
* {@link AdaptContext}) or directly with explicit dirs.
|
|
149
170
|
*
|
|
150
171
|
* @param {AdaptContext} ctx
|
|
@@ -162,6 +183,7 @@ export async function adapt(ctx, options = {}) {
|
|
|
162
183
|
if (!existsSync(server_entry)) {
|
|
163
184
|
throw new Error(`[@octanejs/adapter-vercel] Server entry not found at ${server_entry}.`);
|
|
164
185
|
}
|
|
186
|
+
const runtime = validate_runtime(options.serverless?.runtime ?? get_default_runtime());
|
|
165
187
|
|
|
166
188
|
const output_dir = join(root, VERCEL_OUTPUT_DIR);
|
|
167
189
|
rmSync(output_dir, { recursive: true, force: true });
|
|
@@ -181,27 +203,30 @@ export async function adapt(ctx, options = {}) {
|
|
|
181
203
|
write(join(func_dir, 'index.js'), generate_handler_source());
|
|
182
204
|
write(join(func_dir, 'package.json'), JSON.stringify({ type: 'module' }));
|
|
183
205
|
|
|
184
|
-
const runtime = options.serverless?.runtime ?? get_default_runtime();
|
|
185
206
|
/** @type {Record<string, unknown>} */
|
|
186
207
|
const vc_config = {
|
|
187
208
|
runtime,
|
|
188
209
|
handler: 'index.js',
|
|
189
210
|
launcherType: 'Nodejs',
|
|
190
|
-
|
|
211
|
+
supportsResponseStreaming: true,
|
|
191
212
|
framework: { slug: 'octane', version: get_adapter_version() },
|
|
192
213
|
};
|
|
193
214
|
if (options.serverless?.regions) vc_config.regions = options.serverless.regions;
|
|
194
215
|
if (options.serverless?.memory) vc_config.memory = options.serverless.memory;
|
|
195
216
|
if (options.serverless?.maxDuration) vc_config.maxDuration = options.serverless.maxDuration;
|
|
196
217
|
|
|
197
|
-
// ISR
|
|
198
|
-
//
|
|
218
|
+
// ISR belongs in an ADJACENT prerender-config file, not in the function's
|
|
219
|
+
// .vc-config.json. Build Output API associates functions/index.func with
|
|
220
|
+
// functions/index.prerender-config.json by basename.
|
|
199
221
|
if (options.isr) {
|
|
200
222
|
/** @type {Record<string, unknown>} */
|
|
201
223
|
const prerender = { expiration: options.isr.expiration };
|
|
202
224
|
if (options.isr.bypassToken) prerender.bypassToken = options.isr.bypassToken;
|
|
203
225
|
if (options.isr.allowQuery !== undefined) prerender.allowQuery = options.isr.allowQuery;
|
|
204
|
-
|
|
226
|
+
write(
|
|
227
|
+
join(output_dir, 'functions', 'index.prerender-config.json'),
|
|
228
|
+
JSON.stringify(prerender, null, '\t'),
|
|
229
|
+
);
|
|
205
230
|
}
|
|
206
231
|
|
|
207
232
|
write(join(func_dir, '.vc-config.json'), JSON.stringify(vc_config, null, '\t'));
|
package/src/index.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* router: { … },
|
|
11
11
|
* });
|
|
12
12
|
*
|
|
13
|
-
* `vercel(options)` returns the
|
|
14
|
-
* produces both bundles,
|
|
13
|
+
* `vercel(options)` returns the shared adapter contract: after a production
|
|
14
|
+
* build produces both bundles, the active integration calls `adapt(ctx)` and this
|
|
15
15
|
* adapter restructures them into Vercel's Build Output API v3 under
|
|
16
16
|
* `.vercel/output/` — no vercel.json rewrites, no hand-written api/ function.
|
|
17
17
|
* The same contract shape (`{ name, adapt }`) is what a Cloudflare/Netlify
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
/** @import { VercelAdapterOptions } from '@octanejs/adapter-vercel' */
|
|
27
|
-
/** @import { OctaneAdapter } from '@octanejs/
|
|
27
|
+
/** @import { OctaneAdapter } from '@octanejs/app-core' */
|
|
28
28
|
|
|
29
29
|
import { adapt } from './adapt.js';
|
|
30
30
|
|
package/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OctaneAdapter, AdaptContext } from '@octanejs/
|
|
1
|
+
import type { OctaneAdapter, AdaptContext } from '@octanejs/app-core';
|
|
2
2
|
|
|
3
3
|
// ============================================================================
|
|
4
4
|
// Adapter options
|
|
@@ -8,9 +8,9 @@ import type { OctaneAdapter, AdaptContext } from '@octanejs/vite-plugin';
|
|
|
8
8
|
export interface ServerlessConfig {
|
|
9
9
|
/**
|
|
10
10
|
* Node.js runtime version for the function.
|
|
11
|
-
* @default auto-detected from the build machine's Node major (
|
|
11
|
+
* @default auto-detected from the build machine's Node major (22/24)
|
|
12
12
|
*/
|
|
13
|
-
runtime?:
|
|
13
|
+
runtime?: 'nodejs22.x' | 'nodejs24.x';
|
|
14
14
|
/** Regions to deploy the function to (e.g. ['iad1']). */
|
|
15
15
|
regions?: string[];
|
|
16
16
|
/** Maximum execution duration in seconds. */
|
|
@@ -69,8 +69,8 @@ export interface VercelConfig {
|
|
|
69
69
|
// ============================================================================
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* The octane.config.ts adapter:
|
|
73
|
-
*
|
|
72
|
+
* The octane.config.ts adapter: the active app integration runs its `adapt()`
|
|
73
|
+
* after producing dist/client + dist/server, emitting `.vercel/output/`
|
|
74
74
|
* (Build Output API v3).
|
|
75
75
|
*/
|
|
76
76
|
export function vercel(options?: VercelAdapterOptions): OctaneAdapter;
|
package/tests/adapt.test.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
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
|
-
});
|
package/tsconfig.typecheck.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
}
|