@ruvyxa/adapter-netlify 1.1.2 → 1.1.4
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 +24 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +120 -40
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +159 -52
package/dist/index.d.ts
CHANGED
|
@@ -6,23 +6,35 @@ export interface NetlifyAdapterOptions {
|
|
|
6
6
|
/** Custom Netlify functions directory. Defaults to `${outDir}/netlify/functions`. */
|
|
7
7
|
functionsDir?: string;
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
* generated publish directory and
|
|
11
|
-
* `netlify.toml` is never overwritten.
|
|
9
|
+
* Emit a `netlify.toml` at the project root naming the build command, the
|
|
10
|
+
* generated publish directory, the functions directory, and the headers. An
|
|
11
|
+
* existing project `netlify.toml` is never overwritten.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* On by default, because it is the only mechanism that can answer the one
|
|
14
|
+
* question Netlify cannot learn from the build: **the publish directory**.
|
|
15
|
+
* The Frameworks API has no key for it, and a plugin cannot supply it without
|
|
16
|
+
* a `netlify.toml` of its own — Netlify installs a plugin from its UI only
|
|
17
|
+
* when the plugin is listed in Netlify's own directory, and a community
|
|
18
|
+
* package has to be declared in `[[plugins]]` and installed as a
|
|
19
|
+
* devDependency. So the choice is not "config file or no config file"; it is
|
|
20
|
+
* "one generated file, or the same file written by hand".
|
|
21
|
+
*
|
|
22
|
+
* Netlify reads `netlify.toml` *before* running the build command, so this
|
|
23
|
+
* file has to be committed to take effect — the build writes it once and
|
|
24
|
+
* never again.
|
|
25
|
+
* @default true
|
|
18
26
|
*/
|
|
19
27
|
projectConfig?: boolean;
|
|
20
28
|
/**
|
|
21
29
|
* Emit the Netlify Frameworks API directory (`.netlify/v1/`) at the project
|
|
22
|
-
* root during `ruvyxa build`: the SSR/API function and the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
30
|
+
* root during `ruvyxa build`: the SSR/API function and the cache headers.
|
|
31
|
+
*
|
|
32
|
+
* Defaults to the opposite of {@link projectConfig}, because the two are
|
|
33
|
+
* alternatives rather than layers. Shipping both puts two functions on the
|
|
34
|
+
* site — the one under `.netlify/v1/functions` and the one the `netlify.toml`
|
|
35
|
+
* `functions` directory declares — and both claim `path: '/*'`, so which of
|
|
36
|
+
* them answers a request is not a question with an answer.
|
|
37
|
+
* @default `!projectConfig`
|
|
26
38
|
*/
|
|
27
39
|
frameworksApi?: boolean;
|
|
28
40
|
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAgD,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAgD,MAAM,cAAc,CAAA;AAgBzF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;;;;;;;;;;;;;;;OAkBG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AA8ND;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,OAAO,CAAC,OAAO,GAAE,qBAA0B,GAAG,OAAO,CA8JpE;eAEc,OAAO"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { CLIENT_BUNDLE_PREFIX, clientBuildOutput, DEFAULT_SECURITY_HEADERS, IMMUTABLE_CACHE_CONTROL, nonPublishableStrategies, projectRelativeOutDir, PUBLIC_ASSET_CACHE_CONTROL, publicAssetGlobs, runtimeBuildPolicy, validateBuildContext, } from '@ruvyxa/core';
|
|
2
|
+
import { CLIENT_BUNDLE_PREFIX, clientBuildOutput, documentCacheOptionsSource, isrTemporaryCacheSource, DEFAULT_SECURITY_HEADERS, IMMUTABLE_CACHE_CONTROL, nonPublishableStrategies, projectRelativeOutDir, PUBLIC_ASSET_CACHE_CONTROL, publicAssetGlobs, runtimeBuildPolicy, validateBuildContext, } from '@ruvyxa/core';
|
|
3
3
|
/**
|
|
4
4
|
* Netlify Functions v2 handler source code.
|
|
5
5
|
*
|
|
@@ -7,13 +7,14 @@ import { CLIENT_BUNDLE_PREFIX, clientBuildOutput, DEFAULT_SECURITY_HEADERS, IMMU
|
|
|
7
7
|
* handler using the Web-standard Request/Response API. Reads the route
|
|
8
8
|
* manifest and handles SSR/API/ISR/PPR requests.
|
|
9
9
|
*/
|
|
10
|
-
function netlifyHandlerSource(runtimePolicy, functionConfig) {
|
|
10
|
+
function netlifyHandlerSource(runtimePolicy, functionConfig, buildId) {
|
|
11
11
|
return `import { createHandler, prerenderRelativePath } from './serverless-handler.mjs';
|
|
12
|
-
import { applyPluginHttp, loadActionModule, loadRouteModule } from './route-modules.mjs';
|
|
12
|
+
import { applyPluginHttp, documentCacheHandler, loadActionModule, loadRouteModule } from './route-modules.mjs';
|
|
13
13
|
// Netlify bundles the function with esbuild, so anything the deployed code
|
|
14
14
|
// needs must be reachable through the module graph. A sibling manifest.json
|
|
15
15
|
// read from import.meta.dirname is not, and never reaches /var/task.
|
|
16
16
|
import manifest from './manifest.mjs';
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
17
18
|
import { readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
|
|
18
19
|
import os from 'node:os';
|
|
19
20
|
import path from 'node:path';
|
|
@@ -26,10 +27,42 @@ const runtimePolicy = ${JSON.stringify(runtimePolicy ?? {})};
|
|
|
26
27
|
// serves SSG pages from the publish directory before the function is ever
|
|
27
28
|
// invoked (config.preferStatic).
|
|
28
29
|
const prerenderDir = path.join(import.meta.dirname, 'prerender');
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
${isrTemporaryCacheSource(buildId)}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The cache tag a document is stored under, so one path can be dropped from
|
|
34
|
+
* Netlify's cache without dropping the site's.
|
|
35
|
+
*
|
|
36
|
+
* A tag list is comma-separated, so the path has to become one token — and the
|
|
37
|
+
* mapping to that token has to be injective, which folding every run of
|
|
38
|
+
* non-alphanumerics to a single \`-\` is not: \`/a/b\`, \`/a-b\` and \`/a_b\` all
|
|
39
|
+
* produced \`ruvyxa-a-b\`, so \`revalidatePath('/a/b')\` dropped two other pages
|
|
40
|
+
* from the edge as well. A digest is one token, is the same length whatever the
|
|
41
|
+
* path, and cannot collide by construction.
|
|
42
|
+
*/
|
|
43
|
+
function cacheTag(pathname) {
|
|
44
|
+
return 'ruvyxa-' + createHash('sha256').update(pathname).digest('hex').slice(0, 32);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Drop this path from Netlify's edge, which the durable cache in front of this
|
|
49
|
+
* function would otherwise keep serving until its window expired.
|
|
50
|
+
*
|
|
51
|
+
* \`purgeCache\` infers the site and its credentials from the function runtime,
|
|
52
|
+
* so it needs no configuration — but it lives in \`@netlify/functions\`, which a
|
|
53
|
+
* project may not have installed. Imported at the moment it is needed and
|
|
54
|
+
* behind a catch, because a missing optional dependency must not take the whole
|
|
55
|
+
* function down at module load: every page would 500 to make one revalidation
|
|
56
|
+
* work.
|
|
57
|
+
*/
|
|
58
|
+
async function purgeDurableCache(pathname) {
|
|
59
|
+
try {
|
|
60
|
+
const { purgeCache } = await import('@netlify/functions');
|
|
61
|
+
await purgeCache({ tags: [cacheTag(pathname)] });
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('[ruvyxa] could not purge ' + pathname + ' from the Netlify cache:', error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
33
66
|
|
|
34
67
|
const readEntry = (htmlPath, revalidate) => {
|
|
35
68
|
const html = readFileSync(htmlPath, 'utf8');
|
|
@@ -37,6 +70,35 @@ const readEntry = (htmlPath, revalidate) => {
|
|
|
37
70
|
return { html, stale };
|
|
38
71
|
};
|
|
39
72
|
|
|
73
|
+
// Named rather than passed inline, so a project that declares \`cache.handler\`
|
|
74
|
+
// can stand its own store in front of these. See \`documentCacheOptionsSource\`
|
|
75
|
+
// in \`@ruvyxa/core\`; the registry supplies \`documentCacheHandler\`.
|
|
76
|
+
const platformReadPrerendered = (pathname, revalidate = 60) => {
|
|
77
|
+
// prerenderRelativePath rejects any request path that cannot be mapped to a
|
|
78
|
+
// location inside the cache directories, so reads can never escape them.
|
|
79
|
+
const relative = prerenderRelativePath(pathname);
|
|
80
|
+
if (relative === null) return null;
|
|
81
|
+
try {
|
|
82
|
+
return readEntry(path.join(isrCacheDir, relative), revalidate);
|
|
83
|
+
} catch {
|
|
84
|
+
// fall through to the bundled prerender output
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return readEntry(path.join(prerenderDir, relative), revalidate);
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const platformWritePrerendered = (pathname, html, revalidate, forced) => {
|
|
94
|
+
const relative = prerenderRelativePath(pathname);
|
|
95
|
+
if (relative === null) return;
|
|
96
|
+
const htmlPath = path.join(isrCacheDir, relative);
|
|
97
|
+
mkdirSync(path.dirname(htmlPath), { recursive: true });
|
|
98
|
+
writeFileSync(htmlPath, html, 'utf8');
|
|
99
|
+
if (forced === true) return purgeDurableCache(pathname);
|
|
100
|
+
};
|
|
101
|
+
|
|
40
102
|
const handler = createHandler({
|
|
41
103
|
routes: manifest.routes,
|
|
42
104
|
middleware: runtimePolicy.middleware,
|
|
@@ -46,39 +108,53 @@ const handler = createHandler({
|
|
|
46
108
|
importAction: loadActionModule,
|
|
47
109
|
pluginHttp: applyPluginHttp,
|
|
48
110
|
security: runtimePolicy.security,
|
|
49
|
-
|
|
50
|
-
// prerenderRelativePath rejects any request path that cannot be mapped to a
|
|
51
|
-
// location inside the cache directories, so reads can never escape them.
|
|
52
|
-
const relative = prerenderRelativePath(pathname);
|
|
53
|
-
if (relative === null) return null;
|
|
54
|
-
try {
|
|
55
|
-
return readEntry(path.join(isrCacheDir, relative), revalidate);
|
|
56
|
-
} catch {
|
|
57
|
-
// fall through to the bundled prerender output
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
return readEntry(path.join(prerenderDir, relative), revalidate);
|
|
61
|
-
} catch {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
writePrerendered: (pathname, html, revalidate) => {
|
|
66
|
-
const relative = prerenderRelativePath(pathname);
|
|
67
|
-
if (relative === null) return;
|
|
68
|
-
const htmlPath = path.join(isrCacheDir, relative);
|
|
69
|
-
mkdirSync(path.dirname(htmlPath), { recursive: true });
|
|
70
|
-
writeFileSync(htmlPath, html, 'utf8');
|
|
71
|
-
},
|
|
111
|
+
${documentCacheOptionsSource('platformReadPrerendered', 'platformWritePrerendered')}
|
|
72
112
|
// The project's own not-found page, pre-rendered by the build and carried
|
|
73
113
|
// inline in the manifest: an unmatched URL is answered with the page the
|
|
74
114
|
// application actually wrote, on every host.
|
|
75
115
|
notFoundDocument: manifest.notFoundDocument,
|
|
76
116
|
supportedStrategies: ['ssr', 'ssg', 'csr', 'isr', 'ppr', 'api'],
|
|
117
|
+
// Netlify's own edge writes \`X-Nf-Client-Connection-IP\` and overwrites any
|
|
118
|
+
// inbound copy of it, so on this platform the header *is* the client.
|
|
119
|
+
// Declared here rather than assumed by the handler: the same handler runs on
|
|
120
|
+
// a self-hosted server where nothing writes it. Without a declaration
|
|
121
|
+
// identity fell through to a right-to-left scan of \`X-Forwarded-For\`, whose
|
|
122
|
+
// rightmost entry here is a fixed platform address — so the built-in \`rate\`
|
|
123
|
+
// middleware, the server-action rate limiter, and the replay quota all
|
|
124
|
+
// counted every visitor into one bucket.
|
|
125
|
+
clientIpHeaders: ['x-nf-client-connection-ip'],
|
|
77
126
|
});
|
|
78
127
|
|
|
79
128
|
// Netlify Functions v2 — Web-standard Request/Response
|
|
80
129
|
export default async function(request, context) {
|
|
81
|
-
|
|
130
|
+
const response = await handler(request, context);
|
|
131
|
+
return withDurableCache(request, response);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Re-state a cacheable answer for Netlify's own cache, durably.
|
|
136
|
+
*
|
|
137
|
+
* \`Cache-Control\` reaches the browser and every CDN; \`Netlify-CDN-Cache-Control\`
|
|
138
|
+
* is read by Netlify alone and is the only place the \`durable\` directive means
|
|
139
|
+
* anything. Durable storage is what makes one render serve every edge node
|
|
140
|
+
* instead of one — and it is what \`purgeCache\` above can invalidate, so the two
|
|
141
|
+
* halves have to be added together or \`revalidatePath()\` purges nothing.
|
|
142
|
+
*
|
|
143
|
+
* Only a shared-cacheable response is re-stated: \`s-maxage\` is the marker the
|
|
144
|
+
* handler already puts on an ISR document, and \`no-store\` on everything a
|
|
145
|
+
* request produced for one visitor.
|
|
146
|
+
*/
|
|
147
|
+
function withDurableCache(request, response) {
|
|
148
|
+
const cacheControl = response.headers.get('cache-control');
|
|
149
|
+
if (!cacheControl || !cacheControl.includes('s-maxage')) return response;
|
|
150
|
+
const headers = new Headers(response.headers);
|
|
151
|
+
headers.set('netlify-cdn-cache-control', 'public, durable, ' + cacheControl);
|
|
152
|
+
headers.set('cache-tag', cacheTag(new URL(request.url).pathname));
|
|
153
|
+
return new Response(response.body, {
|
|
154
|
+
status: response.status,
|
|
155
|
+
statusText: response.statusText,
|
|
156
|
+
headers,
|
|
157
|
+
});
|
|
82
158
|
}
|
|
83
159
|
|
|
84
160
|
// Netlify Functions v2 config
|
|
@@ -93,13 +169,13 @@ function packageVersion() {
|
|
|
93
169
|
}
|
|
94
170
|
return metadata.version;
|
|
95
171
|
}
|
|
96
|
-
function functionConfigFor(
|
|
172
|
+
function functionConfigFor() {
|
|
97
173
|
return {
|
|
98
174
|
path: '/*',
|
|
99
175
|
preferStatic: true,
|
|
100
176
|
name: 'Ruvyxa SSR',
|
|
101
177
|
generator: `ruvyxa/${packageVersion()}`,
|
|
102
|
-
includedFiles: [
|
|
178
|
+
includedFiles: ['prerender/**'],
|
|
103
179
|
};
|
|
104
180
|
}
|
|
105
181
|
/**
|
|
@@ -132,6 +208,10 @@ export function netlify(options = {}) {
|
|
|
132
208
|
supports: ['ssr', 'ssg', 'csr', 'isr', 'ppr', 'api'],
|
|
133
209
|
build(ctx) {
|
|
134
210
|
validateBuildContext(ctx, 'netlifyAdapter');
|
|
211
|
+
// Exactly one of the two ships: see `frameworksApi` above for why both
|
|
212
|
+
// at once is a site with two functions claiming the same path.
|
|
213
|
+
const projectConfig = options.projectConfig !== false;
|
|
214
|
+
const frameworksApi = options.frameworksApi ?? !projectConfig;
|
|
135
215
|
const functionsDir = options.functionsDir ?? `${ctx.outDir}/netlify/functions`;
|
|
136
216
|
// Config files are committed or read on other machines; never embed the
|
|
137
217
|
// absolute build-machine outDir in them.
|
|
@@ -204,7 +284,7 @@ export function netlify(options = {}) {
|
|
|
204
284
|
{
|
|
205
285
|
kind: 'function',
|
|
206
286
|
path: 'deploy/netlify/functions/ruvyxa-handler',
|
|
207
|
-
handlerSource: netlifyHandlerSource(runtimePolicy, functionConfigFor('
|
|
287
|
+
handlerSource: netlifyHandlerSource(runtimePolicy, functionConfigFor(), ctx.deployManifest?.buildId ?? ''),
|
|
208
288
|
},
|
|
209
289
|
// Netlify config for the deploy directory
|
|
210
290
|
{
|
|
@@ -215,14 +295,13 @@ export function netlify(options = {}) {
|
|
|
215
295
|
// Frameworks API artifacts: Netlify discovers .netlify/v1/ at the
|
|
216
296
|
// project root automatically, so a fresh site needs only the publish
|
|
217
297
|
// directory set once in the dashboard — no committed config file.
|
|
218
|
-
...(
|
|
219
|
-
? [
|
|
220
|
-
: [
|
|
298
|
+
...(frameworksApi
|
|
299
|
+
? [
|
|
221
300
|
{
|
|
222
301
|
kind: 'function',
|
|
223
302
|
path: '.netlify/v1/functions/ruvyxa-handler',
|
|
224
303
|
scope: 'project',
|
|
225
|
-
handlerSource: netlifyHandlerSource(runtimePolicy, functionConfigFor(
|
|
304
|
+
handlerSource: netlifyHandlerSource(runtimePolicy, functionConfigFor(), ctx.deployManifest?.buildId ?? ''),
|
|
226
305
|
},
|
|
227
306
|
{
|
|
228
307
|
kind: 'file',
|
|
@@ -230,8 +309,9 @@ export function netlify(options = {}) {
|
|
|
230
309
|
scope: 'project',
|
|
231
310
|
contents: frameworksApiConfig + '\n',
|
|
232
311
|
},
|
|
233
|
-
]
|
|
234
|
-
|
|
312
|
+
]
|
|
313
|
+
: []),
|
|
314
|
+
...(projectConfig
|
|
235
315
|
? [
|
|
236
316
|
{
|
|
237
317
|
kind: 'file',
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAGtC,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,qBAAqB,EACrB,0BAA0B,EAC1B,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,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.1.
|
|
3
|
+
"version": "1.1.4",
|
|
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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@ruvyxa/core": "^1.1.
|
|
59
|
+
"@ruvyxa/core": "^1.1.4"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "24.13.3"
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { Adapter, AdapterArtifact, AdapterOutput, BuildContext } from '@ruv
|
|
|
4
4
|
import {
|
|
5
5
|
CLIENT_BUNDLE_PREFIX,
|
|
6
6
|
clientBuildOutput,
|
|
7
|
+
documentCacheOptionsSource,
|
|
8
|
+
isrTemporaryCacheSource,
|
|
7
9
|
DEFAULT_SECURITY_HEADERS,
|
|
8
10
|
IMMUTABLE_CACHE_CONTROL,
|
|
9
11
|
nonPublishableStrategies,
|
|
@@ -21,23 +23,35 @@ export interface NetlifyAdapterOptions {
|
|
|
21
23
|
/** Custom Netlify functions directory. Defaults to `${outDir}/netlify/functions`. */
|
|
22
24
|
functionsDir?: string
|
|
23
25
|
/**
|
|
24
|
-
*
|
|
25
|
-
* generated publish directory and
|
|
26
|
-
* `netlify.toml` is never overwritten.
|
|
26
|
+
* Emit a `netlify.toml` at the project root naming the build command, the
|
|
27
|
+
* generated publish directory, the functions directory, and the headers. An
|
|
28
|
+
* existing project `netlify.toml` is never overwritten.
|
|
27
29
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* On by default, because it is the only mechanism that can answer the one
|
|
31
|
+
* question Netlify cannot learn from the build: **the publish directory**.
|
|
32
|
+
* The Frameworks API has no key for it, and a plugin cannot supply it without
|
|
33
|
+
* a `netlify.toml` of its own — Netlify installs a plugin from its UI only
|
|
34
|
+
* when the plugin is listed in Netlify's own directory, and a community
|
|
35
|
+
* package has to be declared in `[[plugins]]` and installed as a
|
|
36
|
+
* devDependency. So the choice is not "config file or no config file"; it is
|
|
37
|
+
* "one generated file, or the same file written by hand".
|
|
38
|
+
*
|
|
39
|
+
* Netlify reads `netlify.toml` *before* running the build command, so this
|
|
40
|
+
* file has to be committed to take effect — the build writes it once and
|
|
41
|
+
* never again.
|
|
42
|
+
* @default true
|
|
33
43
|
*/
|
|
34
44
|
projectConfig?: boolean
|
|
35
45
|
/**
|
|
36
46
|
* Emit the Netlify Frameworks API directory (`.netlify/v1/`) at the project
|
|
37
|
-
* root during `ruvyxa build`: the SSR/API function and the
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
47
|
+
* root during `ruvyxa build`: the SSR/API function and the cache headers.
|
|
48
|
+
*
|
|
49
|
+
* Defaults to the opposite of {@link projectConfig}, because the two are
|
|
50
|
+
* alternatives rather than layers. Shipping both puts two functions on the
|
|
51
|
+
* site — the one under `.netlify/v1/functions` and the one the `netlify.toml`
|
|
52
|
+
* `functions` directory declares — and both claim `path: '/*'`, so which of
|
|
53
|
+
* them answers a request is not a question with an answer.
|
|
54
|
+
* @default `!projectConfig`
|
|
41
55
|
*/
|
|
42
56
|
frameworksApi?: boolean
|
|
43
57
|
}
|
|
@@ -52,13 +66,15 @@ export interface NetlifyAdapterOptions {
|
|
|
52
66
|
function netlifyHandlerSource(
|
|
53
67
|
runtimePolicy: unknown,
|
|
54
68
|
functionConfig: NetlifyFunctionConfig,
|
|
69
|
+
buildId: string,
|
|
55
70
|
): string {
|
|
56
71
|
return `import { createHandler, prerenderRelativePath } from './serverless-handler.mjs';
|
|
57
|
-
import { applyPluginHttp, loadActionModule, loadRouteModule } from './route-modules.mjs';
|
|
72
|
+
import { applyPluginHttp, documentCacheHandler, loadActionModule, loadRouteModule } from './route-modules.mjs';
|
|
58
73
|
// Netlify bundles the function with esbuild, so anything the deployed code
|
|
59
74
|
// needs must be reachable through the module graph. A sibling manifest.json
|
|
60
75
|
// read from import.meta.dirname is not, and never reaches /var/task.
|
|
61
76
|
import manifest from './manifest.mjs';
|
|
77
|
+
import { createHash } from 'node:crypto';
|
|
62
78
|
import { readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
|
|
63
79
|
import os from 'node:os';
|
|
64
80
|
import path from 'node:path';
|
|
@@ -71,10 +87,42 @@ const runtimePolicy = ${JSON.stringify(runtimePolicy ?? {})};
|
|
|
71
87
|
// serves SSG pages from the publish directory before the function is ever
|
|
72
88
|
// invoked (config.preferStatic).
|
|
73
89
|
const prerenderDir = path.join(import.meta.dirname, 'prerender');
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
${isrTemporaryCacheSource(buildId)}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The cache tag a document is stored under, so one path can be dropped from
|
|
94
|
+
* Netlify's cache without dropping the site's.
|
|
95
|
+
*
|
|
96
|
+
* A tag list is comma-separated, so the path has to become one token — and the
|
|
97
|
+
* mapping to that token has to be injective, which folding every run of
|
|
98
|
+
* non-alphanumerics to a single \`-\` is not: \`/a/b\`, \`/a-b\` and \`/a_b\` all
|
|
99
|
+
* produced \`ruvyxa-a-b\`, so \`revalidatePath('/a/b')\` dropped two other pages
|
|
100
|
+
* from the edge as well. A digest is one token, is the same length whatever the
|
|
101
|
+
* path, and cannot collide by construction.
|
|
102
|
+
*/
|
|
103
|
+
function cacheTag(pathname) {
|
|
104
|
+
return 'ruvyxa-' + createHash('sha256').update(pathname).digest('hex').slice(0, 32);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Drop this path from Netlify's edge, which the durable cache in front of this
|
|
109
|
+
* function would otherwise keep serving until its window expired.
|
|
110
|
+
*
|
|
111
|
+
* \`purgeCache\` infers the site and its credentials from the function runtime,
|
|
112
|
+
* so it needs no configuration — but it lives in \`@netlify/functions\`, which a
|
|
113
|
+
* project may not have installed. Imported at the moment it is needed and
|
|
114
|
+
* behind a catch, because a missing optional dependency must not take the whole
|
|
115
|
+
* function down at module load: every page would 500 to make one revalidation
|
|
116
|
+
* work.
|
|
117
|
+
*/
|
|
118
|
+
async function purgeDurableCache(pathname) {
|
|
119
|
+
try {
|
|
120
|
+
const { purgeCache } = await import('@netlify/functions');
|
|
121
|
+
await purgeCache({ tags: [cacheTag(pathname)] });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('[ruvyxa] could not purge ' + pathname + ' from the Netlify cache:', error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
78
126
|
|
|
79
127
|
const readEntry = (htmlPath, revalidate) => {
|
|
80
128
|
const html = readFileSync(htmlPath, 'utf8');
|
|
@@ -82,6 +130,35 @@ const readEntry = (htmlPath, revalidate) => {
|
|
|
82
130
|
return { html, stale };
|
|
83
131
|
};
|
|
84
132
|
|
|
133
|
+
// Named rather than passed inline, so a project that declares \`cache.handler\`
|
|
134
|
+
// can stand its own store in front of these. See \`documentCacheOptionsSource\`
|
|
135
|
+
// in \`@ruvyxa/core\`; the registry supplies \`documentCacheHandler\`.
|
|
136
|
+
const platformReadPrerendered = (pathname, revalidate = 60) => {
|
|
137
|
+
// prerenderRelativePath rejects any request path that cannot be mapped to a
|
|
138
|
+
// location inside the cache directories, so reads can never escape them.
|
|
139
|
+
const relative = prerenderRelativePath(pathname);
|
|
140
|
+
if (relative === null) return null;
|
|
141
|
+
try {
|
|
142
|
+
return readEntry(path.join(isrCacheDir, relative), revalidate);
|
|
143
|
+
} catch {
|
|
144
|
+
// fall through to the bundled prerender output
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
return readEntry(path.join(prerenderDir, relative), revalidate);
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const platformWritePrerendered = (pathname, html, revalidate, forced) => {
|
|
154
|
+
const relative = prerenderRelativePath(pathname);
|
|
155
|
+
if (relative === null) return;
|
|
156
|
+
const htmlPath = path.join(isrCacheDir, relative);
|
|
157
|
+
mkdirSync(path.dirname(htmlPath), { recursive: true });
|
|
158
|
+
writeFileSync(htmlPath, html, 'utf8');
|
|
159
|
+
if (forced === true) return purgeDurableCache(pathname);
|
|
160
|
+
};
|
|
161
|
+
|
|
85
162
|
const handler = createHandler({
|
|
86
163
|
routes: manifest.routes,
|
|
87
164
|
middleware: runtimePolicy.middleware,
|
|
@@ -91,39 +168,53 @@ const handler = createHandler({
|
|
|
91
168
|
importAction: loadActionModule,
|
|
92
169
|
pluginHttp: applyPluginHttp,
|
|
93
170
|
security: runtimePolicy.security,
|
|
94
|
-
|
|
95
|
-
// prerenderRelativePath rejects any request path that cannot be mapped to a
|
|
96
|
-
// location inside the cache directories, so reads can never escape them.
|
|
97
|
-
const relative = prerenderRelativePath(pathname);
|
|
98
|
-
if (relative === null) return null;
|
|
99
|
-
try {
|
|
100
|
-
return readEntry(path.join(isrCacheDir, relative), revalidate);
|
|
101
|
-
} catch {
|
|
102
|
-
// fall through to the bundled prerender output
|
|
103
|
-
}
|
|
104
|
-
try {
|
|
105
|
-
return readEntry(path.join(prerenderDir, relative), revalidate);
|
|
106
|
-
} catch {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
writePrerendered: (pathname, html, revalidate) => {
|
|
111
|
-
const relative = prerenderRelativePath(pathname);
|
|
112
|
-
if (relative === null) return;
|
|
113
|
-
const htmlPath = path.join(isrCacheDir, relative);
|
|
114
|
-
mkdirSync(path.dirname(htmlPath), { recursive: true });
|
|
115
|
-
writeFileSync(htmlPath, html, 'utf8');
|
|
116
|
-
},
|
|
171
|
+
${documentCacheOptionsSource('platformReadPrerendered', 'platformWritePrerendered')}
|
|
117
172
|
// The project's own not-found page, pre-rendered by the build and carried
|
|
118
173
|
// inline in the manifest: an unmatched URL is answered with the page the
|
|
119
174
|
// application actually wrote, on every host.
|
|
120
175
|
notFoundDocument: manifest.notFoundDocument,
|
|
121
176
|
supportedStrategies: ['ssr', 'ssg', 'csr', 'isr', 'ppr', 'api'],
|
|
177
|
+
// Netlify's own edge writes \`X-Nf-Client-Connection-IP\` and overwrites any
|
|
178
|
+
// inbound copy of it, so on this platform the header *is* the client.
|
|
179
|
+
// Declared here rather than assumed by the handler: the same handler runs on
|
|
180
|
+
// a self-hosted server where nothing writes it. Without a declaration
|
|
181
|
+
// identity fell through to a right-to-left scan of \`X-Forwarded-For\`, whose
|
|
182
|
+
// rightmost entry here is a fixed platform address — so the built-in \`rate\`
|
|
183
|
+
// middleware, the server-action rate limiter, and the replay quota all
|
|
184
|
+
// counted every visitor into one bucket.
|
|
185
|
+
clientIpHeaders: ['x-nf-client-connection-ip'],
|
|
122
186
|
});
|
|
123
187
|
|
|
124
188
|
// Netlify Functions v2 — Web-standard Request/Response
|
|
125
189
|
export default async function(request, context) {
|
|
126
|
-
|
|
190
|
+
const response = await handler(request, context);
|
|
191
|
+
return withDurableCache(request, response);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Re-state a cacheable answer for Netlify's own cache, durably.
|
|
196
|
+
*
|
|
197
|
+
* \`Cache-Control\` reaches the browser and every CDN; \`Netlify-CDN-Cache-Control\`
|
|
198
|
+
* is read by Netlify alone and is the only place the \`durable\` directive means
|
|
199
|
+
* anything. Durable storage is what makes one render serve every edge node
|
|
200
|
+
* instead of one — and it is what \`purgeCache\` above can invalidate, so the two
|
|
201
|
+
* halves have to be added together or \`revalidatePath()\` purges nothing.
|
|
202
|
+
*
|
|
203
|
+
* Only a shared-cacheable response is re-stated: \`s-maxage\` is the marker the
|
|
204
|
+
* handler already puts on an ISR document, and \`no-store\` on everything a
|
|
205
|
+
* request produced for one visitor.
|
|
206
|
+
*/
|
|
207
|
+
function withDurableCache(request, response) {
|
|
208
|
+
const cacheControl = response.headers.get('cache-control');
|
|
209
|
+
if (!cacheControl || !cacheControl.includes('s-maxage')) return response;
|
|
210
|
+
const headers = new Headers(response.headers);
|
|
211
|
+
headers.set('netlify-cdn-cache-control', 'public, durable, ' + cacheControl);
|
|
212
|
+
headers.set('cache-tag', cacheTag(new URL(request.url).pathname));
|
|
213
|
+
return new Response(response.body, {
|
|
214
|
+
status: response.status,
|
|
215
|
+
statusText: response.statusText,
|
|
216
|
+
headers,
|
|
217
|
+
});
|
|
127
218
|
}
|
|
128
219
|
|
|
129
220
|
// Netlify Functions v2 config
|
|
@@ -141,8 +232,18 @@ export const config = ${JSON.stringify(functionConfig, null, 2)};
|
|
|
141
232
|
* `includedFiles` is what carries the deploy-time prerender output into the
|
|
142
233
|
* bundle. Netlify bundles the function with esbuild, which copies only what the
|
|
143
234
|
* module graph reaches, so the HTML `readPrerendered` falls back to was being
|
|
144
|
-
* dropped.
|
|
145
|
-
*
|
|
235
|
+
* dropped.
|
|
236
|
+
*
|
|
237
|
+
* The glob is relative to **the function's own directory**, not to the site's
|
|
238
|
+
* base directory. `zip-it-and-ship-it` sets
|
|
239
|
+
* `includedFilesBasePath = dirname(mainFile)` whenever `includedFiles` comes
|
|
240
|
+
* from an in-source `config` export, and every one of its bundlers passes that
|
|
241
|
+
* to `getPathsOfIncludedFiles`, which globs with `cwd: basePath`. Each artifact
|
|
242
|
+
* used to name its own site-relative path instead — which resolved to
|
|
243
|
+
* `<function>/functions/ruvyxa-handler/prerender/**`, matching nothing, in both
|
|
244
|
+
* the deploy-directory workflow and the project-root one, with a green build
|
|
245
|
+
* behind it. One path is now correct for both, which is why this takes no
|
|
246
|
+
* argument.
|
|
146
247
|
*/
|
|
147
248
|
interface NetlifyFunctionConfig {
|
|
148
249
|
path: string
|
|
@@ -165,13 +266,13 @@ function packageVersion(): string {
|
|
|
165
266
|
return metadata.version
|
|
166
267
|
}
|
|
167
268
|
|
|
168
|
-
function functionConfigFor(
|
|
269
|
+
function functionConfigFor(): NetlifyFunctionConfig {
|
|
169
270
|
return {
|
|
170
271
|
path: '/*',
|
|
171
272
|
preferStatic: true,
|
|
172
273
|
name: 'Ruvyxa SSR',
|
|
173
274
|
generator: `ruvyxa/${packageVersion()}`,
|
|
174
|
-
includedFiles: [
|
|
275
|
+
includedFiles: ['prerender/**'],
|
|
175
276
|
}
|
|
176
277
|
}
|
|
177
278
|
|
|
@@ -209,6 +310,10 @@ export function netlify(options: NetlifyAdapterOptions = {}): Adapter {
|
|
|
209
310
|
supports: ['ssr', 'ssg', 'csr', 'isr', 'ppr', 'api'],
|
|
210
311
|
build(ctx: BuildContext): AdapterOutput {
|
|
211
312
|
validateBuildContext(ctx, 'netlifyAdapter')
|
|
313
|
+
// Exactly one of the two ships: see `frameworksApi` above for why both
|
|
314
|
+
// at once is a site with two functions claiming the same path.
|
|
315
|
+
const projectConfig = options.projectConfig !== false
|
|
316
|
+
const frameworksApi = options.frameworksApi ?? !projectConfig
|
|
212
317
|
const functionsDir = options.functionsDir ?? `${ctx.outDir}/netlify/functions`
|
|
213
318
|
// Config files are committed or read on other machines; never embed the
|
|
214
319
|
// absolute build-machine outDir in them.
|
|
@@ -298,7 +403,8 @@ export function netlify(options: NetlifyAdapterOptions = {}): Adapter {
|
|
|
298
403
|
path: 'deploy/netlify/functions/ruvyxa-handler',
|
|
299
404
|
handlerSource: netlifyHandlerSource(
|
|
300
405
|
runtimePolicy,
|
|
301
|
-
functionConfigFor(
|
|
406
|
+
functionConfigFor(),
|
|
407
|
+
ctx.deployManifest?.buildId ?? '',
|
|
302
408
|
),
|
|
303
409
|
},
|
|
304
410
|
// Netlify config for the deploy directory
|
|
@@ -310,16 +416,16 @@ export function netlify(options: NetlifyAdapterOptions = {}): Adapter {
|
|
|
310
416
|
// Frameworks API artifacts: Netlify discovers .netlify/v1/ at the
|
|
311
417
|
// project root automatically, so a fresh site needs only the publish
|
|
312
418
|
// directory set once in the dashboard — no committed config file.
|
|
313
|
-
...(
|
|
314
|
-
? [
|
|
315
|
-
: [
|
|
419
|
+
...(frameworksApi
|
|
420
|
+
? [
|
|
316
421
|
{
|
|
317
422
|
kind: 'function',
|
|
318
423
|
path: '.netlify/v1/functions/ruvyxa-handler',
|
|
319
424
|
scope: 'project',
|
|
320
425
|
handlerSource: netlifyHandlerSource(
|
|
321
426
|
runtimePolicy,
|
|
322
|
-
functionConfigFor(
|
|
427
|
+
functionConfigFor(),
|
|
428
|
+
ctx.deployManifest?.buildId ?? '',
|
|
323
429
|
),
|
|
324
430
|
} satisfies AdapterArtifact,
|
|
325
431
|
{
|
|
@@ -328,8 +434,9 @@ export function netlify(options: NetlifyAdapterOptions = {}): Adapter {
|
|
|
328
434
|
scope: 'project',
|
|
329
435
|
contents: frameworksApiConfig + '\n',
|
|
330
436
|
} satisfies AdapterArtifact,
|
|
331
|
-
]
|
|
332
|
-
|
|
437
|
+
]
|
|
438
|
+
: []),
|
|
439
|
+
...(projectConfig
|
|
333
440
|
? [
|
|
334
441
|
{
|
|
335
442
|
kind: 'file',
|