@intlayer/engine 9.5.10 → 9.5.12
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/assets/installSkills/skills/bundle-optimization.md +123 -0
- package/dist/assets/installSkills/skills/compat.md +0 -4
- package/dist/assets/installSkills/skills/content.md +8 -2
- package/dist/assets/installSkills/skills/next-js.md +2 -2
- package/dist/assets/installSkills/skills/usage.md +1 -1
- package/dist/cjs/build.cjs +5 -0
- package/dist/cjs/cli.cjs +1 -0
- package/dist/cjs/docReview/alignBlocks.cjs +53 -36
- package/dist/cjs/docReview/alignBlocks.cjs.map +1 -1
- package/dist/cjs/docReview/rebuildDocument.cjs +3 -1
- package/dist/cjs/docReview/rebuildDocument.cjs.map +1 -1
- package/dist/cjs/docReview/segmentDocument.cjs +8 -5
- package/dist/cjs/docReview/segmentDocument.cjs.map +1 -1
- package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs +4 -1
- package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs.map +1 -1
- package/dist/cjs/init/index.cjs +2 -12
- package/dist/cjs/init/index.cjs.map +1 -1
- package/dist/cjs/init/utils/backendPackages.cjs +15 -0
- package/dist/cjs/init/utils/backendPackages.cjs.map +1 -0
- package/dist/cjs/init/utils/index.cjs +2 -3
- package/dist/cjs/installMCP/installMCP.cjs +1 -1
- package/dist/cjs/installMCP/installMCP.cjs.map +1 -1
- package/dist/cjs/installSkills/index.cjs +2 -0
- package/dist/cjs/installSkills/index.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/loadDictionaries.cjs +9 -8
- package/dist/cjs/loadDictionaries/loadDictionaries.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs +3 -1
- package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs +4 -6
- package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/log.cjs +1 -1
- package/dist/cjs/loadDictionaries/log.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs +19 -12
- package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/remoteDictionaryClient.cjs +18 -0
- package/dist/cjs/loadDictionaries/remoteDictionaryClient.cjs.map +1 -0
- package/dist/cjs/prepareIntlayerServer.cjs +132 -0
- package/dist/cjs/prepareIntlayerServer.cjs.map +1 -0
- package/dist/cjs/scan/analyzeBundleContent.cjs +26 -12
- package/dist/cjs/scan/analyzeBundleContent.cjs.map +1 -1
- package/dist/cjs/scan/calculateScore.cjs +4 -1
- package/dist/cjs/scan/calculateScore.cjs.map +1 -1
- package/dist/cjs/scan/checks.cjs +394 -221
- package/dist/cjs/scan/checks.cjs.map +1 -1
- package/dist/cjs/scan/detection/checkDetails.cjs +56 -0
- package/dist/cjs/scan/detection/checkDetails.cjs.map +1 -0
- package/dist/cjs/scan/detection/classifyInternalLinks.cjs +47 -0
- package/dist/cjs/scan/detection/classifyInternalLinks.cjs.map +1 -0
- package/dist/cjs/scan/detection/detectRoutingStrategy.cjs +143 -0
- package/dist/cjs/scan/detection/detectRoutingStrategy.cjs.map +1 -0
- package/dist/cjs/scan/detection/detectTechnologies.cjs +89 -0
- package/dist/cjs/scan/detection/detectTechnologies.cjs.map +1 -0
- package/dist/cjs/scan/detection/index.cjs +36 -0
- package/dist/cjs/scan/detection/localeCode.cjs +118 -0
- package/dist/cjs/scan/detection/localeCode.cjs.map +1 -0
- package/dist/cjs/scan/detection/localizedPages.cjs +48 -0
- package/dist/cjs/scan/detection/localizedPages.cjs.map +1 -0
- package/dist/cjs/scan/detection/technologySignatures.cjs +564 -0
- package/dist/cjs/scan/detection/technologySignatures.cjs.map +1 -0
- package/dist/cjs/scan/detection/url.cjs +53 -0
- package/dist/cjs/scan/detection/url.cjs.map +1 -0
- package/dist/cjs/scan/fetchText.cjs +56 -0
- package/dist/cjs/scan/fetchText.cjs.map +1 -0
- package/dist/cjs/scan/index.cjs +59 -3
- package/dist/cjs/scan/parseHtml.cjs +55 -10
- package/dist/cjs/scan/parseHtml.cjs.map +1 -1
- package/dist/cjs/scan/robots.cjs +65 -0
- package/dist/cjs/scan/robots.cjs.map +1 -0
- package/dist/cjs/scan/runScanChecks.cjs +89 -0
- package/dist/cjs/scan/runScanChecks.cjs.map +1 -0
- package/dist/cjs/scan/scanWebsite.cjs +58 -52
- package/dist/cjs/scan/scanWebsite.cjs.map +1 -1
- package/dist/cjs/scan/sitemap.cjs +109 -0
- package/dist/cjs/scan/sitemap.cjs.map +1 -0
- package/dist/cjs/utils/chunkJSON.cjs +24 -9
- package/dist/cjs/utils/chunkJSON.cjs.map +1 -1
- package/dist/cjs/utils/getChunk.cjs +2 -2
- package/dist/cjs/utils/getChunk.cjs.map +1 -1
- package/dist/cjs/utils/getProcessChainCommands.cjs +117 -0
- package/dist/cjs/utils/getProcessChainCommands.cjs.map +1 -0
- package/dist/cjs/utils/index.cjs +2 -0
- package/dist/cjs/utils/runParallel/index.cjs +1 -1
- package/dist/cjs/utils/runParallel/index.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/pidTree.cjs +23 -11
- package/dist/cjs/utils/runParallel/pidTree.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/ps.cjs +8 -3
- package/dist/cjs/utils/runParallel/ps.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/runTask.cjs +2 -1
- package/dist/cjs/utils/runParallel/runTask.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/wmic.cjs +8 -3
- package/dist/cjs/utils/runParallel/wmic.cjs.map +1 -1
- package/dist/cjs/utils/startContentWatcher.cjs +99 -0
- package/dist/cjs/utils/startContentWatcher.cjs.map +1 -0
- package/dist/cjs/watcher.cjs +2 -2
- package/dist/cjs/watcher.cjs.map +1 -1
- package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs +7 -4
- package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs.map +1 -1
- package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs +2 -2
- package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs.map +1 -1
- package/dist/cjs/writeJsonIfChanged.cjs.map +1 -1
- package/dist/esm/build.mjs +2 -1
- package/dist/esm/cli.mjs +2 -2
- package/dist/esm/docReview/alignBlocks.mjs +53 -36
- package/dist/esm/docReview/alignBlocks.mjs.map +1 -1
- package/dist/esm/docReview/rebuildDocument.mjs +3 -1
- package/dist/esm/docReview/rebuildDocument.mjs.map +1 -1
- package/dist/esm/docReview/segmentDocument.mjs +8 -5
- package/dist/esm/docReview/segmentDocument.mjs.map +1 -1
- package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs +4 -1
- package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs.map +1 -1
- package/dist/esm/init/index.mjs +1 -11
- package/dist/esm/init/index.mjs.map +1 -1
- package/dist/esm/init/utils/backendPackages.mjs +14 -0
- package/dist/esm/init/utils/backendPackages.mjs.map +1 -0
- package/dist/esm/init/utils/index.mjs +2 -2
- package/dist/esm/installMCP/installMCP.mjs +1 -1
- package/dist/esm/installMCP/installMCP.mjs.map +1 -1
- package/dist/esm/installSkills/index.mjs +2 -0
- package/dist/esm/installSkills/index.mjs.map +1 -1
- package/dist/esm/loadDictionaries/loadDictionaries.mjs +9 -8
- package/dist/esm/loadDictionaries/loadDictionaries.mjs.map +1 -1
- package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs +3 -1
- package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs.map +1 -1
- package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs +3 -5
- package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs.map +1 -1
- package/dist/esm/loadDictionaries/log.mjs +1 -1
- package/dist/esm/loadDictionaries/log.mjs.map +1 -1
- package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs +19 -12
- package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs.map +1 -1
- package/dist/esm/loadDictionaries/remoteDictionaryClient.mjs +16 -0
- package/dist/esm/loadDictionaries/remoteDictionaryClient.mjs.map +1 -0
- package/dist/esm/prepareIntlayerServer.mjs +128 -0
- package/dist/esm/prepareIntlayerServer.mjs.map +1 -0
- package/dist/esm/scan/analyzeBundleContent.mjs +26 -12
- package/dist/esm/scan/analyzeBundleContent.mjs.map +1 -1
- package/dist/esm/scan/calculateScore.mjs +4 -1
- package/dist/esm/scan/calculateScore.mjs.map +1 -1
- package/dist/esm/scan/checks.mjs +387 -220
- package/dist/esm/scan/checks.mjs.map +1 -1
- package/dist/esm/scan/detection/checkDetails.mjs +54 -0
- package/dist/esm/scan/detection/checkDetails.mjs.map +1 -0
- package/dist/esm/scan/detection/classifyInternalLinks.mjs +46 -0
- package/dist/esm/scan/detection/classifyInternalLinks.mjs.map +1 -0
- package/dist/esm/scan/detection/detectRoutingStrategy.mjs +141 -0
- package/dist/esm/scan/detection/detectRoutingStrategy.mjs.map +1 -0
- package/dist/esm/scan/detection/detectTechnologies.mjs +87 -0
- package/dist/esm/scan/detection/detectTechnologies.mjs.map +1 -0
- package/dist/esm/scan/detection/index.mjs +10 -0
- package/dist/esm/scan/detection/localeCode.mjs +108 -0
- package/dist/esm/scan/detection/localeCode.mjs.map +1 -0
- package/dist/esm/scan/detection/localizedPages.mjs +46 -0
- package/dist/esm/scan/detection/localizedPages.mjs.map +1 -0
- package/dist/esm/scan/detection/technologySignatures.mjs +563 -0
- package/dist/esm/scan/detection/technologySignatures.mjs.map +1 -0
- package/dist/esm/scan/detection/url.mjs +47 -0
- package/dist/esm/scan/detection/url.mjs.map +1 -0
- package/dist/esm/scan/fetchText.mjs +55 -0
- package/dist/esm/scan/fetchText.mjs.map +1 -0
- package/dist/esm/scan/index.mjs +15 -3
- package/dist/esm/scan/parseHtml.mjs +52 -11
- package/dist/esm/scan/parseHtml.mjs.map +1 -1
- package/dist/esm/scan/robots.mjs +63 -0
- package/dist/esm/scan/robots.mjs.map +1 -0
- package/dist/esm/scan/runScanChecks.mjs +88 -0
- package/dist/esm/scan/runScanChecks.mjs.map +1 -0
- package/dist/esm/scan/scanWebsite.mjs +59 -53
- package/dist/esm/scan/scanWebsite.mjs.map +1 -1
- package/dist/esm/scan/sitemap.mjs +105 -0
- package/dist/esm/scan/sitemap.mjs.map +1 -0
- package/dist/esm/utils/chunkJSON.mjs +24 -9
- package/dist/esm/utils/chunkJSON.mjs.map +1 -1
- package/dist/esm/utils/getChunk.mjs +2 -2
- package/dist/esm/utils/getChunk.mjs.map +1 -1
- package/dist/esm/utils/getProcessChainCommands.mjs +115 -0
- package/dist/esm/utils/getProcessChainCommands.mjs.map +1 -0
- package/dist/esm/utils/index.mjs +2 -1
- package/dist/esm/utils/runParallel/index.mjs +1 -1
- package/dist/esm/utils/runParallel/index.mjs.map +1 -1
- package/dist/esm/utils/runParallel/pidTree.mjs +23 -11
- package/dist/esm/utils/runParallel/pidTree.mjs.map +1 -1
- package/dist/esm/utils/runParallel/ps.mjs +8 -3
- package/dist/esm/utils/runParallel/ps.mjs.map +1 -1
- package/dist/esm/utils/runParallel/runTask.mjs +2 -1
- package/dist/esm/utils/runParallel/runTask.mjs.map +1 -1
- package/dist/esm/utils/runParallel/wmic.mjs +8 -3
- package/dist/esm/utils/runParallel/wmic.mjs.map +1 -1
- package/dist/esm/utils/startContentWatcher.mjs +98 -0
- package/dist/esm/utils/startContentWatcher.mjs.map +1 -0
- package/dist/esm/watcher.mjs +1 -1
- package/dist/esm/watcher.mjs.map +1 -1
- package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs +7 -4
- package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs.map +1 -1
- package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs +2 -2
- package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs.map +1 -1
- package/dist/esm/writeJsonIfChanged.mjs +1 -1
- package/dist/esm/writeJsonIfChanged.mjs.map +1 -1
- package/dist/types/build.d.ts +2 -1
- package/dist/types/cli.d.ts +2 -2
- package/dist/types/docReview/rebuildDocument.d.ts.map +1 -1
- package/dist/types/docReview/segmentDocument.d.ts.map +1 -1
- package/dist/types/init/frameworkSetup/nextAppRouter/transforms.d.ts.map +1 -1
- package/dist/types/init/index.d.ts +1 -1
- package/dist/types/init/index.d.ts.map +1 -1
- package/dist/types/init/utils/backendPackages.d.ts +5 -0
- package/dist/types/init/utils/backendPackages.d.ts.map +1 -0
- package/dist/types/init/utils/index.d.ts +2 -2
- package/dist/types/installSkills/index.d.ts +1 -0
- package/dist/types/installSkills/index.d.ts.map +1 -1
- package/dist/types/loadDictionaries/loadRemoteDictionaries.d.ts.map +1 -1
- package/dist/types/loadDictionaries/logTypeScriptErrors.d.ts.map +1 -1
- package/dist/types/loadDictionaries/remoteDictionaryClient.d.ts +14 -0
- package/dist/types/loadDictionaries/remoteDictionaryClient.d.ts.map +1 -0
- package/dist/types/prepareIntlayerServer.d.ts +67 -0
- package/dist/types/prepareIntlayerServer.d.ts.map +1 -0
- package/dist/types/scan/analyzeBundleContent.d.ts.map +1 -1
- package/dist/types/scan/calculateScore.d.ts +4 -1
- package/dist/types/scan/calculateScore.d.ts.map +1 -1
- package/dist/types/scan/checks.d.ts +114 -21
- package/dist/types/scan/checks.d.ts.map +1 -1
- package/dist/types/scan/detection/checkDetails.d.ts +29 -0
- package/dist/types/scan/detection/checkDetails.d.ts.map +1 -0
- package/dist/types/scan/detection/classifyInternalLinks.d.ts +44 -0
- package/dist/types/scan/detection/classifyInternalLinks.d.ts.map +1 -0
- package/dist/types/scan/detection/detectRoutingStrategy.d.ts +60 -0
- package/dist/types/scan/detection/detectRoutingStrategy.d.ts.map +1 -0
- package/dist/types/scan/detection/detectTechnologies.d.ts +60 -0
- package/dist/types/scan/detection/detectTechnologies.d.ts.map +1 -0
- package/dist/types/scan/detection/index.d.ts +9 -0
- package/dist/types/scan/detection/localeCode.d.ts +49 -0
- package/dist/types/scan/detection/localeCode.d.ts.map +1 -0
- package/dist/types/scan/detection/localizedPages.d.ts +40 -0
- package/dist/types/scan/detection/localizedPages.d.ts.map +1 -0
- package/dist/types/scan/detection/technologySignatures.d.ts +48 -0
- package/dist/types/scan/detection/technologySignatures.d.ts.map +1 -0
- package/dist/types/scan/detection/url.d.ts +27 -0
- package/dist/types/scan/detection/url.d.ts.map +1 -0
- package/dist/types/scan/fetchText.d.ts +30 -0
- package/dist/types/scan/fetchText.d.ts.map +1 -0
- package/dist/types/scan/index.d.ts +16 -4
- package/dist/types/scan/parseHtml.d.ts +17 -0
- package/dist/types/scan/parseHtml.d.ts.map +1 -1
- package/dist/types/scan/robots.d.ts +23 -0
- package/dist/types/scan/robots.d.ts.map +1 -0
- package/dist/types/scan/runScanChecks.d.ts +43 -0
- package/dist/types/scan/runScanChecks.d.ts.map +1 -0
- package/dist/types/scan/scanWebsite.d.ts.map +1 -1
- package/dist/types/scan/sitemap.d.ts +49 -0
- package/dist/types/scan/sitemap.d.ts.map +1 -0
- package/dist/types/scan/types.d.ts +15 -0
- package/dist/types/scan/types.d.ts.map +1 -1
- package/dist/types/utils/chunkJSON.d.ts.map +1 -1
- package/dist/types/utils/getProcessChainCommands.d.ts +35 -0
- package/dist/types/utils/getProcessChainCommands.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +2 -1
- package/dist/types/utils/runParallel/pidTree.d.ts.map +1 -1
- package/dist/types/utils/startContentWatcher.d.ts +40 -0
- package/dist/types/utils/startContentWatcher.d.ts.map +1 -0
- package/dist/types/watcher.d.ts.map +1 -1
- package/dist/types/writeContentDeclaration/writeMarkdownFile.d.ts.map +1 -1
- package/package.json +15 -7
- package/dist/cjs/init/utils/devScript.cjs +0 -57
- package/dist/cjs/init/utils/devScript.cjs.map +0 -1
- package/dist/esm/init/utils/devScript.mjs +0 -55
- package/dist/esm/init/utils/devScript.mjs.map +0 -1
- package/dist/types/init/utils/devScript.d.ts +0 -48
- package/dist/types/init/utils/devScript.d.ts.map +0 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_scan_fetchText = require('./fetchText.cjs');
|
|
2
3
|
const require_scan_calculateScore = require('./calculateScore.cjs');
|
|
4
|
+
const require_scan_detection_detectTechnologies = require('./detection/detectTechnologies.cjs');
|
|
3
5
|
const require_scan_parseHtml = require('./parseHtml.cjs');
|
|
4
|
-
const
|
|
6
|
+
const require_scan_runScanChecks = require('./runScanChecks.cjs');
|
|
5
7
|
let _intlayer_config_colors = require("@intlayer/config/colors");
|
|
6
8
|
let _intlayer_config_logger = require("@intlayer/config/logger");
|
|
7
9
|
|
|
@@ -21,6 +23,22 @@ const logDeepScanRecommendation = () => {
|
|
|
21
23
|
]);
|
|
22
24
|
};
|
|
23
25
|
/**
|
|
26
|
+
* Read, in the page context, which technology globals exist and the version
|
|
27
|
+
* paths they expose. Serialized by puppeteer: must stay self-contained.
|
|
28
|
+
*/
|
|
29
|
+
const readRuntimeGlobals = (globalNames, versionPaths) => {
|
|
30
|
+
const pageWindow = window;
|
|
31
|
+
const globalVersions = {};
|
|
32
|
+
for (const versionPath of versionPaths) {
|
|
33
|
+
const value = versionPath.split(".").reduce((current, key) => current && typeof current === "object" ? current[key] : void 0, pageWindow);
|
|
34
|
+
if (typeof value === "string") globalVersions[versionPath] = value;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
globals: globalNames.filter((name) => pageWindow[name] !== void 0),
|
|
38
|
+
globalVersions
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
24
42
|
* Render the page with a locally installed `puppeteer` to capture
|
|
25
43
|
* client-rendered content, the accurate transfer size, and lazy-loaded chunks.
|
|
26
44
|
*
|
|
@@ -56,11 +74,13 @@ const runDeepScan = async (targetUrl, userAgent, timeoutMs) => {
|
|
|
56
74
|
await page.setExtraHTTPHeaders({ "Accept-Language": "en-US,en;q=0.9" });
|
|
57
75
|
const origin = new URL(targetUrl).origin;
|
|
58
76
|
const jsResponseMap = /* @__PURE__ */ new Map();
|
|
77
|
+
const requestUrls = [];
|
|
59
78
|
let totalPageSize = 0;
|
|
60
79
|
const pendingResponses = [];
|
|
61
80
|
page.on("response", (response) => {
|
|
62
81
|
pendingResponses.push((async () => {
|
|
63
82
|
try {
|
|
83
|
+
requestUrls.push(response.url());
|
|
64
84
|
if (response.status() !== 200) return;
|
|
65
85
|
const buffer = await response.buffer();
|
|
66
86
|
totalPageSize += buffer.length;
|
|
@@ -85,35 +105,27 @@ const runDeepScan = async (targetUrl, userAgent, timeoutMs) => {
|
|
|
85
105
|
isMainBundle: mainBundleUrls.has(url),
|
|
86
106
|
content
|
|
87
107
|
}));
|
|
108
|
+
const { globals: globalNames, versionGlobals } = require_scan_detection_detectTechnologies.getTechnologyGlobalNames();
|
|
109
|
+
const { globals, globalVersions } = await page.evaluate(readRuntimeGlobals, globalNames, versionGlobals).catch(() => ({
|
|
110
|
+
globals: [],
|
|
111
|
+
globalVersions: {}
|
|
112
|
+
}));
|
|
113
|
+
const cookies = await page.cookies().catch(() => []);
|
|
88
114
|
return {
|
|
89
115
|
html,
|
|
90
116
|
totalPageSize,
|
|
91
|
-
chunks
|
|
117
|
+
chunks,
|
|
118
|
+
requestUrls,
|
|
119
|
+
runtimeSignals: {
|
|
120
|
+
globals,
|
|
121
|
+
globalVersions,
|
|
122
|
+
storageKeys: cookies.map(({ name }) => name)
|
|
123
|
+
}
|
|
92
124
|
};
|
|
93
125
|
} finally {
|
|
94
126
|
if (browser) await browser.close();
|
|
95
127
|
}
|
|
96
128
|
};
|
|
97
|
-
/** Fetch the raw HTML document, measuring its byte size. */
|
|
98
|
-
const fetchHtml = async (url, userAgent, timeoutMs) => {
|
|
99
|
-
const controller = new AbortController();
|
|
100
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
101
|
-
try {
|
|
102
|
-
const response = await fetch(url, {
|
|
103
|
-
headers: {
|
|
104
|
-
"User-Agent": userAgent,
|
|
105
|
-
"Accept-Language": "en-US,en;q=0.9"
|
|
106
|
-
},
|
|
107
|
-
signal: controller.signal
|
|
108
|
-
});
|
|
109
|
-
return {
|
|
110
|
-
html: await response.text(),
|
|
111
|
-
finalUrl: response.url || url
|
|
112
|
-
};
|
|
113
|
-
} finally {
|
|
114
|
-
clearTimeout(timer);
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
129
|
/**
|
|
118
130
|
* Fetch every eagerly-loaded script. Same-origin scripts keep their content so
|
|
119
131
|
* their locale weight can be analyzed; third-party scripts only contribute to
|
|
@@ -154,49 +166,43 @@ const fetchScripts = async (scriptUrls, origin, userAgent) => {
|
|
|
154
166
|
const scanWebsite = async (targetUrl, options = {}) => {
|
|
155
167
|
const { deep = true, timeoutMs = DEFAULT_TIMEOUT_MS, userAgent = DEFAULT_USER_AGENT } = options;
|
|
156
168
|
const origin = new URL(targetUrl).origin;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
169
|
+
const fetchOptions = {
|
|
170
|
+
userAgent,
|
|
171
|
+
timeoutMs
|
|
172
|
+
};
|
|
161
173
|
const deepResult = deep ? await runDeepScan(targetUrl, userAgent, timeoutMs) : null;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
chunks = deepResult.chunks;
|
|
166
|
-
totalPageSize = deepResult.totalPageSize;
|
|
167
|
-
} else {
|
|
174
|
+
let pageInput;
|
|
175
|
+
if (deepResult) pageInput = deepResult;
|
|
176
|
+
else {
|
|
168
177
|
if (deep) logDeepScanRecommendation();
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
const scriptUrls = require_scan_parseHtml.extractScriptUrls(
|
|
172
|
-
const { chunks
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
const response = await require_scan_fetchText.fetchText(targetUrl, fetchOptions);
|
|
179
|
+
if (!response?.ok) throw new Error(`Failed to fetch ${targetUrl}${response ? ` (HTTP ${response.status})` : ""}`);
|
|
180
|
+
const scriptUrls = require_scan_parseHtml.extractScriptUrls(response.text, response.finalUrl);
|
|
181
|
+
const { chunks, scriptBytes } = await fetchScripts(scriptUrls, origin, userAgent);
|
|
182
|
+
pageInput = {
|
|
183
|
+
html: response.text,
|
|
184
|
+
chunks,
|
|
185
|
+
totalPageSize: require_scan_parseHtml.byteLength(response.text) + scriptBytes
|
|
186
|
+
};
|
|
175
187
|
}
|
|
176
|
-
const htmlSize = require_scan_parseHtml.byteLength(html);
|
|
177
188
|
const events = [];
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
require_scan_checks.checkUrlStructure(require_scan_parseHtml.extractAnchors(html), origin, targetUrl, events);
|
|
183
|
-
const bundle = require_scan_checks.checkBundleContent(chunks, html, langTag, targetUrl, totalPageSize, events);
|
|
184
|
-
await require_scan_checks.checkRobots(origin, localesSet, userAgent, events);
|
|
185
|
-
await require_scan_checks.checkSitemap(origin, localesSet, userAgent, events);
|
|
189
|
+
const checksResult = await require_scan_runScanChecks.runScanChecks({
|
|
190
|
+
targetUrl,
|
|
191
|
+
...pageInput
|
|
192
|
+
}, (event) => events.push(event), fetchOptions);
|
|
186
193
|
const rawScore = events.reduce((score, event) => require_scan_calculateScore.mutateScore(score, event), {
|
|
187
194
|
score: 0,
|
|
188
195
|
totalScore: 0
|
|
189
196
|
});
|
|
190
197
|
return {
|
|
191
198
|
url: targetUrl,
|
|
192
|
-
mode,
|
|
193
|
-
totalPageSize,
|
|
194
|
-
htmlSize,
|
|
199
|
+
mode: deepResult ? "deep" : "basic",
|
|
200
|
+
totalPageSize: pageInput.totalPageSize,
|
|
201
|
+
htmlSize: require_scan_parseHtml.byteLength(pageInput.html),
|
|
195
202
|
score: require_scan_calculateScore.toScorePercent(rawScore),
|
|
196
203
|
rawScore,
|
|
197
204
|
events,
|
|
198
|
-
|
|
199
|
-
bundle
|
|
205
|
+
...checksResult
|
|
200
206
|
};
|
|
201
207
|
};
|
|
202
208
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanWebsite.cjs","names":["colorize","GREY","GREY_LIGHT","extractScriptUrls","byteLength","checkHtmlAttributes","extractAnchors","checkBundleContent","checkRobots","checkSitemap","mutateScore","toScorePercent"],"sources":["../../../src/scan/scanWebsite.ts"],"sourcesContent":["import { GREY, GREY_LIGHT } from '@intlayer/config/colors';\nimport { colorize, logger } from '@intlayer/config/logger';\nimport { mutateScore, type Score, toScorePercent } from './calculateScore';\nimport {\n checkBundleContent,\n checkCanonical,\n checkHtmlAttributes,\n checkLinguisticStructure,\n checkRobots,\n checkSitemap,\n checkUrlStructure,\n} from './checks';\nimport { byteLength, extractAnchors, extractScriptUrls } from './parseHtml';\nimport type {\n BundleChunkInput,\n ScanEvent,\n ScanOptions,\n ScanResult,\n} from './types';\n\nconst DEFAULT_USER_AGENT =\n 'Mozilla/5.0 (compatible; IntlayerScanBot/1.0; +https://intlayer.org)';\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Log a recommendation to install `puppeteer` for a deeper scan. Mirrors the\n * style used by other optional-dependency hints across the CLI.\n */\nconst logDeepScanRecommendation = (): void => {\n logger([\n colorize('Recommended: Install', GREY),\n colorize('puppeteer', GREY_LIGHT),\n colorize(\n 'package to enable a deeper scan (renders client-side content & lazy-loaded chunks). See documentation:',\n GREY\n ),\n colorize('https://intlayer.org/doc/concept/cli#scan', GREY_LIGHT),\n ]);\n};\n\n/** Outcome of a render-based deep scan. */\ntype DeepScanResult = {\n html: string;\n totalPageSize: number;\n chunks: BundleChunkInput[];\n};\n\n/**\n * Render the page with a locally installed `puppeteer` to capture\n * client-rendered content, the accurate transfer size, and lazy-loaded chunks.\n *\n * `puppeteer` is imported dynamically through a non-literal specifier so it is\n * never bundled and stays an optional dependency: when it is absent the import\n * rejects and the caller falls back to the basic scan.\n *\n * @returns The deep-scan result, or `null` when `puppeteer` is unavailable.\n */\nconst runDeepScan = async (\n targetUrl: string,\n userAgent: string,\n timeoutMs: number\n): Promise<DeepScanResult | null> => {\n const moduleName = 'puppeteer';\n // Optional dependency resolved at runtime — typed as `any` because it is not\n // a declared dependency of this package.\n let puppeteer: any;\n try {\n const mod: any = await import(moduleName);\n puppeteer = mod.default ?? mod;\n } catch {\n return null;\n }\n\n let browser: any;\n try {\n browser = await puppeteer.launch({\n headless: true,\n executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,\n args: [\n '--no-sandbox',\n '--disable-setuid-sandbox',\n '--disable-dev-shm-usage',\n '--disable-gpu',\n ],\n });\n\n const page = await browser.newPage();\n await page.setUserAgent(userAgent);\n await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US,en;q=0.9' });\n\n const origin = new URL(targetUrl).origin;\n const jsResponseMap = new Map<string, string>();\n let totalPageSize = 0;\n const pendingResponses: Promise<void>[] = [];\n\n page.on('response', (response: any) => {\n pendingResponses.push(\n (async () => {\n try {\n if (response.status() !== 200) return;\n const buffer = await response.buffer();\n totalPageSize += buffer.length;\n\n const responseUrl: string = response.url();\n const contentType: string =\n response.headers()['content-type'] ?? '';\n const isJavaScript =\n contentType.includes('javascript') ||\n /\\.(js|mjs|cjs)(\\?|$)/.test(responseUrl);\n if (isJavaScript && responseUrl.startsWith(origin)) {\n jsResponseMap.set(responseUrl, buffer.toString('utf-8'));\n }\n } catch {\n /* response already consumed or aborted */\n }\n })()\n );\n });\n\n await page.goto(targetUrl, {\n waitUntil: 'domcontentloaded',\n timeout: timeoutMs,\n });\n await page\n .waitForNetworkIdle({ idleTime: 1000, timeout: 10_000 })\n .catch(() => {\n /* ok if it never fully idles */\n });\n await Promise.allSettled(pendingResponses);\n\n const html: string = await page.content();\n const mainBundleUrls = new Set(extractScriptUrls(html, targetUrl));\n const chunks: BundleChunkInput[] = Array.from(jsResponseMap.entries()).map(\n ([url, content]) => ({\n url,\n isMainBundle: mainBundleUrls.has(url),\n content,\n })\n );\n\n return { html, totalPageSize, chunks };\n } finally {\n if (browser) await browser.close();\n }\n};\n\n/** Fetch the raw HTML document, measuring its byte size. */\nconst fetchHtml = async (\n url: string,\n userAgent: string,\n timeoutMs: number\n): Promise<{ html: string; finalUrl: string }> => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n const response = await fetch(url, {\n headers: { 'User-Agent': userAgent, 'Accept-Language': 'en-US,en;q=0.9' },\n signal: controller.signal,\n });\n const html = await response.text();\n return { html, finalUrl: response.url || url };\n } finally {\n clearTimeout(timer);\n }\n};\n\n/**\n * Fetch every eagerly-loaded script. Same-origin scripts keep their content so\n * their locale weight can be analyzed; third-party scripts only contribute to\n * the measured page size (their locale-like keys cause false positives).\n */\nconst fetchScripts = async (\n scriptUrls: string[],\n origin: string,\n userAgent: string\n): Promise<{ chunks: BundleChunkInput[]; scriptBytes: number }> => {\n const chunks: BundleChunkInput[] = [];\n let scriptBytes = 0;\n\n await Promise.all(\n scriptUrls.map(async (scriptUrl) => {\n try {\n const response = await fetch(scriptUrl, {\n headers: { 'User-Agent': userAgent },\n });\n if (!response.ok) return;\n const content = await response.text();\n scriptBytes += byteLength(content);\n if (scriptUrl.startsWith(origin)) {\n chunks.push({ url: scriptUrl, isMainBundle: true, content });\n }\n } catch {\n /* ignore unreachable scripts */\n }\n })\n );\n\n return { chunks, scriptBytes };\n};\n\n/**\n * Scan a single web page for i18n/SEO health and bundle weight.\n *\n * In `deep` mode (default) the page is rendered with a locally installed\n * `puppeteer`; when `puppeteer` is missing the scan transparently falls back to\n * a `basic` fetch-based pass and logs a recommendation to install it.\n *\n * @param targetUrl - The absolute URL to scan.\n * @param options - {@link ScanOptions} controlling depth, timeout and UA.\n * @returns The {@link ScanResult} including score, page size and per-check events.\n */\nexport const scanWebsite = async (\n targetUrl: string,\n options: ScanOptions = {}\n): Promise<ScanResult> => {\n const {\n deep = true,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n userAgent = DEFAULT_USER_AGENT,\n } = options;\n\n const origin = new URL(targetUrl).origin;\n\n let mode: ScanResult['mode'] = 'basic';\n let html: string;\n let totalPageSize: number;\n let chunks: BundleChunkInput[];\n\n const deepResult = deep\n ? await runDeepScan(targetUrl, userAgent, timeoutMs)\n : null;\n\n if (deepResult) {\n mode = 'deep';\n html = deepResult.html;\n chunks = deepResult.chunks;\n totalPageSize = deepResult.totalPageSize;\n } else {\n if (deep) logDeepScanRecommendation();\n const { html: fetchedHtml, finalUrl } = await fetchHtml(\n targetUrl,\n userAgent,\n timeoutMs\n );\n html = fetchedHtml;\n const scriptUrls = extractScriptUrls(fetchedHtml, finalUrl);\n const { chunks: fetchedChunks, scriptBytes } = await fetchScripts(\n scriptUrls,\n origin,\n userAgent\n );\n chunks = fetchedChunks;\n totalPageSize = byteLength(fetchedHtml) + scriptBytes;\n }\n\n const htmlSize = byteLength(html);\n\n const events: ScanEvent[] = [];\n const localesSet = new Set<string>();\n\n const { langTag } = checkHtmlAttributes(html, targetUrl, events);\n checkCanonical(html, targetUrl, events);\n checkLinguisticStructure(html, targetUrl, localesSet, events);\n checkUrlStructure(extractAnchors(html), origin, targetUrl, events);\n const bundle = checkBundleContent(\n chunks,\n html,\n langTag,\n targetUrl,\n totalPageSize,\n events\n );\n await checkRobots(origin, localesSet, userAgent, events);\n await checkSitemap(origin, localesSet, userAgent, events);\n\n const rawScore = events.reduce<Score>(\n (score, event) => mutateScore(score, event),\n { score: 0, totalScore: 0 }\n );\n\n return {\n url: targetUrl,\n mode,\n totalPageSize,\n htmlSize,\n score: toScorePercent(rawScore),\n rawScore,\n events,\n locales: Array.from(localesSet),\n bundle,\n };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scanWebsite.cjs","names":["colorize","GREY","GREY_LIGHT","extractScriptUrls","getTechnologyGlobalNames","byteLength","fetchText","runScanChecks","mutateScore","toScorePercent"],"sources":["../../../src/scan/scanWebsite.ts"],"sourcesContent":["import { GREY, GREY_LIGHT } from '@intlayer/config/colors';\nimport { colorize, logger } from '@intlayer/config/logger';\nimport { mutateScore, type Score, toScorePercent } from './calculateScore';\nimport { getTechnologyGlobalNames } from './detection/detectTechnologies';\nimport { fetchText } from './fetchText';\nimport { byteLength, extractScriptUrls } from './parseHtml';\nimport { runScanChecks, type ScanPageInput } from './runScanChecks';\nimport type {\n BundleChunkInput,\n ScanEvent,\n ScanOptions,\n ScanResult,\n} from './types';\n\nconst DEFAULT_USER_AGENT =\n 'Mozilla/5.0 (compatible; IntlayerScanBot/1.0; +https://intlayer.org)';\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Log a recommendation to install `puppeteer` for a deeper scan. Mirrors the\n * style used by other optional-dependency hints across the CLI.\n */\nconst logDeepScanRecommendation = (): void => {\n logger([\n colorize('Recommended: Install', GREY),\n colorize('puppeteer', GREY_LIGHT),\n colorize(\n 'package to enable a deeper scan (renders client-side content & lazy-loaded chunks). See documentation:',\n GREY\n ),\n colorize('https://intlayer.org/doc/concept/cli#scan', GREY_LIGHT),\n ]);\n};\n\n/** Outcome of a render-based deep scan. */\ntype DeepScanResult = Required<Omit<ScanPageInput, 'targetUrl'>>;\n\n/**\n * Read, in the page context, which technology globals exist and the version\n * paths they expose. Serialized by puppeteer: must stay self-contained.\n */\nconst readRuntimeGlobals = (\n globalNames: string[],\n versionPaths: string[]\n): { globals: string[]; globalVersions: Record<string, string> } => {\n const pageWindow = window as unknown as Record<string, unknown>;\n const globalVersions: Record<string, string> = {};\n for (const versionPath of versionPaths) {\n const value = versionPath\n .split('.')\n .reduce<unknown>(\n (current, key) =>\n current && typeof current === 'object'\n ? (current as Record<string, unknown>)[key]\n : undefined,\n pageWindow\n );\n if (typeof value === 'string') globalVersions[versionPath] = value;\n }\n return {\n globals: globalNames.filter((name) => pageWindow[name] !== undefined),\n globalVersions,\n };\n};\n\n/**\n * Render the page with a locally installed `puppeteer` to capture\n * client-rendered content, the accurate transfer size, and lazy-loaded chunks.\n *\n * `puppeteer` is imported dynamically through a non-literal specifier so it is\n * never bundled and stays an optional dependency: when it is absent the import\n * rejects and the caller falls back to the basic scan.\n *\n * @returns The deep-scan result, or `null` when `puppeteer` is unavailable.\n */\nconst runDeepScan = async (\n targetUrl: string,\n userAgent: string,\n timeoutMs: number\n): Promise<DeepScanResult | null> => {\n const moduleName = 'puppeteer';\n // Optional dependency resolved at runtime — typed as `any` because it is not\n // a declared dependency of this package.\n let puppeteer: any;\n try {\n const mod: any = await import(moduleName);\n puppeteer = mod.default ?? mod;\n } catch {\n return null;\n }\n\n let browser: any;\n try {\n browser = await puppeteer.launch({\n headless: true,\n executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,\n args: [\n '--no-sandbox',\n '--disable-setuid-sandbox',\n '--disable-dev-shm-usage',\n '--disable-gpu',\n ],\n });\n\n const page = await browser.newPage();\n await page.setUserAgent(userAgent);\n await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US,en;q=0.9' });\n\n const origin = new URL(targetUrl).origin;\n const jsResponseMap = new Map<string, string>();\n const requestUrls: string[] = [];\n let totalPageSize = 0;\n const pendingResponses: Promise<void>[] = [];\n\n page.on('response', (response: any) => {\n pendingResponses.push(\n (async () => {\n try {\n requestUrls.push(response.url());\n if (response.status() !== 200) return;\n const buffer = await response.buffer();\n totalPageSize += buffer.length;\n\n const responseUrl: string = response.url();\n const contentType: string =\n response.headers()['content-type'] ?? '';\n const isJavaScript =\n contentType.includes('javascript') ||\n /\\.(js|mjs|cjs)(\\?|$)/.test(responseUrl);\n if (isJavaScript && responseUrl.startsWith(origin)) {\n jsResponseMap.set(responseUrl, buffer.toString('utf-8'));\n }\n } catch {\n /* response already consumed or aborted */\n }\n })()\n );\n });\n\n await page.goto(targetUrl, {\n waitUntil: 'domcontentloaded',\n timeout: timeoutMs,\n });\n await page\n .waitForNetworkIdle({ idleTime: 1000, timeout: 10_000 })\n .catch(() => {\n /* ok if it never fully idles */\n });\n await Promise.allSettled(pendingResponses);\n\n const html: string = await page.content();\n const mainBundleUrls = new Set(extractScriptUrls(html, targetUrl));\n const chunks: BundleChunkInput[] = Array.from(jsResponseMap.entries()).map(\n ([url, content]) => ({\n url,\n isMainBundle: mainBundleUrls.has(url),\n content,\n })\n );\n\n const { globals: globalNames, versionGlobals } = getTechnologyGlobalNames();\n const { globals, globalVersions } = await page\n .evaluate(readRuntimeGlobals, globalNames, versionGlobals)\n .catch(() => ({ globals: [], globalVersions: {} }));\n const cookies: { name: string }[] = await page.cookies().catch(() => []);\n\n return {\n html,\n totalPageSize,\n chunks,\n requestUrls,\n runtimeSignals: {\n globals,\n globalVersions,\n storageKeys: cookies.map(({ name }) => name),\n },\n };\n } finally {\n if (browser) await browser.close();\n }\n};\n\n/**\n * Fetch every eagerly-loaded script. Same-origin scripts keep their content so\n * their locale weight can be analyzed; third-party scripts only contribute to\n * the measured page size (their locale-like keys cause false positives).\n */\nconst fetchScripts = async (\n scriptUrls: string[],\n origin: string,\n userAgent: string\n): Promise<{ chunks: BundleChunkInput[]; scriptBytes: number }> => {\n const chunks: BundleChunkInput[] = [];\n let scriptBytes = 0;\n\n await Promise.all(\n scriptUrls.map(async (scriptUrl) => {\n try {\n const response = await fetch(scriptUrl, {\n headers: { 'User-Agent': userAgent },\n });\n if (!response.ok) return;\n const content = await response.text();\n scriptBytes += byteLength(content);\n if (scriptUrl.startsWith(origin)) {\n chunks.push({ url: scriptUrl, isMainBundle: true, content });\n }\n } catch {\n /* ignore unreachable scripts */\n }\n })\n );\n\n return { chunks, scriptBytes };\n};\n\n/**\n * Scan a single web page for i18n/SEO health and bundle weight.\n *\n * In `deep` mode (default) the page is rendered with a locally installed\n * `puppeteer`; when `puppeteer` is missing the scan transparently falls back to\n * a `basic` fetch-based pass and logs a recommendation to install it.\n *\n * @param targetUrl - The absolute URL to scan.\n * @param options - {@link ScanOptions} controlling depth, timeout and UA.\n * @returns The {@link ScanResult} including score, page size and per-check events.\n */\nexport const scanWebsite = async (\n targetUrl: string,\n options: ScanOptions = {}\n): Promise<ScanResult> => {\n const {\n deep = true,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n userAgent = DEFAULT_USER_AGENT,\n } = options;\n\n const origin = new URL(targetUrl).origin;\n const fetchOptions = { userAgent, timeoutMs };\n\n const deepResult = deep\n ? await runDeepScan(targetUrl, userAgent, timeoutMs)\n : null;\n\n let pageInput: Omit<ScanPageInput, 'targetUrl'>;\n\n if (deepResult) {\n pageInput = deepResult;\n } else {\n if (deep) logDeepScanRecommendation();\n const response = await fetchText(targetUrl, fetchOptions);\n if (!response?.ok) {\n throw new Error(\n `Failed to fetch ${targetUrl}${response ? ` (HTTP ${response.status})` : ''}`\n );\n }\n const scriptUrls = extractScriptUrls(response.text, response.finalUrl);\n const { chunks, scriptBytes } = await fetchScripts(\n scriptUrls,\n origin,\n userAgent\n );\n pageInput = {\n html: response.text,\n chunks,\n totalPageSize: byteLength(response.text) + scriptBytes,\n };\n }\n\n const events: ScanEvent[] = [];\n const checksResult = await runScanChecks(\n { targetUrl, ...pageInput },\n (event) => events.push(event),\n fetchOptions\n );\n\n const rawScore = events.reduce<Score>(\n (score, event) => mutateScore(score, event),\n { score: 0, totalScore: 0 }\n );\n\n return {\n url: targetUrl,\n mode: deepResult ? 'deep' : 'basic',\n totalPageSize: pageInput.totalPageSize,\n htmlSize: byteLength(pageInput.html),\n score: toScorePercent(rawScore),\n rawScore,\n events,\n ...checksResult,\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_scan_fetchText = require('./fetchText.cjs');
|
|
3
|
+
const require_scan_robots = require('./robots.cjs');
|
|
4
|
+
|
|
5
|
+
//#region src/scan/sitemap.ts
|
|
6
|
+
const MAX_CHILD_SITEMAPS = 5;
|
|
7
|
+
const MAX_ENTRIES = 5e3;
|
|
8
|
+
/** Decode the XML entities that can appear in sitemap URLs. */
|
|
9
|
+
const decodeXmlEntities = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'");
|
|
10
|
+
/** Read an attribute of an XML tag, accepting both quote styles. */
|
|
11
|
+
const readXmlAttribute = (tag, name) => {
|
|
12
|
+
const match = tag.match(new RegExp(`\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)')`, "i"));
|
|
13
|
+
return match ? decodeXmlEntities(match[2] ?? match[3] ?? "") : void 0;
|
|
14
|
+
};
|
|
15
|
+
/** Extract every `<loc>` value of an XML fragment. */
|
|
16
|
+
const extractLocs = (xml) => Array.from(xml.matchAll(/<loc>\s*([\s\S]*?)\s*<\/loc>/gi), (match) => decodeXmlEntities((match[1] ?? "").replace(/^<!\[CDATA\[|\]\]>$/g, "")));
|
|
17
|
+
/**
|
|
18
|
+
* Parse a sitemap or sitemap index (regex based, no XML dependency). Handles
|
|
19
|
+
* `<xhtml:link rel="alternate" hreflang>` alternates in any namespace prefix.
|
|
20
|
+
*/
|
|
21
|
+
const parseSitemap = (xml) => {
|
|
22
|
+
if (/<sitemapindex\b/i.test(xml)) return {
|
|
23
|
+
entries: [],
|
|
24
|
+
childSitemapUrls: extractLocs(xml)
|
|
25
|
+
};
|
|
26
|
+
const entries = [];
|
|
27
|
+
for (const [urlBlock] of xml.matchAll(/<url\b[\s\S]*?<\/url>/gi)) {
|
|
28
|
+
const loc = extractLocs(urlBlock)[0];
|
|
29
|
+
if (!loc) continue;
|
|
30
|
+
const alternates = [];
|
|
31
|
+
for (const [linkTag] of urlBlock.matchAll(/<(?:\w+:)?link\b[^>]*>/gi)) {
|
|
32
|
+
const hreflang = readXmlAttribute(linkTag, "hreflang");
|
|
33
|
+
const href = readXmlAttribute(linkTag, "href");
|
|
34
|
+
if (hreflang && href) alternates.push({
|
|
35
|
+
hreflang,
|
|
36
|
+
href
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
entries.push({
|
|
40
|
+
loc,
|
|
41
|
+
alternates
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
entries,
|
|
46
|
+
childSitemapUrls: []
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Fetch the given sitemaps, following sitemap indexes one level deep, and
|
|
51
|
+
* gather their `<url>` entries.
|
|
52
|
+
*
|
|
53
|
+
* @param sitemapUrls - Sitemaps to read (from `robots.txt`, else defaults).
|
|
54
|
+
* @param fetchOptions - Network options, including the SSRF guard.
|
|
55
|
+
*/
|
|
56
|
+
const collectSitemapEntries = async (sitemapUrls, fetchOptions) => {
|
|
57
|
+
const fetchedSitemapUrls = [];
|
|
58
|
+
const entries = [];
|
|
59
|
+
const visitedUrls = /* @__PURE__ */ new Set();
|
|
60
|
+
const queue = [...sitemapUrls];
|
|
61
|
+
let childSitemapCount = 0;
|
|
62
|
+
while (queue.length > 0 && entries.length < MAX_ENTRIES) {
|
|
63
|
+
const sitemapUrl = queue.shift();
|
|
64
|
+
if (visitedUrls.has(sitemapUrl)) continue;
|
|
65
|
+
visitedUrls.add(sitemapUrl);
|
|
66
|
+
const response = await require_scan_fetchText.fetchText(sitemapUrl, fetchOptions);
|
|
67
|
+
if (!response?.ok || !/<(urlset|sitemapindex)\b/i.test(response.text)) continue;
|
|
68
|
+
fetchedSitemapUrls.push(sitemapUrl);
|
|
69
|
+
const parsedSitemap = parseSitemap(response.text);
|
|
70
|
+
entries.push(...parsedSitemap.entries);
|
|
71
|
+
for (const childUrl of parsedSitemap.childSitemapUrls) {
|
|
72
|
+
if (childSitemapCount >= MAX_CHILD_SITEMAPS) break;
|
|
73
|
+
childSitemapCount++;
|
|
74
|
+
queue.push(childUrl);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
fetchedSitemapUrls,
|
|
79
|
+
entries: entries.slice(0, MAX_ENTRIES),
|
|
80
|
+
isTruncated: entries.length >= MAX_ENTRIES || queue.length > 0
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
/** Default sitemap locations tried when `robots.txt` declares none. */
|
|
84
|
+
const getDefaultSitemapUrls = (origin) => [`${origin}/sitemap.xml`, `${origin}/sitemap_index.xml`];
|
|
85
|
+
/**
|
|
86
|
+
* List the page URLs of a site from its sitemaps (primary `<loc>` and every
|
|
87
|
+
* hreflang alternate), used to seed a multi-page audit.
|
|
88
|
+
*
|
|
89
|
+
* @returns De-duplicated URLs, or `[targetUrl]` when no sitemap is found.
|
|
90
|
+
*/
|
|
91
|
+
const discoverSitemapUrls = async (targetUrl, fetchOptions) => {
|
|
92
|
+
const { origin } = new URL(targetUrl);
|
|
93
|
+
const robots = await require_scan_fetchText.fetchText(`${origin}/robots.txt`, fetchOptions);
|
|
94
|
+
const declaredSitemaps = robots?.ok ? require_scan_robots.parseRobots(robots.text).sitemapUrls : [];
|
|
95
|
+
const { entries } = await collectSitemapEntries(declaredSitemaps.length > 0 ? declaredSitemaps : getDefaultSitemapUrls(origin), fetchOptions);
|
|
96
|
+
const urls = /* @__PURE__ */ new Set();
|
|
97
|
+
for (const { loc, alternates } of entries) {
|
|
98
|
+
urls.add(loc);
|
|
99
|
+
for (const { href } of alternates) urls.add(href);
|
|
100
|
+
}
|
|
101
|
+
return urls.size > 0 ? Array.from(urls) : [targetUrl];
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
exports.collectSitemapEntries = collectSitemapEntries;
|
|
106
|
+
exports.discoverSitemapUrls = discoverSitemapUrls;
|
|
107
|
+
exports.getDefaultSitemapUrls = getDefaultSitemapUrls;
|
|
108
|
+
exports.parseSitemap = parseSitemap;
|
|
109
|
+
//# sourceMappingURL=sitemap.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.cjs","names":["fetchText","parseRobots"],"sources":["../../../src/scan/sitemap.ts"],"sourcesContent":["import { fetchText, type ScanFetchOptions } from './fetchText';\nimport { parseRobots } from './robots';\n\n/** A `<url>` entry of a sitemap with its hreflang alternates. */\nexport type SitemapEntry = {\n loc: string;\n alternates: { hreflang: string; href: string }[];\n};\n\n/** Result of {@link parseSitemap}. */\nexport type ParsedSitemap = {\n /** `<url>` entries (empty for a sitemap index). */\n entries: SitemapEntry[];\n /** Child sitemap URLs, when the document is a `<sitemapindex>`. */\n childSitemapUrls: string[];\n};\n\n/** Result of {@link collectSitemapEntries}. */\nexport type CollectedSitemaps = {\n /** Sitemap URLs that answered with a valid document. */\n fetchedSitemapUrls: string[];\n entries: SitemapEntry[];\n /** Whether the entry limit was reached before reading every sitemap. */\n isTruncated: boolean;\n};\n\nconst MAX_CHILD_SITEMAPS = 5;\nconst MAX_ENTRIES = 5_000;\n\n/** Decode the XML entities that can appear in sitemap URLs. */\nconst decodeXmlEntities = (text: string): string =>\n text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\");\n\n/** Read an attribute of an XML tag, accepting both quote styles. */\nconst readXmlAttribute = (tag: string, name: string): string | undefined => {\n const match = tag.match(\n new RegExp(`\\\\b${name}\\\\s*=\\\\s*(\"([^\"]*)\"|'([^']*)')`, 'i')\n );\n return match ? decodeXmlEntities(match[2] ?? match[3] ?? '') : undefined;\n};\n\n/** Extract every `<loc>` value of an XML fragment. */\nconst extractLocs = (xml: string): string[] =>\n Array.from(xml.matchAll(/<loc>\\s*([\\s\\S]*?)\\s*<\\/loc>/gi), (match) =>\n decodeXmlEntities((match[1] ?? '').replace(/^<!\\[CDATA\\[|\\]\\]>$/g, ''))\n );\n\n/**\n * Parse a sitemap or sitemap index (regex based, no XML dependency). Handles\n * `<xhtml:link rel=\"alternate\" hreflang>` alternates in any namespace prefix.\n */\nexport const parseSitemap = (xml: string): ParsedSitemap => {\n if (/<sitemapindex\\b/i.test(xml)) {\n return { entries: [], childSitemapUrls: extractLocs(xml) };\n }\n\n const entries: SitemapEntry[] = [];\n for (const [urlBlock] of xml.matchAll(/<url\\b[\\s\\S]*?<\\/url>/gi)) {\n const loc = extractLocs(urlBlock)[0];\n if (!loc) continue;\n\n const alternates: SitemapEntry['alternates'] = [];\n for (const [linkTag] of urlBlock.matchAll(/<(?:\\w+:)?link\\b[^>]*>/gi)) {\n const hreflang = readXmlAttribute(linkTag, 'hreflang');\n const href = readXmlAttribute(linkTag, 'href');\n if (hreflang && href) alternates.push({ hreflang, href });\n }\n entries.push({ loc, alternates });\n }\n\n return { entries, childSitemapUrls: [] };\n};\n\n/**\n * Fetch the given sitemaps, following sitemap indexes one level deep, and\n * gather their `<url>` entries.\n *\n * @param sitemapUrls - Sitemaps to read (from `robots.txt`, else defaults).\n * @param fetchOptions - Network options, including the SSRF guard.\n */\nexport const collectSitemapEntries = async (\n sitemapUrls: string[],\n fetchOptions: ScanFetchOptions\n): Promise<CollectedSitemaps> => {\n const fetchedSitemapUrls: string[] = [];\n const entries: SitemapEntry[] = [];\n const visitedUrls = new Set<string>();\n const queue = [...sitemapUrls];\n let childSitemapCount = 0;\n\n while (queue.length > 0 && entries.length < MAX_ENTRIES) {\n const sitemapUrl = queue.shift() as string;\n if (visitedUrls.has(sitemapUrl)) continue;\n visitedUrls.add(sitemapUrl);\n\n const response = await fetchText(sitemapUrl, fetchOptions);\n if (!response?.ok || !/<(urlset|sitemapindex)\\b/i.test(response.text)) {\n continue;\n }\n\n fetchedSitemapUrls.push(sitemapUrl);\n const parsedSitemap = parseSitemap(response.text);\n entries.push(...parsedSitemap.entries);\n\n for (const childUrl of parsedSitemap.childSitemapUrls) {\n if (childSitemapCount >= MAX_CHILD_SITEMAPS) break;\n childSitemapCount++;\n queue.push(childUrl);\n }\n }\n\n return {\n fetchedSitemapUrls,\n entries: entries.slice(0, MAX_ENTRIES),\n isTruncated: entries.length >= MAX_ENTRIES || queue.length > 0,\n };\n};\n\n/** Default sitemap locations tried when `robots.txt` declares none. */\nexport const getDefaultSitemapUrls = (origin: string): string[] => [\n `${origin}/sitemap.xml`,\n `${origin}/sitemap_index.xml`,\n];\n\n/**\n * List the page URLs of a site from its sitemaps (primary `<loc>` and every\n * hreflang alternate), used to seed a multi-page audit.\n *\n * @returns De-duplicated URLs, or `[targetUrl]` when no sitemap is found.\n */\nexport const discoverSitemapUrls = async (\n targetUrl: string,\n fetchOptions: ScanFetchOptions\n): Promise<string[]> => {\n const { origin } = new URL(targetUrl);\n const robots = await fetchText(`${origin}/robots.txt`, fetchOptions);\n const declaredSitemaps = robots?.ok\n ? parseRobots(robots.text).sitemapUrls\n : [];\n\n const { entries } = await collectSitemapEntries(\n declaredSitemaps.length > 0\n ? declaredSitemaps\n : getDefaultSitemapUrls(origin),\n fetchOptions\n );\n\n const urls = new Set<string>();\n for (const { loc, alternates } of entries) {\n urls.add(loc);\n for (const { href } of alternates) urls.add(href);\n }\n\n return urls.size > 0 ? Array.from(urls) : [targetUrl];\n};\n"],"mappings":"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"}
|
|
@@ -21,7 +21,8 @@ const setAtPath = (root, path, value) => {
|
|
|
21
21
|
let current = root;
|
|
22
22
|
for (let i = 0; i < path.length - 1; i++) {
|
|
23
23
|
const key = path[i];
|
|
24
|
-
|
|
24
|
+
if (key === void 0) continue;
|
|
25
|
+
const isNextIndex = typeof nextKey === "number";
|
|
25
26
|
if (typeof key === "number") {
|
|
26
27
|
if (!Array.isArray(current)) throw new Error(`Expected array at path segment ${i}`);
|
|
27
28
|
if (current[key] === void 0) current[key] = isNextIndex ? [] : {};
|
|
@@ -33,6 +34,7 @@ const setAtPath = (root, path, value) => {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
const last = path[path.length - 1];
|
|
37
|
+
if (last === void 0) return;
|
|
36
38
|
if (typeof last === "number") {
|
|
37
39
|
if (!Array.isArray(current)) throw new Error(`Expected array at final segment`);
|
|
38
40
|
current[last] = value;
|
|
@@ -91,11 +93,14 @@ const flattenToPatches = (value, maxCharsPerChunk, path = [], seen = /* @__PURE_
|
|
|
91
93
|
path: [...currentPath, "__total"],
|
|
92
94
|
value: parts.length
|
|
93
95
|
});
|
|
94
|
-
for (let i = 0; i < parts.length; i++)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
for (let i = 0; i < parts.length; i++) {
|
|
97
|
+
const part = parts[i];
|
|
98
|
+
if (part !== void 0) patches.push({
|
|
99
|
+
op: "set",
|
|
100
|
+
path: [...currentPath, String(i + 1)],
|
|
101
|
+
value: part
|
|
102
|
+
});
|
|
103
|
+
}
|
|
99
104
|
return;
|
|
100
105
|
}
|
|
101
106
|
if (currentValue === null || typeof currentValue !== "object") {
|
|
@@ -108,8 +113,14 @@ const flattenToPatches = (value, maxCharsPerChunk, path = [], seen = /* @__PURE_
|
|
|
108
113
|
}
|
|
109
114
|
if (seen.has(currentValue)) throw new Error("Cannot serialize circular structures to JSON.");
|
|
110
115
|
seen.add(currentValue);
|
|
111
|
-
if (Array.isArray(currentValue)) for (let i = 0; i < currentValue.length; i++)
|
|
112
|
-
|
|
116
|
+
if (Array.isArray(currentValue)) for (let i = 0; i < currentValue.length; i++) {
|
|
117
|
+
const item = currentValue[i];
|
|
118
|
+
if (item !== void 0) walk(item, [...currentPath, i]);
|
|
119
|
+
}
|
|
120
|
+
else for (const key of Object.keys(currentValue)) {
|
|
121
|
+
const childVal = currentValue[key];
|
|
122
|
+
if (childVal !== void 0) walk(childVal, [...currentPath, key]);
|
|
123
|
+
}
|
|
113
124
|
seen.delete(currentValue);
|
|
114
125
|
};
|
|
115
126
|
walk(value, path);
|
|
@@ -237,10 +248,13 @@ const reconstructFromSingleChunk = (chunk) => {
|
|
|
237
248
|
const assembleJSON = (chunks) => {
|
|
238
249
|
if (!chunks || chunks.length === 0) throw new Error("No chunks provided.");
|
|
239
250
|
const sorted = [...chunks].sort((a, b) => a.index - b.index);
|
|
240
|
-
const
|
|
251
|
+
const firstChunk = sorted[0];
|
|
252
|
+
if (!firstChunk) throw new Error("No chunks provided.");
|
|
253
|
+
const { checksum, rootType } = firstChunk;
|
|
241
254
|
const schemaVersion = 1;
|
|
242
255
|
for (let i = 0; i < sorted.length; i++) {
|
|
243
256
|
const chunk = sorted[i];
|
|
257
|
+
if (!chunk) continue;
|
|
244
258
|
if (chunk.schemaVersion !== schemaVersion) {
|
|
245
259
|
console.error("Unsupported schemaVersion.", {
|
|
246
260
|
cause: chunk,
|
|
@@ -326,6 +340,7 @@ const assembleJSON = (chunks) => {
|
|
|
326
340
|
const reconciled = reconcileSplitNodes(root);
|
|
327
341
|
for (let i = 0; i < sorted.length; i++) {
|
|
328
342
|
const chunk = sorted[i];
|
|
343
|
+
if (!chunk) continue;
|
|
329
344
|
if (chunk.total !== sorted.length) throw new Error(`Chunk total does not match provided count. Expected ${sorted.length}, but chunk ${i} has total=${chunk.total}`);
|
|
330
345
|
}
|
|
331
346
|
return reconciled;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunkJSON.cjs","names":["getChunk"],"sources":["../../../src/utils/chunkJSON.ts"],"sourcesContent":["/**\n * Split & reassemble JSON by character budget.\n * - Measures serialized size using JSON.stringify(..).length (characters).\n * - Ensures each chunk is itself valid JSON.\n * - Very large strings are split into safe pieces using getChunk and re-concatenated on assemble.\n * - Protects against circular structures (JSON can't serialize those anyway).\n */\n\nimport { getChunk } from './getChunk';\n\ntype JSONPrimitive = string | number | boolean | null;\ntype JSONValue = JSONPrimitive | JSONObject | JSONArray;\n\nexport type JSONObject = {\n [k: string]: JSONValue;\n};\n\ntype JSONArray = JSONValue[];\n\ntype Path = Array<string | number>;\n\ntype SetPatch = { op: 'set'; path: Path; value: JSONValue };\ntype StrAppendPatch = {\n op: 'str-append';\n path: Path;\n value: string; // part of a longer string\n index: number;\n total: number;\n};\ntype Patch = SetPatch | StrAppendPatch;\n\ntype RootType = 'object' | 'array';\n\nexport type JsonChunk = {\n schemaVersion: 1;\n index: number;\n total: number;\n rootType: RootType;\n checksum: string; // hash of the full original JSON string\n entries: Patch[];\n};\n\nconst isObject = (val: unknown): val is Record<string, unknown> => {\n return typeof val === 'object' && val !== null && !Array.isArray(val);\n};\n\nconst computeDjb2 = (str: string): string => {\n // Simple 32-bit hash; deterministic & fast\n let hash = 5381;\n\n for (let i = 0; i < str.length; i++) {\n hash = ((hash << 5) + hash) ^ str.charCodeAt(i);\n }\n\n // convert to unsigned hex\n return (hash >>> 0).toString(16).padStart(8, '0');\n};\n\nconst setAtPath = (root: any, path: Path, value: JSONValue) => {\n let current = root;\n\n for (let i = 0; i < path.length - 1; i++) {\n const key = path[i];\n const nextKey = path[i + 1];\n const isNextIndex = typeof nextKey === 'number';\n\n if (typeof key === 'number') {\n if (!Array.isArray(current)) {\n throw new Error(`Expected array at path segment ${i}`);\n }\n\n if (current[key] === undefined) {\n current[key] = isNextIndex ? [] : {};\n }\n\n current = current[key];\n } else {\n if (!isObject(current)) {\n throw new Error(`Expected object at path segment ${i}`);\n }\n\n if (!(key in current)) {\n (current as any)[key] = isNextIndex ? [] : {};\n }\n\n current = (current as any)[key];\n }\n }\n\n const last = path[path.length - 1];\n\n if (typeof last === 'number') {\n if (!Array.isArray(current)) {\n throw new Error(`Expected array at final segment`);\n }\n\n current[last] = value as any;\n } else {\n if (!isObject(current)) {\n throw new Error(`Expected object at final segment`);\n }\n\n (current as any)[last] = value as any;\n }\n};\n\nconst pathKey = (path: Path): string => {\n // stable key for grouping string parts\n return JSON.stringify(path);\n};\n\n/**\n * Split a string into parts using getChunk with a charLength budget per part.\n */\nconst splitStringByBudget = (\n str: string,\n maxCharsPerPart: number\n): string[] => {\n if (maxCharsPerPart <= 0) {\n throw new Error('maxChars must be > 0');\n }\n\n const output: string[] = [];\n let offset = 0;\n\n while (offset < str.length) {\n const part = getChunk(str, {\n charStart: offset,\n charLength: maxCharsPerPart,\n });\n\n if (!part) break;\n\n output.push(part);\n offset += part.length;\n }\n\n return output;\n};\n\n/**\n * Flatten JSON into patches (leaf writes). Strings too large to fit in a single\n * chunk are yielded as multiple str-append patches.\n */\nconst flattenToPatches = (\n value: JSONValue,\n maxCharsPerChunk: number,\n path: Path = [],\n seen = new WeakSet<object>()\n): Patch[] => {\n // Conservative per-entry cap so a single entry fits into an empty chunk with envelope overhead.\n // (Envelope ~ a few hundred chars; we keep a comfortable margin.)\n const maxStringPiece = Math.max(\n 1,\n Math.floor((maxCharsPerChunk - 400) * 0.8)\n );\n\n const patches: Patch[] = [];\n\n const walk = (currentValue: JSONValue, currentPath: Path) => {\n if (typeof currentValue === 'string') {\n // If the serialized patch wouldn't fit, split the string into multiple parts\n // and encode as a split-node sentinel with numbered keys.\n const testPatch: SetPatch = {\n op: 'set',\n path: currentPath,\n value: currentValue,\n };\n const testLen = JSON.stringify(testPatch).length + 150; // margin\n\n if (testLen <= maxCharsPerChunk) {\n patches.push(testPatch);\n return;\n }\n\n // Use getChunk-based splitting to produce stable parts\n const parts = splitStringByBudget(currentValue, maxStringPiece);\n\n // Emit split-node metadata and parts as individual leaf writes\n patches.push({\n op: 'set',\n path: [...currentPath, '__splittedType'],\n value: 'string',\n });\n patches.push({\n op: 'set',\n path: [...currentPath, '__total'],\n value: parts.length,\n });\n\n for (let i = 0; i < parts.length; i++) {\n patches.push({\n op: 'set',\n path: [...currentPath, String(i + 1)],\n value: parts[i],\n });\n }\n\n return;\n }\n\n if (currentValue === null || typeof currentValue !== 'object') {\n patches.push({ op: 'set', path: currentPath, value: currentValue });\n return;\n }\n\n if (seen.has(currentValue as object)) {\n throw new Error('Cannot serialize circular structures to JSON.');\n }\n\n seen.add(currentValue as object);\n\n if (Array.isArray(currentValue)) {\n for (let i = 0; i < currentValue.length; i++) {\n walk(currentValue[i] as JSONValue, [...currentPath, i]);\n }\n } else {\n for (const key of Object.keys(currentValue)) {\n walk((currentValue as JSONObject)[key], [...currentPath, key]);\n }\n }\n\n seen.delete(currentValue as object);\n };\n\n walk(value, path);\n return patches;\n};\n\n/**\n * Split JSON into chunks constrained by character count of serialized chunk.\n */\nexport const chunkJSON = (\n value: JSONObject | JSONArray,\n maxChars: number\n): JsonChunk[] => {\n if (!isObject(value) && !Array.isArray(value)) {\n throw new Error('Root must be an object or array.');\n }\n\n if (maxChars < 500) {\n // You can lower this if you truly need; recommended to keep some envelope headroom.\n throw new Error('maxChars is too small. Use at least 500 characters.');\n }\n\n const rootType: RootType = Array.isArray(value) ? 'array' : 'object';\n let sourceString: string;\n\n try {\n sourceString = JSON.stringify(value);\n } catch {\n // Provide a deterministic error message for circular refs\n throw new Error('Cannot serialize circular structures to JSON.');\n }\n\n const checksum = computeDjb2(sourceString);\n const allPatches = flattenToPatches(value as JSONValue, maxChars);\n\n const chunks: JsonChunk[] = [];\n let currentChunk: JsonChunk = {\n schemaVersion: 1,\n index: 0, // provisional\n total: 0, // provisional\n rootType,\n checksum,\n entries: [],\n };\n\n const emptyEnvelopeSize = JSON.stringify({\n ...currentChunk,\n entries: [],\n }).length;\n\n const tryFlush = () => {\n if (currentChunk.entries.length > 0) {\n chunks.push(currentChunk);\n currentChunk = {\n schemaVersion: 1,\n index: 0,\n total: 0,\n rootType,\n checksum,\n entries: [],\n };\n }\n };\n\n for (const patch of allPatches) {\n // Would adding this patch exceed maxChars?\n const withPatchSize =\n emptyEnvelopeSize +\n JSON.stringify(currentChunk.entries).length + // current entries array\n (currentChunk.entries.length ? 1 : 0) + // possible comma\n JSON.stringify(patch).length;\n\n if (withPatchSize <= maxChars) {\n currentChunk.entries.push(patch);\n } else {\n // Start a new chunk if current has items\n if (currentChunk.entries.length > 0) {\n tryFlush();\n }\n\n // Ensure single patch fits into an empty chunk\n const singleSize = emptyEnvelopeSize + JSON.stringify([patch]).length;\n\n if (singleSize > maxChars) {\n // This should only happen for massive strings, which we pre-split;\n // if it happens for a non-string, we cannot split further.\n throw new Error(\n 'A single entry exceeds maxChars and cannot be split. Reduce entry size or increase maxChars.'\n );\n }\n\n currentChunk.entries.push(patch);\n }\n }\n\n tryFlush();\n\n // Ensure at least one chunk exists (even for empty root)\n if (chunks.length === 0) {\n chunks.push({\n schemaVersion: 1,\n index: 0,\n total: 0, // provisional\n rootType,\n checksum,\n entries: [],\n });\n }\n\n // Finalize indices & totals\n const totalChunks = chunks.length;\n\n chunks.forEach((chunk, index) => {\n chunk.index = index;\n chunk.total = totalChunks;\n });\n\n return chunks;\n};\n\n/**\n * Reassemble JSON from chunks.\n * - Validates checksums and indices.\n * - Applies 'set' patches and merges string pieces from 'str-append'.\n */\n/**\n * Reconstruct content from a single chunk without validation.\n * Useful for processing individual chunks in a pipeline where you don't have all chunks yet.\n * Note: This will only reconstruct the partial content contained in this chunk.\n */\nexport const reconstructFromSingleChunk = (\n chunk: JsonChunk\n): JSONObject | JSONArray => {\n const root: any = chunk.rootType === 'array' ? [] : {};\n\n // Apply all 'set' patches from this chunk\n for (const entry of chunk.entries) {\n if (entry.op === 'set') {\n setAtPath(root, entry.path, entry.value);\n }\n }\n\n // Reconcile split-node sentinels for strings/arrays\n // When reconstructing from a single chunk, we may have incomplete split nodes\n const reconcileSplitNodes = (node: any): any => {\n if (node === null || typeof node !== 'object') return node;\n\n if (Array.isArray(node)) {\n for (let i = 0; i < node.length; i++) {\n node[i] = reconcileSplitNodes(node[i]);\n }\n return node;\n }\n\n // string split-node\n if ((node as any)['__splittedType'] === 'string') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total <= 0) {\n // Invalid split node, return as-is\n return node;\n }\n\n const parts: string[] = [];\n let hasAllParts = true;\n\n for (let i = 1; i <= total; i++) {\n const piece = (node as any)[String(i)];\n\n if (typeof piece !== 'string') {\n hasAllParts = false;\n break;\n }\n\n parts.push(piece);\n }\n\n // Only reconstruct if we have all parts, otherwise return the node as-is\n if (hasAllParts) {\n return parts.join('');\n }\n\n return node;\n }\n\n // array split-node (optional support)\n if ((node as any)['__splittedType'] === 'array') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total < 0) {\n // Invalid split node, return as-is\n return node;\n }\n\n const output: any[] = [];\n let hasAllParts = true;\n\n for (let i = 1; i <= total; i++) {\n const slice = (node as any)[String(i)];\n\n if (!Array.isArray(slice)) {\n hasAllParts = false;\n break;\n }\n\n for (let j = 0; j < slice.length; j++) {\n output.push(reconcileSplitNodes(slice[j]));\n }\n }\n\n // Only reconstruct if we have all parts\n if (hasAllParts) {\n return output;\n }\n\n return node;\n }\n\n // walk normal object\n for (const key of Object.keys(node)) {\n node[key] = reconcileSplitNodes(node[key]);\n }\n\n return node;\n };\n\n return reconcileSplitNodes(root);\n};\n\nexport const assembleJSON = (chunks: JsonChunk[]): JSONObject | JSONArray => {\n if (!chunks || chunks.length === 0) {\n throw new Error('No chunks provided.');\n }\n\n // Basic validation & sort\n const sorted = [...chunks].sort((a, b) => a.index - b.index);\n const { checksum, rootType } = sorted[0];\n const schemaVersion = 1;\n\n for (let i = 0; i < sorted.length; i++) {\n const chunk = sorted[i];\n\n if (chunk.schemaVersion !== schemaVersion) {\n console.error('Unsupported schemaVersion.', {\n cause: chunk,\n schemaVersion,\n });\n throw new Error('Unsupported schemaVersion.');\n }\n\n if (chunk.rootType !== rootType) {\n console.error('Chunks rootType mismatch.', {\n cause: chunk,\n rootType,\n });\n throw new Error('Chunks rootType mismatch.');\n }\n\n if (chunk.checksum !== checksum) {\n console.error('Chunks checksum mismatch (different source objects?).', {\n cause: chunk,\n checksum,\n });\n throw new Error('Chunks checksum mismatch (different source objects?).');\n }\n\n if (chunk.index !== i) {\n console.error('Chunk indices are not contiguous or sorted.', {\n cause: chunk,\n index: chunk.index,\n i,\n });\n throw new Error('Chunk indices are not contiguous or sorted.');\n }\n\n // Defer total check until after reconstruction to prefer more specific errors\n }\n\n const root: any = rootType === 'array' ? [] : {};\n\n // Collect string parts by path\n const stringParts = new Map<\n string,\n { path: Path; total: number; received: StrAppendPatch[] }\n >();\n\n const applySet = (patch: SetPatch) =>\n setAtPath(root, patch.path, patch.value);\n\n for (const chunk of sorted) {\n for (const entry of chunk.entries) {\n if (entry.op === 'set') {\n applySet(entry);\n } else {\n const key = pathKey(entry.path);\n const record = stringParts.get(key) ?? {\n path: entry.path,\n total: entry.total,\n received: [],\n };\n\n if (record.total !== entry.total) {\n throw new Error('Inconsistent string part totals for a path.');\n }\n\n record.received.push(entry);\n stringParts.set(key, record);\n }\n }\n }\n\n // Stitch strings\n for (const { path, total, received } of stringParts.values()) {\n if (received.length !== total) {\n throw new Error('Missing string parts for a path; incomplete chunk set.');\n }\n\n received.sort((a, b) => a.index - b.index);\n const fullString = received.map((part) => part.value).join('');\n setAtPath(root, path, fullString);\n }\n\n // Reconcile split-node sentinels for strings/arrays after all patches applied\n const reconcileSplitNodes = (node: any): any => {\n if (node === null || typeof node !== 'object') return node;\n\n if (Array.isArray(node)) {\n for (let i = 0; i < node.length; i++) {\n node[i] = reconcileSplitNodes(node[i]);\n }\n return node;\n }\n\n // string split-node\n if ((node as any)['__splittedType'] === 'string') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total <= 0) {\n throw new Error('Invalid split-node total for a path.');\n }\n\n const parts: string[] = [];\n\n for (let i = 1; i <= total; i++) {\n const piece = (node as any)[String(i)];\n\n if (typeof piece !== 'string') {\n throw new Error(\n 'Missing string parts for a path; incomplete chunk set.'\n );\n }\n\n parts.push(piece);\n }\n\n return parts.join('');\n }\n\n // array split-node (optional support)\n if ((node as any)['__splittedType'] === 'array') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total < 0) {\n throw new Error('Invalid split-node total for a path.');\n }\n\n const output: any[] = [];\n\n for (let i = 1; i <= total; i++) {\n const slice = (node as any)[String(i)];\n\n if (!Array.isArray(slice)) {\n throw new Error(\n 'Missing string parts for a path; incomplete chunk set.'\n );\n }\n\n for (let j = 0; j < slice.length; j++) {\n output.push(reconcileSplitNodes(slice[j]));\n }\n }\n\n return output;\n }\n\n // walk normal object\n for (const key of Object.keys(node)) {\n node[key] = reconcileSplitNodes(node[key]);\n }\n\n return node;\n };\n\n const reconciled = reconcileSplitNodes(root);\n\n // Now validate totals match provided count\n for (let i = 0; i < sorted.length; i++) {\n const chunk = sorted[i];\n\n if (chunk.total !== sorted.length) {\n throw new Error(\n `Chunk total does not match provided count. Expected ${sorted.length}, but chunk ${i} has total=${chunk.total}`\n );\n }\n }\n\n return reconciled;\n};\n\n/* -------------------------------------------\n * Example usage\n * -------------------------------------------\nconst big: JSONObject = {\n title: \"Document\",\n content: \"…a very very long text…\",\n items: Array.from({ length: 2000 }, (_, i) => ({ id: i, label: `Item ${i}` }))\n};\n\n// Split to ~16k-char chunks\nconst chunks = chunkJSON(big, 16_000);\n\n// Send each `chunks[i]` as JSON to your backend.\n// Later, reassemble:\nconst restored = assembleJSON(chunks);\nconsole.log(JSON.stringify(restored) === JSON.stringify(big)); // true\n*/\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chunkJSON.cjs","names":["getChunk"],"sources":["../../../src/utils/chunkJSON.ts"],"sourcesContent":["/**\n * Split & reassemble JSON by character budget.\n * - Measures serialized size using JSON.stringify(..).length (characters).\n * - Ensures each chunk is itself valid JSON.\n * - Very large strings are split into safe pieces using getChunk and re-concatenated on assemble.\n * - Protects against circular structures (JSON can't serialize those anyway).\n */\n\nimport { getChunk } from './getChunk';\n\ntype JSONPrimitive = string | number | boolean | null;\ntype JSONValue = JSONPrimitive | JSONObject | JSONArray;\n\nexport type JSONObject = {\n [k: string]: JSONValue;\n};\n\ntype JSONArray = JSONValue[];\n\ntype Path = Array<string | number>;\n\ntype SetPatch = { op: 'set'; path: Path; value: JSONValue };\ntype StrAppendPatch = {\n op: 'str-append';\n path: Path;\n value: string; // part of a longer string\n index: number;\n total: number;\n};\ntype Patch = SetPatch | StrAppendPatch;\n\ntype RootType = 'object' | 'array';\n\nexport type JsonChunk = {\n schemaVersion: 1;\n index: number;\n total: number;\n rootType: RootType;\n checksum: string; // hash of the full original JSON string\n entries: Patch[];\n};\n\nconst isObject = (val: unknown): val is Record<string, unknown> => {\n return typeof val === 'object' && val !== null && !Array.isArray(val);\n};\n\nconst computeDjb2 = (str: string): string => {\n // Simple 32-bit hash; deterministic & fast\n let hash = 5381;\n\n for (let i = 0; i < str.length; i++) {\n hash = ((hash << 5) + hash) ^ str.charCodeAt(i);\n }\n\n // convert to unsigned hex\n return (hash >>> 0).toString(16).padStart(8, '0');\n};\n\nconst setAtPath = (root: any, path: Path, value: JSONValue) => {\n let current = root;\n\n for (let i = 0; i < path.length - 1; i++) {\n const key = path[i];\n if (key === undefined) continue;\n\n const isNextIndex = typeof nextKey === 'number';\n\n if (typeof key === 'number') {\n if (!Array.isArray(current)) {\n throw new Error(`Expected array at path segment ${i}`);\n }\n\n if (current[key] === undefined) {\n current[key] = isNextIndex ? [] : {};\n }\n\n current = current[key];\n } else {\n if (!isObject(current)) {\n throw new Error(`Expected object at path segment ${i}`);\n }\n\n if (!(key in current)) {\n (current as Record<string, any>)[key] = isNextIndex ? [] : {};\n }\n\n current = (current as Record<string, any>)[key];\n }\n }\n\n const last = path[path.length - 1];\n\n if (last === undefined) return;\n\n if (typeof last === 'number') {\n if (!Array.isArray(current)) {\n throw new Error(`Expected array at final segment`);\n }\n\n current[last] = value as any;\n } else {\n if (!isObject(current)) {\n throw new Error(`Expected object at final segment`);\n }\n\n (current as Record<string, any>)[last] = value as any;\n }\n};\n\nconst pathKey = (path: Path): string => {\n // stable key for grouping string parts\n return JSON.stringify(path);\n};\n\n/**\n * Split a string into parts using getChunk with a charLength budget per part.\n */\nconst splitStringByBudget = (\n str: string,\n maxCharsPerPart: number\n): string[] => {\n if (maxCharsPerPart <= 0) {\n throw new Error('maxChars must be > 0');\n }\n\n const output: string[] = [];\n let offset = 0;\n\n while (offset < str.length) {\n const part = getChunk(str, {\n charStart: offset,\n charLength: maxCharsPerPart,\n });\n\n if (!part) break;\n\n output.push(part);\n offset += part.length;\n }\n\n return output;\n};\n\n/**\n * Flatten JSON into patches (leaf writes). Strings too large to fit in a single\n * chunk are yielded as multiple str-append patches.\n */\nconst flattenToPatches = (\n value: JSONValue,\n maxCharsPerChunk: number,\n path: Path = [],\n seen = new WeakSet<object>()\n): Patch[] => {\n // Conservative per-entry cap so a single entry fits into an empty chunk with envelope overhead.\n // (Envelope ~ a few hundred chars; we keep a comfortable margin.)\n const maxStringPiece = Math.max(\n 1,\n Math.floor((maxCharsPerChunk - 400) * 0.8)\n );\n\n const patches: Patch[] = [];\n\n const walk = (currentValue: JSONValue, currentPath: Path) => {\n if (typeof currentValue === 'string') {\n // If the serialized patch wouldn't fit, split the string into multiple parts\n // and encode as a split-node sentinel with numbered keys.\n const testPatch: SetPatch = {\n op: 'set',\n path: currentPath,\n value: currentValue,\n };\n const testLen = JSON.stringify(testPatch).length + 150; // margin\n\n if (testLen <= maxCharsPerChunk) {\n patches.push(testPatch);\n return;\n }\n\n // Use getChunk-based splitting to produce stable parts\n const parts = splitStringByBudget(currentValue, maxStringPiece);\n\n // Emit split-node metadata and parts as individual leaf writes\n patches.push({\n op: 'set',\n path: [...currentPath, '__splittedType'],\n value: 'string',\n });\n patches.push({\n op: 'set',\n path: [...currentPath, '__total'],\n value: parts.length,\n });\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n\n if (part !== undefined) {\n patches.push({\n op: 'set',\n path: [...currentPath, String(i + 1)],\n value: part,\n });\n }\n }\n\n return;\n }\n\n if (currentValue === null || typeof currentValue !== 'object') {\n patches.push({ op: 'set', path: currentPath, value: currentValue });\n return;\n }\n\n if (seen.has(currentValue as object)) {\n throw new Error('Cannot serialize circular structures to JSON.');\n }\n\n seen.add(currentValue as object);\n\n if (Array.isArray(currentValue)) {\n for (let i = 0; i < currentValue.length; i++) {\n const item = currentValue[i];\n if (item !== undefined) {\n walk(item as JSONValue, [...currentPath, i]);\n }\n }\n } else {\n for (const key of Object.keys(currentValue)) {\n const childVal = (currentValue as JSONObject)[key];\n\n if (childVal !== undefined) {\n walk(childVal, [...currentPath, key]);\n }\n }\n }\n\n seen.delete(currentValue as object);\n };\n\n walk(value, path);\n return patches;\n};\n\n/**\n * Split JSON into chunks constrained by character count of serialized chunk.\n */\nexport const chunkJSON = (\n value: JSONObject | JSONArray,\n maxChars: number\n): JsonChunk[] => {\n if (!isObject(value) && !Array.isArray(value)) {\n throw new Error('Root must be an object or array.');\n }\n\n if (maxChars < 500) {\n // You can lower this if you truly need; recommended to keep some envelope headroom.\n throw new Error('maxChars is too small. Use at least 500 characters.');\n }\n\n const rootType: RootType = Array.isArray(value) ? 'array' : 'object';\n let sourceString: string;\n\n try {\n sourceString = JSON.stringify(value);\n } catch {\n // Provide a deterministic error message for circular refs\n throw new Error('Cannot serialize circular structures to JSON.');\n }\n\n const checksum = computeDjb2(sourceString);\n const allPatches = flattenToPatches(value as JSONValue, maxChars);\n\n const chunks: JsonChunk[] = [];\n let currentChunk: JsonChunk = {\n schemaVersion: 1,\n index: 0, // provisional\n total: 0, // provisional\n rootType,\n checksum,\n entries: [],\n };\n\n const emptyEnvelopeSize = JSON.stringify({\n ...currentChunk,\n entries: [],\n }).length;\n\n const tryFlush = () => {\n if (currentChunk.entries.length > 0) {\n chunks.push(currentChunk);\n currentChunk = {\n schemaVersion: 1,\n index: 0,\n total: 0,\n rootType,\n checksum,\n entries: [],\n };\n }\n };\n\n for (const patch of allPatches) {\n // Would adding this patch exceed maxChars?\n const withPatchSize =\n emptyEnvelopeSize +\n JSON.stringify(currentChunk.entries).length + // current entries array\n (currentChunk.entries.length ? 1 : 0) + // possible comma\n JSON.stringify(patch).length;\n\n if (withPatchSize <= maxChars) {\n currentChunk.entries.push(patch);\n } else {\n // Start a new chunk if current has items\n if (currentChunk.entries.length > 0) {\n tryFlush();\n }\n\n // Ensure single patch fits into an empty chunk\n const singleSize = emptyEnvelopeSize + JSON.stringify([patch]).length;\n\n if (singleSize > maxChars) {\n // This should only happen for massive strings, which we pre-split;\n // if it happens for a non-string, we cannot split further.\n throw new Error(\n 'A single entry exceeds maxChars and cannot be split. Reduce entry size or increase maxChars.'\n );\n }\n\n currentChunk.entries.push(patch);\n }\n }\n\n tryFlush();\n\n // Ensure at least one chunk exists (even for empty root)\n if (chunks.length === 0) {\n chunks.push({\n schemaVersion: 1,\n index: 0,\n total: 0, // provisional\n rootType,\n checksum,\n entries: [],\n });\n }\n\n // Finalize indices & totals\n const totalChunks = chunks.length;\n\n chunks.forEach((chunk, index) => {\n chunk.index = index;\n chunk.total = totalChunks;\n });\n\n return chunks;\n};\n\n/**\n * Reassemble JSON from chunks.\n * - Validates checksums and indices.\n * - Applies 'set' patches and merges string pieces from 'str-append'.\n */\n/**\n * Reconstruct content from a single chunk without validation.\n * Useful for processing individual chunks in a pipeline where you don't have all chunks yet.\n * Note: This will only reconstruct the partial content contained in this chunk.\n */\nexport const reconstructFromSingleChunk = (\n chunk: JsonChunk\n): JSONObject | JSONArray => {\n const root: any = chunk.rootType === 'array' ? [] : {};\n\n // Apply all 'set' patches from this chunk\n for (const entry of chunk.entries) {\n if (entry.op === 'set') {\n setAtPath(root, entry.path, entry.value);\n }\n }\n\n // Reconcile split-node sentinels for strings/arrays\n // When reconstructing from a single chunk, we may have incomplete split nodes\n const reconcileSplitNodes = (node: any): any => {\n if (node === null || typeof node !== 'object') return node;\n\n if (Array.isArray(node)) {\n for (let i = 0; i < node.length; i++) {\n node[i] = reconcileSplitNodes(node[i]);\n }\n return node;\n }\n\n // string split-node\n if ((node as any)['__splittedType'] === 'string') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total <= 0) {\n // Invalid split node, return as-is\n return node;\n }\n\n const parts: string[] = [];\n let hasAllParts = true;\n\n for (let i = 1; i <= total; i++) {\n const piece = (node as any)[String(i)];\n\n if (typeof piece !== 'string') {\n hasAllParts = false;\n break;\n }\n\n parts.push(piece);\n }\n\n // Only reconstruct if we have all parts, otherwise return the node as-is\n if (hasAllParts) {\n return parts.join('');\n }\n\n return node;\n }\n\n // array split-node (optional support)\n if ((node as any)['__splittedType'] === 'array') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total < 0) {\n // Invalid split node, return as-is\n return node;\n }\n\n const output: any[] = [];\n let hasAllParts = true;\n\n for (let i = 1; i <= total; i++) {\n const slice = (node as any)[String(i)];\n\n if (!Array.isArray(slice)) {\n hasAllParts = false;\n break;\n }\n\n for (let j = 0; j < slice.length; j++) {\n output.push(reconcileSplitNodes(slice[j]));\n }\n }\n\n // Only reconstruct if we have all parts\n if (hasAllParts) {\n return output;\n }\n\n return node;\n }\n\n // walk normal object\n for (const key of Object.keys(node)) {\n node[key] = reconcileSplitNodes(node[key]);\n }\n\n return node;\n };\n\n return reconcileSplitNodes(root);\n};\n\nexport const assembleJSON = (chunks: JsonChunk[]): JSONObject | JSONArray => {\n if (!chunks || chunks.length === 0) {\n throw new Error('No chunks provided.');\n }\n\n // Basic validation & sort\n const sorted = [...chunks].sort((a, b) => a.index - b.index);\n const firstChunk = sorted[0];\n\n if (!firstChunk) {\n throw new Error('No chunks provided.');\n }\n\n const { checksum, rootType } = firstChunk;\n const schemaVersion = 1;\n\n for (let i = 0; i < sorted.length; i++) {\n const chunk = sorted[i];\n\n if (!chunk) continue;\n\n if (chunk.schemaVersion !== schemaVersion) {\n console.error('Unsupported schemaVersion.', {\n cause: chunk,\n schemaVersion,\n });\n throw new Error('Unsupported schemaVersion.');\n }\n\n if (chunk.rootType !== rootType) {\n console.error('Chunks rootType mismatch.', {\n cause: chunk,\n rootType,\n });\n throw new Error('Chunks rootType mismatch.');\n }\n\n if (chunk.checksum !== checksum) {\n console.error('Chunks checksum mismatch (different source objects?).', {\n cause: chunk,\n checksum,\n });\n throw new Error('Chunks checksum mismatch (different source objects?).');\n }\n\n if (chunk.index !== i) {\n console.error('Chunk indices are not contiguous or sorted.', {\n cause: chunk,\n index: chunk.index,\n i,\n });\n throw new Error('Chunk indices are not contiguous or sorted.');\n }\n\n // Defer total check until after reconstruction to prefer more specific errors\n }\n\n const root: any = rootType === 'array' ? [] : {};\n\n // Collect string parts by path\n const stringParts = new Map<\n string,\n { path: Path; total: number; received: StrAppendPatch[] }\n >();\n\n const applySet = (patch: SetPatch) =>\n setAtPath(root, patch.path, patch.value);\n\n for (const chunk of sorted) {\n for (const entry of chunk.entries) {\n if (entry.op === 'set') {\n applySet(entry);\n } else {\n const key = pathKey(entry.path);\n const record = stringParts.get(key) ?? {\n path: entry.path,\n total: entry.total,\n received: [],\n };\n\n if (record.total !== entry.total) {\n throw new Error('Inconsistent string part totals for a path.');\n }\n\n record.received.push(entry);\n stringParts.set(key, record);\n }\n }\n }\n\n // Stitch strings\n for (const { path, total, received } of stringParts.values()) {\n if (received.length !== total) {\n throw new Error('Missing string parts for a path; incomplete chunk set.');\n }\n\n received.sort((a, b) => a.index - b.index);\n const fullString = received.map((part) => part.value).join('');\n setAtPath(root, path, fullString);\n }\n\n // Reconcile split-node sentinels for strings/arrays after all patches applied\n const reconcileSplitNodes = (node: any): any => {\n if (node === null || typeof node !== 'object') return node;\n\n if (Array.isArray(node)) {\n for (let i = 0; i < node.length; i++) {\n node[i] = reconcileSplitNodes(node[i]);\n }\n return node;\n }\n\n // string split-node\n if ((node as any)['__splittedType'] === 'string') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total <= 0) {\n throw new Error('Invalid split-node total for a path.');\n }\n\n const parts: string[] = [];\n\n for (let i = 1; i <= total; i++) {\n const piece = (node as any)[String(i)];\n\n if (typeof piece !== 'string') {\n throw new Error(\n 'Missing string parts for a path; incomplete chunk set.'\n );\n }\n\n parts.push(piece);\n }\n\n return parts.join('');\n }\n\n // array split-node (optional support)\n if ((node as any)['__splittedType'] === 'array') {\n const total = (node as any)['__total'];\n\n if (typeof total !== 'number' || total < 0) {\n throw new Error('Invalid split-node total for a path.');\n }\n\n const output: any[] = [];\n\n for (let i = 1; i <= total; i++) {\n const slice = (node as any)[String(i)];\n\n if (!Array.isArray(slice)) {\n throw new Error(\n 'Missing string parts for a path; incomplete chunk set.'\n );\n }\n\n for (let j = 0; j < slice.length; j++) {\n output.push(reconcileSplitNodes(slice[j]));\n }\n }\n\n return output;\n }\n\n // walk normal object\n for (const key of Object.keys(node)) {\n node[key] = reconcileSplitNodes(node[key]);\n }\n\n return node;\n };\n\n const reconciled = reconcileSplitNodes(root);\n\n // Now validate totals match provided count\n for (let i = 0; i < sorted.length; i++) {\n const chunk = sorted[i];\n\n if (!chunk) continue;\n\n if (chunk.total !== sorted.length) {\n throw new Error(\n `Chunk total does not match provided count. Expected ${sorted.length}, but chunk ${i} has total=${chunk.total}`\n );\n }\n }\n\n return reconciled;\n};\n\n/* -------------------------------------------\n * Example usage\n * -------------------------------------------\nconst big: JSONObject = {\n title: \"Document\",\n content: \"…a very very long text…\",\n items: Array.from({ length: 2000 }, (_, i) => ({ id: i, label: `Item ${i}` }))\n};\n\n// Split to ~16k-char chunks\nconst chunks = chunkJSON(big, 16_000);\n\n// Send each `chunks[i]` as JSON to your backend.\n// Later, reassemble:\nconst restored = assembleJSON(chunks);\nconsole.log(JSON.stringify(restored) === JSON.stringify(big)); // true\n*/\n"],"mappings":"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"}
|
|
@@ -9,13 +9,13 @@ const getChunk = (text, options = {}) => {
|
|
|
9
9
|
const getCharIndexOfLineStart = (lineNumber) => {
|
|
10
10
|
if (lineNumber <= 0) return 0;
|
|
11
11
|
let idx = 0;
|
|
12
|
-
for (let i = 0; i < Math.min(lineNumber, lines.length); i++) idx += lines[i]
|
|
12
|
+
for (let i = 0; i < Math.min(lineNumber, lines.length); i++) idx += lines[i]?.length ?? 0;
|
|
13
13
|
return idx;
|
|
14
14
|
};
|
|
15
15
|
const getCharIndexOfLineEnd = (lineNumber) => {
|
|
16
16
|
if (lineNumber >= lines.length) return text.length;
|
|
17
17
|
const line = lines[lineNumber];
|
|
18
|
-
return getCharIndexOfLineStart(lineNumber) + line
|
|
18
|
+
return getCharIndexOfLineStart(lineNumber) + (line?.length ?? 0);
|
|
19
19
|
};
|
|
20
20
|
let effectiveStart = 0;
|
|
21
21
|
let effectiveEnd = text.length;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getChunk.cjs","names":["splitTextByLines"],"sources":["../../../src/utils/getChunk.ts"],"sourcesContent":["import { splitTextByLines } from './splitTextByLine';\n\ntype TrunkOptions = {\n lineStart?: number;\n lineLength?: number;\n charStart?: number;\n charLength?: number;\n};\n\nexport const getChunk = (text: string, options: TrunkOptions = {}): string => {\n const { lineStart, lineLength, charStart, charLength } = options;\n\n // Fast-path: if no filters were provided, return the whole text\n if (\n lineStart === undefined &&\n lineLength === undefined &&\n charStart === undefined &&\n charLength === undefined\n ) {\n return text;\n }\n\n // ---------------------------------------------------------------------------\n // Utility helpers to convert between line numbers and absolute char indices\n // ---------------------------------------------------------------------------\n const lines = splitTextByLines(text);\n\n const getCharIndexOfLineStart = (lineNumber: number): number => {\n if (lineNumber <= 0) return 0;\n // Sum the length of every previous line\n let idx = 0;\n for (let i = 0; i < Math.min(lineNumber, lines.length); i++) {\n idx += lines[i]
|
|
1
|
+
{"version":3,"file":"getChunk.cjs","names":["splitTextByLines"],"sources":["../../../src/utils/getChunk.ts"],"sourcesContent":["import { splitTextByLines } from './splitTextByLine';\n\ntype TrunkOptions = {\n lineStart?: number;\n lineLength?: number;\n charStart?: number;\n charLength?: number;\n};\n\nexport const getChunk = (text: string, options: TrunkOptions = {}): string => {\n const { lineStart, lineLength, charStart, charLength } = options;\n\n // Fast-path: if no filters were provided, return the whole text\n if (\n lineStart === undefined &&\n lineLength === undefined &&\n charStart === undefined &&\n charLength === undefined\n ) {\n return text;\n }\n\n // ---------------------------------------------------------------------------\n // Utility helpers to convert between line numbers and absolute char indices\n // ---------------------------------------------------------------------------\n const lines = splitTextByLines(text);\n\n const getCharIndexOfLineStart = (lineNumber: number): number => {\n if (lineNumber <= 0) return 0;\n // Sum the length of every previous line\n let idx = 0;\n for (let i = 0; i < Math.min(lineNumber, lines.length); i++) {\n idx += lines[i]?.length ?? 0;\n }\n return idx;\n };\n\n const getCharIndexOfLineEnd = (lineNumber: number): number => {\n // If the requested line number exceeds the number of lines, clamp to the last character\n if (lineNumber >= lines.length) {\n return text.length;\n }\n const line = lines[lineNumber];\n const lineEnd = getCharIndexOfLineStart(lineNumber) + (line?.length ?? 0);\n\n return lineEnd;\n };\n\n // ---------------------------------------------------------------------------\n // Compute the effective (inclusive) charStart/charEnd for the requested slice\n // ---------------------------------------------------------------------------\n let effectiveStart = 0; // inclusive\n let effectiveEnd = text.length; // exclusive\n\n // Apply line boundaries if provided\n if (lineStart !== undefined) {\n effectiveStart = Math.max(\n effectiveStart,\n getCharIndexOfLineStart(lineStart)\n );\n }\n\n // Apply character boundaries if provided\n if (charStart !== undefined) {\n effectiveStart = Math.max(effectiveStart, charStart);\n }\n\n // Apply line length boundary from lineStart (or 0)\n if (lineLength !== undefined) {\n const endLine = (lineStart ?? 0) + lineLength - 1;\n effectiveEnd = Math.min(effectiveEnd, getCharIndexOfLineEnd(endLine));\n }\n\n // Apply character length boundary from effectiveStart\n if (charLength !== undefined) {\n effectiveEnd = Math.min(\n effectiveEnd,\n (charStart ?? effectiveStart) + charLength\n );\n }\n\n // If bounds do not overlap, return empty string\n if (effectiveStart >= effectiveEnd) {\n return '';\n }\n\n return text.slice(effectiveStart, effectiveEnd);\n};\n"],"mappings":";;;;AASA,MAAa,YAAY,MAAc,UAAwB,CAAC,MAAc;CAC5E,MAAM,EAAE,WAAW,YAAY,WAAW,eAAe;CAGzD,IACE,cAAc,UACd,eAAe,UACf,cAAc,UACd,eAAe,QAEf,OAAO;CAMT,MAAM,QAAQA,+CAAiB,IAAI;CAEnC,MAAM,2BAA2B,eAA+B;EAC9D,IAAI,cAAc,GAAG,OAAO;EAE5B,IAAI,MAAM;EACV,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,IAAI,YAAY,MAAM,MAAM,GAAG,KACtD,OAAO,MAAM,EAAE,EAAE,UAAU;EAE7B,OAAO;CACT;CAEA,MAAM,yBAAyB,eAA+B;EAE5D,IAAI,cAAc,MAAM,QACtB,OAAO,KAAK;EAEd,MAAM,OAAO,MAAM;EAGnB,OAFgB,wBAAwB,UAAU,KAAK,MAAM,UAAU;CAGzE;CAKA,IAAI,iBAAiB;CACrB,IAAI,eAAe,KAAK;CAGxB,IAAI,cAAc,QAChB,iBAAiB,KAAK,IACpB,gBACA,wBAAwB,SAAS,CACnC;CAIF,IAAI,cAAc,QAChB,iBAAiB,KAAK,IAAI,gBAAgB,SAAS;CAIrD,IAAI,eAAe,QAAW;EAC5B,MAAM,WAAW,aAAa,KAAK,aAAa;EAChD,eAAe,KAAK,IAAI,cAAc,sBAAsB,OAAO,CAAC;CACtE;CAGA,IAAI,eAAe,QACjB,eAAe,KAAK,IAClB,eACC,aAAa,kBAAkB,UAClC;CAIF,IAAI,kBAAkB,cACpB,OAAO;CAGT,OAAO,KAAK,MAAM,gBAAgB,YAAY;AAChD"}
|