@askrjs/askr 0.0.95 → 0.0.96
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/benchmark.js +2 -2
- package/dist/ssg/index.js +160 -119
- package/package.json +1 -1
package/dist/benchmark.js
CHANGED
|
@@ -63,8 +63,8 @@ function BenchmarkTable({ rows, isSelected, onSelect, onRemove }) {
|
|
|
63
63
|
installRendererBridge();
|
|
64
64
|
const benchmarkMetadata = {
|
|
65
65
|
packageName: "@askrjs/askr",
|
|
66
|
-
packageVersion: "0.0.
|
|
67
|
-
buildLabel: "0.0.
|
|
66
|
+
packageVersion: "0.0.96",
|
|
67
|
+
buildLabel: "0.0.96-local"
|
|
68
68
|
};
|
|
69
69
|
function getBenchmarkMetadata() {
|
|
70
70
|
return { ...benchmarkMetadata };
|
package/dist/ssg/index.js
CHANGED
|
@@ -10,6 +10,23 @@ import * as pathModule from "node:path";
|
|
|
10
10
|
import * as os from "node:os";
|
|
11
11
|
import * as fsSync from "node:fs";
|
|
12
12
|
//#region src/ssg/route-utils.ts
|
|
13
|
+
function getPortableOutputPathKey(filePath) {
|
|
14
|
+
return filePath.normalize("NFC").replaceAll("\\", "/").toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
function describeRoute(descriptor) {
|
|
17
|
+
if (descriptor.route.path === descriptor.path) return `"${descriptor.path}"`;
|
|
18
|
+
return `"${descriptor.route.path}" (resolved as "${descriptor.path}")`;
|
|
19
|
+
}
|
|
20
|
+
/** Reject routes that could overwrite one another on any supported filesystem. */
|
|
21
|
+
function validateOutputPathCollisions(descriptors) {
|
|
22
|
+
const seen = /* @__PURE__ */ new Map();
|
|
23
|
+
for (const descriptor of descriptors) {
|
|
24
|
+
const key = getPortableOutputPathKey(descriptor.filePath);
|
|
25
|
+
const existing = seen.get(key);
|
|
26
|
+
if (existing) throw new Error(`SSG output path collision: routes ${describeRoute(existing)} and ${describeRoute(descriptor)} both map to "${descriptor.filePath}" after case-insensitive normalization`);
|
|
27
|
+
seen.set(key, descriptor);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
13
30
|
function assertSafeRoutePath(pathStr) {
|
|
14
31
|
if (!pathStr.startsWith("/") || pathStr.startsWith("//") || pathStr.includes("\\") || pathStr.includes("\0") || pathStr.split("/").some((segment) => segment === "." || segment === "..")) throw new Error(`SSG route path must be an absolute URL path without dot segments or backslashes: ${pathStr}`);
|
|
15
32
|
}
|
|
@@ -663,6 +680,24 @@ function resolveParallelism(requested) {
|
|
|
663
680
|
const availableParallelism = os.availableParallelism;
|
|
664
681
|
return Math.max(1, availableParallelism?.() ?? os.cpus().length ?? 1);
|
|
665
682
|
}
|
|
683
|
+
const outputDirectoryQueues = /* @__PURE__ */ new Map();
|
|
684
|
+
async function withOutputDirectoryLock(outputDir, operation) {
|
|
685
|
+
const key = pathModule.resolve(outputDir).normalize("NFC").toLowerCase();
|
|
686
|
+
const previous = outputDirectoryQueues.get(key) ?? Promise.resolve();
|
|
687
|
+
let release;
|
|
688
|
+
const current = new Promise((resolve) => {
|
|
689
|
+
release = resolve;
|
|
690
|
+
});
|
|
691
|
+
const tail = previous.then(() => current, () => current);
|
|
692
|
+
outputDirectoryQueues.set(key, tail);
|
|
693
|
+
await previous.catch(() => void 0);
|
|
694
|
+
try {
|
|
695
|
+
return await operation();
|
|
696
|
+
} finally {
|
|
697
|
+
release();
|
|
698
|
+
if (outputDirectoryQueues.get(key) === tail) outputDirectoryQueues.delete(key);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
666
701
|
async function createStagingDirectory(outputDir) {
|
|
667
702
|
const parent = pathModule.dirname(outputDir);
|
|
668
703
|
const base = pathModule.basename(outputDir);
|
|
@@ -753,136 +788,142 @@ function createStaticGen(options) {
|
|
|
753
788
|
* Returns detailed results
|
|
754
789
|
*/
|
|
755
790
|
async generate(generateOptions = {}) {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
791
|
+
return withOutputDirectoryLock(options.outputDir, async () => {
|
|
792
|
+
const splitRoutes = await splitStaticRoutes(configuredRoutes);
|
|
793
|
+
const routes = splitRoutes.routes;
|
|
794
|
+
validateRoutes(routes);
|
|
795
|
+
const changedKeys = dedupeStrings(generateOptions.changedKeys);
|
|
796
|
+
const changedRoutes = dedupeStrings(generateOptions.changedRoutes);
|
|
797
|
+
const requestedMode = generateOptions.mode ?? "full";
|
|
798
|
+
const previousManifest = !generateOptions.forceFull && requestedMode === "incremental" ? readIncrementalManifest(options.outputDir, seed) : null;
|
|
799
|
+
const effectiveMode = generateOptions.forceFull || requestedMode !== "incremental" || previousManifest === null ? "full" : "incremental";
|
|
800
|
+
const dataMap = resolveSsgData(routes, { dataOverrides: options.dataOverrides });
|
|
801
|
+
const descriptors = routes.map(resolveRouteDescriptor);
|
|
802
|
+
validateOutputPathCollisions(descriptors);
|
|
803
|
+
const routeResultsById = /* @__PURE__ */ new Map();
|
|
804
|
+
for (const runtimeRoute of splitRoutes.runtimeOnly) routeResultsById.set(runtimeRoute.routeId, runtimeRoute.result);
|
|
805
|
+
const eligibleDescriptors = descriptors;
|
|
806
|
+
const previousEntries = new Map((previousManifest?.routes ?? []).map((entry) => [entry.routeId, entry]));
|
|
807
|
+
const currentRouteIds = new Set(eligibleDescriptors.map((descriptor) => descriptor.routeId));
|
|
808
|
+
const selected = [];
|
|
809
|
+
const routesToRender = [];
|
|
810
|
+
for (let index = 0; index < eligibleDescriptors.length; index += 1) {
|
|
811
|
+
const descriptor = eligibleDescriptors[index];
|
|
812
|
+
const entry = selectRouteForGeneration(descriptor, previousEntries.get(descriptor.routeId) ?? null, effectiveMode, changedKeys, changedRoutes);
|
|
813
|
+
selected.push(entry);
|
|
814
|
+
if (entry.reason !== "unchanged") routesToRender.push(entry);
|
|
815
|
+
}
|
|
816
|
+
const renderStartTime = performance.now();
|
|
817
|
+
const renderedResults = routesToRender.length > 0 ? await batchRenderRoutes(getRoutesToRender(routesToRender), {
|
|
818
|
+
seed,
|
|
819
|
+
dataMap,
|
|
820
|
+
concurrency: resolvedConcurrency,
|
|
821
|
+
document: options.document,
|
|
822
|
+
styleRegistrationValidation: options.styleRegistrationValidation
|
|
823
|
+
}) : [];
|
|
824
|
+
performance.now() - renderStartTime;
|
|
825
|
+
const renderedByRouteId = /* @__PURE__ */ new Map();
|
|
826
|
+
for (let index = 0; index < renderedResults.length; index += 1) renderedByRouteId.set(routesToRender[index].descriptor.routeId, renderedResults[index]);
|
|
827
|
+
let cacheHits = 0;
|
|
828
|
+
const nextManifestRoutes = [];
|
|
829
|
+
for (const entry of selected) {
|
|
830
|
+
const { descriptor, previous, reason } = entry;
|
|
831
|
+
const resolvedData = resolveSsgRouteData(dataMap, descriptor.route.path, descriptor.path);
|
|
832
|
+
const baseData = resolvedData.hasData ? resolvedData.data : void 0;
|
|
833
|
+
const resourceCount = resolvedData.hasData && baseData ? Object.keys(baseData).length : 0;
|
|
834
|
+
if (reason === "unchanged") {
|
|
835
|
+
routeResultsById.set(descriptor.routeId, {
|
|
836
|
+
path: descriptor.path,
|
|
837
|
+
filePath: descriptor.filePath,
|
|
838
|
+
html: "",
|
|
839
|
+
fileSize: previous?.htmlHash !== null && previous !== null ? getExistingOutputFileSize(options.outputDir, descriptor.filePath) : 0,
|
|
840
|
+
renderDuration: 0,
|
|
841
|
+
resourceCount,
|
|
842
|
+
status: "skipped",
|
|
843
|
+
reason: "unchanged",
|
|
844
|
+
written: false
|
|
845
|
+
});
|
|
846
|
+
if (previous) nextManifestRoutes.push({
|
|
847
|
+
...previous,
|
|
848
|
+
path: descriptor.path,
|
|
849
|
+
filePath: descriptor.filePath,
|
|
850
|
+
invalidationKeys: descriptor.invalidationKeys.slice()
|
|
851
|
+
});
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
const rendered = renderedByRouteId.get(descriptor.routeId);
|
|
855
|
+
if (!rendered) throw new Error(`Missing rendered result for route "${descriptor.path}"`);
|
|
856
|
+
const nextResult = {
|
|
857
|
+
...rendered,
|
|
799
858
|
path: descriptor.path,
|
|
800
859
|
filePath: descriptor.filePath,
|
|
801
|
-
html: "",
|
|
802
|
-
fileSize: previous?.htmlHash !== null && previous !== null ? getExistingOutputFileSize(options.outputDir, descriptor.filePath) : 0,
|
|
803
|
-
renderDuration: 0,
|
|
804
860
|
resourceCount,
|
|
805
|
-
|
|
806
|
-
reason: "unchanged",
|
|
861
|
+
reason,
|
|
807
862
|
written: false
|
|
808
|
-
}
|
|
809
|
-
if (
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
resourceCount,
|
|
824
|
-
reason,
|
|
825
|
-
written: false
|
|
826
|
-
};
|
|
827
|
-
if (nextResult.status === "success") {
|
|
828
|
-
const htmlDigest = hashHtml(nextResult.html);
|
|
829
|
-
const shouldWrite = effectiveMode === "full" || !previous || previous.htmlHash !== htmlDigest || !outputFileExists(options.outputDir, descriptor.filePath);
|
|
830
|
-
nextResult.written = shouldWrite;
|
|
831
|
-
if (!shouldWrite) cacheHits += 1;
|
|
832
|
-
nextManifestRoutes.push({
|
|
863
|
+
};
|
|
864
|
+
if (nextResult.status === "success") {
|
|
865
|
+
const htmlDigest = hashHtml(nextResult.html);
|
|
866
|
+
const shouldWrite = effectiveMode === "full" || !previous || previous.htmlHash !== htmlDigest || !outputFileExists(options.outputDir, descriptor.filePath);
|
|
867
|
+
nextResult.written = shouldWrite;
|
|
868
|
+
if (!shouldWrite) cacheHits += 1;
|
|
869
|
+
nextManifestRoutes.push({
|
|
870
|
+
routeId: descriptor.routeId,
|
|
871
|
+
path: descriptor.path,
|
|
872
|
+
filePath: descriptor.filePath,
|
|
873
|
+
invalidationKeys: descriptor.invalidationKeys.slice(),
|
|
874
|
+
htmlHash: htmlDigest,
|
|
875
|
+
lastStatus: "success"
|
|
876
|
+
});
|
|
877
|
+
} else nextManifestRoutes.push({
|
|
833
878
|
routeId: descriptor.routeId,
|
|
834
879
|
path: descriptor.path,
|
|
835
880
|
filePath: descriptor.filePath,
|
|
836
881
|
invalidationKeys: descriptor.invalidationKeys.slice(),
|
|
837
|
-
htmlHash:
|
|
838
|
-
lastStatus: "
|
|
882
|
+
htmlHash: previous?.htmlHash ?? null,
|
|
883
|
+
lastStatus: "error"
|
|
839
884
|
});
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const removedResults = collectRemovedRouteResults(previousManifest, currentRouteIds);
|
|
851
|
-
const routeResults = [];
|
|
852
|
-
for (const routeId of splitRoutes.outputRouteIds) {
|
|
853
|
-
const routeResult = routeResultsById.get(routeId);
|
|
854
|
-
if (routeResult) routeResults.push(routeResult);
|
|
855
|
-
}
|
|
856
|
-
routeResults.push(...removedResults);
|
|
857
|
-
result = generateSSGResult(routeResults, {
|
|
858
|
-
mode: effectiveMode,
|
|
859
|
-
cacheHits,
|
|
860
|
-
invalidatedKeys: changedKeys,
|
|
861
|
-
invalidatedRoutes: changedRoutes
|
|
862
|
-
});
|
|
863
|
-
if (effectiveMode === "full" && result.failed > 0) return result;
|
|
864
|
-
const targetOutputDir = effectiveMode === "full" ? await createStagingDirectory(options.outputDir) : options.outputDir;
|
|
865
|
-
try {
|
|
866
|
-
const writeStartTime = performance.now();
|
|
867
|
-
await writeStaticFiles(routeResults, targetOutputDir, { concurrency: resolvedConcurrency });
|
|
868
|
-
performance.now() - writeStartTime;
|
|
869
|
-
await writeMetadata(resultToMetadata(result), targetOutputDir);
|
|
870
|
-
await writeIncrementalManifest({
|
|
871
|
-
schemaVersion: 1,
|
|
872
|
-
seed,
|
|
885
|
+
routeResultsById.set(descriptor.routeId, nextResult);
|
|
886
|
+
}
|
|
887
|
+
const removedResults = collectRemovedRouteResults(previousManifest, currentRouteIds);
|
|
888
|
+
const routeResults = [];
|
|
889
|
+
for (const routeId of splitRoutes.outputRouteIds) {
|
|
890
|
+
const routeResult = routeResultsById.get(routeId);
|
|
891
|
+
if (routeResult) routeResults.push(routeResult);
|
|
892
|
+
}
|
|
893
|
+
routeResults.push(...removedResults);
|
|
894
|
+
result = generateSSGResult(routeResults, {
|
|
873
895
|
mode: effectiveMode,
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
if (effectiveMode === "full") await replaceOutputDirectory(targetOutputDir, options.outputDir);
|
|
878
|
-
} catch (error) {
|
|
879
|
-
if (effectiveMode === "full") await fs.rm(targetOutputDir, {
|
|
880
|
-
recursive: true,
|
|
881
|
-
force: true
|
|
896
|
+
cacheHits,
|
|
897
|
+
invalidatedKeys: changedKeys,
|
|
898
|
+
invalidatedRoutes: changedRoutes
|
|
882
899
|
});
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
900
|
+
if (effectiveMode === "full" && result.failed > 0) {
|
|
901
|
+
for (const routeResult of result.routes) if (routeResult.status === "success") routeResult.written = false;
|
|
902
|
+
return result;
|
|
903
|
+
}
|
|
904
|
+
const targetOutputDir = effectiveMode === "full" ? await createStagingDirectory(options.outputDir) : options.outputDir;
|
|
905
|
+
try {
|
|
906
|
+
const writeStartTime = performance.now();
|
|
907
|
+
await writeStaticFiles(routeResults, targetOutputDir, { concurrency: resolvedConcurrency });
|
|
908
|
+
performance.now() - writeStartTime;
|
|
909
|
+
await writeMetadata(resultToMetadata(result), targetOutputDir);
|
|
910
|
+
await writeIncrementalManifest({
|
|
911
|
+
schemaVersion: 1,
|
|
912
|
+
seed,
|
|
913
|
+
mode: effectiveMode,
|
|
914
|
+
routes: nextManifestRoutes
|
|
915
|
+
}, targetOutputDir);
|
|
916
|
+
await copyStaticAssets(options.assets, targetOutputDir);
|
|
917
|
+
if (effectiveMode === "full") await replaceOutputDirectory(targetOutputDir, options.outputDir);
|
|
918
|
+
} catch (error) {
|
|
919
|
+
if (effectiveMode === "full") await fs.rm(targetOutputDir, {
|
|
920
|
+
recursive: true,
|
|
921
|
+
force: true
|
|
922
|
+
});
|
|
923
|
+
throw error;
|
|
924
|
+
}
|
|
925
|
+
return result;
|
|
926
|
+
});
|
|
886
927
|
},
|
|
887
928
|
/**
|
|
888
929
|
* Get effective config in a serializable form for diagnostics.
|