@octanejs/adapter-vercel 0.0.2 → 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 CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@octanejs/adapter-vercel",
3
- "version": "0.0.2",
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/vite-plugin": "0.1.4"
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 @octanejs/vite-plugin) into `.vercel/output/`:
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/vite-plugin' */
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
- const valid = [20, 22, 24];
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 ${valid.join('/')} or set serverless.runtime explicitly.`,
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
- routes.push({
118
- src: '/assets/.+',
119
- headers: { 'Cache-Control': 'public, max-age=31536000, immutable' },
120
- continue: true,
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 @octanejs/vite-plugin's closeBundle (which passes its
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
- experimentalResponseStreaming: true,
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: cache the function's response at the edge, revalidate after
198
- // `expiration` seconds (false = never expire).
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
- vc_config.prerender = prerender;
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 plugin's adapter contract: after `vite build`
14
- * produces both bundles, @octanejs/vite-plugin calls `adapt(ctx)` and this
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/vite-plugin' */
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/vite-plugin';
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 (20/22/24)
11
+ * @default auto-detected from the build machine's Node major (22/24)
12
12
  */
13
- runtime?: string;
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: @octanejs/vite-plugin runs its `adapt()` after
73
- * `vite build` produces dist/client + dist/server, emitting `.vercel/output/`
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/CHANGELOG.md DELETED
@@ -1,7 +0,0 @@
1
- # @octanejs/adapter-vercel
2
-
3
- ## 0.0.2
4
-
5
- ### Patch Changes
6
-
7
- - 6d332ad: New package: Vercel adapter (Build Output API v3). `adapter: vercel()` in octane.config.ts makes `vite build` emit `.vercel/output` — the hashed client assets as static files plus one self-contained Node serverless function wrapping the SSR handler (the plugin's server bundle is self-contained, so no dependency tracing is needed). Options cover the serverless function (runtime/regions/memory/maxDuration), ISR, cleanUrls/trailingSlash, extra headers, and redirects; routing is filesystem-first with everything else — including the 404 catch-all — server-rendered by the function.
@@ -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
- });
@@ -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
- }