@rsc-kit/core 0.16.1 → 0.16.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/dist/apiPrerender.d.ts +2 -0
- package/dist/apiPrerender.js +111 -81
- package/dist/apiPrerender.js.map +1 -1
- package/dist/appAssets.d.ts +4 -1
- package/dist/appAssets.js +55 -19
- package/dist/appAssets.js.map +1 -1
- package/dist/buildReport.d.ts +2 -0
- package/dist/buildReport.js.map +1 -1
- package/dist/files.js +29 -5
- package/dist/files.js.map +1 -1
- package/dist/host.d.ts +11 -0
- package/dist/host.js +61 -5
- package/dist/host.js.map +1 -1
- package/dist/js/createViteRscApp.js +57 -11
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/devNotice.d.ts +13 -0
- package/dist/js/devNotice.js +45 -0
- package/dist/js/devNotice.js.map +1 -0
- package/dist/js/errors.js +4 -0
- package/dist/js/errors.js.map +1 -1
- package/dist/js/navigate.js +23 -0
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/staleAssets.d.ts +17 -0
- package/dist/js/staleAssets.js +39 -0
- package/dist/js/staleAssets.js.map +1 -0
- package/dist/js/useSearchParams.d.ts +6 -0
- package/dist/js/useSearchParams.js +16 -3
- package/dist/js/useSearchParams.js.map +1 -1
- package/dist/prerender.d.ts +2 -2
- package/dist/prerender.js +190 -124
- package/dist/prerender.js.map +1 -1
- package/dist/request.d.ts +3 -1
- package/dist/request.js +131 -45
- package/dist/request.js.map +1 -1
- package/dist/routing.d.ts +0 -7
- package/dist/routing.js +54 -20
- package/dist/routing.js.map +1 -1
- package/dist/vite.d.ts +13 -12
- package/dist/vite.js +554 -324
- package/dist/vite.js.map +1 -1
- package/package.json +2 -2
package/dist/apiPrerender.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface ApiPrerenderResult {
|
|
|
20
20
|
name: string;
|
|
21
21
|
type: 'frozen' | 'dynamic';
|
|
22
22
|
reason: string | null;
|
|
23
|
+
/** Stored, and worth a second look - it froze a value that will not be the same tomorrow. */
|
|
24
|
+
warning?: string;
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
25
27
|
* Try to answer every api route once, at build time, and store what can be.
|
package/dist/apiPrerender.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
// answer to a POST is a stored answer to something that was supposed to happen
|
|
12
12
|
// once.
|
|
13
13
|
import { pathKey } from './prerender.js';
|
|
14
|
-
import { requestReadBy, withRequest } from './request.js';
|
|
14
|
+
import { requestReadBy, withRequest, withResponseDraft } from './request.js';
|
|
15
|
+
import { watchNondeterminism, whileRendering } from './nondeterminism.js';
|
|
15
16
|
import { allowFor } from './routing.js';
|
|
16
17
|
/**
|
|
17
18
|
* How long a route gets to answer before it is called dynamic.
|
|
@@ -113,87 +114,116 @@ export async function prerenderApiRoutes(engine, manifest, write) {
|
|
|
113
114
|
if (!engine.handleApiRoute || !manifest.apis?.length)
|
|
114
115
|
return [];
|
|
115
116
|
const results = [];
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
117
|
+
// Date.now() and friends are watched for the length of the loop, the way
|
|
118
|
+
// the page prerender watches them; whileRendering records what each route
|
|
119
|
+
// reached for while it answered.
|
|
120
|
+
const unwatch = watchNondeterminism();
|
|
121
|
+
try {
|
|
122
|
+
for (const route of manifest.apis) {
|
|
123
|
+
const said = (type, reason) => {
|
|
124
|
+
results.push({ url: labelFor(route), name: route.name, type, reason });
|
|
125
|
+
};
|
|
126
|
+
if (!route.methods.includes('GET')) {
|
|
127
|
+
said('dynamic', 'no GET to store');
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// A guarded route answers differently depending on who is asking, which is
|
|
131
|
+
// the whole purpose of the guard. Storing one answer and serving it to
|
|
132
|
+
// everyone is how a guard is silently removed.
|
|
133
|
+
if (route.middleware.length > 0) {
|
|
134
|
+
said('dynamic', 'guarded by middleware');
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const url = urlFor(route);
|
|
138
|
+
if (!url) {
|
|
139
|
+
said('dynamic', 'one url per param value, and none are listed');
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const touched = new Set();
|
|
143
|
+
const request = probeRequest('https://prerender.invalid' + url, touched);
|
|
144
|
+
// No request in scope, so headers(), cookies() and connection() suspend
|
|
145
|
+
// forever rather than resolving to whatever the build machine had. The
|
|
146
|
+
// budget below is what turns that into an answer.
|
|
147
|
+
// Inside a response draft, because that is where the engine puts a cookie
|
|
148
|
+
// a route sets - a literal Set-Cookie on its Response is moved there too
|
|
149
|
+
// whenever a draft is open - and a probe with no draft reads a Response
|
|
150
|
+
// the cookie may already have left. Whether it went to the draft or stayed
|
|
151
|
+
// on the Response depends on what else is running in the process; the
|
|
152
|
+
// question here is only whether there was one.
|
|
153
|
+
const answered = await withResponseDraft(({ taken }) => withRequest(null, async () => {
|
|
154
|
+
let readBy = [];
|
|
155
|
+
const response = await Promise.race([
|
|
156
|
+
whileRendering(() => engine.handleApiRoute(route.name, request, {}, allowFor(route)))
|
|
157
|
+
.then(([value, reached]) => ({ value, reached }))
|
|
158
|
+
.catch((error) => ({ error })),
|
|
159
|
+
new Promise((resolve) => setTimeout(() => {
|
|
160
|
+
readBy = requestReadBy();
|
|
161
|
+
resolve(null);
|
|
162
|
+
}, BUDGET_MS)),
|
|
163
|
+
]);
|
|
164
|
+
return { response, readBy: readBy.length ? readBy : requestReadBy(), drafted: taken() };
|
|
165
|
+
}));
|
|
166
|
+
if (answered.response === null) {
|
|
167
|
+
const why = answered.readBy.length
|
|
168
|
+
? 'dynamic — called ' + answered.readBy.join(', ')
|
|
169
|
+
: 'did not answer within the build budget';
|
|
170
|
+
said('dynamic', why);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if ('error' in answered.response) {
|
|
174
|
+
// Not a build failure. A route that throws with no request may be doing
|
|
175
|
+
// exactly the right thing — refusing a caller it cannot identify — and
|
|
176
|
+
// refusing the build over it would make that route unbuildable.
|
|
177
|
+
said('dynamic', 'threw without a request');
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (touched.size > 0) {
|
|
181
|
+
said('dynamic', 'reads the request — ' + [...touched].sort().join(', '));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Awaiting the query string is not a reason to give up on the route — the
|
|
185
|
+
// bare url still has one right answer. It only narrows which requests the
|
|
186
|
+
// stored answer is good for.
|
|
187
|
+
const varies = answered.readBy.includes('searchParams');
|
|
188
|
+
const response = answered.response.value;
|
|
189
|
+
// A cookie is an answer for one visitor, whatever the route read to
|
|
190
|
+
// decide on it. Stored, the build's cookie would be handed to everyone.
|
|
191
|
+
if (response.headers.has('set-cookie') || answered.drafted.has('set-cookie')) {
|
|
192
|
+
said('dynamic', 'sets a cookie — an answer for one visitor, not one to store');
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const body = asText(new Uint8Array(await response.arrayBuffer()));
|
|
196
|
+
if (body === null) {
|
|
197
|
+
said('dynamic', 'answers with bytes rather than text');
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
await write(apiKey(url), JSON.stringify({
|
|
201
|
+
status: response.status,
|
|
202
|
+
// Lower-cased and sorted, so two builds of the same route produce the
|
|
203
|
+
// same bytes. Headers iteration does not promise a case or an order,
|
|
204
|
+
// and a file that differs between builds for no reason defeats
|
|
205
|
+
// content-addressed caching and makes a diff unreadable. Names are
|
|
206
|
+
// case-insensitive, so nothing is lost by picking one.
|
|
207
|
+
headers: [...response.headers]
|
|
208
|
+
.map(([name, value]) => [name.toLowerCase(), value])
|
|
209
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
210
|
+
body,
|
|
211
|
+
varies,
|
|
212
|
+
}));
|
|
213
|
+
said('frozen', varies ? 'stored for the bare url — it reads the query string' : null);
|
|
214
|
+
// The same warning a page gets: a stored answer keeps whatever Date.now()
|
|
215
|
+
// or Math.random() returned at build time, and a json body has no browser
|
|
216
|
+
// to move it to.
|
|
217
|
+
const reached = answered.response.reached;
|
|
218
|
+
if (reached.length > 0) {
|
|
219
|
+
results[results.length - 1].warning =
|
|
220
|
+
`froze ${reached.join(' and ')} — a stored answer keeps whatever that returned at build time. ` +
|
|
221
|
+
'If it should differ per call, read the request (await connection()) so the route runs on demand.';
|
|
222
|
+
}
|
|
182
223
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// same bytes. Headers iteration does not promise a case or an order,
|
|
187
|
-
// and a file that differs between builds for no reason defeats
|
|
188
|
-
// content-addressed caching and makes a diff unreadable. Names are
|
|
189
|
-
// case-insensitive, so nothing is lost by picking one.
|
|
190
|
-
headers: [...response.headers]
|
|
191
|
-
.map(([name, value]) => [name.toLowerCase(), value])
|
|
192
|
-
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
193
|
-
body,
|
|
194
|
-
varies,
|
|
195
|
-
}));
|
|
196
|
-
said('frozen', varies ? 'stored for the bare url — it reads the query string' : null);
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
unwatch();
|
|
197
227
|
}
|
|
198
228
|
return results;
|
|
199
229
|
}
|
package/dist/apiPrerender.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apiPrerender.js","sourceRoot":"","sources":["../src/apiPrerender.ts"],"names":[],"mappings":"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","sourcesContent":["// Freezing an api route the way a page is frozen.\n//\n// Not as an opt-in flag, deliberately. A page is stored by default and opts\n// OUT by touching the request — connection(), cookies(), headers() all suspend\n// at build time because there is no request there, and that is what marks the\n// page dynamic. A route works the same way and for the same reason: one model\n// to learn rather than two, and the honest default in both cases is \"the build\n// tried, and here is what it found\".\n//\n// GET only. Everything else is a method a caller may not repeat, and a stored\n// answer to a POST is a stored answer to something that was supposed to happen\n// once.\n\nimport { pathKey } from './prerender.js'\nimport { requestReadBy, withRequest } from './request.js'\nimport type { ManifestApiRoute, RouteManifest } from './manifest.js'\nimport { allowFor } from './routing.js'\n\n/**\n * How long a route gets to answer before it is called dynamic.\n *\n * A route that reads the request does not fail here — it never settles, because\n * the accessors suspend forever with no request to read. So the budget is what\n * turns \"waiting\" into an answer, and it only has to be long enough for a route\n * that was going to finish.\n */\nconst BUDGET_MS = 2_000\n\n/** What a stored answer holds. Enough to rebuild the Response exactly. */\nexport interface FrozenApiResponse {\n status: number\n headers: [string, string][]\n body: string\n /**\n * Whether the answer depends on the query string.\n *\n * False when the handler never awaited `searchParams` and declared no schema\n * for it, which means the same answer is right for `?utm_source=anything`.\n * True and the stored answer is only good for the bare url.\n */\n varies: boolean\n}\n\n/** The file a frozen route is stored as. */\nexport function apiKey(url: string): string {\n return `${pathKey(url)}.api.json`\n}\n\n/**\n * Reading anything here means the answer depends on the caller.\n *\n * Deliberately not `url`: the url is the key the answer is stored under, so\n * reading it tells you the same thing on every request that would hit the\n * stored file. The query string is handled by refusing to serve a stored\n * answer to a request that has one, which needs no detection at all.\n */\nconst PER_CALLER = new Set([\n 'headers',\n 'body',\n 'bodyUsed',\n 'text',\n 'json',\n 'formData',\n 'arrayBuffer',\n 'blob',\n 'bytes',\n 'signal',\n 'referrer',\n 'credentials',\n])\n\n/**\n * A Request that records what was read out of it.\n *\n * A proxy rather than a subclass because the interesting properties are\n * getters on Request.prototype, and `this` has to stay the real Request or\n * every one of them throws about an illegal invocation.\n */\nfunction probeRequest(url: string, touched: Set<string>): Request {\n const real = new Request(url, { method: 'GET' })\n\n return new Proxy(real, {\n get(target, property) {\n if (typeof property === 'string' && PER_CALLER.has(property)) touched.add(property)\n\n const value = Reflect.get(target, property, target)\n\n return typeof value === 'function' ? value.bind(target) : value\n },\n })\n}\n\n/** The url a route with no parameters answers. */\nfunction urlFor(route: ManifestApiRoute): string | null {\n // A parameterised route has as many urls as there are values, and nothing\n // here knows them. Pages solve this with generateStaticParams; until a route\n // can say the same, one is answered per request.\n if (route.segments.some((segment) => segment.type !== 'static')) return null\n\n return '/' + route.segments.map((segment) => segment.value).join('/')\n}\n\n/**\n * What the build calls a route in its output.\n *\n * The pattern for a parameterised one, spelled the way pages already spell\n * theirs, rather than the module name — a line reading\n * \"/app/api/greet/[name]/route\" names a file on disk and the rest of the table\n * names urls.\n */\nfunction labelFor(route: ManifestApiRoute): string {\n return (\n '/' +\n route.segments\n .map((segment) => (segment.type === 'static' ? segment.value : `_${segment.value}_`))\n .join('/')\n )\n}\n\n/** Whether a body is text this can store and hand back unchanged. */\nfunction asText(bytes: Uint8Array): string | null {\n try {\n const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes)\n\n return text\n } catch {\n // Binary. Storable in principle, as base64, at the cost of a third of its\n // size on disk and a decode per request — for a route that is far more\n // likely to be streaming a file it should be serving as a file.\n return null\n }\n}\n\nexport interface ApiPrerenderResult {\n url: string\n name: string\n type: 'frozen' | 'dynamic'\n reason: string | null\n}\n\n/**\n * Try to answer every api route once, at build time, and store what can be.\n *\n * Sequential rather than parallel: there are usually few of them, each is a\n * function call rather than a React render, and the ones that are going to be\n * dynamic spend the whole budget waiting — which is time, not work.\n */\nexport async function prerenderApiRoutes(\n engine: { handleApiRoute?: (n: string, r: Request, p: Record<string, string>, a: string) => Promise<Response> },\n manifest: RouteManifest,\n write: (name: string, contents: string) => Promise<void>,\n): Promise<ApiPrerenderResult[]> {\n if (!engine.handleApiRoute || !manifest.apis?.length) return []\n\n const results: ApiPrerenderResult[] = []\n\n for (const route of manifest.apis) {\n const said = (type: 'frozen' | 'dynamic', reason: string | null) => {\n results.push({ url: labelFor(route), name: route.name, type, reason })\n }\n\n if (!route.methods.includes('GET')) {\n said('dynamic', 'no GET to store')\n continue\n }\n\n // A guarded route answers differently depending on who is asking, which is\n // the whole purpose of the guard. Storing one answer and serving it to\n // everyone is how a guard is silently removed.\n if (route.middleware.length > 0) {\n said('dynamic', 'guarded by middleware')\n continue\n }\n\n const url = urlFor(route)\n\n if (!url) {\n said('dynamic', 'one url per param value, and none are listed')\n continue\n }\n\n const touched = new Set<string>()\n const request = probeRequest('https://prerender.invalid' + url, touched)\n\n // No request in scope, so headers(), cookies() and connection() suspend\n // forever rather than resolving to whatever the build machine had. The\n // budget below is what turns that into an answer.\n const answered = await withRequest(null as never, async () => {\n let readBy: string[] = []\n\n const response = await Promise.race([\n engine\n .handleApiRoute!(route.name, request, {}, allowFor(route))\n .then((value) => ({ value }))\n .catch((error) => ({ error })),\n new Promise<null>((resolve) =>\n setTimeout(() => {\n readBy = requestReadBy()\n resolve(null)\n }, BUDGET_MS),\n ),\n ])\n\n return { response, readBy: readBy.length ? readBy : requestReadBy() }\n })\n\n if (answered.response === null) {\n const why = answered.readBy.length\n ? 'dynamic — called ' + answered.readBy.join(', ')\n : 'did not answer within the build budget'\n\n said('dynamic', why)\n continue\n }\n\n if ('error' in answered.response) {\n // Not a build failure. A route that throws with no request may be doing\n // exactly the right thing — refusing a caller it cannot identify — and\n // refusing the build over it would make that route unbuildable.\n said('dynamic', 'threw without a request')\n continue\n }\n\n if (touched.size > 0) {\n said('dynamic', 'reads the request — ' + [...touched].sort().join(', '))\n continue\n }\n\n // Awaiting the query string is not a reason to give up on the route — the\n // bare url still has one right answer. It only narrows which requests the\n // stored answer is good for.\n const varies = answered.readBy.includes('searchParams')\n\n const response = answered.response.value\n const body = asText(new Uint8Array(await response.arrayBuffer()))\n\n if (body === null) {\n said('dynamic', 'answers with bytes rather than text')\n continue\n }\n\n await write(\n apiKey(url),\n JSON.stringify({\n status: response.status,\n // Lower-cased and sorted, so two builds of the same route produce the\n // same bytes. Headers iteration does not promise a case or an order,\n // and a file that differs between builds for no reason defeats\n // content-addressed caching and makes a diff unreadable. Names are\n // case-insensitive, so nothing is lost by picking one.\n headers: [...response.headers]\n .map(([name, value]) => [name.toLowerCase(), value] as [string, string])\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n body,\n varies,\n } satisfies FrozenApiResponse),\n )\n\n said('frozen', varies ? 'stored for the bare url — it reads the query string' : null)\n }\n\n return results\n}\n"]}
|
|
1
|
+
{"version":3,"file":"apiPrerender.js","sourceRoot":"","sources":["../src/apiPrerender.ts"],"names":[],"mappings":"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","sourcesContent":["// Freezing an api route the way a page is frozen.\n//\n// Not as an opt-in flag, deliberately. A page is stored by default and opts\n// OUT by touching the request — connection(), cookies(), headers() all suspend\n// at build time because there is no request there, and that is what marks the\n// page dynamic. A route works the same way and for the same reason: one model\n// to learn rather than two, and the honest default in both cases is \"the build\n// tried, and here is what it found\".\n//\n// GET only. Everything else is a method a caller may not repeat, and a stored\n// answer to a POST is a stored answer to something that was supposed to happen\n// once.\n\nimport { pathKey } from './prerender.js'\nimport { requestReadBy, withRequest, withResponseDraft } from './request.js'\nimport { watchNondeterminism, whileRendering } from './nondeterminism.js'\nimport type { ManifestApiRoute, RouteManifest } from './manifest.js'\nimport { allowFor } from './routing.js'\n\n/**\n * How long a route gets to answer before it is called dynamic.\n *\n * A route that reads the request does not fail here — it never settles, because\n * the accessors suspend forever with no request to read. So the budget is what\n * turns \"waiting\" into an answer, and it only has to be long enough for a route\n * that was going to finish.\n */\nconst BUDGET_MS = 2_000\n\n/** What a stored answer holds. Enough to rebuild the Response exactly. */\nexport interface FrozenApiResponse {\n status: number\n headers: [string, string][]\n body: string\n /**\n * Whether the answer depends on the query string.\n *\n * False when the handler never awaited `searchParams` and declared no schema\n * for it, which means the same answer is right for `?utm_source=anything`.\n * True and the stored answer is only good for the bare url.\n */\n varies: boolean\n}\n\n/** The file a frozen route is stored as. */\nexport function apiKey(url: string): string {\n return `${pathKey(url)}.api.json`\n}\n\n/**\n * Reading anything here means the answer depends on the caller.\n *\n * Deliberately not `url`: the url is the key the answer is stored under, so\n * reading it tells you the same thing on every request that would hit the\n * stored file. The query string is handled by refusing to serve a stored\n * answer to a request that has one, which needs no detection at all.\n */\nconst PER_CALLER = new Set([\n 'headers',\n 'body',\n 'bodyUsed',\n 'text',\n 'json',\n 'formData',\n 'arrayBuffer',\n 'blob',\n 'bytes',\n 'signal',\n 'referrer',\n 'credentials',\n])\n\n/**\n * A Request that records what was read out of it.\n *\n * A proxy rather than a subclass because the interesting properties are\n * getters on Request.prototype, and `this` has to stay the real Request or\n * every one of them throws about an illegal invocation.\n */\nfunction probeRequest(url: string, touched: Set<string>): Request {\n const real = new Request(url, { method: 'GET' })\n\n return new Proxy(real, {\n get(target, property) {\n if (typeof property === 'string' && PER_CALLER.has(property)) touched.add(property)\n\n const value = Reflect.get(target, property, target)\n\n return typeof value === 'function' ? value.bind(target) : value\n },\n })\n}\n\n/** The url a route with no parameters answers. */\nfunction urlFor(route: ManifestApiRoute): string | null {\n // A parameterised route has as many urls as there are values, and nothing\n // here knows them. Pages solve this with generateStaticParams; until a route\n // can say the same, one is answered per request.\n if (route.segments.some((segment) => segment.type !== 'static')) return null\n\n return '/' + route.segments.map((segment) => segment.value).join('/')\n}\n\n/**\n * What the build calls a route in its output.\n *\n * The pattern for a parameterised one, spelled the way pages already spell\n * theirs, rather than the module name — a line reading\n * \"/app/api/greet/[name]/route\" names a file on disk and the rest of the table\n * names urls.\n */\nfunction labelFor(route: ManifestApiRoute): string {\n return (\n '/' +\n route.segments\n .map((segment) => (segment.type === 'static' ? segment.value : `_${segment.value}_`))\n .join('/')\n )\n}\n\n/** Whether a body is text this can store and hand back unchanged. */\nfunction asText(bytes: Uint8Array): string | null {\n try {\n const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes)\n\n return text\n } catch {\n // Binary. Storable in principle, as base64, at the cost of a third of its\n // size on disk and a decode per request — for a route that is far more\n // likely to be streaming a file it should be serving as a file.\n return null\n }\n}\n\nexport interface ApiPrerenderResult {\n url: string\n name: string\n type: 'frozen' | 'dynamic'\n reason: string | null\n /** Stored, and worth a second look - it froze a value that will not be the same tomorrow. */\n warning?: string\n}\n\n/**\n * Try to answer every api route once, at build time, and store what can be.\n *\n * Sequential rather than parallel: there are usually few of them, each is a\n * function call rather than a React render, and the ones that are going to be\n * dynamic spend the whole budget waiting — which is time, not work.\n */\nexport async function prerenderApiRoutes(\n engine: { handleApiRoute?: (n: string, r: Request, p: Record<string, string>, a: string) => Promise<Response> },\n manifest: RouteManifest,\n write: (name: string, contents: string) => Promise<void>,\n): Promise<ApiPrerenderResult[]> {\n if (!engine.handleApiRoute || !manifest.apis?.length) return []\n\n const results: ApiPrerenderResult[] = []\n\n // Date.now() and friends are watched for the length of the loop, the way\n // the page prerender watches them; whileRendering records what each route\n // reached for while it answered.\n const unwatch = watchNondeterminism()\n\n try {\n for (const route of manifest.apis) {\n const said = (type: 'frozen' | 'dynamic', reason: string | null) => {\n results.push({ url: labelFor(route), name: route.name, type, reason })\n }\n\n if (!route.methods.includes('GET')) {\n said('dynamic', 'no GET to store')\n continue\n }\n\n // A guarded route answers differently depending on who is asking, which is\n // the whole purpose of the guard. Storing one answer and serving it to\n // everyone is how a guard is silently removed.\n if (route.middleware.length > 0) {\n said('dynamic', 'guarded by middleware')\n continue\n }\n\n const url = urlFor(route)\n\n if (!url) {\n said('dynamic', 'one url per param value, and none are listed')\n continue\n }\n\n const touched = new Set<string>()\n const request = probeRequest('https://prerender.invalid' + url, touched)\n\n // No request in scope, so headers(), cookies() and connection() suspend\n // forever rather than resolving to whatever the build machine had. The\n // budget below is what turns that into an answer.\n // Inside a response draft, because that is where the engine puts a cookie\n // a route sets - a literal Set-Cookie on its Response is moved there too\n // whenever a draft is open - and a probe with no draft reads a Response\n // the cookie may already have left. Whether it went to the draft or stayed\n // on the Response depends on what else is running in the process; the\n // question here is only whether there was one.\n const answered = await withResponseDraft(({ taken }) =>\n withRequest(null as never, async () => {\n let readBy: string[] = []\n\n const response = await Promise.race([\n whileRendering(() => engine.handleApiRoute!(route.name, request, {}, allowFor(route)))\n .then(([value, reached]) => ({ value, reached }))\n .catch((error) => ({ error })),\n new Promise<null>((resolve) =>\n setTimeout(() => {\n readBy = requestReadBy()\n resolve(null)\n }, BUDGET_MS),\n ),\n ])\n\n return { response, readBy: readBy.length ? readBy : requestReadBy(), drafted: taken() }\n }),\n )\n\n if (answered.response === null) {\n const why = answered.readBy.length\n ? 'dynamic — called ' + answered.readBy.join(', ')\n : 'did not answer within the build budget'\n\n said('dynamic', why)\n continue\n }\n\n if ('error' in answered.response) {\n // Not a build failure. A route that throws with no request may be doing\n // exactly the right thing — refusing a caller it cannot identify — and\n // refusing the build over it would make that route unbuildable.\n said('dynamic', 'threw without a request')\n continue\n }\n\n if (touched.size > 0) {\n said('dynamic', 'reads the request — ' + [...touched].sort().join(', '))\n continue\n }\n\n // Awaiting the query string is not a reason to give up on the route — the\n // bare url still has one right answer. It only narrows which requests the\n // stored answer is good for.\n const varies = answered.readBy.includes('searchParams')\n\n const response = answered.response.value\n\n // A cookie is an answer for one visitor, whatever the route read to\n // decide on it. Stored, the build's cookie would be handed to everyone.\n if (response.headers.has('set-cookie') || answered.drafted.has('set-cookie')) {\n said('dynamic', 'sets a cookie — an answer for one visitor, not one to store')\n continue\n }\n\n const body = asText(new Uint8Array(await response.arrayBuffer()))\n\n if (body === null) {\n said('dynamic', 'answers with bytes rather than text')\n continue\n }\n\n await write(\n apiKey(url),\n JSON.stringify({\n status: response.status,\n // Lower-cased and sorted, so two builds of the same route produce the\n // same bytes. Headers iteration does not promise a case or an order,\n // and a file that differs between builds for no reason defeats\n // content-addressed caching and makes a diff unreadable. Names are\n // case-insensitive, so nothing is lost by picking one.\n headers: [...response.headers]\n .map(([name, value]) => [name.toLowerCase(), value] as [string, string])\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n body,\n varies,\n } satisfies FrozenApiResponse),\n )\n\n said('frozen', varies ? 'stored for the bare url — it reads the query string' : null)\n\n // The same warning a page gets: a stored answer keeps whatever Date.now()\n // or Math.random() returned at build time, and a json body has no browser\n // to move it to.\n const reached = answered.response.reached\n\n if (reached.length > 0) {\n results[results.length - 1]!.warning =\n `froze ${reached.join(' and ')} — a stored answer keeps whatever that returned at build time. ` +\n 'If it should differ per call, read the request (await connection()) so the route runs on demand.'\n }\n }\n } finally {\n unwatch()\n }\n\n return results\n}\n"]}
|
package/dist/appAssets.d.ts
CHANGED
|
@@ -23,8 +23,11 @@ export interface AppAssets {
|
|
|
23
23
|
export declare const ASSET_BASE = "/_app";
|
|
24
24
|
/** What the app declared by putting a file where it could be found. */
|
|
25
25
|
export declare function appAssets(appDir: string): AppAssets;
|
|
26
|
+
export declare function typeOf(file: string): string;
|
|
26
27
|
/** The head tags these produce, as plain data the generated entry turns into elements. */
|
|
28
|
+
/** Every asset found, favicon first: what the build copies and the dev server answers. */
|
|
29
|
+
export declare function allAppAssets(assets: AppAssets): AppAsset[];
|
|
27
30
|
export declare function headTags(assets: AppAssets, origin?: string): {
|
|
28
|
-
tag:
|
|
31
|
+
tag: "link" | "meta";
|
|
29
32
|
props: Record<string, string>;
|
|
30
33
|
}[];
|
package/dist/appAssets.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// They live in `app/` rather than `public/` on purpose. `public/` is "serve
|
|
17
17
|
// this verbatim"; these are read as well as served — an icon's size decides
|
|
18
18
|
// what goes in the manifest, and its presence decides what goes in the head.
|
|
19
|
-
import { existsSync, readdirSync } from
|
|
19
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
20
20
|
const IMAGE = /\.(png|svg|jpg|jpeg|webp|gif|avif)$/i;
|
|
21
21
|
/**
|
|
22
22
|
* Where these are served from.
|
|
@@ -26,7 +26,7 @@ const IMAGE = /\.(png|svg|jpg|jpeg|webp|gif|avif)$/i;
|
|
|
26
26
|
* these, in either direction. The exception is the favicon, which browsers ask
|
|
27
27
|
* for at `/favicon.ico` whatever any markup says.
|
|
28
28
|
*/
|
|
29
|
-
export const ASSET_BASE =
|
|
29
|
+
export const ASSET_BASE = "/_app";
|
|
30
30
|
function assetsIn(dir, match) {
|
|
31
31
|
return readdirSync(dir)
|
|
32
32
|
.filter(match)
|
|
@@ -36,7 +36,13 @@ function assetsIn(dir, match) {
|
|
|
36
36
|
/** What the app declared by putting a file where it could be found. */
|
|
37
37
|
export function appAssets(appDir) {
|
|
38
38
|
if (!existsSync(appDir)) {
|
|
39
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
favicon: null,
|
|
41
|
+
icons: [],
|
|
42
|
+
appleIcon: null,
|
|
43
|
+
openGraph: null,
|
|
44
|
+
twitter: null,
|
|
45
|
+
};
|
|
40
46
|
}
|
|
41
47
|
const names = readdirSync(appDir);
|
|
42
48
|
const has = (name) => names.includes(name);
|
|
@@ -44,46 +50,76 @@ export function appAssets(appDir) {
|
|
|
44
50
|
// Served where a browser looks for it regardless of markup: a request for
|
|
45
51
|
// /favicon.ico goes out whether or not a link tag exists, and answering it
|
|
46
52
|
// from somewhere else means answering a 404 to the request that matters.
|
|
47
|
-
favicon: has(
|
|
53
|
+
favicon: has("favicon.ico")
|
|
54
|
+
? { file: "favicon.ico", href: "/favicon.ico" }
|
|
55
|
+
: null,
|
|
48
56
|
icons: assetsIn(appDir, (name) => /^icon[-\w]*/.test(name) && IMAGE.test(name)),
|
|
49
57
|
appleIcon: assetsIn(appDir, (name) => /^apple-icon[-\w]*/.test(name) && IMAGE.test(name))[0] ?? null,
|
|
50
58
|
openGraph: assetsIn(appDir, (name) => /^opengraph-image[-\w]*/.test(name) && IMAGE.test(name))[0] ?? null,
|
|
51
59
|
twitter: assetsIn(appDir, (name) => /^twitter-image[-\w]*/.test(name) && IMAGE.test(name))[0] ?? null,
|
|
52
60
|
};
|
|
53
61
|
}
|
|
54
|
-
function typeOf(file) {
|
|
55
|
-
const extension = file.slice(file.lastIndexOf(
|
|
56
|
-
return extension ===
|
|
57
|
-
?
|
|
58
|
-
: extension ===
|
|
59
|
-
?
|
|
60
|
-
: extension ===
|
|
61
|
-
?
|
|
62
|
+
export function typeOf(file) {
|
|
63
|
+
const extension = file.slice(file.lastIndexOf(".") + 1).toLowerCase();
|
|
64
|
+
return extension === "svg"
|
|
65
|
+
? "image/svg+xml"
|
|
66
|
+
: extension === "jpg" || extension === "jpeg"
|
|
67
|
+
? "image/jpeg"
|
|
68
|
+
: extension === "ico"
|
|
69
|
+
? "image/x-icon"
|
|
62
70
|
: `image/${extension}`;
|
|
63
71
|
}
|
|
64
72
|
/** The head tags these produce, as plain data the generated entry turns into elements. */
|
|
73
|
+
/** Every asset found, favicon first: what the build copies and the dev server answers. */
|
|
74
|
+
export function allAppAssets(assets) {
|
|
75
|
+
return [
|
|
76
|
+
...(assets.favicon ? [assets.favicon] : []),
|
|
77
|
+
...assets.icons,
|
|
78
|
+
...(assets.appleIcon ? [assets.appleIcon] : []),
|
|
79
|
+
...(assets.openGraph ? [assets.openGraph] : []),
|
|
80
|
+
...(assets.twitter ? [assets.twitter] : []),
|
|
81
|
+
];
|
|
82
|
+
}
|
|
65
83
|
export function headTags(assets, origin) {
|
|
66
84
|
const tags = [];
|
|
67
85
|
if (assets.favicon) {
|
|
68
|
-
tags.push({
|
|
86
|
+
tags.push({
|
|
87
|
+
tag: "link",
|
|
88
|
+
props: { rel: "icon", href: assets.favicon.href, type: "image/x-icon" },
|
|
89
|
+
});
|
|
69
90
|
}
|
|
70
91
|
for (const icon of assets.icons) {
|
|
71
|
-
tags.push({
|
|
92
|
+
tags.push({
|
|
93
|
+
tag: "link",
|
|
94
|
+
props: { rel: "icon", href: icon.href, type: typeOf(icon.file) },
|
|
95
|
+
});
|
|
72
96
|
}
|
|
73
97
|
if (assets.appleIcon) {
|
|
74
|
-
tags.push({
|
|
98
|
+
tags.push({
|
|
99
|
+
tag: "link",
|
|
100
|
+
props: { rel: "apple-touch-icon", href: assets.appleIcon.href },
|
|
101
|
+
});
|
|
75
102
|
}
|
|
76
103
|
// Absolute when the app said where it lives. The og spec asks for an absolute
|
|
77
104
|
// url and several crawlers still mean it — a relative one is read by some and
|
|
78
105
|
// ignored by others, which is the worst of both. Relative is what is left
|
|
79
106
|
// when nothing said, and is better than omitting the tag.
|
|
80
|
-
const absolute = (href) =>
|
|
107
|
+
const absolute = (href) => origin ? new URL(href, origin).href : href;
|
|
81
108
|
if (assets.openGraph) {
|
|
82
|
-
tags.push({
|
|
109
|
+
tags.push({
|
|
110
|
+
tag: "meta",
|
|
111
|
+
props: { property: "og:image", content: absolute(assets.openGraph.href) },
|
|
112
|
+
});
|
|
83
113
|
}
|
|
84
114
|
if (assets.twitter) {
|
|
85
|
-
tags.push({
|
|
86
|
-
|
|
115
|
+
tags.push({
|
|
116
|
+
tag: "meta",
|
|
117
|
+
props: { name: "twitter:image", content: absolute(assets.twitter.href) },
|
|
118
|
+
});
|
|
119
|
+
tags.push({
|
|
120
|
+
tag: "meta",
|
|
121
|
+
props: { name: "twitter:card", content: "summary_large_image" },
|
|
122
|
+
});
|
|
87
123
|
}
|
|
88
124
|
return tags;
|
|
89
125
|
}
|
package/dist/appAssets.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appAssets.js","sourceRoot":"","sources":["../src/appAssets.ts"],"names":[],"mappings":"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","sourcesContent":["// Files that describe the app, found by name rather than configured.\n//\n// src/app/\n// favicon.ico <link rel=\"icon\">\n// icon.png <link rel=\"icon\">, and the manifest's icons\n// icon-192.png …as many as you like; sizes read from the name\n// apple-icon.png <link rel=\"apple-touch-icon\">\n// opengraph-image.png <meta property=\"og:image\">\n// twitter-image.png <meta name=\"twitter:image\">\n//\n// The same bargain as every other file in this directory: put it where it\n// belongs and it works, with nothing to register. Next established these names\n// and there is nothing to gain by inventing different ones — an app moving\n// between the two should not have to rename its favicon.\n//\n// They live in `app/` rather than `public/` on purpose. `public/` is \"serve\n// this verbatim\"; these are read as well as served — an icon's size decides\n// what goes in the manifest, and its presence decides what goes in the head.\n\nimport { existsSync, readdirSync } from 'node:fs'\n\n/** One found file, and where it will be served from. */\nexport interface AppAsset {\n /** The file's name in the app directory. */\n file: string\n /** The url it is served at. */\n href: string\n}\n\nexport interface AppAssets {\n favicon: AppAsset | null\n icons: AppAsset[]\n appleIcon: AppAsset | null\n openGraph: AppAsset | null\n twitter: AppAsset | null\n}\n\nconst IMAGE = /\\.(png|svg|jpg|jpeg|webp|gif|avif)$/i\n\n/**\n * Where these are served from.\n *\n * Their own directory rather than the output root, so an app that happens to\n * have a `public/icon.png` as well is not silently overwritten by one of\n * these, in either direction. The exception is the favicon, which browsers ask\n * for at `/favicon.ico` whatever any markup says.\n */\nexport const ASSET_BASE = '/_app'\n\nfunction assetsIn(dir: string, match: (name: string) => boolean): AppAsset[] {\n return readdirSync(dir)\n .filter(match)\n .sort()\n .map((file) => ({ file, href: `${ASSET_BASE}/${file}` }))\n}\n\n/** What the app declared by putting a file where it could be found. */\nexport function appAssets(appDir: string): AppAssets {\n if (!existsSync(appDir)) {\n return { favicon: null, icons: [], appleIcon: null, openGraph: null, twitter: null }\n }\n\n const names = readdirSync(appDir)\n const has = (name: string) => names.includes(name)\n\n return {\n // Served where a browser looks for it regardless of markup: a request for\n // /favicon.ico goes out whether or not a link tag exists, and answering it\n // from somewhere else means answering a 404 to the request that matters.\n favicon: has('favicon.ico') ? { file: 'favicon.ico', href: '/favicon.ico' } : null,\n icons: assetsIn(appDir, (name) => /^icon[-\\w]*/.test(name) && IMAGE.test(name)),\n appleIcon:\n assetsIn(appDir, (name) => /^apple-icon[-\\w]*/.test(name) && IMAGE.test(name))[0] ?? null,\n openGraph:\n assetsIn(appDir, (name) => /^opengraph-image[-\\w]*/.test(name) && IMAGE.test(name))[0] ?? null,\n twitter:\n assetsIn(appDir, (name) => /^twitter-image[-\\w]*/.test(name) && IMAGE.test(name))[0] ?? null,\n }\n}\n\nfunction typeOf(file: string): string {\n const extension = file.slice(file.lastIndexOf('.') + 1).toLowerCase()\n\n return extension === 'svg'\n ? 'image/svg+xml'\n : extension === 'jpg' || extension === 'jpeg'\n ? 'image/jpeg'\n : extension === 'ico'\n ? 'image/x-icon'\n : `image/${extension}`\n}\n\n/** The head tags these produce, as plain data the generated entry turns into elements. */\nexport function headTags(\n assets: AppAssets,\n origin?: string,\n): { tag: 'link' | 'meta'; props: Record<string, string> }[] {\n const tags: { tag: 'link' | 'meta'; props: Record<string, string> }[] = []\n\n if (assets.favicon) {\n tags.push({ tag: 'link', props: { rel: 'icon', href: assets.favicon.href, type: 'image/x-icon' } })\n }\n\n for (const icon of assets.icons) {\n tags.push({ tag: 'link', props: { rel: 'icon', href: icon.href, type: typeOf(icon.file) } })\n }\n\n if (assets.appleIcon) {\n tags.push({ tag: 'link', props: { rel: 'apple-touch-icon', href: assets.appleIcon.href } })\n }\n\n // Absolute when the app said where it lives. The og spec asks for an absolute\n // url and several crawlers still mean it — a relative one is read by some and\n // ignored by others, which is the worst of both. Relative is what is left\n // when nothing said, and is better than omitting the tag.\n const absolute = (href: string) => (origin ? new URL(href, origin).href : href)\n\n if (assets.openGraph) {\n tags.push({ tag: 'meta', props: { property: 'og:image', content: absolute(assets.openGraph.href) } })\n }\n\n if (assets.twitter) {\n tags.push({ tag: 'meta', props: { name: 'twitter:image', content: absolute(assets.twitter.href) } })\n tags.push({ tag: 'meta', props: { name: 'twitter:card', content: 'summary_large_image' } })\n }\n\n return tags\n}\n"]}
|
|
1
|
+
{"version":3,"file":"appAssets.js","sourceRoot":"","sources":["../src/appAssets.ts"],"names":[],"mappings":"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","sourcesContent":["// Files that describe the app, found by name rather than configured.\n//\n// src/app/\n// favicon.ico <link rel=\"icon\">\n// icon.png <link rel=\"icon\">, and the manifest's icons\n// icon-192.png …as many as you like; sizes read from the name\n// apple-icon.png <link rel=\"apple-touch-icon\">\n// opengraph-image.png <meta property=\"og:image\">\n// twitter-image.png <meta name=\"twitter:image\">\n//\n// The same bargain as every other file in this directory: put it where it\n// belongs and it works, with nothing to register. Next established these names\n// and there is nothing to gain by inventing different ones — an app moving\n// between the two should not have to rename its favicon.\n//\n// They live in `app/` rather than `public/` on purpose. `public/` is \"serve\n// this verbatim\"; these are read as well as served — an icon's size decides\n// what goes in the manifest, and its presence decides what goes in the head.\n\nimport { existsSync, readdirSync } from \"node:fs\";\n\n/** One found file, and where it will be served from. */\nexport interface AppAsset {\n /** The file's name in the app directory. */\n file: string;\n /** The url it is served at. */\n href: string;\n}\n\nexport interface AppAssets {\n favicon: AppAsset | null;\n icons: AppAsset[];\n appleIcon: AppAsset | null;\n openGraph: AppAsset | null;\n twitter: AppAsset | null;\n}\n\nconst IMAGE = /\\.(png|svg|jpg|jpeg|webp|gif|avif)$/i;\n\n/**\n * Where these are served from.\n *\n * Their own directory rather than the output root, so an app that happens to\n * have a `public/icon.png` as well is not silently overwritten by one of\n * these, in either direction. The exception is the favicon, which browsers ask\n * for at `/favicon.ico` whatever any markup says.\n */\nexport const ASSET_BASE = \"/_app\";\n\nfunction assetsIn(dir: string, match: (name: string) => boolean): AppAsset[] {\n return readdirSync(dir)\n .filter(match)\n .sort()\n .map((file) => ({ file, href: `${ASSET_BASE}/${file}` }));\n}\n\n/** What the app declared by putting a file where it could be found. */\nexport function appAssets(appDir: string): AppAssets {\n if (!existsSync(appDir)) {\n return {\n favicon: null,\n icons: [],\n appleIcon: null,\n openGraph: null,\n twitter: null,\n };\n }\n\n const names = readdirSync(appDir);\n const has = (name: string) => names.includes(name);\n\n return {\n // Served where a browser looks for it regardless of markup: a request for\n // /favicon.ico goes out whether or not a link tag exists, and answering it\n // from somewhere else means answering a 404 to the request that matters.\n favicon: has(\"favicon.ico\")\n ? { file: \"favicon.ico\", href: \"/favicon.ico\" }\n : null,\n icons: assetsIn(\n appDir,\n (name) => /^icon[-\\w]*/.test(name) && IMAGE.test(name),\n ),\n appleIcon:\n assetsIn(\n appDir,\n (name) => /^apple-icon[-\\w]*/.test(name) && IMAGE.test(name),\n )[0] ?? null,\n openGraph:\n assetsIn(\n appDir,\n (name) => /^opengraph-image[-\\w]*/.test(name) && IMAGE.test(name),\n )[0] ?? null,\n twitter:\n assetsIn(\n appDir,\n (name) => /^twitter-image[-\\w]*/.test(name) && IMAGE.test(name),\n )[0] ?? null,\n };\n}\n\nexport function typeOf(file: string): string {\n const extension = file.slice(file.lastIndexOf(\".\") + 1).toLowerCase();\n\n return extension === \"svg\"\n ? \"image/svg+xml\"\n : extension === \"jpg\" || extension === \"jpeg\"\n ? \"image/jpeg\"\n : extension === \"ico\"\n ? \"image/x-icon\"\n : `image/${extension}`;\n}\n\n/** The head tags these produce, as plain data the generated entry turns into elements. */\n/** Every asset found, favicon first: what the build copies and the dev server answers. */\nexport function allAppAssets(assets: AppAssets): AppAsset[] {\n return [\n ...(assets.favicon ? [assets.favicon] : []),\n ...assets.icons,\n ...(assets.appleIcon ? [assets.appleIcon] : []),\n ...(assets.openGraph ? [assets.openGraph] : []),\n ...(assets.twitter ? [assets.twitter] : []),\n ];\n}\n\nexport function headTags(\n assets: AppAssets,\n origin?: string,\n): { tag: \"link\" | \"meta\"; props: Record<string, string> }[] {\n const tags: { tag: \"link\" | \"meta\"; props: Record<string, string> }[] = [];\n\n if (assets.favicon) {\n tags.push({\n tag: \"link\",\n props: { rel: \"icon\", href: assets.favicon.href, type: \"image/x-icon\" },\n });\n }\n\n for (const icon of assets.icons) {\n tags.push({\n tag: \"link\",\n props: { rel: \"icon\", href: icon.href, type: typeOf(icon.file) },\n });\n }\n\n if (assets.appleIcon) {\n tags.push({\n tag: \"link\",\n props: { rel: \"apple-touch-icon\", href: assets.appleIcon.href },\n });\n }\n\n // Absolute when the app said where it lives. The og spec asks for an absolute\n // url and several crawlers still mean it — a relative one is read by some and\n // ignored by others, which is the worst of both. Relative is what is left\n // when nothing said, and is better than omitting the tag.\n const absolute = (href: string) =>\n origin ? new URL(href, origin).href : href;\n\n if (assets.openGraph) {\n tags.push({\n tag: \"meta\",\n props: { property: \"og:image\", content: absolute(assets.openGraph.href) },\n });\n }\n\n if (assets.twitter) {\n tags.push({\n tag: \"meta\",\n props: { name: \"twitter:image\", content: absolute(assets.twitter.href) },\n });\n tags.push({\n tag: \"meta\",\n props: { name: \"twitter:card\", content: \"summary_large_image\" },\n });\n }\n\n return tags;\n}\n"]}
|
package/dist/buildReport.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface ReportedApiRoute {
|
|
|
28
28
|
name: string;
|
|
29
29
|
type: string;
|
|
30
30
|
reason: string | null;
|
|
31
|
+
/** Stored, and froze a value that will not be the same tomorrow. Absent from older reports. */
|
|
32
|
+
warning?: string | null;
|
|
31
33
|
}
|
|
32
34
|
export interface BuildReport {
|
|
33
35
|
version: 1;
|
package/dist/buildReport.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildReport.js","sourceRoot":"","sources":["../src/buildReport.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,yEAAyE;AACzE,iEAAiE;AACjE,EAAE;AACF,yEAAyE;AACzE,6EAA6E;AAC7E,+CAA+C;
|
|
1
|
+
{"version":3,"file":"buildReport.js","sourceRoot":"","sources":["../src/buildReport.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,yEAAyE;AACzE,iEAAiE;AACjE,EAAE;AACF,yEAAyE;AACzE,6EAA6E;AAC7E,+CAA+C;AAsD/C,8EAA8E;AAC9E,MAAM,CAAC,MAAM,WAAW,GAAG,mBAAmB,CAAA;AAE9C,MAAM,UAAU,WAAW,CACzB,MAAuB,EACvB,IAAwB,EACxB,OAAO,GAAqB,EAAE;IAE9B,MAAM,KAAK,GAAG,CAAC,GAAG,KAAe,EAAE,EAAE,CACnC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM;QACnD,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAA;IAEnD,MAAM,MAAM,GAAgB;QAC1B,OAAO,EAAE,CAAC;QACV,MAAM;QACN,IAAI;QACJ,OAAO;QACP,MAAM,EAAE;YACN,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC;YACvB,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC;YACvB,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;YACzB,yEAAyE;YACzE,uEAAuE;YACvE,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,CAAC;SAClC;KACF,CAAA;IAED,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAA;AAC/C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,MAAuB;IAChD,MAAM,IAAI,GAA2B,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAA;IAE9F,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CACrB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAClF,CAAA;AACH,CAAC","sourcesContent":["// What the build decided, written down.\n//\n// The classification exists already — it is printed as the build runs, and\n// then it is gone. That is fine for a person watching a terminal and useless\n// for anything that wants to ask afterwards: a CI step asserting nothing\n// regressed, an editor, an agent being asked why a page is slow.\n//\n// So the same facts go to a file. Not a new computation and not a second\n// source of truth — the report is written from the results the build already\n// produced, in the same pass that prints them.\n\n/** One route, as the build left it. */\nexport interface ReportedRoute {\n url: string\n component: string\n /** frozen | shell | blocked | dynamic | error — see PrerenderResult. blocked and error failed the build. */\n type: string\n /** Why it is not frozen, in the words the build printed. */\n reason: string | null\n /** Something true and worth knowing that is not a failure. */\n warning: string | null\n /** A fact about how it was stored - \"no client components, so ships no javascript\". Absent from older reports. */\n note?: string | null\n /** Gzipped bytes of javascript this url makes the browser download. */\n clientJs: number | null\n}\n\n/** A server action, and whether anything checks who calls it. */\nexport interface ReportedAction {\n id: string\n name: string\n file: string\n /** Built by createActionClient, so its middleware ran. */\n client: boolean\n /** A read (GET) rather than a mutation. */\n query: boolean\n}\n\nexport interface ReportedApiRoute {\n url: string\n name: string\n type: string\n reason: string | null\n /** Stored, and froze a value that will not be the same tomorrow. Absent from older reports. */\n warning?: string | null\n}\n\nexport interface BuildReport {\n version: 1\n /** Routes that render, in the order the build reported them. */\n routes: ReportedRoute[]\n /** route.ts endpoints. */\n apis: ReportedApiRoute[]\n /** Every \"use server\" export the app registered. Absent from reports older than this field. */\n actions?: ReportedAction[]\n totals: {\n static: number\n partial: number\n dynamic: number\n failed: number\n }\n}\n\n/** The name the report is written under, inside the build's own directory. */\nexport const REPORT_FILE = 'build-report.json'\n\nexport function buildReport(\n routes: ReportedRoute[],\n apis: ReportedApiRoute[],\n actions: ReportedAction[] = [],\n): string {\n const count = (...types: string[]) =>\n routes.filter((r) => types.includes(r.type)).length +\n apis.filter((a) => types.includes(a.type)).length\n\n const report: BuildReport = {\n version: 1,\n routes,\n apis,\n actions,\n totals: {\n static: count('frozen'),\n partial: count('shell'),\n dynamic: count('dynamic'),\n // Blocked is refused, not dynamic: a page that painted nothing before it\n // read the request has no shell to store and the build did not finish.\n failed: count('error', 'blocked'),\n },\n }\n\n return JSON.stringify(report, null, 2) + '\\n'\n}\n\n/**\n * The routes worth asking about, most interesting first.\n *\n * \"Interesting\" is not a judgement about the app — it is the order someone\n * looking for a problem reads in. A failure first, then a page that could not\n * be stored at all, then one that ships a shell, then the ones that are fine.\n */\nexport function byInterest(routes: ReportedRoute[]): ReportedRoute[] {\n const rank: Record<string, number> = { error: 0, blocked: 1, shell: 2, dynamic: 3, frozen: 4 }\n\n return [...routes].sort(\n (a, b) => (rank[a.type] ?? 9) - (rank[b.type] ?? 9) || a.url.localeCompare(b.url),\n )\n}\n"]}
|
package/dist/files.js
CHANGED
|
@@ -26,12 +26,31 @@ export function prerenderedFrom(dir) {
|
|
|
26
26
|
* caught two exceptions before finding anything. Measured at 3,000 req/s
|
|
27
27
|
* against 28,000 for the same page read from memory.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* Contents are cached for the life of the process. A build's output does
|
|
30
|
+
* not change under a running server - a deploy restarts it, and a server
|
|
31
|
+
* that outlived a rebuild would be serving a stale engine against new
|
|
32
|
+
* pages anyway - so the first read is the only one. Under load the same
|
|
33
|
+
* page used to be opened, read and closed thousands of times a second,
|
|
34
|
+
* which a profile showed as the one cost on a stored page that was ours
|
|
35
|
+
* rather than React's. Capped by bytes, so a site with more pages than
|
|
36
|
+
* memory holds the hot ones and re-reads the rest.
|
|
33
37
|
*/
|
|
34
38
|
let present = null;
|
|
39
|
+
const CONTENT_CAP_BYTES = 64 * 1024 * 1024;
|
|
40
|
+
const contents = new Map();
|
|
41
|
+
let held = 0;
|
|
42
|
+
const remember = (name, text) => {
|
|
43
|
+
const size = text.length * 2;
|
|
44
|
+
if (size > CONTENT_CAP_BYTES / 4)
|
|
45
|
+
return;
|
|
46
|
+
while (held + size > CONTENT_CAP_BYTES && contents.size > 0) {
|
|
47
|
+
const oldest = contents.keys().next().value;
|
|
48
|
+
held -= contents.get(oldest).length * 2;
|
|
49
|
+
contents.delete(oldest);
|
|
50
|
+
}
|
|
51
|
+
contents.set(name, text);
|
|
52
|
+
held += size;
|
|
53
|
+
};
|
|
35
54
|
const listing = async () => {
|
|
36
55
|
present ??= readdir(dir, { recursive: true })
|
|
37
56
|
.then((names) => new Set(names.map((n) => String(n).replace(/\\/g, '/'))))
|
|
@@ -45,8 +64,13 @@ export function prerenderedFrom(dir) {
|
|
|
45
64
|
return null;
|
|
46
65
|
if (!(await listing()).has(name))
|
|
47
66
|
return null;
|
|
67
|
+
const cached = contents.get(name);
|
|
68
|
+
if (cached !== undefined)
|
|
69
|
+
return cached;
|
|
48
70
|
try {
|
|
49
|
-
|
|
71
|
+
const text = await readFile(join(dir, name), 'utf-8');
|
|
72
|
+
remember(name, text);
|
|
73
|
+
return text;
|
|
50
74
|
}
|
|
51
75
|
catch {
|
|
52
76
|
return null;
|