@js-recon/js-recon 1.3.2 → 1.4.1-alpha.10
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/.github/workflows/build-and-prettify.yaml +207 -14
- package/.github/workflows/pr_checker.yml +2 -2
- package/.github/workflows/promote-js-recon.yml +104 -0
- package/.github/workflows/publish-js-recon.yml +22 -80
- package/.github/workflows/rules-smoke-test.yaml +79 -0
- package/CHANGELOG.md +177 -4
- package/CLAUDE.md +350 -16
- package/Dockerfile.release +29 -0
- package/README.md +48 -19
- package/build/analyze/engine/csMastSEngine.js +129 -0
- package/build/analyze/engine/csMastSEngine.js.map +1 -0
- package/build/analyze/engine/index.js +15 -0
- package/build/analyze/engine/index.js.map +1 -1
- package/build/analyze/helpers/engineHelpers/findDirectAssignment.js +2 -1
- package/build/analyze/helpers/engineHelpers/findDirectAssignment.js.map +1 -1
- package/build/analyze/helpers/engineHelpers/findMemberExpressionAssignment.js +2 -1
- package/build/analyze/helpers/engineHelpers/findMemberExpressionAssignment.js.map +1 -1
- package/build/analyze/helpers/engineHelpers/resolveFunctionIdentifier.js +2 -1
- package/build/analyze/helpers/engineHelpers/resolveFunctionIdentifier.js.map +1 -1
- package/build/analyze/helpers/engineHelpers/taintFlow.js +11 -2
- package/build/analyze/helpers/engineHelpers/taintFlow.js.map +1 -1
- package/build/analyze/helpers/initRules.js.map +1 -1
- package/build/analyze/helpers/schemas.js +6 -2
- package/build/analyze/helpers/schemas.js.map +1 -1
- package/build/analyze/helpers/validate.js +3 -3
- package/build/analyze/helpers/validate.js.map +1 -1
- package/build/analyze/index.js +1 -0
- package/build/analyze/index.js.map +1 -1
- package/build/completion/index.js +432 -0
- package/build/completion/index.js.map +1 -0
- package/build/cs_mast/index.js +213 -0
- package/build/cs_mast/index.js.map +1 -0
- package/build/endpoints/next_js/client_jsFilesHref.js +2 -1
- package/build/endpoints/next_js/client_jsFilesHref.js.map +1 -1
- package/build/endpoints/next_js/client_jsonParse.js +2 -1
- package/build/endpoints/next_js/client_jsonParse.js.map +1 -1
- package/build/endpoints/next_js/client_mappedJsonFile.js +2 -1
- package/build/endpoints/next_js/client_mappedJsonFile.js.map +1 -1
- package/build/endpoints/next_js/client_subsequentRequests.js +2 -1
- package/build/endpoints/next_js/client_subsequentRequests.js.map +1 -1
- package/build/fingerprint/index.js +56 -33
- package/build/fingerprint/index.js.map +1 -1
- package/build/globalConfig.js +3 -3
- package/build/globalConfig.js.map +1 -1
- package/build/index.js +210 -10
- package/build/index.js.map +1 -1
- package/build/lazyLoad/angular/angular_getFromMainJs.js +2 -1
- package/build/lazyLoad/angular/angular_getFromMainJs.js.map +1 -1
- package/build/lazyLoad/downloadFilesUtil.js +6 -5
- package/build/lazyLoad/downloadFilesUtil.js.map +1 -1
- package/build/lazyLoad/downloadLoadedJsUtil.js +59 -5
- package/build/lazyLoad/downloadLoadedJsUtil.js.map +1 -1
- package/build/lazyLoad/downloadQueue.js +10 -9
- package/build/lazyLoad/downloadQueue.js.map +1 -1
- package/build/lazyLoad/index.js +127 -85
- package/build/lazyLoad/index.js.map +1 -1
- package/build/lazyLoad/methodFilter.js +50 -0
- package/build/lazyLoad/methodFilter.js.map +1 -0
- package/build/lazyLoad/next_js/NextJsCrawler.js +69 -48
- package/build/lazyLoad/next_js/NextJsCrawler.js.map +1 -1
- package/build/lazyLoad/next_js/next_GetLazyResourcesBuildManifestJs.js +36 -34
- package/build/lazyLoad/next_js/next_GetLazyResourcesBuildManifestJs.js.map +1 -1
- package/build/lazyLoad/next_js/next_GetLazyResourcesWebpackJs.js +66 -13
- package/build/lazyLoad/next_js/next_GetLazyResourcesWebpackJs.js.map +1 -1
- package/build/lazyLoad/next_js/next_parseLayoutJs.js +80 -68
- package/build/lazyLoad/next_js/next_parseLayoutJs.js.map +1 -1
- package/build/lazyLoad/next_js/next_promiseResolve.js +22 -16
- package/build/lazyLoad/next_js/next_promiseResolve.js.map +1 -1
- package/build/lazyLoad/nuxt_js/nuxt_astParse.js +49 -47
- package/build/lazyLoad/nuxt_js/nuxt_astParse.js.map +1 -1
- package/build/lazyLoad/nuxt_js/nuxt_getBuildsManifest.js +64 -0
- package/build/lazyLoad/nuxt_js/nuxt_getBuildsManifest.js.map +1 -0
- package/build/lazyLoad/nuxt_js/nuxt_getFromPageSource.js +3 -1
- package/build/lazyLoad/nuxt_js/nuxt_getFromPageSource.js.map +1 -1
- package/build/lazyLoad/nuxt_js/nuxt_stringAnalysisJSFiles.js +3 -2
- package/build/lazyLoad/nuxt_js/nuxt_stringAnalysisJSFiles.js.map +1 -1
- package/build/lazyLoad/react/react_followImports.js +16 -7
- package/build/lazyLoad/react/react_followImports.js.map +1 -1
- package/build/lazyLoad/react/react_webpackChunkPaths.js +175 -123
- package/build/lazyLoad/react/react_webpackChunkPaths.js.map +1 -1
- package/build/lazyLoad/svelte/svelte_getFromPageSource.js +16 -0
- package/build/lazyLoad/svelte/svelte_getFromPageSource.js.map +1 -1
- package/build/lazyLoad/svelte/svelte_getVersionJson.js +40 -0
- package/build/lazyLoad/svelte/svelte_getVersionJson.js.map +1 -0
- package/build/lazyLoad/svelte/svelte_stringAnalysisJSFiles.js +3 -2
- package/build/lazyLoad/svelte/svelte_stringAnalysisJSFiles.js.map +1 -1
- package/build/lazyLoad/techDetect/checkAngularJS.js +27 -6
- package/build/lazyLoad/techDetect/checkAngularJS.js.map +1 -1
- package/build/lazyLoad/techDetect/checkNextJS.js +24 -15
- package/build/lazyLoad/techDetect/checkNextJS.js.map +1 -1
- package/build/lazyLoad/techDetect/checkReact.js +8 -0
- package/build/lazyLoad/techDetect/checkReact.js.map +1 -1
- package/build/lazyLoad/techDetect/checkSvelte.js +17 -0
- package/build/lazyLoad/techDetect/checkSvelte.js.map +1 -1
- package/build/lazyLoad/techDetect/index.js +33 -5
- package/build/lazyLoad/techDetect/index.js.map +1 -1
- package/build/lazyLoad/vue/vue_RuntimeJs.js +2 -1
- package/build/lazyLoad/vue/vue_RuntimeJs.js.map +1 -1
- package/build/lazyLoad/vue/vue_SingleJsFileOnHome.js +2 -1
- package/build/lazyLoad/vue/vue_SingleJsFileOnHome.js.map +1 -1
- package/build/lazyLoad/vue/vue_discoverJsFiles.js +59 -25
- package/build/lazyLoad/vue/vue_discoverJsFiles.js.map +1 -1
- package/build/lazyLoad/vue/vue_getClientSidePaths.js +2 -1
- package/build/lazyLoad/vue/vue_getClientSidePaths.js.map +1 -1
- package/build/lazyLoad/vue/vue_jsImports.js +2 -1
- package/build/lazyLoad/vue/vue_jsImports.js.map +1 -1
- package/build/lazyLoad/vue/vue_recursiveClientSidePathDownload.js +2 -2
- package/build/lazyLoad/vue/vue_recursiveClientSidePathDownload.js.map +1 -1
- package/build/lazyLoad/vue/vue_viteMapDeps.js +62 -54
- package/build/lazyLoad/vue/vue_viteMapDeps.js.map +1 -1
- package/build/map/angular_js/getAngularConnections.js +88 -0
- package/build/map/angular_js/getAngularConnections.js.map +1 -0
- package/build/map/angular_js/interactive.js +4 -0
- package/build/map/angular_js/interactive.js.map +1 -0
- package/build/map/graphql/resolveGraphql.js +2 -1
- package/build/map/graphql/resolveGraphql.js.map +1 -1
- package/build/map/index.js +38 -0
- package/build/map/index.js.map +1 -1
- package/build/map/next_js/getExports.js +2 -1
- package/build/map/next_js/getExports.js.map +1 -1
- package/build/map/next_js/getFetchInstances.js +2 -1
- package/build/map/next_js/getFetchInstances.js.map +1 -1
- package/build/map/next_js/getTurbopackConnections.js +2 -1
- package/build/map/next_js/getTurbopackConnections.js.map +1 -1
- package/build/map/next_js/getWebpackConnections.js +2 -1
- package/build/map/next_js/getWebpackConnections.js.map +1 -1
- package/build/map/next_js/resolveAxios.js +2 -1
- package/build/map/next_js/resolveAxios.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/directCallsWithoutAssignment.js +2 -1
- package/build/map/next_js/resolveAxiosHelpers/directCallsWithoutAssignment.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/findAxiosInstance.js +2 -1
- package/build/map/next_js/resolveAxiosHelpers/findAxiosInstance.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/findCrossChunkParams.js +2 -1
- package/build/map/next_js/resolveAxiosHelpers/findCrossChunkParams.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/handleAxiosCreate.js +2 -1
- package/build/map/next_js/resolveAxiosHelpers/handleAxiosCreate.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/handleZDotCreate.js +2 -1
- package/build/map/next_js/resolveAxiosHelpers/handleZDotCreate.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/interceptorHeaders.js +2 -1
- package/build/map/next_js/resolveAxiosHelpers/interceptorHeaders.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/processExportedEndpoints.js +2 -1
- package/build/map/next_js/resolveAxiosHelpers/processExportedEndpoints.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/traceAxiosInstanceExports.js +2 -1
- package/build/map/next_js/resolveAxiosHelpers/traceAxiosInstanceExports.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/traceBody.js +2 -1
- package/build/map/next_js/resolveAxiosHelpers/traceBody.js.map +1 -1
- package/build/map/next_js/resolveFetch.js +2 -1
- package/build/map/next_js/resolveFetch.js.map +1 -1
- package/build/map/next_js/resolveNewRequest.js +2 -1
- package/build/map/next_js/resolveNewRequest.js.map +1 -1
- package/build/map/next_js/resolveServerActions.js +2 -1
- package/build/map/next_js/resolveServerActions.js.map +1 -1
- package/build/map/next_js/utils.js +2 -1
- package/build/map/next_js/utils.js.map +1 -1
- package/build/map/react_js/getReactConnections.js +5 -4
- package/build/map/react_js/getReactConnections.js.map +1 -1
- package/build/map/vue_js/crossFileResolver.js +3 -2
- package/build/map/vue_js/crossFileResolver.js.map +1 -1
- package/build/map/vue_js/getViteConnections.js +5 -4
- package/build/map/vue_js/getViteConnections.js.map +1 -1
- package/build/map/vue_js/taint_utils.js +2 -1
- package/build/map/vue_js/taint_utils.js.map +1 -1
- package/build/map/vue_js/vue_resolveFetch.js +3 -2
- package/build/map/vue_js/vue_resolveFetch.js.map +1 -1
- package/build/map/vue_js/vue_resolveHttpClient.js +5 -3
- package/build/map/vue_js/vue_resolveHttpClient.js.map +1 -1
- package/build/map/vue_js/vue_resolveXhr.js +5 -3
- package/build/map/vue_js/vue_resolveXhr.js.map +1 -1
- package/build/mcp/cli.js.map +1 -1
- package/build/refactor/index.js +973 -27
- package/build/refactor/index.js.map +1 -1
- package/build/refactor/next/helpers.js +395 -0
- package/build/refactor/next/helpers.js.map +1 -0
- package/build/refactor/next/index.js +261 -71
- package/build/refactor/next/index.js.map +1 -1
- package/build/refactor/next/transform.js +1027 -0
- package/build/refactor/next/transform.js.map +1 -0
- package/build/refactor/next/validator.js +119 -0
- package/build/refactor/next/validator.js.map +1 -0
- package/build/refactor/react/helpers.js +106 -0
- package/build/refactor/react/helpers.js.map +1 -0
- package/build/refactor/react/index.js +290 -585
- package/build/refactor/react/index.js.map +1 -1
- package/build/refactor/react/library-classify.js +397 -0
- package/build/refactor/react/library-classify.js.map +1 -0
- package/build/refactor/react/transform.js +1852 -0
- package/build/refactor/react/transform.js.map +1 -0
- package/build/refactor/react/validator.js +123 -0
- package/build/refactor/react/validator.js.map +1 -0
- package/build/refactor/react-vite/index.js +652 -0
- package/build/refactor/react-vite/index.js.map +1 -0
- package/build/refactor/react-vite/vendor-analyze.js +336 -0
- package/build/refactor/react-vite/vendor-analyze.js.map +1 -0
- package/build/refactor/remote/cache.js +250 -0
- package/build/refactor/remote/cache.js.map +1 -0
- package/build/refactor/remote/config.js +63 -0
- package/build/refactor/remote/config.js.map +1 -0
- package/build/refactor/remote/hf-client.js +202 -0
- package/build/refactor/remote/hf-client.js.map +1 -0
- package/build/refactor/remote/version-detect.js +355 -0
- package/build/refactor/remote/version-detect.js.map +1 -0
- package/build/refactor/vue/index.js +176 -0
- package/build/refactor/vue/index.js.map +1 -0
- package/build/refactor/vue/vendor-analyze-vue.js +271 -0
- package/build/refactor/vue/vendor-analyze-vue.js.map +1 -0
- package/build/refactor/vue/vite.js +216 -0
- package/build/refactor/vue/vite.js.map +1 -0
- package/build/report/utility/genHtml.js +15 -4
- package/build/report/utility/genHtml.js.map +1 -1
- package/build/run/bundler-detect.js +154 -0
- package/build/run/bundler-detect.js.map +1 -0
- package/build/run/index.js +208 -16
- package/build/run/index.js.map +1 -1
- package/build/sourcemaps/index.js +89 -0
- package/build/sourcemaps/index.js.map +1 -0
- package/build/strings/index.js +6 -2
- package/build/strings/index.js.map +1 -1
- package/build/strings/trufflehog.js +71 -0
- package/build/strings/trufflehog.js.map +1 -0
- package/build/utility/banner.js +68 -0
- package/build/utility/banner.js.map +1 -0
- package/build/utility/getChromiumPath.js +40 -0
- package/build/utility/getChromiumPath.js.map +1 -0
- package/build/utility/globals.js +7 -0
- package/build/utility/globals.js.map +1 -1
- package/build/utility/makeReq.js +21 -11
- package/build/utility/makeReq.js.map +1 -1
- package/build/utility/postmanGenerator.js +2 -2
- package/build/utility/postmanGenerator.js.map +1 -1
- package/build/utility/puppeteerInstance.js +7 -1
- package/build/utility/puppeteerInstance.js.map +1 -1
- package/package.json +14 -8
- package/scripts/smoke-test.js +74 -0
package/build/refactor/index.js
CHANGED
|
@@ -9,15 +9,612 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { execSync } from "child_process";
|
|
12
14
|
import prettier from "prettier";
|
|
13
15
|
// Next.js
|
|
14
|
-
import refactorNext from "./next/index.js";
|
|
16
|
+
import refactorNext, { refactorNextWebpack } from "./next/index.js";
|
|
15
17
|
// React
|
|
16
18
|
import refactorReact from "./react/index.js";
|
|
19
|
+
// React (Vite)
|
|
20
|
+
import refactorVite from "./react-vite/index.js";
|
|
21
|
+
// Vue
|
|
22
|
+
import refactorVueWebpack from "./vue/index.js";
|
|
23
|
+
import { refactorVueVite } from "./vue/vite.js";
|
|
24
|
+
// Remote HuggingFace client + cache
|
|
25
|
+
import { TECH_TO_BRANCH, getHfRawUrl, fetchCollisionsJson, listCollisionsFiles, listCollisionsFileHashes, validateRemoteBranch, validateRemotePath, getSampleSize, getTechnology, } from "./remote/hf-client.js";
|
|
26
|
+
import { loadRefactorConfig, validateRefactorConfig, purgeDynamicVersionDetectionScatConfig, saveDynamicVersionDetectionScatConfig, } from "./remote/config.js";
|
|
27
|
+
import { loadListCache, saveListCache, shouldRefreshListCache, loadCachedSignature, saveSignatureToCache, isSignatureCacheFresh, validateCaches, } from "./remote/cache.js";
|
|
28
|
+
import { detectReactVersion, VERSION_TECH_TO_BUNDLER, listAvailableVersions, selectDynamicScatConfigs, validateStaticScatConfig, } from "./remote/version-detect.js";
|
|
29
|
+
/**
|
|
30
|
+
* Derives the assets directory from the URL embedded in any chunk's code comment.
|
|
31
|
+
* Chunks have a header like `// File Source: http://localhost:3001/assets/foo.js`.
|
|
32
|
+
* Returns null if the assets directory cannot be determined or does not exist.
|
|
33
|
+
*/
|
|
34
|
+
function findAssetsDir(chunks, mappedJsonPath) {
|
|
35
|
+
var _a;
|
|
36
|
+
for (const chunk of Object.values(chunks)) {
|
|
37
|
+
const firstLine = ((_a = chunk.code) !== null && _a !== void 0 ? _a : "").split("\n")[0];
|
|
38
|
+
const urlMatch = firstLine.match(/\/\/ File Source: (https?:\/\/[^\s]+)/);
|
|
39
|
+
if (!urlMatch)
|
|
40
|
+
continue;
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(urlMatch[1]);
|
|
43
|
+
const port = url.port;
|
|
44
|
+
const hostname = url.hostname + (port ? `_${port}` : "");
|
|
45
|
+
const assetsDir = path.join(path.dirname(path.resolve(mappedJsonPath)), "output", hostname, "assets");
|
|
46
|
+
if (fs.existsSync(assetsDir))
|
|
47
|
+
return assetsDir;
|
|
48
|
+
}
|
|
49
|
+
catch (_b) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns paths to .js files in `assetsDir` that are NOT covered by any chunk in `chunks`.
|
|
57
|
+
* Chunks cover a file when `path.basename(chunk.file)` matches the assets filename.
|
|
58
|
+
*/
|
|
59
|
+
function findVendorChunkFiles(chunks, assetsDir) {
|
|
60
|
+
const coveredFiles = new Set();
|
|
61
|
+
for (const chunk of Object.values(chunks)) {
|
|
62
|
+
if (chunk.file)
|
|
63
|
+
coveredFiles.add(path.basename(chunk.file));
|
|
64
|
+
}
|
|
65
|
+
return fs
|
|
66
|
+
.readdirSync(assetsDir)
|
|
67
|
+
.filter((f) => f.endsWith(".js") && !coveredFiles.has(f))
|
|
68
|
+
.map((f) => path.join(assetsDir, f));
|
|
69
|
+
}
|
|
70
|
+
// Maps a refactor tech to the scat-combo directory name in a baseline tree.
|
|
71
|
+
// The directory name is the scat categories joined by "-" in the order they appear in
|
|
72
|
+
// ALL_SCAT_CATEGORIES (matching the naming convention used by jsr-cs-mast-s-gen/experiment/csmast.mjs).
|
|
73
|
+
const BASELINE_SCAT_DIR = {
|
|
74
|
+
"react-webpack": "lit-decl-loop-cond",
|
|
75
|
+
"react-vite": "lit-decl-loop-cond",
|
|
76
|
+
"next-webpack": "lit-decl-loop-cond",
|
|
77
|
+
};
|
|
78
|
+
// Canonical ordering of scat categories (matches ALL_SCAT_CATEGORIES in csmast.mjs).
|
|
79
|
+
// Used when converting a user-supplied --scat list to a bucket directory name.
|
|
80
|
+
const ALL_SCAT_CATEGORIES = ["lit", "id", "op", "decl", "loop", "cond", "name", "val", "op_name"];
|
|
81
|
+
// Converts a scat list to the bucket directory name, preserving canonical category order.
|
|
82
|
+
export const scatToDir = (scat) => {
|
|
83
|
+
const scatSet = new Set(scat);
|
|
84
|
+
return ALL_SCAT_CATEGORIES.filter((c) => scatSet.has(c)).join("-") || scat.join("-");
|
|
85
|
+
};
|
|
86
|
+
// Parses a collisions.json file and returns signatures whose count equals the maximum.
|
|
87
|
+
export const loadCollisionsFile = (filePath) => {
|
|
88
|
+
const records = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
89
|
+
const maxCount = records.reduce((m, r) => (r.count > m ? r.count : m), 0);
|
|
90
|
+
return new Set(records.filter((r) => r.count >= maxCount).map((r) => r.signature));
|
|
91
|
+
};
|
|
92
|
+
// Resolves `--collisions <path>` to a library signature set. Accepts:
|
|
93
|
+
// - a file path → reads directly (max-count signatures)
|
|
94
|
+
// - a standard directory → tries known candidate paths in order:
|
|
95
|
+
// <dir>/baselines/<tech>/<scat>/collisions.json
|
|
96
|
+
// <dir>/<tech>/<scat>/collisions.json
|
|
97
|
+
// <dir>/<scat>/collisions.json
|
|
98
|
+
// <dir>/collisions.json
|
|
99
|
+
// - a per-feature results directory → detects <dir>/<feature>/<scat>/collisions.json
|
|
100
|
+
// and intersects max-count signatures across all feature subdirs (reads only
|
|
101
|
+
// the relevant scat files, not the full dataset tree)
|
|
102
|
+
const buildLibSigs = (input, tech, scatOverride) => {
|
|
103
|
+
if (!fs.existsSync(input))
|
|
104
|
+
return null;
|
|
105
|
+
const stat = fs.statSync(input);
|
|
106
|
+
const scat = scatOverride ? scatToDir(scatOverride) : BASELINE_SCAT_DIR[tech];
|
|
107
|
+
// Case 1: direct file path
|
|
108
|
+
if (stat.isFile()) {
|
|
109
|
+
return { sigs: loadCollisionsFile(input), desc: input };
|
|
110
|
+
}
|
|
111
|
+
// Case 2: standard directory candidates (existing layout)
|
|
112
|
+
const candidates = [
|
|
113
|
+
scat ? path.join(input, "baselines", tech, scat, "collisions.json") : null,
|
|
114
|
+
scat ? path.join(input, tech, scat, "collisions.json") : null,
|
|
115
|
+
scat ? path.join(input, scat, "collisions.json") : null,
|
|
116
|
+
path.join(input, "collisions.json"),
|
|
117
|
+
].filter(Boolean);
|
|
118
|
+
for (const c of candidates) {
|
|
119
|
+
if (fs.existsSync(c) && fs.statSync(c).isFile()) {
|
|
120
|
+
return { sigs: loadCollisionsFile(c), desc: c };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Case 3: per-feature results directory — <dir>/<feature>/<scat>/collisions.json
|
|
124
|
+
// Only the scat-relevant files are read (one per feature subdir), so memory usage
|
|
125
|
+
// stays low even when the full dataset tree is hundreds of gigabytes.
|
|
126
|
+
if (scat) {
|
|
127
|
+
const featureFiles = [];
|
|
128
|
+
for (const entry of fs.readdirSync(input, { withFileTypes: true })) {
|
|
129
|
+
if (!entry.isDirectory())
|
|
130
|
+
continue;
|
|
131
|
+
const p = path.join(input, entry.name, scat, "collisions.json");
|
|
132
|
+
if (fs.existsSync(p))
|
|
133
|
+
featureFiles.push(p);
|
|
134
|
+
}
|
|
135
|
+
if (featureFiles.length > 0) {
|
|
136
|
+
// Intersect max-count signatures across all feature subdirs.
|
|
137
|
+
// A signature present in every feature's max-count set must come from
|
|
138
|
+
// code shared by all apps — i.e. library code.
|
|
139
|
+
let intersection = null;
|
|
140
|
+
for (const p of featureFiles) {
|
|
141
|
+
const sigs = loadCollisionsFile(p);
|
|
142
|
+
if (intersection === null) {
|
|
143
|
+
intersection = sigs;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
for (const sig of intersection) {
|
|
147
|
+
if (!sigs.has(sig))
|
|
148
|
+
intersection.delete(sig);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (intersection && intersection.size > 0) {
|
|
153
|
+
return {
|
|
154
|
+
sigs: intersection,
|
|
155
|
+
desc: `${input} (intersection of ${featureFiles.length} feature dirs)`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
};
|
|
162
|
+
// Loads library signatures from the remote HuggingFace dataset.
|
|
163
|
+
// Returns null if the branch is not configured for `tech` or if validation fails.
|
|
164
|
+
// When opts.remoteCollisions is provided it is used directly as the bucket path,
|
|
165
|
+
// bypassing the TECH_TO_BRANCH lookup and metadata-file validation.
|
|
166
|
+
// Exits with code 25 if opts.remoteCollisions is set but the path does not exist.
|
|
167
|
+
const loadRemoteLibSigs = (tech, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
168
|
+
var _a, _b, _c, _d, _e;
|
|
169
|
+
const branch = (_a = opts.remoteCollisions) !== null && _a !== void 0 ? _a : TECH_TO_BRANCH[tech];
|
|
170
|
+
if (!branch)
|
|
171
|
+
return null;
|
|
172
|
+
const scatDir = opts.scat ? scatToDir(opts.scat) : BASELINE_SCAT_DIR[tech];
|
|
173
|
+
if (!scatDir)
|
|
174
|
+
return null;
|
|
175
|
+
// Load and validate config.
|
|
176
|
+
const config = loadRefactorConfig();
|
|
177
|
+
const configWarnings = validateRefactorConfig(config);
|
|
178
|
+
for (const w of configWarnings) {
|
|
179
|
+
console.log(chalk.yellow(`[!] Refactor config warning: ${w}`));
|
|
180
|
+
}
|
|
181
|
+
// Validate cache files.
|
|
182
|
+
const cacheWarnings = validateCaches();
|
|
183
|
+
for (const w of cacheWarnings) {
|
|
184
|
+
console.log(chalk.yellow(`[!] Cache validation warning: ${w} (will refresh)`));
|
|
185
|
+
}
|
|
186
|
+
if (opts.remoteCollisions) {
|
|
187
|
+
// User-supplied path: validate it exists in the HF bucket.
|
|
188
|
+
console.log(chalk.cyan(`[i] Validating remote dataset path "${branch}"...`));
|
|
189
|
+
const pathExists = yield validateRemotePath(branch);
|
|
190
|
+
if (!pathExists) {
|
|
191
|
+
console.error(chalk.red(`[!] Remote dataset path "${branch}" not found in the shriyanss/cs-mast-s-dataset bucket.`));
|
|
192
|
+
process.exit(25);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// Auto-detected branch: validate metadata files exist.
|
|
197
|
+
console.log(chalk.cyan(`[i] Validating remote bucket prefix "${branch}"...`));
|
|
198
|
+
const branchOk = yield validateRemoteBranch(branch);
|
|
199
|
+
if (!branchOk) {
|
|
200
|
+
console.log(chalk.red(`[!] Remote bucket prefix "${branch}" is missing required metadata (sample_size / technology). Skipping remote signatures.`));
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
// Verify the bucket prefix technology matches.
|
|
204
|
+
const remoteTech = yield getTechnology(branch);
|
|
205
|
+
if (remoteTech !== tech) {
|
|
206
|
+
console.log(chalk.red(`[!] Remote bucket prefix "${branch}" is for technology "${remoteTech}", not "${tech}". Skipping remote signatures.`));
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Try to load the sample size for quality filtering.
|
|
211
|
+
// Custom paths may not have a sample_size file; fall back to 0 (skip quality filter).
|
|
212
|
+
let sampleSize = 0;
|
|
213
|
+
if (opts.remoteCollisions) {
|
|
214
|
+
try {
|
|
215
|
+
sampleSize = yield getSampleSize(branch);
|
|
216
|
+
}
|
|
217
|
+
catch (_f) {
|
|
218
|
+
// Custom path may not have sample_size; quality filter will be skipped (sampleSize=0)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
sampleSize = yield getSampleSize(branch);
|
|
223
|
+
}
|
|
224
|
+
// Build / refresh file list cache.
|
|
225
|
+
// Also refresh if the specific branch is not yet in the cache (e.g. first run
|
|
226
|
+
// with a new --remote-collisions path, or a new tech added after initial cache).
|
|
227
|
+
let listCache = cacheWarnings.length > 0 ? null : loadListCache();
|
|
228
|
+
const branchMissingFromCache = !(listCache === null || listCache === void 0 ? void 0 : listCache.branches[branch]);
|
|
229
|
+
if (shouldRefreshListCache(listCache, opts) || branchMissingFromCache) {
|
|
230
|
+
console.log(chalk.cyan(`[i] Refreshing remote file list cache for bucket prefix "${branch}"...`));
|
|
231
|
+
const paths = yield listCollisionsFiles(branch, scatDir);
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
const branches = (_b = listCache === null || listCache === void 0 ? void 0 : listCache.branches) !== null && _b !== void 0 ? _b : {};
|
|
234
|
+
branches[branch] = paths;
|
|
235
|
+
listCache = { generatedAt: now, branches };
|
|
236
|
+
saveListCache(listCache);
|
|
237
|
+
console.log(chalk.cyan(`[i] File list cache updated: ${paths.length} files found`));
|
|
238
|
+
}
|
|
239
|
+
const filePaths = (_c = listCache === null || listCache === void 0 ? void 0 : listCache.branches[branch]) !== null && _c !== void 0 ? _c : [];
|
|
240
|
+
// Filter to only paths that match <something>/<scatDir>/collisions.json
|
|
241
|
+
const matchingPaths = filePaths.filter((p) => p.endsWith(`/${scatDir}/collisions.json`));
|
|
242
|
+
if (matchingPaths.length === 0) {
|
|
243
|
+
console.log(chalk.yellow(`[!] No remote collisions files found for scat "${scatDir}" in branch "${branch}"`));
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
console.log(chalk.cyan(`[i] Loading ${matchingPaths.length} remote signature files (quality threshold: ${opts.signatureQuality}%)...`));
|
|
247
|
+
// Content-based cache validation: fetch the upstream tree's per-file hashes once per
|
|
248
|
+
// branch so each cache entry below can be checked against current dataset content
|
|
249
|
+
// instead of trusting its local age alone. Skipped entirely under --skip-cache-checks.
|
|
250
|
+
// A failure here (e.g. HF rate-limiting under concurrent load) must never abort the
|
|
251
|
+
// whole refactor run — fall back to an empty map, which degrades each cache-freshness
|
|
252
|
+
// check below to the pre-existing age-based TTL.
|
|
253
|
+
let remoteHashes = new Map();
|
|
254
|
+
if (!opts.skipCacheChecks) {
|
|
255
|
+
try {
|
|
256
|
+
remoteHashes = yield listCollisionsFileHashes(branch, scatDir);
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
console.log(chalk.yellow(`[!] Could not fetch upstream content hashes for cache validation (${e.message}) — falling back to age-based cache checks.`));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let intersection = null;
|
|
263
|
+
let loadedCount = 0;
|
|
264
|
+
for (const relPath of matchingPaths) {
|
|
265
|
+
let records = null;
|
|
266
|
+
const remoteHash = (_d = remoteHashes.get(relPath)) !== null && _d !== void 0 ? _d : null;
|
|
267
|
+
// Check local signature cache first.
|
|
268
|
+
if (isSignatureCacheFresh(branch, relPath, opts.skipCacheChecks, remoteHash)) {
|
|
269
|
+
records = loadCachedSignature(branch, relPath);
|
|
270
|
+
}
|
|
271
|
+
if (records === null) {
|
|
272
|
+
// Fetch from remote.
|
|
273
|
+
const url = getHfRawUrl(branch, relPath);
|
|
274
|
+
records = yield fetchCollisionsJson(url);
|
|
275
|
+
if (records === null) {
|
|
276
|
+
// 404 or network error.
|
|
277
|
+
// For user-specified --remote-collisions paths the list is stable;
|
|
278
|
+
// just skip this file and continue without poisoning the list cache.
|
|
279
|
+
// For auto-detected branches, invalidate the list cache so a stale
|
|
280
|
+
// entry (e.g. a deleted file) is not tried again on the next run.
|
|
281
|
+
if (!opts.remoteCollisions && !opts.skipCacheChecks) {
|
|
282
|
+
console.log(chalk.yellow(`[!] Could not fetch ${relPath} (404 or error) — refreshing list cache`));
|
|
283
|
+
const freshPaths = yield listCollisionsFiles(branch, scatDir);
|
|
284
|
+
const updatedBranches = (_e = listCache === null || listCache === void 0 ? void 0 : listCache.branches) !== null && _e !== void 0 ? _e : {};
|
|
285
|
+
updatedBranches[branch] = freshPaths;
|
|
286
|
+
saveListCache({ generatedAt: Date.now(), branches: updatedBranches });
|
|
287
|
+
listCache = { generatedAt: Date.now(), branches: updatedBranches };
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
console.log(chalk.yellow(`[!] Could not fetch ${relPath} — skipping`));
|
|
291
|
+
}
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
saveSignatureToCache(branch, relPath, records, config.maxCacheSizeMb, remoteHash);
|
|
295
|
+
}
|
|
296
|
+
// Apply signature quality filter: (count / sampleSize) * 100 >= threshold.
|
|
297
|
+
// When sampleSize is 0 (no sample_size file on the remote path), skip quality
|
|
298
|
+
// filtering and include all records.
|
|
299
|
+
const filtered = sampleSize === 0 ? records : records.filter((r) => (r.count / sampleSize) * 100 >= opts.signatureQuality);
|
|
300
|
+
// Skip feature dirs with no records — they indicate missing/empty dataset
|
|
301
|
+
// entries and would collapse the intersection to zero if included.
|
|
302
|
+
if (filtered.length === 0)
|
|
303
|
+
continue;
|
|
304
|
+
// Intersect with previous sets.
|
|
305
|
+
const sigSet = new Set(filtered.map((r) => r.signature));
|
|
306
|
+
if (intersection === null) {
|
|
307
|
+
intersection = sigSet;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
for (const sig of intersection) {
|
|
311
|
+
if (!sigSet.has(sig))
|
|
312
|
+
intersection.delete(sig);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
loadedCount++;
|
|
316
|
+
}
|
|
317
|
+
if (!intersection || intersection.size === 0) {
|
|
318
|
+
console.log(chalk.red(`[!] Remote signatures loaded but intersection is empty — library stripping will find nothing to skip ` +
|
|
319
|
+
`(quality threshold may be too high, or every dataset entry fetched 0 records). ` +
|
|
320
|
+
`If this is unexpected, purge ~/.js-recon/refactor/{signature_cache,cs-mast-s-list-cache.json} and retry.`));
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
console.log(chalk.cyan(`[i] Loaded ${intersection.size} library signatures from remote (${loadedCount} files, bucket prefix: ${branch})`));
|
|
324
|
+
return { sigs: intersection, desc: `remote:${branch} (${loadedCount} files, quality>=${opts.signatureQuality}%)` };
|
|
325
|
+
});
|
|
17
326
|
const availableTechs = {
|
|
18
|
-
next: "Next.js",
|
|
19
|
-
|
|
327
|
+
"next-turbopack": "Next.js (Turbopack)",
|
|
328
|
+
"next-webpack": "Next.js (webpack)",
|
|
329
|
+
"react-webpack": "React (webpack)",
|
|
330
|
+
"react-vite": "React (Vite)",
|
|
331
|
+
"vue-webpack": "Vue.js (webpack)",
|
|
332
|
+
"vue-vite": "Vue.js (Vite)",
|
|
20
333
|
};
|
|
334
|
+
/**
|
|
335
|
+
* Scaffolds a minimal webpack project in `outputDir` (package.json,
|
|
336
|
+
* webpack.config.js, index.html), then runs `npm install` + `npm run build`
|
|
337
|
+
* as a build check. Uses babel-loader so JSX in .js files is handled without
|
|
338
|
+
* renaming.
|
|
339
|
+
*
|
|
340
|
+
* The entry file is the first written file that contains `createRoot(` — i.e.
|
|
341
|
+
* the app entry point. If no such file is found, the first written file is used.
|
|
342
|
+
*/
|
|
343
|
+
function runBuildCheck(outputDir, writtenFiles, detectedVersion) {
|
|
344
|
+
var _a;
|
|
345
|
+
if (writtenFiles.length === 0)
|
|
346
|
+
return;
|
|
347
|
+
// Find the entry file: the module that calls createRoot().
|
|
348
|
+
const entryFile = (_a = writtenFiles.find((f) => {
|
|
349
|
+
try {
|
|
350
|
+
return fs.readFileSync(f, "utf8").includes("createRoot(");
|
|
351
|
+
}
|
|
352
|
+
catch (_a) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
})) !== null && _a !== void 0 ? _a : writtenFiles[0];
|
|
356
|
+
const entryRelative = `./${path.basename(entryFile)}`;
|
|
357
|
+
console.log(chalk.cyan(`[i] Setting up webpack build check in ${outputDir}/ (entry: ${entryRelative})`));
|
|
358
|
+
// Use detected React version when available, fall back to React 18.
|
|
359
|
+
const reactVersion = detectedVersion ? `^${detectedVersion.reactNpm}` : "^18.3.1";
|
|
360
|
+
const reactDomVersion = (detectedVersion === null || detectedVersion === void 0 ? void 0 : detectedVersion.reactDomNpm)
|
|
361
|
+
? `^${detectedVersion.reactDomNpm}`
|
|
362
|
+
: detectedVersion
|
|
363
|
+
? undefined // pre-0.14: no react-dom
|
|
364
|
+
: "^18.3.1";
|
|
365
|
+
if (detectedVersion) {
|
|
366
|
+
console.log(chalk.cyan(`[i] Using detected React version ${detectedVersion.versionKey} in package.json (react@${reactVersion})`));
|
|
367
|
+
}
|
|
368
|
+
const dependencies = Object.assign(Object.assign({ react: reactVersion }, (reactDomVersion ? { "react-dom": reactDomVersion } : {})), { "react-router-dom": "^6.27.0" });
|
|
369
|
+
// package.json — no "type": "module" so webpack.config.js can use CommonJS require
|
|
370
|
+
const pkg = {
|
|
371
|
+
name: "refactored-app",
|
|
372
|
+
version: "1.0.0",
|
|
373
|
+
scripts: { build: "webpack" },
|
|
374
|
+
dependencies,
|
|
375
|
+
devDependencies: {
|
|
376
|
+
"@babel/core": "^7.25.0",
|
|
377
|
+
"@babel/preset-env": "^7.25.0",
|
|
378
|
+
"@babel/preset-react": "^7.25.0",
|
|
379
|
+
"babel-loader": "^9.2.1",
|
|
380
|
+
"html-webpack-plugin": "^5.6.0",
|
|
381
|
+
webpack: "^5.95.0",
|
|
382
|
+
"webpack-cli": "^5.1.4",
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
fs.writeFileSync(path.join(outputDir, "package.json"), JSON.stringify(pkg, null, 2));
|
|
386
|
+
// webpack.config.js — babel-loader with preset-react handles JSX in .js files
|
|
387
|
+
const webpackConfig = `const path = require('path');
|
|
388
|
+
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
389
|
+
module.exports = {
|
|
390
|
+
mode: 'production',
|
|
391
|
+
entry: '${entryRelative}',
|
|
392
|
+
output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js', clean: true },
|
|
393
|
+
module: {
|
|
394
|
+
rules: [{
|
|
395
|
+
test: /\\.jsx?$/,
|
|
396
|
+
exclude: /node_modules/,
|
|
397
|
+
use: {
|
|
398
|
+
loader: 'babel-loader',
|
|
399
|
+
options: {
|
|
400
|
+
presets: [
|
|
401
|
+
['@babel/preset-env', { targets: 'defaults' }],
|
|
402
|
+
['@babel/preset-react', { runtime: 'automatic' }],
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
}],
|
|
407
|
+
},
|
|
408
|
+
resolve: { extensions: ['.js', '.jsx'] },
|
|
409
|
+
plugins: [new HtmlWebpackPlugin({ template: './index.html' })],
|
|
410
|
+
};
|
|
411
|
+
`;
|
|
412
|
+
fs.writeFileSync(path.join(outputDir, "webpack.config.js"), webpackConfig);
|
|
413
|
+
// index.html — HtmlWebpackPlugin injects the bundle script automatically
|
|
414
|
+
const indexHtml = `<!DOCTYPE html>
|
|
415
|
+
<html lang="en">
|
|
416
|
+
<head><meta charset="UTF-8" /><title>Refactored App</title></head>
|
|
417
|
+
<body><div id="root"></div></body>
|
|
418
|
+
</html>
|
|
419
|
+
`;
|
|
420
|
+
fs.writeFileSync(path.join(outputDir, "index.html"), indexHtml);
|
|
421
|
+
console.log(chalk.cyan("[i] Installing dependencies..."));
|
|
422
|
+
try {
|
|
423
|
+
execSync("npm install", { cwd: outputDir, stdio: "inherit" });
|
|
424
|
+
}
|
|
425
|
+
catch (_b) {
|
|
426
|
+
console.log(chalk.red("[!] npm install failed — skipping build check"));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
console.log(chalk.cyan("[i] Running build check..."));
|
|
430
|
+
try {
|
|
431
|
+
execSync("npm run build", { cwd: outputDir, stdio: "inherit" });
|
|
432
|
+
console.log(chalk.green("[✓] Build check passed"));
|
|
433
|
+
}
|
|
434
|
+
catch (_c) {
|
|
435
|
+
console.log(chalk.red("[!] Build check failed — review output above"));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Scaffolds a minimal Vite project in `outputDir` and runs `npm install` + `npm run build`
|
|
440
|
+
* as a build check. Uses @vitejs/plugin-react so JSX in .js files is handled.
|
|
441
|
+
*
|
|
442
|
+
* The entry file is the first written file that contains `createRoot(`.
|
|
443
|
+
*/
|
|
444
|
+
function runViteBuildCheck(outputDir, writtenFiles, detectedVersion) {
|
|
445
|
+
var _a;
|
|
446
|
+
if (writtenFiles.length === 0)
|
|
447
|
+
return;
|
|
448
|
+
// Deduplicate — callers should not pass duplicates, but guard here too so a
|
|
449
|
+
// duplicate does not cause the renameSync below to throw ENOENT on the
|
|
450
|
+
// second occurrence of the same path.
|
|
451
|
+
const uniqueFiles = [...new Set(writtenFiles)];
|
|
452
|
+
// Rename .js files to .jsx — Vite's import analysis can't parse JSX in .js files,
|
|
453
|
+
// and refactored Vite chunks always contain JSX. The original source files were .jsx.
|
|
454
|
+
const jsxFiles = uniqueFiles.map((f) => {
|
|
455
|
+
if (!f.endsWith(".js"))
|
|
456
|
+
return f;
|
|
457
|
+
const jsxPath = f.replace(/\.js$/, ".jsx");
|
|
458
|
+
fs.renameSync(f, jsxPath);
|
|
459
|
+
return jsxPath;
|
|
460
|
+
});
|
|
461
|
+
// After renaming to .jsx, update relative dynamic imports inside each file:
|
|
462
|
+
// import('./Foo.js') → import('./Foo.jsx') — only for same-dir relative imports.
|
|
463
|
+
const outputFileBasenames = new Set(jsxFiles.map((f) => path.basename(f)));
|
|
464
|
+
for (const f of jsxFiles) {
|
|
465
|
+
let content = fs.readFileSync(f, "utf8");
|
|
466
|
+
// Replace ./Foo.js with ./Foo.jsx only when Foo.jsx exists as an output file.
|
|
467
|
+
content = content.replace(/(import\([`'"])(\.\/[^`'"]+?)\.js([`'"]\))/g, (_match, open, stem, close) => {
|
|
468
|
+
const candidate = path.basename(stem) + ".jsx";
|
|
469
|
+
return outputFileBasenames.has(candidate) ? `${open}${stem}.jsx${close}` : _match;
|
|
470
|
+
});
|
|
471
|
+
fs.writeFileSync(f, content);
|
|
472
|
+
}
|
|
473
|
+
const entryFile = (_a = jsxFiles.find((f) => {
|
|
474
|
+
try {
|
|
475
|
+
return fs.readFileSync(f, "utf8").includes("createRoot(");
|
|
476
|
+
}
|
|
477
|
+
catch (_a) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
})) !== null && _a !== void 0 ? _a : jsxFiles[0];
|
|
481
|
+
const entryRelative = `./${path.basename(entryFile)}`;
|
|
482
|
+
console.log(chalk.cyan(`[i] Setting up Vite build check in ${outputDir}/ (entry: ${entryRelative})`));
|
|
483
|
+
// Use detected React version when available, fall back to React 18.
|
|
484
|
+
const reactVersion = detectedVersion ? `^${detectedVersion.reactNpm}` : "^18.3.1";
|
|
485
|
+
const reactDomVersion = (detectedVersion === null || detectedVersion === void 0 ? void 0 : detectedVersion.reactDomNpm)
|
|
486
|
+
? `^${detectedVersion.reactDomNpm}`
|
|
487
|
+
: detectedVersion
|
|
488
|
+
? undefined
|
|
489
|
+
: "^18.3.1";
|
|
490
|
+
if (detectedVersion) {
|
|
491
|
+
console.log(chalk.cyan(`[i] Using detected React version ${detectedVersion.versionKey} in package.json (react@${reactVersion})`));
|
|
492
|
+
}
|
|
493
|
+
const dependencies = Object.assign(Object.assign({ react: reactVersion }, (reactDomVersion ? { "react-dom": reactDomVersion } : {})), { "react-router-dom": "^6.27.0" });
|
|
494
|
+
const pkg = {
|
|
495
|
+
name: "refactored-app",
|
|
496
|
+
version: "1.0.0",
|
|
497
|
+
type: "module",
|
|
498
|
+
scripts: { build: "vite build" },
|
|
499
|
+
dependencies,
|
|
500
|
+
devDependencies: {
|
|
501
|
+
"@vitejs/plugin-react": "^4.3.1",
|
|
502
|
+
vite: "^5.4.0",
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
fs.writeFileSync(path.join(outputDir, "package.json"), JSON.stringify(pkg, null, 2));
|
|
506
|
+
const viteConfig = `import { defineConfig } from 'vite';
|
|
507
|
+
import react from '@vitejs/plugin-react';
|
|
508
|
+
export default defineConfig({
|
|
509
|
+
plugins: [react()],
|
|
510
|
+
build: { rollupOptions: { input: '${entryRelative}' } },
|
|
511
|
+
});
|
|
512
|
+
`;
|
|
513
|
+
fs.writeFileSync(path.join(outputDir, "vite.config.js"), viteConfig);
|
|
514
|
+
const indexHtml = `<!DOCTYPE html>
|
|
515
|
+
<html lang="en">
|
|
516
|
+
<head><meta charset="UTF-8" /><title>Refactored App</title></head>
|
|
517
|
+
<body>
|
|
518
|
+
<div id="root"></div>
|
|
519
|
+
<script type="module" src="${entryRelative}"></script>
|
|
520
|
+
</body>
|
|
521
|
+
</html>
|
|
522
|
+
`;
|
|
523
|
+
fs.writeFileSync(path.join(outputDir, "index.html"), indexHtml);
|
|
524
|
+
console.log(chalk.cyan("[i] Installing dependencies..."));
|
|
525
|
+
try {
|
|
526
|
+
execSync("npm install", { cwd: outputDir, stdio: "inherit" });
|
|
527
|
+
}
|
|
528
|
+
catch (_b) {
|
|
529
|
+
console.log(chalk.red("[!] npm install failed — skipping build check"));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
console.log(chalk.cyan("[i] Running Vite build check..."));
|
|
533
|
+
try {
|
|
534
|
+
execSync("npm run build", { cwd: outputDir, stdio: "inherit" });
|
|
535
|
+
console.log(chalk.green("[✓] Vite build check passed"));
|
|
536
|
+
}
|
|
537
|
+
catch (_c) {
|
|
538
|
+
console.log(chalk.red("[!] Vite build check failed — review output above"));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Resolves the scat dir list to use for version detection based on the user's options.
|
|
543
|
+
* Returns an empty array when version detection should be skipped (e.g., invalid static config).
|
|
544
|
+
*
|
|
545
|
+
* Dynamic mode (default):
|
|
546
|
+
* - Reads cached dynamic scat configs from ~/.js-recon/refactor/config.json.
|
|
547
|
+
* - If --detect-version-dynamic-conf-purge is set, clears the cache first.
|
|
548
|
+
* - If no cached configs exist, runs selectDynamicScatConfigs() and saves the result.
|
|
549
|
+
*
|
|
550
|
+
* Static mode (user passes comma-separated categories):
|
|
551
|
+
* - Validates that reliable_signatures.json is non-empty for all known versions.
|
|
552
|
+
* - Exits with code 26 if the config is invalid or empty for any version.
|
|
553
|
+
*/
|
|
554
|
+
function resolveVersionDetectionScatDirs(bundler, opts) {
|
|
555
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
556
|
+
var _a, _b;
|
|
557
|
+
const detectConfig = (_a = opts.detectVersionConfig) !== null && _a !== void 0 ? _a : "dynamic";
|
|
558
|
+
if (detectConfig === "dynamic") {
|
|
559
|
+
if (opts.detectVersionDynamicConfPurge) {
|
|
560
|
+
console.log(chalk.cyan("[i] Version detection: purging cached dynamic scat config..."));
|
|
561
|
+
purgeDynamicVersionDetectionScatConfig();
|
|
562
|
+
}
|
|
563
|
+
const cfg = loadRefactorConfig();
|
|
564
|
+
if (cfg.dynamicVersionDetectionScatConfig && cfg.dynamicVersionDetectionScatConfig.length > 0) {
|
|
565
|
+
console.log(chalk.cyan(`[i] Version detection: using cached dynamic scat config (${cfg.dynamicVersionDetectionScatConfig.join(", ")})`));
|
|
566
|
+
return cfg.dynamicVersionDetectionScatConfig;
|
|
567
|
+
}
|
|
568
|
+
// No cached config — discover versions and run dynamic selection.
|
|
569
|
+
let versions;
|
|
570
|
+
try {
|
|
571
|
+
versions = yield listAvailableVersions(bundler);
|
|
572
|
+
}
|
|
573
|
+
catch (e) {
|
|
574
|
+
console.log(chalk.yellow(`[!] Version detection: could not list versions (${String(e)})`));
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
577
|
+
if (versions.length === 0) {
|
|
578
|
+
console.log(chalk.yellow(`[!] Version detection: no version data found for ${bundler}`));
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
const threshold = Math.max(1, (_b = opts.detectVersionDynamicThreshold) !== null && _b !== void 0 ? _b : 3);
|
|
582
|
+
const selected = yield selectDynamicScatConfigs(bundler, versions, threshold);
|
|
583
|
+
if (selected.length > 0) {
|
|
584
|
+
saveDynamicVersionDetectionScatConfig(selected);
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
console.log(chalk.yellow(`[!] Version detection: dynamic selection found no valid scat configs`));
|
|
588
|
+
}
|
|
589
|
+
return selected;
|
|
590
|
+
}
|
|
591
|
+
// Static config: parse comma-separated scat categories and convert to bucket dir name.
|
|
592
|
+
const categories = detectConfig
|
|
593
|
+
.split(",")
|
|
594
|
+
.map((s) => s.trim())
|
|
595
|
+
.filter(Boolean);
|
|
596
|
+
const scatDir = scatToDir(categories);
|
|
597
|
+
if (!scatDir) {
|
|
598
|
+
console.log(chalk.red(`[!] --detect-version-config "${detectConfig}" produced an empty scat dir; check category names`));
|
|
599
|
+
process.exit(26);
|
|
600
|
+
}
|
|
601
|
+
// Validate by checking that reliable_signatures.json is non-empty for all versions.
|
|
602
|
+
let versions;
|
|
603
|
+
try {
|
|
604
|
+
versions = yield listAvailableVersions(bundler);
|
|
605
|
+
}
|
|
606
|
+
catch (e) {
|
|
607
|
+
console.log(chalk.yellow(`[!] Version detection: could not list versions to validate static config (${String(e)})`));
|
|
608
|
+
return [];
|
|
609
|
+
}
|
|
610
|
+
const valid = yield validateStaticScatConfig(bundler, versions, scatDir);
|
|
611
|
+
if (!valid) {
|
|
612
|
+
console.log(chalk.red(`[!] --detect-version-config "${detectConfig}" (scat dir: "${scatDir}") has empty reliable_signatures.json for one or more versions. Cannot use for version detection.`));
|
|
613
|
+
process.exit(26);
|
|
614
|
+
}
|
|
615
|
+
return [scatDir];
|
|
616
|
+
});
|
|
617
|
+
}
|
|
21
618
|
/**
|
|
22
619
|
* Refactors JavaScript code chunks based on technology-specific patterns.
|
|
23
620
|
*
|
|
@@ -31,20 +628,21 @@ const availableTechs = {
|
|
|
31
628
|
* @param list - Whether to list available technologies instead of running refactoring
|
|
32
629
|
* @returns Promise that resolves when refactoring is complete
|
|
33
630
|
*/
|
|
34
|
-
const refactor = (mappedJson, outputDir, tech, list) => __awaiter(void 0, void 0, void 0, function* () {
|
|
631
|
+
const refactor = (mappedJson, outputDir, tech, list, collisionsFile, remoteOpts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
632
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
35
633
|
console.log(chalk.cyan("[i] Loading refactor module..."));
|
|
36
|
-
// check if the file exists
|
|
37
|
-
if (!fs.existsSync(mappedJson)) {
|
|
38
|
-
console.error(chalk.red("[!] Mapped JSON file does not exist"));
|
|
39
|
-
process.exit(7);
|
|
40
|
-
}
|
|
41
634
|
if (list) {
|
|
42
635
|
console.log(chalk.cyan("[i] Listing available technologies"));
|
|
43
|
-
for (const
|
|
44
|
-
console.log(chalk.green(`- ${
|
|
636
|
+
for (const key of Object.keys(availableTechs)) {
|
|
637
|
+
console.log(chalk.green(`- ${key}: ${availableTechs[key]}`));
|
|
45
638
|
}
|
|
46
639
|
return;
|
|
47
640
|
}
|
|
641
|
+
// check if the file exists
|
|
642
|
+
if (!fs.existsSync(mappedJson)) {
|
|
643
|
+
console.error(chalk.red("[!] Mapped JSON file does not exist"));
|
|
644
|
+
process.exit(7);
|
|
645
|
+
}
|
|
48
646
|
// verify if the tech provided is valid
|
|
49
647
|
if (!Object.keys(availableTechs).includes(tech)) {
|
|
50
648
|
console.error(chalk.red("[!] Invalid technology provided"));
|
|
@@ -60,24 +658,372 @@ const refactor = (mappedJson, outputDir, tech, list) => __awaiter(void 0, void 0
|
|
|
60
658
|
}
|
|
61
659
|
// read the mapped JSON file
|
|
62
660
|
const chunks = JSON.parse(fs.readFileSync(mappedJson, "utf8"));
|
|
661
|
+
// Load CS-MAST cross-app baseline signatures.
|
|
662
|
+
// Priority: --collisions (local path) > remote HF (default) > none.
|
|
663
|
+
let libSigs;
|
|
664
|
+
let libStrippingSkipped = null;
|
|
665
|
+
if (collisionsFile) {
|
|
666
|
+
// Explicit local path — use existing resolver unchanged.
|
|
667
|
+
const result = buildLibSigs(collisionsFile, tech, remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.scat);
|
|
668
|
+
if (!result) {
|
|
669
|
+
console.log(chalk.red(`[!] Could not resolve library signatures from: ${collisionsFile}`));
|
|
670
|
+
const scat = BASELINE_SCAT_DIR[tech];
|
|
671
|
+
if (scat) {
|
|
672
|
+
console.log(chalk.red(` accepted layouts:\n` +
|
|
673
|
+
` <file> (direct collisions.json)\n` +
|
|
674
|
+
` <dir>/baselines/${tech}/${scat}/collisions.json\n` +
|
|
675
|
+
` <dir>/${tech}/${scat}/collisions.json\n` +
|
|
676
|
+
` <dir>/${scat}/collisions.json\n` +
|
|
677
|
+
` <dir>/collisions.json\n` +
|
|
678
|
+
` <dir>/<feature>/${scat}/collisions.json (per-feature results dir)`));
|
|
679
|
+
}
|
|
680
|
+
process.exit(10);
|
|
681
|
+
}
|
|
682
|
+
libSigs = result.sigs;
|
|
683
|
+
console.log(chalk.cyan(`[i] Loaded ${libSigs.size} library signatures from ${result.desc}`));
|
|
684
|
+
}
|
|
685
|
+
else if ((remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.remoteCollisions) || (!(remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.noRemote) && TECH_TO_BRANCH[tech])) {
|
|
686
|
+
// Load from remote HuggingFace dataset.
|
|
687
|
+
// --remote-collisions uses the explicit path; otherwise falls back to TECH_TO_BRANCH.
|
|
688
|
+
const opts = {
|
|
689
|
+
signatureQuality: (_a = remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.signatureQuality) !== null && _a !== void 0 ? _a : 100,
|
|
690
|
+
refreshCache: (_b = remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.refreshCache) !== null && _b !== void 0 ? _b : false,
|
|
691
|
+
skipCacheChecks: (_c = remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.skipCacheChecks) !== null && _c !== void 0 ? _c : false,
|
|
692
|
+
remoteCollisions: remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.remoteCollisions,
|
|
693
|
+
scat: remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.scat,
|
|
694
|
+
};
|
|
695
|
+
const result = yield loadRemoteLibSigs(tech, opts);
|
|
696
|
+
if (result) {
|
|
697
|
+
libSigs = result.sigs;
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
console.log(chalk.yellow(`[~] Remote signatures unavailable — proceeding without library stripping`));
|
|
701
|
+
const branch = (_e = (_d = opts.remoteCollisions) !== null && _d !== void 0 ? _d : TECH_TO_BRANCH[tech]) !== null && _e !== void 0 ? _e : "unknown";
|
|
702
|
+
const scatDir = opts.scat ? scatToDir(opts.scat) : ((_f = BASELINE_SCAT_DIR[tech]) !== null && _f !== void 0 ? _f : "unknown");
|
|
703
|
+
libStrippingSkipped = { tech, scat: scatDir, branch };
|
|
704
|
+
}
|
|
705
|
+
}
|
|
63
706
|
// iterate through the chunks
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
707
|
+
if (tech === "next-turbopack") {
|
|
708
|
+
for (const [, value] of Object.entries(chunks)) {
|
|
709
|
+
const moduleFiles = yield refactorNext(value);
|
|
710
|
+
for (const [moduleId, rawCode] of Object.entries(moduleFiles)) {
|
|
711
|
+
const formatted = yield prettier.format(rawCode, {
|
|
712
|
+
parser: "babel",
|
|
713
|
+
singleQuote: true,
|
|
714
|
+
trailingComma: "none",
|
|
715
|
+
});
|
|
716
|
+
// Skip writing empty files
|
|
717
|
+
if (formatted.trim().length === 0) {
|
|
718
|
+
console.log(chalk.gray(`[~] Module ${moduleId} is empty after stripping — skipping`));
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const filePath = `${outputDir}/${moduleId}.js`;
|
|
722
|
+
fs.writeFileSync(filePath, formatted);
|
|
723
|
+
console.log(chalk.green(`[✓] Module ${moduleId} written to ${filePath}`));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
else if (tech === "next-webpack") {
|
|
728
|
+
if (remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.scat) {
|
|
729
|
+
console.log(chalk.cyan(`[i] Using custom scat config: ${remoteOpts.scat.join(",")}`));
|
|
730
|
+
}
|
|
731
|
+
for (const [, value] of Object.entries(chunks)) {
|
|
732
|
+
const moduleFiles = yield refactorNextWebpack(value, libSigs, remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.scat);
|
|
733
|
+
for (const [moduleId, rawCode] of Object.entries(moduleFiles)) {
|
|
734
|
+
let formatted;
|
|
735
|
+
try {
|
|
736
|
+
formatted = yield prettier.format(rawCode, {
|
|
737
|
+
parser: "babel",
|
|
738
|
+
singleQuote: true,
|
|
739
|
+
trailingComma: "none",
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
catch (_j) {
|
|
743
|
+
formatted = rawCode;
|
|
744
|
+
}
|
|
745
|
+
if (formatted.trim().length === 0) {
|
|
746
|
+
console.log(chalk.gray(`[~] Module ${moduleId} is empty after stripping — skipping`));
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
const filePath = `${outputDir}/${moduleId}.js`;
|
|
750
|
+
fs.writeFileSync(filePath, formatted);
|
|
751
|
+
console.log(chalk.green(`[✓] Module ${moduleId} written to ${filePath}`));
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
else if (tech === "react-webpack") {
|
|
756
|
+
// Sort chunks so the main bundle (contains library module definitions) is processed
|
|
757
|
+
// first. Its libModuleMap is then available when processing lazy chunks that import
|
|
758
|
+
// those same library module IDs (e.g. 540=React, 848=jsx-runtime, 671=react-router-dom).
|
|
759
|
+
const sortedEntries = Object.entries(chunks).sort(([a], [b]) => {
|
|
760
|
+
const aIsLazy = /^\d+_/.test(a);
|
|
761
|
+
const bIsLazy = /^\d+_/.test(b);
|
|
762
|
+
if (!aIsLazy && bIsLazy)
|
|
763
|
+
return -1; // main bundle first
|
|
764
|
+
if (aIsLazy && !bIsLazy)
|
|
765
|
+
return 1;
|
|
766
|
+
return 0;
|
|
77
767
|
});
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
768
|
+
// Accumulated library module map — grows as main bundles are processed.
|
|
769
|
+
// Passed to each subsequent chunk so lazy chunks can rewrite library imports.
|
|
770
|
+
const accLibModuleMap = new Map();
|
|
771
|
+
// Pre-scan vendor chunks from the assets directory that are not in mapped.json.
|
|
772
|
+
// These files (e.g. vendor-router.*.js, vendor-react-dom.*.js) contain library
|
|
773
|
+
// modules whose export maps must be classified before processing application chunks.
|
|
774
|
+
const assetsDir = findAssetsDir(chunks, mappedJson);
|
|
775
|
+
if (assetsDir) {
|
|
776
|
+
const vendorFiles = findVendorChunkFiles(chunks, assetsDir);
|
|
777
|
+
for (const vendorFile of vendorFiles) {
|
|
778
|
+
console.log(chalk.cyan(`[i] Pre-scanning vendor chunk: ${path.basename(vendorFile)}`));
|
|
779
|
+
const vendorCode = fs.readFileSync(vendorFile, "utf8");
|
|
780
|
+
const vendorChunk = {
|
|
781
|
+
id: path.basename(vendorFile, ".js"),
|
|
782
|
+
description: "",
|
|
783
|
+
loadedOn: [],
|
|
784
|
+
containsFetch: false,
|
|
785
|
+
isAxiosLibrary: false,
|
|
786
|
+
exports: [],
|
|
787
|
+
callStack: [],
|
|
788
|
+
code: vendorCode,
|
|
789
|
+
imports: [],
|
|
790
|
+
file: vendorFile,
|
|
791
|
+
};
|
|
792
|
+
const vendorResult = yield refactorReact(vendorChunk, undefined, undefined, true);
|
|
793
|
+
for (const [id, info] of vendorResult.libModuleMap) {
|
|
794
|
+
accLibModuleMap.set(id, info);
|
|
795
|
+
console.log(chalk.gray(` [-] Vendor module ${id} → ${info.type} (${info.exportMap.size} exports)`));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.scat) {
|
|
800
|
+
console.log(chalk.cyan(`[i] Using custom scat config: ${remoteOpts.scat.join(",")}`));
|
|
801
|
+
}
|
|
802
|
+
const writtenFiles = [];
|
|
803
|
+
for (const [, value] of sortedEntries) {
|
|
804
|
+
const result = yield refactorReact(value, libSigs, accLibModuleMap, false, remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.scat);
|
|
805
|
+
// Merge this chunk's library classifications into the accumulator.
|
|
806
|
+
for (const [id, info] of result.libModuleMap) {
|
|
807
|
+
accLibModuleMap.set(id, info);
|
|
808
|
+
}
|
|
809
|
+
for (const [moduleId, rawCode] of Object.entries(result.files)) {
|
|
810
|
+
const formatted = yield prettier.format(rawCode, {
|
|
811
|
+
parser: "babel",
|
|
812
|
+
singleQuote: true,
|
|
813
|
+
trailingComma: "none",
|
|
814
|
+
});
|
|
815
|
+
// Skip writing empty files (e.g. index.js after webpack runtime stripping).
|
|
816
|
+
if (formatted.trim().length === 0) {
|
|
817
|
+
console.log(chalk.gray(`[~] Module ${moduleId} is empty after stripping — skipping`));
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
const filePath = `${outputDir}/${moduleId}.js`;
|
|
821
|
+
fs.writeFileSync(filePath, formatted);
|
|
822
|
+
writtenFiles.push(filePath);
|
|
823
|
+
console.log(chalk.green(`[✓] Module ${moduleId} written to ${outputDir}/${moduleId}.js`));
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
// Version detection — run before build check so the detected version can be
|
|
827
|
+
// written into package.json by runBuildCheck.
|
|
828
|
+
let detectedVersion;
|
|
829
|
+
if ((remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.detectVersion) && VERSION_TECH_TO_BUNDLER[tech]) {
|
|
830
|
+
const bundler = VERSION_TECH_TO_BUNDLER[tech];
|
|
831
|
+
const vScatDirs = yield resolveVersionDetectionScatDirs(bundler, remoteOpts);
|
|
832
|
+
if (vScatDirs.length > 0) {
|
|
833
|
+
// Include vendor chunk file contents (e.g. vendor-react-dom.*.js) that hold
|
|
834
|
+
// React library code but are not present in mapped.json. Without these, the
|
|
835
|
+
// signature set contains only app-level code and misses version-specific React
|
|
836
|
+
// library signatures.
|
|
837
|
+
const vendorCodes = [];
|
|
838
|
+
const vDetectAssetsDir = findAssetsDir(chunks, mappedJson);
|
|
839
|
+
if (vDetectAssetsDir) {
|
|
840
|
+
for (const vendorFile of findVendorChunkFiles(chunks, vDetectAssetsDir)) {
|
|
841
|
+
try {
|
|
842
|
+
vendorCodes.push(fs.readFileSync(vendorFile, "utf8"));
|
|
843
|
+
}
|
|
844
|
+
catch (_k) {
|
|
845
|
+
// non-fatal
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
const vResult = yield detectReactVersion(chunks, tech, vScatDirs, vendorCodes.length > 0 ? vendorCodes : undefined);
|
|
850
|
+
if (vResult) {
|
|
851
|
+
console.log(chalk.green(`[✓] Detected React version: ${vResult.versionKey} (${vResult.matchCount} signature matches)`));
|
|
852
|
+
detectedVersion = vResult;
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
console.log(chalk.yellow(`[!] Version detection: no matching version found`));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
// Build check — scaffold a minimal webpack project and verify the refactored
|
|
860
|
+
// code compiles.
|
|
861
|
+
runBuildCheck(outputDir, writtenFiles, detectedVersion);
|
|
862
|
+
}
|
|
863
|
+
else if (tech === "react-vite") {
|
|
864
|
+
// Pre-scan vendor chunks from the assets directory that are not in mapped.json.
|
|
865
|
+
// Vendor chunks (vendor-react-*.js) are referenced by app chunks but are typically
|
|
866
|
+
// absent from mapped.json. Without them, vendorExportMaps is empty and vendor
|
|
867
|
+
// import statements are never rewritten — leaving unresolvable references in output.
|
|
868
|
+
const vitAssetsDir = findAssetsDir(chunks, mappedJson);
|
|
869
|
+
if (vitAssetsDir) {
|
|
870
|
+
const missingFiles = findVendorChunkFiles(chunks, vitAssetsDir);
|
|
871
|
+
for (const vendorFile of missingFiles) {
|
|
872
|
+
const basename = path.basename(vendorFile);
|
|
873
|
+
if (!/vendor[-_]react/i.test(basename) && !/rolldown[-_]runtime/i.test(basename))
|
|
874
|
+
continue;
|
|
875
|
+
console.log(chalk.cyan(`[i] Including missing vendor chunk: ${basename}`));
|
|
876
|
+
const key = basename.replace(/\.js$/, "");
|
|
877
|
+
chunks[key] = {
|
|
878
|
+
id: basename,
|
|
879
|
+
description: "",
|
|
880
|
+
loadedOn: [],
|
|
881
|
+
containsFetch: false,
|
|
882
|
+
isAxiosLibrary: false,
|
|
883
|
+
exports: [],
|
|
884
|
+
callStack: [],
|
|
885
|
+
code: fs.readFileSync(vendorFile, "utf8"),
|
|
886
|
+
imports: [],
|
|
887
|
+
file: `assets/${basename}`,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const viteFiles = yield refactorVite(chunks, libSigs, remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.scat);
|
|
892
|
+
// Group chunk outputs by their output file path before writing.
|
|
893
|
+
// Multiple mapped.json sub-chunks can originate from the same Vite source
|
|
894
|
+
// file (one per top-level function detected by `map`). Writing each
|
|
895
|
+
// sub-chunk independently would overwrite previous writes so only the last
|
|
896
|
+
// sub-chunk would survive. Grouping and concatenating avoids the overwrite:
|
|
897
|
+
// all sub-chunks for the same file are merged into one formatted output.
|
|
898
|
+
const fileGroups = new Map();
|
|
899
|
+
for (const [chunkKey, rawCode] of Object.entries(viteFiles)) {
|
|
900
|
+
// Use the original chunk basename so dynamic imports (which reference
|
|
901
|
+
// the original Vite filenames) resolve correctly in the build check.
|
|
902
|
+
const chunkInfo = chunks[chunkKey];
|
|
903
|
+
const originalFile = chunkInfo ? ((_g = chunkInfo.file) !== null && _g !== void 0 ? _g : chunkInfo.id) : null;
|
|
904
|
+
const outputBasename = originalFile
|
|
905
|
+
? path.basename(originalFile)
|
|
906
|
+
: chunkKey.replace(/[/\\]/g, "_").replace(/\.js$/, "") + ".js";
|
|
907
|
+
const filePath = `${outputDir}/${outputBasename}`;
|
|
908
|
+
if (!fileGroups.has(filePath)) {
|
|
909
|
+
fileGroups.set(filePath, { codes: [], keys: [] });
|
|
910
|
+
}
|
|
911
|
+
fileGroups.get(filePath).codes.push(rawCode);
|
|
912
|
+
fileGroups.get(filePath).keys.push(chunkKey);
|
|
913
|
+
}
|
|
914
|
+
const writtenFiles = [];
|
|
915
|
+
for (const [filePath, { codes, keys }] of fileGroups) {
|
|
916
|
+
// Put import-bearing code parts first so ESM import statements end up
|
|
917
|
+
// at the top of the concatenated file. Helper sub-chunks typically have
|
|
918
|
+
// no imports; the component sub-chunk that has peer imports (e.g.
|
|
919
|
+
// from ./shared-*.js) must come first or the combined file is invalid.
|
|
920
|
+
const importBearing = codes.filter((c) => /^import\s/m.test(c));
|
|
921
|
+
const rest = codes.filter((c) => !/^import\s/m.test(c));
|
|
922
|
+
const combined = [...importBearing, ...rest].join("\n\n");
|
|
923
|
+
let formatted;
|
|
924
|
+
try {
|
|
925
|
+
formatted = yield prettier.format(combined, {
|
|
926
|
+
parser: "babel",
|
|
927
|
+
singleQuote: true,
|
|
928
|
+
trailingComma: "none",
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
catch (_l) {
|
|
932
|
+
formatted = combined;
|
|
933
|
+
}
|
|
934
|
+
if (formatted.trim().length === 0) {
|
|
935
|
+
console.log(chalk.gray(`[~] Chunk ${keys.join("+")} is empty after refactoring — skipping`));
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
fs.writeFileSync(filePath, formatted);
|
|
939
|
+
// Push exactly once per output file — prevents duplicate entries in
|
|
940
|
+
// writtenFiles that would cause the rename loop in runViteBuildCheck
|
|
941
|
+
// to attempt renaming the same path twice (ENOENT on the second try).
|
|
942
|
+
writtenFiles.push(filePath);
|
|
943
|
+
for (const key of keys) {
|
|
944
|
+
console.log(chalk.green(`[✓] Chunk ${key} written to ${filePath}`));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
// Version detection — run before build check so the detected version can be
|
|
948
|
+
// written into package.json by runViteBuildCheck.
|
|
949
|
+
let viteDetectedVersion;
|
|
950
|
+
if ((remoteOpts === null || remoteOpts === void 0 ? void 0 : remoteOpts.detectVersion) && VERSION_TECH_TO_BUNDLER[tech]) {
|
|
951
|
+
const bundler = VERSION_TECH_TO_BUNDLER[tech];
|
|
952
|
+
const vScatDirs = yield resolveVersionDetectionScatDirs(bundler, remoteOpts);
|
|
953
|
+
if (vScatDirs.length > 0) {
|
|
954
|
+
const vResult = yield detectReactVersion(chunks, tech, vScatDirs);
|
|
955
|
+
if (vResult) {
|
|
956
|
+
console.log(chalk.green(`[✓] Detected React version: ${vResult.versionKey} (${vResult.matchCount} signature matches)`));
|
|
957
|
+
viteDetectedVersion = vResult;
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
console.log(chalk.yellow(`[!] Version detection: no matching version found`));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// Build check with Vite scaffold
|
|
965
|
+
runViteBuildCheck(outputDir, writtenFiles, viteDetectedVersion);
|
|
966
|
+
}
|
|
967
|
+
else if (tech === "vue-webpack") {
|
|
968
|
+
for (const [, value] of Object.entries(chunks)) {
|
|
969
|
+
const moduleFiles = yield refactorVueWebpack(value);
|
|
970
|
+
for (const [moduleId, rawCode] of Object.entries(moduleFiles)) {
|
|
971
|
+
let formatted;
|
|
972
|
+
try {
|
|
973
|
+
formatted = yield prettier.format(rawCode, {
|
|
974
|
+
parser: "babel",
|
|
975
|
+
singleQuote: true,
|
|
976
|
+
trailingComma: "none",
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
catch (_m) {
|
|
980
|
+
formatted = rawCode;
|
|
981
|
+
}
|
|
982
|
+
if (formatted.trim().length === 0) {
|
|
983
|
+
console.log(chalk.gray(`[~] Module ${moduleId} is empty after stripping — skipping`));
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
const filePath = `${outputDir}/${moduleId}.js`;
|
|
987
|
+
fs.writeFileSync(filePath, formatted);
|
|
988
|
+
console.log(chalk.green(`[✓] Module ${moduleId} written to ${filePath}`));
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
else if (tech === "vue-vite") {
|
|
993
|
+
const viteFiles = yield refactorVueVite(chunks);
|
|
994
|
+
for (const [chunkKey, rawCode] of Object.entries(viteFiles)) {
|
|
995
|
+
const chunkInfo = chunks[chunkKey];
|
|
996
|
+
const originalFile = chunkInfo ? ((_h = chunkInfo.file) !== null && _h !== void 0 ? _h : chunkInfo.id) : null;
|
|
997
|
+
const outputBasename = originalFile
|
|
998
|
+
? path.basename(originalFile)
|
|
999
|
+
: chunkKey.replace(/[/\\]/g, "_").replace(/\.js$/, "") + ".js";
|
|
1000
|
+
const filePath = `${outputDir}/${outputBasename}`;
|
|
1001
|
+
let formatted;
|
|
1002
|
+
try {
|
|
1003
|
+
formatted = yield prettier.format(rawCode, {
|
|
1004
|
+
parser: "babel",
|
|
1005
|
+
singleQuote: true,
|
|
1006
|
+
trailingComma: "none",
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
catch (_o) {
|
|
1010
|
+
formatted = rawCode;
|
|
1011
|
+
}
|
|
1012
|
+
if (formatted.trim().length === 0) {
|
|
1013
|
+
console.log(chalk.gray(`[~] Chunk ${chunkKey} is empty after refactoring — skipping`));
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
fs.writeFileSync(filePath, formatted);
|
|
1017
|
+
console.log(chalk.green(`[✓] Chunk ${chunkKey} written to ${filePath}`));
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (libStrippingSkipped) {
|
|
1021
|
+
const { tech: skippedTech, scat, branch } = libStrippingSkipped;
|
|
1022
|
+
console.log(chalk.yellow.bold("=".repeat(60)));
|
|
1023
|
+
console.log(chalk.yellow.bold("[!] LIBRARY STRIPPING WAS SKIPPED FOR THIS RUN"));
|
|
1024
|
+
console.log(chalk.yellow(` tech: ${skippedTech} scat: ${scat} branch: ${branch}`));
|
|
1025
|
+
console.log(chalk.yellow(" No remote collisions data available — output includes library code."));
|
|
1026
|
+
console.log(chalk.yellow.bold("=".repeat(60)));
|
|
81
1027
|
}
|
|
82
1028
|
console.log(chalk.green("[✓] Refactoring complete."));
|
|
83
1029
|
});
|