@fluentui/react-compiler-analyzer 0.0.0-experimental.rc-analyzer.20260430-fc3bb2e193.0
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/CHANGELOG.md +13 -0
- package/README.md +185 -0
- package/bin/react-compiler-analyzer.js +10 -0
- package/dist/README.md +185 -0
- package/dist/package.json +24 -0
- package/dist/src/analyzer.d.ts +9 -0
- package/dist/src/analyzer.js +271 -0
- package/dist/src/analyzer.js.map +1 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +19 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/commands/coverage.d.ts +13 -0
- package/dist/src/commands/coverage.js +65 -0
- package/dist/src/commands/coverage.js.map +1 -0
- package/dist/src/commands/directives.d.ts +11 -0
- package/dist/src/commands/directives.js +60 -0
- package/dist/src/commands/directives.js.map +1 -0
- package/dist/src/commands/shared.d.ts +18 -0
- package/dist/src/commands/shared.js +79 -0
- package/dist/src/commands/shared.js.map +1 -0
- package/dist/src/compiler-utils.d.ts +29 -0
- package/dist/src/compiler-utils.js +29 -0
- package/dist/src/compiler-utils.js.map +1 -0
- package/dist/src/coverage-analyzer.d.ts +9 -0
- package/dist/src/coverage-analyzer.js +177 -0
- package/dist/src/coverage-analyzer.js.map +1 -0
- package/dist/src/coverage-fixer.d.ts +12 -0
- package/dist/src/coverage-fixer.js +71 -0
- package/dist/src/coverage-fixer.js.map +1 -0
- package/dist/src/coverage-reporter.d.ts +16 -0
- package/dist/src/coverage-reporter.js +167 -0
- package/dist/src/coverage-reporter.js.map +1 -0
- package/dist/src/discovery.d.ts +14 -0
- package/dist/src/discovery.js +70 -0
- package/dist/src/discovery.js.map +1 -0
- package/dist/src/fixer.d.ts +9 -0
- package/dist/src/fixer.js +79 -0
- package/dist/src/fixer.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/manual-memo-plugin.d.ts +16 -0
- package/dist/src/manual-memo-plugin.js +82 -0
- package/dist/src/manual-memo-plugin.js.map +1 -0
- package/dist/src/reporter.d.ts +9 -0
- package/dist/src/reporter.js +117 -0
- package/dist/src/reporter.js.map +1 -0
- package/dist/src/types.d.ts +70 -0
- package/dist/src/types.js +7 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +24 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: all[name]
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
discoverAllFiles: function() {
|
|
13
|
+
return discoverAllFiles;
|
|
14
|
+
},
|
|
15
|
+
discoverDirectiveFiles: function() {
|
|
16
|
+
return discoverDirectiveFiles;
|
|
17
|
+
},
|
|
18
|
+
findPackageName: function() {
|
|
19
|
+
return findPackageName;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const _nodefs = require("node:fs");
|
|
23
|
+
const _promises = require("node:fs/promises");
|
|
24
|
+
const _nodepath = require("node:path");
|
|
25
|
+
const USE_NO_MEMO_RE = /['(]use no memo[')]/;
|
|
26
|
+
async function findPackageName(startDir) {
|
|
27
|
+
let dir = (0, _nodepath.resolve)(startDir);
|
|
28
|
+
const root = (0, _nodepath.resolve)('/');
|
|
29
|
+
while(dir !== root){
|
|
30
|
+
const pkgJsonPath = (0, _nodepath.join)(dir, 'package.json');
|
|
31
|
+
if ((0, _nodefs.existsSync)(pkgJsonPath)) try {
|
|
32
|
+
const content = await (0, _promises.readFile)(pkgJsonPath, 'utf-8');
|
|
33
|
+
const pkg = JSON.parse(content);
|
|
34
|
+
if (typeof pkg.name === 'string') return pkg.name;
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore parse errors, keep walking
|
|
37
|
+
}
|
|
38
|
+
dir = (0, _nodepath.dirname)(dir);
|
|
39
|
+
}
|
|
40
|
+
return (0, _nodepath.basename)(startDir);
|
|
41
|
+
}
|
|
42
|
+
async function discoverDirectiveFiles(scanDir, packageName, exclude, verbose) {
|
|
43
|
+
const files = [];
|
|
44
|
+
const tsFiles = (0, _nodefs.globSync)('**/*.{ts,tsx}', {
|
|
45
|
+
cwd: scanDir,
|
|
46
|
+
exclude
|
|
47
|
+
}).map((relative)=>(0, _nodepath.join)(scanDir, relative));
|
|
48
|
+
for (const filePath of tsFiles){
|
|
49
|
+
const content = await (0, _promises.readFile)(filePath, 'utf-8');
|
|
50
|
+
if (USE_NO_MEMO_RE.test(content)) files.push({
|
|
51
|
+
filePath,
|
|
52
|
+
packageName
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (verbose && files.length === 0) console.log(` No 'use no memo' files found in ${scanDir}`);
|
|
56
|
+
return files;
|
|
57
|
+
}
|
|
58
|
+
async function discoverAllFiles(scanDir, packageName, exclude, verbose) {
|
|
59
|
+
const tsFiles = (0, _nodefs.globSync)('**/*.{ts,tsx}', {
|
|
60
|
+
cwd: scanDir,
|
|
61
|
+
exclude
|
|
62
|
+
}).map((relative)=>(0, _nodepath.join)(scanDir, relative));
|
|
63
|
+
if (verbose) console.log(` Found ${tsFiles.length} TypeScript files in ${scanDir}`);
|
|
64
|
+
return tsFiles.map((filePath)=>({
|
|
65
|
+
filePath,
|
|
66
|
+
packageName
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/discovery.ts"],"sourcesContent":["import { existsSync, globSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { basename, dirname, join, resolve } from 'node:path';\n\nimport type { FileEntry } from './types';\n\nconst USE_NO_MEMO_RE = /['(]use no memo[')]/;\n\n/**\n * Walk up from `startDir` to find the nearest package.json and return its `name` field.\n * Falls back to the basename of `startDir`.\n */\nexport async function findPackageName(startDir: string): Promise<string> {\n let dir = resolve(startDir);\n const root = resolve('/');\n\n while (dir !== root) {\n const pkgJsonPath = join(dir, 'package.json');\n if (existsSync(pkgJsonPath)) {\n try {\n const content = await readFile(pkgJsonPath, 'utf-8');\n const pkg = JSON.parse(content);\n if (typeof pkg.name === 'string') {\n return pkg.name;\n }\n } catch {\n // ignore parse errors, keep walking\n }\n }\n dir = dirname(dir);\n }\n\n return basename(startDir);\n}\n\n/**\n * Discover files containing 'use no memo' in the given directory.\n */\nexport async function discoverDirectiveFiles(\n scanDir: string,\n packageName: string,\n exclude: string[],\n verbose: boolean,\n): Promise<FileEntry[]> {\n const files: FileEntry[] = [];\n\n const tsFiles = globSync('**/*.{ts,tsx}', {\n cwd: scanDir,\n exclude,\n }).map(relative => join(scanDir, relative));\n\n for (const filePath of tsFiles) {\n const content = await readFile(filePath, 'utf-8');\n if (USE_NO_MEMO_RE.test(content)) {\n files.push({ filePath, packageName });\n }\n }\n\n if (verbose && files.length === 0) {\n console.log(` No 'use no memo' files found in ${scanDir}`);\n }\n\n return files;\n}\n\n/**\n * Discover all TypeScript files in the given directory (for coverage analysis).\n */\nexport async function discoverAllFiles(\n scanDir: string,\n packageName: string,\n exclude: string[],\n verbose: boolean,\n): Promise<FileEntry[]> {\n const tsFiles = globSync('**/*.{ts,tsx}', {\n cwd: scanDir,\n exclude,\n }).map(relative => join(scanDir, relative));\n\n if (verbose) {\n console.log(` Found ${tsFiles.length} TypeScript files in ${scanDir}`);\n }\n\n return tsFiles.map(filePath => ({ filePath, packageName }));\n}\n"],"names":["discoverAllFiles","discoverDirectiveFiles","findPackageName","USE_NO_MEMO_RE","startDir","dir","resolve","root","pkgJsonPath","join","existsSync","content","readFile","pkg","JSON","parse","name","dirname","basename","scanDir","packageName","exclude","verbose","files","tsFiles","globSync","cwd","map","relative","filePath","test","push","length","console","log"],"mappings":";;;;;;;;;;;IAoEsBA,gBAAgB;eAAhBA;;IA9BAC,sBAAsB;eAAtBA;;IA1BAC,eAAe;eAAfA;;;wBAZe;0BACZ;0BACwB;AAIjD,MAAMC,iBAAiB;AAMhB,eAAeD,gBAAgBE,QAAgB;IACpD,IAAIC,MAAMC,IAAAA,iBAAO,EAACF;IAClB,MAAMG,OAAOD,IAAAA,iBAAO,EAAC;IAErB,MAAOD,QAAQE,KAAM;QACnB,MAAMC,cAAcC,IAAAA,cAAI,EAACJ,KAAK;QAC9B,IAAIK,IAAAA,kBAAU,EAACF,cACb,IAAI;YACF,MAAMG,UAAU,MAAMC,IAAAA,kBAAQ,EAACJ,aAAa;YAC5C,MAAMK,MAAMC,KAAKC,KAAK,CAACJ;YACvB,IAAI,OAAOE,IAAIG,IAAI,KAAK,UACtB,OAAOH,IAAIG,IAAI;QAEnB,EAAE,OAAM;QACN,oCAAoC;QACtC;QAEFX,MAAMY,IAAAA,iBAAO,EAACZ;IAChB;IAEA,OAAOa,IAAAA,kBAAQ,EAACd;AAClB;AAKO,eAAeH,uBACpBkB,OAAe,EACfC,WAAmB,EACnBC,OAAiB,EACjBC,OAAgB;IAEhB,MAAMC,QAAqB,EAAE;IAE7B,MAAMC,UAAUC,IAAAA,gBAAQ,EAAC,iBAAiB;QACxCC,KAAKP;QACLE;IACF,GAAGM,GAAG,CAACC,CAAAA,WAAYnB,IAAAA,cAAI,EAACU,SAASS;IAEjC,KAAK,MAAMC,YAAYL,QAAS;QAC9B,MAAMb,UAAU,MAAMC,IAAAA,kBAAQ,EAACiB,UAAU;QACzC,IAAI1B,eAAe2B,IAAI,CAACnB,UACtBY,MAAMQ,IAAI,CAAC;YAAEF;YAAUT;QAAY;IAEvC;IAEA,IAAIE,WAAWC,MAAMS,MAAM,KAAK,GAC9BC,QAAQC,GAAG,CAAC,CAAC,kCAAkC,EAAEf,SAAS;IAG5D,OAAOI;AACT;AAKO,eAAevB,iBACpBmB,OAAe,EACfC,WAAmB,EACnBC,OAAiB,EACjBC,OAAgB;IAEhB,MAAME,UAAUC,IAAAA,gBAAQ,EAAC,iBAAiB;QACxCC,KAAKP;QACLE;IACF,GAAGM,GAAG,CAACC,CAAAA,WAAYnB,IAAAA,cAAI,EAACU,SAASS;IAEjC,IAAIN,SACFW,QAAQC,GAAG,CAAC,CAAC,QAAQ,EAAEV,QAAQQ,MAAM,CAAC,qBAAqB,EAAEb,SAAS;IAGxE,OAAOK,QAAQG,GAAG,CAACE,CAAAA,WAAa,CAAA;YAAEA;YAAUT;QAAY,CAAA;AAC1D"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DirectiveAnalysis, FixResult } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Auto-fix directives in source files:
|
|
4
|
+
* - Remove redundant 'use no memo' directives
|
|
5
|
+
* - Annotate active directives with `// justified: <reason>`
|
|
6
|
+
*
|
|
7
|
+
* Processes changes bottom-to-top within each file to avoid offset shifts.
|
|
8
|
+
*/
|
|
9
|
+
export declare function applyFixes(results: DirectiveAnalysis[]): Promise<FixResult>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "applyFixes", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return applyFixes;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _promises = require("node:fs/promises");
|
|
12
|
+
async function applyFixes(results) {
|
|
13
|
+
const actionable = results.filter((r)=>r.status === 'redundant' || r.status === 'active');
|
|
14
|
+
if (actionable.length === 0) return {
|
|
15
|
+
filesModified: 0,
|
|
16
|
+
directivesRemoved: 0,
|
|
17
|
+
directivesJustified: 0
|
|
18
|
+
};
|
|
19
|
+
// Group actions by file
|
|
20
|
+
const byFile = new Map();
|
|
21
|
+
for (const r of actionable){
|
|
22
|
+
const actions = byFile.get(r.filePath) ?? [];
|
|
23
|
+
if (r.status === 'redundant') actions.push({
|
|
24
|
+
kind: 'remove',
|
|
25
|
+
line: r.line
|
|
26
|
+
});
|
|
27
|
+
else if (r.status === 'active') actions.push({
|
|
28
|
+
kind: 'justify',
|
|
29
|
+
line: r.line,
|
|
30
|
+
reason: buildJustification(r)
|
|
31
|
+
});
|
|
32
|
+
byFile.set(r.filePath, actions);
|
|
33
|
+
}
|
|
34
|
+
let filesModified = 0;
|
|
35
|
+
let directivesRemoved = 0;
|
|
36
|
+
let directivesJustified = 0;
|
|
37
|
+
for (const [filePath, actions] of byFile){
|
|
38
|
+
const source = await (0, _promises.readFile)(filePath, 'utf-8');
|
|
39
|
+
const lines = source.split('\n');
|
|
40
|
+
// Sort descending so we process from bottom-to-top
|
|
41
|
+
const sorted = [
|
|
42
|
+
...actions
|
|
43
|
+
].sort((a, b)=>b.line - a.line);
|
|
44
|
+
for (const action of sorted){
|
|
45
|
+
const idx = action.line - 1; // 0-based index
|
|
46
|
+
if (idx < 0 || idx >= lines.length) continue;
|
|
47
|
+
if (action.kind === 'remove') {
|
|
48
|
+
lines.splice(idx, 1);
|
|
49
|
+
directivesRemoved++;
|
|
50
|
+
} else {
|
|
51
|
+
// Append justification comment to the directive line
|
|
52
|
+
const currentLine = lines[idx];
|
|
53
|
+
// Strip any existing trailing comment (shouldn't have one, but be safe)
|
|
54
|
+
const withoutTrailingComment = currentLine.replace(/\s*\/\/.*$/, '');
|
|
55
|
+
// Ensure semicolon before the comment
|
|
56
|
+
const base = withoutTrailingComment.trimEnd().endsWith(';') ? withoutTrailingComment.trimEnd() : withoutTrailingComment.trimEnd() + ';';
|
|
57
|
+
lines[idx] = `${base} // justified: ${action.reason}`;
|
|
58
|
+
directivesJustified++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
await (0, _promises.writeFile)(filePath, lines.join('\n'), 'utf-8');
|
|
62
|
+
filesModified++;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
filesModified,
|
|
66
|
+
directivesRemoved,
|
|
67
|
+
directivesJustified
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build a concise justification string for an active directive.
|
|
72
|
+
* Summarizes why the compiler would optimize the function (and thus
|
|
73
|
+
* why the directive is intentionally keeping it unoptimized).
|
|
74
|
+
*/ function buildJustification(r) {
|
|
75
|
+
const fnName = r.functionName ?? 'unknown function';
|
|
76
|
+
return `compiler would optimize ${fnName} — manual opt-out to preserve runtime behavior`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=fixer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/fixer.ts"],"sourcesContent":["import { readFile, writeFile } from 'node:fs/promises';\n\nimport type { DirectiveAnalysis, FixResult } from './types';\n\ntype LineAction = { kind: 'remove'; line: number } | { kind: 'justify'; line: number; reason: string };\n\n/**\n * Auto-fix directives in source files:\n * - Remove redundant 'use no memo' directives\n * - Annotate active directives with `// justified: <reason>`\n *\n * Processes changes bottom-to-top within each file to avoid offset shifts.\n */\nexport async function applyFixes(results: DirectiveAnalysis[]): Promise<FixResult> {\n const actionable = results.filter(r => r.status === 'redundant' || r.status === 'active');\n if (actionable.length === 0) {\n return { filesModified: 0, directivesRemoved: 0, directivesJustified: 0 };\n }\n\n // Group actions by file\n const byFile = new Map<string, LineAction[]>();\n for (const r of actionable) {\n const actions = byFile.get(r.filePath) ?? [];\n if (r.status === 'redundant') {\n actions.push({ kind: 'remove', line: r.line });\n } else if (r.status === 'active') {\n actions.push({ kind: 'justify', line: r.line, reason: buildJustification(r) });\n }\n byFile.set(r.filePath, actions);\n }\n\n let filesModified = 0;\n let directivesRemoved = 0;\n let directivesJustified = 0;\n\n for (const [filePath, actions] of byFile) {\n const source = await readFile(filePath, 'utf-8');\n const lines = source.split('\\n');\n\n // Sort descending so we process from bottom-to-top\n const sorted = [...actions].sort((a, b) => b.line - a.line);\n\n for (const action of sorted) {\n const idx = action.line - 1; // 0-based index\n if (idx < 0 || idx >= lines.length) {\n continue;\n }\n\n if (action.kind === 'remove') {\n lines.splice(idx, 1);\n directivesRemoved++;\n } else {\n // Append justification comment to the directive line\n const currentLine = lines[idx];\n // Strip any existing trailing comment (shouldn't have one, but be safe)\n const withoutTrailingComment = currentLine.replace(/\\s*\\/\\/.*$/, '');\n // Ensure semicolon before the comment\n const base = withoutTrailingComment.trimEnd().endsWith(';')\n ? withoutTrailingComment.trimEnd()\n : withoutTrailingComment.trimEnd() + ';';\n lines[idx] = `${base} // justified: ${action.reason}`;\n directivesJustified++;\n }\n }\n\n await writeFile(filePath, lines.join('\\n'), 'utf-8');\n filesModified++;\n }\n\n return { filesModified, directivesRemoved, directivesJustified };\n}\n\n/**\n * Build a concise justification string for an active directive.\n * Summarizes why the compiler would optimize the function (and thus\n * why the directive is intentionally keeping it unoptimized).\n */\nfunction buildJustification(r: DirectiveAnalysis): string {\n const fnName = r.functionName ?? 'unknown function';\n return `compiler would optimize ${fnName} — manual opt-out to preserve runtime behavior`;\n}\n"],"names":["applyFixes","results","actionable","filter","r","status","length","filesModified","directivesRemoved","directivesJustified","byFile","Map","actions","get","filePath","push","kind","line","reason","buildJustification","set","source","readFile","lines","split","sorted","sort","a","b","action","idx","splice","currentLine","withoutTrailingComment","replace","base","trimEnd","endsWith","writeFile","join","fnName","functionName"],"mappings":";;;;+BAasBA;;;eAAAA;;;0BAbc;AAa7B,eAAeA,WAAWC,OAA4B;IAC3D,MAAMC,aAAaD,QAAQE,MAAM,CAACC,CAAAA,IAAKA,EAAEC,MAAM,KAAK,eAAeD,EAAEC,MAAM,KAAK;IAChF,IAAIH,WAAWI,MAAM,KAAK,GACxB,OAAO;QAAEC,eAAe;QAAGC,mBAAmB;QAAGC,qBAAqB;IAAE;IAG1E,wBAAwB;IACxB,MAAMC,SAAS,IAAIC;IACnB,KAAK,MAAMP,KAAKF,WAAY;QAC1B,MAAMU,UAAUF,OAAOG,GAAG,CAACT,EAAEU,QAAQ,KAAK,EAAE;QAC5C,IAAIV,EAAEC,MAAM,KAAK,aACfO,QAAQG,IAAI,CAAC;YAAEC,MAAM;YAAUC,MAAMb,EAAEa,IAAI;QAAC;aACvC,IAAIb,EAAEC,MAAM,KAAK,UACtBO,QAAQG,IAAI,CAAC;YAAEC,MAAM;YAAWC,MAAMb,EAAEa,IAAI;YAAEC,QAAQC,mBAAmBf;QAAG;QAE9EM,OAAOU,GAAG,CAAChB,EAAEU,QAAQ,EAAEF;IACzB;IAEA,IAAIL,gBAAgB;IACpB,IAAIC,oBAAoB;IACxB,IAAIC,sBAAsB;IAE1B,KAAK,MAAM,CAACK,UAAUF,QAAQ,IAAIF,OAAQ;QACxC,MAAMW,SAAS,MAAMC,IAAAA,kBAAQ,EAACR,UAAU;QACxC,MAAMS,QAAQF,OAAOG,KAAK,CAAC;QAE3B,mDAAmD;QACnD,MAAMC,SAAS;eAAIb;SAAQ,CAACc,IAAI,CAAC,CAACC,GAAGC,IAAMA,EAAEX,IAAI,GAAGU,EAAEV,IAAI;QAE1D,KAAK,MAAMY,UAAUJ,OAAQ;YAC3B,MAAMK,MAAMD,OAAOZ,IAAI,GAAG,GAAG,gBAAgB;YAC7C,IAAIa,MAAM,KAAKA,OAAOP,MAAMjB,MAAM,EAChC;YAGF,IAAIuB,OAAOb,IAAI,KAAK,UAAU;gBAC5BO,MAAMQ,MAAM,CAACD,KAAK;gBAClBtB;YACF,OAAO;gBACL,qDAAqD;gBACrD,MAAMwB,cAAcT,KAAK,CAACO,IAAI;gBAC9B,wEAAwE;gBACxE,MAAMG,yBAAyBD,YAAYE,OAAO,CAAC,cAAc;gBACjE,sCAAsC;gBACtC,MAAMC,OAAOF,uBAAuBG,OAAO,GAAGC,QAAQ,CAAC,OACnDJ,uBAAuBG,OAAO,KAC9BH,uBAAuBG,OAAO,KAAK;gBACvCb,KAAK,CAACO,IAAI,GAAG,GAAGK,KAAK,eAAe,EAAEN,OAAOX,MAAM,EAAE;gBACrDT;YACF;QACF;QAEA,MAAM6B,IAAAA,mBAAS,EAACxB,UAAUS,MAAMgB,IAAI,CAAC,OAAO;QAC5ChC;IACF;IAEA,OAAO;QAAEA;QAAeC;QAAmBC;IAAoB;AACjE;AAEA;;;;CAIC,GACD,SAASU,mBAAmBf,CAAoB;IAC9C,MAAMoC,SAASpC,EAAEqC,YAAY,IAAI;IACjC,OAAO,CAAC,wBAAwB,EAAED,OAAO,8CAA8C,CAAC;AAC1F"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PluginObj } from '@babel/core';
|
|
2
|
+
export interface ManualMemoEntry {
|
|
3
|
+
useMemo: number;
|
|
4
|
+
useCallback: number;
|
|
5
|
+
reactMemo: boolean;
|
|
6
|
+
bodyInsertionLine: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ManualMemoPluginOptions {
|
|
9
|
+
/** Shared map keyed by `line:column` of the enclosing function start */
|
|
10
|
+
results: Map<string, ManualMemoEntry>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Babel plugin that detects manual memoization (useMemo, useCallback, React.memo)
|
|
14
|
+
* within function bodies. Populates a shared Map in plugin options keyed by function location.
|
|
15
|
+
*/
|
|
16
|
+
export declare function manualMemoPlugin(): PluginObj;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "manualMemoPlugin", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return manualMemoPlugin;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
function fnKey(loc) {
|
|
12
|
+
return `${loc.line}:${loc.column}`;
|
|
13
|
+
}
|
|
14
|
+
function getBodyInsertionLine(fnPath) {
|
|
15
|
+
const body = fnPath.node.body;
|
|
16
|
+
if (body.type === 'BlockStatement' && body.loc) // Insert after the opening brace — first statement line, or body start + 1
|
|
17
|
+
return body.loc.start.line + 1;
|
|
18
|
+
// Arrow function with expression body — use the function start line
|
|
19
|
+
if (fnPath.node.loc) return fnPath.node.loc.start.line;
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
function hasUseMemoDirective(fnPath) {
|
|
23
|
+
const body = fnPath.node.body;
|
|
24
|
+
if (body.type !== 'BlockStatement') return false;
|
|
25
|
+
for (const directive of body.directives ?? []){
|
|
26
|
+
if (directive.value.value === 'use memo') return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function manualMemoPlugin() {
|
|
31
|
+
return {
|
|
32
|
+
name: 'manual-memo-detection',
|
|
33
|
+
visitor: {
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
35
|
+
CallExpression (path, state) {
|
|
36
|
+
const opts = state.opts;
|
|
37
|
+
const callee = path.node.callee;
|
|
38
|
+
let hookName = null;
|
|
39
|
+
// Direct call: memo(...), useMemo(...), useCallback(...)
|
|
40
|
+
if (callee.type === 'Identifier') {
|
|
41
|
+
const binding = path.scope.getBinding(callee.name);
|
|
42
|
+
if (binding?.path.parent?.type === 'ImportDeclaration' && binding.path.parent.source.value === 'react') {
|
|
43
|
+
if (callee.name === 'useMemo') hookName = 'useMemo';
|
|
44
|
+
else if (callee.name === 'useCallback') hookName = 'useCallback';
|
|
45
|
+
else if (callee.name === 'memo') hookName = 'reactMemo';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Member expression: React.memo(...), React.useMemo(...), React.useCallback(...)
|
|
49
|
+
if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier' && callee.property.type === 'Identifier') {
|
|
50
|
+
const binding = path.scope.getBinding(callee.object.name);
|
|
51
|
+
if (binding?.path.parent?.type === 'ImportDeclaration' && binding.path.parent.source.value === 'react') {
|
|
52
|
+
if (callee.property.name === 'memo') hookName = 'reactMemo';
|
|
53
|
+
else if (callee.property.name === 'useMemo') hookName = 'useMemo';
|
|
54
|
+
else if (callee.property.name === 'useCallback') hookName = 'useCallback';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!hookName) return;
|
|
58
|
+
// Find the enclosing function
|
|
59
|
+
const fnPath = path.findParent((p)=>p.isFunctionDeclaration() || p.isFunctionExpression() || p.isArrowFunctionExpression());
|
|
60
|
+
if (!fnPath || !fnPath.node.loc) return;
|
|
61
|
+
// Skip functions that already have 'use memo' directive
|
|
62
|
+
if (hasUseMemoDirective(fnPath)) return;
|
|
63
|
+
const key = fnKey(fnPath.node.loc.start);
|
|
64
|
+
let entry = opts.results.get(key);
|
|
65
|
+
if (!entry) {
|
|
66
|
+
entry = {
|
|
67
|
+
useMemo: 0,
|
|
68
|
+
useCallback: 0,
|
|
69
|
+
reactMemo: false,
|
|
70
|
+
bodyInsertionLine: getBodyInsertionLine(fnPath)
|
|
71
|
+
};
|
|
72
|
+
opts.results.set(key, entry);
|
|
73
|
+
}
|
|
74
|
+
if (hookName === 'useMemo') entry.useMemo++;
|
|
75
|
+
else if (hookName === 'useCallback') entry.useCallback++;
|
|
76
|
+
else if (hookName === 'reactMemo') entry.reactMemo = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=manual-memo-plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/manual-memo-plugin.ts"],"sourcesContent":["import type { PluginObj, NodePath } from '@babel/core';\nimport type { Function as BabelFunction } from '@babel/types';\n\nimport type { ManualMemoization } from './types';\n\nexport interface ManualMemoEntry {\n useMemo: number;\n useCallback: number;\n reactMemo: boolean;\n bodyInsertionLine: number;\n}\n\nexport interface ManualMemoPluginOptions {\n /** Shared map keyed by `line:column` of the enclosing function start */\n results: Map<string, ManualMemoEntry>;\n}\n\nfunction fnKey(loc: { line: number; column: number }): string {\n return `${loc.line}:${loc.column}`;\n}\n\nfunction getBodyInsertionLine(fnPath: NodePath<BabelFunction>): number {\n const body = fnPath.node.body;\n if (body.type === 'BlockStatement' && body.loc) {\n // Insert after the opening brace — first statement line, or body start + 1\n return body.loc.start.line + 1;\n }\n // Arrow function with expression body — use the function start line\n if (fnPath.node.loc) {\n return fnPath.node.loc.start.line;\n }\n return 0;\n}\n\nfunction hasUseMemoDirective(fnPath: NodePath<BabelFunction>): boolean {\n const body = fnPath.node.body;\n if (body.type !== 'BlockStatement') {\n return false;\n }\n for (const directive of body.directives ?? []) {\n if (directive.value.value === 'use memo') {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Babel plugin that detects manual memoization (useMemo, useCallback, React.memo)\n * within function bodies. Populates a shared Map in plugin options keyed by function location.\n */\nexport function manualMemoPlugin(): PluginObj {\n return {\n name: 'manual-memo-detection',\n visitor: {\n // eslint-disable-next-line @typescript-eslint/naming-convention\n CallExpression(path, state) {\n const opts = state.opts as unknown as ManualMemoPluginOptions;\n const callee = path.node.callee;\n\n let hookName: 'useMemo' | 'useCallback' | 'reactMemo' | null = null;\n\n // Direct call: memo(...), useMemo(...), useCallback(...)\n if (callee.type === 'Identifier') {\n const binding = path.scope.getBinding(callee.name);\n if (binding?.path.parent?.type === 'ImportDeclaration' && binding.path.parent.source.value === 'react') {\n if (callee.name === 'useMemo') {\n hookName = 'useMemo';\n } else if (callee.name === 'useCallback') {\n hookName = 'useCallback';\n } else if (callee.name === 'memo') {\n hookName = 'reactMemo';\n }\n }\n }\n\n // Member expression: React.memo(...), React.useMemo(...), React.useCallback(...)\n if (\n callee.type === 'MemberExpression' &&\n callee.object.type === 'Identifier' &&\n callee.property.type === 'Identifier'\n ) {\n const binding = path.scope.getBinding(callee.object.name);\n if (binding?.path.parent?.type === 'ImportDeclaration' && binding.path.parent.source.value === 'react') {\n if (callee.property.name === 'memo') {\n hookName = 'reactMemo';\n } else if (callee.property.name === 'useMemo') {\n hookName = 'useMemo';\n } else if (callee.property.name === 'useCallback') {\n hookName = 'useCallback';\n }\n }\n }\n\n if (!hookName) {\n return;\n }\n\n // Find the enclosing function\n const fnPath = path.findParent(\n p => p.isFunctionDeclaration() || p.isFunctionExpression() || p.isArrowFunctionExpression(),\n ) as NodePath<BabelFunction> | null;\n\n if (!fnPath || !fnPath.node.loc) {\n return;\n }\n\n // Skip functions that already have 'use memo' directive\n if (hasUseMemoDirective(fnPath)) {\n return;\n }\n\n const key = fnKey(fnPath.node.loc.start);\n let entry = opts.results.get(key);\n\n if (!entry) {\n entry = {\n useMemo: 0,\n useCallback: 0,\n reactMemo: false,\n bodyInsertionLine: getBodyInsertionLine(fnPath),\n };\n opts.results.set(key, entry);\n }\n\n if (hookName === 'useMemo') {\n entry.useMemo++;\n } else if (hookName === 'useCallback') {\n entry.useCallback++;\n } else if (hookName === 'reactMemo') {\n entry.reactMemo = true;\n }\n },\n },\n };\n}\n"],"names":["manualMemoPlugin","fnKey","loc","line","column","getBodyInsertionLine","fnPath","body","node","type","start","hasUseMemoDirective","directive","directives","value","name","visitor","CallExpression","path","state","opts","callee","hookName","binding","scope","getBinding","parent","source","object","property","findParent","p","isFunctionDeclaration","isFunctionExpression","isArrowFunctionExpression","key","entry","results","get","useMemo","useCallback","reactMemo","bodyInsertionLine","set"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DirectiveAnalysis } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Print a markdown-formatted report of all directive analyses, grouped by package and status.
|
|
4
|
+
*/
|
|
5
|
+
export declare function printReport(results: DirectiveAnalysis[], workspaceRoot: string, fullReasons: boolean): void;
|
|
6
|
+
/**
|
|
7
|
+
* Print a summary of the analysis results.
|
|
8
|
+
*/
|
|
9
|
+
export declare function printSummary(results: DirectiveAnalysis[]): void;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: all[name]
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
printReport: function() {
|
|
13
|
+
return printReport;
|
|
14
|
+
},
|
|
15
|
+
printSummary: function() {
|
|
16
|
+
return printSummary;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const _nodepath = require("node:path");
|
|
20
|
+
const TABLE_REASON_MAX_LEN = 80;
|
|
21
|
+
function printReport(results, workspaceRoot, fullReasons) {
|
|
22
|
+
if (results.length === 0) {
|
|
23
|
+
console.log("\nNo 'use no memo' directives found.");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Group by package
|
|
27
|
+
const byPackage = new Map();
|
|
28
|
+
for (const r of results){
|
|
29
|
+
const existing = byPackage.get(r.packageName) ?? [];
|
|
30
|
+
existing.push(r);
|
|
31
|
+
byPackage.set(r.packageName, existing);
|
|
32
|
+
}
|
|
33
|
+
// Sort packages alphabetically
|
|
34
|
+
const sortedPackages = [
|
|
35
|
+
...byPackage.keys()
|
|
36
|
+
].sort();
|
|
37
|
+
for (const pkg of sortedPackages){
|
|
38
|
+
const pkgResults = byPackage.get(pkg);
|
|
39
|
+
const active = pkgResults.filter((r)=>r.status === 'active');
|
|
40
|
+
const redundant = pkgResults.filter((r)=>r.status === 'redundant');
|
|
41
|
+
const skipped = pkgResults.filter((r)=>r.status === 'skipped');
|
|
42
|
+
console.log(`\n## ${pkg}\n`);
|
|
43
|
+
if (active.length > 0) {
|
|
44
|
+
console.log('### Active (needs `// justified:` comment)\n');
|
|
45
|
+
printTable(active, workspaceRoot, fullReasons);
|
|
46
|
+
}
|
|
47
|
+
if (redundant.length > 0) {
|
|
48
|
+
console.log('### Redundant (removable)\n');
|
|
49
|
+
printTable(redundant, workspaceRoot, fullReasons);
|
|
50
|
+
}
|
|
51
|
+
if (skipped.length > 0) {
|
|
52
|
+
console.log('### Skipped (already justified)\n');
|
|
53
|
+
printTable(skipped, workspaceRoot, fullReasons);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function printTable(results, workspaceRoot, fullReasons) {
|
|
58
|
+
console.log('| Location | Function | Compiler Event | Reason |');
|
|
59
|
+
console.log('|----------|----------|----------------|--------|');
|
|
60
|
+
for (const r of results){
|
|
61
|
+
const relPath = (0, _nodepath.relative)(workspaceRoot, r.filePath);
|
|
62
|
+
const fn = r.functionName ?? '(unknown)';
|
|
63
|
+
const reason = r.reason ? fullReasons ? escapeTableCell(r.reason) : truncate(r.reason, TABLE_REASON_MAX_LEN) : '';
|
|
64
|
+
console.log(`| ${relPath}:${r.line} | ${fn} | ${r.compilerEvent} | ${reason} |`);
|
|
65
|
+
}
|
|
66
|
+
console.log('');
|
|
67
|
+
if (fullReasons) {
|
|
68
|
+
// Print full reasons as details blocks below the table for readability
|
|
69
|
+
const withReasons = results.filter((r)=>r.reason && r.reason.includes('\n'));
|
|
70
|
+
if (withReasons.length > 0) {
|
|
71
|
+
console.log('<details><summary>Full compiler output</summary>\n');
|
|
72
|
+
for (const r of withReasons){
|
|
73
|
+
const relPath = (0, _nodepath.relative)(workspaceRoot, r.filePath);
|
|
74
|
+
const fn = r.functionName ?? '(unknown)';
|
|
75
|
+
console.log(`#### ${relPath}:${r.line} — ${fn}\n`);
|
|
76
|
+
console.log('```');
|
|
77
|
+
console.log(r.reason);
|
|
78
|
+
console.log('```\n');
|
|
79
|
+
}
|
|
80
|
+
console.log('</details>\n');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function printSummary(results) {
|
|
85
|
+
const total = results.length;
|
|
86
|
+
const redundant = results.filter((r)=>r.status === 'redundant').length;
|
|
87
|
+
const active = results.filter((r)=>r.status === 'active').length;
|
|
88
|
+
const skipped = results.filter((r)=>r.status === 'skipped').length;
|
|
89
|
+
console.log('## Summary\n');
|
|
90
|
+
console.log(`- **Total directives:** ${total}`);
|
|
91
|
+
console.log(`- **Redundant** (removable): ${redundant}`);
|
|
92
|
+
console.log(`- **Active** (needs \`// justified:\` comment): ${active}`);
|
|
93
|
+
console.log(`- **Skipped** (already justified): ${skipped}`);
|
|
94
|
+
console.log('');
|
|
95
|
+
if (redundant > 0 && active > 0) {
|
|
96
|
+
console.log(`> **${redundant}** redundant directive(s) can be safely removed.`);
|
|
97
|
+
console.log(`> **${active}** active directive(s) need a \`// justified: <reason>\` comment — the compiler would optimize these functions without the directive.`);
|
|
98
|
+
console.log('>');
|
|
99
|
+
console.log('> Run with `--fix` to auto-remove redundant directives and annotate active ones.\n');
|
|
100
|
+
} else if (redundant > 0) {
|
|
101
|
+
console.log(`> **${redundant}** redundant \`'use no memo'\` directive(s) found.`);
|
|
102
|
+
console.log('> Run with `--fix` to auto-remove them.\n');
|
|
103
|
+
} else if (active > 0) {
|
|
104
|
+
console.log(`> **${active}** active directive(s) need a \`// justified: <reason>\` comment.`);
|
|
105
|
+
console.log('> Run with `--fix` to annotate them.\n');
|
|
106
|
+
} else console.log('> All directives are justified. Nothing to do.\n');
|
|
107
|
+
}
|
|
108
|
+
function truncate(str, maxLen) {
|
|
109
|
+
const cleaned = str.replace(/\n/g, ' ');
|
|
110
|
+
return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 3) + '...' : cleaned;
|
|
111
|
+
}
|
|
112
|
+
function escapeTableCell(str) {
|
|
113
|
+
// For table cells, collapse to single line
|
|
114
|
+
return str.replace(/\n/g, ' ').replace(/\|/g, '\\|');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
//# sourceMappingURL=reporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/reporter.ts"],"sourcesContent":["import { relative } from 'node:path';\n\nimport type { DirectiveAnalysis } from './types';\n\nconst TABLE_REASON_MAX_LEN = 80;\n\n/**\n * Print a markdown-formatted report of all directive analyses, grouped by package and status.\n */\nexport function printReport(results: DirectiveAnalysis[], workspaceRoot: string, fullReasons: boolean): void {\n if (results.length === 0) {\n console.log(\"\\nNo 'use no memo' directives found.\");\n return;\n }\n\n // Group by package\n const byPackage = new Map<string, DirectiveAnalysis[]>();\n for (const r of results) {\n const existing = byPackage.get(r.packageName) ?? [];\n existing.push(r);\n byPackage.set(r.packageName, existing);\n }\n\n // Sort packages alphabetically\n const sortedPackages = [...byPackage.keys()].sort();\n\n for (const pkg of sortedPackages) {\n const pkgResults = byPackage.get(pkg)!;\n const active = pkgResults.filter(r => r.status === 'active');\n const redundant = pkgResults.filter(r => r.status === 'redundant');\n const skipped = pkgResults.filter(r => r.status === 'skipped');\n\n console.log(`\\n## ${pkg}\\n`);\n\n if (active.length > 0) {\n console.log('### Active (needs `// justified:` comment)\\n');\n printTable(active, workspaceRoot, fullReasons);\n }\n\n if (redundant.length > 0) {\n console.log('### Redundant (removable)\\n');\n printTable(redundant, workspaceRoot, fullReasons);\n }\n\n if (skipped.length > 0) {\n console.log('### Skipped (already justified)\\n');\n printTable(skipped, workspaceRoot, fullReasons);\n }\n }\n}\n\nfunction printTable(results: DirectiveAnalysis[], workspaceRoot: string, fullReasons: boolean): void {\n console.log('| Location | Function | Compiler Event | Reason |');\n console.log('|----------|----------|----------------|--------|');\n\n for (const r of results) {\n const relPath = relative(workspaceRoot, r.filePath);\n const fn = r.functionName ?? '(unknown)';\n const reason = r.reason ? (fullReasons ? escapeTableCell(r.reason) : truncate(r.reason, TABLE_REASON_MAX_LEN)) : '';\n console.log(`| ${relPath}:${r.line} | ${fn} | ${r.compilerEvent} | ${reason} |`);\n }\n console.log('');\n\n if (fullReasons) {\n // Print full reasons as details blocks below the table for readability\n const withReasons = results.filter(r => r.reason && r.reason.includes('\\n'));\n if (withReasons.length > 0) {\n console.log('<details><summary>Full compiler output</summary>\\n');\n for (const r of withReasons) {\n const relPath = relative(workspaceRoot, r.filePath);\n const fn = r.functionName ?? '(unknown)';\n console.log(`#### ${relPath}:${r.line} — ${fn}\\n`);\n console.log('```');\n console.log(r.reason);\n console.log('```\\n');\n }\n console.log('</details>\\n');\n }\n }\n}\n\n/**\n * Print a summary of the analysis results.\n */\nexport function printSummary(results: DirectiveAnalysis[]): void {\n const total = results.length;\n const redundant = results.filter(r => r.status === 'redundant').length;\n const active = results.filter(r => r.status === 'active').length;\n const skipped = results.filter(r => r.status === 'skipped').length;\n\n console.log('## Summary\\n');\n console.log(`- **Total directives:** ${total}`);\n console.log(`- **Redundant** (removable): ${redundant}`);\n console.log(`- **Active** (needs \\`// justified:\\` comment): ${active}`);\n console.log(`- **Skipped** (already justified): ${skipped}`);\n console.log('');\n\n if (redundant > 0 && active > 0) {\n console.log(`> **${redundant}** redundant directive(s) can be safely removed.`);\n console.log(\n `> **${active}** active directive(s) need a \\`// justified: <reason>\\` comment — the compiler would optimize these functions without the directive.`,\n );\n console.log('>');\n console.log('> Run with `--fix` to auto-remove redundant directives and annotate active ones.\\n');\n } else if (redundant > 0) {\n console.log(`> **${redundant}** redundant \\`'use no memo'\\` directive(s) found.`);\n console.log('> Run with `--fix` to auto-remove them.\\n');\n } else if (active > 0) {\n console.log(`> **${active}** active directive(s) need a \\`// justified: <reason>\\` comment.`);\n console.log('> Run with `--fix` to annotate them.\\n');\n } else {\n console.log('> All directives are justified. Nothing to do.\\n');\n }\n}\n\nfunction truncate(str: string, maxLen: number): string {\n const cleaned = str.replace(/\\n/g, ' ');\n return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 3) + '...' : cleaned;\n}\n\nfunction escapeTableCell(str: string): string {\n // For table cells, collapse to single line\n return str.replace(/\\n/g, ' ').replace(/\\|/g, '\\\\|');\n}\n"],"names":["printReport","printSummary","TABLE_REASON_MAX_LEN","results","workspaceRoot","fullReasons","length","console","log","byPackage","Map","r","existing","get","packageName","push","set","sortedPackages","keys","sort","pkg","pkgResults","active","filter","status","redundant","skipped","printTable","relPath","relative","filePath","fn","functionName","reason","escapeTableCell","truncate","line","compilerEvent","withReasons","includes","total","str","maxLen","cleaned","replace","slice"],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface FileEntry {
|
|
2
|
+
filePath: string;
|
|
3
|
+
packageName: string;
|
|
4
|
+
}
|
|
5
|
+
export type DirectiveStatus = 'redundant' | 'active' | 'skipped';
|
|
6
|
+
export interface DirectiveLocation {
|
|
7
|
+
/** 1-based line number in the original source */
|
|
8
|
+
line: number;
|
|
9
|
+
/** The full text of the directive line (for removal) */
|
|
10
|
+
lineText: string;
|
|
11
|
+
/** Whether the directive has a `// justified: <reason>` comment */
|
|
12
|
+
justified: boolean;
|
|
13
|
+
/** The justification reason, if present */
|
|
14
|
+
justification?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface DirectiveAnalysis {
|
|
17
|
+
filePath: string;
|
|
18
|
+
packageName: string;
|
|
19
|
+
line: number;
|
|
20
|
+
functionName: string | null;
|
|
21
|
+
status: DirectiveStatus;
|
|
22
|
+
compilerEvent: 'CompileError' | 'CompileSuccess' | 'PipelineError' | 'none' | 'skipped';
|
|
23
|
+
reason?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface AnalyzerOptions {
|
|
26
|
+
concurrency: number;
|
|
27
|
+
verbose: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface FixResult {
|
|
30
|
+
filesModified: number;
|
|
31
|
+
directivesRemoved: number;
|
|
32
|
+
directivesJustified: number;
|
|
33
|
+
}
|
|
34
|
+
export type CompilationMode = 'infer' | 'annotation' | 'all';
|
|
35
|
+
export type FunctionStatus = 'compiled' | 'skipped' | 'error';
|
|
36
|
+
export interface MemoStats {
|
|
37
|
+
memoSlots: number;
|
|
38
|
+
memoBlocks: number;
|
|
39
|
+
memoValues: number;
|
|
40
|
+
prunedMemoBlocks: number;
|
|
41
|
+
prunedMemoValues: number;
|
|
42
|
+
}
|
|
43
|
+
export interface ManualMemoization {
|
|
44
|
+
useMemo: number;
|
|
45
|
+
useCallback: number;
|
|
46
|
+
reactMemo: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface FunctionAnalysis {
|
|
49
|
+
filePath: string;
|
|
50
|
+
packageName: string;
|
|
51
|
+
line: number;
|
|
52
|
+
column: number;
|
|
53
|
+
functionName: string | null;
|
|
54
|
+
status: FunctionStatus;
|
|
55
|
+
compilerEvent: 'CompileSuccess' | 'CompileError' | 'CompileSkip' | 'PipelineError';
|
|
56
|
+
reason?: string;
|
|
57
|
+
memoStats?: MemoStats;
|
|
58
|
+
manualMemo?: ManualMemoization;
|
|
59
|
+
bodyInsertionLine?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface AnnotateResult {
|
|
62
|
+
filesModified: number;
|
|
63
|
+
functionsAnnotated: number;
|
|
64
|
+
}
|
|
65
|
+
export interface CoverageAnalyzerOptions {
|
|
66
|
+
concurrency: number;
|
|
67
|
+
verbose: boolean;
|
|
68
|
+
compilationMode: CompilationMode;
|
|
69
|
+
annotate?: boolean;
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types.ts"],"sourcesContent":["// ── Shared types ──\n\nexport interface FileEntry {\n filePath: string;\n packageName: string;\n}\n\n// ── Directive analysis types ──\n\nexport type DirectiveStatus = 'redundant' | 'active' | 'skipped';\n\nexport interface DirectiveLocation {\n /** 1-based line number in the original source */\n line: number;\n /** The full text of the directive line (for removal) */\n lineText: string;\n /** Whether the directive has a `// justified: <reason>` comment */\n justified: boolean;\n /** The justification reason, if present */\n justification?: string;\n}\n\nexport interface DirectiveAnalysis {\n filePath: string;\n packageName: string;\n line: number;\n functionName: string | null;\n status: DirectiveStatus;\n compilerEvent: 'CompileError' | 'CompileSuccess' | 'PipelineError' | 'none' | 'skipped';\n reason?: string;\n}\n\nexport interface AnalyzerOptions {\n concurrency: number;\n verbose: boolean;\n}\n\nexport interface FixResult {\n filesModified: number;\n directivesRemoved: number;\n directivesJustified: number;\n}\n\n// ── Coverage analysis types ──\n\nexport type CompilationMode = 'infer' | 'annotation' | 'all';\n\nexport type FunctionStatus = 'compiled' | 'skipped' | 'error';\n\nexport interface MemoStats {\n memoSlots: number;\n memoBlocks: number;\n memoValues: number;\n prunedMemoBlocks: number;\n prunedMemoValues: number;\n}\n\nexport interface ManualMemoization {\n useMemo: number;\n useCallback: number;\n reactMemo: boolean;\n}\n\nexport interface FunctionAnalysis {\n filePath: string;\n packageName: string;\n line: number;\n column: number;\n functionName: string | null;\n status: FunctionStatus;\n compilerEvent: 'CompileSuccess' | 'CompileError' | 'CompileSkip' | 'PipelineError';\n reason?: string;\n memoStats?: MemoStats;\n manualMemo?: ManualMemoization;\n bodyInsertionLine?: number;\n}\n\nexport interface AnnotateResult {\n filesModified: number;\n functionsAnnotated: number;\n}\n\nexport interface CoverageAnalyzerOptions {\n concurrency: number;\n verbose: boolean;\n compilationMode: CompilationMode;\n annotate?: boolean;\n}\n"],"names":[],"mappings":"AAAA,qBAAqB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fluentui/react-compiler-analyzer",
|
|
3
|
+
"version": "0.0.0-experimental.rc-analyzer.20260430-fc3bb2e193.0",
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"main": "./dist/src/index.js",
|
|
6
|
+
"types": "./dist/src/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"bin",
|
|
9
|
+
"*.md",
|
|
10
|
+
"dist",
|
|
11
|
+
"lib",
|
|
12
|
+
"lib-commonjs"
|
|
13
|
+
],
|
|
14
|
+
"bin": {
|
|
15
|
+
"react-compiler-analyzer": "./bin/react-compiler-analyzer.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@swc/helpers": "~0.5.1",
|
|
19
|
+
"yargs": "^17.7.2",
|
|
20
|
+
"@babel/core": "^7.28.5",
|
|
21
|
+
"@babel/preset-typescript": "^7.28.5",
|
|
22
|
+
"babel-plugin-react-compiler": "^1.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|