@rsc-kit/core 0.20.12 → 0.21.0
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.js +1 -1
- package/dist/apiPrerender.js.map +1 -1
- package/dist/appAssets.d.ts +6 -0
- package/dist/appAssets.js +34 -0
- package/dist/appAssets.js.map +1 -1
- package/dist/appleSplash.d.ts +10 -0
- package/dist/appleSplash.js +74 -0
- package/dist/appleSplash.js.map +1 -0
- package/dist/earlyHints.js +7 -2
- package/dist/earlyHints.js.map +1 -1
- package/dist/export.js +3 -1
- package/dist/export.js.map +1 -1
- package/dist/files.d.ts +0 -8
- package/dist/files.js +7 -18
- package/dist/files.js.map +1 -1
- package/dist/headers.d.ts +8 -0
- package/dist/headers.js +8 -0
- package/dist/headers.js.map +1 -1
- package/dist/host.d.ts +15 -0
- package/dist/host.js +133 -27
- package/dist/host.js.map +1 -1
- package/dist/js/Form.d.ts +47 -54
- package/dist/js/Form.js +41 -48
- package/dist/js/Form.js.map +1 -1
- package/dist/js/chunkPreload.d.ts +26 -0
- package/dist/js/chunkPreload.js +66 -0
- package/dist/js/chunkPreload.js.map +1 -0
- package/dist/js/installCapture.d.ts +15 -0
- package/dist/js/installCapture.js +17 -0
- package/dist/js/installCapture.js.map +1 -0
- package/dist/js/navigate.js +4 -0
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/useInstall.d.ts +15 -0
- package/dist/js/useInstall.js +102 -0
- package/dist/js/useInstall.js.map +1 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js.map +1 -1
- package/dist/metadata.d.ts +31 -0
- package/dist/metadata.js.map +1 -1
- package/dist/prerender.d.ts +24 -5
- package/dist/prerender.js +107 -19
- package/dist/prerender.js.map +1 -1
- package/dist/request.js +3 -2
- package/dist/request.js.map +1 -1
- package/dist/shellHead.js +4 -2
- package/dist/shellHead.js.map +1 -1
- package/dist/stableNames.d.ts +52 -0
- package/dist/stableNames.js +91 -0
- package/dist/stableNames.js.map +1 -0
- package/dist/vite.d.ts +5 -3
- package/dist/vite.js +215 -13
- package/dist/vite.js.map +1 -1
- package/dist/webManifest.d.ts +37 -0
- package/dist/webManifest.js +51 -6
- package/dist/webManifest.js.map +1 -1
- package/package.json +8 -4
package/dist/apiPrerender.js
CHANGED
|
@@ -139,7 +139,7 @@ export async function prerenderApiRoutes(engine, manifest, write) {
|
|
|
139
139
|
// A guarded route answers differently depending on who is asking, which is
|
|
140
140
|
// the whole purpose of the guard. Storing one answer and serving it to
|
|
141
141
|
// everyone is how a guard is silently removed.
|
|
142
|
-
if (route.middleware.length > 0) {
|
|
142
|
+
if (route.middleware.length > 0 || (route.hostMiddleware?.length ?? 0) > 0) {
|
|
143
143
|
said('dynamic', 'guarded by middleware');
|
|
144
144
|
continue;
|
|
145
145
|
}
|
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 { UNPROBED, 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 * `url` is on the list although the path is the key the answer is stored\n * under, because nobody reads it for the path: a handler written the Next way\n * reads `new URL(request.url).searchParams`, which the probe cannot see the\n * way it sees the awaited `searchParams` — and a stored answer marked as\n * varying with nothing would then be served to every query, a webhook's\n * verification handshake included. Reading the url also puts the build\n * machine's origin within reach of the answer. A route that wants the bare\n * url stored awaits `searchParams` instead; the table says so.\n */\nconst PER_CALLER = new Set([\n 'url',\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 // The engine's own way past the probe, for the url read that resolves\n // the awaited searchParams - booked to searchParams, not to url.\n if (property === UNPROBED) return target\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 const hint = touched.has('url') ? ' (await searchParams to read the query and keep the bare url stored)' : ''\n\n said('dynamic', 'reads the request — ' + [...touched].sort().join(', ') + hint)\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 refusal is an answer to the request the build sent - none - not to\n // the ones visitors will. A route that answers 403 with nothing read is\n // usually checking something the probe cannot see, and a stored 403 was\n // once every webhook handshake's answer. Left to run, and said so.\n if (response.status >= 400) {\n said('dynamic', `answered ${response.status} to the build — a refusal is not an answer to store`)\n continue\n }\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 ')}${route.source ? ' in ' + route.source : ''} — 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"]}
|
|
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 { UNPROBED, 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 * `url` is on the list although the path is the key the answer is stored\n * under, because nobody reads it for the path: a handler written the Next way\n * reads `new URL(request.url).searchParams`, which the probe cannot see the\n * way it sees the awaited `searchParams` — and a stored answer marked as\n * varying with nothing would then be served to every query, a webhook's\n * verification handshake included. Reading the url also puts the build\n * machine's origin within reach of the answer. A route that wants the bare\n * url stored awaits `searchParams` instead; the table says so.\n */\nconst PER_CALLER = new Set([\n 'url',\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 // The engine's own way past the probe, for the url read that resolves\n // the awaited searchParams - booked to searchParams, not to url.\n if (property === UNPROBED) return target\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 || (route.hostMiddleware?.length ?? 0) > 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 const hint = touched.has('url') ? ' (await searchParams to read the query and keep the bare url stored)' : ''\n\n said('dynamic', 'reads the request — ' + [...touched].sort().join(', ') + hint)\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 refusal is an answer to the request the build sent - none - not to\n // the ones visitors will. A route that answers 403 with nothing read is\n // usually checking something the probe cannot see, and a stored 403 was\n // once every webhook handshake's answer. Left to run, and said so.\n if (response.status >= 400) {\n said('dynamic', `answered ${response.status} to the build — a refusal is not an answer to store`)\n continue\n }\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 ')}${route.source ? ' in ' + route.source : ''} — 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
|
@@ -9,6 +9,12 @@ export interface AppAssets {
|
|
|
9
9
|
favicon: AppAsset | null;
|
|
10
10
|
icons: AppAsset[];
|
|
11
11
|
appleIcon: AppAsset | null;
|
|
12
|
+
/** For the manifest's screenshots: wide for desktop, narrow for a phone. */
|
|
13
|
+
screenshots: AppAsset[];
|
|
14
|
+
/** iOS launch screens, each with the media query its size maps to. */
|
|
15
|
+
appleSplash: (AppAsset & {
|
|
16
|
+
media: string | null;
|
|
17
|
+
})[];
|
|
12
18
|
openGraph: AppAsset | null;
|
|
13
19
|
twitter: AppAsset | null;
|
|
14
20
|
}
|
package/dist/appAssets.js
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
// icon.png <link rel="icon">, and the manifest's icons
|
|
6
6
|
// icon-192.png …as many as you like; sizes read from the name
|
|
7
7
|
// apple-icon.png <link rel="apple-touch-icon">
|
|
8
|
+
// screenshot-wide-1280x720.png the manifest's screenshots, for Chrome's
|
|
9
|
+
// screenshot-narrow-750x1334.png richer install sheet; size and form
|
|
10
|
+
// factor read from the name
|
|
11
|
+
// apple-splash-1179x2556.png <link rel="apple-touch-startup-image" media=…>
|
|
12
|
+
// …one per device and orientation; see appleSplash.ts
|
|
8
13
|
// opengraph-image.png <meta property="og:image">
|
|
9
14
|
// twitter-image.png <meta name="twitter:image">
|
|
10
15
|
//
|
|
@@ -17,6 +22,7 @@
|
|
|
17
22
|
// this verbatim"; these are read as well as served — an icon's size decides
|
|
18
23
|
// what goes in the manifest, and its presence decides what goes in the head.
|
|
19
24
|
import { existsSync, readdirSync } from "node:fs";
|
|
25
|
+
import { isAppleSplash, splashMedia } from "./appleSplash.js";
|
|
20
26
|
const IMAGE = /\.(png|svg|jpg|jpeg|webp|gif|avif)$/i;
|
|
21
27
|
/**
|
|
22
28
|
* Where these are served from.
|
|
@@ -40,6 +46,8 @@ export function appAssets(appDir) {
|
|
|
40
46
|
favicon: null,
|
|
41
47
|
icons: [],
|
|
42
48
|
appleIcon: null,
|
|
49
|
+
appleSplash: [],
|
|
50
|
+
screenshots: [],
|
|
43
51
|
openGraph: null,
|
|
44
52
|
twitter: null,
|
|
45
53
|
};
|
|
@@ -55,6 +63,11 @@ export function appAssets(appDir) {
|
|
|
55
63
|
: null,
|
|
56
64
|
icons: assetsIn(appDir, (name) => /^icon[-\w]*/.test(name) && IMAGE.test(name)),
|
|
57
65
|
appleIcon: assetsIn(appDir, (name) => /^apple-icon[-\w]*/.test(name) && IMAGE.test(name))[0] ?? null,
|
|
66
|
+
screenshots: assetsIn(appDir, (name) => /^screenshot-(wide|narrow)[-\w]*\.(png|jpe?g|webp)$/i.test(name)),
|
|
67
|
+
appleSplash: assetsIn(appDir, isAppleSplash).map((asset) => ({
|
|
68
|
+
...asset,
|
|
69
|
+
media: splashMedia(asset.file),
|
|
70
|
+
})),
|
|
58
71
|
openGraph: assetsIn(appDir, (name) => /^opengraph-image[-\w]*/.test(name) && IMAGE.test(name))[0] ?? null,
|
|
59
72
|
twitter: assetsIn(appDir, (name) => /^twitter-image[-\w]*/.test(name) && IMAGE.test(name))[0] ?? null,
|
|
60
73
|
};
|
|
@@ -76,6 +89,8 @@ export function allAppAssets(assets) {
|
|
|
76
89
|
...(assets.favicon ? [assets.favicon] : []),
|
|
77
90
|
...assets.icons,
|
|
78
91
|
...(assets.appleIcon ? [assets.appleIcon] : []),
|
|
92
|
+
...assets.appleSplash,
|
|
93
|
+
...assets.screenshots,
|
|
79
94
|
...(assets.openGraph ? [assets.openGraph] : []),
|
|
80
95
|
...(assets.twitter ? [assets.twitter] : []),
|
|
81
96
|
];
|
|
@@ -89,6 +104,10 @@ export function headTags(assets, origin) {
|
|
|
89
104
|
});
|
|
90
105
|
}
|
|
91
106
|
for (const icon of assets.icons) {
|
|
107
|
+
// A maskable icon is for a launcher, padded for its mask; as a tab's icon
|
|
108
|
+
// it would be the small one. It goes in the manifest and nowhere else.
|
|
109
|
+
if (/(^|-)maskable\b/i.test(icon.file))
|
|
110
|
+
continue;
|
|
92
111
|
tags.push({
|
|
93
112
|
tag: "link",
|
|
94
113
|
props: { rel: "icon", href: icon.href, type: typeOf(icon.file) },
|
|
@@ -100,6 +119,21 @@ export function headTags(assets, origin) {
|
|
|
100
119
|
props: { rel: "apple-touch-icon", href: assets.appleIcon.href },
|
|
101
120
|
});
|
|
102
121
|
}
|
|
122
|
+
// Only the sizes a device has: a query that matches nothing is bytes in
|
|
123
|
+
// every page for no one, and the build names those files instead.
|
|
124
|
+
const splashes = assets.appleSplash.filter((splash) => splash.media !== null);
|
|
125
|
+
for (const splash of splashes) {
|
|
126
|
+
tags.push({
|
|
127
|
+
tag: "link",
|
|
128
|
+
props: { rel: "apple-touch-startup-image", href: splash.href, media: splash.media },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Safari shows a launch screen only to an app launched from the home
|
|
132
|
+
// screen as an app. Whoever put these in app/ meant that.
|
|
133
|
+
if (splashes.length > 0) {
|
|
134
|
+
tags.push({ tag: "meta", props: { name: "mobile-web-app-capable", content: "yes" } });
|
|
135
|
+
tags.push({ tag: "meta", props: { name: "apple-mobile-web-app-capable", content: "yes" } });
|
|
136
|
+
}
|
|
103
137
|
// Absolute when the app said where it lives. The og spec asks for an absolute
|
|
104
138
|
// url and several crawlers still mean it — a relative one is read by some and
|
|
105
139
|
// ignored by others, which is the worst of both. Relative is what is left
|
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 {\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"]}
|
|
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// screenshot-wide-1280x720.png the manifest's screenshots, for Chrome's\n// screenshot-narrow-750x1334.png richer install sheet; size and form\n// factor read from the name\n// apple-splash-1179x2556.png <link rel=\"apple-touch-startup-image\" media=…>\n// …one per device and orientation; see appleSplash.ts\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\";\nimport { isAppleSplash, splashMedia } from \"./appleSplash.js\";\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 /** For the manifest's screenshots: wide for desktop, narrow for a phone. */\n screenshots: AppAsset[];\n /** iOS launch screens, each with the media query its size maps to. */\n appleSplash: (AppAsset & { media: string | 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 appleSplash: [],\n screenshots: [],\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 screenshots: assetsIn(appDir, (name) =>\n /^screenshot-(wide|narrow)[-\\w]*\\.(png|jpe?g|webp)$/i.test(name),\n ),\n appleSplash: assetsIn(appDir, isAppleSplash).map((asset) => ({\n ...asset,\n media: splashMedia(asset.file),\n })),\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.appleSplash,\n ...assets.screenshots,\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 // A maskable icon is for a launcher, padded for its mask; as a tab's icon\n // it would be the small one. It goes in the manifest and nowhere else.\n if (/(^|-)maskable\\b/i.test(icon.file)) continue;\n\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 // Only the sizes a device has: a query that matches nothing is bytes in\n // every page for no one, and the build names those files instead.\n const splashes = assets.appleSplash.filter((splash) => splash.media !== null);\n\n for (const splash of splashes) {\n tags.push({\n tag: \"link\",\n props: { rel: \"apple-touch-startup-image\", href: splash.href, media: splash.media! },\n });\n }\n\n // Safari shows a launch screen only to an app launched from the home\n // screen as an app. Whoever put these in app/ meant that.\n if (splashes.length > 0) {\n tags.push({ tag: \"meta\", props: { name: \"mobile-web-app-capable\", content: \"yes\" } });\n tags.push({ tag: \"meta\", props: { name: \"apple-mobile-web-app-capable\", content: \"yes\" } });\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"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Whether a file in app/ is meant as a launch screen. */
|
|
2
|
+
export declare function isAppleSplash(file: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* The media query a launch screen is shown behind, or null for a size no
|
|
5
|
+
* device in the table has. Landscape is the same device with the size turned
|
|
6
|
+
* round: iOS reports device-width and device-height in portrait either way.
|
|
7
|
+
*/
|
|
8
|
+
export declare function splashMedia(file: string): string | null;
|
|
9
|
+
/** The launch screens whose size matches no device, for the build to name. */
|
|
10
|
+
export declare function unknownSplashes(files: string[]): string[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// iOS launch screens, from files named by their size.
|
|
2
|
+
//
|
|
3
|
+
// src/app/
|
|
4
|
+
// apple-splash-1179x2556.png an iPhone 15, portrait
|
|
5
|
+
// apple-splash-2556x1179.png the same phone, landscape
|
|
6
|
+
//
|
|
7
|
+
// Safari ignores the manifest for a home-screen app's launch screen. It wants
|
|
8
|
+
// one image per device and orientation, each behind a media query naming the
|
|
9
|
+
// device's size and pixel ratio - and without a match it shows a blank
|
|
10
|
+
// screen. Writing those queries by hand is where this goes wrong: a pixel
|
|
11
|
+
// ratio off by one matches no device, silently.
|
|
12
|
+
//
|
|
13
|
+
// So the image carries its size in its name, the same bargain as
|
|
14
|
+
// `icon-192.png`, and the query comes from a table. Nothing opens the file:
|
|
15
|
+
// no decoding, no resizing, nothing at build, dev or runtime. The images are
|
|
16
|
+
// yours - make them however you like, at the sizes of the devices you care
|
|
17
|
+
// about. A size no device has is named by the build rather than dropped.
|
|
18
|
+
/**
|
|
19
|
+
* Portrait pixel size → screen. Current iPhones and iPads; one entry per
|
|
20
|
+
* distinct screen, however many models share it.
|
|
21
|
+
*/
|
|
22
|
+
const SCREENS = {
|
|
23
|
+
// iPhone
|
|
24
|
+
'1320x2868': { width: 440, height: 956, ratio: 3 }, // 16 Pro Max
|
|
25
|
+
'1206x2622': { width: 402, height: 874, ratio: 3 }, // 16 Pro
|
|
26
|
+
'1290x2796': { width: 430, height: 932, ratio: 3 }, // 14 Pro Max, 15 Plus / Pro Max, 16 Plus
|
|
27
|
+
'1179x2556': { width: 393, height: 852, ratio: 3 }, // 14 Pro, 15, 15 Pro, 16
|
|
28
|
+
'1284x2778': { width: 428, height: 926, ratio: 3 }, // 12 / 13 Pro Max, 14 Plus
|
|
29
|
+
'1170x2532': { width: 390, height: 844, ratio: 3 }, // 12, 12 Pro, 13, 13 Pro, 14
|
|
30
|
+
'1242x2688': { width: 414, height: 896, ratio: 3 }, // XS Max, 11 Pro Max
|
|
31
|
+
'1125x2436': { width: 375, height: 812, ratio: 3 }, // X, XS, 11 Pro, 12 mini, 13 mini
|
|
32
|
+
'828x1792': { width: 414, height: 896, ratio: 2 }, // XR, 11
|
|
33
|
+
'1242x2208': { width: 414, height: 736, ratio: 3 }, // 6 / 7 / 8 Plus
|
|
34
|
+
'750x1334': { width: 375, height: 667, ratio: 2 }, // 6 / 7 / 8, SE 2nd and 3rd
|
|
35
|
+
'640x1136': { width: 320, height: 568, ratio: 2 }, // SE 1st
|
|
36
|
+
// iPad
|
|
37
|
+
'2064x2752': { width: 1032, height: 1376, ratio: 2 }, // Pro 13" (M4)
|
|
38
|
+
'2048x2732': { width: 1024, height: 1366, ratio: 2 }, // Pro 12.9"
|
|
39
|
+
'1668x2420': { width: 834, height: 1210, ratio: 2 }, // Pro 11" (M4)
|
|
40
|
+
'1668x2388': { width: 834, height: 1194, ratio: 2 }, // Pro 11"
|
|
41
|
+
'1640x2360': { width: 820, height: 1180, ratio: 2 }, // Air 10.9", 10th generation
|
|
42
|
+
'1668x2224': { width: 834, height: 1112, ratio: 2 }, // Pro 10.5", Air 3rd
|
|
43
|
+
'1620x2160': { width: 810, height: 1080, ratio: 2 }, // 10.2"
|
|
44
|
+
'1488x2266': { width: 744, height: 1133, ratio: 2 }, // mini 6th
|
|
45
|
+
'1536x2048': { width: 768, height: 1024, ratio: 2 }, // mini, Air, 9.7"
|
|
46
|
+
};
|
|
47
|
+
const NAME = /^apple-splash-(\d{3,4})x(\d{3,4})\.(png|jpe?g)$/i;
|
|
48
|
+
/** Whether a file in app/ is meant as a launch screen. */
|
|
49
|
+
export function isAppleSplash(file) {
|
|
50
|
+
return NAME.test(file);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The media query a launch screen is shown behind, or null for a size no
|
|
54
|
+
* device in the table has. Landscape is the same device with the size turned
|
|
55
|
+
* round: iOS reports device-width and device-height in portrait either way.
|
|
56
|
+
*/
|
|
57
|
+
export function splashMedia(file) {
|
|
58
|
+
const match = NAME.exec(file);
|
|
59
|
+
if (!match)
|
|
60
|
+
return null;
|
|
61
|
+
const w = Number(match[1]);
|
|
62
|
+
const h = Number(match[2]);
|
|
63
|
+
const landscape = w > h;
|
|
64
|
+
const screen = SCREENS[landscape ? `${h}x${w}` : `${w}x${h}`];
|
|
65
|
+
if (!screen)
|
|
66
|
+
return null;
|
|
67
|
+
return (`screen and (device-width: ${screen.width}px) and (device-height: ${screen.height}px)` +
|
|
68
|
+
` and (-webkit-device-pixel-ratio: ${screen.ratio}) and (orientation: ${landscape ? 'landscape' : 'portrait'})`);
|
|
69
|
+
}
|
|
70
|
+
/** The launch screens whose size matches no device, for the build to name. */
|
|
71
|
+
export function unknownSplashes(files) {
|
|
72
|
+
return files.filter((file) => isAppleSplash(file) && splashMedia(file) === null);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=appleSplash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appleSplash.js","sourceRoot":"","sources":["../src/appleSplash.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,aAAa;AACb,0DAA0D;AAC1D,6DAA6D;AAC7D,EAAE;AACF,8EAA8E;AAC9E,6EAA6E;AAC7E,uEAAuE;AACvE,0EAA0E;AAC1E,gDAAgD;AAChD,EAAE;AACF,iEAAiE;AACjE,4EAA4E;AAC5E,6EAA6E;AAC7E,2EAA2E;AAC3E,yEAAyE;AASzE;;;GAGG;AACH,MAAM,OAAO,GAA2B;IACtC,SAAS;IACT,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,aAAa;IACjE,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,SAAS;IAC7D,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,yCAAyC;IAC7F,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,yBAAyB;IAC7E,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,2BAA2B;IAC/E,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,6BAA6B;IACjF,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,qBAAqB;IACzE,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,kCAAkC;IACtF,UAAU,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,SAAS;IAC5D,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,iBAAiB;IACrE,UAAU,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,4BAA4B;IAC/E,UAAU,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,SAAS;IAC5D,OAAO;IACP,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,eAAe;IACrE,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,YAAY;IAClE,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,eAAe;IACpE,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,UAAU;IAC/D,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,6BAA6B;IAClF,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,qBAAqB;IAC1E,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,QAAQ;IAC7D,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,WAAW;IAChE,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,kBAAkB;CACxE,CAAA;AAED,MAAM,IAAI,GAAG,kDAAkD,CAAA;AAE/D,0DAA0D;AAC1D,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACxB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAE7B,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IAEvB,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1B,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1B,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,CAAA;IACvB,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAE7D,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAExB,OAAO,CACL,6BAA6B,MAAM,CAAC,KAAK,2BAA2B,MAAM,CAAC,MAAM,KAAK;QACtF,qCAAqC,MAAM,CAAC,KAAK,uBAAuB,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,GAAG,CAChH,CAAA;AACH,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,eAAe,CAAC,KAAe;IAC7C,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAA;AAClF,CAAC","sourcesContent":["// iOS launch screens, from files named by their size.\n//\n// src/app/\n// apple-splash-1179x2556.png an iPhone 15, portrait\n// apple-splash-2556x1179.png the same phone, landscape\n//\n// Safari ignores the manifest for a home-screen app's launch screen. It wants\n// one image per device and orientation, each behind a media query naming the\n// device's size and pixel ratio - and without a match it shows a blank\n// screen. Writing those queries by hand is where this goes wrong: a pixel\n// ratio off by one matches no device, silently.\n//\n// So the image carries its size in its name, the same bargain as\n// `icon-192.png`, and the query comes from a table. Nothing opens the file:\n// no decoding, no resizing, nothing at build, dev or runtime. The images are\n// yours - make them however you like, at the sizes of the devices you care\n// about. A size no device has is named by the build rather than dropped.\n\n/** A device's screen: its size in css pixels, portrait, and its pixel ratio. */\ninterface Screen {\n width: number\n height: number\n ratio: number\n}\n\n/**\n * Portrait pixel size → screen. Current iPhones and iPads; one entry per\n * distinct screen, however many models share it.\n */\nconst SCREENS: Record<string, Screen> = {\n // iPhone\n '1320x2868': { width: 440, height: 956, ratio: 3 }, // 16 Pro Max\n '1206x2622': { width: 402, height: 874, ratio: 3 }, // 16 Pro\n '1290x2796': { width: 430, height: 932, ratio: 3 }, // 14 Pro Max, 15 Plus / Pro Max, 16 Plus\n '1179x2556': { width: 393, height: 852, ratio: 3 }, // 14 Pro, 15, 15 Pro, 16\n '1284x2778': { width: 428, height: 926, ratio: 3 }, // 12 / 13 Pro Max, 14 Plus\n '1170x2532': { width: 390, height: 844, ratio: 3 }, // 12, 12 Pro, 13, 13 Pro, 14\n '1242x2688': { width: 414, height: 896, ratio: 3 }, // XS Max, 11 Pro Max\n '1125x2436': { width: 375, height: 812, ratio: 3 }, // X, XS, 11 Pro, 12 mini, 13 mini\n '828x1792': { width: 414, height: 896, ratio: 2 }, // XR, 11\n '1242x2208': { width: 414, height: 736, ratio: 3 }, // 6 / 7 / 8 Plus\n '750x1334': { width: 375, height: 667, ratio: 2 }, // 6 / 7 / 8, SE 2nd and 3rd\n '640x1136': { width: 320, height: 568, ratio: 2 }, // SE 1st\n // iPad\n '2064x2752': { width: 1032, height: 1376, ratio: 2 }, // Pro 13\" (M4)\n '2048x2732': { width: 1024, height: 1366, ratio: 2 }, // Pro 12.9\"\n '1668x2420': { width: 834, height: 1210, ratio: 2 }, // Pro 11\" (M4)\n '1668x2388': { width: 834, height: 1194, ratio: 2 }, // Pro 11\"\n '1640x2360': { width: 820, height: 1180, ratio: 2 }, // Air 10.9\", 10th generation\n '1668x2224': { width: 834, height: 1112, ratio: 2 }, // Pro 10.5\", Air 3rd\n '1620x2160': { width: 810, height: 1080, ratio: 2 }, // 10.2\"\n '1488x2266': { width: 744, height: 1133, ratio: 2 }, // mini 6th\n '1536x2048': { width: 768, height: 1024, ratio: 2 }, // mini, Air, 9.7\"\n}\n\nconst NAME = /^apple-splash-(\\d{3,4})x(\\d{3,4})\\.(png|jpe?g)$/i\n\n/** Whether a file in app/ is meant as a launch screen. */\nexport function isAppleSplash(file: string): boolean {\n return NAME.test(file)\n}\n\n/**\n * The media query a launch screen is shown behind, or null for a size no\n * device in the table has. Landscape is the same device with the size turned\n * round: iOS reports device-width and device-height in portrait either way.\n */\nexport function splashMedia(file: string): string | null {\n const match = NAME.exec(file)\n\n if (!match) return null\n\n const w = Number(match[1])\n const h = Number(match[2])\n const landscape = w > h\n const screen = SCREENS[landscape ? `${h}x${w}` : `${w}x${h}`]\n\n if (!screen) return null\n\n return (\n `screen and (device-width: ${screen.width}px) and (device-height: ${screen.height}px)` +\n ` and (-webkit-device-pixel-ratio: ${screen.ratio}) and (orientation: ${landscape ? 'landscape' : 'portrait'})`\n )\n}\n\n/** The launch screens whose size matches no device, for the build to name. */\nexport function unknownSplashes(files: string[]): string[] {\n return files.filter((file) => isAppleSplash(file) && splashMedia(file) === null)\n}\n"]}
|
package/dist/earlyHints.js
CHANGED
|
@@ -40,8 +40,13 @@ export function criticalAssetsOf(html) {
|
|
|
40
40
|
const href = ATTR('href', tag);
|
|
41
41
|
if (!href || !href.startsWith('/'))
|
|
42
42
|
continue;
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// A stylesheet kept only for print is one the document has inlined:
|
|
44
|
+
// hinting it would fetch at the highest priority what the page asked for
|
|
45
|
+
// at the lowest.
|
|
46
|
+
if (rel === 'stylesheet') {
|
|
47
|
+
if (ATTR('media', tag)?.toLowerCase() !== 'print')
|
|
48
|
+
found.styles.push(href);
|
|
49
|
+
}
|
|
45
50
|
else if (rel === 'preload' && ATTR('as', tag)?.toLowerCase() === 'font')
|
|
46
51
|
found.fonts.push(href);
|
|
47
52
|
}
|
package/dist/earlyHints.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"earlyHints.js","sourceRoot":"","sources":["../src/earlyHints.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,yEAAyE;AACzE,wEAAwE;AACxE,sEAAsE;AACtE,yEAAyE;AACzE,oBAAoB;AACpB,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,0EAA0E;AAC1E,4EAA4E;AAC5E,4EAA4E;AAC5E,+DAA+D;AAW/D,0EAA0E;AAC1E,MAAM,UAAU,UAAU,CAAC,MAAsB;IAC/C,MAAM,KAAK,GAAa,EAAE,CAAA;IAE1B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,0BAA0B,CAAC,CAAA;IAChF,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,sBAAsB,CAAC,CAAA;IAC7E,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,sCAAsC,CAAC,CAAA;IAE3F,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC;AAED,MAAM,UAAU,GAAG,MAAM,CAAA;AACzB,MAAM,IAAI,GAAG,gBAAgB,CAAA;AAC7B,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,GAAW,EAAiB,EAAE;IACxD,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,IAAI,mBAAmB,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAEtE,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAChC,CAAC,CAAA;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAA;IACtC,MAAM,KAAK,GAAmB,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAA;IAEpE,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,WAAW,EAAE,CAAA;QAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;QAE9B,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAQ;QAE5C,IAAI,GAAG,KAAK,YAAY;
|
|
1
|
+
{"version":3,"file":"earlyHints.js","sourceRoot":"","sources":["../src/earlyHints.ts"],"names":[],"mappings":"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","sourcesContent":["// The Link header a document answers with, for a CDN to send ahead as 103\n// Early Hints - so the stylesheet, the client entry and the fonts are on\n// their way while the HTML is still being written. Cloudflare turns the\n// header into hints and caches them at the edge for the next visitor;\n// anything else passes it through, where a browser still reads it as the\n// document arrives.\n//\n// The critical set and nothing more: a hint promotes whatever it names, and\n// naming every chunk demotes the document behind them. The stylesheet and\n// the client entry come from the build's own manifest, the same names the\n// document carries. The fonts are the app's - preloaded in its layout - and\n// are read from a stored document's own head; a rendered document's head is\n// not known until it is rendered, after its headers have gone.\n\nexport interface CriticalAssets {\n /** Stylesheets the document links: `<link rel=\"stylesheet\">`. */\n styles: string[];\n /** The client entry: `<script type=\"module\">`'s import. */\n modules: string[];\n /** Fonts the document preloads: `<link rel=\"preload\" as=\"font\">`. */\n fonts: string[];\n}\n\n/** One `Link` header value for the set. Empty string for an empty set. */\nexport function linkHeader(assets: CriticalAssets): string {\n const parts: string[] = []\n\n for (const href of assets.styles) parts.push(`<${href}>; rel=preload; as=style`)\n for (const href of assets.modules) parts.push(`<${href}>; rel=modulepreload`)\n for (const href of assets.fonts) parts.push(`<${href}>; rel=preload; as=font; crossorigin`)\n\n return parts.join(', ')\n}\n\nconst HEAD_LIMIT = 16_384\nconst LINK = /<link\\s[^>]*>/g\nconst ATTR = (name: string, tag: string): string | null => {\n const match = new RegExp(`\\\\b${name}=[\"']([^\"']*)[\"']`, 'i').exec(tag)\n\n return match ? match[1] : null\n}\n\n/**\n * What a stored document's head names: its stylesheets and preloaded fonts.\n * The first 16 KB, which is the head of any document this build writes.\n */\nexport function criticalAssetsOf(html: string): CriticalAssets {\n const head = html.slice(0, HEAD_LIMIT)\n const found: CriticalAssets = { styles: [], modules: [], fonts: [] }\n\n for (const tag of head.match(LINK) ?? []) {\n const rel = ATTR('rel', tag)?.toLowerCase()\n const href = ATTR('href', tag)\n\n if (!href || !href.startsWith('/')) continue\n\n // A stylesheet kept only for print is one the document has inlined:\n // hinting it would fetch at the highest priority what the page asked for\n // at the lowest.\n if (rel === 'stylesheet') {\n if (ATTR('media', tag)?.toLowerCase() !== 'print') found.styles.push(href)\n }\n else if (rel === 'preload' && ATTR('as', tag)?.toLowerCase() === 'font') found.fonts.push(href)\n }\n\n // The bootstrap script is the last thing in the body, after the shell; its\n // import names the entry. The tail, then, and the head for a short page.\n const entry = /import\\([\"'](\\/[^\"']+\\.js)[\"']\\)/.exec(html.slice(-4096)) ?? /import\\([\"'](\\/[^\"']+\\.js)[\"']\\)/.exec(head)\n\n if (entry) found.modules.push(entry[1])\n\n return found\n}\n\n/**\n * One set over another: the build's names, for a document whose own are not\n * known.\n *\n * `primary` is null when nothing was read - a document rendered per request,\n * whose head does not exist until after its headers have gone. A document\n * that WAS read is the authority on itself, including when the answer is\n * \"nothing\": a stored page with no client component names no module, and\n * hinting the build's entry made the browser fetch 82 KB of runtime that\n * the page never runs - the whole claim of a route that ships no\n * JavaScript, undone by its own headers. Per field it was worse still: a\n * page with its stylesheet inlined was hinted the build's stylesheet too.\n */\nexport function mergeAssets(primary: CriticalAssets | null, fallback: CriticalAssets | null): CriticalAssets {\n return primary ?? fallback ?? EMPTY\n}\n\nconst EMPTY: CriticalAssets = { styles: [], modules: [], fonts: [] }\n"]}
|
package/dist/export.js
CHANGED
|
@@ -43,7 +43,9 @@ export async function exportSite(options) {
|
|
|
43
43
|
// and a static host serves without running anything. Exporting one publishes
|
|
44
44
|
// the page to whoever asks, which is the opposite of what the guard said.
|
|
45
45
|
const guarded = new Set(manifest.routes
|
|
46
|
-
|
|
46
|
+
// The host's middleware too: `export const middleware = ['auth']` is a
|
|
47
|
+
// guard a static host cannot run either.
|
|
48
|
+
.filter((route) => route.middleware?.length || route.hostMiddleware?.length)
|
|
47
49
|
.map((route) => route.component));
|
|
48
50
|
const refused = results
|
|
49
51
|
.filter((r) => r.type !== 'frozen' || guarded.has(r.component))
|
package/dist/export.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export.js","sourceRoot":"","sources":["../src/export.ts"],"names":[],"mappings":"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","sourcesContent":["// Writing the whole site out as files, for a host that only serves files.\n//\n// Everything a server was doing has to have happened already, which makes the\n// refusals the important part. A site exported with a route missing, or with a\n// shell nothing will ever fill, does not fail at build time and does not fail\n// on load — it serves a page that sits there, or a 404 for a url that worked\n// yesterday. So anything that is not fully static stops the export and says\n// which routes and why.\n//\n// Layout is a directory per route with an index inside it, so urls stay\n// extensionless, and the Flight payload sits beside it under a filename the\n// client was built to ask for.\n//\n// out/index.html /\n// out/index.rsc\n// out/docs/index.html /docs\n// out/docs/index.rsc\n// out/assets/… the browser bundle\n\nimport { pathKey } from './prerender.js'\nimport type { PrerenderResult } from './prerender.js'\nimport type { RouteManifest } from './manifest.js'\n\nexport interface ExportOptions {\n /** What prerender() decided, so this can refuse what it cannot serve. */\n results: PrerenderResult[]\n /** Reads what prerender() wrote. `prerenderedFrom` in `@rsc-kit/core/files`. */\n read: (name: string) => Promise<string | null> | string | null\n /**\n * Writes the site, keyed by its url path — `docs/index.html`, not an\n * absolute one. `writeTo` in `@rsc-kit/core/files` puts those under a\n * directory; a deploy that uploads straight to a bucket writes keys.\n */\n write: (path: string, contents: string) => Promise<void> | void\n /** The route table, for what the build decided about payload urls. */\n manifest: RouteManifest\n /**\n * Brings the browser bundle along, if this export has to carry it.\n *\n * A callback rather than a directory pair: copying a tree of built files is\n * a filesystem operation by nature, and a deploy that uploads them has its\n * own tooling for it. `copyAssets` in `@rsc-kit/core/files` does the local one.\n */\n assets?: () => Promise<void> | void\n /**\n * Export anyway, leaving out whatever is not static.\n *\n * The result is a site with holes in it, which is sometimes what you want\n * while moving an app towards being exportable. It is never the default,\n * because the holes are 404s at urls that worked before.\n */\n force?: boolean\n}\n\nexport class NotExportable extends Error {\n constructor(public readonly refused: PrerenderResult[]) {\n super(\n 'This site cannot be exported as it is. A static host runs nothing, so these routes have ' +\n 'no way to finish rendering:\\n\\n' +\n refused.map((r) => ` ${r.url} — ${r.reason ?? describe(r.type)}`).join('\\n') +\n '\\n\\nMake them static, or pass force to export the rest and leave these out.',\n )\n this.name = 'NotExportable'\n }\n}\n\nfunction describe(type: PrerenderResult['type']): string {\n if (type === 'shell') {\n // Worth spelling out: a shell looks like a working page in the build\n // output, and on a static host it is a page that loads and stays empty.\n return 'only a shell was stored, and nothing on a static host will fill it'\n }\n\n return 'failed to render'\n}\n\nexport async function exportSite(options: ExportOptions): Promise<{ pages: number; refused: PrerenderResult[] }> {\n const { results, read, write, manifest, assets, force = false } = options\n\n // A guarded route is frozen on purpose — the content is the same for\n // everyone allowed to see it — but the guard runs when the page is served,\n // and a static host serves without running anything. Exporting one publishes\n // the page to whoever asks, which is the opposite of what the guard said.\n const guarded = new Set(\n (manifest.routes as { component: string; middleware?: string[] }[])\n .filter((route) => route.middleware?.length)\n .map((route) => route.component),\n )\n\n const refused = results\n .filter((r) => r.type !== 'frozen' || guarded.has(r.component))\n .map((r) =>\n r.type === 'frozen' && guarded.has(r.component)\n ? { ...r, reason: 'guarded, and a static host has nothing to run the guard' }\n : r,\n )\n\n if (refused.length > 0 && !force) throw new NotExportable(refused)\n\n // The client has to ask for payloads by url, because a static host cannot\n // read the header that would otherwise select one. That is decided by the\n // build, so a site exported from a server build ships a client asking the\n // wrong way — every navigation silently falls back to a full page load.\n const payloadName = manifest.build?.payloadName\n\n if (!payloadName) {\n throw new Error(\n 'This build was made for a server, so its client asks for payloads with a header rather ' +\n \"than by url. Build with output: 'export' before exporting.\",\n )\n }\n\n const copy = async (name: string, into: string) => {\n const contents = await read(name)\n\n // Missing where the results said it would be: the prerender output and the\n // results it returned disagree, which is not something to paper over with\n // a half-written site.\n if (contents === null) {\n throw new Error(`Nothing to export at ${name}. Prerender and export must read the same output.`)\n }\n\n await write(into, contents)\n }\n\n let pages = 0\n\n for (const result of results) {\n if (result.type !== 'frozen') continue\n\n const key = pathKey(result.url)\n // The root is the out dir itself; everything else is a directory with an\n // index, so /docs stays /docs rather than becoming /docs.html.\n const dir = key === 'index' ? '' : `${key}/`\n\n // A route that only redirects is frozen as its answer rather than as a\n // page, so there is no .html to copy — and reaching for one threw, which\n // is why exporting an app with a redirect in it failed outright.\n //\n // A meta refresh because it is the one redirect every static host performs\n // without being configured; `_redirects` and its equivalents are per-host\n // files this has no business writing. The canonical link is what keeps a\n // crawler pointed at the destination rather than at this page.\n const redirect = await read(`${key}.redirect.json`)\n\n if (redirect !== null) {\n const { location } = JSON.parse(redirect) as { status: number; location: string }\n const escaped = location.replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<')\n\n await write(\n `${dir}index.html`,\n `<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">` +\n `<meta http-equiv=\"refresh\" content=\"0; url=${escaped}\">` +\n `<link rel=\"canonical\" href=\"${escaped}\">` +\n `<title>Redirecting</title></head>` +\n `<body><a href=\"${escaped}\">${escaped}</a></body></html>\\n`,\n )\n\n pages++\n continue\n }\n\n await copy(`${key}.html`, `${dir}index.html`)\n await copy(`${key}.flight`, `${dir}${payloadName}`)\n\n // One file per depth a client might already hold, addressed by name\n // because a file server cannot vary on a header. Without these every\n // navigation is a whole document, which replaces the root and unmounts\n // the pages retained behind it — so going back does not restore the form\n // you were filling in.\n for (let depth = 1; ; depth++) {\n const variant = await read(`${key}.seg${depth}.flight`)\n\n if (variant === null) break\n\n await write(`${dir}${payloadName.replace(/^index\\./, `index.seg${depth}.`)}`, variant)\n }\n\n pages++\n }\n\n await assets?.()\n\n return { pages, refused }\n}\n\n\n"]}
|
|
1
|
+
{"version":3,"file":"export.js","sourceRoot":"","sources":["../src/export.ts"],"names":[],"mappings":"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","sourcesContent":["// Writing the whole site out as files, for a host that only serves files.\n//\n// Everything a server was doing has to have happened already, which makes the\n// refusals the important part. A site exported with a route missing, or with a\n// shell nothing will ever fill, does not fail at build time and does not fail\n// on load — it serves a page that sits there, or a 404 for a url that worked\n// yesterday. So anything that is not fully static stops the export and says\n// which routes and why.\n//\n// Layout is a directory per route with an index inside it, so urls stay\n// extensionless, and the Flight payload sits beside it under a filename the\n// client was built to ask for.\n//\n// out/index.html /\n// out/index.rsc\n// out/docs/index.html /docs\n// out/docs/index.rsc\n// out/assets/… the browser bundle\n\nimport { pathKey } from './prerender.js'\nimport type { PrerenderResult } from './prerender.js'\nimport type { RouteManifest } from './manifest.js'\n\nexport interface ExportOptions {\n /** What prerender() decided, so this can refuse what it cannot serve. */\n results: PrerenderResult[]\n /** Reads what prerender() wrote. `prerenderedFrom` in `@rsc-kit/core/files`. */\n read: (name: string) => Promise<string | null> | string | null\n /**\n * Writes the site, keyed by its url path — `docs/index.html`, not an\n * absolute one. `writeTo` in `@rsc-kit/core/files` puts those under a\n * directory; a deploy that uploads straight to a bucket writes keys.\n */\n write: (path: string, contents: string) => Promise<void> | void\n /** The route table, for what the build decided about payload urls. */\n manifest: RouteManifest\n /**\n * Brings the browser bundle along, if this export has to carry it.\n *\n * A callback rather than a directory pair: copying a tree of built files is\n * a filesystem operation by nature, and a deploy that uploads them has its\n * own tooling for it. `copyAssets` in `@rsc-kit/core/files` does the local one.\n */\n assets?: () => Promise<void> | void\n /**\n * Export anyway, leaving out whatever is not static.\n *\n * The result is a site with holes in it, which is sometimes what you want\n * while moving an app towards being exportable. It is never the default,\n * because the holes are 404s at urls that worked before.\n */\n force?: boolean\n}\n\nexport class NotExportable extends Error {\n constructor(public readonly refused: PrerenderResult[]) {\n super(\n 'This site cannot be exported as it is. A static host runs nothing, so these routes have ' +\n 'no way to finish rendering:\\n\\n' +\n refused.map((r) => ` ${r.url} — ${r.reason ?? describe(r.type)}`).join('\\n') +\n '\\n\\nMake them static, or pass force to export the rest and leave these out.',\n )\n this.name = 'NotExportable'\n }\n}\n\nfunction describe(type: PrerenderResult['type']): string {\n if (type === 'shell') {\n // Worth spelling out: a shell looks like a working page in the build\n // output, and on a static host it is a page that loads and stays empty.\n return 'only a shell was stored, and nothing on a static host will fill it'\n }\n\n return 'failed to render'\n}\n\nexport async function exportSite(options: ExportOptions): Promise<{ pages: number; refused: PrerenderResult[] }> {\n const { results, read, write, manifest, assets, force = false } = options\n\n // A guarded route is frozen on purpose — the content is the same for\n // everyone allowed to see it — but the guard runs when the page is served,\n // and a static host serves without running anything. Exporting one publishes\n // the page to whoever asks, which is the opposite of what the guard said.\n const guarded = new Set(\n (manifest.routes as { component: string; middleware?: string[]; hostMiddleware?: string[] }[])\n // The host's middleware too: `export const middleware = ['auth']` is a\n // guard a static host cannot run either.\n .filter((route) => route.middleware?.length || route.hostMiddleware?.length)\n .map((route) => route.component),\n )\n\n const refused = results\n .filter((r) => r.type !== 'frozen' || guarded.has(r.component))\n .map((r) =>\n r.type === 'frozen' && guarded.has(r.component)\n ? { ...r, reason: 'guarded, and a static host has nothing to run the guard' }\n : r,\n )\n\n if (refused.length > 0 && !force) throw new NotExportable(refused)\n\n // The client has to ask for payloads by url, because a static host cannot\n // read the header that would otherwise select one. That is decided by the\n // build, so a site exported from a server build ships a client asking the\n // wrong way — every navigation silently falls back to a full page load.\n const payloadName = manifest.build?.payloadName\n\n if (!payloadName) {\n throw new Error(\n 'This build was made for a server, so its client asks for payloads with a header rather ' +\n \"than by url. Build with output: 'export' before exporting.\",\n )\n }\n\n const copy = async (name: string, into: string) => {\n const contents = await read(name)\n\n // Missing where the results said it would be: the prerender output and the\n // results it returned disagree, which is not something to paper over with\n // a half-written site.\n if (contents === null) {\n throw new Error(`Nothing to export at ${name}. Prerender and export must read the same output.`)\n }\n\n await write(into, contents)\n }\n\n let pages = 0\n\n for (const result of results) {\n if (result.type !== 'frozen') continue\n\n const key = pathKey(result.url)\n // The root is the out dir itself; everything else is a directory with an\n // index, so /docs stays /docs rather than becoming /docs.html.\n const dir = key === 'index' ? '' : `${key}/`\n\n // A route that only redirects is frozen as its answer rather than as a\n // page, so there is no .html to copy — and reaching for one threw, which\n // is why exporting an app with a redirect in it failed outright.\n //\n // A meta refresh because it is the one redirect every static host performs\n // without being configured; `_redirects` and its equivalents are per-host\n // files this has no business writing. The canonical link is what keeps a\n // crawler pointed at the destination rather than at this page.\n const redirect = await read(`${key}.redirect.json`)\n\n if (redirect !== null) {\n const { location } = JSON.parse(redirect) as { status: number; location: string }\n const escaped = location.replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<')\n\n await write(\n `${dir}index.html`,\n `<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">` +\n `<meta http-equiv=\"refresh\" content=\"0; url=${escaped}\">` +\n `<link rel=\"canonical\" href=\"${escaped}\">` +\n `<title>Redirecting</title></head>` +\n `<body><a href=\"${escaped}\">${escaped}</a></body></html>\\n`,\n )\n\n pages++\n continue\n }\n\n await copy(`${key}.html`, `${dir}index.html`)\n await copy(`${key}.flight`, `${dir}${payloadName}`)\n\n // One file per depth a client might already hold, addressed by name\n // because a file server cannot vary on a header. Without these every\n // navigation is a whole document, which replaces the root and unmounts\n // the pages retained behind it — so going back does not restore the form\n // you were filling in.\n for (let depth = 1; ; depth++) {\n const variant = await read(`${key}.seg${depth}.flight`)\n\n if (variant === null) break\n\n await write(`${dir}${payloadName.replace(/^index\\./, `index.seg${depth}.`)}`, variant)\n }\n\n pages++\n }\n\n await assets?.()\n\n return { pages, refused }\n}\n\n\n"]}
|
package/dist/files.d.ts
CHANGED
|
@@ -53,14 +53,6 @@ export declare const ASSETS_PREFIX: (dirName: string) => string;
|
|
|
53
53
|
interface AssetsBinding {
|
|
54
54
|
fetch(input: Request | string): Promise<Response>;
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Read stored pages through the assets binding, each name once per isolate.
|
|
58
|
-
*
|
|
59
|
-
* The host asks for `{url}.html`, then `{url}.ppr.html`, then the pattern's,
|
|
60
|
-
* on every request: three asks per page, most of them misses. The binding
|
|
61
|
-
* is a fetch, not a memory read, so the answers - misses included - are kept
|
|
62
|
-
* for the life of the isolate, the way the disk reader keeps its listing.
|
|
63
|
-
*/
|
|
64
56
|
export declare function assetsReader(assets: AssetsBinding, prefix: string): (name: string) => Promise<string | null>;
|
|
65
57
|
/**
|
|
66
58
|
* The entry a binary is compiled from, written beside the server.
|
package/dist/files.js
CHANGED
|
@@ -188,11 +188,17 @@ async function workersAssets() {
|
|
|
188
188
|
* is a fetch, not a memory read, so the answers - misses included - are kept
|
|
189
189
|
* for the life of the isolate, the way the disk reader keeps its listing.
|
|
190
190
|
*/
|
|
191
|
+
const KNOWN_LIMIT = 4096;
|
|
191
192
|
export function assetsReader(assets, prefix) {
|
|
192
193
|
const known = new Map();
|
|
193
194
|
return (name) => {
|
|
194
195
|
let pending = known.get(name);
|
|
195
196
|
if (!pending) {
|
|
197
|
+
// Bounded. Misses are kept too, and every made-up path is three of
|
|
198
|
+
// them: kept for the life of the isolate, a crawler of random urls grew
|
|
199
|
+
// this without end. Dropping the oldest costs one fetch, later.
|
|
200
|
+
if (known.size >= KNOWN_LIMIT)
|
|
201
|
+
known.delete(known.keys().next().value);
|
|
196
202
|
const path = `${prefix}/${name.split('/').map(encodeURIComponent).join('/')}`;
|
|
197
203
|
pending = assets
|
|
198
204
|
.fetch(new Request(`https://assets.invalid${path}`))
|
|
@@ -260,24 +266,7 @@ function safeUrl(relative, base) {
|
|
|
260
266
|
*/
|
|
261
267
|
export function compileEntrySource(dirName) {
|
|
262
268
|
return ('// The entry to compile into one binary:\n' +
|
|
263
|
-
'// bun build --compile
|
|
264
|
-
'//\n' +
|
|
265
|
-
'// --keep-names is not optional. --compile implies --production, which\n' +
|
|
266
|
-
'// minifies, and minifying renames every function. A partially prerendered\n' +
|
|
267
|
-
'// page is finished by replaying a tree the build recorded by component\n' +
|
|
268
|
-
'// NAME, so a binary that renamed them finds the wrong name in every slot -\n' +
|
|
269
|
-
'// "Expected the resume to render <J> in this slot but instead it rendered\n' +
|
|
270
|
-
'// <J2>" - and every hole falls back to the client. The page then arrives\n' +
|
|
271
|
-
'// looking finished with nothing wired up, only on a cold load, only in\n' +
|
|
272
|
-
'// production. The check below says so rather than leaving it to be found\n' +
|
|
273
|
-
'// from a log days later.\n' +
|
|
274
|
-
'if (function rscKitNameProbe() {}.name !== "rscKitNameProbe") {\n' +
|
|
275
|
-
' console.warn(\n' +
|
|
276
|
-
' "[rsc-kit] This binary was compiled without --keep-names, so function names were minified away. " +\n' +
|
|
277
|
-
' "Any partially prerendered page will fail to resume and arrive inert. Rebuild with: " +\n' +
|
|
278
|
-
' "bun build --compile --keep-names .output/server/compile.mjs --outfile dist/app",\n' +
|
|
279
|
-
' );\n' +
|
|
280
|
-
'}\n' +
|
|
269
|
+
'// bun build --compile .output/server/compile.mjs --outfile dist/app\n' +
|
|
281
270
|
'//\n' +
|
|
282
271
|
'//\n' +
|
|
283
272
|
'// A binary carries what is imported by name. The server reads its frozen pages\n' +
|