@askrjs/askr 0.0.35 → 0.0.37
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/README.md +17 -9
- package/dist/bench/components/benchmark-row.js.map +1 -1
- package/dist/bench/components/benchmark-table.js +1 -1
- package/dist/bench/components/benchmark-table.js.map +1 -1
- package/dist/benchmark.js +3 -3
- package/dist/benchmark.js.map +1 -1
- package/dist/bin/askr-ssg.js.map +1 -1
- package/dist/boot/index.d.ts +1 -7
- package/dist/boot/index.d.ts.map +1 -1
- package/dist/boot/index.js +31 -42
- package/dist/boot/index.js.map +1 -1
- package/dist/common/control.js.map +1 -1
- package/dist/common/env.js.map +1 -1
- package/dist/common/ssr-errors.js.map +1 -1
- package/dist/common/vnode.js.map +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/link.d.ts +4 -5
- package/dist/components/link.d.ts.map +1 -1
- package/dist/components/link.js +2 -1
- package/dist/components/link.js.map +1 -1
- package/dist/control/case.js.map +1 -1
- package/dist/control/for.js.map +1 -1
- package/dist/control/shared.js.map +1 -1
- package/dist/control/show.js.map +1 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +165 -23
- package/dist/data/index.js.map +1 -1
- package/dist/dev/invariant.js.map +1 -1
- package/dist/dev/logger.js.map +1 -1
- package/dist/foundations/icon/icon.d.ts.map +1 -1
- package/dist/foundations/icon/icon.js.map +1 -1
- package/dist/foundations/icon/icon.types.d.ts +2 -1
- package/dist/foundations/icon/icon.types.d.ts.map +1 -1
- package/dist/foundations/icon/index.d.ts +3 -0
- package/dist/foundations/icon/index.js +2 -0
- package/dist/foundations/index.d.ts +1 -18
- package/dist/foundations/index.js +2 -18
- package/dist/foundations/interactions/dismissable.js.map +1 -1
- package/dist/foundations/interactions/focusable.js.map +1 -1
- package/dist/foundations/interactions/hoverable.js.map +1 -1
- package/dist/foundations/interactions/index.d.ts +7 -0
- package/dist/foundations/interactions/index.js +7 -0
- package/dist/foundations/interactions/interaction-policy.js +2 -2
- package/dist/foundations/interactions/interaction-policy.js.map +1 -1
- package/dist/foundations/interactions/pressable.js.map +1 -1
- package/dist/foundations/interactions/roving-focus.js.map +1 -1
- package/dist/foundations/state/controllable.js.map +1 -1
- package/dist/foundations/state/index.d.ts +2 -0
- package/dist/foundations/state/index.js +2 -0
- package/dist/foundations/structures/collection.js.map +1 -1
- package/dist/foundations/structures/index.d.ts +8 -0
- package/dist/foundations/structures/index.js +7 -0
- package/dist/foundations/structures/layer.js.map +1 -1
- package/dist/foundations/structures/layout.js.map +1 -1
- package/dist/foundations/structures/portal.js.map +1 -1
- package/dist/foundations/structures/presence.js.map +1 -1
- package/dist/foundations/structures/slot.js.map +1 -1
- package/dist/foundations/utilities/aria.js.map +1 -1
- package/dist/foundations/utilities/compose-handlers.js.map +1 -1
- package/dist/foundations/utilities/compose-ref.js.map +1 -1
- package/dist/foundations/utilities/index.d.ts +7 -0
- package/dist/foundations/utilities/index.js +6 -0
- package/dist/foundations/utilities/merge-props.js.map +1 -1
- package/dist/foundations/utilities/use-id.js.map +1 -1
- package/dist/fx/fx.js.map +1 -1
- package/dist/fx/timing.js.map +1 -1
- package/dist/index.d.ts +2 -16
- package/dist/index.js +1 -17
- package/dist/index.js.map +1 -1
- package/dist/jsx/utils.js.map +1 -1
- package/dist/jsx-dev-runtime.d.ts +1 -1
- package/dist/jsx-dev-runtime.js.map +1 -1
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js.map +1 -1
- package/dist/renderer/children.js.map +1 -1
- package/dist/renderer/cleanup.js.map +1 -1
- package/dist/renderer/dom.js +92 -357
- package/dist/renderer/dom.js.map +1 -1
- package/dist/renderer/evaluate.js +7 -0
- package/dist/renderer/evaluate.js.map +1 -1
- package/dist/renderer/fastpath.js +20 -18
- package/dist/renderer/fastpath.js.map +1 -1
- package/dist/renderer/for-commit.js +486 -0
- package/dist/renderer/for-commit.js.map +1 -0
- package/dist/renderer/index.js.map +1 -1
- package/dist/renderer/keyed.js +84 -23
- package/dist/renderer/keyed.js.map +1 -1
- package/dist/renderer/reconcile.js +11 -18
- package/dist/renderer/reconcile.js.map +1 -1
- package/dist/renderer/utils.js +5 -1
- package/dist/renderer/utils.js.map +1 -1
- package/dist/resources/index.d.ts +1 -2
- package/dist/resources/index.js +1 -2
- package/dist/router/index.d.ts +2 -2
- package/dist/router/index.js +2 -2
- package/dist/router/match.js.map +1 -1
- package/dist/router/navigate.d.ts.map +1 -1
- package/dist/router/navigate.js +4 -2
- package/dist/router/navigate.js.map +1 -1
- package/dist/router/policy.js.map +1 -1
- package/dist/router/route-context.js.map +1 -1
- package/dist/router/route.d.ts +1 -1
- package/dist/router/route.d.ts.map +1 -1
- package/dist/router/route.js +2 -33
- package/dist/router/route.js.map +1 -1
- package/dist/runtime/child-scope.d.ts +1 -0
- package/dist/runtime/child-scope.d.ts.map +1 -1
- package/dist/runtime/child-scope.js +2 -0
- package/dist/runtime/child-scope.js.map +1 -1
- package/dist/runtime/component.js +1 -1
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/control.js.map +1 -1
- package/dist/runtime/derive.d.ts +2 -1
- package/dist/runtime/derive.d.ts.map +1 -1
- package/dist/runtime/derive.js +15 -10
- package/dist/runtime/derive.js.map +1 -1
- package/dist/runtime/dev-namespace.js.map +1 -1
- package/dist/runtime/effect.d.ts.map +1 -1
- package/dist/runtime/effect.js +34 -9
- package/dist/runtime/effect.js.map +1 -1
- package/dist/runtime/events.js.map +1 -1
- package/dist/runtime/execution-model.js.map +1 -1
- package/dist/runtime/fastlane.js.map +1 -1
- package/dist/runtime/for-bench.d.ts +1 -1
- package/dist/runtime/for-bench.d.ts.map +1 -1
- package/dist/runtime/for-bench.js +15 -14
- package/dist/runtime/for-bench.js.map +1 -1
- package/dist/runtime/for.d.ts +1 -0
- package/dist/runtime/for.d.ts.map +1 -1
- package/dist/runtime/for.js +90 -31
- package/dist/runtime/for.js.map +1 -1
- package/dist/runtime/operations.d.ts.map +1 -1
- package/dist/runtime/operations.js +10 -8
- package/dist/runtime/operations.js.map +1 -1
- package/dist/runtime/perf-metrics.js +8 -2
- package/dist/runtime/perf-metrics.js.map +1 -1
- package/dist/runtime/readable.js.map +1 -1
- package/dist/runtime/resource-cell.js +3 -2
- package/dist/runtime/resource-cell.js.map +1 -1
- package/dist/runtime/scheduler.d.ts +0 -1
- package/dist/runtime/scheduler.d.ts.map +1 -1
- package/dist/runtime/scheduler.js +6 -19
- package/dist/runtime/scheduler.js.map +1 -1
- package/dist/runtime/selector.d.ts.map +1 -1
- package/dist/runtime/selector.js +182 -103
- package/dist/runtime/selector.js.map +1 -1
- package/dist/runtime/snapshot-source.d.ts +10 -0
- package/dist/runtime/snapshot-source.d.ts.map +1 -0
- package/dist/runtime/snapshot-source.js +17 -0
- package/dist/runtime/snapshot-source.js.map +1 -0
- package/dist/runtime/ssr-bridge.js.map +1 -1
- package/dist/runtime/state.d.ts.map +1 -1
- package/dist/runtime/state.js +1 -5
- package/dist/runtime/state.js.map +1 -1
- package/dist/ssg/batch-render.js.map +1 -1
- package/dist/ssg/create-static-gen.js.map +1 -1
- package/dist/ssg/generate-metadata.js.map +1 -1
- package/dist/ssg/incremental-manifest.js.map +1 -1
- package/dist/ssg/index.d.ts +1 -6
- package/dist/ssg/index.js +1 -6
- package/dist/ssg/resolve-ssg-data.js.map +1 -1
- package/dist/ssg/route-utils.js.map +1 -1
- package/dist/ssg/write-static-files.js.map +1 -1
- package/dist/ssr/attrs.js.map +1 -1
- package/dist/ssr/context.d.ts +3 -1
- package/dist/ssr/context.d.ts.map +1 -1
- package/dist/ssr/context.js +31 -11
- package/dist/ssr/context.js.map +1 -1
- package/dist/ssr/escape.js +56 -16
- package/dist/ssr/escape.js.map +1 -1
- package/dist/ssr/index.d.ts +2 -31
- package/dist/ssr/index.d.ts.map +1 -1
- package/dist/ssr/index.js +19 -49
- package/dist/ssr/index.js.map +1 -1
- package/dist/ssr/render-keys.js +1 -12
- package/dist/ssr/render-keys.js.map +1 -1
- package/dist/ssr/sink.js.map +1 -1
- package/dist/ssr/verify-hydration.js +25 -0
- package/dist/ssr/verify-hydration.js.map +1 -0
- package/package.json +44 -19
- package/dist/_virtual/_rolldown/runtime.js +0 -7
- package/dist/foundations/core.d.ts +0 -15
- package/dist/foundations/core.js +0 -13
- package/dist/foundations/structures.d.ts +0 -7
- package/dist/foundations/structures.js +0 -6
- package/dist/ssg/batch-render.d.ts +0 -16
- package/dist/ssg/batch-render.d.ts.map +0 -1
- package/dist/ssg/generate-metadata.d.ts +0 -24
- package/dist/ssg/generate-metadata.d.ts.map +0 -1
- package/dist/ssg/resolve-ssg-data.d.ts +0 -23
- package/dist/ssg/resolve-ssg-data.d.ts.map +0 -1
- package/dist/ssg/route-utils.d.ts +0 -16
- package/dist/ssg/route-utils.d.ts.map +0 -1
- package/dist/ssg/write-static-files.d.ts +0 -15
- package/dist/ssg/write-static-files.d.ts.map +0 -1
- package/dist/ssr/render-keys.d.ts +0 -37
- package/dist/ssr/render-keys.d.ts.map +0 -1
- package/dist/ssr/stream-render.js +0 -76
- package/dist/ssr/stream-render.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-static-gen.js","names":[],"sources":["../../src/ssg/create-static-gen.ts"],"sourcesContent":["/**\n * Static Site Generation API\n *\n * Main orchestrator for batch rendering, file I/O, and metadata generation\n */\n\nimport type {\n RouteRenderReason,\n RouteRenderResult,\n SSGGenerateOptions,\n SSGMode,\n SSGOptions,\n SSGResult,\n} from './types';\nimport { resolveSsgData, validateRoutes } from './resolve-ssg-data';\nimport { batchRenderRoutes } from './batch-render';\nimport { writeStaticFiles } from './write-static-files';\nimport {\n generateSSGResult,\n resultToMetadata,\n writeMetadata,\n} from './generate-metadata';\nimport {\n getExistingOutputFileSize,\n outputFileExists,\n readIncrementalManifest,\n type IncrementalManifest,\n type IncrementalManifestRouteEntry,\n writeIncrementalManifest,\n hashHtml,\n SSG_MANIFEST_SCHEMA_VERSION,\n} from './incremental-manifest';\nimport {\n resolveRouteDescriptor,\n type ResolvedRouteDescriptor,\n} from './route-utils';\nimport { addPerfDuration, incrementPerfMetric } from '../runtime/perf-metrics';\n\ninterface SelectedRoute {\n descriptor: ResolvedRouteDescriptor;\n reason: RouteRenderReason;\n previous: IncrementalManifestRouteEntry | null;\n}\n\nfunction getRuntimeOnlyDiagnostic(\n descriptor: ResolvedRouteDescriptor\n): string | null {\n const { route } = descriptor;\n\n if (route.auth === true) {\n return `Skipped prerender for \"${descriptor.path}\": authenticated routes are runtime-only by default.`;\n }\n\n if (route.role) {\n return `Skipped prerender for \"${descriptor.path}\": role-gated routes are runtime-only by default.`;\n }\n\n if (route.permission) {\n return `Skipped prerender for \"${descriptor.path}\": permission-gated routes are runtime-only by default.`;\n }\n\n if (route.policies && route.policies.length > 0) {\n return `Skipped prerender for \"${descriptor.path}\": routes with custom policies are runtime-only by default.`;\n }\n\n return null;\n}\n\nfunction resolveParallelism(requested: number | 'auto' | undefined): number {\n if (requested !== 'auto') {\n return Math.max(1, requested ?? 1);\n }\n\n const maybeNavigator = globalThis as typeof globalThis & {\n navigator?: { hardwareConcurrency?: number };\n process?: { env?: Record<string, string | undefined> };\n };\n\n const envWorkers = Number(\n maybeNavigator.process?.env?.ASKR_SSG_WORKERS ??\n maybeNavigator.process?.env?.NUMBER_OF_PROCESSORS ??\n maybeNavigator.process?.env?.UV_THREADPOOL_SIZE\n );\n if (Number.isFinite(envWorkers) && envWorkers > 0) {\n return Math.max(1, Math.trunc(envWorkers));\n }\n\n if (typeof maybeNavigator.navigator?.hardwareConcurrency === 'number') {\n return Math.max(1, maybeNavigator.navigator.hardwareConcurrency);\n }\n\n return 1;\n}\n\n/**\n * Create a Static Site Generator\n *\n * Usage:\n * ```ts\n * const ssg = createStaticGen({\n * routes: [\n * { path: '/', component: HomePage },\n * { path: '/about', component: AboutPage },\n * ],\n * outputDir: './dist',\n * dataOverrides: {\n * '/api/posts': { posts: [...] }\n * }\n * });\n *\n * const result = await ssg.generate();\n * console.log(`Generated ${result.successful}/${result.totalRoutes} routes`);\n * ```\n */\nexport function createStaticGen(options: SSGOptions) {\n let result: SSGResult | null = null;\n const seed = options.seed ?? 12345;\n const resolvedParallelism = resolveParallelism(options.parallelism);\n const resolvedConcurrency = Math.max(\n 1,\n options.concurrency ?? resolvedParallelism\n );\n\n if (!Array.isArray(options.routes) || options.routes.length === 0) {\n throw new Error('routes array is required');\n }\n\n if (!options.outputDir || options.outputDir.trim().length === 0) {\n throw new Error('outputDir is required');\n }\n\n return {\n /**\n * Generate static HTML for all routes\n * Writes to outputDir with metadata.json\n * Returns detailed results\n */\n async generate(\n generateOptions: SSGGenerateOptions = {}\n ): Promise<SSGResult> {\n // Validate input\n validateRoutes(options.routes);\n\n const changedKeys = dedupeStrings(generateOptions.changedKeys);\n const changedRoutes = dedupeStrings(generateOptions.changedRoutes);\n const requestedMode = generateOptions.mode ?? 'full';\n const previousManifest =\n !generateOptions.forceFull && requestedMode === 'incremental'\n ? readIncrementalManifest(options.outputDir, seed)\n : null;\n const effectiveMode: SSGMode =\n generateOptions.forceFull ||\n requestedMode !== 'incremental' ||\n previousManifest === null\n ? 'full'\n : 'incremental';\n\n // Resolve data\n const dataMap = resolveSsgData(options.routes, {\n dataOverrides: options.dataOverrides,\n });\n\n const descriptors = options.routes.map(resolveRouteDescriptor);\n const routeResultsById = new Map<string, RouteRenderResult>();\n const eligibleDescriptors: ResolvedRouteDescriptor[] = [];\n\n for (const descriptor of descriptors) {\n const diagnostic = getRuntimeOnlyDiagnostic(descriptor);\n if (diagnostic) {\n routeResultsById.set(descriptor.routeId, {\n path: descriptor.path,\n filePath: descriptor.filePath,\n html: '',\n fileSize: 0,\n renderDuration: 0,\n resourceCount: 0,\n status: 'skipped',\n reason: 'runtime-only',\n written: false,\n error: diagnostic,\n });\n continue;\n }\n\n eligibleDescriptors.push(descriptor);\n }\n\n const previousEntries = new Map(\n (previousManifest?.routes ?? []).map((entry) => [entry.routeId, entry])\n );\n const currentRouteIds = new Set(\n eligibleDescriptors.map((descriptor) => descriptor.routeId)\n );\n\n const selected: SelectedRoute[] = [];\n const routesToRender: SelectedRoute[] = [];\n for (let index = 0; index < eligibleDescriptors.length; index += 1) {\n const descriptor = eligibleDescriptors[index];\n const entry = selectRouteForGeneration(\n descriptor,\n previousEntries.get(descriptor.routeId) ?? null,\n effectiveMode,\n changedKeys,\n changedRoutes\n );\n selected.push(entry);\n if (entry.reason !== 'unchanged') {\n routesToRender.push(entry);\n }\n }\n const renderStartTime = performance.now();\n incrementPerfMetric('ssgWorkerCount', resolvedParallelism);\n const renderedResults =\n routesToRender.length > 0\n ? await batchRenderRoutes(getRoutesToRender(routesToRender), {\n seed,\n dataMap,\n concurrency: resolvedConcurrency,\n })\n : [];\n addPerfDuration('ssgRenderTimeMs', performance.now() - renderStartTime);\n const renderedByRouteId = new Map<string, RouteRenderResult>();\n for (let index = 0; index < renderedResults.length; index += 1) {\n renderedByRouteId.set(\n routesToRender[index].descriptor.routeId,\n renderedResults[index]\n );\n }\n\n let cacheHits = 0;\n const nextManifestRoutes: IncrementalManifestRouteEntry[] = [];\n\n for (const entry of selected) {\n const { descriptor, previous, reason } = entry;\n const baseData =\n dataMap[descriptor.route.path] ?? dataMap[descriptor.path] ?? {};\n const resourceCount = Object.keys(baseData).length;\n\n if (reason === 'unchanged') {\n routeResultsById.set(descriptor.routeId, {\n path: descriptor.path,\n filePath: descriptor.filePath,\n html: '',\n fileSize:\n previous?.htmlHash !== null && previous !== null\n ? getExistingOutputFileSize(\n options.outputDir,\n descriptor.filePath\n )\n : 0,\n renderDuration: 0,\n resourceCount,\n status: 'skipped',\n reason: 'unchanged',\n written: false,\n });\n\n if (previous) {\n nextManifestRoutes.push({\n ...previous,\n path: descriptor.path,\n filePath: descriptor.filePath,\n invalidationKeys: descriptor.invalidationKeys.slice(),\n });\n }\n continue;\n }\n\n const rendered = renderedByRouteId.get(descriptor.routeId);\n if (!rendered) {\n throw new Error(\n `Missing rendered result for route \"${descriptor.path}\"`\n );\n }\n\n const nextResult: RouteRenderResult = {\n ...rendered,\n path: descriptor.path,\n filePath: descriptor.filePath,\n resourceCount,\n reason,\n written: false,\n };\n\n if (nextResult.status === 'success') {\n const htmlDigest = hashHtml(nextResult.html);\n const shouldWrite =\n effectiveMode === 'full' ||\n !previous ||\n previous.htmlHash !== htmlDigest ||\n !outputFileExists(options.outputDir, descriptor.filePath);\n\n nextResult.written = shouldWrite;\n if (!shouldWrite) {\n cacheHits += 1;\n }\n\n nextManifestRoutes.push({\n routeId: descriptor.routeId,\n path: descriptor.path,\n filePath: descriptor.filePath,\n invalidationKeys: descriptor.invalidationKeys.slice(),\n htmlHash: htmlDigest,\n lastStatus: 'success',\n });\n } else {\n nextManifestRoutes.push({\n routeId: descriptor.routeId,\n path: descriptor.path,\n filePath: descriptor.filePath,\n invalidationKeys: descriptor.invalidationKeys.slice(),\n htmlHash: previous?.htmlHash ?? null,\n lastStatus: 'error',\n });\n }\n\n routeResultsById.set(descriptor.routeId, nextResult);\n }\n\n const removedResults = collectRemovedRouteResults(\n previousManifest,\n currentRouteIds\n );\n const routeResults: RouteRenderResult[] = [];\n for (let index = 0; index < descriptors.length; index += 1) {\n const routeResult = routeResultsById.get(descriptors[index].routeId);\n if (routeResult) {\n routeResults.push(routeResult);\n }\n }\n routeResults.push(...removedResults);\n\n // Write HTML files to disk and remove stale output\n const writeStartTime = performance.now();\n await writeStaticFiles(routeResults, options.outputDir, {\n concurrency: resolvedConcurrency,\n });\n addPerfDuration('ssgWriteTimeMs', performance.now() - writeStartTime);\n\n // Generate result object\n result = generateSSGResult(routeResults, {\n mode: effectiveMode,\n cacheHits,\n invalidatedKeys: changedKeys,\n invalidatedRoutes: changedRoutes,\n });\n\n // Write metadata\n const metadata = resultToMetadata(result);\n await writeMetadata(metadata, options.outputDir);\n\n await writeIncrementalManifest(\n {\n schemaVersion: SSG_MANIFEST_SCHEMA_VERSION,\n seed,\n mode: effectiveMode,\n routes: nextManifestRoutes,\n },\n options.outputDir\n );\n\n return result;\n },\n\n /**\n * Get effective config in a serializable form for diagnostics.\n */\n getConfig() {\n return {\n routeCount: options.routes.length,\n outputDir: options.outputDir,\n seed,\n concurrency: resolvedConcurrency,\n parallelism: resolvedParallelism,\n hasDataOverrides: !!options.dataOverrides,\n };\n },\n\n /**\n * Get the last generation result\n * Returns null if generate() hasn't been called\n */\n getResult(): SSGResult | null {\n return result;\n },\n };\n}\n\nfunction dedupeStrings(values?: string[]): string[] {\n return values ? Array.from(new Set(values)) : [];\n}\n\nfunction getRoutesToRender(selected: SelectedRoute[]): SSGOptions['routes'] {\n const routes: SSGOptions['routes'] = [];\n for (let index = 0; index < selected.length; index += 1) {\n routes.push(selected[index].descriptor.route);\n }\n return routes;\n}\n\nfunction selectRouteForGeneration(\n descriptor: ResolvedRouteDescriptor,\n previous: IncrementalManifestRouteEntry | null,\n mode: SSGMode,\n changedKeys: string[],\n changedRoutes: string[]\n): SelectedRoute {\n if (mode === 'full') {\n return { descriptor, reason: 'full', previous };\n }\n\n if (!previous) {\n return { descriptor, reason: 'new-route', previous };\n }\n\n if (previous.lastStatus === 'error' || previous.htmlHash === null) {\n return { descriptor, reason: 'new-route', previous };\n }\n\n if (descriptor.invalidationKeys.length === 0) {\n return { descriptor, reason: 'no-keys', previous };\n }\n\n if (changedRoutes.includes(descriptor.path)) {\n return { descriptor, reason: 'changed-route', previous };\n }\n\n if (descriptor.invalidationKeys.some((key) => changedKeys.includes(key))) {\n return { descriptor, reason: 'changed-key', previous };\n }\n\n return { descriptor, reason: 'unchanged', previous };\n}\n\nfunction collectRemovedRouteResults(\n manifest: IncrementalManifest | null,\n currentRouteIds: Set<string>\n): RouteRenderResult[] {\n if (!manifest) {\n return [];\n }\n\n const results: RouteRenderResult[] = [];\n for (let index = 0; index < manifest.routes.length; index += 1) {\n const entry = manifest.routes[index];\n if (currentRouteIds.has(entry.routeId)) {\n continue;\n }\n results.push({\n path: entry.path,\n filePath: entry.filePath,\n html: '',\n fileSize: 0,\n renderDuration: 0,\n resourceCount: 0,\n status: 'removed' as const,\n reason: 'deleted' as const,\n written: false,\n });\n }\n\n return results;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"create-static-gen.js","names":[],"sources":["../../src/ssg/create-static-gen.ts"],"sourcesContent":["/**\n * Static Site Generation API\n *\n * Main orchestrator for batch rendering, file I/O, and metadata generation\n */\n\nimport type {\n RouteRenderReason,\n RouteRenderResult,\n SSGGenerateOptions,\n SSGMode,\n SSGOptions,\n SSGResult,\n} from './types';\nimport { resolveSsgData, validateRoutes } from './resolve-ssg-data';\nimport { batchRenderRoutes } from './batch-render';\nimport { writeStaticFiles } from './write-static-files';\nimport {\n generateSSGResult,\n resultToMetadata,\n writeMetadata,\n} from './generate-metadata';\nimport {\n getExistingOutputFileSize,\n outputFileExists,\n readIncrementalManifest,\n type IncrementalManifest,\n type IncrementalManifestRouteEntry,\n writeIncrementalManifest,\n hashHtml,\n SSG_MANIFEST_SCHEMA_VERSION,\n} from './incremental-manifest';\nimport {\n resolveRouteDescriptor,\n type ResolvedRouteDescriptor,\n} from './route-utils';\nimport { addPerfDuration, incrementPerfMetric } from '../runtime/perf-metrics';\n\ninterface SelectedRoute {\n descriptor: ResolvedRouteDescriptor;\n reason: RouteRenderReason;\n previous: IncrementalManifestRouteEntry | null;\n}\n\nfunction getRuntimeOnlyDiagnostic(\n descriptor: ResolvedRouteDescriptor\n): string | null {\n const { route } = descriptor;\n\n if (route.auth === true) {\n return `Skipped prerender for \"${descriptor.path}\": authenticated routes are runtime-only by default.`;\n }\n\n if (route.role) {\n return `Skipped prerender for \"${descriptor.path}\": role-gated routes are runtime-only by default.`;\n }\n\n if (route.permission) {\n return `Skipped prerender for \"${descriptor.path}\": permission-gated routes are runtime-only by default.`;\n }\n\n if (route.policies && route.policies.length > 0) {\n return `Skipped prerender for \"${descriptor.path}\": routes with custom policies are runtime-only by default.`;\n }\n\n return null;\n}\n\nfunction resolveParallelism(requested: number | 'auto' | undefined): number {\n if (requested !== 'auto') {\n return Math.max(1, requested ?? 1);\n }\n\n const maybeNavigator = globalThis as typeof globalThis & {\n navigator?: { hardwareConcurrency?: number };\n process?: { env?: Record<string, string | undefined> };\n };\n\n const envWorkers = Number(\n maybeNavigator.process?.env?.ASKR_SSG_WORKERS ??\n maybeNavigator.process?.env?.NUMBER_OF_PROCESSORS ??\n maybeNavigator.process?.env?.UV_THREADPOOL_SIZE\n );\n if (Number.isFinite(envWorkers) && envWorkers > 0) {\n return Math.max(1, Math.trunc(envWorkers));\n }\n\n if (typeof maybeNavigator.navigator?.hardwareConcurrency === 'number') {\n return Math.max(1, maybeNavigator.navigator.hardwareConcurrency);\n }\n\n return 1;\n}\n\n/**\n * Create a Static Site Generator\n *\n * Usage:\n * ```ts\n * const ssg = createStaticGen({\n * routes: [\n * { path: '/', component: HomePage },\n * { path: '/about', component: AboutPage },\n * ],\n * outputDir: './dist',\n * dataOverrides: {\n * '/api/posts': { posts: [...] }\n * }\n * });\n *\n * const result = await ssg.generate();\n * console.log(`Generated ${result.successful}/${result.totalRoutes} routes`);\n * ```\n */\nexport function createStaticGen(options: SSGOptions) {\n let result: SSGResult | null = null;\n const seed = options.seed ?? 12345;\n const resolvedParallelism = resolveParallelism(options.parallelism);\n const resolvedConcurrency = Math.max(\n 1,\n options.concurrency ?? resolvedParallelism\n );\n\n if (!Array.isArray(options.routes) || options.routes.length === 0) {\n throw new Error('routes array is required');\n }\n\n if (!options.outputDir || options.outputDir.trim().length === 0) {\n throw new Error('outputDir is required');\n }\n\n return {\n /**\n * Generate static HTML for all routes\n * Writes to outputDir with metadata.json\n * Returns detailed results\n */\n async generate(\n generateOptions: SSGGenerateOptions = {}\n ): Promise<SSGResult> {\n // Validate input\n validateRoutes(options.routes);\n\n const changedKeys = dedupeStrings(generateOptions.changedKeys);\n const changedRoutes = dedupeStrings(generateOptions.changedRoutes);\n const requestedMode = generateOptions.mode ?? 'full';\n const previousManifest =\n !generateOptions.forceFull && requestedMode === 'incremental'\n ? readIncrementalManifest(options.outputDir, seed)\n : null;\n const effectiveMode: SSGMode =\n generateOptions.forceFull ||\n requestedMode !== 'incremental' ||\n previousManifest === null\n ? 'full'\n : 'incremental';\n\n // Resolve data\n const dataMap = resolveSsgData(options.routes, {\n dataOverrides: options.dataOverrides,\n });\n\n const descriptors = options.routes.map(resolveRouteDescriptor);\n const routeResultsById = new Map<string, RouteRenderResult>();\n const eligibleDescriptors: ResolvedRouteDescriptor[] = [];\n\n for (const descriptor of descriptors) {\n const diagnostic = getRuntimeOnlyDiagnostic(descriptor);\n if (diagnostic) {\n routeResultsById.set(descriptor.routeId, {\n path: descriptor.path,\n filePath: descriptor.filePath,\n html: '',\n fileSize: 0,\n renderDuration: 0,\n resourceCount: 0,\n status: 'skipped',\n reason: 'runtime-only',\n written: false,\n error: diagnostic,\n });\n continue;\n }\n\n eligibleDescriptors.push(descriptor);\n }\n\n const previousEntries = new Map(\n (previousManifest?.routes ?? []).map((entry) => [entry.routeId, entry])\n );\n const currentRouteIds = new Set(\n eligibleDescriptors.map((descriptor) => descriptor.routeId)\n );\n\n const selected: SelectedRoute[] = [];\n const routesToRender: SelectedRoute[] = [];\n for (let index = 0; index < eligibleDescriptors.length; index += 1) {\n const descriptor = eligibleDescriptors[index];\n const entry = selectRouteForGeneration(\n descriptor,\n previousEntries.get(descriptor.routeId) ?? null,\n effectiveMode,\n changedKeys,\n changedRoutes\n );\n selected.push(entry);\n if (entry.reason !== 'unchanged') {\n routesToRender.push(entry);\n }\n }\n const renderStartTime = performance.now();\n incrementPerfMetric('ssgWorkerCount', resolvedParallelism);\n const renderedResults =\n routesToRender.length > 0\n ? await batchRenderRoutes(getRoutesToRender(routesToRender), {\n seed,\n dataMap,\n concurrency: resolvedConcurrency,\n })\n : [];\n addPerfDuration('ssgRenderTimeMs', performance.now() - renderStartTime);\n const renderedByRouteId = new Map<string, RouteRenderResult>();\n for (let index = 0; index < renderedResults.length; index += 1) {\n renderedByRouteId.set(\n routesToRender[index].descriptor.routeId,\n renderedResults[index]\n );\n }\n\n let cacheHits = 0;\n const nextManifestRoutes: IncrementalManifestRouteEntry[] = [];\n\n for (const entry of selected) {\n const { descriptor, previous, reason } = entry;\n const baseData =\n dataMap[descriptor.route.path] ?? dataMap[descriptor.path] ?? {};\n const resourceCount = Object.keys(baseData).length;\n\n if (reason === 'unchanged') {\n routeResultsById.set(descriptor.routeId, {\n path: descriptor.path,\n filePath: descriptor.filePath,\n html: '',\n fileSize:\n previous?.htmlHash !== null && previous !== null\n ? getExistingOutputFileSize(\n options.outputDir,\n descriptor.filePath\n )\n : 0,\n renderDuration: 0,\n resourceCount,\n status: 'skipped',\n reason: 'unchanged',\n written: false,\n });\n\n if (previous) {\n nextManifestRoutes.push({\n ...previous,\n path: descriptor.path,\n filePath: descriptor.filePath,\n invalidationKeys: descriptor.invalidationKeys.slice(),\n });\n }\n continue;\n }\n\n const rendered = renderedByRouteId.get(descriptor.routeId);\n if (!rendered) {\n throw new Error(\n `Missing rendered result for route \"${descriptor.path}\"`\n );\n }\n\n const nextResult: RouteRenderResult = {\n ...rendered,\n path: descriptor.path,\n filePath: descriptor.filePath,\n resourceCount,\n reason,\n written: false,\n };\n\n if (nextResult.status === 'success') {\n const htmlDigest = hashHtml(nextResult.html);\n const shouldWrite =\n effectiveMode === 'full' ||\n !previous ||\n previous.htmlHash !== htmlDigest ||\n !outputFileExists(options.outputDir, descriptor.filePath);\n\n nextResult.written = shouldWrite;\n if (!shouldWrite) {\n cacheHits += 1;\n }\n\n nextManifestRoutes.push({\n routeId: descriptor.routeId,\n path: descriptor.path,\n filePath: descriptor.filePath,\n invalidationKeys: descriptor.invalidationKeys.slice(),\n htmlHash: htmlDigest,\n lastStatus: 'success',\n });\n } else {\n nextManifestRoutes.push({\n routeId: descriptor.routeId,\n path: descriptor.path,\n filePath: descriptor.filePath,\n invalidationKeys: descriptor.invalidationKeys.slice(),\n htmlHash: previous?.htmlHash ?? null,\n lastStatus: 'error',\n });\n }\n\n routeResultsById.set(descriptor.routeId, nextResult);\n }\n\n const removedResults = collectRemovedRouteResults(\n previousManifest,\n currentRouteIds\n );\n const routeResults: RouteRenderResult[] = [];\n for (let index = 0; index < descriptors.length; index += 1) {\n const routeResult = routeResultsById.get(descriptors[index].routeId);\n if (routeResult) {\n routeResults.push(routeResult);\n }\n }\n routeResults.push(...removedResults);\n\n // Write HTML files to disk and remove stale output\n const writeStartTime = performance.now();\n await writeStaticFiles(routeResults, options.outputDir, {\n concurrency: resolvedConcurrency,\n });\n addPerfDuration('ssgWriteTimeMs', performance.now() - writeStartTime);\n\n // Generate result object\n result = generateSSGResult(routeResults, {\n mode: effectiveMode,\n cacheHits,\n invalidatedKeys: changedKeys,\n invalidatedRoutes: changedRoutes,\n });\n\n // Write metadata\n const metadata = resultToMetadata(result);\n await writeMetadata(metadata, options.outputDir);\n\n await writeIncrementalManifest(\n {\n schemaVersion: SSG_MANIFEST_SCHEMA_VERSION,\n seed,\n mode: effectiveMode,\n routes: nextManifestRoutes,\n },\n options.outputDir\n );\n\n return result;\n },\n\n /**\n * Get effective config in a serializable form for diagnostics.\n */\n getConfig() {\n return {\n routeCount: options.routes.length,\n outputDir: options.outputDir,\n seed,\n concurrency: resolvedConcurrency,\n parallelism: resolvedParallelism,\n hasDataOverrides: !!options.dataOverrides,\n };\n },\n\n /**\n * Get the last generation result\n * Returns null if generate() hasn't been called\n */\n getResult(): SSGResult | null {\n return result;\n },\n };\n}\n\nfunction dedupeStrings(values?: string[]): string[] {\n return values ? Array.from(new Set(values)) : [];\n}\n\nfunction getRoutesToRender(selected: SelectedRoute[]): SSGOptions['routes'] {\n const routes: SSGOptions['routes'] = [];\n for (let index = 0; index < selected.length; index += 1) {\n routes.push(selected[index].descriptor.route);\n }\n return routes;\n}\n\nfunction selectRouteForGeneration(\n descriptor: ResolvedRouteDescriptor,\n previous: IncrementalManifestRouteEntry | null,\n mode: SSGMode,\n changedKeys: string[],\n changedRoutes: string[]\n): SelectedRoute {\n if (mode === 'full') {\n return { descriptor, reason: 'full', previous };\n }\n\n if (!previous) {\n return { descriptor, reason: 'new-route', previous };\n }\n\n if (previous.lastStatus === 'error' || previous.htmlHash === null) {\n return { descriptor, reason: 'new-route', previous };\n }\n\n if (descriptor.invalidationKeys.length === 0) {\n return { descriptor, reason: 'no-keys', previous };\n }\n\n if (changedRoutes.includes(descriptor.path)) {\n return { descriptor, reason: 'changed-route', previous };\n }\n\n if (descriptor.invalidationKeys.some((key) => changedKeys.includes(key))) {\n return { descriptor, reason: 'changed-key', previous };\n }\n\n return { descriptor, reason: 'unchanged', previous };\n}\n\nfunction collectRemovedRouteResults(\n manifest: IncrementalManifest | null,\n currentRouteIds: Set<string>\n): RouteRenderResult[] {\n if (!manifest) {\n return [];\n }\n\n const results: RouteRenderResult[] = [];\n for (let index = 0; index < manifest.routes.length; index += 1) {\n const entry = manifest.routes[index];\n if (currentRouteIds.has(entry.routeId)) {\n continue;\n }\n results.push({\n path: entry.path,\n filePath: entry.filePath,\n html: '',\n fileSize: 0,\n renderDuration: 0,\n resourceCount: 0,\n status: 'removed' as const,\n reason: 'deleted' as const,\n written: false,\n });\n }\n\n return results;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-metadata.js","names":[],"sources":["../../src/ssg/generate-metadata.ts"],"sourcesContent":["/**\n * Metadata generation for SSG\n *\n * Generates and serializes metadata about generated static files.\n * This module is Node-only and not intended for browser builds.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as pathModule from 'node:path';\nimport type {\n RouteRenderResult,\n SSGMetadata,\n SSGMode,\n SSGResult,\n} from './types';\n\ninterface GenerateResultOptions {\n mode?: SSGMode;\n cacheHits?: number;\n invalidatedKeys?: string[];\n invalidatedRoutes?: string[];\n}\n\n/**\n * Generate SSGResult from render results\n */\nexport function generateSSGResult(\n results: RouteRenderResult[],\n options: GenerateResultOptions = {}\n): SSGResult {\n let successful = 0;\n let failed = 0;\n let totalDuration = 0;\n let rebuilt = 0;\n let skipped = 0;\n let removed = 0;\n\n for (let index = 0; index < results.length; index += 1) {\n const result = results[index];\n totalDuration += result.renderDuration;\n\n if (result.status === 'success') {\n successful += 1;\n rebuilt += 1;\n } else if (result.status === 'error') {\n failed += 1;\n rebuilt += 1;\n } else if (result.status === 'skipped') {\n skipped += 1;\n } else if (result.status === 'removed') {\n removed += 1;\n }\n }\n\n return {\n generatedAt: new Date().toISOString(),\n totalRoutes: results.length,\n successful,\n failed,\n totalDuration: Math.round(totalDuration),\n mode: options.mode ?? 'full',\n rebuilt,\n skipped,\n removed,\n cacheHits: options.cacheHits ?? 0,\n invalidatedKeys: options.invalidatedKeys?.slice() ?? [],\n invalidatedRoutes: options.invalidatedRoutes?.slice() ?? [],\n routes: results,\n };\n}\n\n/**\n * Convert SSGResult to metadata for JSON serialization\n */\nexport function resultToMetadata(result: SSGResult): SSGMetadata {\n const routes: SSGMetadata['routes'] = [];\n\n for (let index = 0; index < result.routes.length; index += 1) {\n const route = result.routes[index];\n routes.push({\n path: route.path,\n filePath: route.filePath,\n fileSize: route.fileSize,\n renderDuration: route.renderDuration,\n resourceCount: route.resourceCount,\n status: route.status,\n reason: route.reason,\n written: route.written,\n error: route.error,\n });\n }\n\n return {\n generatedAt: result.generatedAt,\n totalRoutes: result.totalRoutes,\n successful: result.successful,\n failed: result.failed,\n totalDuration: result.totalDuration,\n mode: result.mode,\n rebuilt: result.rebuilt,\n skipped: result.skipped,\n removed: result.removed,\n cacheHits: result.cacheHits,\n invalidatedKeys: result.invalidatedKeys.slice(),\n invalidatedRoutes: result.invalidatedRoutes.slice(),\n routes,\n };\n}\n\n/**\n * Write metadata.json to output directory\n */\nexport async function writeMetadata(\n metadata: Record<string, unknown>,\n outputDir: string\n): Promise<void> {\n const filePath = pathModule.join(outputDir, 'metadata.json');\n\n // Ensure directory exists\n await fs.mkdir(outputDir, { recursive: true });\n\n // Write metadata file with formatting\n await fs.writeFile(filePath, JSON.stringify(metadata, null, 2), 'utf8');\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,SAAgB,kBACd,SACA,UAAiC,EAAE,EACxB;CACX,IAAI,aAAa;CACjB,IAAI,SAAS;CACb,IAAI,gBAAgB;CACpB,IAAI,UAAU;CACd,IAAI,UAAU;CACd,IAAI,UAAU;
|
|
1
|
+
{"version":3,"file":"generate-metadata.js","names":[],"sources":["../../src/ssg/generate-metadata.ts"],"sourcesContent":["/**\n * Metadata generation for SSG\n *\n * Generates and serializes metadata about generated static files.\n * This module is Node-only and not intended for browser builds.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as pathModule from 'node:path';\nimport type {\n RouteRenderResult,\n SSGMetadata,\n SSGMode,\n SSGResult,\n} from './types';\n\ninterface GenerateResultOptions {\n mode?: SSGMode;\n cacheHits?: number;\n invalidatedKeys?: string[];\n invalidatedRoutes?: string[];\n}\n\n/**\n * Generate SSGResult from render results\n */\nexport function generateSSGResult(\n results: RouteRenderResult[],\n options: GenerateResultOptions = {}\n): SSGResult {\n let successful = 0;\n let failed = 0;\n let totalDuration = 0;\n let rebuilt = 0;\n let skipped = 0;\n let removed = 0;\n\n for (let index = 0; index < results.length; index += 1) {\n const result = results[index];\n totalDuration += result.renderDuration;\n\n if (result.status === 'success') {\n successful += 1;\n rebuilt += 1;\n } else if (result.status === 'error') {\n failed += 1;\n rebuilt += 1;\n } else if (result.status === 'skipped') {\n skipped += 1;\n } else if (result.status === 'removed') {\n removed += 1;\n }\n }\n\n return {\n generatedAt: new Date().toISOString(),\n totalRoutes: results.length,\n successful,\n failed,\n totalDuration: Math.round(totalDuration),\n mode: options.mode ?? 'full',\n rebuilt,\n skipped,\n removed,\n cacheHits: options.cacheHits ?? 0,\n invalidatedKeys: options.invalidatedKeys?.slice() ?? [],\n invalidatedRoutes: options.invalidatedRoutes?.slice() ?? [],\n routes: results,\n };\n}\n\n/**\n * Convert SSGResult to metadata for JSON serialization\n */\nexport function resultToMetadata(result: SSGResult): SSGMetadata {\n const routes: SSGMetadata['routes'] = [];\n\n for (let index = 0; index < result.routes.length; index += 1) {\n const route = result.routes[index];\n routes.push({\n path: route.path,\n filePath: route.filePath,\n fileSize: route.fileSize,\n renderDuration: route.renderDuration,\n resourceCount: route.resourceCount,\n status: route.status,\n reason: route.reason,\n written: route.written,\n error: route.error,\n });\n }\n\n return {\n generatedAt: result.generatedAt,\n totalRoutes: result.totalRoutes,\n successful: result.successful,\n failed: result.failed,\n totalDuration: result.totalDuration,\n mode: result.mode,\n rebuilt: result.rebuilt,\n skipped: result.skipped,\n removed: result.removed,\n cacheHits: result.cacheHits,\n invalidatedKeys: result.invalidatedKeys.slice(),\n invalidatedRoutes: result.invalidatedRoutes.slice(),\n routes,\n };\n}\n\n/**\n * Write metadata.json to output directory\n */\nexport async function writeMetadata(\n metadata: Record<string, unknown>,\n outputDir: string\n): Promise<void> {\n const filePath = pathModule.join(outputDir, 'metadata.json');\n\n // Ensure directory exists\n await fs.mkdir(outputDir, { recursive: true });\n\n // Write metadata file with formatting\n await fs.writeFile(filePath, JSON.stringify(metadata, null, 2), 'utf8');\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,SAAgB,kBACd,SACA,UAAiC,EAAE,EACxB;CACX,IAAI,aAAa;CACjB,IAAI,SAAS;CACb,IAAI,gBAAgB;CACpB,IAAI,UAAU;CACd,IAAI,UAAU;CACd,IAAI,UAAU;CAEd,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;EACtD,MAAM,SAAS,QAAQ;EACvB,iBAAiB,OAAO;EAExB,IAAI,OAAO,WAAW,WAAW;GAC/B,cAAc;GACd,WAAW;SACN,IAAI,OAAO,WAAW,SAAS;GACpC,UAAU;GACV,WAAW;SACN,IAAI,OAAO,WAAW,WAC3B,WAAW;OACN,IAAI,OAAO,WAAW,WAC3B,WAAW;;CAIf,OAAO;EACL,8BAAa,IAAI,MAAM,EAAC,aAAa;EACrC,aAAa,QAAQ;EACrB;EACA;EACA,eAAe,KAAK,MAAM,cAAc;EACxC,MAAM,QAAQ,QAAQ;EACtB;EACA;EACA;EACA,WAAW,QAAQ,aAAa;EAChC,iBAAiB,QAAQ,iBAAiB,OAAO,IAAI,EAAE;EACvD,mBAAmB,QAAQ,mBAAmB,OAAO,IAAI,EAAE;EAC3D,QAAQ;EACT;;;;;AAMH,SAAgB,iBAAiB,QAAgC;CAC/D,MAAM,SAAgC,EAAE;CAExC,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,OAAO,QAAQ,SAAS,GAAG;EAC5D,MAAM,QAAQ,OAAO,OAAO;EAC5B,OAAO,KAAK;GACV,MAAM,MAAM;GACZ,UAAU,MAAM;GAChB,UAAU,MAAM;GAChB,gBAAgB,MAAM;GACtB,eAAe,MAAM;GACrB,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,SAAS,MAAM;GACf,OAAO,MAAM;GACd,CAAC;;CAGJ,OAAO;EACL,aAAa,OAAO;EACpB,aAAa,OAAO;EACpB,YAAY,OAAO;EACnB,QAAQ,OAAO;EACf,eAAe,OAAO;EACtB,MAAM,OAAO;EACb,SAAS,OAAO;EAChB,SAAS,OAAO;EAChB,SAAS,OAAO;EAChB,WAAW,OAAO;EAClB,iBAAiB,OAAO,gBAAgB,OAAO;EAC/C,mBAAmB,OAAO,kBAAkB,OAAO;EACnD;EACD;;;;;AAMH,eAAsB,cACpB,UACA,WACe;CACf,MAAM,WAAW,WAAW,KAAK,WAAW,gBAAgB;CAG5D,MAAM,GAAG,MAAM,WAAW,EAAE,WAAW,MAAM,CAAC;CAG9C,MAAM,GAAG,UAAU,UAAU,KAAK,UAAU,UAAU,MAAM,EAAE,EAAE,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"incremental-manifest.js","names":[],"sources":["../../src/ssg/incremental-manifest.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as fsPromises from 'node:fs/promises';\nimport * as pathModule from 'node:path';\nimport type { RouteRenderStatus, SSGMode } from './types';\n\nexport const SSG_MANIFEST_SCHEMA_VERSION = 1;\nconst MANIFEST_DIR = '.askr';\nconst MANIFEST_FILE = 'ssg-manifest.json';\n\nexport interface IncrementalManifestRouteEntry {\n routeId: string;\n path: string;\n filePath: string;\n invalidationKeys: string[];\n htmlHash: string | null;\n lastStatus: Extract<RouteRenderStatus, 'success' | 'error'>;\n}\n\nexport interface IncrementalManifest {\n schemaVersion: number;\n seed: number;\n mode: SSGMode;\n routes: IncrementalManifestRouteEntry[];\n}\n\nexport function hashHtml(html: string): string {\n // Deterministic 32-bit FNV-1a hash is sufficient for incremental change checks.\n let hash = 0x811c9dc5;\n for (let index = 0; index < html.length; index += 1) {\n hash ^= html.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16).padStart(8, '0');\n}\n\nexport function getIncrementalManifestPath(outputDir: string): string {\n return pathModule.join(outputDir, MANIFEST_DIR, MANIFEST_FILE);\n}\n\nexport function readIncrementalManifest(\n outputDir: string,\n seed: number\n): IncrementalManifest | null {\n const manifestPath = getIncrementalManifestPath(outputDir);\n if (!fs.existsSync(manifestPath)) {\n return null;\n }\n\n try {\n const raw = fs.readFileSync(manifestPath, 'utf8');\n const parsed = JSON.parse(raw) as Partial<IncrementalManifest>;\n\n if (\n parsed.schemaVersion !== SSG_MANIFEST_SCHEMA_VERSION ||\n parsed.seed !== seed ||\n !Array.isArray(parsed.routes)\n ) {\n return null;\n }\n\n const routes = parsed.routes.filter(\n (entry): entry is IncrementalManifestRouteEntry =>\n !!entry &&\n typeof entry.routeId === 'string' &&\n typeof entry.path === 'string' &&\n typeof entry.filePath === 'string' &&\n Array.isArray(entry.invalidationKeys) &&\n (entry.htmlHash === null || typeof entry.htmlHash === 'string') &&\n (entry.lastStatus === 'success' || entry.lastStatus === 'error')\n );\n\n if (routes.length !== parsed.routes.length) {\n return null;\n }\n\n return {\n schemaVersion: SSG_MANIFEST_SCHEMA_VERSION,\n seed,\n mode: parsed.mode === 'incremental' ? 'incremental' : 'full',\n routes,\n };\n } catch {\n return null;\n }\n}\n\nexport async function writeIncrementalManifest(\n manifest: IncrementalManifest,\n outputDir: string\n): Promise<void> {\n const manifestPath = getIncrementalManifestPath(outputDir);\n await fsPromises.mkdir(pathModule.dirname(manifestPath), { recursive: true });\n await fsPromises.writeFile(\n manifestPath,\n JSON.stringify(manifest, null, 2),\n 'utf8'\n );\n}\n\nexport function getExistingOutputFileSize(\n outputDir: string,\n filePath: string\n): number {\n const fullPath = pathModule.join(outputDir, filePath);\n if (!fs.existsSync(fullPath)) {\n return 0;\n }\n\n try {\n return fs.statSync(fullPath).size;\n } catch {\n return 0;\n }\n}\n\nexport function outputFileExists(outputDir: string, filePath: string): boolean {\n return fs.existsSync(pathModule.join(outputDir, filePath));\n}\n"],"mappings":";;;AAMA,MAAM,eAAe;AACrB,MAAM,gBAAgB;AAkBtB,SAAgB,SAAS,MAAsB;CAE7C,IAAI,OAAO;
|
|
1
|
+
{"version":3,"file":"incremental-manifest.js","names":[],"sources":["../../src/ssg/incremental-manifest.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as fsPromises from 'node:fs/promises';\nimport * as pathModule from 'node:path';\nimport type { RouteRenderStatus, SSGMode } from './types';\n\nexport const SSG_MANIFEST_SCHEMA_VERSION = 1;\nconst MANIFEST_DIR = '.askr';\nconst MANIFEST_FILE = 'ssg-manifest.json';\n\nexport interface IncrementalManifestRouteEntry {\n routeId: string;\n path: string;\n filePath: string;\n invalidationKeys: string[];\n htmlHash: string | null;\n lastStatus: Extract<RouteRenderStatus, 'success' | 'error'>;\n}\n\nexport interface IncrementalManifest {\n schemaVersion: number;\n seed: number;\n mode: SSGMode;\n routes: IncrementalManifestRouteEntry[];\n}\n\nexport function hashHtml(html: string): string {\n // Deterministic 32-bit FNV-1a hash is sufficient for incremental change checks.\n let hash = 0x811c9dc5;\n for (let index = 0; index < html.length; index += 1) {\n hash ^= html.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16).padStart(8, '0');\n}\n\nexport function getIncrementalManifestPath(outputDir: string): string {\n return pathModule.join(outputDir, MANIFEST_DIR, MANIFEST_FILE);\n}\n\nexport function readIncrementalManifest(\n outputDir: string,\n seed: number\n): IncrementalManifest | null {\n const manifestPath = getIncrementalManifestPath(outputDir);\n if (!fs.existsSync(manifestPath)) {\n return null;\n }\n\n try {\n const raw = fs.readFileSync(manifestPath, 'utf8');\n const parsed = JSON.parse(raw) as Partial<IncrementalManifest>;\n\n if (\n parsed.schemaVersion !== SSG_MANIFEST_SCHEMA_VERSION ||\n parsed.seed !== seed ||\n !Array.isArray(parsed.routes)\n ) {\n return null;\n }\n\n const routes = parsed.routes.filter(\n (entry): entry is IncrementalManifestRouteEntry =>\n !!entry &&\n typeof entry.routeId === 'string' &&\n typeof entry.path === 'string' &&\n typeof entry.filePath === 'string' &&\n Array.isArray(entry.invalidationKeys) &&\n (entry.htmlHash === null || typeof entry.htmlHash === 'string') &&\n (entry.lastStatus === 'success' || entry.lastStatus === 'error')\n );\n\n if (routes.length !== parsed.routes.length) {\n return null;\n }\n\n return {\n schemaVersion: SSG_MANIFEST_SCHEMA_VERSION,\n seed,\n mode: parsed.mode === 'incremental' ? 'incremental' : 'full',\n routes,\n };\n } catch {\n return null;\n }\n}\n\nexport async function writeIncrementalManifest(\n manifest: IncrementalManifest,\n outputDir: string\n): Promise<void> {\n const manifestPath = getIncrementalManifestPath(outputDir);\n await fsPromises.mkdir(pathModule.dirname(manifestPath), { recursive: true });\n await fsPromises.writeFile(\n manifestPath,\n JSON.stringify(manifest, null, 2),\n 'utf8'\n );\n}\n\nexport function getExistingOutputFileSize(\n outputDir: string,\n filePath: string\n): number {\n const fullPath = pathModule.join(outputDir, filePath);\n if (!fs.existsSync(fullPath)) {\n return 0;\n }\n\n try {\n return fs.statSync(fullPath).size;\n } catch {\n return 0;\n }\n}\n\nexport function outputFileExists(outputDir: string, filePath: string): boolean {\n return fs.existsSync(pathModule.join(outputDir, filePath));\n}\n"],"mappings":";;;AAMA,MAAM,eAAe;AACrB,MAAM,gBAAgB;AAkBtB,SAAgB,SAAS,MAAsB;CAE7C,IAAI,OAAO;CACX,KAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,GAAG;EACnD,QAAQ,KAAK,WAAW,MAAM;EAC9B,OAAO,KAAK,KAAK,MAAM,SAAW;;CAEpC,QAAQ,SAAS,GAAG,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI;;AAGnD,SAAgB,2BAA2B,WAA2B;CACpE,OAAO,WAAW,KAAK,WAAW,cAAc,cAAc;;AAGhE,SAAgB,wBACd,WACA,MAC4B;CAC5B,MAAM,eAAe,2BAA2B,UAAU;CAC1D,IAAI,CAAC,GAAG,WAAW,aAAa,EAC9B,OAAO;CAGT,IAAI;EACF,MAAM,MAAM,GAAG,aAAa,cAAc,OAAO;EACjD,MAAM,SAAS,KAAK,MAAM,IAAI;EAE9B,IACE,OAAO,kBAAA,KACP,OAAO,SAAS,QAChB,CAAC,MAAM,QAAQ,OAAO,OAAO,EAE7B,OAAO;EAGT,MAAM,SAAS,OAAO,OAAO,QAC1B,UACC,CAAC,CAAC,SACF,OAAO,MAAM,YAAY,YACzB,OAAO,MAAM,SAAS,YACtB,OAAO,MAAM,aAAa,YAC1B,MAAM,QAAQ,MAAM,iBAAiB,KACpC,MAAM,aAAa,QAAQ,OAAO,MAAM,aAAa,cACrD,MAAM,eAAe,aAAa,MAAM,eAAe,SAC3D;EAED,IAAI,OAAO,WAAW,OAAO,OAAO,QAClC,OAAO;EAGT,OAAO;GACL,eAAA;GACA;GACA,MAAM,OAAO,SAAS,gBAAgB,gBAAgB;GACtD;GACD;SACK;EACN,OAAO;;;AAIX,eAAsB,yBACpB,UACA,WACe;CACf,MAAM,eAAe,2BAA2B,UAAU;CAC1D,MAAM,WAAW,MAAM,WAAW,QAAQ,aAAa,EAAE,EAAE,WAAW,MAAM,CAAC;CAC7E,MAAM,WAAW,UACf,cACA,KAAK,UAAU,UAAU,MAAM,EAAE,EACjC,OACD;;AAGH,SAAgB,0BACd,WACA,UACQ;CACR,MAAM,WAAW,WAAW,KAAK,WAAW,SAAS;CACrD,IAAI,CAAC,GAAG,WAAW,SAAS,EAC1B,OAAO;CAGT,IAAI;EACF,OAAO,GAAG,SAAS,SAAS,CAAC;SACvB;EACN,OAAO;;;AAIX,SAAgB,iBAAiB,WAAmB,UAA2B;CAC7E,OAAO,GAAG,WAAW,WAAW,KAAK,WAAW,SAAS,CAAC"}
|
package/dist/ssg/index.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
1
|
import { DiscoveredResources, RouteConfig, RouteRenderReason, RouteRenderResult, RouteRenderStatus, SSGGenerateOptions, SSGMetadata, SSGMode, SSGOptions, SSGResult } from "./types.js";
|
|
2
2
|
import { createStaticGen } from "./create-static-gen.js";
|
|
3
|
-
|
|
4
|
-
import { getOutputFilePath } from "./route-utils.js";
|
|
5
|
-
import { writeStaticFiles } from "./write-static-files.js";
|
|
6
|
-
import { generateSSGResult, resultToMetadata, writeMetadata } from "./generate-metadata.js";
|
|
7
|
-
import { resolveSsgData, validateRoutes } from "./resolve-ssg-data.js";
|
|
8
|
-
export { type DiscoveredResources, type RouteConfig, type RouteRenderReason, type RouteRenderResult, type RouteRenderStatus, type SSGGenerateOptions, type SSGMetadata, type SSGMode, type SSGOptions, type SSGResult, batchRenderRoutes, createStaticGen, generateSSGResult, getOutputFilePath, resolveSsgData, resultToMetadata, validateRoutes, writeMetadata, writeStaticFiles };
|
|
3
|
+
export { type DiscoveredResources, type RouteConfig, type RouteRenderReason, type RouteRenderResult, type RouteRenderStatus, type SSGGenerateOptions, type SSGMetadata, type SSGMode, type SSGOptions, type SSGResult, createStaticGen };
|
package/dist/ssg/index.js
CHANGED
|
@@ -1,7 +1,2 @@
|
|
|
1
|
-
import { resolveSsgData, validateRoutes } from "./resolve-ssg-data.js";
|
|
2
|
-
import { getOutputFilePath } from "./route-utils.js";
|
|
3
|
-
import { batchRenderRoutes } from "./batch-render.js";
|
|
4
|
-
import { writeStaticFiles } from "./write-static-files.js";
|
|
5
|
-
import { generateSSGResult, resultToMetadata, writeMetadata } from "./generate-metadata.js";
|
|
6
1
|
import { createStaticGen } from "./create-static-gen.js";
|
|
7
|
-
export {
|
|
2
|
+
export { createStaticGen };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-ssg-data.js","names":[],"sources":["../../src/ssg/resolve-ssg-data.ts"],"sourcesContent":["/**\n * Data resolution for SSG\n *\n * Merges user-supplied data overrides with any auto-discovered resources.\n * In phase 1, primarily handles user-supplied data.\n */\n\nimport type { SSRData } from '../common/ssr';\nimport type { RouteConfig } from './types';\n\ninterface DataResolutionOptions {\n /** User-supplied data overrides per route path */\n dataOverrides?: Record<string, unknown>;\n}\n\n/**\n * Resolve and validate data for SSG routes\n * Returns a map of route path -> SSRData\n *\n * In phase 1: accepts user-supplied dataOverrides\n * In phase 2: can be extended to auto-discover resources\n */\nexport function resolveSsgData(\n routes: RouteConfig[],\n options: DataResolutionOptions = {}\n): Record<string, SSRData> {\n const { dataOverrides = {} } = options;\n const dataMap: Record<string, SSRData> = {};\n\n for (const route of routes) {\n // Check if user provided data for this route\n if (route.path in dataOverrides) {\n const data = dataOverrides[route.path];\n if (typeof data !== 'object' || data === null || Array.isArray(data)) {\n throw new Error(\n `data for route \"${route.path}\" must be an object, got ${typeof data}`\n );\n }\n dataMap[route.path] = data as SSRData;\n }\n // In phase 1, routes without data are rendered with no SSR data\n // Phase 2 can add auto-discovery here\n }\n\n return dataMap;\n}\n\n/**\n * Validate routes are properly configured\n */\nexport function validateRoutes(routes: RouteConfig[]): void {\n if (!Array.isArray(routes)) {\n throw new Error('routes must be an array');\n }\n\n if (routes.length === 0) {\n throw new Error('routes array cannot be empty');\n }\n\n const seen = new Set<string>();\n\n for (const route of routes) {\n if (typeof route.path !== 'string' || !route.path.startsWith('/')) {\n throw new Error(\n `route path must be a string starting with \"/\", got \"${route.path}\"`\n );\n }\n\n const handler = route.handler ?? route.component;\n if (typeof handler !== 'function') {\n throw new Error(\n `route handler must be a function for path \"${route.path}\"`\n );\n }\n\n const key = `${route.path}::${JSON.stringify(route.params || {})}`;\n if (seen.has(key)) {\n throw new Error(\n `duplicate route entry detected for path \"${route.path}\"`\n );\n }\n seen.add(key);\n\n if (route.path.includes('{')) {\n const paramNames = Array.from(route.path.matchAll(/\\{([^}]+)\\}/g)).map(\n (m) => m[1]\n );\n if (!route.params) {\n throw new Error(\n `route \"${route.path}\" uses path parameters and requires params`\n );\n }\n for (const name of paramNames) {\n if (!(name in route.params)) {\n throw new Error(\n `route \"${route.path}\" missing required param \"${name}\"`\n );\n }\n }\n }\n\n if (route.invalidationKeys !== undefined) {\n if (!Array.isArray(route.invalidationKeys)) {\n throw new Error(\n `route \"${route.path}\" invalidationKeys must be an array of strings`\n );\n }\n for (const key of route.invalidationKeys) {\n if (typeof key !== 'string' || key.length === 0) {\n throw new Error(\n `route \"${route.path}\" invalidationKeys must contain only non-empty strings`\n );\n }\n }\n }\n }\n}\n"],"mappings":";;;;;;;;AAsBA,SAAgB,eACd,QACA,UAAiC,EAAE,EACV;CACzB,MAAM,EAAE,gBAAgB,EAAE,KAAK;CAC/B,MAAM,UAAmC,EAAE;
|
|
1
|
+
{"version":3,"file":"resolve-ssg-data.js","names":[],"sources":["../../src/ssg/resolve-ssg-data.ts"],"sourcesContent":["/**\n * Data resolution for SSG\n *\n * Merges user-supplied data overrides with any auto-discovered resources.\n * In phase 1, primarily handles user-supplied data.\n */\n\nimport type { SSRData } from '../common/ssr';\nimport type { RouteConfig } from './types';\n\ninterface DataResolutionOptions {\n /** User-supplied data overrides per route path */\n dataOverrides?: Record<string, unknown>;\n}\n\n/**\n * Resolve and validate data for SSG routes\n * Returns a map of route path -> SSRData\n *\n * In phase 1: accepts user-supplied dataOverrides\n * In phase 2: can be extended to auto-discover resources\n */\nexport function resolveSsgData(\n routes: RouteConfig[],\n options: DataResolutionOptions = {}\n): Record<string, SSRData> {\n const { dataOverrides = {} } = options;\n const dataMap: Record<string, SSRData> = {};\n\n for (const route of routes) {\n // Check if user provided data for this route\n if (route.path in dataOverrides) {\n const data = dataOverrides[route.path];\n if (typeof data !== 'object' || data === null || Array.isArray(data)) {\n throw new Error(\n `data for route \"${route.path}\" must be an object, got ${typeof data}`\n );\n }\n dataMap[route.path] = data as SSRData;\n }\n // In phase 1, routes without data are rendered with no SSR data\n // Phase 2 can add auto-discovery here\n }\n\n return dataMap;\n}\n\n/**\n * Validate routes are properly configured\n */\nexport function validateRoutes(routes: RouteConfig[]): void {\n if (!Array.isArray(routes)) {\n throw new Error('routes must be an array');\n }\n\n if (routes.length === 0) {\n throw new Error('routes array cannot be empty');\n }\n\n const seen = new Set<string>();\n\n for (const route of routes) {\n if (typeof route.path !== 'string' || !route.path.startsWith('/')) {\n throw new Error(\n `route path must be a string starting with \"/\", got \"${route.path}\"`\n );\n }\n\n const handler = route.handler ?? route.component;\n if (typeof handler !== 'function') {\n throw new Error(\n `route handler must be a function for path \"${route.path}\"`\n );\n }\n\n const key = `${route.path}::${JSON.stringify(route.params || {})}`;\n if (seen.has(key)) {\n throw new Error(\n `duplicate route entry detected for path \"${route.path}\"`\n );\n }\n seen.add(key);\n\n if (route.path.includes('{')) {\n const paramNames = Array.from(route.path.matchAll(/\\{([^}]+)\\}/g)).map(\n (m) => m[1]\n );\n if (!route.params) {\n throw new Error(\n `route \"${route.path}\" uses path parameters and requires params`\n );\n }\n for (const name of paramNames) {\n if (!(name in route.params)) {\n throw new Error(\n `route \"${route.path}\" missing required param \"${name}\"`\n );\n }\n }\n }\n\n if (route.invalidationKeys !== undefined) {\n if (!Array.isArray(route.invalidationKeys)) {\n throw new Error(\n `route \"${route.path}\" invalidationKeys must be an array of strings`\n );\n }\n for (const key of route.invalidationKeys) {\n if (typeof key !== 'string' || key.length === 0) {\n throw new Error(\n `route \"${route.path}\" invalidationKeys must contain only non-empty strings`\n );\n }\n }\n }\n }\n}\n"],"mappings":";;;;;;;;AAsBA,SAAgB,eACd,QACA,UAAiC,EAAE,EACV;CACzB,MAAM,EAAE,gBAAgB,EAAE,KAAK;CAC/B,MAAM,UAAmC,EAAE;CAE3C,KAAK,MAAM,SAAS,QAElB,IAAI,MAAM,QAAQ,eAAe;EAC/B,MAAM,OAAO,cAAc,MAAM;EACjC,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,KAAK,EAClE,MAAM,IAAI,MACR,mBAAmB,MAAM,KAAK,2BAA2B,OAAO,OACjE;EAEH,QAAQ,MAAM,QAAQ;;CAM1B,OAAO;;;;;AAMT,SAAgB,eAAe,QAA6B;CAC1D,IAAI,CAAC,MAAM,QAAQ,OAAO,EACxB,MAAM,IAAI,MAAM,0BAA0B;CAG5C,IAAI,OAAO,WAAW,GACpB,MAAM,IAAI,MAAM,+BAA+B;CAGjD,MAAM,uBAAO,IAAI,KAAa;CAE9B,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,OAAO,MAAM,SAAS,YAAY,CAAC,MAAM,KAAK,WAAW,IAAI,EAC/D,MAAM,IAAI,MACR,uDAAuD,MAAM,KAAK,GACnE;EAIH,IAAI,QADY,MAAM,WAAW,MAAM,eAChB,YACrB,MAAM,IAAI,MACR,8CAA8C,MAAM,KAAK,GAC1D;EAGH,MAAM,MAAM,GAAG,MAAM,KAAK,IAAI,KAAK,UAAU,MAAM,UAAU,EAAE,CAAC;EAChE,IAAI,KAAK,IAAI,IAAI,EACf,MAAM,IAAI,MACR,4CAA4C,MAAM,KAAK,GACxD;EAEH,KAAK,IAAI,IAAI;EAEb,IAAI,MAAM,KAAK,SAAS,IAAI,EAAE;GAC5B,MAAM,aAAa,MAAM,KAAK,MAAM,KAAK,SAAS,eAAe,CAAC,CAAC,KAChE,MAAM,EAAE,GACV;GACD,IAAI,CAAC,MAAM,QACT,MAAM,IAAI,MACR,UAAU,MAAM,KAAK,4CACtB;GAEH,KAAK,MAAM,QAAQ,YACjB,IAAI,EAAE,QAAQ,MAAM,SAClB,MAAM,IAAI,MACR,UAAU,MAAM,KAAK,4BAA4B,KAAK,GACvD;;EAKP,IAAI,MAAM,qBAAqB,QAAW;GACxC,IAAI,CAAC,MAAM,QAAQ,MAAM,iBAAiB,EACxC,MAAM,IAAI,MACR,UAAU,MAAM,KAAK,gDACtB;GAEH,KAAK,MAAM,OAAO,MAAM,kBACtB,IAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,GAC5C,MAAM,IAAI,MACR,UAAU,MAAM,KAAK,wDACtB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-utils.js","names":[],"sources":["../../src/ssg/route-utils.ts"],"sourcesContent":["import type { RouteConfig } from './types';\n\nexport interface ResolvedRouteDescriptor {\n route: RouteConfig;\n path: string;\n filePath: string;\n routeId: string;\n invalidationKeys: string[];\n}\n\nexport function getOutputFilePath(pathStr: string): string {\n if (pathStr === '/') {\n return 'index.html';\n }\n const normalized = pathStr.replace(/^\\/|\\/$/g, '');\n return `${normalized}/index.html`;\n}\n\nexport function interpolateRoutePath(\n routePath: string,\n params?: Record<string, string>\n): string {\n if (!params) return routePath;\n return routePath.replace(\n /\\{([^}]+)\\}/g,\n (_, key: string) => params[key] ?? ''\n );\n}\n\nexport function resolveRouteDescriptor(\n route: RouteConfig\n): ResolvedRouteDescriptor {\n const path = interpolateRoutePath(route.path, route.params);\n const filePath = getOutputFilePath(path);\n return {\n route,\n path,\n filePath,\n routeId: `${path}::${filePath}`,\n invalidationKeys: route.invalidationKeys?.slice() ?? [],\n };\n}\n"],"mappings":";AAUA,SAAgB,kBAAkB,SAAyB;
|
|
1
|
+
{"version":3,"file":"route-utils.js","names":[],"sources":["../../src/ssg/route-utils.ts"],"sourcesContent":["import type { RouteConfig } from './types';\n\nexport interface ResolvedRouteDescriptor {\n route: RouteConfig;\n path: string;\n filePath: string;\n routeId: string;\n invalidationKeys: string[];\n}\n\nexport function getOutputFilePath(pathStr: string): string {\n if (pathStr === '/') {\n return 'index.html';\n }\n const normalized = pathStr.replace(/^\\/|\\/$/g, '');\n return `${normalized}/index.html`;\n}\n\nexport function interpolateRoutePath(\n routePath: string,\n params?: Record<string, string>\n): string {\n if (!params) return routePath;\n return routePath.replace(\n /\\{([^}]+)\\}/g,\n (_, key: string) => params[key] ?? ''\n );\n}\n\nexport function resolveRouteDescriptor(\n route: RouteConfig\n): ResolvedRouteDescriptor {\n const path = interpolateRoutePath(route.path, route.params);\n const filePath = getOutputFilePath(path);\n return {\n route,\n path,\n filePath,\n routeId: `${path}::${filePath}`,\n invalidationKeys: route.invalidationKeys?.slice() ?? [],\n };\n}\n"],"mappings":";AAUA,SAAgB,kBAAkB,SAAyB;CACzD,IAAI,YAAY,KACd,OAAO;CAGT,OAAO,GADY,QAAQ,QAAQ,YAAY,GACrC,CAAW;;AAGvB,SAAgB,qBACd,WACA,QACQ;CACR,IAAI,CAAC,QAAQ,OAAO;CACpB,OAAO,UAAU,QACf,iBACC,GAAG,QAAgB,OAAO,QAAQ,GACpC;;AAGH,SAAgB,uBACd,OACyB;CACzB,MAAM,OAAO,qBAAqB,MAAM,MAAM,MAAM,OAAO;CAC3D,MAAM,WAAW,kBAAkB,KAAK;CACxC,OAAO;EACL;EACA;EACA;EACA,SAAS,GAAG,KAAK,IAAI;EACrB,kBAAkB,MAAM,kBAAkB,OAAO,IAAI,EAAE;EACxD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-static-files.js","names":[],"sources":["../../src/ssg/write-static-files.ts"],"sourcesContent":["/**\n * File I/O for Static Site Generation\n *\n * Uses Node.js fs/path modules to write rendered HTML files to disk.\n * This module is Node-only and not intended for browser builds.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as fsSync from 'node:fs';\nimport * as pathModule from 'node:path';\nimport type { RouteRenderResult } from './types';\n\ninterface WriteStaticFilesOptions {\n concurrency?: number;\n}\n\n/**\n * Write rendered routes to disk\n * Creates outputDir/{route-path}/index.html structure\n */\nexport async function writeStaticFiles(\n results: RouteRenderResult[],\n outputDir: string,\n options: WriteStaticFilesOptions = {}\n): Promise<void> {\n await fs.mkdir(outputDir, { recursive: true });\n\n for (const result of results) {\n if (result.status !== 'removed') {\n continue;\n }\n\n const fullPath = pathModule.join(outputDir, result.filePath);\n if (fsSync.existsSync(fullPath)) {\n await fs.rm(fullPath, { force: true });\n await pruneEmptyDirs(pathModule.dirname(fullPath), outputDir);\n }\n }\n\n const pendingWrites: RouteRenderResult[] = [];\n for (let index = 0; index < results.length; index += 1) {\n const result = results[index];\n if (result.status === 'error') {\n console.warn(`Skipping failed route: ${result.path} - ${result.error}`);\n continue;\n }\n if (result.status === 'success' && result.written) {\n pendingWrites.push(result);\n }\n }\n\n const directories: string[] = [];\n const seenDirectories = new Set<string>();\n for (const result of pendingWrites) {\n const dir = pathModule.dirname(pathModule.join(outputDir, result.filePath));\n if (!seenDirectories.has(dir)) {\n seenDirectories.add(dir);\n directories.push(dir);\n }\n }\n\n for (const dir of directories) {\n await fs.mkdir(dir, { recursive: true });\n }\n\n const concurrency = Math.max(\n 1,\n Math.min(options.concurrency ?? 8, pendingWrites.length || 1)\n );\n let nextIndex = 0;\n\n const worker = async () => {\n while (true) {\n const current = nextIndex;\n nextIndex += 1;\n if (current >= pendingWrites.length) {\n return;\n }\n\n const result = pendingWrites[current];\n const fullPath = pathModule.join(outputDir, result.filePath);\n await fs.writeFile(fullPath, result.html, 'utf8');\n }\n };\n\n await Promise.all(Array.from({ length: concurrency }, () => worker()));\n}\n\n/**\n * Get the output file path for a route\n * E.g., \"/blog/post\" -> \"blog/post\" or \"/\" -> \"\"\n */\nexport { getOutputFilePath } from './route-utils';\n\nasync function pruneEmptyDirs(\n startDir: string,\n rootDir: string\n): Promise<void> {\n let current = startDir;\n const normalizedRoot = pathModule.resolve(rootDir);\n\n while (current.startsWith(normalizedRoot)) {\n if (!fsSync.existsSync(current)) {\n break;\n }\n\n if ((await fs.readdir(current)).length > 0) {\n break;\n }\n\n await fs.rmdir(current);\n if (pathModule.resolve(current) === normalizedRoot) {\n break;\n }\n current = pathModule.dirname(current);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAoBA,eAAsB,iBACpB,SACA,WACA,UAAmC,EAAE,EACtB;
|
|
1
|
+
{"version":3,"file":"write-static-files.js","names":[],"sources":["../../src/ssg/write-static-files.ts"],"sourcesContent":["/**\n * File I/O for Static Site Generation\n *\n * Uses Node.js fs/path modules to write rendered HTML files to disk.\n * This module is Node-only and not intended for browser builds.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as fsSync from 'node:fs';\nimport * as pathModule from 'node:path';\nimport type { RouteRenderResult } from './types';\n\ninterface WriteStaticFilesOptions {\n concurrency?: number;\n}\n\n/**\n * Write rendered routes to disk\n * Creates outputDir/{route-path}/index.html structure\n */\nexport async function writeStaticFiles(\n results: RouteRenderResult[],\n outputDir: string,\n options: WriteStaticFilesOptions = {}\n): Promise<void> {\n await fs.mkdir(outputDir, { recursive: true });\n\n for (const result of results) {\n if (result.status !== 'removed') {\n continue;\n }\n\n const fullPath = pathModule.join(outputDir, result.filePath);\n if (fsSync.existsSync(fullPath)) {\n await fs.rm(fullPath, { force: true });\n await pruneEmptyDirs(pathModule.dirname(fullPath), outputDir);\n }\n }\n\n const pendingWrites: RouteRenderResult[] = [];\n for (let index = 0; index < results.length; index += 1) {\n const result = results[index];\n if (result.status === 'error') {\n console.warn(`Skipping failed route: ${result.path} - ${result.error}`);\n continue;\n }\n if (result.status === 'success' && result.written) {\n pendingWrites.push(result);\n }\n }\n\n const directories: string[] = [];\n const seenDirectories = new Set<string>();\n for (const result of pendingWrites) {\n const dir = pathModule.dirname(pathModule.join(outputDir, result.filePath));\n if (!seenDirectories.has(dir)) {\n seenDirectories.add(dir);\n directories.push(dir);\n }\n }\n\n for (const dir of directories) {\n await fs.mkdir(dir, { recursive: true });\n }\n\n const concurrency = Math.max(\n 1,\n Math.min(options.concurrency ?? 8, pendingWrites.length || 1)\n );\n let nextIndex = 0;\n\n const worker = async () => {\n while (true) {\n const current = nextIndex;\n nextIndex += 1;\n if (current >= pendingWrites.length) {\n return;\n }\n\n const result = pendingWrites[current];\n const fullPath = pathModule.join(outputDir, result.filePath);\n await fs.writeFile(fullPath, result.html, 'utf8');\n }\n };\n\n await Promise.all(Array.from({ length: concurrency }, () => worker()));\n}\n\n/**\n * Get the output file path for a route\n * E.g., \"/blog/post\" -> \"blog/post\" or \"/\" -> \"\"\n */\nexport { getOutputFilePath } from './route-utils';\n\nasync function pruneEmptyDirs(\n startDir: string,\n rootDir: string\n): Promise<void> {\n let current = startDir;\n const normalizedRoot = pathModule.resolve(rootDir);\n\n while (current.startsWith(normalizedRoot)) {\n if (!fsSync.existsSync(current)) {\n break;\n }\n\n if ((await fs.readdir(current)).length > 0) {\n break;\n }\n\n await fs.rmdir(current);\n if (pathModule.resolve(current) === normalizedRoot) {\n break;\n }\n current = pathModule.dirname(current);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAoBA,eAAsB,iBACpB,SACA,WACA,UAAmC,EAAE,EACtB;CACf,MAAM,GAAG,MAAM,WAAW,EAAE,WAAW,MAAM,CAAC;CAE9C,KAAK,MAAM,UAAU,SAAS;EAC5B,IAAI,OAAO,WAAW,WACpB;EAGF,MAAM,WAAW,WAAW,KAAK,WAAW,OAAO,SAAS;EAC5D,IAAI,OAAO,WAAW,SAAS,EAAE;GAC/B,MAAM,GAAG,GAAG,UAAU,EAAE,OAAO,MAAM,CAAC;GACtC,MAAM,eAAe,WAAW,QAAQ,SAAS,EAAE,UAAU;;;CAIjE,MAAM,gBAAqC,EAAE;CAC7C,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;EACtD,MAAM,SAAS,QAAQ;EACvB,IAAI,OAAO,WAAW,SAAS;GAC7B,QAAQ,KAAK,0BAA0B,OAAO,KAAK,KAAK,OAAO,QAAQ;GACvE;;EAEF,IAAI,OAAO,WAAW,aAAa,OAAO,SACxC,cAAc,KAAK,OAAO;;CAI9B,MAAM,cAAwB,EAAE;CAChC,MAAM,kCAAkB,IAAI,KAAa;CACzC,KAAK,MAAM,UAAU,eAAe;EAClC,MAAM,MAAM,WAAW,QAAQ,WAAW,KAAK,WAAW,OAAO,SAAS,CAAC;EAC3E,IAAI,CAAC,gBAAgB,IAAI,IAAI,EAAE;GAC7B,gBAAgB,IAAI,IAAI;GACxB,YAAY,KAAK,IAAI;;;CAIzB,KAAK,MAAM,OAAO,aAChB,MAAM,GAAG,MAAM,KAAK,EAAE,WAAW,MAAM,CAAC;CAG1C,MAAM,cAAc,KAAK,IACvB,GACA,KAAK,IAAI,QAAQ,eAAe,GAAG,cAAc,UAAU,EAAE,CAC9D;CACD,IAAI,YAAY;CAEhB,MAAM,SAAS,YAAY;EACzB,OAAO,MAAM;GACX,MAAM,UAAU;GAChB,aAAa;GACb,IAAI,WAAW,cAAc,QAC3B;GAGF,MAAM,SAAS,cAAc;GAC7B,MAAM,WAAW,WAAW,KAAK,WAAW,OAAO,SAAS;GAC5D,MAAM,GAAG,UAAU,UAAU,OAAO,MAAM,OAAO;;;CAIrD,MAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,aAAa,QAAQ,QAAQ,CAAC,CAAC;;AASxE,eAAe,eACb,UACA,SACe;CACf,IAAI,UAAU;CACd,MAAM,iBAAiB,WAAW,QAAQ,QAAQ;CAElD,OAAO,QAAQ,WAAW,eAAe,EAAE;EACzC,IAAI,CAAC,OAAO,WAAW,QAAQ,EAC7B;EAGF,KAAK,MAAM,GAAG,QAAQ,QAAQ,EAAE,SAAS,GACvC;EAGF,MAAM,GAAG,MAAM,QAAQ;EACvB,IAAI,WAAW,QAAQ,QAAQ,KAAK,gBAClC;EAEF,UAAU,WAAW,QAAQ,QAAQ"}
|
package/dist/ssr/attrs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attrs.js","names":[],"sources":["../../src/ssr/attrs.ts"],"sourcesContent":["/**\n * HTML attribute rendering for SSR\n */\n\nimport type { Props } from '../common/props';\nimport type { RenderSink } from './sink';\nimport { escapeAttr, needsEscapeAttr, styleObjToCss } from './escape';\n\n/** Result of renderAttrs including any raw HTML from dangerouslySetInnerHTML */\nexport type AttrsResult = {\n attrs: string;\n dangerousHtml?: string;\n};\n\nconst ESCAPED_ATTR_VALUE_CACHE_LIMIT = 512;\nconst escapedAttrValueCache = new Map<string, string>();\n\n// Fast check for event handler pattern (on + uppercase letter)\nfunction isEventHandler(key: string): boolean {\n return (\n key.length >= 3 &&\n key.charCodeAt(0) === 111 && // 'o'\n key.charCodeAt(1) === 110 && // 'n'\n key.charCodeAt(2) >= 65 &&\n key.charCodeAt(2) <= 90 // 'A'-'Z'\n );\n}\n\nfunction getEscapedAttrValue(value: string): string {\n if (value.length > 64) {\n return escapeAttr(value);\n }\n\n const cached = escapedAttrValueCache.get(value);\n if (cached !== undefined) {\n return cached;\n }\n\n const escaped = escapeAttr(value);\n // Skip caching strings that required no escaping — same reference returned,\n // no point occupying cache slots with identity entries.\n if (escaped === value) return value;\n if (escapedAttrValueCache.size >= ESCAPED_ATTR_VALUE_CACHE_LIMIT) {\n escapedAttrValueCache.clear();\n }\n escapedAttrValueCache.set(value, escaped);\n return escaped;\n}\n\n/**\n * Render attributes directly to a sink without intermediate string allocations.\n * This is the hot path for streaming SSR.\n */\nexport function renderAttrsDirect(\n props: Props | undefined,\n sink: Pick<RenderSink, 'write'>\n): void {\n if (!props || typeof props !== 'object') return;\n\n const propsObj = props as Record<string, unknown>;\n for (const key in propsObj) {\n const value = propsObj[key];\n\n // Skip special props\n if (\n key === 'children' ||\n key === 'key' ||\n key === 'ref' ||\n key === 'dangerouslySetInnerHTML'\n )\n continue;\n\n // Skip event handlers\n if (isEventHandler(key)) continue;\n\n // Skip internal props\n if (key.charCodeAt(0) === 95) continue; // '_'\n\n // Normalize class attribute\n const attrName = key === 'className' ? 'class' : key;\n\n // Handle style objects\n if (attrName === 'style') {\n const css = typeof value === 'string' ? value : styleObjToCss(value);\n if (!css) continue;\n sink.write(' style=\"');\n // Escape inline - most style values don't need escaping\n if (needsEscapeAttr(css)) {\n sink.write(getEscapedAttrValue(css));\n } else {\n sink.write(css);\n }\n sink.write('\"');\n continue;\n }\n\n // Boolean attributes\n if (value === true) {\n sink.write(' ');\n sink.write(attrName);\n continue;\n }\n\n if (value === false || value === null || value === undefined) continue;\n\n // Regular attributes\n const strValue = String(value);\n sink.write(' ');\n sink.write(attrName);\n sink.write('=\"');\n // escapeAttr returns the original string when nothing needs escaping,\n // so no separate needsEscapeAttr pre-scan is required.\n sink.write(getEscapedAttrValue(strValue));\n sink.write('\"');\n }\n}\n\n/**\n * Render attributes to HTML string, excluding event handlers\n * Optimized for minimal allocations using push-based approach\n *\n * Returns both the attribute string and any dangerouslySetInnerHTML content.\n */\nexport function renderAttrs(props?: Props): string;\nexport function renderAttrs(\n props: Props | undefined,\n opts: { returnDangerousHtml: true }\n): AttrsResult;\nexport function renderAttrs(\n props?: Props,\n opts?: { returnDangerousHtml?: boolean }\n): string | AttrsResult {\n if (!props || typeof props !== 'object') {\n return opts?.returnDangerousHtml ? { attrs: '' } : '';\n }\n\n const attrParts: string[] = [];\n let dangerousHtml: string | undefined;\n\n const propsObj = props as Record<string, unknown>;\n for (const key in propsObj) {\n const value = propsObj[key];\n\n // Skip children in attrs\n if (key === 'children') continue;\n\n // Skip internal identity refs (framework-only)\n if (key === 'key' || key === 'ref') continue;\n\n // Handle dangerouslySetInnerHTML\n if (key === 'dangerouslySetInnerHTML') {\n if (value && typeof value === 'object' && '__html' in (value as object)) {\n dangerousHtml = String((value as { __html: unknown }).__html);\n }\n continue;\n }\n\n // Skip event handlers\n if (isEventHandler(key)) continue;\n\n // Skip internal props\n if (key.charCodeAt(0) === 95) continue; // '_'\n\n // Normalize class attribute (`class` preferred, accept `className` for compatibility)\n const attrName = key === 'class' || key === 'className' ? 'class' : key;\n\n // Handle style objects\n if (attrName === 'style') {\n const css = typeof value === 'string' ? value : styleObjToCss(value);\n if (css === null || css === '') continue;\n attrParts.push(` style=\"${getEscapedAttrValue(css)}\"`);\n continue;\n }\n\n // Boolean attributes\n if (value === true) {\n attrParts.push(` ${attrName}`);\n } else if (value === false || value === null || value === undefined) {\n continue;\n } else {\n const strValue = String(value);\n attrParts.push(` ${attrName}=\"${getEscapedAttrValue(strValue)}\"`);\n }\n }\n\n const result = attrParts.join('');\n\n if (opts?.returnDangerousHtml) {\n return { attrs: result, dangerousHtml };\n }\n return result;\n}\n"],"mappings":";;AAcA,MAAM,iCAAiC;AACvC,MAAM,wCAAwB,IAAI,KAAqB;AAGvD,SAAS,eAAe,KAAsB;
|
|
1
|
+
{"version":3,"file":"attrs.js","names":[],"sources":["../../src/ssr/attrs.ts"],"sourcesContent":["/**\n * HTML attribute rendering for SSR\n */\n\nimport type { Props } from '../common/props';\nimport type { RenderSink } from './sink';\nimport { escapeAttr, needsEscapeAttr, styleObjToCss } from './escape';\n\n/** Result of renderAttrs including any raw HTML from dangerouslySetInnerHTML */\nexport type AttrsResult = {\n attrs: string;\n dangerousHtml?: string;\n};\n\nconst ESCAPED_ATTR_VALUE_CACHE_LIMIT = 512;\nconst escapedAttrValueCache = new Map<string, string>();\n\n// Fast check for event handler pattern (on + uppercase letter)\nfunction isEventHandler(key: string): boolean {\n return (\n key.length >= 3 &&\n key.charCodeAt(0) === 111 && // 'o'\n key.charCodeAt(1) === 110 && // 'n'\n key.charCodeAt(2) >= 65 &&\n key.charCodeAt(2) <= 90 // 'A'-'Z'\n );\n}\n\nfunction getEscapedAttrValue(value: string): string {\n if (value.length > 64) {\n return escapeAttr(value);\n }\n\n const cached = escapedAttrValueCache.get(value);\n if (cached !== undefined) {\n return cached;\n }\n\n const escaped = escapeAttr(value);\n // Skip caching strings that required no escaping — same reference returned,\n // no point occupying cache slots with identity entries.\n if (escaped === value) return value;\n if (escapedAttrValueCache.size >= ESCAPED_ATTR_VALUE_CACHE_LIMIT) {\n escapedAttrValueCache.clear();\n }\n escapedAttrValueCache.set(value, escaped);\n return escaped;\n}\n\n/**\n * Render attributes directly to a sink without intermediate string allocations.\n * This is the hot path for streaming SSR.\n */\nexport function renderAttrsDirect(\n props: Props | undefined,\n sink: Pick<RenderSink, 'write'>\n): void {\n if (!props || typeof props !== 'object') return;\n\n const propsObj = props as Record<string, unknown>;\n for (const key in propsObj) {\n const value = propsObj[key];\n\n // Skip special props\n if (\n key === 'children' ||\n key === 'key' ||\n key === 'ref' ||\n key === 'dangerouslySetInnerHTML'\n )\n continue;\n\n // Skip event handlers\n if (isEventHandler(key)) continue;\n\n // Skip internal props\n if (key.charCodeAt(0) === 95) continue; // '_'\n\n // Normalize class attribute\n const attrName = key === 'className' ? 'class' : key;\n\n // Handle style objects\n if (attrName === 'style') {\n const css = typeof value === 'string' ? value : styleObjToCss(value);\n if (!css) continue;\n sink.write(' style=\"');\n // Escape inline - most style values don't need escaping\n if (needsEscapeAttr(css)) {\n sink.write(getEscapedAttrValue(css));\n } else {\n sink.write(css);\n }\n sink.write('\"');\n continue;\n }\n\n // Boolean attributes\n if (value === true) {\n sink.write(' ');\n sink.write(attrName);\n continue;\n }\n\n if (value === false || value === null || value === undefined) continue;\n\n // Regular attributes\n const strValue = String(value);\n sink.write(' ');\n sink.write(attrName);\n sink.write('=\"');\n // escapeAttr returns the original string when nothing needs escaping,\n // so no separate needsEscapeAttr pre-scan is required.\n sink.write(getEscapedAttrValue(strValue));\n sink.write('\"');\n }\n}\n\n/**\n * Render attributes to HTML string, excluding event handlers\n * Optimized for minimal allocations using push-based approach\n *\n * Returns both the attribute string and any dangerouslySetInnerHTML content.\n */\nexport function renderAttrs(props?: Props): string;\nexport function renderAttrs(\n props: Props | undefined,\n opts: { returnDangerousHtml: true }\n): AttrsResult;\nexport function renderAttrs(\n props?: Props,\n opts?: { returnDangerousHtml?: boolean }\n): string | AttrsResult {\n if (!props || typeof props !== 'object') {\n return opts?.returnDangerousHtml ? { attrs: '' } : '';\n }\n\n const attrParts: string[] = [];\n let dangerousHtml: string | undefined;\n\n const propsObj = props as Record<string, unknown>;\n for (const key in propsObj) {\n const value = propsObj[key];\n\n // Skip children in attrs\n if (key === 'children') continue;\n\n // Skip internal identity refs (framework-only)\n if (key === 'key' || key === 'ref') continue;\n\n // Handle dangerouslySetInnerHTML\n if (key === 'dangerouslySetInnerHTML') {\n if (value && typeof value === 'object' && '__html' in (value as object)) {\n dangerousHtml = String((value as { __html: unknown }).__html);\n }\n continue;\n }\n\n // Skip event handlers\n if (isEventHandler(key)) continue;\n\n // Skip internal props\n if (key.charCodeAt(0) === 95) continue; // '_'\n\n // Normalize class attribute (`class` preferred, accept `className` for compatibility)\n const attrName = key === 'class' || key === 'className' ? 'class' : key;\n\n // Handle style objects\n if (attrName === 'style') {\n const css = typeof value === 'string' ? value : styleObjToCss(value);\n if (css === null || css === '') continue;\n attrParts.push(` style=\"${getEscapedAttrValue(css)}\"`);\n continue;\n }\n\n // Boolean attributes\n if (value === true) {\n attrParts.push(` ${attrName}`);\n } else if (value === false || value === null || value === undefined) {\n continue;\n } else {\n const strValue = String(value);\n attrParts.push(` ${attrName}=\"${getEscapedAttrValue(strValue)}\"`);\n }\n }\n\n const result = attrParts.join('');\n\n if (opts?.returnDangerousHtml) {\n return { attrs: result, dangerousHtml };\n }\n return result;\n}\n"],"mappings":"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"}
|
package/dist/ssr/context.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Route, RouteAuthOptions } from "../common/router.js";
|
|
2
|
-
import { SSRData } from "../common/ssr.js";
|
|
3
2
|
import { SSRDataMissingError } from "../common/ssr-errors.js";
|
|
3
|
+
import { SSRData } from "../common/ssr.js";
|
|
4
|
+
|
|
4
5
|
//#region src/ssr/context.d.ts
|
|
5
6
|
interface RenderContext {
|
|
6
7
|
url: string;
|
|
@@ -10,6 +11,7 @@ interface RenderContext {
|
|
|
10
11
|
routes?: readonly Route[];
|
|
11
12
|
routeAuth?: RouteAuthOptions;
|
|
12
13
|
signal?: AbortSignal;
|
|
14
|
+
queryCache?: Map<string, unknown>;
|
|
13
15
|
keyCounter: number;
|
|
14
16
|
renderData: Record<string, unknown> | null;
|
|
15
17
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","names":[],"sources":["../../src/ssr/context.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"context.d.ts","names":[],"sources":["../../src/ssr/context.ts"],"mappings":";;;;;UAgBiB,aAAA;EACf,GAAA;EACA,IAAA;EACA,IAAA,GAAO,OAAA;EACP,MAAA,GAAS,MAAA;EACT,MAAA,YAAkB,KAAA;EAClB,SAAA,GAAY,gBAAA;EACZ,MAAA,GAAS,WAAA;EACT,UAAA,GAAa,GAAA;EAEb,UAAA;EACA,UAAA,EAAY,MAAA;AAAA;AAAA,KAIF,UAAA,GAAa,aAAA;AAAA,iBAoDT,mBAAA,CACd,IAAA,WACA,IAAA;EACE,GAAA;EACA,IAAA,GAAO,OAAA;EACP,MAAA,GAAS,MAAA;EACT,MAAA,YAAkB,KAAA;EAClB,SAAA,GAAY,gBAAA;EACZ,MAAA,GAAS,WAAA;AAAA,IAEV,aAAA;;;;;iBAqBa,iBAAA,GAAA,CAAqB,GAAA,EAAK,aAAA,EAAe,EAAA,QAAU,CAAA,GAAI,CAAA;AAnFvE;;;;AAAA,iBAsGgB,gBAAA,CAAA,GAAoB,aAAA;AAAA,cASvB,aAAA,SAAa,gBAAA;AAAA,cACb,cAAA,SAAc,iBAAA;AAAA,cACd,oBAAA,SAAoB,gBAAA;AAAA,iBAEjB,iBAAA,GAAA,CAAqB,GAAA,EAAK,aAAA,EAAe,EAAA,QAAU,CAAA,GAAI,CAAA;;;;;iBASvD,mBAAA,CAAA"}
|
package/dist/ssr/context.js
CHANGED
|
@@ -1,21 +1,38 @@
|
|
|
1
|
-
import { __require } from "../_virtual/_rolldown/runtime.js";
|
|
2
1
|
import { SSRDataMissingError } from "../common/ssr-errors.js";
|
|
3
2
|
import "./errors.js";
|
|
3
|
+
import { clearEscapeCache } from "./escape.js";
|
|
4
4
|
//#region src/ssr/context.ts
|
|
5
5
|
/**
|
|
6
6
|
* SSR Context Management
|
|
7
7
|
*
|
|
8
|
-
* Provides
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Provides render-context storage for server-side rendering.
|
|
9
|
+
* Node SSR lazily installs AsyncLocalStorage on first use; browser builds use
|
|
10
|
+
* the fallback stack.
|
|
11
11
|
*/
|
|
12
|
-
let
|
|
13
|
-
|
|
14
|
-
const asyncHooks = __require("async_hooks");
|
|
15
|
-
if (asyncHooks?.AsyncLocalStorage) asyncLocalStorage = new asyncHooks.AsyncLocalStorage();
|
|
16
|
-
} catch {}
|
|
12
|
+
let renderContextAccessor = null;
|
|
13
|
+
let renderContextAccessorInitialized = false;
|
|
17
14
|
let fallbackStack = null;
|
|
15
|
+
function ensureRenderContextAccessor() {
|
|
16
|
+
if (renderContextAccessorInitialized) return;
|
|
17
|
+
renderContextAccessorInitialized = true;
|
|
18
|
+
if (typeof process === "undefined" || !process.versions?.node) return;
|
|
19
|
+
try {
|
|
20
|
+
const asyncHooks = new Function("return require(" + JSON.stringify("async_hooks") + ");")();
|
|
21
|
+
if (asyncHooks.AsyncLocalStorage) {
|
|
22
|
+
const asyncLocalStorage = new asyncHooks.AsyncLocalStorage();
|
|
23
|
+
renderContextAccessor = {
|
|
24
|
+
getStore() {
|
|
25
|
+
return asyncLocalStorage.getStore();
|
|
26
|
+
},
|
|
27
|
+
run(store, fn) {
|
|
28
|
+
return asyncLocalStorage.run(store, fn);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
18
34
|
function createRenderContext(seed = 12345, opts = {}) {
|
|
35
|
+
clearEscapeCache();
|
|
19
36
|
return {
|
|
20
37
|
url: opts.url ?? "",
|
|
21
38
|
seed,
|
|
@@ -24,6 +41,7 @@ function createRenderContext(seed = 12345, opts = {}) {
|
|
|
24
41
|
routes: opts.routes,
|
|
25
42
|
routeAuth: opts.routeAuth,
|
|
26
43
|
signal: opts.signal,
|
|
44
|
+
queryCache: /* @__PURE__ */ new Map(),
|
|
27
45
|
keyCounter: 0,
|
|
28
46
|
renderData: null
|
|
29
47
|
};
|
|
@@ -33,7 +51,8 @@ function createRenderContext(seed = 12345, opts = {}) {
|
|
|
33
51
|
* Concurrency-safe in Node.js via AsyncLocalStorage.
|
|
34
52
|
*/
|
|
35
53
|
function withRenderContext(ctx, fn) {
|
|
36
|
-
|
|
54
|
+
ensureRenderContextAccessor();
|
|
55
|
+
if (renderContextAccessor) return renderContextAccessor.run(ctx, fn);
|
|
37
56
|
const prev = fallbackStack;
|
|
38
57
|
fallbackStack = ctx;
|
|
39
58
|
try {
|
|
@@ -47,7 +66,8 @@ function withRenderContext(ctx, fn) {
|
|
|
47
66
|
* Returns null if not inside a render.
|
|
48
67
|
*/
|
|
49
68
|
function getRenderContext() {
|
|
50
|
-
|
|
69
|
+
ensureRenderContextAccessor();
|
|
70
|
+
if (renderContextAccessor) return renderContextAccessor.getStore() ?? null;
|
|
51
71
|
return fallbackStack;
|
|
52
72
|
}
|
|
53
73
|
/**
|
package/dist/ssr/context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","names":[],"sources":["../../src/ssr/context.ts"],"sourcesContent":["/**\n * SSR Context Management\n *\n * Provides
|
|
1
|
+
{"version":3,"file":"context.js","names":[],"sources":["../../src/ssr/context.ts"],"sourcesContent":["/**\n * SSR Context Management\n *\n * Provides render-context storage for server-side rendering.\n * Node SSR lazily installs AsyncLocalStorage on first use; browser builds use\n * the fallback stack.\n */\n\nimport { SSRDataMissingError } from './errors';\nimport { clearEscapeCache } from './escape';\n\nexport type { SSRData } from '../common/ssr';\nimport type { SSRData } from '../common/ssr';\nimport type { Route, RouteAuthOptions } from '../common/router';\n\n// Unified per-render context combining SSRContext and RenderContext\nexport interface RenderContext {\n url: string;\n seed: number;\n data?: SSRData;\n params?: Record<string, string>;\n routes?: readonly Route[];\n routeAuth?: RouteAuthOptions;\n signal?: AbortSignal;\n queryCache?: Map<string, unknown>;\n // Per-render key state (moved from render-keys.ts globals)\n keyCounter: number;\n renderData: Record<string, unknown> | null;\n}\n\n// Legacy alias for compatibility\nexport type SSRContext = RenderContext;\n\ntype RenderContextAccessor = {\n getStore(): RenderContext | undefined;\n run<R>(store: RenderContext, fn: () => R): R;\n};\n\ntype AsyncHooksModule = {\n AsyncLocalStorage?: new () => RenderContextAccessor;\n};\n\nlet renderContextAccessor: RenderContextAccessor | null = null;\nlet renderContextAccessorInitialized = false;\n\n// Fallback stack for non-Node environments\nlet fallbackStack: RenderContext | null = null;\n\nfunction ensureRenderContextAccessor(): void {\n if (renderContextAccessorInitialized) {\n return;\n }\n\n renderContextAccessorInitialized = true;\n\n if (typeof process === 'undefined' || !process.versions?.node) {\n return;\n }\n\n try {\n // Hide the Node builtin from browser dependency scanners.\n // eslint-disable-next-line @typescript-eslint/no-implied-eval\n const loadAsyncHooks = new Function(\n 'return require(' + JSON.stringify('async_hooks') + ');'\n ) as () => AsyncHooksModule;\n const asyncHooks = loadAsyncHooks();\n\n if (asyncHooks.AsyncLocalStorage) {\n const asyncLocalStorage = new asyncHooks.AsyncLocalStorage();\n renderContextAccessor = {\n getStore() {\n return asyncLocalStorage.getStore();\n },\n run<R>(store: RenderContext, fn: () => R): R {\n return asyncLocalStorage.run(store, fn);\n },\n };\n }\n } catch {\n // Keep the fallback stack when async_hooks is unavailable.\n }\n}\n\nexport function createRenderContext(\n seed = 12345,\n opts: {\n url?: string;\n data?: SSRData;\n params?: Record<string, string>;\n routes?: readonly Route[];\n routeAuth?: RouteAuthOptions;\n signal?: AbortSignal;\n } = {}\n): RenderContext {\n clearEscapeCache();\n\n return {\n url: opts.url ?? '',\n seed,\n data: opts.data,\n params: opts.params,\n routes: opts.routes,\n routeAuth: opts.routeAuth,\n signal: opts.signal,\n queryCache: new Map<string, unknown>(),\n keyCounter: 0,\n renderData: null,\n };\n}\n\n/**\n * Run a function with the given render context.\n * Concurrency-safe in Node.js via AsyncLocalStorage.\n */\nexport function withRenderContext<T>(ctx: RenderContext, fn: () => T): T {\n ensureRenderContextAccessor();\n if (renderContextAccessor) {\n return renderContextAccessor.run(ctx, fn);\n }\n // Fallback: stack-based (not concurrency-safe)\n const prev = fallbackStack;\n fallbackStack = ctx;\n try {\n return fn();\n } finally {\n fallbackStack = prev;\n }\n}\n\n/**\n * Get the current render context.\n * Returns null if not inside a render.\n */\nexport function getRenderContext(): RenderContext | null {\n ensureRenderContextAccessor();\n if (renderContextAccessor) {\n return renderContextAccessor.getStore() ?? null;\n }\n return fallbackStack;\n}\n\n// Legacy API aliases (deprecated, for backwards compatibility)\nexport const getSSRContext = getRenderContext;\nexport const withSSRContext = withRenderContext;\nexport const getCurrentSSRContext = getRenderContext;\n\nexport function runWithSSRContext<T>(ctx: RenderContext, fn: () => T): T {\n // This was a separate path for sync detection; now unified\n return withRenderContext(ctx, fn);\n}\n\n/**\n * Centralized SSR enforcement helper — throws a consistent error when async\n * data is encountered during synchronous SSR.\n */\nexport function throwSSRDataMissing(): never {\n throw new SSRDataMissingError();\n}\n\nexport { SSRDataMissingError };\n"],"mappings":";;;;;;;;;;;AA0CA,IAAI,wBAAsD;AAC1D,IAAI,mCAAmC;AAGvC,IAAI,gBAAsC;AAE1C,SAAS,8BAAoC;CAC3C,IAAI,kCACF;CAGF,mCAAmC;CAEnC,IAAI,OAAO,YAAY,eAAe,CAAC,QAAQ,UAAU,MACvD;CAGF,IAAI;EAMF,MAAM,aAAa,IAHQ,SACzB,oBAAoB,KAAK,UAAU,cAAc,GAAG,KAEnC,EAAgB;EAEnC,IAAI,WAAW,mBAAmB;GAChC,MAAM,oBAAoB,IAAI,WAAW,mBAAmB;GAC5D,wBAAwB;IACtB,WAAW;KACT,OAAO,kBAAkB,UAAU;;IAErC,IAAO,OAAsB,IAAgB;KAC3C,OAAO,kBAAkB,IAAI,OAAO,GAAG;;IAE1C;;SAEG;;AAKV,SAAgB,oBACd,OAAO,OACP,OAOI,EAAE,EACS;CACf,kBAAkB;CAElB,OAAO;EACL,KAAK,KAAK,OAAO;EACjB;EACA,MAAM,KAAK;EACX,QAAQ,KAAK;EACb,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,QAAQ,KAAK;EACb,4BAAY,IAAI,KAAsB;EACtC,YAAY;EACZ,YAAY;EACb;;;;;;AAOH,SAAgB,kBAAqB,KAAoB,IAAgB;CACvE,6BAA6B;CAC7B,IAAI,uBACF,OAAO,sBAAsB,IAAI,KAAK,GAAG;CAG3C,MAAM,OAAO;CACb,gBAAgB;CAChB,IAAI;EACF,OAAO,IAAI;WACH;EACR,gBAAgB;;;;;;;AAQpB,SAAgB,mBAAyC;CACvD,6BAA6B;CAC7B,IAAI,uBACF,OAAO,sBAAsB,UAAU,IAAI;CAE7C,OAAO;;;;;;AAiBT,SAAgB,sBAA6B;CAC3C,MAAM,IAAI,qBAAqB"}
|
package/dist/ssr/escape.js
CHANGED
|
@@ -24,7 +24,50 @@ const VOID_ELEMENTS = new Set([
|
|
|
24
24
|
const escapeCache = /* @__PURE__ */ new Map();
|
|
25
25
|
const MAX_CACHE_SIZE = 256;
|
|
26
26
|
const CSS_UNSAFE_RE = /[{}<>\\]/g;
|
|
27
|
-
const
|
|
27
|
+
const CSS_URI_SCHEME_RE = /(?:^|[\s(,])([a-z][a-z0-9+.-]*):/i;
|
|
28
|
+
const CSS_FUNCTION_NAME_RE = /([a-z-][a-z0-9-]*)\s*\(/gi;
|
|
29
|
+
const CSS_ALLOWED_FUNCTIONS = new Set([
|
|
30
|
+
"var",
|
|
31
|
+
"calc",
|
|
32
|
+
"min",
|
|
33
|
+
"max",
|
|
34
|
+
"clamp",
|
|
35
|
+
"rgb",
|
|
36
|
+
"rgba",
|
|
37
|
+
"hsl",
|
|
38
|
+
"hsla",
|
|
39
|
+
"lab",
|
|
40
|
+
"lch",
|
|
41
|
+
"oklab",
|
|
42
|
+
"oklch",
|
|
43
|
+
"color",
|
|
44
|
+
"color-mix",
|
|
45
|
+
"translate",
|
|
46
|
+
"translatex",
|
|
47
|
+
"translatey",
|
|
48
|
+
"translatez",
|
|
49
|
+
"scale",
|
|
50
|
+
"scalex",
|
|
51
|
+
"scaley",
|
|
52
|
+
"scalez",
|
|
53
|
+
"rotate",
|
|
54
|
+
"rotatex",
|
|
55
|
+
"rotatey",
|
|
56
|
+
"rotatez",
|
|
57
|
+
"skew",
|
|
58
|
+
"skewx",
|
|
59
|
+
"skewy",
|
|
60
|
+
"matrix",
|
|
61
|
+
"matrix3d",
|
|
62
|
+
"linear-gradient",
|
|
63
|
+
"radial-gradient",
|
|
64
|
+
"conic-gradient",
|
|
65
|
+
"repeating-linear-gradient",
|
|
66
|
+
"repeating-radial-gradient",
|
|
67
|
+
"repeating-conic-gradient",
|
|
68
|
+
"cubic-bezier",
|
|
69
|
+
"steps"
|
|
70
|
+
]);
|
|
28
71
|
const STYLE_PROP_CACHE = /* @__PURE__ */ new Map();
|
|
29
72
|
const MAX_STYLE_PROP_CACHE_SIZE = 512;
|
|
30
73
|
function toKebabCached(prop) {
|
|
@@ -35,6 +78,13 @@ function toKebabCached(prop) {
|
|
|
35
78
|
return kebab;
|
|
36
79
|
}
|
|
37
80
|
/**
|
|
81
|
+
* Clear the escape cache. Call between SSR requests in long-running servers
|
|
82
|
+
* to prevent memory buildup from unique strings.
|
|
83
|
+
*/
|
|
84
|
+
function clearEscapeCache() {
|
|
85
|
+
escapeCache.clear();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
38
88
|
* Fast check if a string needs text escaping.
|
|
39
89
|
* Used to skip the full escape call when not needed.
|
|
40
90
|
*/
|
|
@@ -114,21 +164,11 @@ function escapeAttr(value) {
|
|
|
114
164
|
*/
|
|
115
165
|
function escapeCssValue(value) {
|
|
116
166
|
const str = String(value);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const ch = str.charCodeAt(i);
|
|
121
|
-
if (ch === 123 || ch === 125 || ch === 60 || ch === 62 || ch === 92) {
|
|
122
|
-
hasUnsafeChars = true;
|
|
123
|
-
if (openParen >= 0) break;
|
|
124
|
-
} else if (ch === 40 && openParen < 0) {
|
|
125
|
-
openParen = i;
|
|
126
|
-
if (hasUnsafeChars) break;
|
|
127
|
-
}
|
|
167
|
+
if (CSS_URI_SCHEME_RE.test(str)) return "";
|
|
168
|
+
if (str.includes("(")) {
|
|
169
|
+
for (const match of str.matchAll(CSS_FUNCTION_NAME_RE)) if (!CSS_ALLOWED_FUNCTIONS.has(match[1].toLowerCase())) return "";
|
|
128
170
|
}
|
|
129
|
-
|
|
130
|
-
if (openParen !== -1 && CSS_DANGEROUS_FN_RE.test(str)) return "";
|
|
131
|
-
return hasUnsafeChars ? str.replace(CSS_UNSAFE_RE, "") : str;
|
|
171
|
+
return str.replace(CSS_UNSAFE_RE, "");
|
|
132
172
|
}
|
|
133
173
|
/**
|
|
134
174
|
* Convert style object to CSS string with value escaping
|
|
@@ -148,6 +188,6 @@ function styleObjToCss(value) {
|
|
|
148
188
|
return result || null;
|
|
149
189
|
}
|
|
150
190
|
//#endregion
|
|
151
|
-
export { VOID_ELEMENTS, escapeAttr, escapeText, needsEscapeAttr, styleObjToCss };
|
|
191
|
+
export { VOID_ELEMENTS, clearEscapeCache, escapeAttr, escapeText, needsEscapeAttr, styleObjToCss };
|
|
152
192
|
|
|
153
193
|
//# sourceMappingURL=escape.js.map
|
package/dist/ssr/escape.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"escape.js","names":[],"sources":["../../src/ssr/escape.ts"],"sourcesContent":["/**\n * HTML escaping utilities for SSR\n *\n * Centralizes text and attribute escaping to avoid duplication\n * between sync and streaming SSR renderers.\n */\n\n// HTML5 void elements that don't have closing tags\nexport const VOID_ELEMENTS = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n]);\n\n// Escape cache for common values (bounded and clearable for long-running servers)\nconst escapeCache = new Map<string, string>();\nconst MAX_CACHE_SIZE = 256;\n\nconst _TEXT_ESCAPE_RE = /[&<>]/g;\n\nconst CSS_UNSAFE_RE = /[{}<>\\\\]/g;\nconst
|
|
1
|
+
{"version":3,"file":"escape.js","names":[],"sources":["../../src/ssr/escape.ts"],"sourcesContent":["/**\n * HTML escaping utilities for SSR\n *\n * Centralizes text and attribute escaping to avoid duplication\n * between sync and streaming SSR renderers.\n */\n\n// HTML5 void elements that don't have closing tags\nexport const VOID_ELEMENTS = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n]);\n\n// Escape cache for common values (bounded and clearable for long-running servers)\nconst escapeCache = new Map<string, string>();\nconst MAX_CACHE_SIZE = 256;\n\nconst _TEXT_ESCAPE_RE = /[&<>]/g;\n\nconst CSS_UNSAFE_RE = /[{}<>\\\\]/g;\nconst CSS_URI_SCHEME_RE = /(?:^|[\\s(,])([a-z][a-z0-9+.-]*):/i;\nconst CSS_FUNCTION_NAME_RE = /([a-z-][a-z0-9-]*)\\s*\\(/gi;\n\nconst CSS_ALLOWED_FUNCTIONS = new Set([\n 'var',\n 'calc',\n 'min',\n 'max',\n 'clamp',\n 'rgb',\n 'rgba',\n 'hsl',\n 'hsla',\n 'lab',\n 'lch',\n 'oklab',\n 'oklch',\n 'color',\n 'color-mix',\n 'translate',\n 'translatex',\n 'translatey',\n 'translatez',\n 'scale',\n 'scalex',\n 'scaley',\n 'scalez',\n 'rotate',\n 'rotatex',\n 'rotatey',\n 'rotatez',\n 'skew',\n 'skewx',\n 'skewy',\n 'matrix',\n 'matrix3d',\n 'linear-gradient',\n 'radial-gradient',\n 'conic-gradient',\n 'repeating-linear-gradient',\n 'repeating-radial-gradient',\n 'repeating-conic-gradient',\n 'cubic-bezier',\n 'steps',\n]);\n\nconst STYLE_PROP_CACHE = new Map<string, string>();\nconst MAX_STYLE_PROP_CACHE_SIZE = 512;\n\n// Pre-compute escape map functions for faster replacement\nconst _textEscapeMap = (ch: string): string => {\n const code = ch.charCodeAt(0);\n if (code === 38) return '&'; // &\n if (code === 60) return '<'; // <\n if (code === 62) return '>'; // >\n return ch;\n};\n\nfunction toKebabCached(prop: string): string {\n const cached = STYLE_PROP_CACHE.get(prop);\n if (cached !== undefined) return cached;\n const kebab = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);\n if (STYLE_PROP_CACHE.size < MAX_STYLE_PROP_CACHE_SIZE) {\n STYLE_PROP_CACHE.set(prop, kebab);\n }\n return kebab;\n}\n\n/**\n * Clear the escape cache. Call between SSR requests in long-running servers\n * to prevent memory buildup from unique strings.\n */\nexport function clearEscapeCache(): void {\n escapeCache.clear();\n}\n\n/**\n * Fast check if a string needs text escaping.\n * Used to skip the full escape call when not needed.\n */\nexport function needsEscapeText(text: string): boolean {\n for (let i = 0; i < text.length; i++) {\n const ch = text.charCodeAt(i);\n if (ch === 38 || ch === 60 || ch === 62) return true;\n }\n return false;\n}\n\n/**\n * Fast check if a string needs attribute escaping.\n * Checks for & \" ' < > characters.\n */\nexport function needsEscapeAttr(value: string): boolean {\n for (let i = 0; i < value.length; i++) {\n const ch = value.charCodeAt(i);\n // & \" ' < >\n if (ch === 38 || ch === 34 || ch === 39 || ch === 60 || ch === 62) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Escape HTML special characters in text content\n * Fast-path: cache first, then scan-then-escape for new strings\n */\nexport function escapeText(text: string): string {\n // Only use cache for short strings (likely to be repeated)\n const useCache = text.length <= 64;\n\n if (useCache) {\n const cached = escapeCache.get(text);\n if (cached !== undefined) return cached;\n }\n\n // Fast path: check if escaping needed\n if (!needsEscapeText(text)) {\n if (useCache && escapeCache.size < MAX_CACHE_SIZE) {\n escapeCache.set(text, text);\n }\n return text;\n }\n\n // Single-pass scan-and-build: avoids 3 separate regex passes\n let escaped = '';\n let lastIndex = 0;\n for (let i = 0; i < text.length; i++) {\n const ch = text.charCodeAt(i);\n let entity: string;\n if (ch === 38) entity = '&';\n else if (ch === 60) entity = '<';\n else if (ch === 62) entity = '>';\n else continue;\n escaped += text.slice(lastIndex, i) + entity;\n lastIndex = i + 1;\n }\n const result = escaped + text.slice(lastIndex);\n\n if (useCache && escapeCache.size < MAX_CACHE_SIZE) {\n escapeCache.set(text, result);\n }\n return result;\n}\n\n/**\n * Escape HTML special characters in attribute values.\n * Single-pass scan: returns the original string unchanged when no special\n * characters are found, so callers can drop separate needsEscapeAttr checks.\n */\nexport function escapeAttr(value: string): string {\n let escaped = '';\n let lastIndex = 0;\n for (let i = 0; i < value.length; i++) {\n const ch = value.charCodeAt(i);\n let entity: string;\n if (ch === 38) entity = '&';\n else if (ch === 34) entity = '"';\n else if (ch === 39) entity = ''';\n else if (ch === 60) entity = '<';\n else if (ch === 62) entity = '>';\n else continue;\n escaped += value.slice(lastIndex, i) + entity;\n lastIndex = i + 1;\n }\n // Return original reference when nothing needed escaping (no allocation)\n return lastIndex === 0 ? value : escaped + value.slice(lastIndex);\n}\n\n/**\n * Escape CSS value to prevent injection attacks.\n * Removes characters that could break out of CSS context.\n */\nfunction escapeCssValue(value: string): string {\n const str = String(value);\n\n if (CSS_URI_SCHEME_RE.test(str)) {\n return '';\n }\n\n if (str.includes('(')) {\n for (const match of str.matchAll(CSS_FUNCTION_NAME_RE)) {\n if (!CSS_ALLOWED_FUNCTIONS.has(match[1].toLowerCase())) {\n return '';\n }\n }\n }\n\n // Remove characters that could break out of CSS value context\n return str.replace(CSS_UNSAFE_RE, '');\n}\n\n/**\n * Convert style object to CSS string with value escaping\n * Optimized to avoid Object.entries allocation\n */\nexport function styleObjToCss(value: unknown): string | null {\n if (!value || typeof value !== 'object') return null;\n\n const styleObj = value as Record<string, unknown>;\n let result = '';\n\n for (const k in styleObj) {\n const v = styleObj[k];\n if (v === null || v === undefined || v === false) continue;\n\n const prop = toKebabCached(k);\n const safeValue = escapeCssValue(String(v));\n if (safeValue) {\n result += `${prop}:${safeValue};`;\n }\n }\n\n return result || null;\n}\n"],"mappings":"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"}
|