@askrjs/askr 0.0.35 → 0.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bench/components/benchmark-row.js.map +1 -1
- package/dist/bench/components/benchmark-table.js.map +1 -1
- package/dist/benchmark.js +2 -2
- package/dist/benchmark.js.map +1 -1
- package/dist/bin/askr-ssg.js.map +1 -1
- package/dist/boot/index.d.ts.map +1 -1
- package/dist/boot/index.js +9 -29
- 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/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.js.map +1 -1
- 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/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/structures/collection.js.map +1 -1
- 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/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 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/jsx/utils.js.map +1 -1
- package/dist/jsx-dev-runtime.js.map +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 +128 -53
- 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.map +1 -1
- package/dist/renderer/index.js.map +1 -1
- package/dist/renderer/keyed.js +1 -1
- package/dist/renderer/keyed.js.map +1 -1
- package/dist/renderer/reconcile.js.map +1 -1
- package/dist/renderer/utils.js.map +1 -1
- 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 +3 -34
- 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 +63 -16
- package/dist/runtime/for.js.map +1 -1
- package/dist/runtime/operations.d.ts.map +1 -1
- package/dist/runtime/operations.js +9 -7
- 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 +6 -4
- 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/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 +1 -0
- package/dist/ssr/context.d.ts.map +1 -1
- package/dist/ssr/context.js +3 -0
- 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.map +1 -1
- package/dist/ssr/index.js +15 -3
- package/dist/ssr/index.js.map +1 -1
- package/dist/ssr/render-keys.js.map +1 -1
- package/dist/ssr/sink.js.map +1 -1
- package/dist/ssr/stream-render.js +14 -2
- package/dist/ssr/stream-render.js.map +1 -1
- package/package.json +8 -6
|
@@ -86,7 +86,7 @@ var Scheduler = class {
|
|
|
86
86
|
queue.head = 0;
|
|
87
87
|
}
|
|
88
88
|
scheduleFlushKick() {
|
|
89
|
-
if (this.running || this.kickScheduled || this.inHandler || isBulkCommitActive() || !this.hasPendingTasks()) return;
|
|
89
|
+
if (this.running || this.kickScheduled || this.inHandler || this.allowSyncProgress || isBulkCommitActive() || !this.hasPendingTasks()) return;
|
|
90
90
|
this.kickScheduled = true;
|
|
91
91
|
queueMicrotask(() => {
|
|
92
92
|
this.kickScheduled = false;
|
|
@@ -141,7 +141,7 @@ var Scheduler = class {
|
|
|
141
141
|
executedInLane++;
|
|
142
142
|
didRunTask = true;
|
|
143
143
|
} catch (err) {
|
|
144
|
-
if (this.executionDepth > 0) this.executionDepth
|
|
144
|
+
if (this.executionDepth > 0) this.executionDepth--;
|
|
145
145
|
fatal = err;
|
|
146
146
|
break;
|
|
147
147
|
}
|
|
@@ -155,28 +155,19 @@ var Scheduler = class {
|
|
|
155
155
|
this.running = false;
|
|
156
156
|
this.depth = 0;
|
|
157
157
|
this.executionDepth = 0;
|
|
158
|
-
for (const lane of SCHEDULER_LANES)
|
|
158
|
+
for (const lane of SCHEDULER_LANES) {
|
|
159
|
+
const queue = this.lanes[lane];
|
|
160
|
+
if (queue.head > 0) this.compactLane(queue);
|
|
161
|
+
}
|
|
159
162
|
this.flushVersion++;
|
|
160
163
|
this.resolveWaiters();
|
|
161
164
|
recordSchedulerFlushTaskCount(executedTaskCount);
|
|
162
165
|
}
|
|
163
166
|
if (fatal) throw fatal;
|
|
164
167
|
}
|
|
165
|
-
finalizeFlush() {}
|
|
166
168
|
runWithSyncProgress(fn) {
|
|
167
169
|
const prev = this.allowSyncProgress;
|
|
168
170
|
this.allowSyncProgress = true;
|
|
169
|
-
const g = globalThis;
|
|
170
|
-
const origQueueMicrotask = g.queueMicrotask;
|
|
171
|
-
const origSetTimeout = g.setTimeout;
|
|
172
|
-
if (isDevelopmentEnvironment()) {
|
|
173
|
-
g.queueMicrotask = () => {
|
|
174
|
-
throw new Error("[Scheduler] queueMicrotask not allowed during runWithSyncProgress");
|
|
175
|
-
};
|
|
176
|
-
g.setTimeout = () => {
|
|
177
|
-
throw new Error("[Scheduler] setTimeout not allowed during runWithSyncProgress");
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
171
|
const startVersion = this.flushVersion;
|
|
181
172
|
try {
|
|
182
173
|
const res = fn();
|
|
@@ -186,10 +177,6 @@ var Scheduler = class {
|
|
|
186
177
|
}
|
|
187
178
|
return res;
|
|
188
179
|
} finally {
|
|
189
|
-
if (isDevelopmentEnvironment()) {
|
|
190
|
-
g.queueMicrotask = origQueueMicrotask;
|
|
191
|
-
g.setTimeout = origSetTimeout;
|
|
192
|
-
}
|
|
193
180
|
try {
|
|
194
181
|
if (this.flushVersion === startVersion) {
|
|
195
182
|
this.flushVersion++;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduler.js","names":[],"sources":["../../src/runtime/scheduler.ts"],"sourcesContent":["/**\n * Serialized update scheduler — safer design (no inline execution, explicit flush)\n *\n * Key ideas:\n * - Never execute a task inline from `enqueue`.\n * - `flush()` is explicit and non-reentrant.\n * - `runWithSyncProgress()` allows enqueues temporarily but does not run tasks\n * inline; it runs `fn` and then does an explicit `flush()`.\n * - `waitForFlush()` is race-free with a monotonic `flushVersion`.\n */\n\nimport { isDevelopmentEnvironment } from '../common/env';\nimport { assertSchedulingPrecondition, invariant } from '../dev/invariant';\nimport { logger } from '../dev/logger';\nimport { recordSchedulerFlushTaskCount } from './perf-metrics';\n\nconst MAX_FLUSH_DEPTH = 50;\n\ntype Task = () => void;\nexport type SchedulerLane = 'derived' | 'component' | 'reactive' | 'post';\n\nconst SCHEDULER_LANES: SchedulerLane[] = [\n 'derived',\n 'component',\n 'reactive',\n 'post',\n];\n\ninterface LaneQueue {\n tasks: Task[];\n head: number;\n}\n\nfunction isBulkCommitActive(): boolean {\n try {\n const fb = (\n globalThis as {\n __ASKR_FASTLANE?: { isBulkCommitActive?: () => boolean };\n }\n ).__ASKR_FASTLANE;\n return typeof fb?.isBulkCommitActive === 'function'\n ? !!fb.isBulkCommitActive()\n : false;\n } catch (e) {\n void e;\n return false;\n }\n}\n\nexport class Scheduler {\n private lanes: Record<SchedulerLane, LaneQueue> = {\n derived: { tasks: [], head: 0 },\n component: { tasks: [], head: 0 },\n reactive: { tasks: [], head: 0 },\n post: { tasks: [], head: 0 },\n };\n\n private running = false;\n private inHandler = false;\n private depth = 0;\n private executionDepth = 0; // for compat with existing diagnostics\n\n // Monotonic flush version increments at end of each flush\n private flushVersion = 0;\n\n // Best-effort microtask kick scheduling\n private kickScheduled = false;\n\n // Escape hatch flag for runWithSyncProgress\n private allowSyncProgress = false;\n\n // Waiters waiting for flushVersion >= target\n private waiters: Array<{\n target: number;\n resolve: () => void;\n reject: (err: unknown) => void;\n timer?: ReturnType<typeof setTimeout>;\n }> = [];\n\n // Keep a lightweight taskCount for compatibility/diagnostics\n private taskCount = 0;\n\n private hasPendingTasks(): boolean {\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n if (queue.head < queue.tasks.length) {\n return true;\n }\n }\n return false;\n }\n\n private getPendingTaskCount(): number {\n let total = 0;\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n total += queue.tasks.length - queue.head;\n }\n return total;\n }\n\n private compactLane(queue: LaneQueue): void {\n if (queue.head >= queue.tasks.length) {\n queue.tasks.length = 0;\n queue.head = 0;\n return;\n }\n\n if (queue.head <= 0) {\n return;\n }\n\n const remaining = queue.tasks.length - queue.head;\n for (let i = 0; i < remaining; i++) {\n queue.tasks[i] = queue.tasks[queue.head + i];\n }\n queue.tasks.length = remaining;\n queue.head = 0;\n }\n\n private scheduleFlushKick(): void {\n if (\n this.running ||\n this.kickScheduled ||\n this.inHandler ||\n isBulkCommitActive() ||\n !this.hasPendingTasks()\n ) {\n return;\n }\n\n this.kickScheduled = true;\n queueMicrotask(() => {\n this.kickScheduled = false;\n if (this.running || isBulkCommitActive() || !this.hasPendingTasks()) {\n return;\n }\n try {\n this.flush();\n } catch (err) {\n setTimeout(() => {\n throw err;\n });\n }\n });\n }\n\n enqueue(task: Task): void {\n this.enqueueInLane('component', task);\n }\n\n enqueueInLane(lane: SchedulerLane, task: Task): void {\n assertSchedulingPrecondition(\n typeof task === 'function',\n 'enqueue() requires a function'\n );\n\n // Strict rule: during bulk commit, only allow enqueues if runWithSyncProgress enabled\n if (isBulkCommitActive() && !this.allowSyncProgress) {\n if (isDevelopmentEnvironment()) {\n throw new Error(\n '[Scheduler] enqueue() during bulk commit (not allowed)'\n );\n }\n return;\n }\n\n // Enqueue task and account counts\n this.lanes[lane].tasks.push(task);\n this.taskCount++;\n\n this.scheduleFlushKick();\n }\n\n flush(): void {\n invariant(\n !this.running,\n '[Scheduler] flush() called while already running'\n );\n\n // Dev-only guard: disallow flush during bulk commit unless allowed\n if (isDevelopmentEnvironment()) {\n if (isBulkCommitActive() && !this.allowSyncProgress) {\n throw new Error(\n '[Scheduler] flush() started during bulk commit (not allowed)'\n );\n }\n }\n\n this.running = true;\n this.depth = 0;\n let fatal: unknown = null;\n let executedTaskCount = 0;\n const checkFlushDepth = isDevelopmentEnvironment();\n\n try {\n while (fatal === null) {\n let didRunTask = false;\n\n for (const lane of SCHEDULER_LANES) {\n const laneQueue = this.lanes[lane];\n let executedInLane = 0;\n\n while (laneQueue.head < laneQueue.tasks.length) {\n this.depth++;\n if (checkFlushDepth && this.depth > MAX_FLUSH_DEPTH) {\n throw new Error(\n `[Scheduler] exceeded MAX_FLUSH_DEPTH (${MAX_FLUSH_DEPTH}). Likely infinite update loop.`\n );\n }\n\n const task = laneQueue.tasks[laneQueue.head++];\n try {\n this.executionDepth++;\n task();\n this.executionDepth--;\n executedTaskCount++;\n executedInLane++;\n didRunTask = true;\n } catch (err) {\n // ensure executionDepth stays balanced\n if (this.executionDepth > 0) this.executionDepth = 0;\n fatal = err;\n break;\n }\n }\n\n if (executedInLane > 0) {\n this.taskCount = Math.max(0, this.taskCount - executedInLane);\n }\n\n if (fatal !== null) {\n break;\n }\n }\n\n if (!didRunTask) {\n break;\n }\n }\n } finally {\n this.running = false;\n this.depth = 0;\n this.executionDepth = 0;\n\n for (const lane of SCHEDULER_LANES) {\n this.compactLane(this.lanes[lane]);\n }\n\n // Advance flush epoch and resolve waiters\n this.flushVersion++;\n this.resolveWaiters();\n recordSchedulerFlushTaskCount(executedTaskCount);\n }\n\n if (fatal) throw fatal;\n }\n\n private finalizeFlush(): void {\n // Not used - finalize happens in finally block above\n }\n\n runWithSyncProgress<T>(fn: () => T): T {\n const prev = this.allowSyncProgress;\n this.allowSyncProgress = true;\n\n const g = globalThis as {\n queueMicrotask?: (...args: unknown[]) => void;\n setTimeout?: (...args: unknown[]) => unknown;\n };\n const origQueueMicrotask = g.queueMicrotask;\n const origSetTimeout = g.setTimeout;\n\n if (isDevelopmentEnvironment()) {\n g.queueMicrotask = () => {\n throw new Error(\n '[Scheduler] queueMicrotask not allowed during runWithSyncProgress'\n );\n };\n g.setTimeout = () => {\n throw new Error(\n '[Scheduler] setTimeout not allowed during runWithSyncProgress'\n );\n };\n }\n\n // Snapshot flushVersion so we can ensure we always complete an epoch\n const startVersion = this.flushVersion;\n\n try {\n const res = fn();\n\n // Flush deterministically if tasks were enqueued (and we're not already running)\n if (!this.running && this.hasPendingTasks()) {\n this.flush();\n }\n\n if (isDevelopmentEnvironment()) {\n if (this.hasPendingTasks()) {\n throw new Error(\n '[Scheduler] tasks remain after runWithSyncProgress flush'\n );\n }\n }\n\n return res;\n } finally {\n // Restore guarded globals\n if (isDevelopmentEnvironment()) {\n g.queueMicrotask = origQueueMicrotask;\n g.setTimeout = origSetTimeout;\n }\n\n // If no flush happened during the protected window, complete an epoch so\n // observers (tests) see progress even when fast-lane did synchronous work\n // without enqueuing tasks.\n try {\n if (this.flushVersion === startVersion) {\n this.flushVersion++;\n this.resolveWaiters();\n }\n } catch (e) {\n void e;\n }\n\n this.allowSyncProgress = prev;\n }\n }\n\n waitForFlush(targetVersion?: number, timeoutMs = 2000): Promise<void> {\n const target =\n typeof targetVersion === 'number' ? targetVersion : this.flushVersion + 1;\n if (this.flushVersion >= target) return Promise.resolve();\n\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n const ns =\n (\n globalThis as unknown as Record<string, unknown> & {\n __ASKR__?: Record<string, unknown>;\n }\n ).__ASKR__ || {};\n const diag = {\n flushVersion: this.flushVersion,\n queueLen: this.getPendingTaskCount(),\n running: this.running,\n inHandler: this.inHandler,\n bulk: isBulkCommitActive(),\n namespace: ns,\n };\n reject(\n new Error(\n `waitForFlush timeout ${timeoutMs}ms: ${JSON.stringify(diag)}`\n )\n );\n }, timeoutMs);\n\n this.waiters.push({ target, resolve, reject, timer });\n });\n }\n\n getState() {\n // Provide the compatibility shape expected by diagnostics/tests\n return {\n queueLength: this.getPendingTaskCount(),\n running: this.running,\n depth: this.depth,\n executionDepth: this.executionDepth,\n taskCount: this.taskCount,\n flushVersion: this.flushVersion,\n laneQueues: {\n derived: this.lanes.derived.tasks.length - this.lanes.derived.head,\n component:\n this.lanes.component.tasks.length - this.lanes.component.head,\n reactive: this.lanes.reactive.tasks.length - this.lanes.reactive.head,\n post: this.lanes.post.tasks.length - this.lanes.post.head,\n },\n // New fields for optional inspection\n inHandler: this.inHandler,\n allowSyncProgress: this.allowSyncProgress,\n };\n }\n\n getFlushVersion(): number {\n return this.flushVersion;\n }\n\n flushIfQueued(): void {\n if (!this.running && this.hasPendingTasks()) {\n this.flush();\n }\n }\n\n runInHandlerScope<T>(fn: () => T, flushMode: 'defer' | 'sync' = 'defer'): T {\n const prevInHandler = this.inHandler;\n this.inHandler = true;\n\n try {\n return fn();\n } finally {\n this.inHandler = prevInHandler;\n\n if (!this.inHandler) {\n if (flushMode === 'sync') {\n this.flushIfQueued();\n } else {\n this.scheduleFlushKick();\n }\n }\n }\n }\n\n setInHandler(v: boolean) {\n this.inHandler = v;\n if (!v) {\n this.scheduleFlushKick();\n }\n }\n\n isInHandler(): boolean {\n return this.inHandler;\n }\n\n isExecuting(): boolean {\n return this.running || this.executionDepth > 0;\n }\n\n // Clear pending synchronous tasks (used by fastlane enter/exit)\n clearPendingSyncTasks(): number {\n const remaining = this.getPendingTaskCount();\n if (remaining <= 0) return 0;\n\n if (this.running) {\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n queue.tasks.length = queue.head;\n }\n this.taskCount = Math.max(0, this.taskCount - remaining);\n queueMicrotask(() => {\n try {\n this.flushVersion++;\n this.resolveWaiters();\n } catch (e) {\n void e;\n }\n });\n return remaining;\n }\n\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n queue.tasks.length = 0;\n queue.head = 0;\n }\n this.taskCount = Math.max(0, this.taskCount - remaining);\n this.flushVersion++;\n this.resolveWaiters();\n return remaining;\n }\n\n private resolveWaiters() {\n if (this.waiters.length === 0) return;\n const ready: Array<() => void> = [];\n const remaining: typeof this.waiters = [];\n\n for (const w of this.waiters) {\n if (this.flushVersion >= w.target) {\n if (w.timer) clearTimeout(w.timer);\n ready.push(w.resolve);\n } else {\n remaining.push(w);\n }\n }\n\n this.waiters = remaining;\n for (const r of ready) r();\n }\n}\n\nexport const globalScheduler = new Scheduler();\n\nexport function isSchedulerExecuting(): boolean {\n return globalScheduler.isExecuting();\n}\n\nexport function scheduleEventHandler(handler: EventListener): EventListener {\n return (event: Event) => {\n try {\n globalScheduler.runInHandlerScope(() => {\n handler.call(null, event);\n });\n } catch (error) {\n logger.error('[Askr] Event handler error:', error);\n }\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scheduler.js","names":[],"sources":["../../src/runtime/scheduler.ts"],"sourcesContent":["/**\n * Serialized update scheduler — safer design (no inline execution, explicit flush)\n *\n * Key ideas:\n * - Never execute a task inline from `enqueue`.\n * - `flush()` is explicit and non-reentrant.\n * - `runWithSyncProgress()` allows enqueues temporarily but does not run tasks\n * inline; it runs `fn` and then does an explicit `flush()`.\n * - `waitForFlush()` is race-free with a monotonic `flushVersion`.\n */\n\nimport { isDevelopmentEnvironment } from '../common/env';\nimport { assertSchedulingPrecondition, invariant } from '../dev/invariant';\nimport { logger } from '../dev/logger';\nimport { recordSchedulerFlushTaskCount } from './perf-metrics';\n\nconst MAX_FLUSH_DEPTH = 50;\n\ntype Task = () => void;\nexport type SchedulerLane = 'derived' | 'component' | 'reactive' | 'post';\n\nconst SCHEDULER_LANES: SchedulerLane[] = [\n 'derived',\n 'component',\n 'reactive',\n 'post',\n];\n\ninterface LaneQueue {\n tasks: Task[];\n head: number;\n}\n\nfunction isBulkCommitActive(): boolean {\n try {\n const fb = (\n globalThis as {\n __ASKR_FASTLANE?: { isBulkCommitActive?: () => boolean };\n }\n ).__ASKR_FASTLANE;\n return typeof fb?.isBulkCommitActive === 'function'\n ? !!fb.isBulkCommitActive()\n : false;\n } catch (e) {\n void e;\n return false;\n }\n}\n\nexport class Scheduler {\n private lanes: Record<SchedulerLane, LaneQueue> = {\n derived: { tasks: [], head: 0 },\n component: { tasks: [], head: 0 },\n reactive: { tasks: [], head: 0 },\n post: { tasks: [], head: 0 },\n };\n\n private running = false;\n private inHandler = false;\n private depth = 0;\n private executionDepth = 0; // for compat with existing diagnostics\n\n // Monotonic flush version increments at end of each flush\n private flushVersion = 0;\n\n // Best-effort microtask kick scheduling\n private kickScheduled = false;\n\n // Escape hatch flag for runWithSyncProgress\n private allowSyncProgress = false;\n\n // Waiters waiting for flushVersion >= target\n private waiters: Array<{\n target: number;\n resolve: () => void;\n reject: (err: unknown) => void;\n timer?: ReturnType<typeof setTimeout>;\n }> = [];\n\n // Keep a lightweight taskCount for compatibility/diagnostics\n private taskCount = 0;\n\n private hasPendingTasks(): boolean {\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n if (queue.head < queue.tasks.length) {\n return true;\n }\n }\n return false;\n }\n\n private getPendingTaskCount(): number {\n let total = 0;\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n total += queue.tasks.length - queue.head;\n }\n return total;\n }\n\n private compactLane(queue: LaneQueue): void {\n if (queue.head >= queue.tasks.length) {\n queue.tasks.length = 0;\n queue.head = 0;\n return;\n }\n\n if (queue.head <= 0) {\n return;\n }\n\n const remaining = queue.tasks.length - queue.head;\n for (let i = 0; i < remaining; i++) {\n queue.tasks[i] = queue.tasks[queue.head + i];\n }\n queue.tasks.length = remaining;\n queue.head = 0;\n }\n\n private scheduleFlushKick(): void {\n if (\n this.running ||\n this.kickScheduled ||\n this.inHandler ||\n this.allowSyncProgress ||\n isBulkCommitActive() ||\n !this.hasPendingTasks()\n ) {\n return;\n }\n\n this.kickScheduled = true;\n queueMicrotask(() => {\n this.kickScheduled = false;\n if (this.running || isBulkCommitActive() || !this.hasPendingTasks()) {\n return;\n }\n try {\n this.flush();\n } catch (err) {\n setTimeout(() => {\n throw err;\n });\n }\n });\n }\n\n enqueue(task: Task): void {\n this.enqueueInLane('component', task);\n }\n\n enqueueInLane(lane: SchedulerLane, task: Task): void {\n assertSchedulingPrecondition(\n typeof task === 'function',\n 'enqueue() requires a function'\n );\n\n // Strict rule: during bulk commit, only allow enqueues if runWithSyncProgress enabled\n if (isBulkCommitActive() && !this.allowSyncProgress) {\n if (isDevelopmentEnvironment()) {\n throw new Error(\n '[Scheduler] enqueue() during bulk commit (not allowed)'\n );\n }\n return;\n }\n\n // Enqueue task and account counts\n this.lanes[lane].tasks.push(task);\n this.taskCount++;\n\n this.scheduleFlushKick();\n }\n\n flush(): void {\n invariant(\n !this.running,\n '[Scheduler] flush() called while already running'\n );\n\n // Dev-only guard: disallow flush during bulk commit unless allowed\n if (isDevelopmentEnvironment()) {\n if (isBulkCommitActive() && !this.allowSyncProgress) {\n throw new Error(\n '[Scheduler] flush() started during bulk commit (not allowed)'\n );\n }\n }\n\n this.running = true;\n this.depth = 0;\n let fatal: unknown = null;\n let executedTaskCount = 0;\n const checkFlushDepth = isDevelopmentEnvironment();\n\n try {\n while (fatal === null) {\n let didRunTask = false;\n\n for (const lane of SCHEDULER_LANES) {\n const laneQueue = this.lanes[lane];\n let executedInLane = 0;\n\n while (laneQueue.head < laneQueue.tasks.length) {\n this.depth++;\n if (checkFlushDepth && this.depth > MAX_FLUSH_DEPTH) {\n throw new Error(\n `[Scheduler] exceeded MAX_FLUSH_DEPTH (${MAX_FLUSH_DEPTH}). Likely infinite update loop.`\n );\n }\n\n const task = laneQueue.tasks[laneQueue.head++];\n try {\n this.executionDepth++;\n task();\n this.executionDepth--;\n executedTaskCount++;\n executedInLane++;\n didRunTask = true;\n } catch (err) {\n // ensure executionDepth stays balanced for the task that threw\n if (this.executionDepth > 0) this.executionDepth--;\n fatal = err;\n break;\n }\n }\n\n if (executedInLane > 0) {\n this.taskCount = Math.max(0, this.taskCount - executedInLane);\n }\n\n if (fatal !== null) {\n break;\n }\n }\n\n if (!didRunTask) {\n break;\n }\n }\n } finally {\n this.running = false;\n this.depth = 0;\n this.executionDepth = 0;\n\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n if (queue.head > 0) {\n this.compactLane(queue);\n }\n }\n\n // Advance flush epoch and resolve waiters\n this.flushVersion++;\n this.resolveWaiters();\n recordSchedulerFlushTaskCount(executedTaskCount);\n }\n\n if (fatal) throw fatal;\n }\n\n runWithSyncProgress<T>(fn: () => T): T {\n const prev = this.allowSyncProgress;\n this.allowSyncProgress = true;\n\n // Snapshot flushVersion so we can ensure we always complete an epoch\n const startVersion = this.flushVersion;\n\n try {\n const res = fn();\n\n // Flush deterministically if tasks were enqueued (and we're not already running)\n if (!this.running && this.hasPendingTasks()) {\n this.flush();\n }\n\n if (isDevelopmentEnvironment()) {\n if (this.hasPendingTasks()) {\n throw new Error(\n '[Scheduler] tasks remain after runWithSyncProgress flush'\n );\n }\n }\n\n return res;\n } finally {\n // If no flush happened during the protected window, complete an epoch so\n // observers (tests) see progress even when fast-lane did synchronous work\n // without enqueuing tasks.\n try {\n if (this.flushVersion === startVersion) {\n this.flushVersion++;\n this.resolveWaiters();\n }\n } catch (e) {\n void e;\n }\n\n this.allowSyncProgress = prev;\n }\n }\n\n waitForFlush(targetVersion?: number, timeoutMs = 2000): Promise<void> {\n const target =\n typeof targetVersion === 'number' ? targetVersion : this.flushVersion + 1;\n if (this.flushVersion >= target) return Promise.resolve();\n\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n const ns =\n (\n globalThis as unknown as Record<string, unknown> & {\n __ASKR__?: Record<string, unknown>;\n }\n ).__ASKR__ || {};\n const diag = {\n flushVersion: this.flushVersion,\n queueLen: this.getPendingTaskCount(),\n running: this.running,\n inHandler: this.inHandler,\n bulk: isBulkCommitActive(),\n namespace: ns,\n };\n reject(\n new Error(\n `waitForFlush timeout ${timeoutMs}ms: ${JSON.stringify(diag)}`\n )\n );\n }, timeoutMs);\n\n this.waiters.push({ target, resolve, reject, timer });\n });\n }\n\n getState() {\n // Provide the compatibility shape expected by diagnostics/tests\n return {\n queueLength: this.getPendingTaskCount(),\n running: this.running,\n depth: this.depth,\n executionDepth: this.executionDepth,\n taskCount: this.taskCount,\n flushVersion: this.flushVersion,\n laneQueues: {\n derived: this.lanes.derived.tasks.length - this.lanes.derived.head,\n component:\n this.lanes.component.tasks.length - this.lanes.component.head,\n reactive: this.lanes.reactive.tasks.length - this.lanes.reactive.head,\n post: this.lanes.post.tasks.length - this.lanes.post.head,\n },\n // New fields for optional inspection\n inHandler: this.inHandler,\n allowSyncProgress: this.allowSyncProgress,\n };\n }\n\n getFlushVersion(): number {\n return this.flushVersion;\n }\n\n flushIfQueued(): void {\n if (!this.running && this.hasPendingTasks()) {\n this.flush();\n }\n }\n\n runInHandlerScope<T>(fn: () => T, flushMode: 'defer' | 'sync' = 'defer'): T {\n const prevInHandler = this.inHandler;\n this.inHandler = true;\n\n try {\n return fn();\n } finally {\n this.inHandler = prevInHandler;\n\n if (!this.inHandler) {\n if (flushMode === 'sync') {\n this.flushIfQueued();\n } else {\n this.scheduleFlushKick();\n }\n }\n }\n }\n\n setInHandler(v: boolean) {\n this.inHandler = v;\n if (!v) {\n this.scheduleFlushKick();\n }\n }\n\n isInHandler(): boolean {\n return this.inHandler;\n }\n\n isExecuting(): boolean {\n return this.running || this.executionDepth > 0;\n }\n\n // Clear pending synchronous tasks (used by fastlane enter/exit)\n clearPendingSyncTasks(): number {\n const remaining = this.getPendingTaskCount();\n if (remaining <= 0) return 0;\n\n if (this.running) {\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n queue.tasks.length = queue.head;\n }\n this.taskCount = Math.max(0, this.taskCount - remaining);\n queueMicrotask(() => {\n try {\n this.flushVersion++;\n this.resolveWaiters();\n } catch (e) {\n void e;\n }\n });\n return remaining;\n }\n\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n queue.tasks.length = 0;\n queue.head = 0;\n }\n this.taskCount = Math.max(0, this.taskCount - remaining);\n this.flushVersion++;\n this.resolveWaiters();\n return remaining;\n }\n\n private resolveWaiters() {\n if (this.waiters.length === 0) return;\n const ready: Array<() => void> = [];\n const remaining: typeof this.waiters = [];\n\n for (const w of this.waiters) {\n if (this.flushVersion >= w.target) {\n if (w.timer) clearTimeout(w.timer);\n ready.push(w.resolve);\n } else {\n remaining.push(w);\n }\n }\n\n this.waiters = remaining;\n for (const r of ready) r();\n }\n}\n\nexport const globalScheduler = new Scheduler();\n\nexport function isSchedulerExecuting(): boolean {\n return globalScheduler.isExecuting();\n}\n\nexport function scheduleEventHandler(handler: EventListener): EventListener {\n return (event: Event) => {\n try {\n globalScheduler.runInHandlerScope(() => {\n handler.call(null, event);\n });\n } catch (error) {\n logger.error('[Askr] Event handler error:', error);\n }\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selector.d.ts","names":[],"sources":["../../src/runtime/selector.ts"],"mappings":";UA6BiB,QAAA;EAAA,CACd,SAAA,EAAW,CAAA;AAAA;AAAA,KAOT,cAAA;EACH,cAAA,CAAe,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA;AAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"selector.d.ts","names":[],"sources":["../../src/runtime/selector.ts"],"mappings":";UA6BiB,QAAA;EAAA,CACd,SAAA,EAAW,CAAA;AAAA;AAAA,KAOT,cAAA;EACH,cAAA,CAAe,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA;AAAA;AAAA,iBA4RV,QAAA,GAAA,CACd,MAAA,QAAc,CAAA,EACd,MAAA,GAAQ,cAAA,CAAe,CAAA,IACtB,QAAA,CAAS,CAAA"}
|
package/dist/runtime/selector.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { incrementPerfMetric } from "./perf-metrics.js";
|
|
1
|
+
import { getPerfMetricsStore, incrementPerfMetric } from "./perf-metrics.js";
|
|
2
2
|
import { globalScheduler } from "./scheduler.js";
|
|
3
3
|
import { clearDerivedDependencySubscriptions, markReactivePropsDirtySource, markReadableDerivedSubscribersDirty, markReadableUsage, notifyReadableReaders, recordReadableRead, syncDerivedDependencySubscriptions, withDerivedReadTracking } from "./readable.js";
|
|
4
4
|
import { claimHookIndex, getCurrentInstance } from "./component.js";
|
|
@@ -116,11 +116,13 @@ function recomputeSelectorCell(cell, notifyDownstream) {
|
|
|
116
116
|
return cell._value;
|
|
117
117
|
}
|
|
118
118
|
function createSelectorCell(instance, hookIndex, source, equals) {
|
|
119
|
+
const perfMetricsStore = getPerfMetricsStore();
|
|
119
120
|
const cell = function selectorPredicate(candidate) {
|
|
120
121
|
recordReadableRead(getCandidateSource(cell, candidate));
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
122
|
+
if (perfMetricsStore) perfMetricsStore.selectorCandidateReads += 1;
|
|
123
|
+
const selectorCell = cell;
|
|
124
|
+
const current = selectorCell._dirty || !selectorCell._hasValue ? recomputeSelectorCell(selectorCell, selectorCell._scheduled) : selectorCell._value;
|
|
125
|
+
return selectorCell._equals(current, candidate);
|
|
124
126
|
};
|
|
125
127
|
cell._owner = instance;
|
|
126
128
|
cell._hookIndex = hookIndex;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selector.js","names":[],"sources":["../../src/runtime/selector.ts"],"sourcesContent":["import {\n claimHookIndex,\n getCurrentInstance,\n type ComponentInstance,\n} from './component';\nimport { globalScheduler } from './scheduler';\nimport {\n clearDerivedDependencySubscriptions,\n markReadableDerivedSubscribersDirty,\n markReadableUsage,\n markReactivePropsDirtySource,\n notifyReadableReaders,\n recordReadableRead,\n syncDerivedDependencySubscriptions,\n withDerivedReadTracking,\n type DerivedSubscriber,\n type ReadableSource,\n} from './readable';\nimport { incrementPerfMetric } from './perf-metrics';\n\ntype PrimitiveKey =\n | string\n | number\n | boolean\n | symbol\n | bigint\n | null\n | undefined;\n\nexport interface Selector<T> {\n (candidate: T): boolean;\n}\n\ninterface SelectorCandidateSource<T> extends ReadableSource<boolean> {\n _candidate: T;\n}\n\ntype SelectorEquals<T> = {\n bivarianceHack(a: T, b: T): boolean;\n}['bivarianceHack'];\n\ninterface SelectorCell<T> extends Selector<T>, DerivedSubscriber {\n _owner: ComponentInstance;\n _hookIndex: number;\n _source: () => T;\n _equals: SelectorEquals<T>;\n _value: T;\n _hasValue: boolean;\n _dirty: boolean;\n _scheduled: boolean;\n _evaluating: boolean;\n _sources: Set<ReadableSource<unknown>>;\n _pendingDependencySources?: Set<ReadableSource<unknown>>;\n _primitiveCandidates: Map<PrimitiveKey, SelectorCandidateSource<T>>;\n _objectCandidates: WeakMap<object, SelectorCandidateSource<T>>;\n _objectCandidateSources: Set<SelectorCandidateSource<T>>;\n _cleanup(): void;\n}\n\nconst selectorCells = new WeakMap<\n ComponentInstance,\n Map<number, SelectorCell<unknown>>\n>();\nconst dirtySelectorCells = new Set<SelectorCell<unknown>>();\nlet hasPendingSelectorFlush = false;\n\nfunction getSelectorStore(\n instance: ComponentInstance\n): Map<number, SelectorCell<unknown>> {\n let store = selectorCells.get(instance);\n if (!store) {\n store = new Map();\n selectorCells.set(instance, store);\n }\n return store;\n}\n\nfunction scheduleSelectorFlush(): void {\n if (hasPendingSelectorFlush) {\n return;\n }\n hasPendingSelectorFlush = true;\n globalScheduler.enqueueInLane('derived', flushDirtySelectorCells);\n}\n\nfunction markSelectorCellDirty(cell: SelectorCell<unknown>): void {\n cell._dirty = true;\n if (cell._scheduled) {\n return;\n }\n\n cell._scheduled = true;\n dirtySelectorCells.add(cell);\n scheduleSelectorFlush();\n}\n\nfunction flushDirtySelectorCells(): void {\n hasPendingSelectorFlush = false;\n\n if (dirtySelectorCells.size === 0) {\n return;\n }\n\n const pending = Array.from(dirtySelectorCells);\n dirtySelectorCells.clear();\n\n for (const cell of pending) {\n cell._scheduled = false;\n if (!cell._dirty) {\n continue;\n }\n recomputeSelectorCell(cell, true);\n }\n}\n\nfunction isObjectCandidate(value: unknown): value is object {\n return (\n (typeof value === 'object' && value !== null) || typeof value === 'function'\n );\n}\n\nfunction isDefaultSelectorEquals<T>(equals: SelectorEquals<T>): boolean {\n return equals === Object.is;\n}\n\nfunction createCandidateSource<T>(candidate: T): SelectorCandidateSource<T> {\n const source = (() => false) as SelectorCandidateSource<T>;\n source._candidate = candidate;\n return source;\n}\n\nfunction getCandidateSource<T>(\n cell: SelectorCell<T>,\n candidate: T\n): SelectorCandidateSource<T> {\n if (isObjectCandidate(candidate)) {\n const cached = cell._objectCandidates.get(candidate);\n if (cached) {\n return cached;\n }\n\n const created = createCandidateSource(candidate);\n created._candidate = candidate;\n cell._objectCandidates.set(candidate, created);\n cell._objectCandidateSources.add(created);\n return created;\n }\n\n const key = candidate as PrimitiveKey;\n const cached = cell._primitiveCandidates.get(key);\n if (cached) {\n return cached;\n }\n\n const created = createCandidateSource(candidate);\n created._candidate = candidate;\n cell._primitiveCandidates.set(key, created);\n return created;\n}\n\nfunction notifySelectorSource(source: SelectorCandidateSource<unknown>): void {\n incrementPerfMetric('selectorInvalidations');\n markReadableDerivedSubscribersDirty(source);\n markReactivePropsDirtySource(source);\n notifyReadableReaders(source);\n}\n\nfunction notifyAllSelectorSources<T>(cell: SelectorCell<T>): void {\n for (const source of cell._primitiveCandidates.values()) {\n notifySelectorSource(source);\n }\n for (const source of cell._objectCandidateSources) {\n notifySelectorSource(source);\n }\n}\n\nfunction notifySelectorValueChange<T>(\n cell: SelectorCell<T>,\n prevValue: T,\n nextValue: T\n): void {\n if (!isDefaultSelectorEquals(cell._equals)) {\n notifyAllSelectorSources(cell);\n return;\n }\n\n if (!Object.is(prevValue, nextValue)) {\n notifySelectorSource(getCandidateSource(cell, prevValue));\n notifySelectorSource(getCandidateSource(cell, nextValue));\n }\n}\n\nfunction recomputeSelectorCell<T>(\n cell: SelectorCell<T>,\n notifyDownstream: boolean\n): T {\n if (!cell._dirty && cell._hasValue) {\n return cell._value;\n }\n\n if (cell._evaluating) {\n throw new Error('selector() cannot read itself recursively');\n }\n\n cell._evaluating = true;\n cell._dirty = false;\n cell._pendingDependencySources = new Set();\n\n const prevSources = cell._sources;\n const hadValue = cell._hasValue;\n const prevValue = cell._value;\n let nextValue: T;\n\n try {\n nextValue = withDerivedReadTracking(cell, cell._source);\n } catch (error) {\n cell._dirty = true;\n cell._pendingDependencySources = undefined;\n throw error;\n } finally {\n cell._evaluating = false;\n }\n\n const nextSources = cell._pendingDependencySources ?? new Set();\n cell._pendingDependencySources = undefined;\n syncDerivedDependencySubscriptions(cell, prevSources, nextSources);\n cell._sources = nextSources;\n\n const valueChanged = !cell._hasValue || !cell._equals(prevValue, nextValue);\n cell._hasValue = true;\n cell._value = nextValue;\n\n if (valueChanged && notifyDownstream && hadValue) {\n notifySelectorValueChange(cell, prevValue, nextValue);\n }\n\n return cell._value;\n}\n\nfunction createSelectorCell<T>(\n instance: ComponentInstance,\n hookIndex: number,\n source: () => T,\n equals: SelectorEquals<T>\n): SelectorCell<T> {\n const cell = function selectorPredicate(candidate: T): boolean {\n const sourceRef = getCandidateSource(cell as SelectorCell<T>, candidate);\n recordReadableRead(sourceRef);\n incrementPerfMetric('selectorCandidateReads');\n const current = recomputeSelectorCell(\n cell as SelectorCell<T>,\n (cell as SelectorCell<T>)._scheduled\n );\n return (cell as SelectorCell<T>)._equals(current, candidate);\n } as SelectorCell<T>;\n\n cell._owner = instance;\n cell._hookIndex = hookIndex;\n cell._source = source;\n cell._equals = equals;\n cell._value = undefined as T;\n cell._hasValue = false;\n cell._dirty = true;\n cell._scheduled = false;\n cell._evaluating = false;\n cell._sources = new Set();\n cell._primitiveCandidates = new Map();\n cell._objectCandidates = new WeakMap();\n cell._objectCandidateSources = new Set();\n cell._markDirty = () => {\n markSelectorCellDirty(cell as unknown as SelectorCell<unknown>);\n };\n cell._cleanup = () => {\n cell._scheduled = false;\n cell._dirty = false;\n cell._hasValue = false;\n dirtySelectorCells.delete(cell as unknown as SelectorCell<unknown>);\n clearDerivedDependencySubscriptions(cell, cell._sources);\n for (const sourceRef of cell._primitiveCandidates.values()) {\n sourceRef._readers?.clear();\n sourceRef._derivedSubscribers?.clear();\n }\n for (const sourceRef of cell._objectCandidateSources) {\n sourceRef._readers?.clear();\n sourceRef._derivedSubscribers?.clear();\n }\n cell._primitiveCandidates.clear();\n cell._objectCandidateSources.clear();\n };\n\n (instance.cleanupFns ??= []).push(() => {\n cell._cleanup();\n selectorCells.get(instance)?.delete(hookIndex);\n });\n\n return cell;\n}\n\nfunction getOrCreateSelectorCell<T>(\n instance: ComponentInstance,\n hookIndex: number,\n source: () => T,\n equals: SelectorEquals<T>\n): SelectorCell<T> {\n const store = getSelectorStore(instance);\n const existing = store.get(hookIndex) as SelectorCell<T> | undefined;\n if (existing) {\n existing._source = source;\n existing._equals = equals;\n existing._dirty = true;\n return existing;\n }\n\n const created = createSelectorCell(instance, hookIndex, source, equals);\n store.set(hookIndex, created as unknown as SelectorCell<unknown>);\n return created;\n}\n\nexport function selector<T>(\n source: () => T,\n equals: SelectorEquals<T> = Object.is\n): Selector<T> {\n markReadableUsage(source);\n\n const instance = getCurrentInstance();\n if (!instance) {\n throw new Error(\n 'selector() can only be called during component render execution. ' +\n 'Move selector() calls to the top level of your component function.'\n );\n }\n\n const hookIndex = claimHookIndex(instance, 'selector');\n const cell = getOrCreateSelectorCell(instance, hookIndex, source, equals);\n recomputeSelectorCell(cell, false);\n return cell;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"selector.js","names":[],"sources":["../../src/runtime/selector.ts"],"sourcesContent":["import {\n claimHookIndex,\n getCurrentInstance,\n type ComponentInstance,\n} from './component';\nimport { globalScheduler } from './scheduler';\nimport {\n clearDerivedDependencySubscriptions,\n markReadableDerivedSubscribersDirty,\n markReadableUsage,\n markReactivePropsDirtySource,\n notifyReadableReaders,\n recordReadableRead,\n syncDerivedDependencySubscriptions,\n withDerivedReadTracking,\n type DerivedSubscriber,\n type ReadableSource,\n} from './readable';\nimport { getPerfMetricsStore, incrementPerfMetric } from './perf-metrics';\n\ntype PrimitiveKey =\n | string\n | number\n | boolean\n | symbol\n | bigint\n | null\n | undefined;\n\nexport interface Selector<T> {\n (candidate: T): boolean;\n}\n\ninterface SelectorCandidateSource<T> extends ReadableSource<boolean> {\n _candidate: T;\n}\n\ntype SelectorEquals<T> = {\n bivarianceHack(a: T, b: T): boolean;\n}['bivarianceHack'];\n\ninterface SelectorCell<T> extends Selector<T>, DerivedSubscriber {\n _owner: ComponentInstance;\n _hookIndex: number;\n _source: () => T;\n _equals: SelectorEquals<T>;\n _value: T;\n _hasValue: boolean;\n _dirty: boolean;\n _scheduled: boolean;\n _evaluating: boolean;\n _sources: Set<ReadableSource<unknown>>;\n _pendingDependencySources?: Set<ReadableSource<unknown>>;\n _primitiveCandidates: Map<PrimitiveKey, SelectorCandidateSource<T>>;\n _objectCandidates: WeakMap<object, SelectorCandidateSource<T>>;\n _objectCandidateSources: Set<SelectorCandidateSource<T>>;\n _cleanup(): void;\n}\n\nconst selectorCells = new WeakMap<\n ComponentInstance,\n Map<number, SelectorCell<unknown>>\n>();\nconst dirtySelectorCells = new Set<SelectorCell<unknown>>();\nlet hasPendingSelectorFlush = false;\n\nfunction getSelectorStore(\n instance: ComponentInstance\n): Map<number, SelectorCell<unknown>> {\n let store = selectorCells.get(instance);\n if (!store) {\n store = new Map();\n selectorCells.set(instance, store);\n }\n return store;\n}\n\nfunction scheduleSelectorFlush(): void {\n if (hasPendingSelectorFlush) {\n return;\n }\n hasPendingSelectorFlush = true;\n globalScheduler.enqueueInLane('derived', flushDirtySelectorCells);\n}\n\nfunction markSelectorCellDirty(cell: SelectorCell<unknown>): void {\n cell._dirty = true;\n if (cell._scheduled) {\n return;\n }\n\n cell._scheduled = true;\n dirtySelectorCells.add(cell);\n scheduleSelectorFlush();\n}\n\nfunction flushDirtySelectorCells(): void {\n hasPendingSelectorFlush = false;\n\n if (dirtySelectorCells.size === 0) {\n return;\n }\n\n const pending = Array.from(dirtySelectorCells);\n dirtySelectorCells.clear();\n\n for (const cell of pending) {\n cell._scheduled = false;\n if (!cell._dirty) {\n continue;\n }\n recomputeSelectorCell(cell, true);\n }\n}\n\nfunction isObjectCandidate(value: unknown): value is object {\n return (\n (typeof value === 'object' && value !== null) || typeof value === 'function'\n );\n}\n\nfunction isDefaultSelectorEquals<T>(equals: SelectorEquals<T>): boolean {\n return equals === Object.is;\n}\n\nfunction createCandidateSource<T>(candidate: T): SelectorCandidateSource<T> {\n const source = (() => false) as SelectorCandidateSource<T>;\n source._candidate = candidate;\n return source;\n}\n\nfunction getCandidateSource<T>(\n cell: SelectorCell<T>,\n candidate: T\n): SelectorCandidateSource<T> {\n if (isObjectCandidate(candidate)) {\n const cached = cell._objectCandidates.get(candidate);\n if (cached) {\n return cached;\n }\n\n const created = createCandidateSource(candidate);\n created._candidate = candidate;\n cell._objectCandidates.set(candidate, created);\n cell._objectCandidateSources.add(created);\n return created;\n }\n\n const key = candidate as PrimitiveKey;\n const cached = cell._primitiveCandidates.get(key);\n if (cached) {\n return cached;\n }\n\n const created = createCandidateSource(candidate);\n created._candidate = candidate;\n cell._primitiveCandidates.set(key, created);\n return created;\n}\n\nfunction notifySelectorSource(source: SelectorCandidateSource<unknown>): void {\n incrementPerfMetric('selectorInvalidations');\n markReadableDerivedSubscribersDirty(source);\n markReactivePropsDirtySource(source);\n notifyReadableReaders(source);\n}\n\nfunction notifyAllSelectorSources<T>(cell: SelectorCell<T>): void {\n for (const source of cell._primitiveCandidates.values()) {\n notifySelectorSource(source);\n }\n for (const source of cell._objectCandidateSources) {\n notifySelectorSource(source);\n }\n}\n\nfunction notifySelectorValueChange<T>(\n cell: SelectorCell<T>,\n prevValue: T,\n nextValue: T\n): void {\n if (!isDefaultSelectorEquals(cell._equals)) {\n notifyAllSelectorSources(cell);\n return;\n }\n\n if (!Object.is(prevValue, nextValue)) {\n notifySelectorSource(getCandidateSource(cell, prevValue));\n notifySelectorSource(getCandidateSource(cell, nextValue));\n }\n}\n\nfunction recomputeSelectorCell<T>(\n cell: SelectorCell<T>,\n notifyDownstream: boolean\n): T {\n if (!cell._dirty && cell._hasValue) {\n return cell._value;\n }\n\n if (cell._evaluating) {\n throw new Error('selector() cannot read itself recursively');\n }\n\n cell._evaluating = true;\n cell._dirty = false;\n cell._pendingDependencySources = new Set();\n\n const prevSources = cell._sources;\n const hadValue = cell._hasValue;\n const prevValue = cell._value;\n let nextValue: T;\n\n try {\n nextValue = withDerivedReadTracking(cell, cell._source);\n } catch (error) {\n cell._dirty = true;\n cell._pendingDependencySources = undefined;\n throw error;\n } finally {\n cell._evaluating = false;\n }\n\n const nextSources = cell._pendingDependencySources ?? new Set();\n cell._pendingDependencySources = undefined;\n syncDerivedDependencySubscriptions(cell, prevSources, nextSources);\n cell._sources = nextSources;\n\n const valueChanged = !cell._hasValue || !cell._equals(prevValue, nextValue);\n cell._hasValue = true;\n cell._value = nextValue;\n\n if (valueChanged && notifyDownstream && hadValue) {\n notifySelectorValueChange(cell, prevValue, nextValue);\n }\n\n return cell._value;\n}\n\nfunction createSelectorCell<T>(\n instance: ComponentInstance,\n hookIndex: number,\n source: () => T,\n equals: SelectorEquals<T>\n): SelectorCell<T> {\n const perfMetricsStore = getPerfMetricsStore();\n const cell = function selectorPredicate(candidate: T): boolean {\n const sourceRef = getCandidateSource(cell as SelectorCell<T>, candidate);\n recordReadableRead(sourceRef);\n if (perfMetricsStore) {\n perfMetricsStore.selectorCandidateReads += 1;\n }\n const selectorCell = cell as SelectorCell<T>;\n const current =\n selectorCell._dirty || !selectorCell._hasValue\n ? recomputeSelectorCell(selectorCell, selectorCell._scheduled)\n : selectorCell._value;\n return selectorCell._equals(current, candidate);\n } as SelectorCell<T>;\n\n cell._owner = instance;\n cell._hookIndex = hookIndex;\n cell._source = source;\n cell._equals = equals;\n cell._value = undefined as T;\n cell._hasValue = false;\n cell._dirty = true;\n cell._scheduled = false;\n cell._evaluating = false;\n cell._sources = new Set();\n cell._primitiveCandidates = new Map();\n cell._objectCandidates = new WeakMap();\n cell._objectCandidateSources = new Set();\n cell._markDirty = () => {\n markSelectorCellDirty(cell as unknown as SelectorCell<unknown>);\n };\n cell._cleanup = () => {\n cell._scheduled = false;\n cell._dirty = false;\n cell._hasValue = false;\n dirtySelectorCells.delete(cell as unknown as SelectorCell<unknown>);\n clearDerivedDependencySubscriptions(cell, cell._sources);\n for (const sourceRef of cell._primitiveCandidates.values()) {\n sourceRef._readers?.clear();\n sourceRef._derivedSubscribers?.clear();\n }\n for (const sourceRef of cell._objectCandidateSources) {\n sourceRef._readers?.clear();\n sourceRef._derivedSubscribers?.clear();\n }\n cell._primitiveCandidates.clear();\n cell._objectCandidateSources.clear();\n };\n\n (instance.cleanupFns ??= []).push(() => {\n cell._cleanup();\n selectorCells.get(instance)?.delete(hookIndex);\n });\n\n return cell;\n}\n\nfunction getOrCreateSelectorCell<T>(\n instance: ComponentInstance,\n hookIndex: number,\n source: () => T,\n equals: SelectorEquals<T>\n): SelectorCell<T> {\n const store = getSelectorStore(instance);\n const existing = store.get(hookIndex) as SelectorCell<T> | undefined;\n if (existing) {\n existing._source = source;\n existing._equals = equals;\n existing._dirty = true;\n return existing;\n }\n\n const created = createSelectorCell(instance, hookIndex, source, equals);\n store.set(hookIndex, created as unknown as SelectorCell<unknown>);\n return created;\n}\n\nexport function selector<T>(\n source: () => T,\n equals: SelectorEquals<T> = Object.is\n): Selector<T> {\n markReadableUsage(source);\n\n const instance = getCurrentInstance();\n if (!instance) {\n throw new Error(\n 'selector() can only be called during component render execution. ' +\n 'Move selector() calls to the top level of your component function.'\n );\n }\n\n const hookIndex = claimHookIndex(instance, 'selector');\n const cell = getOrCreateSelectorCell(instance, hookIndex, source, equals);\n recomputeSelectorCell(cell, false);\n return cell;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/runtime/snapshot-source.d.ts
|
|
2
|
+
declare const SNAPSHOT_SOURCE_BRAND: unique symbol;
|
|
3
|
+
type SnapshotSourceBrand = {
|
|
4
|
+
readonly [SNAPSHOT_SOURCE_BRAND]: true;
|
|
5
|
+
};
|
|
6
|
+
declare function brandSnapshotSource<T extends object>(value: T): T & SnapshotSourceBrand;
|
|
7
|
+
declare function isSnapshotSource(value: unknown): value is SnapshotSourceBrand;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { SnapshotSourceBrand };
|
|
10
|
+
//# sourceMappingURL=snapshot-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-source.d.ts","names":[],"sources":["../../src/runtime/snapshot-source.ts"],"mappings":";cAAa,qBAAA;AAAA,KAED,mBAAA;EAAA,UACA,qBAAA;AAAA;AAAA,iBAGI,mBAAA,kBAAA,CACd,KAAA,EAAO,CAAA,GACN,CAAA,GAAI,mBAAA;AAAA,iBASS,gBAAA,CAAiB,KAAA,YAAiB,KAAA,IAAS,mBAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/runtime/snapshot-source.ts
|
|
2
|
+
const SNAPSHOT_SOURCE_BRAND = Symbol.for("__ASKR_SNAPSHOT_SOURCE__");
|
|
3
|
+
function brandSnapshotSource(value) {
|
|
4
|
+
Object.defineProperty(value, SNAPSHOT_SOURCE_BRAND, {
|
|
5
|
+
value: true,
|
|
6
|
+
enumerable: false,
|
|
7
|
+
configurable: false
|
|
8
|
+
});
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
function isSnapshotSource(value) {
|
|
12
|
+
return !!value && typeof value === "object" && value[SNAPSHOT_SOURCE_BRAND] === true;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { brandSnapshotSource, isSnapshotSource };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=snapshot-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-source.js","names":[],"sources":["../../src/runtime/snapshot-source.ts"],"sourcesContent":["export const SNAPSHOT_SOURCE_BRAND = Symbol.for('__ASKR_SNAPSHOT_SOURCE__');\n\nexport type SnapshotSourceBrand = {\n readonly [SNAPSHOT_SOURCE_BRAND]: true;\n};\n\nexport function brandSnapshotSource<T extends object>(\n value: T\n): T & SnapshotSourceBrand {\n Object.defineProperty(value, SNAPSHOT_SOURCE_BRAND, {\n value: true,\n enumerable: false,\n configurable: false,\n });\n return value as T & SnapshotSourceBrand;\n}\n\nexport function isSnapshotSource(value: unknown): value is SnapshotSourceBrand {\n return (\n !!value &&\n typeof value === 'object' &&\n (value as Record<PropertyKey, unknown>)[SNAPSHOT_SOURCE_BRAND] === true\n );\n}\n"],"mappings":";AAAA,MAAa,wBAAwB,OAAO,IAAI,2BAA2B;AAM3E,SAAgB,oBACd,OACyB;CACzB,OAAO,eAAe,OAAO,uBAAuB;EAClD,OAAO;EACP,YAAY;EACZ,cAAc;EACf,CAAC;CACF,OAAO;;AAGT,SAAgB,iBAAiB,OAA8C;CAC7E,OACE,CAAC,CAAC,SACF,OAAO,UAAU,YAChB,MAAuC,2BAA2B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-bridge.js","names":[],"sources":["../../src/runtime/ssr-bridge.ts"],"sourcesContent":["import type { RenderContext } from '../common/ssr';\n\nexport type SSRBridge = {\n getCurrentSSRContext(): RenderContext | null;\n throwSSRDataMissing(): never;\n\n // Deterministic SSR render-phase data lookup\n getCurrentRenderData(): Record<string, unknown> | null;\n getNextKey(): string;\n};\n\nconst defaultBridge: SSRBridge = {\n getCurrentSSRContext() {\n return null;\n },\n throwSSRDataMissing() {\n throw new Error(\n '[Askr] SSR data missing (SSR bridge not installed). ' +\n 'If you are rendering on the server, ensure you are using the askr SSR entrypoints.'\n );\n },\n getCurrentRenderData() {\n return null;\n },\n getNextKey() {\n throw new Error(\n '[Askr] getNextKey() called outside SSR render phase (SSR bridge not installed).'\n );\n },\n};\n\nlet bridge: SSRBridge = defaultBridge;\n\nexport function installSSRBridge(next: SSRBridge): void {\n bridge = next;\n}\n\nexport function getSSRBridge(): SSRBridge {\n return bridge;\n}\n"],"mappings":"AA+BA,IAAI,SAAoB;CAnBtB,uBAAuB;
|
|
1
|
+
{"version":3,"file":"ssr-bridge.js","names":[],"sources":["../../src/runtime/ssr-bridge.ts"],"sourcesContent":["import type { RenderContext } from '../common/ssr';\n\nexport type SSRBridge = {\n getCurrentSSRContext(): RenderContext | null;\n throwSSRDataMissing(): never;\n\n // Deterministic SSR render-phase data lookup\n getCurrentRenderData(): Record<string, unknown> | null;\n getNextKey(): string;\n};\n\nconst defaultBridge: SSRBridge = {\n getCurrentSSRContext() {\n return null;\n },\n throwSSRDataMissing() {\n throw new Error(\n '[Askr] SSR data missing (SSR bridge not installed). ' +\n 'If you are rendering on the server, ensure you are using the askr SSR entrypoints.'\n );\n },\n getCurrentRenderData() {\n return null;\n },\n getNextKey() {\n throw new Error(\n '[Askr] getNextKey() called outside SSR render phase (SSR bridge not installed).'\n );\n },\n};\n\nlet bridge: SSRBridge = defaultBridge;\n\nexport function installSSRBridge(next: SSRBridge): void {\n bridge = next;\n}\n\nexport function getSSRBridge(): SSRBridge {\n return bridge;\n}\n"],"mappings":"AA+BA,IAAI,SAAoB;CAnBtB,uBAAuB;EACrB,OAAO;;CAET,sBAAsB;EACpB,MAAM,IAAI,MACR,yIAED;;CAEH,uBAAuB;EACrB,OAAO;;CAET,aAAa;EACX,MAAM,IAAI,MACR,kFACD;;CAImB;AAExB,SAAgB,iBAAiB,MAAuB;CACtD,SAAS;;AAGX,SAAgB,eAA0B;CACxC,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","names":[],"sources":["../../src/runtime/state.ts"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"state.d.ts","names":[],"sources":["../../src/runtime/state.ts"],"mappings":";;;;;;;;;;UAkCiB,KAAA,YAAiB,cAAA,CAAe,CAAA;EAAA,IAC3C,CAAA;EACJ,GAAA,CAAI,KAAA,EAAO,CAAA;EACX,GAAA,CAAI,OAAA,GAAU,IAAA,EAAM,CAAA,KAAM,CAAA;EAAA,CACzB,MAAA,CAAO,QAAP,KAAoB,QAAA,CAAS,KAAA,CAAM,CAAA,IAAK,KAAA,CAAM,CAAA;AAAA;;;;;;;;;;;;;;;;AA6BjD;;;;;;;;;;;iBAAgB,KAAA,GAAA,CAAS,YAAA,EAAc,CAAA,GAAI,KAAA,CAAM,CAAA"}
|
package/dist/runtime/state.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { globalScheduler } from "./scheduler.js";
|
|
2
1
|
import { markReactivePropsDirtySource, markReadableDerivedSubscribersDirty, notifyReadableReaders, recordReadableRead } from "./readable.js";
|
|
3
2
|
import { isBulkCommitActive } from "./fastlane.js";
|
|
4
3
|
import { claimHookIndex, getCurrentInstance } from "./component.js";
|
|
@@ -61,7 +60,6 @@ function state(initialValue) {
|
|
|
61
60
|
*/
|
|
62
61
|
function createStateCell(initialValue, instance) {
|
|
63
62
|
let value = initialValue;
|
|
64
|
-
let pendingNotifyFlushVersion = -1;
|
|
65
63
|
const readers = /* @__PURE__ */ new Map();
|
|
66
64
|
function read() {
|
|
67
65
|
read._hasBeenRead = true;
|
|
@@ -83,9 +81,7 @@ function createStateCell(initialValue, instance) {
|
|
|
83
81
|
value = newValue;
|
|
84
82
|
markReadableDerivedSubscribersDirty(read);
|
|
85
83
|
markReactivePropsDirtySource(read);
|
|
86
|
-
|
|
87
|
-
if (pendingNotifyFlushVersion === flushVersion && !globalScheduler.isExecuting()) return;
|
|
88
|
-
pendingNotifyFlushVersion = notifyReadableReaders(read) && !globalScheduler.isExecuting() ? flushVersion : -1;
|
|
84
|
+
notifyReadableReaders(read);
|
|
89
85
|
};
|
|
90
86
|
read[Symbol.iterator] = function* () {
|
|
91
87
|
yield read;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.js","names":[],"sources":["../../src/runtime/state.ts"],"sourcesContent":["/**\n * State primitive for Askr components\n * Optimized for minimal overhead and fast updates\n *\n * INVARIANTS ENFORCED:\n * - state() only callable during component render (currentInstance exists)\n * - state() called at top-level only (indices must be monotonically increasing)\n * - state values persist across re-renders (stored in stateValues array)\n * - state.set() cannot be called during render (causes infinite loops)\n * - state.set() always enqueues through scheduler (never direct mutation)\n * - state.set() callback (notifyUpdate) always available\n */\n\nimport {
|
|
1
|
+
{"version":3,"file":"state.js","names":[],"sources":["../../src/runtime/state.ts"],"sourcesContent":["/**\n * State primitive for Askr components\n * Optimized for minimal overhead and fast updates\n *\n * INVARIANTS ENFORCED:\n * - state() only callable during component render (currentInstance exists)\n * - state() called at top-level only (indices must be monotonically increasing)\n * - state values persist across re-renders (stored in stateValues array)\n * - state.set() cannot be called during render (causes infinite loops)\n * - state.set() always enqueues through scheduler (never direct mutation)\n * - state.set() callback (notifyUpdate) always available\n */\n\nimport {\n claimHookIndex,\n getCurrentInstance,\n type ComponentInstance,\n} from './component';\nimport { isBulkCommitActive } from './fastlane';\nimport {\n recordReadableRead,\n type ReadableSource,\n markReadableDerivedSubscribersDirty,\n markReactivePropsDirtySource,\n notifyReadableReaders,\n} from './readable';\n\n/**\n * State value holder - callable to read, has set method to update\n * @example\n * const count = state(0);\n * count(); // read: 0\n * count.set(1); // write: triggers re-render\n */\nexport interface State<T> extends ReadableSource<T> {\n (): T;\n set(value: T): void;\n set(updater: (prev: T) => T): void;\n [Symbol.iterator](): Iterator<State<T> | State<T>['set']>; // Allows destructuring: const [get, set] = state(0)\n}\n\n/**\n * Creates a local state value for a component\n * Optimized for:\n * - O(1) read performance\n * - Minimal allocation per state\n * - Fast scheduler integration\n *\n * IMPORTANT: state() must be called during component render execution.\n * It captures the current component instance from context.\n * Calling outside a component function will throw an error.\n *\n * @example\n * ```ts\n * // ✅ Correct: called during render\n * export function Counter() {\n * const count = state(0);\n * return { type: 'button', children: [count()] };\n * }\n *\n * // ❌ Wrong: called outside component\n * const count = state(0);\n * export function BadComponent() {\n * return { type: 'div' };\n * }\n * ```\n */\nexport function state<T>(initialValue: T): State<T> {\n // INVARIANT: state() must be called during component render\n const instance = getCurrentInstance();\n if (!instance) {\n throw new Error(\n 'state() can only be called during component render execution. ' +\n 'Move state() calls to the top level of your component function.'\n );\n }\n\n const index = claimHookIndex(instance, 'state');\n const stateValues = instance.stateValues;\n\n // INVARIANT: Reuse existing state if it exists (fast path on re-renders)\n // This ensures state identity and persistence and enforces ownership stability\n if (stateValues[index]) {\n const existing = stateValues[index] as State<T> & {\n _owner?: ComponentInstance;\n };\n // Ownership must be stable: the state cell belongs to the instance that\n // created it and must never change. This checks for accidental reuse.\n if (existing._owner !== instance) {\n throw new Error(\n `State ownership violation: state() called at index ${index} is owned by a different component instance. ` +\n `State ownership is positional and immutable.`\n );\n }\n return existing as State<T>;\n }\n\n // Create new state (slow path, only on first render) — delegated to helper\n const cell = createStateCell(initialValue, instance);\n\n // INVARIANT: Store state in instance for persistence across renders\n stateValues[index] = cell;\n\n return cell;\n}\n\n/**\n * Internal helper: create the backing state cell (value + readers + set semantics)\n * This extraction makes it easier to later split hook wiring from storage.\n */\nfunction createStateCell<T>(\n initialValue: T,\n instance: ComponentInstance\n): State<T> {\n let value = initialValue;\n\n // Per-state reader map: component -> last-committed render token\n const readers = new Map<ComponentInstance, number>();\n\n // Use a function as the state object (callable directly)\n function read(): T {\n (read as State<T>)._hasBeenRead = true;\n recordReadableRead(read as State<T>);\n\n return value;\n }\n\n // Attach the readers map to the callable so other runtime parts can access it\n (read as State<T>)._readers = readers;\n\n // Record explicit ownership of this state cell. Ownership is the component\n // instance that created the state cell and must never change for the life\n // of the cell. We expose this for runtime invariant checks/tests.\n (read as State<T> & { _owner?: ComponentInstance })._owner = instance;\n\n // Attach set method directly to function\n read.set = (newValueOrUpdater: T | ((prev: T) => T)): void => {\n // INVARIANT: State cannot be mutated during component render\n // (when currentInstance is non-null). It must be scheduled for consistency.\n const currentInst = getCurrentInstance();\n if (currentInst !== null) {\n throw new Error(\n `[Askr] state.set() cannot be called during component render. ` +\n `State mutations during render break the actor model and cause infinite loops. ` +\n `Move state updates to event handlers or use conditional rendering instead.`\n );\n }\n\n // Compute new value if an updater was provided\n let newValue: T;\n if (typeof newValueOrUpdater === 'function') {\n // Note: function-valued state cannot be set directly via a function argument;\n // such an argument is treated as a functional updater (this follows the common\n // convention from other libraries). If you need to store a function as state,\n // wrap it in an object.\n const updater = newValueOrUpdater as (prev: T) => T;\n newValue = updater(value);\n } else {\n newValue = newValueOrUpdater as T;\n }\n\n // Skip work if value didn't change\n if (Object.is(value, newValue)) return;\n\n // If a bulk commit is active, update backing value only and DO NOT notify or enqueue.\n // Bulk commits must be side-effect silent with respect to runtime notifications.\n if (isBulkCommitActive()) {\n // In bulk commit mode we must be side-effect free: update backing\n // value only and do not notify, enqueue, or log.\n value = newValue;\n return;\n }\n\n // INVARIANT: Update the value\n value = newValue;\n\n markReadableDerivedSubscribersDirty(read as State<T>);\n markReactivePropsDirtySource(read as State<T>);\n\n // notifyUpdate may be temporarily unavailable (e.g. during hydration).\n // We intentionally avoid logging here to keep the state mutation path\n // side-effect free. The scheduler will process updates when the system\n // is stable.\n // After value change, notify only components that *read* this state in their last committed render.\n void notifyReadableReaders(read as State<T>);\n };\n\n // Allow destructuring assignment: const [get, set] = state(0);\n // The state function is iterable and yields the getter then the setter\n (\n read as unknown as {\n [Symbol.iterator]?: () => Iterator<State<T> | State<T>['set']>;\n }\n )[Symbol.iterator] = function* (): Generator<\n State<T> | State<T>['set'],\n void,\n unknown\n > {\n yield read;\n yield read.set;\n };\n\n return read as State<T>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEA,SAAgB,MAAS,cAA2B;CAElD,MAAM,WAAW,oBAAoB;CACrC,IAAI,CAAC,UACH,MAAM,IAAI,MACR,gIAED;CAGH,MAAM,QAAQ,eAAe,UAAU,QAAQ;CAC/C,MAAM,cAAc,SAAS;CAI7B,IAAI,YAAY,QAAQ;EACtB,MAAM,WAAW,YAAY;EAK7B,IAAI,SAAS,WAAW,UACtB,MAAM,IAAI,MACR,sDAAsD,MAAM,2FAE7D;EAEH,OAAO;;CAIT,MAAM,OAAO,gBAAgB,cAAc,SAAS;CAGpD,YAAY,SAAS;CAErB,OAAO;;;;;;AAOT,SAAS,gBACP,cACA,UACU;CACV,IAAI,QAAQ;CAGZ,MAAM,0BAAU,IAAI,KAAgC;CAGpD,SAAS,OAAU;EACjB,KAAmB,eAAe;EAClC,mBAAmB,KAAiB;EAEpC,OAAO;;CAIT,KAAmB,WAAW;CAK9B,KAAoD,SAAS;CAG7D,KAAK,OAAO,sBAAkD;EAI5D,IADoB,oBAChB,KAAgB,MAClB,MAAM,IAAI,MACR,wNAGD;EAIH,IAAI;EACJ,IAAI,OAAO,sBAAsB,YAM/B,WAAW,kBAAQ,MAAM;OAEzB,WAAW;EAIb,IAAI,OAAO,GAAG,OAAO,SAAS,EAAE;EAIhC,IAAI,oBAAoB,EAAE;GAGxB,QAAQ;GACR;;EAIF,QAAQ;EAER,oCAAoC,KAAiB;EACrD,6BAA6B,KAAiB;EAO9C,AAAK,sBAAsB,KAAiB;;CAK9C,KAIE,OAAO,YAAY,aAInB;EACA,MAAM;EACN,MAAM,KAAK;;CAGb,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch-render.js","names":[],"sources":["../../src/ssg/batch-render.ts"],"sourcesContent":["/**\n * Batch rendering of multiple routes for SSG\n */\n\nimport { renderResolvedToStringSync } from '../ssr';\nimport type { RouteConfig, RouteRenderResult } from './types';\nimport type { RouteHandler } from '../common/router';\nimport type { ComponentFunction } from '../common/component';\nimport type { SSRData } from '../common/ssr';\nimport { getOutputFilePath, interpolateRoutePath } from './route-utils';\n\ninterface BatchRenderOptions {\n seed?: number;\n dataMap?: Record<string, SSRData>;\n concurrency?: number;\n}\n\n/**\n * Render multiple routes in parallel with error handling\n */\nexport async function batchRenderRoutes(\n routes: RouteConfig[],\n options: BatchRenderOptions = {}\n): Promise<RouteRenderResult[]> {\n const { seed = 12345, dataMap = {}, concurrency = 1 } = options;\n\n const workerCount = Math.max(1, Math.min(concurrency, routes.length || 1));\n const results: RouteRenderResult[] = [];\n results.length = routes.length;\n let nextIndex = 0;\n\n const renderOne = async (route: RouteConfig): Promise<RouteRenderResult> => {\n const startTime = performance.now();\n const url = interpolateRoutePath(route.path, route.params);\n const baseData = dataMap[route.path] ?? dataMap[url] ?? {};\n\n const mergedHandler: RouteHandler = route.handler\n ? route.handler\n : (params, ctx?: unknown) => {\n const component = route.component as ComponentFunction;\n return component(\n { ...route.props, ...params },\n ctx as { signal: AbortSignal; ssr?: unknown }\n );\n };\n\n try {\n const routeEntry = {\n path: route.path,\n handler: mergedHandler,\n namespace: route.namespace,\n };\n\n const html = renderResolvedToStringSync({\n url,\n routes: [routeEntry],\n handler: mergedHandler,\n params: route.params,\n options: {\n seed,\n data: baseData,\n },\n });\n\n const duration = performance.now() - startTime;\n return {\n path: url,\n filePath: getOutputFilePath(url),\n html,\n fileSize: Buffer.byteLength(html, 'utf8'),\n renderDuration: Math.round(duration),\n resourceCount: Object.keys(baseData).length,\n status: 'success',\n reason: 'full',\n written: false,\n };\n } catch (error) {\n const duration = performance.now() - startTime;\n return {\n path: url,\n filePath: getOutputFilePath(url),\n html: '',\n fileSize: 0,\n renderDuration: Math.round(duration),\n resourceCount: Object.keys(baseData).length,\n status: 'error',\n reason: 'full',\n written: false,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n };\n\n const worker = async () => {\n while (true) {\n const current = nextIndex;\n nextIndex += 1;\n if (current >= routes.length) return;\n results[current] = await renderOne(routes[current]);\n }\n };\n\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\n return results;\n}\n"],"mappings":";;;;;;;;;AAoBA,eAAsB,kBACpB,QACA,UAA8B,EAAE,EACF;CAC9B,MAAM,EAAE,OAAO,OAAO,UAAU,EAAE,EAAE,cAAc,MAAM;CAExD,MAAM,cAAc,KAAK,IAAI,GAAG,KAAK,IAAI,aAAa,OAAO,UAAU,EAAE,CAAC;CAC1E,MAAM,UAA+B,EAAE;
|
|
1
|
+
{"version":3,"file":"batch-render.js","names":[],"sources":["../../src/ssg/batch-render.ts"],"sourcesContent":["/**\n * Batch rendering of multiple routes for SSG\n */\n\nimport { renderResolvedToStringSync } from '../ssr';\nimport type { RouteConfig, RouteRenderResult } from './types';\nimport type { RouteHandler } from '../common/router';\nimport type { ComponentFunction } from '../common/component';\nimport type { SSRData } from '../common/ssr';\nimport { getOutputFilePath, interpolateRoutePath } from './route-utils';\n\ninterface BatchRenderOptions {\n seed?: number;\n dataMap?: Record<string, SSRData>;\n concurrency?: number;\n}\n\n/**\n * Render multiple routes in parallel with error handling\n */\nexport async function batchRenderRoutes(\n routes: RouteConfig[],\n options: BatchRenderOptions = {}\n): Promise<RouteRenderResult[]> {\n const { seed = 12345, dataMap = {}, concurrency = 1 } = options;\n\n const workerCount = Math.max(1, Math.min(concurrency, routes.length || 1));\n const results: RouteRenderResult[] = [];\n results.length = routes.length;\n let nextIndex = 0;\n\n const renderOne = async (route: RouteConfig): Promise<RouteRenderResult> => {\n const startTime = performance.now();\n const url = interpolateRoutePath(route.path, route.params);\n const baseData = dataMap[route.path] ?? dataMap[url] ?? {};\n\n const mergedHandler: RouteHandler = route.handler\n ? route.handler\n : (params, ctx?: unknown) => {\n const component = route.component as ComponentFunction;\n return component(\n { ...route.props, ...params },\n ctx as { signal: AbortSignal; ssr?: unknown }\n );\n };\n\n try {\n const routeEntry = {\n path: route.path,\n handler: mergedHandler,\n namespace: route.namespace,\n };\n\n const html = renderResolvedToStringSync({\n url,\n routes: [routeEntry],\n handler: mergedHandler,\n params: route.params,\n options: {\n seed,\n data: baseData,\n },\n });\n\n const duration = performance.now() - startTime;\n return {\n path: url,\n filePath: getOutputFilePath(url),\n html,\n fileSize: Buffer.byteLength(html, 'utf8'),\n renderDuration: Math.round(duration),\n resourceCount: Object.keys(baseData).length,\n status: 'success',\n reason: 'full',\n written: false,\n };\n } catch (error) {\n const duration = performance.now() - startTime;\n return {\n path: url,\n filePath: getOutputFilePath(url),\n html: '',\n fileSize: 0,\n renderDuration: Math.round(duration),\n resourceCount: Object.keys(baseData).length,\n status: 'error',\n reason: 'full',\n written: false,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n };\n\n const worker = async () => {\n while (true) {\n const current = nextIndex;\n nextIndex += 1;\n if (current >= routes.length) return;\n results[current] = await renderOne(routes[current]);\n }\n };\n\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\n return results;\n}\n"],"mappings":";;;;;;;;;AAoBA,eAAsB,kBACpB,QACA,UAA8B,EAAE,EACF;CAC9B,MAAM,EAAE,OAAO,OAAO,UAAU,EAAE,EAAE,cAAc,MAAM;CAExD,MAAM,cAAc,KAAK,IAAI,GAAG,KAAK,IAAI,aAAa,OAAO,UAAU,EAAE,CAAC;CAC1E,MAAM,UAA+B,EAAE;CACvC,QAAQ,SAAS,OAAO;CACxB,IAAI,YAAY;CAEhB,MAAM,YAAY,OAAO,UAAmD;EAC1E,MAAM,YAAY,YAAY,KAAK;EACnC,MAAM,MAAM,qBAAqB,MAAM,MAAM,MAAM,OAAO;EAC1D,MAAM,WAAW,QAAQ,MAAM,SAAS,QAAQ,QAAQ,EAAE;EAE1D,MAAM,gBAA8B,MAAM,UACtC,MAAM,WACL,QAAQ,QAAkB;GACzB,MAAM,YAAY,MAAM;GACxB,OAAO,UACL;IAAE,GAAG,MAAM;IAAO,GAAG;IAAQ,EAC7B,IACD;;EAGP,IAAI;GAOF,MAAM,OAAO,2BAA2B;IACtC;IACA,QAAQ,CAAC;KAPT,MAAM,MAAM;KACZ,SAAS;KACT,WAAW,MAAM;KAKR,CAAW;IACpB,SAAS;IACT,QAAQ,MAAM;IACd,SAAS;KACP;KACA,MAAM;KACP;IACF,CAAC;GAEF,MAAM,WAAW,YAAY,KAAK,GAAG;GACrC,OAAO;IACL,MAAM;IACN,UAAU,kBAAkB,IAAI;IAChC;IACA,UAAU,OAAO,WAAW,MAAM,OAAO;IACzC,gBAAgB,KAAK,MAAM,SAAS;IACpC,eAAe,OAAO,KAAK,SAAS,CAAC;IACrC,QAAQ;IACR,QAAQ;IACR,SAAS;IACV;WACM,OAAO;GACd,MAAM,WAAW,YAAY,KAAK,GAAG;GACrC,OAAO;IACL,MAAM;IACN,UAAU,kBAAkB,IAAI;IAChC,MAAM;IACN,UAAU;IACV,gBAAgB,KAAK,MAAM,SAAS;IACpC,eAAe,OAAO,KAAK,SAAS,CAAC;IACrC,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;IAC9D;;;CAIL,MAAM,SAAS,YAAY;EACzB,OAAO,MAAM;GACX,MAAM,UAAU;GAChB,aAAa;GACb,IAAI,WAAW,OAAO,QAAQ;GAC9B,QAAQ,WAAW,MAAM,UAAU,OAAO,SAAS;;;CAIvD,MAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,aAAa,QAAQ,QAAQ,CAAC,CAAC;CACtE,OAAO"}
|
|
@@ -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":";;;;;;;;AA4CA,SAAS,yBACP,YACe;CACf,MAAM,EAAE,UAAU;AAElB,KAAI,MAAM,SAAS,KACjB,QAAO,0BAA0B,WAAW,KAAK;AAGnD,KAAI,MAAM,KACR,QAAO,0BAA0B,WAAW,KAAK;AAGnD,KAAI,MAAM,WACR,QAAO,0BAA0B,WAAW,KAAK;AAGnD,KAAI,MAAM,YAAY,MAAM,SAAS,SAAS,EAC5C,QAAO,0BAA0B,WAAW,KAAK;AAGnD,QAAO;;AAGT,SAAS,mBAAmB,WAAgD;AAC1E,KAAI,cAAc,OAChB,QAAO,KAAK,IAAI,GAAG,aAAa,EAAE;CAGpC,MAAM,iBAAiB;CAKvB,MAAM,aAAa,OACjB,eAAe,SAAS,KAAK,oBAC3B,eAAe,SAAS,KAAK,wBAC7B,eAAe,SAAS,KAAK,mBAChC;AACD,KAAI,OAAO,SAAS,WAAW,IAAI,aAAa,EAC9C,QAAO,KAAK,IAAI,GAAG,KAAK,MAAM,WAAW,CAAC;AAG5C,KAAI,OAAO,eAAe,WAAW,wBAAwB,SAC3D,QAAO,KAAK,IAAI,GAAG,eAAe,UAAU,oBAAoB;AAGlE,QAAO;;;;;;;;;;;;;;;;;;;;;;AAuBT,SAAgB,gBAAgB,SAAqB;CACnD,IAAI,SAA2B;CAC/B,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,sBAAsB,mBAAmB,QAAQ,YAAY;CACnE,MAAM,sBAAsB,KAAK,IAC/B,GACA,QAAQ,eAAe,oBACxB;AAED,KAAI,CAAC,MAAM,QAAQ,QAAQ,OAAO,IAAI,QAAQ,OAAO,WAAW,EAC9D,OAAM,IAAI,MAAM,2BAA2B;AAG7C,KAAI,CAAC,QAAQ,aAAa,QAAQ,UAAU,MAAM,CAAC,WAAW,EAC5D,OAAM,IAAI,MAAM,wBAAwB;AAG1C,QAAO;;;;;;EAML,MAAM,SACJ,kBAAsC,EAAE,EACpB;AAEpB,kBAAe,QAAQ,OAAO;GAE9B,MAAM,cAAc,cAAc,gBAAgB,YAAY;GAC9D,MAAM,gBAAgB,cAAc,gBAAgB,cAAc;GAClE,MAAM,gBAAgB,gBAAgB,QAAQ;GAC9C,MAAM,mBACJ,CAAC,gBAAgB,aAAa,kBAAkB,gBAC5C,wBAAwB,QAAQ,WAAW,KAAK,GAChD;GACN,MAAM,gBACJ,gBAAgB,aAChB,kBAAkB,iBAClB,qBAAqB,OACjB,SACA;GAGN,MAAM,UAAU,eAAe,QAAQ,QAAQ,EAC7C,eAAe,QAAQ,eACxB,CAAC;GAEF,MAAM,cAAc,QAAQ,OAAO,IAAI,uBAAuB;GAC9D,MAAM,mCAAmB,IAAI,KAAgC;GAC7D,MAAM,sBAAiD,EAAE;AAEzD,QAAK,MAAM,cAAc,aAAa;IACpC,MAAM,aAAa,yBAAyB,WAAW;AACvD,QAAI,YAAY;AACd,sBAAiB,IAAI,WAAW,SAAS;MACvC,MAAM,WAAW;MACjB,UAAU,WAAW;MACrB,MAAM;MACN,UAAU;MACV,gBAAgB;MAChB,eAAe;MACf,QAAQ;MACR,QAAQ;MACR,SAAS;MACT,OAAO;MACR,CAAC;AACF;;AAGF,wBAAoB,KAAK,WAAW;;GAGtC,MAAM,kBAAkB,IAAI,KACzB,kBAAkB,UAAU,EAAE,EAAE,KAAK,UAAU,CAAC,MAAM,SAAS,MAAM,CAAC,CACxE;GACD,MAAM,kBAAkB,IAAI,IAC1B,oBAAoB,KAAK,eAAe,WAAW,QAAQ,CAC5D;GAED,MAAM,WAA4B,EAAE;GACpC,MAAM,iBAAkC,EAAE;AAC1C,QAAK,IAAI,QAAQ,GAAG,QAAQ,oBAAoB,QAAQ,SAAS,GAAG;IAClE,MAAM,aAAa,oBAAoB;IACvC,MAAM,QAAQ,yBACZ,YACA,gBAAgB,IAAI,WAAW,QAAQ,IAAI,MAC3C,eACA,aACA,cACD;AACD,aAAS,KAAK,MAAM;AACpB,QAAI,MAAM,WAAW,YACnB,gBAAe,KAAK,MAAM;;GAG9B,MAAM,kBAAkB,YAAY,KAAK;AACzC,uBAAoB,kBAAkB,oBAAoB;GAC1D,MAAM,kBACJ,eAAe,SAAS,IACpB,MAAM,kBAAkB,kBAAkB,eAAe,EAAE;IACzD;IACA;IACA,aAAa;IACd,CAAC,GACF,EAAE;AACR,mBAAgB,mBAAmB,YAAY,KAAK,GAAG,gBAAgB;GACvE,MAAM,oCAAoB,IAAI,KAAgC;AAC9D,QAAK,IAAI,QAAQ,GAAG,QAAQ,gBAAgB,QAAQ,SAAS,EAC3D,mBAAkB,IAChB,eAAe,OAAO,WAAW,SACjC,gBAAgB,OACjB;GAGH,IAAI,YAAY;GAChB,MAAM,qBAAsD,EAAE;AAE9D,QAAK,MAAM,SAAS,UAAU;IAC5B,MAAM,EAAE,YAAY,UAAU,WAAW;IACzC,MAAM,WACJ,QAAQ,WAAW,MAAM,SAAS,QAAQ,WAAW,SAAS,EAAE;IAClE,MAAM,gBAAgB,OAAO,KAAK,SAAS,CAAC;AAE5C,QAAI,WAAW,aAAa;AAC1B,sBAAiB,IAAI,WAAW,SAAS;MACvC,MAAM,WAAW;MACjB,UAAU,WAAW;MACrB,MAAM;MACN,UACE,UAAU,aAAa,QAAQ,aAAa,OACxC,0BACE,QAAQ,WACR,WAAW,SACZ,GACD;MACN,gBAAgB;MAChB;MACA,QAAQ;MACR,QAAQ;MACR,SAAS;MACV,CAAC;AAEF,SAAI,SACF,oBAAmB,KAAK;MACtB,GAAG;MACH,MAAM,WAAW;MACjB,UAAU,WAAW;MACrB,kBAAkB,WAAW,iBAAiB,OAAO;MACtD,CAAC;AAEJ;;IAGF,MAAM,WAAW,kBAAkB,IAAI,WAAW,QAAQ;AAC1D,QAAI,CAAC,SACH,OAAM,IAAI,MACR,sCAAsC,WAAW,KAAK,GACvD;IAGH,MAAM,aAAgC;KACpC,GAAG;KACH,MAAM,WAAW;KACjB,UAAU,WAAW;KACrB;KACA;KACA,SAAS;KACV;AAED,QAAI,WAAW,WAAW,WAAW;KACnC,MAAM,aAAa,SAAS,WAAW,KAAK;KAC5C,MAAM,cACJ,kBAAkB,UAClB,CAAC,YACD,SAAS,aAAa,cACtB,CAAC,iBAAiB,QAAQ,WAAW,WAAW,SAAS;AAE3D,gBAAW,UAAU;AACrB,SAAI,CAAC,YACH,cAAa;AAGf,wBAAmB,KAAK;MACtB,SAAS,WAAW;MACpB,MAAM,WAAW;MACjB,UAAU,WAAW;MACrB,kBAAkB,WAAW,iBAAiB,OAAO;MACrD,UAAU;MACV,YAAY;MACb,CAAC;UAEF,oBAAmB,KAAK;KACtB,SAAS,WAAW;KACpB,MAAM,WAAW;KACjB,UAAU,WAAW;KACrB,kBAAkB,WAAW,iBAAiB,OAAO;KACrD,UAAU,UAAU,YAAY;KAChC,YAAY;KACb,CAAC;AAGJ,qBAAiB,IAAI,WAAW,SAAS,WAAW;;GAGtD,MAAM,iBAAiB,2BACrB,kBACA,gBACD;GACD,MAAM,eAAoC,EAAE;AAC5C,QAAK,IAAI,QAAQ,GAAG,QAAQ,YAAY,QAAQ,SAAS,GAAG;IAC1D,MAAM,cAAc,iBAAiB,IAAI,YAAY,OAAO,QAAQ;AACpE,QAAI,YACF,cAAa,KAAK,YAAY;;AAGlC,gBAAa,KAAK,GAAG,eAAe;GAGpC,MAAM,iBAAiB,YAAY,KAAK;AACxC,SAAM,iBAAiB,cAAc,QAAQ,WAAW,EACtD,aAAa,qBACd,CAAC;AACF,mBAAgB,kBAAkB,YAAY,KAAK,GAAG,eAAe;AAGrE,YAAS,kBAAkB,cAAc;IACvC,MAAM;IACN;IACA,iBAAiB;IACjB,mBAAmB;IACpB,CAAC;AAIF,SAAM,cADW,iBAAiB,OACd,EAAU,QAAQ,UAAU;AAEhD,SAAM,yBACJ;IACE,eAAA;IACA;IACA,MAAM;IACN,QAAQ;IACT,EACD,QAAQ,UACT;AAED,UAAO;;;;;EAMT,YAAY;AACV,UAAO;IACL,YAAY,QAAQ,OAAO;IAC3B,WAAW,QAAQ;IACnB;IACA,aAAa;IACb,aAAa;IACb,kBAAkB,CAAC,CAAC,QAAQ;IAC7B;;;;;;EAOH,YAA8B;AAC5B,UAAO;;EAEV;;AAGH,SAAS,cAAc,QAA6B;AAClD,QAAO,SAAS,MAAM,KAAK,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE;;AAGlD,SAAS,kBAAkB,UAAiD;CAC1E,MAAM,SAA+B,EAAE;AACvC,MAAK,IAAI,QAAQ,GAAG,QAAQ,SAAS,QAAQ,SAAS,EACpD,QAAO,KAAK,SAAS,OAAO,WAAW,MAAM;AAE/C,QAAO;;AAGT,SAAS,yBACP,YACA,UACA,MACA,aACA,eACe;AACf,KAAI,SAAS,OACX,QAAO;EAAE;EAAY,QAAQ;EAAQ;EAAU;AAGjD,KAAI,CAAC,SACH,QAAO;EAAE;EAAY,QAAQ;EAAa;EAAU;AAGtD,KAAI,SAAS,eAAe,WAAW,SAAS,aAAa,KAC3D,QAAO;EAAE;EAAY,QAAQ;EAAa;EAAU;AAGtD,KAAI,WAAW,iBAAiB,WAAW,EACzC,QAAO;EAAE;EAAY,QAAQ;EAAW;EAAU;AAGpD,KAAI,cAAc,SAAS,WAAW,KAAK,CACzC,QAAO;EAAE;EAAY,QAAQ;EAAiB;EAAU;AAG1D,KAAI,WAAW,iBAAiB,MAAM,QAAQ,YAAY,SAAS,IAAI,CAAC,CACtE,QAAO;EAAE;EAAY,QAAQ;EAAe;EAAU;AAGxD,QAAO;EAAE;EAAY,QAAQ;EAAa;EAAU;;AAGtD,SAAS,2BACP,UACA,iBACqB;AACrB,KAAI,CAAC,SACH,QAAO,EAAE;CAGX,MAAM,UAA+B,EAAE;AACvC,MAAK,IAAI,QAAQ,GAAG,QAAQ,SAAS,OAAO,QAAQ,SAAS,GAAG;EAC9D,MAAM,QAAQ,SAAS,OAAO;AAC9B,MAAI,gBAAgB,IAAI,MAAM,QAAQ,CACpC;AAEF,UAAQ,KAAK;GACX,MAAM,MAAM;GACZ,UAAU,MAAM;GAChB,MAAM;GACN,UAAU;GACV,gBAAgB;GAChB,eAAe;GACf,QAAQ;GACR,QAAQ;GACR,SAAS;GACV,CAAC;;AAGJ,QAAO"}
|
|
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"}
|
|
@@ -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"}
|