@ruvyxa/adapter-netlify 1.0.18 → 1.0.20

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/dist/index.d.ts CHANGED
@@ -7,12 +7,24 @@ export interface NetlifyAdapterOptions {
7
7
  functionsDir?: string;
8
8
  /**
9
9
  * Also emit a `netlify.toml` at the project root pointing Netlify at the
10
- * generated publish directory and functions, so a fresh site deploys
11
- * without dashboard configuration. An existing project `netlify.toml` is
12
- * never overwritten.
13
- * @default true
10
+ * generated publish directory and functions. An existing project
11
+ * `netlify.toml` is never overwritten.
12
+ *
13
+ * Off by default: the adapter already emits the serverless function and
14
+ * cache headers through Netlify's Frameworks API (`.netlify/v1/`, a
15
+ * gitignored build artifact), so the only remaining setup is the publish
16
+ * directory — set once in the Netlify dashboard, or opt into this file.
17
+ * @default false
14
18
  */
15
19
  projectConfig?: boolean;
20
+ /**
21
+ * Emit the Netlify Frameworks API directory (`.netlify/v1/`) at the project
22
+ * root during `ruvyxa build`: the SSR/API function and the immutable cache
23
+ * headers for hashed client bundles. Netlify picks the directory up
24
+ * automatically on the next deploy — no config file at the project root.
25
+ * @default true
26
+ */
27
+ frameworksApi?: boolean;
16
28
  }
17
29
  /**
18
30
  * Create a Netlify deployment adapter for Ruvyxa.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAgD,MAAM,cAAc,CAAA;AAGzF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAgED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAyE3E;eAEc,cAAc"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAgD,MAAM,cAAc,CAAA;AAWzF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAmFD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAwI3E;eAEc,cAAc"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { clientBuildOutput, validateBuildContext } from '@ruvyxa/core';
1
+ import { CLIENT_BUNDLE_PREFIX, clientBuildOutput, IMMUTABLE_CACHE_CONTROL, projectRelativeOutDir, PUBLIC_ASSET_CACHE_CONTROL, publicAssetGlobs, validateBuildContext, } from '@ruvyxa/core';
2
2
  /**
3
3
  * Netlify Functions v2 handler source code.
4
4
  *
@@ -9,12 +9,29 @@ import { clientBuildOutput, validateBuildContext } from '@ruvyxa/core';
9
9
  function netlifyHandlerSource() {
10
10
  return `import { createHandler, prerenderRelativePath } from './serverless-handler.mjs';
11
11
  import { loadRouteModule } from './route-modules.mjs';
12
+ // Netlify bundles the function with esbuild, so anything the deployed code
13
+ // needs must be reachable through the module graph. A sibling manifest.json
14
+ // read from import.meta.dirname is not, and never reaches /var/task.
15
+ import manifest from './manifest.mjs';
12
16
  import { readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
17
+ import os from 'node:os';
13
18
  import path from 'node:path';
14
19
 
15
- const manifestPath = path.join(import.meta.dirname, 'manifest.json');
16
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
20
+ // Deploy-time prerender output is likewise dropped by the bundler unless the
21
+ // site declares included_files. Reads are best-effort: a miss falls through to
22
+ // an on-demand render, and Netlify serves SSG pages from the publish directory
23
+ // before the function is ever invoked (config.preferStatic).
17
24
  const prerenderDir = path.join(import.meta.dirname, 'prerender');
25
+ // The function bundle directory is read-only at runtime; only the platform
26
+ // tmp directory accepts writes. ISR revalidations land there and are read
27
+ // back before the bundled deploy-time prerender output.
28
+ const isrCacheDir = path.join(os.tmpdir(), 'ruvyxa-isr-cache');
29
+
30
+ const readEntry = (htmlPath, revalidate) => {
31
+ const html = readFileSync(htmlPath, 'utf8');
32
+ const stale = Date.now() - statSync(htmlPath).mtimeMs >= revalidate * 1000;
33
+ return { html, stale };
34
+ };
18
35
 
19
36
  const handler = createHandler({
20
37
  routes: manifest.routes,
@@ -22,14 +39,16 @@ const handler = createHandler({
22
39
  importApi: loadRouteModule,
23
40
  readPrerendered: (pathname, revalidate = 60) => {
24
41
  // prerenderRelativePath rejects any request path that cannot be mapped to a
25
- // location inside prerenderDir, so the cache read can never escape it.
42
+ // location inside the cache directories, so reads can never escape them.
26
43
  const relative = prerenderRelativePath(pathname);
27
44
  if (relative === null) return null;
28
45
  try {
29
- const htmlPath = path.join(prerenderDir, relative);
30
- const html = readFileSync(htmlPath, 'utf8');
31
- const stale = Date.now() - statSync(htmlPath).mtimeMs >= revalidate * 1000;
32
- return { html, stale };
46
+ return readEntry(path.join(isrCacheDir, relative), revalidate);
47
+ } catch {
48
+ // fall through to the bundled prerender output
49
+ }
50
+ try {
51
+ return readEntry(path.join(prerenderDir, relative), revalidate);
33
52
  } catch {
34
53
  return null;
35
54
  }
@@ -37,7 +56,7 @@ const handler = createHandler({
37
56
  writePrerendered: (pathname, html, revalidate) => {
38
57
  const relative = prerenderRelativePath(pathname);
39
58
  if (relative === null) return;
40
- const htmlPath = path.join(prerenderDir, relative);
59
+ const htmlPath = path.join(isrCacheDir, relative);
41
60
  try {
42
61
  mkdirSync(path.dirname(htmlPath), { recursive: true });
43
62
  writeFileSync(htmlPath, html, 'utf8');
@@ -91,17 +110,40 @@ export function netlifyAdapter(options = {}) {
91
110
  build(ctx) {
92
111
  validateBuildContext(ctx, 'netlifyAdapter');
93
112
  const functionsDir = options.functionsDir ?? `${ctx.outDir}/netlify/functions`;
113
+ // Config files are committed or read on other machines; never embed the
114
+ // absolute build-machine outDir in them.
115
+ const relativeOutDir = projectRelativeOutDir(ctx);
116
+ // Hashed client bundles are served under /__ruvyxa/client/ (see the
117
+ // chunk manifest src fields); the immutable header must match that URL.
118
+ // `public/` assets are not hashed and otherwise inherit Netlify's
119
+ // revalidate-every-request default, so they get the same lifetime the
120
+ // Rust server sends for the same files.
121
+ const headerRules = [
122
+ { for: `${CLIENT_BUNDLE_PREFIX}*`, cacheControl: IMMUTABLE_CACHE_CONTROL },
123
+ ...publicAssetGlobs().map((glob) => ({
124
+ for: glob,
125
+ cacheControl: PUBLIC_ASSET_CACHE_CONTROL,
126
+ })),
127
+ ];
128
+ const immutableHeaderToml = headerRules
129
+ .map((rule) => `[[headers]]\n for = "${rule.for}"\n [headers.values]\n` +
130
+ ` Cache-Control = "${rule.cacheControl}"\n`)
131
+ .join('');
94
132
  // Netlify.toml for the deploy directory
95
- const netlifyToml = '[build]\n publish = "publish"\n functions = "functions"\n\n' +
96
- '[[headers]]\n for = "/client/*"\n [headers.values]\n' +
97
- ' Cache-Control = "public, max-age=31536000, immutable"\n';
98
- // Project-root netlify.toml pointing at the build output
133
+ const netlifyToml = '[build]\n publish = "publish"\n functions = "functions"\n\n' + immutableHeaderToml;
134
+ // Project-root netlify.toml pointing at the build output (opt-in)
99
135
  const projectNetlifyToml = '[build]\n' +
100
136
  ' command = "npx --no-install ruvyxa build"\n' +
101
- ` publish = "${ctx.outDir}/deploy/netlify/publish"\n` +
102
- ` functions = "${ctx.outDir}/deploy/netlify/functions"\n\n` +
103
- '[[headers]]\n for = "/client/*"\n [headers.values]\n' +
104
- ' Cache-Control = "public, max-age=31536000, immutable"\n';
137
+ ` publish = "${relativeOutDir}/deploy/netlify/publish"\n` +
138
+ ` functions = "${relativeOutDir}/deploy/netlify/functions"\n\n` +
139
+ immutableHeaderToml;
140
+ // Frameworks API deploy configuration (.netlify/v1/config.json)
141
+ const frameworksApiConfig = JSON.stringify({
142
+ headers: headerRules.map((rule) => ({
143
+ for: rule.for,
144
+ values: { 'Cache-Control': rule.cacheControl },
145
+ })),
146
+ }, null, 2);
105
147
  return {
106
148
  name: 'netlify',
107
149
  target: 'serverless',
@@ -112,8 +154,19 @@ export function netlifyAdapter(options = {}) {
112
154
  functionsDir,
113
155
  configFiles: ['netlify.toml'],
114
156
  artifacts: [
115
- // Static assets (pre-rendered pages + client bundles)
116
- { kind: 'static-site', path: 'deploy/netlify/publish' },
157
+ // Static assets (pre-rendered pages + client bundles). `optional`:
158
+ // an API-only or all-SSR app has no prerendered pages; the function
159
+ // still serves every route, so the missing prerender directory must
160
+ // not fail the build.
161
+ // `preferStatic: true` means a published page is served without ever
162
+ // reaching the function, so ISR/PPR pages must stay unpublished for
163
+ // revalidation to happen at all.
164
+ {
165
+ kind: 'static-site',
166
+ path: 'deploy/netlify/publish',
167
+ optional: true,
168
+ excludeStrategies: ['isr', 'ppr'],
169
+ },
117
170
  // Serverless function bundle
118
171
  {
119
172
  kind: 'function',
@@ -126,9 +179,27 @@ export function netlifyAdapter(options = {}) {
126
179
  path: 'deploy/netlify/netlify.toml',
127
180
  contents: netlifyToml,
128
181
  },
129
- ...(options.projectConfig === false
182
+ // Frameworks API artifacts: Netlify discovers .netlify/v1/ at the
183
+ // project root automatically, so a fresh site needs only the publish
184
+ // directory set once in the dashboard — no committed config file.
185
+ ...(options.frameworksApi === false
130
186
  ? []
131
187
  : [
188
+ {
189
+ kind: 'function',
190
+ path: '.netlify/v1/functions/ruvyxa-handler',
191
+ scope: 'project',
192
+ handlerSource: netlifyHandlerSource(),
193
+ },
194
+ {
195
+ kind: 'file',
196
+ path: '.netlify/v1/config.json',
197
+ scope: 'project',
198
+ contents: frameworksApiConfig + '\n',
199
+ },
200
+ ]),
201
+ ...(options.projectConfig === true
202
+ ? [
132
203
  {
133
204
  kind: 'file',
134
205
  path: 'netlify.toml',
@@ -136,7 +207,8 @@ export function netlifyAdapter(options = {}) {
136
207
  skipIfExists: true,
137
208
  contents: projectNetlifyToml,
138
209
  },
139
- ]),
210
+ ]
211
+ : []),
140
212
  ],
141
213
  };
142
214
  },
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAkBtE;;;;;;GAMG;AACH,SAAS,oBAAoB;IAC3B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDR,CAAA;AACD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,OAAO,GAA0B,EAAE;IAChE,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,IAAI,OAAO,OAAO,CAAC,YAAY,KAAK,QAAQ,EAAE,CAAC;QACnF,MAAM,IAAI,KAAK,CACb,kEAAkE,OAAO,OAAO,CAAC,YAAY,EAAE,CAChG,CAAA;IACH,CAAC;IAED,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC7E,MAAM,IAAI,KAAK,CAAC,sEAAsE,CAAC,CAAA;IACzF,CAAC;IAED,OAAO;QACL,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,YAAY;QACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC;QACpD,KAAK,CAAC,GAAiB;YACrB,oBAAoB,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAA;YAC3C,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,GAAG,GAAG,CAAC,MAAM,oBAAoB,CAAA;YAE9E,wCAAwC;YACxC,MAAM,WAAW,GACf,+DAA+D;gBAC/D,wDAAwD;gBACxD,6DAA6D,CAAA;YAE/D,yDAAyD;YACzD,MAAM,kBAAkB,GACtB,WAAW;gBACX,+CAA+C;gBAC/C,gBAAgB,GAAG,CAAC,MAAM,4BAA4B;gBACtD,kBAAkB,GAAG,CAAC,MAAM,gCAAgC;gBAC5D,wDAAwD;gBACxD,6DAA6D,CAAA;YAE/D,OAAO;gBACL,IAAI,EAAE,SAAS;gBACf,MAAM,EAAE,YAAY;gBACpB,QAAQ,EAAE,SAAS;gBACnB,KAAK,EAAE,GAAG,GAAG,CAAC,MAAM,aAAa;gBACjC,SAAS,EAAE,GAAG,GAAG,CAAC,MAAM,SAAS;gBACjC,GAAG,iBAAiB,CAAC,GAAG,CAAC;gBACzB,YAAY;gBACZ,WAAW,EAAE,CAAC,cAAc,CAAC;gBAC7B,SAAS,EAAE;oBACT,sDAAsD;oBACtD,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,wBAAwB,EAAE;oBACvD,6BAA6B;oBAC7B;wBACE,IAAI,EAAE,UAAU;wBAChB,IAAI,EAAE,yCAAyC;wBAC/C,aAAa,EAAE,oBAAoB,EAAE;qBACtC;oBACD,0CAA0C;oBAC1C;wBACE,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE,6BAA6B;wBACnC,QAAQ,EAAE,WAAW;qBACtB;oBACD,GAAG,CAAC,OAAO,CAAC,aAAa,KAAK,KAAK;wBACjC,CAAC,CAAC,EAAE;wBACJ,CAAC,CAAC;4BACE;gCACE,IAAI,EAAE,MAAM;gCACZ,IAAI,EAAE,cAAc;gCACpB,KAAK,EAAE,SAAS;gCAChB,YAAY,EAAE,IAAI;gCAClB,QAAQ,EAAE,kBAAkB;6BACH;yBAC5B,CAAC;iBACP;aACF,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED,eAAe,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruvyxa/adapter-netlify",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Shape Ruvyxa builds for Netlify functions and assets with a small typed adapter contract.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -8,6 +8,7 @@
8
8
  "types": "./dist/index.d.ts",
9
9
  "files": [
10
10
  "dist",
11
+ "src",
11
12
  "README.md"
12
13
  ],
13
14
  "keywords": [
@@ -26,7 +27,7 @@
26
27
  },
27
28
  "homepage": "https://github.com/thirawat27/ruvyxa#readme",
28
29
  "engines": {
29
- "node": ">=22.0.0"
30
+ "node": ">=22.12.0"
30
31
  },
31
32
  "publishConfig": {
32
33
  "access": "public"
@@ -38,7 +39,7 @@
38
39
  }
39
40
  },
40
41
  "dependencies": {
41
- "@ruvyxa/core": "^1.0.18"
42
+ "@ruvyxa/core": "^1.0.20"
42
43
  },
43
44
  "scripts": {
44
45
  "build": "tsc -p tsconfig.json",
package/src/index.ts ADDED
@@ -0,0 +1,276 @@
1
+ import type { Adapter, AdapterArtifact, AdapterOutput, BuildContext } from '@ruvyxa/core'
2
+ import {
3
+ CLIENT_BUNDLE_PREFIX,
4
+ clientBuildOutput,
5
+ IMMUTABLE_CACHE_CONTROL,
6
+ projectRelativeOutDir,
7
+ PUBLIC_ASSET_CACHE_CONTROL,
8
+ publicAssetGlobs,
9
+ validateBuildContext,
10
+ } from '@ruvyxa/core'
11
+
12
+ /**
13
+ * Options for Netlify deployment.
14
+ */
15
+ export interface NetlifyAdapterOptions {
16
+ /** Custom Netlify functions directory. Defaults to `${outDir}/netlify/functions`. */
17
+ functionsDir?: string
18
+ /**
19
+ * Also emit a `netlify.toml` at the project root pointing Netlify at the
20
+ * generated publish directory and functions. An existing project
21
+ * `netlify.toml` is never overwritten.
22
+ *
23
+ * Off by default: the adapter already emits the serverless function and
24
+ * cache headers through Netlify's Frameworks API (`.netlify/v1/`, a
25
+ * gitignored build artifact), so the only remaining setup is the publish
26
+ * directory — set once in the Netlify dashboard, or opt into this file.
27
+ * @default false
28
+ */
29
+ projectConfig?: boolean
30
+ /**
31
+ * Emit the Netlify Frameworks API directory (`.netlify/v1/`) at the project
32
+ * root during `ruvyxa build`: the SSR/API function and the immutable cache
33
+ * headers for hashed client bundles. Netlify picks the directory up
34
+ * automatically on the next deploy — no config file at the project root.
35
+ * @default true
36
+ */
37
+ frameworksApi?: boolean
38
+ }
39
+
40
+ /**
41
+ * Netlify Functions v2 handler source code.
42
+ *
43
+ * Wraps the generic Ruvyxa serverless handler into a Netlify Functions v2
44
+ * handler using the Web-standard Request/Response API. Reads the route
45
+ * manifest and handles SSR/API/ISR/PPR requests.
46
+ */
47
+ function netlifyHandlerSource(): string {
48
+ return `import { createHandler, prerenderRelativePath } from './serverless-handler.mjs';
49
+ import { loadRouteModule } from './route-modules.mjs';
50
+ // Netlify bundles the function with esbuild, so anything the deployed code
51
+ // needs must be reachable through the module graph. A sibling manifest.json
52
+ // read from import.meta.dirname is not, and never reaches /var/task.
53
+ import manifest from './manifest.mjs';
54
+ import { readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
55
+ import os from 'node:os';
56
+ import path from 'node:path';
57
+
58
+ // Deploy-time prerender output is likewise dropped by the bundler unless the
59
+ // site declares included_files. Reads are best-effort: a miss falls through to
60
+ // an on-demand render, and Netlify serves SSG pages from the publish directory
61
+ // before the function is ever invoked (config.preferStatic).
62
+ const prerenderDir = path.join(import.meta.dirname, 'prerender');
63
+ // The function bundle directory is read-only at runtime; only the platform
64
+ // tmp directory accepts writes. ISR revalidations land there and are read
65
+ // back before the bundled deploy-time prerender output.
66
+ const isrCacheDir = path.join(os.tmpdir(), 'ruvyxa-isr-cache');
67
+
68
+ const readEntry = (htmlPath, revalidate) => {
69
+ const html = readFileSync(htmlPath, 'utf8');
70
+ const stale = Date.now() - statSync(htmlPath).mtimeMs >= revalidate * 1000;
71
+ return { html, stale };
72
+ };
73
+
74
+ const handler = createHandler({
75
+ routes: manifest.routes,
76
+ importPage: loadRouteModule,
77
+ importApi: loadRouteModule,
78
+ readPrerendered: (pathname, revalidate = 60) => {
79
+ // prerenderRelativePath rejects any request path that cannot be mapped to a
80
+ // location inside the cache directories, so reads can never escape them.
81
+ const relative = prerenderRelativePath(pathname);
82
+ if (relative === null) return null;
83
+ try {
84
+ return readEntry(path.join(isrCacheDir, relative), revalidate);
85
+ } catch {
86
+ // fall through to the bundled prerender output
87
+ }
88
+ try {
89
+ return readEntry(path.join(prerenderDir, relative), revalidate);
90
+ } catch {
91
+ return null;
92
+ }
93
+ },
94
+ writePrerendered: (pathname, html, revalidate) => {
95
+ const relative = prerenderRelativePath(pathname);
96
+ if (relative === null) return;
97
+ const htmlPath = path.join(isrCacheDir, relative);
98
+ try {
99
+ mkdirSync(path.dirname(htmlPath), { recursive: true });
100
+ writeFileSync(htmlPath, html, 'utf8');
101
+ } catch {
102
+ // ISR cache write failures are non-fatal
103
+ }
104
+ },
105
+ supportedStrategies: ['ssr', 'ssg', 'csr', 'isr', 'ppr', 'api'],
106
+ });
107
+
108
+ // Netlify Functions v2 — Web-standard Request/Response
109
+ export default async function(request, context) {
110
+ return handler(request, context);
111
+ }
112
+
113
+ // Netlify Functions v2 config
114
+ export const config = {
115
+ path: '/*',
116
+ preferStatic: true,
117
+ };
118
+ `
119
+ }
120
+
121
+ /**
122
+ * Create a Netlify deployment adapter for Ruvyxa.
123
+ *
124
+ * Produces a Functions v2 handler (Web-standard Request/Response) and static
125
+ * assets for deployment via Netlify CLI. Supports SSR, API routes, ISR, PPR,
126
+ * SSG, and CSR.
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * import { config } from "ruvyxa/config"
131
+ * import { netlifyAdapter } from "@ruvyxa/adapter-netlify"
132
+ *
133
+ * export default config({
134
+ * adapter: netlifyAdapter()
135
+ * })
136
+ * ```
137
+ */
138
+ export function netlifyAdapter(options: NetlifyAdapterOptions = {}): Adapter {
139
+ if (options.functionsDir !== undefined && typeof options.functionsDir !== 'string') {
140
+ throw new Error(
141
+ `[RUV2001] netlifyAdapter: "functionsDir" must be a string, got ${typeof options.functionsDir}`,
142
+ )
143
+ }
144
+
145
+ if (options.functionsDir !== undefined && options.functionsDir.trim() === '') {
146
+ throw new Error(`[RUV2001] netlifyAdapter: "functionsDir" must not be an empty string`)
147
+ }
148
+
149
+ return {
150
+ name: 'netlify',
151
+ target: 'serverless',
152
+ supports: ['ssr', 'ssg', 'csr', 'isr', 'ppr', 'api'],
153
+ build(ctx: BuildContext): AdapterOutput {
154
+ validateBuildContext(ctx, 'netlifyAdapter')
155
+ const functionsDir = options.functionsDir ?? `${ctx.outDir}/netlify/functions`
156
+ // Config files are committed or read on other machines; never embed the
157
+ // absolute build-machine outDir in them.
158
+ const relativeOutDir = projectRelativeOutDir(ctx)
159
+
160
+ // Hashed client bundles are served under /__ruvyxa/client/ (see the
161
+ // chunk manifest src fields); the immutable header must match that URL.
162
+ // `public/` assets are not hashed and otherwise inherit Netlify's
163
+ // revalidate-every-request default, so they get the same lifetime the
164
+ // Rust server sends for the same files.
165
+ const headerRules: Array<{ for: string; cacheControl: string }> = [
166
+ { for: `${CLIENT_BUNDLE_PREFIX}*`, cacheControl: IMMUTABLE_CACHE_CONTROL },
167
+ ...publicAssetGlobs().map((glob) => ({
168
+ for: glob,
169
+ cacheControl: PUBLIC_ASSET_CACHE_CONTROL,
170
+ })),
171
+ ]
172
+
173
+ const immutableHeaderToml = headerRules
174
+ .map(
175
+ (rule) =>
176
+ `[[headers]]\n for = "${rule.for}"\n [headers.values]\n` +
177
+ ` Cache-Control = "${rule.cacheControl}"\n`,
178
+ )
179
+ .join('')
180
+
181
+ // Netlify.toml for the deploy directory
182
+ const netlifyToml =
183
+ '[build]\n publish = "publish"\n functions = "functions"\n\n' + immutableHeaderToml
184
+
185
+ // Project-root netlify.toml pointing at the build output (opt-in)
186
+ const projectNetlifyToml =
187
+ '[build]\n' +
188
+ ' command = "npx --no-install ruvyxa build"\n' +
189
+ ` publish = "${relativeOutDir}/deploy/netlify/publish"\n` +
190
+ ` functions = "${relativeOutDir}/deploy/netlify/functions"\n\n` +
191
+ immutableHeaderToml
192
+
193
+ // Frameworks API deploy configuration (.netlify/v1/config.json)
194
+ const frameworksApiConfig = JSON.stringify(
195
+ {
196
+ headers: headerRules.map((rule) => ({
197
+ for: rule.for,
198
+ values: { 'Cache-Control': rule.cacheControl },
199
+ })),
200
+ },
201
+ null,
202
+ 2,
203
+ )
204
+
205
+ return {
206
+ name: 'netlify',
207
+ target: 'serverless',
208
+ platform: 'netlify',
209
+ entry: `${ctx.outDir}/server/app`,
210
+ assetsDir: `${ctx.outDir}/assets`,
211
+ ...clientBuildOutput(ctx),
212
+ functionsDir,
213
+ configFiles: ['netlify.toml'],
214
+ artifacts: [
215
+ // Static assets (pre-rendered pages + client bundles). `optional`:
216
+ // an API-only or all-SSR app has no prerendered pages; the function
217
+ // still serves every route, so the missing prerender directory must
218
+ // not fail the build.
219
+ // `preferStatic: true` means a published page is served without ever
220
+ // reaching the function, so ISR/PPR pages must stay unpublished for
221
+ // revalidation to happen at all.
222
+ {
223
+ kind: 'static-site',
224
+ path: 'deploy/netlify/publish',
225
+ optional: true,
226
+ excludeStrategies: ['isr', 'ppr'],
227
+ },
228
+ // Serverless function bundle
229
+ {
230
+ kind: 'function',
231
+ path: 'deploy/netlify/functions/ruvyxa-handler',
232
+ handlerSource: netlifyHandlerSource(),
233
+ },
234
+ // Netlify config for the deploy directory
235
+ {
236
+ kind: 'file',
237
+ path: 'deploy/netlify/netlify.toml',
238
+ contents: netlifyToml,
239
+ },
240
+ // Frameworks API artifacts: Netlify discovers .netlify/v1/ at the
241
+ // project root automatically, so a fresh site needs only the publish
242
+ // directory set once in the dashboard — no committed config file.
243
+ ...(options.frameworksApi === false
244
+ ? []
245
+ : [
246
+ {
247
+ kind: 'function',
248
+ path: '.netlify/v1/functions/ruvyxa-handler',
249
+ scope: 'project',
250
+ handlerSource: netlifyHandlerSource(),
251
+ } satisfies AdapterArtifact,
252
+ {
253
+ kind: 'file',
254
+ path: '.netlify/v1/config.json',
255
+ scope: 'project',
256
+ contents: frameworksApiConfig + '\n',
257
+ } satisfies AdapterArtifact,
258
+ ]),
259
+ ...(options.projectConfig === true
260
+ ? [
261
+ {
262
+ kind: 'file',
263
+ path: 'netlify.toml',
264
+ scope: 'project',
265
+ skipIfExists: true,
266
+ contents: projectNetlifyToml,
267
+ } satisfies AdapterArtifact,
268
+ ]
269
+ : []),
270
+ ],
271
+ }
272
+ },
273
+ }
274
+ }
275
+
276
+ export default netlifyAdapter