@ruvyxa/adapter-netlify 1.0.19 → 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.
@@ -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;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AA6ED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAmH3E;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, projectRelativeOutDir, 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,18 @@ import { clientBuildOutput, projectRelativeOutDir, validateBuildContext } from '
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';
13
17
  import os from 'node:os';
14
18
  import path from 'node:path';
15
19
 
16
- const manifestPath = path.join(import.meta.dirname, 'manifest.json');
17
- 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).
18
24
  const prerenderDir = path.join(import.meta.dirname, 'prerender');
19
25
  // The function bundle directory is read-only at runtime; only the platform
20
26
  // tmp directory accepts writes. ISR revalidations land there and are read
@@ -109,8 +115,20 @@ export function netlifyAdapter(options = {}) {
109
115
  const relativeOutDir = projectRelativeOutDir(ctx);
110
116
  // Hashed client bundles are served under /__ruvyxa/client/ (see the
111
117
  // chunk manifest src fields); the immutable header must match that URL.
112
- const immutableHeaderToml = '[[headers]]\n for = "/__ruvyxa/client/*"\n [headers.values]\n' +
113
- ' Cache-Control = "public, max-age=31536000, immutable"\n';
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('');
114
132
  // Netlify.toml for the deploy directory
115
133
  const netlifyToml = '[build]\n publish = "publish"\n functions = "functions"\n\n' + immutableHeaderToml;
116
134
  // Project-root netlify.toml pointing at the build output (opt-in)
@@ -121,12 +139,10 @@ export function netlifyAdapter(options = {}) {
121
139
  immutableHeaderToml;
122
140
  // Frameworks API deploy configuration (.netlify/v1/config.json)
123
141
  const frameworksApiConfig = JSON.stringify({
124
- headers: [
125
- {
126
- for: '/__ruvyxa/client/*',
127
- values: { 'Cache-Control': 'public, max-age=31536000, immutable' },
128
- },
129
- ],
142
+ headers: headerRules.map((rule) => ({
143
+ for: rule.for,
144
+ values: { 'Cache-Control': rule.cacheControl },
145
+ })),
130
146
  }, null, 2);
131
147
  return {
132
148
  name: 'netlify',
@@ -142,7 +158,15 @@ export function netlifyAdapter(options = {}) {
142
158
  // an API-only or all-SSR app has no prerendered pages; the function
143
159
  // still serves every route, so the missing prerender directory must
144
160
  // not fail the build.
145
- { kind: 'static-site', path: 'deploy/netlify/publish', optional: true },
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
+ },
146
170
  // Serverless function bundle
147
171
  {
148
172
  kind: 'function',
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
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.19",
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.19"
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