@askrjs/vite 0.2.1 → 0.2.2

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/server.js CHANGED
@@ -174,6 +174,7 @@ function composeDocumentBody(fragment, prefix, suffix) {
174
174
  let suffixPending = true;
175
175
  let reading = false;
176
176
  let cancelled = false;
177
+ let finished = false;
177
178
  return new ReadableStream({
178
179
  pull(controller) {
179
180
  if (prefixPending) {
@@ -181,7 +182,7 @@ function composeDocumentBody(fragment, prefix, suffix) {
181
182
  if (prefix) controller.enqueue(encoder.encode(prefix));
182
183
  return;
183
184
  }
184
- if (reading) return;
185
+ if (reading || finished) return;
185
186
  reading = true;
186
187
  reader.read().then((part) => {
187
188
  reading = false;
@@ -190,8 +191,10 @@ function composeDocumentBody(fragment, prefix, suffix) {
190
191
  controller.enqueue(part.value);
191
192
  return;
192
193
  }
193
- if (suffixPending && suffix) controller.enqueue(encoder.encode(suffix));
194
+ const finalSuffix = suffixPending ? suffix : "";
195
+ finished = true;
194
196
  suffixPending = false;
197
+ if (finalSuffix) controller.enqueue(encoder.encode(finalSuffix));
195
198
  controller.close();
196
199
  }, (error) => {
197
200
  reading = false;
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":[],"sources":["../src/server/head.ts","../src/server/document.ts","../src/server/development.ts","../src/server/plugin.ts"],"sourcesContent":["import { parseFragment, serialize, type DefaultTreeAdapterMap } from \"parse5\";\n\nconst htmlNamespace = \"http://www.w3.org/1999/xhtml\";\nconst linkAttributes = new Set([\n \"as\",\n \"crossorigin\",\n \"data-askr-head\",\n \"fetchpriority\",\n \"href\",\n \"hreflang\",\n \"imagesizes\",\n \"imagesrcset\",\n \"media\",\n \"referrerpolicy\",\n \"rel\",\n \"sizes\",\n \"type\",\n]);\n\nfunction headMarkupError(message: string): never {\n throw new TypeError(`Invalid x-askr-head metadata: ${message}`);\n}\n\nfunction attributesOf(node: DefaultTreeAdapterMap[\"element\"]): Map<string, string> {\n const attributes = new Map<string, string>();\n for (const attribute of node.attrs) {\n if (attribute.namespace || attribute.prefix)\n headMarkupError(\"namespaced attributes are not allowed.\");\n attributes.set(attribute.name.toLowerCase(), attribute.value);\n }\n return attributes;\n}\n\nfunction requireAllowedAttributes(\n attributes: Map<string, string>,\n allowed: ReadonlySet<string>,\n): void {\n for (const name of attributes.keys()) {\n if (!allowed.has(name)) headMarkupError(`attribute ${name} is not allowed.`);\n }\n if (attributes.get(\"data-askr-head\") !== \"\")\n headMarkupError(\"every metadata node must be Askr-owned.\");\n}\n\nfunction isTextNode(\n node: DefaultTreeAdapterMap[\"childNode\"],\n): node is DefaultTreeAdapterMap[\"textNode\"] {\n return \"value\" in node;\n}\n\nfunction textContent(node: DefaultTreeAdapterMap[\"element\"]): string {\n let value = \"\";\n for (const child of node.childNodes) {\n if (!isTextNode(child)) headMarkupError(`${node.tagName} must contain only text.`);\n value += child.value;\n }\n return value;\n}\n\nfunction validateHeadElement(node: DefaultTreeAdapterMap[\"element\"]): void {\n if (node.namespaceURI !== htmlNamespace) headMarkupError(\"foreign elements are not allowed.\");\n const attributes = attributesOf(node);\n if (node.tagName === \"title\") {\n requireAllowedAttributes(attributes, new Set([\"data-askr-head\"]));\n textContent(node);\n return;\n }\n if (node.tagName === \"meta\") {\n requireAllowedAttributes(\n attributes,\n new Set([\"content\", \"data-askr-head\", \"name\", \"property\"]),\n );\n const hasName = attributes.has(\"name\");\n const hasProperty = attributes.has(\"property\");\n if (hasName === hasProperty || !attributes.has(\"content\"))\n headMarkupError(\"meta requires content and exactly one of name or property.\");\n if (node.childNodes.length) headMarkupError(\"meta cannot contain child nodes.\");\n return;\n }\n if (node.tagName === \"link\") {\n requireAllowedAttributes(attributes, linkAttributes);\n if (!attributes.has(\"rel\") || !attributes.has(\"href\"))\n headMarkupError(\"link requires rel and href.\");\n if (node.childNodes.length) headMarkupError(\"link cannot contain child nodes.\");\n return;\n }\n if (node.tagName === \"script\") {\n requireAllowedAttributes(attributes, new Set([\"data-askr-head\", \"type\"]));\n if (attributes.get(\"type\") !== \"application/ld+json\")\n headMarkupError(\"only application/ld+json script nodes are allowed.\");\n const json = textContent(node);\n try {\n JSON.parse(json);\n } catch {\n headMarkupError(\"JSON-LD content must be valid JSON.\");\n }\n return;\n }\n headMarkupError(`element ${node.tagName} is not allowed.`);\n}\n\nexport function normalizeAskrHead(head: string): string {\n const fragment = parseFragment(head);\n for (const node of fragment.childNodes) {\n if (isTextNode(node)) {\n if (node.value.trim()) headMarkupError(\"top-level text is not allowed.\");\n continue;\n }\n if (!(\"tagName\" in node)) headMarkupError(\"non-element nodes are not allowed.\");\n validateHeadElement(node);\n }\n return serialize(fragment);\n}\n","import type { ServerApp } from \"@askrjs/server\";\nimport { normalizeAskrHead } from \"./head\";\n\ninterface ViteTelemetryFields {\n status?: number;\n}\n\ninterface ViteTelemetry {\n viteDocument?<T>(fields: ViteTelemetryFields, work: () => T): T;\n}\n\n/** Options for composing a rendered Askr fragment into the HTML document. */\nexport interface AskrDocumentOptions {\n telemetry?: ViteTelemetry;\n}\n\n/** Placeholder marker in `index.html` replaced with the rendered app fragment. */\nexport const ASKR_APP_MARKER = \"<!--askr-app-->\";\n/** Placeholder marker in `index.html` replaced with rendered `<head>` content. */\nexport const ASKR_HEAD_MARKER = \"<!--askr-head-->\";\n\nfunction markerCount(document: string): number {\n return document.split(ASKR_APP_MARKER).length - 1;\n}\n\nfunction headMarkerCount(document: string): number {\n return document.split(ASKR_HEAD_MARKER).length - 1;\n}\n\nfunction escapeAttribute(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n\nfunction patchHtmlAttribute(document: string, name: \"lang\" | \"dir\", value: string): string {\n return document.replace(/<html\\b([^>]*)>/i, (_match, attributes: string) => {\n const matcher = new RegExp(\n `\\\\s+${name}(?:\\\\s*=\\\\s*(?:\"[^\"]*\"|'[^']*'|[^\\\\s\"'=<>\\u0060]+))?`,\n \"gi\",\n );\n const withoutExisting = attributes.replace(matcher, \"\");\n return `<html${withoutExisting} ${name}=\"${escapeAttribute(value)}\">`;\n });\n}\n\n/**\n * Replace the single {@link ASKR_APP_MARKER} in `document` with `fragment`.\n *\n * @throws If `document` does not contain exactly one app marker.\n */\nexport function insertAskrFragment(document: string, fragment: string): string {\n const count = markerCount(document);\n if (count !== 1) {\n throw new Error(\n `index.html must contain exactly one ${ASKR_APP_MARKER} marker; found ${count}.`,\n );\n }\n return document.replace(ASKR_APP_MARKER, fragment);\n}\n\n/**\n * Replace the {@link ASKR_HEAD_MARKER} in `document` with normalized `head`\n * markup, and patch the `<html>` tag's `lang`/`dir` attributes when given.\n */\nexport function composeAskrHead(\n document: string,\n head: string,\n lang?: string,\n dir?: string,\n): string {\n let output = document.replace(ASKR_HEAD_MARKER, normalizeAskrHead(head));\n if (lang) output = patchHtmlAttribute(output, \"lang\", lang);\n if (dir) output = patchHtmlAttribute(output, \"dir\", dir);\n return output;\n}\n\n/** Whether `response` carries the `askr-fragment=1` content-type marker. */\nexport function isAskrFragment(response: Response): boolean {\n return (\n response.headers\n .get(\"content-type\")\n ?.split(\";\")\n .some((part) => part.trim().toLowerCase() === \"askr-fragment=1\") ?? false\n );\n}\n\nfunction documentHeaders(response: Response): Headers {\n const headers = new Headers(response.headers);\n const type = response.headers\n .get(\"content-type\")\n ?.split(\";\")\n .map((part) => part.trim())\n .filter((part) => part.toLowerCase() !== \"askr-fragment=1\")\n .join(\"; \");\n if (type) headers.set(\"content-type\", type);\n for (const name of [\n \"accept-ranges\",\n \"content-digest\",\n \"content-encoding\",\n \"content-length\",\n \"content-md5\",\n \"content-range\",\n \"digest\",\n \"etag\",\n \"last-modified\",\n \"repr-digest\",\n \"trailer\",\n ]) {\n headers.delete(name);\n }\n const internalHeaders: string[] = [];\n headers.forEach((_value, key) => {\n if (key.toLowerCase().startsWith(\"x-askr-\")) internalHeaders.push(key);\n });\n for (const key of internalHeaders) headers.delete(key);\n return headers;\n}\n\nfunction composeDocumentBody(\n fragment: ReadableStream<Uint8Array> | null,\n prefix: string,\n suffix: string,\n): BodyInit {\n if (!fragment) return `${prefix}${suffix}`;\n const encoder = new TextEncoder();\n const reader = fragment.getReader();\n let prefixPending = true;\n let suffixPending = true;\n let reading = false;\n let cancelled = false;\n\n return new ReadableStream<Uint8Array>({\n pull(controller) {\n if (prefixPending) {\n prefixPending = false;\n if (prefix) controller.enqueue(encoder.encode(prefix));\n return;\n }\n if (reading) return;\n reading = true;\n void reader.read().then(\n (part) => {\n reading = false;\n if (cancelled) return;\n if (!part.done) {\n controller.enqueue(part.value);\n return;\n }\n if (suffixPending && suffix) controller.enqueue(encoder.encode(suffix));\n suffixPending = false;\n controller.close();\n },\n (error) => {\n reading = false;\n if (cancelled) return;\n controller.error(error);\n void reader.cancel(error).catch(() => undefined);\n },\n );\n },\n async cancel(reason) {\n cancelled = true;\n await reader.cancel(reason).catch(() => undefined);\n },\n });\n}\n\n/**\n * Compose an Askr fragment response into the full `transformedDocument` HTML,\n * streaming the fragment's body between the document prefix and suffix.\n * Non-fragment responses are returned unchanged.\n *\n * @param response The upstream response, expected to be an Askr fragment.\n * @param transformedDocument The `index.html` document with app/head markers.\n * @param options Optional telemetry hook wrapping the compose work.\n * @returns The composed full-document `Response`, or `response` unchanged.\n */\nexport async function composeAskrDocumentResponse(\n response: Response,\n transformedDocument: string,\n options: AskrDocumentOptions = {},\n): Promise<Response> {\n if (!isAskrFragment(response)) return response;\n const compose = () => {\n if (headMarkerCount(transformedDocument) !== 1) {\n throw new Error(\n `index.html must contain exactly one ${ASKR_HEAD_MARKER} marker; found ${headMarkerCount(transformedDocument)}.`,\n );\n }\n const withHead = composeAskrHead(\n transformedDocument,\n response.headers.get(\"x-askr-head\") ?? \"\",\n response.headers.get(\"x-askr-html-lang\") ?? undefined,\n response.headers.get(\"x-askr-html-dir\") ?? undefined,\n );\n const appCount = markerCount(withHead);\n if (appCount !== 1) {\n throw new Error(\n `index.html must contain exactly one ${ASKR_APP_MARKER} marker; found ${appCount}.`,\n );\n }\n const markerIndex = withHead.indexOf(ASKR_APP_MARKER);\n const prefix = withHead.slice(0, markerIndex);\n const suffix = withHead.slice(markerIndex + ASKR_APP_MARKER.length);\n return new Response(composeDocumentBody(response.body, prefix, suffix), {\n status: response.status,\n statusText: response.statusText,\n headers: documentHeaders(response),\n });\n };\n return options.telemetry?.viteDocument\n ? options.telemetry.viteDocument({ status: response.status }, compose)\n : compose();\n}\n\n/**\n * Wrap `app` so every response is composed into `transformedDocument` via\n * {@link composeAskrDocumentResponse}.\n */\nexport function createDocumentApp(\n app: ServerApp,\n transformedDocument: string,\n options: AskrDocumentOptions = {},\n): ServerApp {\n return {\n async fetch(request) {\n return composeAskrDocumentResponse(await app.fetch(request), transformedDocument, options);\n },\n };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { ServerApp } from \"@askrjs/server\";\nimport type { ViteDevServer } from \"vite\";\nimport { composeAskrDocumentResponse, isAskrFragment } from \"./document\";\nimport type { AskrServerOptions } from \"./types\";\n\nfunction serverApp(module: Record<string, unknown>, options: AskrServerOptions): ServerApp {\n const value = module[options.exportName ?? \"app\"] ?? module.default;\n if (!value || typeof (value as ServerApp).fetch !== \"function\") {\n throw new Error(\n `@askrjs/vite: ${options.entry} must export a ServerApp as ${options.exportName ?? \"app\"} or default`,\n );\n }\n return value as ServerApp;\n}\n\nfunction moduleTelemetry(module: Record<string, unknown>) {\n const telemetry = module[\"telemetry\"];\n return telemetry && typeof telemetry === \"object\"\n ? {\n telemetry: telemetry as NonNullable<\n Parameters<typeof composeAskrDocumentResponse>[2]\n >[\"telemetry\"],\n }\n : undefined;\n}\n\nexport function createDevelopmentApp(server: ViteDevServer, options: AskrServerOptions): ServerApp {\n return {\n async fetch(request) {\n const module = await server.ssrLoadModule(options.entry);\n const response = await serverApp(module, options).fetch(request);\n if (!isAskrFragment(response)) return response;\n const file = resolve(server.config.root, options.indexHtml ?? \"index.html\");\n const document = await readFile(file, \"utf8\");\n const transformed = await server.transformIndexHtml(new URL(request.url).pathname, document);\n return composeAskrDocumentResponse(response, transformed, moduleTelemetry(module));\n },\n };\n}\n","import { createNodeHandler } from \"@askrjs/node\";\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { Plugin, ResolvedConfig, ViteDevServer } from \"vite\";\nimport { createDevelopmentApp } from \"./development\";\nimport type { AskrServerOptions } from \"./types\";\n\n/** Virtual module id that resolves to the generated server document app. */\nexport const ASKR_SERVER_MODULE_ID = \"virtual:askr-server\";\nconst RESOLVED_SERVER_MODULE_ID = \"\\0askr:server\";\n\n/** Portable public identity for the server integration plugin. */\nexport interface AskrServerPlugin {\n readonly name: \"askr:server\";\n}\n\nfunction createNodeHandlerOptions(server: ViteDevServer): {\n baseUrl?: string;\n allowedHosts?: readonly string[];\n} {\n const serverConfig = server.config?.server;\n const baseUrl = serverConfig?.origin;\n\n if (\n serverConfig?.allowedHosts &&\n serverConfig.allowedHosts !== true &&\n serverConfig.allowedHosts.length > 0\n ) {\n return { baseUrl, allowedHosts: serverConfig.allowedHosts };\n }\n\n if (baseUrl) {\n return { baseUrl };\n }\n\n const host = typeof serverConfig?.host === \"string\" ? serverConfig.host : \"localhost\";\n const allowedHosts = new Set([host, \"localhost\", \"127.0.0.1\", \"[::1]\"]);\n return { allowedHosts: Array.from(allowedHosts) };\n}\n\n/**\n * Create the Vite plugin that serves Askr's server-rendered document during\n * development and generates the {@link ASKR_SERVER_MODULE_ID} virtual module\n * that composes the built server app with the HTML document.\n *\n * @param options Server entry and document configuration.\n * @returns A Vite-compatible plugin object exposing the `askr:server` name.\n */\nexport function askrServer(options: AskrServerOptions): AskrServerPlugin {\n let config: ResolvedConfig | undefined;\n const plugin = {\n name: \"askr:server\" as const,\n config() {\n // Askr owns document routing. Vite should serve modules and assets, but\n // must not rewrite page URLs to /index.html before the Askr fallback.\n // Keep the Askr package family inside Vite's module graph so file-linked\n // peers cannot load a second runtime with a different component context.\n // Vitest derives its dependency inlining from resolve.noExternal, which\n // is also Vite 8's replacement for the deprecated ssr.noExternal option.\n return {\n appType: \"custom\",\n resolve: { noExternal: [\"@askrjs/*\"] },\n };\n },\n configResolved(resolved) {\n config = resolved;\n },\n resolveId(id) {\n return id === ASKR_SERVER_MODULE_ID ? RESOLVED_SERVER_MODULE_ID : null;\n },\n async load(id) {\n if (id !== RESOLVED_SERVER_MODULE_ID) return null;\n if (!config) throw new Error(\"@askrjs/vite: server plugin was not configured.\");\n const sourceDocument = resolve(config.root, options.indexHtml ?? \"index.html\");\n const serverOutDocument = resolve(config.root, config.build.outDir, \"index.html\");\n const clientOutDocument = resolve(config.root, config.build.outDir, \"..\", \"index.html\");\n const builtDocument = [serverOutDocument, clientOutDocument].find(existsSync);\n const document = await readFile(builtDocument ?? sourceDocument, \"utf8\");\n const entry = resolve(config.root, options.entry);\n const exportName = options.exportName ?? \"app\";\n return [\n `import * as source from ${JSON.stringify(entry)};`,\n `import { createDocumentApp } from '@askrjs/vite/server';`,\n `const sourceApp = source[${JSON.stringify(exportName)}] ?? source.default;`,\n `const sourceTelemetry = Reflect.get(source, 'telemetry');`,\n `if (!sourceApp?.fetch) throw new Error(${JSON.stringify(`@askrjs/vite: ${options.entry} must export a ServerApp`)});`,\n `export const app = createDocumentApp(sourceApp, ${JSON.stringify(document)}, sourceTelemetry ? { telemetry: sourceTelemetry } : undefined);`,\n `export default app;`,\n ].join(\"\\n\");\n },\n configureServer(server) {\n const handler = createNodeHandler(\n createDevelopmentApp(server, options),\n createNodeHandlerOptions(server),\n );\n // Page rendering is the development fallback. Register it after Vite's\n // module, asset, and HTML middleware so client entries and imported CSS\n // are served by Vite instead of being mistaken for application routes.\n return () => server.middlewares.use(handler);\n },\n } satisfies Plugin;\n return plugin;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/server/head.ts","../src/server/document.ts","../src/server/development.ts","../src/server/plugin.ts"],"sourcesContent":["import { parseFragment, serialize, type DefaultTreeAdapterMap } from \"parse5\";\n\nconst htmlNamespace = \"http://www.w3.org/1999/xhtml\";\nconst linkAttributes = new Set([\n \"as\",\n \"crossorigin\",\n \"data-askr-head\",\n \"fetchpriority\",\n \"href\",\n \"hreflang\",\n \"imagesizes\",\n \"imagesrcset\",\n \"media\",\n \"referrerpolicy\",\n \"rel\",\n \"sizes\",\n \"type\",\n]);\n\nfunction headMarkupError(message: string): never {\n throw new TypeError(`Invalid x-askr-head metadata: ${message}`);\n}\n\nfunction attributesOf(node: DefaultTreeAdapterMap[\"element\"]): Map<string, string> {\n const attributes = new Map<string, string>();\n for (const attribute of node.attrs) {\n if (attribute.namespace || attribute.prefix)\n headMarkupError(\"namespaced attributes are not allowed.\");\n attributes.set(attribute.name.toLowerCase(), attribute.value);\n }\n return attributes;\n}\n\nfunction requireAllowedAttributes(\n attributes: Map<string, string>,\n allowed: ReadonlySet<string>,\n): void {\n for (const name of attributes.keys()) {\n if (!allowed.has(name)) headMarkupError(`attribute ${name} is not allowed.`);\n }\n if (attributes.get(\"data-askr-head\") !== \"\")\n headMarkupError(\"every metadata node must be Askr-owned.\");\n}\n\nfunction isTextNode(\n node: DefaultTreeAdapterMap[\"childNode\"],\n): node is DefaultTreeAdapterMap[\"textNode\"] {\n return \"value\" in node;\n}\n\nfunction textContent(node: DefaultTreeAdapterMap[\"element\"]): string {\n let value = \"\";\n for (const child of node.childNodes) {\n if (!isTextNode(child)) headMarkupError(`${node.tagName} must contain only text.`);\n value += child.value;\n }\n return value;\n}\n\nfunction validateHeadElement(node: DefaultTreeAdapterMap[\"element\"]): void {\n if (node.namespaceURI !== htmlNamespace) headMarkupError(\"foreign elements are not allowed.\");\n const attributes = attributesOf(node);\n if (node.tagName === \"title\") {\n requireAllowedAttributes(attributes, new Set([\"data-askr-head\"]));\n textContent(node);\n return;\n }\n if (node.tagName === \"meta\") {\n requireAllowedAttributes(\n attributes,\n new Set([\"content\", \"data-askr-head\", \"name\", \"property\"]),\n );\n const hasName = attributes.has(\"name\");\n const hasProperty = attributes.has(\"property\");\n if (hasName === hasProperty || !attributes.has(\"content\"))\n headMarkupError(\"meta requires content and exactly one of name or property.\");\n if (node.childNodes.length) headMarkupError(\"meta cannot contain child nodes.\");\n return;\n }\n if (node.tagName === \"link\") {\n requireAllowedAttributes(attributes, linkAttributes);\n if (!attributes.has(\"rel\") || !attributes.has(\"href\"))\n headMarkupError(\"link requires rel and href.\");\n if (node.childNodes.length) headMarkupError(\"link cannot contain child nodes.\");\n return;\n }\n if (node.tagName === \"script\") {\n requireAllowedAttributes(attributes, new Set([\"data-askr-head\", \"type\"]));\n if (attributes.get(\"type\") !== \"application/ld+json\")\n headMarkupError(\"only application/ld+json script nodes are allowed.\");\n const json = textContent(node);\n try {\n JSON.parse(json);\n } catch {\n headMarkupError(\"JSON-LD content must be valid JSON.\");\n }\n return;\n }\n headMarkupError(`element ${node.tagName} is not allowed.`);\n}\n\nexport function normalizeAskrHead(head: string): string {\n const fragment = parseFragment(head);\n for (const node of fragment.childNodes) {\n if (isTextNode(node)) {\n if (node.value.trim()) headMarkupError(\"top-level text is not allowed.\");\n continue;\n }\n if (!(\"tagName\" in node)) headMarkupError(\"non-element nodes are not allowed.\");\n validateHeadElement(node);\n }\n return serialize(fragment);\n}\n","import type { ServerApp } from \"@askrjs/server\";\nimport { normalizeAskrHead } from \"./head\";\n\ninterface ViteTelemetryFields {\n status?: number;\n}\n\ninterface ViteTelemetry {\n viteDocument?<T>(fields: ViteTelemetryFields, work: () => T): T;\n}\n\n/** Options for composing a rendered Askr fragment into the HTML document. */\nexport interface AskrDocumentOptions {\n telemetry?: ViteTelemetry;\n}\n\n/** Placeholder marker in `index.html` replaced with the rendered app fragment. */\nexport const ASKR_APP_MARKER = \"<!--askr-app-->\";\n/** Placeholder marker in `index.html` replaced with rendered `<head>` content. */\nexport const ASKR_HEAD_MARKER = \"<!--askr-head-->\";\n\nfunction markerCount(document: string): number {\n return document.split(ASKR_APP_MARKER).length - 1;\n}\n\nfunction headMarkerCount(document: string): number {\n return document.split(ASKR_HEAD_MARKER).length - 1;\n}\n\nfunction escapeAttribute(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n\nfunction patchHtmlAttribute(document: string, name: \"lang\" | \"dir\", value: string): string {\n return document.replace(/<html\\b([^>]*)>/i, (_match, attributes: string) => {\n const matcher = new RegExp(\n `\\\\s+${name}(?:\\\\s*=\\\\s*(?:\"[^\"]*\"|'[^']*'|[^\\\\s\"'=<>\\u0060]+))?`,\n \"gi\",\n );\n const withoutExisting = attributes.replace(matcher, \"\");\n return `<html${withoutExisting} ${name}=\"${escapeAttribute(value)}\">`;\n });\n}\n\n/**\n * Replace the single {@link ASKR_APP_MARKER} in `document` with `fragment`.\n *\n * @throws If `document` does not contain exactly one app marker.\n */\nexport function insertAskrFragment(document: string, fragment: string): string {\n const count = markerCount(document);\n if (count !== 1) {\n throw new Error(\n `index.html must contain exactly one ${ASKR_APP_MARKER} marker; found ${count}.`,\n );\n }\n return document.replace(ASKR_APP_MARKER, fragment);\n}\n\n/**\n * Replace the {@link ASKR_HEAD_MARKER} in `document` with normalized `head`\n * markup, and patch the `<html>` tag's `lang`/`dir` attributes when given.\n */\nexport function composeAskrHead(\n document: string,\n head: string,\n lang?: string,\n dir?: string,\n): string {\n let output = document.replace(ASKR_HEAD_MARKER, normalizeAskrHead(head));\n if (lang) output = patchHtmlAttribute(output, \"lang\", lang);\n if (dir) output = patchHtmlAttribute(output, \"dir\", dir);\n return output;\n}\n\n/** Whether `response` carries the `askr-fragment=1` content-type marker. */\nexport function isAskrFragment(response: Response): boolean {\n return (\n response.headers\n .get(\"content-type\")\n ?.split(\";\")\n .some((part) => part.trim().toLowerCase() === \"askr-fragment=1\") ?? false\n );\n}\n\nfunction documentHeaders(response: Response): Headers {\n const headers = new Headers(response.headers);\n const type = response.headers\n .get(\"content-type\")\n ?.split(\";\")\n .map((part) => part.trim())\n .filter((part) => part.toLowerCase() !== \"askr-fragment=1\")\n .join(\"; \");\n if (type) headers.set(\"content-type\", type);\n for (const name of [\n \"accept-ranges\",\n \"content-digest\",\n \"content-encoding\",\n \"content-length\",\n \"content-md5\",\n \"content-range\",\n \"digest\",\n \"etag\",\n \"last-modified\",\n \"repr-digest\",\n \"trailer\",\n ]) {\n headers.delete(name);\n }\n const internalHeaders: string[] = [];\n headers.forEach((_value, key) => {\n if (key.toLowerCase().startsWith(\"x-askr-\")) internalHeaders.push(key);\n });\n for (const key of internalHeaders) headers.delete(key);\n return headers;\n}\n\nfunction composeDocumentBody(\n fragment: ReadableStream<Uint8Array> | null,\n prefix: string,\n suffix: string,\n): BodyInit {\n if (!fragment) return `${prefix}${suffix}`;\n const encoder = new TextEncoder();\n const reader = fragment.getReader();\n let prefixPending = true;\n let suffixPending = true;\n let reading = false;\n let cancelled = false;\n let finished = false;\n\n return new ReadableStream<Uint8Array>({\n pull(controller) {\n if (prefixPending) {\n prefixPending = false;\n if (prefix) controller.enqueue(encoder.encode(prefix));\n return;\n }\n if (reading || finished) return;\n reading = true;\n void reader.read().then(\n (part) => {\n reading = false;\n if (cancelled) return;\n if (!part.done) {\n controller.enqueue(part.value);\n return;\n }\n const finalSuffix = suffixPending ? suffix : \"\";\n finished = true;\n suffixPending = false;\n if (finalSuffix) controller.enqueue(encoder.encode(finalSuffix));\n controller.close();\n },\n (error) => {\n reading = false;\n if (cancelled) return;\n controller.error(error);\n void reader.cancel(error).catch(() => undefined);\n },\n );\n },\n async cancel(reason) {\n cancelled = true;\n await reader.cancel(reason).catch(() => undefined);\n },\n });\n}\n\n/**\n * Compose an Askr fragment response into the full `transformedDocument` HTML,\n * streaming the fragment's body between the document prefix and suffix.\n * Non-fragment responses are returned unchanged.\n *\n * @param response The upstream response, expected to be an Askr fragment.\n * @param transformedDocument The `index.html` document with app/head markers.\n * @param options Optional telemetry hook wrapping the compose work.\n * @returns The composed full-document `Response`, or `response` unchanged.\n */\nexport async function composeAskrDocumentResponse(\n response: Response,\n transformedDocument: string,\n options: AskrDocumentOptions = {},\n): Promise<Response> {\n if (!isAskrFragment(response)) return response;\n const compose = () => {\n if (headMarkerCount(transformedDocument) !== 1) {\n throw new Error(\n `index.html must contain exactly one ${ASKR_HEAD_MARKER} marker; found ${headMarkerCount(transformedDocument)}.`,\n );\n }\n const withHead = composeAskrHead(\n transformedDocument,\n response.headers.get(\"x-askr-head\") ?? \"\",\n response.headers.get(\"x-askr-html-lang\") ?? undefined,\n response.headers.get(\"x-askr-html-dir\") ?? undefined,\n );\n const appCount = markerCount(withHead);\n if (appCount !== 1) {\n throw new Error(\n `index.html must contain exactly one ${ASKR_APP_MARKER} marker; found ${appCount}.`,\n );\n }\n const markerIndex = withHead.indexOf(ASKR_APP_MARKER);\n const prefix = withHead.slice(0, markerIndex);\n const suffix = withHead.slice(markerIndex + ASKR_APP_MARKER.length);\n return new Response(composeDocumentBody(response.body, prefix, suffix), {\n status: response.status,\n statusText: response.statusText,\n headers: documentHeaders(response),\n });\n };\n return options.telemetry?.viteDocument\n ? options.telemetry.viteDocument({ status: response.status }, compose)\n : compose();\n}\n\n/**\n * Wrap `app` so every response is composed into `transformedDocument` via\n * {@link composeAskrDocumentResponse}.\n */\nexport function createDocumentApp(\n app: ServerApp,\n transformedDocument: string,\n options: AskrDocumentOptions = {},\n): ServerApp {\n return {\n async fetch(request) {\n return composeAskrDocumentResponse(await app.fetch(request), transformedDocument, options);\n },\n };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type { ServerApp } from \"@askrjs/server\";\nimport type { ViteDevServer } from \"vite\";\nimport { composeAskrDocumentResponse, isAskrFragment } from \"./document\";\nimport type { AskrServerOptions } from \"./types\";\n\nfunction serverApp(module: Record<string, unknown>, options: AskrServerOptions): ServerApp {\n const value = module[options.exportName ?? \"app\"] ?? module.default;\n if (!value || typeof (value as ServerApp).fetch !== \"function\") {\n throw new Error(\n `@askrjs/vite: ${options.entry} must export a ServerApp as ${options.exportName ?? \"app\"} or default`,\n );\n }\n return value as ServerApp;\n}\n\nfunction moduleTelemetry(module: Record<string, unknown>) {\n const telemetry = module[\"telemetry\"];\n return telemetry && typeof telemetry === \"object\"\n ? {\n telemetry: telemetry as NonNullable<\n Parameters<typeof composeAskrDocumentResponse>[2]\n >[\"telemetry\"],\n }\n : undefined;\n}\n\nexport function createDevelopmentApp(server: ViteDevServer, options: AskrServerOptions): ServerApp {\n return {\n async fetch(request) {\n const module = await server.ssrLoadModule(options.entry);\n const response = await serverApp(module, options).fetch(request);\n if (!isAskrFragment(response)) return response;\n const file = resolve(server.config.root, options.indexHtml ?? \"index.html\");\n const document = await readFile(file, \"utf8\");\n const transformed = await server.transformIndexHtml(new URL(request.url).pathname, document);\n return composeAskrDocumentResponse(response, transformed, moduleTelemetry(module));\n },\n };\n}\n","import { createNodeHandler } from \"@askrjs/node\";\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { Plugin, ResolvedConfig, ViteDevServer } from \"vite\";\nimport { createDevelopmentApp } from \"./development\";\nimport type { AskrServerOptions } from \"./types\";\n\n/** Virtual module id that resolves to the generated server document app. */\nexport const ASKR_SERVER_MODULE_ID = \"virtual:askr-server\";\nconst RESOLVED_SERVER_MODULE_ID = \"\\0askr:server\";\n\n/** Portable public identity for the server integration plugin. */\nexport interface AskrServerPlugin {\n readonly name: \"askr:server\";\n}\n\nfunction createNodeHandlerOptions(server: ViteDevServer): {\n baseUrl?: string;\n allowedHosts?: readonly string[];\n} {\n const serverConfig = server.config?.server;\n const baseUrl = serverConfig?.origin;\n\n if (\n serverConfig?.allowedHosts &&\n serverConfig.allowedHosts !== true &&\n serverConfig.allowedHosts.length > 0\n ) {\n return { baseUrl, allowedHosts: serverConfig.allowedHosts };\n }\n\n if (baseUrl) {\n return { baseUrl };\n }\n\n const host = typeof serverConfig?.host === \"string\" ? serverConfig.host : \"localhost\";\n const allowedHosts = new Set([host, \"localhost\", \"127.0.0.1\", \"[::1]\"]);\n return { allowedHosts: Array.from(allowedHosts) };\n}\n\n/**\n * Create the Vite plugin that serves Askr's server-rendered document during\n * development and generates the {@link ASKR_SERVER_MODULE_ID} virtual module\n * that composes the built server app with the HTML document.\n *\n * @param options Server entry and document configuration.\n * @returns A Vite-compatible plugin object exposing the `askr:server` name.\n */\nexport function askrServer(options: AskrServerOptions): AskrServerPlugin {\n let config: ResolvedConfig | undefined;\n const plugin = {\n name: \"askr:server\" as const,\n config() {\n // Askr owns document routing. Vite should serve modules and assets, but\n // must not rewrite page URLs to /index.html before the Askr fallback.\n // Keep the Askr package family inside Vite's module graph so file-linked\n // peers cannot load a second runtime with a different component context.\n // Vitest derives its dependency inlining from resolve.noExternal, which\n // is also Vite 8's replacement for the deprecated ssr.noExternal option.\n return {\n appType: \"custom\",\n resolve: { noExternal: [\"@askrjs/*\"] },\n };\n },\n configResolved(resolved) {\n config = resolved;\n },\n resolveId(id) {\n return id === ASKR_SERVER_MODULE_ID ? RESOLVED_SERVER_MODULE_ID : null;\n },\n async load(id) {\n if (id !== RESOLVED_SERVER_MODULE_ID) return null;\n if (!config) throw new Error(\"@askrjs/vite: server plugin was not configured.\");\n const sourceDocument = resolve(config.root, options.indexHtml ?? \"index.html\");\n const serverOutDocument = resolve(config.root, config.build.outDir, \"index.html\");\n const clientOutDocument = resolve(config.root, config.build.outDir, \"..\", \"index.html\");\n const builtDocument = [serverOutDocument, clientOutDocument].find(existsSync);\n const document = await readFile(builtDocument ?? sourceDocument, \"utf8\");\n const entry = resolve(config.root, options.entry);\n const exportName = options.exportName ?? \"app\";\n return [\n `import * as source from ${JSON.stringify(entry)};`,\n `import { createDocumentApp } from '@askrjs/vite/server';`,\n `const sourceApp = source[${JSON.stringify(exportName)}] ?? source.default;`,\n `const sourceTelemetry = Reflect.get(source, 'telemetry');`,\n `if (!sourceApp?.fetch) throw new Error(${JSON.stringify(`@askrjs/vite: ${options.entry} must export a ServerApp`)});`,\n `export const app = createDocumentApp(sourceApp, ${JSON.stringify(document)}, sourceTelemetry ? { telemetry: sourceTelemetry } : undefined);`,\n `export default app;`,\n ].join(\"\\n\");\n },\n configureServer(server) {\n const handler = createNodeHandler(\n createDevelopmentApp(server, options),\n createNodeHandlerOptions(server),\n );\n // Page rendering is the development fallback. Register it after Vite's\n // module, asset, and HTML middleware so client entries and imported CSS\n // are served by Vite instead of being mistaken for application routes.\n return () => server.middlewares.use(handler);\n },\n } satisfies Plugin;\n return plugin;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askrjs/vite",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Vite plugin for Askr JSX and template transforms",
5
5
  "homepage": "https://github.com/askrjs/askr-vite#readme",
6
6
  "bugs": {
@@ -59,26 +59,26 @@
59
59
  "prepublishOnly": "npm run check"
60
60
  },
61
61
  "dependencies": {
62
- "@askrjs/node": ">=0.2.0 <0.3.0",
63
- "oxc-parser": "^0.144.0",
62
+ "@askrjs/node": ">=0.2.1 <0.3.0",
63
+ "oxc-parser": "^0.147.0",
64
64
  "parse5": "^8.0.1"
65
65
  },
66
66
  "devDependencies": {
67
- "@askrjs/askr": ">=0.2.0 <0.3.0",
68
- "@askrjs/server": ">=0.2.0 <0.3.0",
69
- "@types/node": "^26.2.0",
70
- "publint": "^0.3.23",
67
+ "@askrjs/askr": ">=0.2.3 <0.3.0",
68
+ "@askrjs/server": ">=0.2.1 <0.3.0",
69
+ "@types/node": "^26.3.0",
70
+ "publint": "^0.3.24",
71
71
  "sharp": "^0.35.3",
72
72
  "typescript": "^7.0.2",
73
- "vite": "^8.2.1",
74
- "vite-plus": "^0.2.8",
75
- "vitest": "^4.1.10"
73
+ "vite": "^8.2.2",
74
+ "vite-plus": "^0.3.0",
75
+ "vitest": "^4.1.11"
76
76
  },
77
77
  "peerDependencies": {
78
- "@askrjs/askr": ">=0.2.0 <0.3.0",
78
+ "@askrjs/askr": ">=0.2.3 <0.3.0",
79
79
  "sharp": "^0.35.3",
80
- "vite": "^8.2.1",
81
- "vite-plus": "^0.2.8"
80
+ "vite": "^8.2.2",
81
+ "vite-plus": "^0.2.8 || ^0.3.0"
82
82
  },
83
83
  "peerDependenciesMeta": {
84
84
  "sharp": {