@mindees/compiler 0.8.0 → 0.10.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/dist/budget.d.ts +18 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +30 -0
- package/dist/budget.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/perf-lint.js +12 -0
- package/dist/perf-lint.js.map +1 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +17 -5
- package/dist/transform.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/budget.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CompileResult, Diagnostic } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/budget.d.ts
|
|
4
|
+
/** A per-module performance budget. A field left undefined isn't enforced. */
|
|
5
|
+
interface BudgetOptions {
|
|
6
|
+
/** Max compiled output size in **bytes** (UTF-8). */
|
|
7
|
+
readonly maxBytes?: number;
|
|
8
|
+
/** Max total elements in the module's UI tree (pre-flatten count). */
|
|
9
|
+
readonly maxElements?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Check a compile result against `budget`. Returns **error** diagnostics for every exceeded limit
|
|
13
|
+
* (empty when within budget). `compileChecked` refuses to emit when any are present.
|
|
14
|
+
*/
|
|
15
|
+
declare function checkBudget(result: Pick<CompileResult, 'code' | 'stats'>, budget: BudgetOptions): Diagnostic[];
|
|
16
|
+
//#endregion
|
|
17
|
+
export { BudgetOptions, checkBudget };
|
|
18
|
+
//# sourceMappingURL=budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.d.ts","names":[],"sources":["../src/budget.ts"],"mappings":";;;;UAYiB,aAAA;EAiBF;EAAA,SAfJ,QAAA;EAgBD;EAAA,SAdC,WAAW;AAAA;;;;;iBAYN,WAAA,CACd,MAAA,EAAQ,IAAA,CAAK,aAAA,qBACb,MAAA,EAAQ,aAAA,GACP,UAAA"}
|
package/dist/budget.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/budget.ts
|
|
2
|
+
/** UTF-8 byte length of `text` (platform-neutral; no `Buffer`). */
|
|
3
|
+
function byteLength(text) {
|
|
4
|
+
return new TextEncoder().encode(text).length;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Check a compile result against `budget`. Returns **error** diagnostics for every exceeded limit
|
|
8
|
+
* (empty when within budget). `compileChecked` refuses to emit when any are present.
|
|
9
|
+
*/
|
|
10
|
+
function checkBudget(result, budget) {
|
|
11
|
+
const diagnostics = [];
|
|
12
|
+
if (budget.maxBytes !== void 0) {
|
|
13
|
+
const bytes = byteLength(result.code);
|
|
14
|
+
if (bytes > budget.maxBytes) diagnostics.push({
|
|
15
|
+
severity: "error",
|
|
16
|
+
code: "MDC_BUDGET_BYTES",
|
|
17
|
+
message: `Bundle size ${bytes} B exceeds the budget of ${budget.maxBytes} B (over by ${bytes - budget.maxBytes} B). Split the screen, lazy-load, or raise the budget.`
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (budget.maxElements !== void 0 && result.stats.totalElements > budget.maxElements) diagnostics.push({
|
|
21
|
+
severity: "error",
|
|
22
|
+
code: "MDC_BUDGET_ELEMENTS",
|
|
23
|
+
message: `${result.stats.totalElements} UI elements exceed the budget of ${budget.maxElements}. Virtualize long lists (List/For) or split the screen.`
|
|
24
|
+
});
|
|
25
|
+
return diagnostics;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { checkBudget };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.js","names":[],"sources":["../src/budget.ts"],"sourcesContent":["/**\n * Build-time **performance budgets** (spec §12: \"the compiler fails the build if a screen exceeds\n * configured … bundle-size budgets — so '100% optimized' is enforced, not aspirational\"). Unlike the\n * perf-lint (warnings), a budget violation is an **error** that refuses to emit — neither React Native\n * nor Flutter enforces a perf budget at build time.\n *\n * @module\n */\n\nimport type { CompileResult, Diagnostic } from './types'\n\n/** A per-module performance budget. A field left undefined isn't enforced. */\nexport interface BudgetOptions {\n /** Max compiled output size in **bytes** (UTF-8). */\n readonly maxBytes?: number\n /** Max total elements in the module's UI tree (pre-flatten count). */\n readonly maxElements?: number\n}\n\n/** UTF-8 byte length of `text` (platform-neutral; no `Buffer`). */\nfunction byteLength(text: string): number {\n return new TextEncoder().encode(text).length\n}\n\n/**\n * Check a compile result against `budget`. Returns **error** diagnostics for every exceeded limit\n * (empty when within budget). `compileChecked` refuses to emit when any are present.\n */\nexport function checkBudget(\n result: Pick<CompileResult, 'code' | 'stats'>,\n budget: BudgetOptions,\n): Diagnostic[] {\n const diagnostics: Diagnostic[] = []\n if (budget.maxBytes !== undefined) {\n const bytes = byteLength(result.code)\n if (bytes > budget.maxBytes) {\n diagnostics.push({\n severity: 'error',\n code: 'MDC_BUDGET_BYTES',\n message: `Bundle size ${bytes} B exceeds the budget of ${budget.maxBytes} B (over by ${bytes - budget.maxBytes} B). Split the screen, lazy-load, or raise the budget.`,\n })\n }\n }\n if (budget.maxElements !== undefined && result.stats.totalElements > budget.maxElements) {\n diagnostics.push({\n severity: 'error',\n code: 'MDC_BUDGET_ELEMENTS',\n message: `${result.stats.totalElements} UI elements exceed the budget of ${budget.maxElements}. Virtualize long lists (List/For) or split the screen.`,\n })\n }\n return diagnostics\n}\n"],"mappings":";;AAoBA,SAAS,WAAW,MAAsB;CACxC,OAAO,IAAI,YAAY,EAAE,OAAO,IAAI,EAAE;AACxC;;;;;AAMA,SAAgB,YACd,QACA,QACc;CACd,MAAM,cAA4B,CAAC;CACnC,IAAI,OAAO,aAAa,KAAA,GAAW;EACjC,MAAM,QAAQ,WAAW,OAAO,IAAI;EACpC,IAAI,QAAQ,OAAO,UACjB,YAAY,KAAK;GACf,UAAU;GACV,MAAM;GACN,SAAS,eAAe,MAAM,2BAA2B,OAAO,SAAS,cAAc,QAAQ,OAAO,SAAS;EACjH,CAAC;CAEL;CACA,IAAI,OAAO,gBAAgB,KAAA,KAAa,OAAO,MAAM,gBAAgB,OAAO,aAC1E,YAAY,KAAK;EACf,UAAU;EACV,MAAM;EACN,SAAS,GAAG,OAAO,MAAM,cAAc,oCAAoC,OAAO,YAAY;CAChG,CAAC;CAEH,OAAO;AACT"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NativeTarget, compileToNative } from "./aot.js";
|
|
2
2
|
import { PerfLintOptions, perfLint } from "./perf-lint.js";
|
|
3
3
|
import { CompileOptions, CompileResult, CompileStats, Diagnostic, DiagnosticSeverity, MdcPlugin, SourcePosition } from "./types.js";
|
|
4
|
+
import { BudgetOptions, checkBudget } from "./budget.js";
|
|
4
5
|
import { STATIC_MARKER, createFlattenTransformer } from "./flatten.js";
|
|
5
6
|
import { GenerateRouteModuleOptions, RouteEntry, RouteManifest, buildRouteManifest, chunkName, fileToRoute, generateRouteModule } from "./routes.js";
|
|
6
7
|
import { compile, compileChecked } from "./transform.js";
|
|
@@ -11,7 +12,7 @@ import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@min
|
|
|
11
12
|
/** The npm package name. */
|
|
12
13
|
declare const name = "@mindees/compiler";
|
|
13
14
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
14
|
-
declare const VERSION = "0.
|
|
15
|
+
declare const VERSION = "0.10.0";
|
|
15
16
|
/**
|
|
16
17
|
* Current maturity. The build-time optimizer — type-check gate, TSX→createElement
|
|
17
18
|
* transform, tree-flattening, per-route manifest, plugin API — is implemented
|
|
@@ -26,5 +27,5 @@ declare const maturity: Maturity;
|
|
|
26
27
|
*/
|
|
27
28
|
declare const info: PackageInfo;
|
|
28
29
|
//#endregion
|
|
29
|
-
export { type CompileOptions, type CompileResult, type CompileStats, type Diagnostic, type DiagnosticSeverity, type GenerateRouteModuleOptions, type Maturity, type MdcPlugin, type NativeTarget, NotImplementedError, type PackageInfo, type PerfLintOptions, type RouteEntry, type RouteManifest, STATIC_MARKER, type SourcePosition, VERSION, buildRouteManifest, chunkName, compile, compileChecked, compileToNative, createFlattenTransformer, fileToRoute, generateRouteModule, hasErrors, info, maturity, name, notImplemented, perfLint, typecheck };
|
|
30
|
+
export { type BudgetOptions, type CompileOptions, type CompileResult, type CompileStats, type Diagnostic, type DiagnosticSeverity, type GenerateRouteModuleOptions, type Maturity, type MdcPlugin, type NativeTarget, NotImplementedError, type PackageInfo, type PerfLintOptions, type RouteEntry, type RouteManifest, STATIC_MARKER, type SourcePosition, VERSION, buildRouteManifest, checkBudget, chunkName, compile, compileChecked, compileToNative, createFlattenTransformer, fileToRoute, generateRouteModule, hasErrors, info, maturity, name, notImplemented, perfLint, typecheck };
|
|
30
31
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;;;cAqCa,IAAA;AAGO;AAAA,cAAP,OAAA;;;;AAQmC;AAOhD;;cAPa,QAAA,EAAU,QAAyB;;AAOoC;;;;cAAvE,IAAA,EAAM,WAAiE"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { compileToNative } from "./aot.js";
|
|
2
|
+
import { checkBudget } from "./budget.js";
|
|
2
3
|
import { STATIC_MARKER, createFlattenTransformer } from "./flatten.js";
|
|
3
4
|
import { hasErrors, typecheck } from "./typecheck.js";
|
|
4
5
|
import { perfLint } from "./perf-lint.js";
|
|
@@ -9,7 +10,7 @@ import { NotImplementedError, notImplemented } from "@mindees/core";
|
|
|
9
10
|
/** The npm package name. */
|
|
10
11
|
const name = "@mindees/compiler";
|
|
11
12
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
12
|
-
const VERSION = "0.
|
|
13
|
+
const VERSION = "0.10.0";
|
|
13
14
|
/**
|
|
14
15
|
* Current maturity. The build-time optimizer — type-check gate, TSX→createElement
|
|
15
16
|
* transform, tree-flattening, per-route manifest, plugin API — is implemented
|
|
@@ -28,6 +29,6 @@ const info = Object.freeze({
|
|
|
28
29
|
maturity
|
|
29
30
|
});
|
|
30
31
|
//#endregion
|
|
31
|
-
export { NotImplementedError, STATIC_MARKER, VERSION, buildRouteManifest, chunkName, compile, compileChecked, compileToNative, createFlattenTransformer, fileToRoute, generateRouteModule, hasErrors, info, maturity, name, notImplemented, perfLint, typecheck };
|
|
32
|
+
export { NotImplementedError, STATIC_MARKER, VERSION, buildRouteManifest, checkBudget, chunkName, compile, compileChecked, compileToNative, createFlattenTransformer, fileToRoute, generateRouteModule, hasErrors, info, maturity, name, notImplemented, perfLint, typecheck };
|
|
32
33
|
|
|
33
34
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** TS → native AOT (research track). */\nexport { compileToNative, type NativeTarget } from './aot'\n/** Tree-flattening optimizer pass. */\nexport { createFlattenTransformer, STATIC_MARKER } from './flatten'\n/** Build-time perf-lint (opt-in via `compileChecked(src, { perf: true })`). */\nexport { type PerfLintOptions, perfLint } from './perf-lint'\n/** Per-route code-splitting manifest + file-based route codegen. */\nexport {\n buildRouteManifest,\n chunkName,\n fileToRoute,\n type GenerateRouteModuleOptions,\n generateRouteModule,\n type RouteEntry,\n type RouteManifest,\n} from './routes'\n/** Compile pipeline (TSX → optimized JS). */\nexport { compile, compileChecked } from './transform'\n/** The type-check gate. */\nexport { hasErrors, typecheck } from './typecheck'\n/** Shared types. */\nexport type {\n CompileOptions,\n CompileResult,\n CompileStats,\n Diagnostic,\n DiagnosticSeverity,\n MdcPlugin,\n SourcePosition,\n} from './types'\n\n/** The npm package name. */\nexport const name = '@mindees/compiler'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** TS → native AOT (research track). */\nexport { compileToNative, type NativeTarget } from './aot'\n/** Build-time performance budget (opt-in via `compileChecked(src, { budget })`) — fails the build. */\nexport { type BudgetOptions, checkBudget } from './budget'\n/** Tree-flattening optimizer pass. */\nexport { createFlattenTransformer, STATIC_MARKER } from './flatten'\n/** Build-time perf-lint (opt-in via `compileChecked(src, { perf: true })`). */\nexport { type PerfLintOptions, perfLint } from './perf-lint'\n/** Per-route code-splitting manifest + file-based route codegen. */\nexport {\n buildRouteManifest,\n chunkName,\n fileToRoute,\n type GenerateRouteModuleOptions,\n generateRouteModule,\n type RouteEntry,\n type RouteManifest,\n} from './routes'\n/** Compile pipeline (TSX → optimized JS). */\nexport { compile, compileChecked } from './transform'\n/** The type-check gate. */\nexport { hasErrors, typecheck } from './typecheck'\n/** Shared types. */\nexport type {\n CompileOptions,\n CompileResult,\n CompileStats,\n Diagnostic,\n DiagnosticSeverity,\n MdcPlugin,\n SourcePosition,\n} from './types'\n\n/** The npm package name. */\nexport const name = '@mindees/compiler'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.10.0'\n\n/**\n * Current maturity. The build-time optimizer — type-check gate, TSX→createElement\n * transform, tree-flattening, per-route manifest, plugin API — is implemented\n * and tested on the TypeScript Compiler API. TS→native AOT is a research track\n * (throws `NotImplementedError`); the working path is TS → optimized JS.\n */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;AAqCA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;;;;;;AAQvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
|
package/dist/perf-lint.js
CHANGED
|
@@ -46,12 +46,24 @@ function perfLint(source, fileName = "module.tsx", options = {}) {
|
|
|
46
46
|
return !OFF_BY_DEFAULT.has(code);
|
|
47
47
|
};
|
|
48
48
|
const accessors = /* @__PURE__ */ new Set();
|
|
49
|
+
const addParamName = (param) => {
|
|
50
|
+
if (param && ts.isIdentifier(param.name)) accessors.add(param.name.text);
|
|
51
|
+
};
|
|
49
52
|
const collectAccessors = (node) => {
|
|
50
53
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
51
54
|
const callee = node.initializer.expression;
|
|
52
55
|
const name = ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : "";
|
|
53
56
|
if (ACCESSOR_FACTORY.test(name)) accessors.add(node.name.text);
|
|
54
57
|
}
|
|
58
|
+
if (ts.isCallExpression(node) && keyed.has(ts.isIdentifier(node.expression) ? node.expression.text : "")) {
|
|
59
|
+
const arg = node.arguments[0];
|
|
60
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
61
|
+
for (const p of arg.properties) if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && (p.name.text === "children" || p.name.text === "renderItem") && (ts.isArrowFunction(p.initializer) || ts.isFunctionExpression(p.initializer))) {
|
|
62
|
+
addParamName(p.initializer.parameters[0]);
|
|
63
|
+
addParamName(p.initializer.parameters[1]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
55
67
|
ts.forEachChild(node, collectAccessors);
|
|
56
68
|
};
|
|
57
69
|
collectAccessors(sf);
|
package/dist/perf-lint.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"perf-lint.js","names":[],"sources":["../src/perf-lint.ts"],"sourcesContent":["/**\n * Build-time **perf-lint** — an opt-in pass that flags real performance footguns in the\n * fine-grained reactive + Helix render model, as `warning` {@link Diagnostic}s (it NEVER blocks the\n * build). Honest by design: every rule reports a concrete structural fact and *why* it's slow in\n * THIS model — no invented frame-time numbers. A diagnostic neither React Native nor Flutter ships.\n *\n * Enable via `compileChecked(src, { perf: true })` (or `{ perf: { rules, listSizeThreshold } }`).\n * Suppress a finding with a leading `// mdc-perf-ignore` (all) or `// mdc-perf-ignore MDC_PERF_001`\n * (one code) comment, or `rules: { MDC_PERF_001: 'off' }`.\n *\n * @module\n */\n\nimport ts from 'typescript'\nimport { scriptKindForFile } from './typecheck'\nimport type { Diagnostic } from './types'\n\n/** Per-call configuration for {@link perfLint}. */\nexport interface PerfLintOptions {\n /** Turn individual rules on/off (default: all on except `MDC_PERF_007`). */\n readonly rules?: Record<string, 'off' | 'warning'>\n /** Element count at/above which `MDC_PERF_007` fires (default 50). */\n readonly listSizeThreshold?: number\n /** Identifiers treated as keyed list builders (default For/keyedRegion/List/createList/SectionList/createSectionList). */\n readonly keyedNames?: readonly string[]\n}\n\nconst DEFAULT_KEYED = [\n 'For',\n 'keyedRegion',\n 'List',\n 'createList',\n 'SectionList',\n 'createSectionList',\n]\nconst OFF_BY_DEFAULT = new Set(['MDC_PERF_007'])\n/** A `const x = signal()|computed()|memo()|use<Capital>()` binds a reactive accessor. */\nconst ACCESSOR_FACTORY = /^(signal|computed|memo|use[A-Z])/\nconst HEAVY_METHODS = new Set([\n 'map',\n 'filter',\n 'reduce',\n 'forEach',\n 'sort',\n 'flatMap',\n 'reduceRight',\n])\nconst SUBSCRIBE_CALLS =\n /(addEventListener|setInterval|setTimeout|subscribe|^on$|observe|addListener)/\n\n/** Run the perf-lint over one module's source; returns `warning` diagnostics (possibly empty). */\nexport function perfLint(\n source: string,\n fileName = 'module.tsx',\n options: PerfLintOptions = {},\n): Diagnostic[] {\n const sf = ts.createSourceFile(\n fileName,\n source,\n ts.ScriptTarget.ES2023,\n /* setParentNodes */ true,\n scriptKindForFile(fileName),\n )\n const keyed = new Set(options.keyedNames ?? DEFAULT_KEYED)\n const threshold = options.listSizeThreshold ?? 50\n const out: Diagnostic[] = []\n\n const isEnabled = (code: string): boolean => {\n const setting = options.rules?.[code]\n if (setting) return setting !== 'off'\n return !OFF_BY_DEFAULT.has(code)\n }\n\n // --- pre-pass: the set of identifiers bound to reactive accessors (signal/computed/memo/use*) ---\n const accessors = new Set<string>()\n const collectAccessors = (node: ts.Node): void => {\n if (\n ts.isVariableDeclaration(node) &&\n ts.isIdentifier(node.name) &&\n node.initializer &&\n ts.isCallExpression(node.initializer)\n ) {\n const callee = node.initializer.expression\n const name = ts.isIdentifier(callee)\n ? callee.text\n : ts.isPropertyAccessExpression(callee)\n ? callee.name.text\n : ''\n if (ACCESSOR_FACTORY.test(name)) accessors.add(node.name.text)\n }\n ts.forEachChild(node, collectAccessors)\n }\n collectAccessors(sf)\n\n const positionOf = (node: ts.Node) => {\n const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf))\n return { line: line + 1, column: character + 1 }\n }\n // Line-based suppression: an `mdc-perf-ignore [CODE]` comment on the finding's line or the line\n // above. This works uniformly for `//` comments AND JSX `{/* … */}` comment children (whose comment\n // text isn't in the flagged node's leading trivia).\n const sourceLines = source.split('\\n')\n const isSuppressed = (node: ts.Node, code: string): boolean => {\n const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf))\n for (const ln of [line, line - 1]) {\n const m = /mdc-perf-ignore(?:\\s+(MDC_PERF_\\d+))?/.exec(sourceLines[ln] ?? '')\n if (m && (!m[1] || m[1] === code)) return true\n }\n return false\n }\n const emit = (node: ts.Node, code: string, message: string): void => {\n if (!isEnabled(code) || isSuppressed(node, code)) return\n out.push({ severity: 'warning', code, message, file: fileName, position: positionOf(node) })\n }\n\n // --- shared AST helpers ---\n const unwrapArrow = (expr: ts.Expression): ts.Expression => {\n // `() => X` → X (the arrow's expression body), else the node itself.\n if (ts.isArrowFunction(expr) && !ts.isBlock(expr.body)) return expr.body\n return expr\n }\n const returnsJsx = (fn: ts.Expression): boolean => {\n if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return false\n let found = false\n const look = (n: ts.Node): void => {\n if (found) return\n if (ts.isJsxElement(n) || ts.isJsxFragment(n) || ts.isJsxSelfClosingElement(n)) {\n found = true\n return\n }\n // don't descend into a NESTED function (its JSX isn't this callback's return)\n if (\n n !== fn &&\n (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n))\n )\n return\n ts.forEachChild(n, look)\n }\n look(fn.body)\n return found\n }\n const calleeName = (call: ts.CallExpression): string =>\n ts.isIdentifier(call.expression) ? call.expression.text : ''\n const propAccessName = (call: ts.CallExpression): string =>\n ts.isPropertyAccessExpression(call.expression) ? call.expression.name.text : ''\n /** Does the subtree contain ANY call expression? (A returned literal with no call is provably static.) */\n const containsAnyCall = (root: ts.Node): boolean => {\n let found = false\n const look = (n: ts.Node): void => {\n if (found) return\n if (ts.isCallExpression(n)) {\n found = true\n return\n }\n ts.forEachChild(n, look)\n }\n look(root)\n return found\n }\n /** Does the subtree reactively read a known accessor (a zero-arg call `acc()`)? */\n const readsAccessor = (root: ts.Node): boolean => {\n let found = false\n const look = (n: ts.Node): void => {\n if (found) return\n if (\n ts.isCallExpression(n) &&\n n.arguments.length === 0 &&\n ts.isIdentifier(n.expression) &&\n accessors.has(n.expression.text)\n ) {\n found = true\n return\n }\n ts.forEachChild(n, look)\n }\n look(root)\n return found\n }\n /** Does the subtree contain a heavy synchronous construct (loop / array-method chain / JSON)? */\n const hasHeavyWork = (root: ts.Node): boolean => {\n let found = false\n const look = (n: ts.Node): void => {\n if (found) return\n if (\n ts.isForStatement(n) ||\n ts.isForOfStatement(n) ||\n ts.isForInStatement(n) ||\n ts.isWhileStatement(n)\n ) {\n found = true\n return\n }\n if (ts.isCallExpression(n) && HEAVY_METHODS.has(propAccessName(n))) {\n found = true\n return\n }\n if (\n ts.isCallExpression(n) &&\n ts.isPropertyAccessExpression(n.expression) &&\n ts.isIdentifier(n.expression.expression) &&\n n.expression.expression.text === 'JSON'\n ) {\n found = true\n return\n }\n ts.forEachChild(n, look)\n }\n look(root)\n return found\n }\n\n // --- rule matchers ---\n const ruleMapJsxChild = (node: ts.Node): void => {\n if (!ts.isJsxExpression(node) || !node.expression) return\n const parent = node.parent\n if (!parent || !(ts.isJsxElement(parent) || ts.isJsxFragment(parent))) return\n const expr = unwrapArrow(node.expression)\n if (!ts.isCallExpression(expr) || propAccessName(expr) !== 'map') return\n const cb = expr.arguments[0]\n if (!cb || !returnsJsx(cb)) return\n emit(\n node,\n 'MDC_PERF_001',\n 'List rendered with a bare .map() — on any change this region re-mounts EVERY row ' +\n '(losing focus/scroll/state). Use For({ each, key, children }) (keyed: only the diff is ' +\n 'created/moved/disposed) or List({...}) for large lists.',\n )\n }\n\n const ruleMissingKey = (node: ts.Node): void => {\n if (!ts.isCallExpression(node)) return\n const name = calleeName(node)\n if (!keyed.has(name)) return\n const arg = node.arguments[0]\n if (!arg || !ts.isObjectLiteralExpression(arg)) return\n let hasKey = false\n let hasSpread = false\n let hasEach = false\n for (const p of arg.properties) {\n if (ts.isSpreadAssignment(p)) hasSpread = true\n if (\n (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&\n p.name &&\n ts.isIdentifier(p.name)\n ) {\n if (p.name.text === 'key') hasKey = true\n if (p.name.text === 'each') hasEach = true\n }\n }\n // Only the `{ each, children }` keyed-region shape (For/keyedRegion) defaults to identity keying;\n // List/createList key differently (e.g. keyExtractor) and use a different option shape — requiring\n // `each` avoids false-positives on those.\n if (hasKey || hasSpread || !hasEach) return\n emit(\n node,\n 'MDC_PERF_002',\n `${name}() has no \\`key\\` — it defaults to object identity, so freshly-built rows never ` +\n 'match and every row is disposed + recreated (or throws on a duplicate primitive). Add ' +\n 'key: (item) => item.id (a stable id).',\n )\n }\n\n const ruleHeavyEffect = (node: ts.Node): void => {\n if (!ts.isCallExpression(node) || calleeName(node) !== 'effect') return\n // effect(fn, { priority: 'normal' }) is already deferred — exempt.\n if (node.arguments.length >= 2) return\n const fn = node.arguments[0]\n if (!fn || (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn))) return\n if (hasHeavyWork(fn.body) && readsAccessor(fn.body)) {\n emit(\n node,\n 'MDC_PERF_003',\n 'Heavy synchronous work in a default (sync-lane) effect — it runs inline on every ' +\n 'dependency write, blocking the interaction. Move derived values into computed()/memo() ' +\n \"(lazy + cached), or run heavy work on the deferred lane: effect(fn, { priority: 'normal' }).\",\n )\n }\n }\n\n const ruleRepeatedReadInLoop = (node: ts.Node): void => {\n let body: ts.Node | undefined\n if (\n ts.isForStatement(node) ||\n ts.isForOfStatement(node) ||\n ts.isForInStatement(node) ||\n ts.isWhileStatement(node)\n ) {\n body = node.statement\n } else if (\n ts.isCallExpression(node) &&\n (propAccessName(node) === 'map' || propAccessName(node) === 'forEach')\n ) {\n const cb = node.arguments[0]\n if (cb && (ts.isArrowFunction(cb) || ts.isFunctionExpression(cb))) body = cb.body\n }\n if (!body) return\n // Count zero-arg reads of each known accessor inside the loop body. A nested function resets the\n // scope (its reads run later, not per-iteration), so don't descend into one.\n const occ = new Map<string, { count: number; first: ts.Node }>()\n const walk = (n: ts.Node): void => {\n if (\n n !== body &&\n (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n))\n )\n return\n if (\n ts.isCallExpression(n) &&\n n.arguments.length === 0 &&\n ts.isIdentifier(n.expression) &&\n accessors.has(n.expression.text)\n ) {\n const id = n.expression.text\n const e = occ.get(id)\n if (e) e.count++\n else occ.set(id, { count: 1, first: n })\n }\n ts.forEachChild(n, walk)\n }\n walk(body)\n for (const [id, e] of occ) {\n if (e.count >= 2) {\n emit(\n e.first,\n 'MDC_PERF_004',\n `\\`${id}()\\` is read ${e.count}× inside a loop — each call re-reads the signal. Hoist ` +\n `it once above the loop: const v = ${id}().`,\n )\n }\n }\n }\n\n const ruleEffectNoCleanup = (node: ts.Node): void => {\n if (!ts.isCallExpression(node) || calleeName(node) !== 'effect') return\n const fn = node.arguments[0]\n if (!fn || (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn))) return\n let subscribes = false\n let hasCleanup = false\n const look = (n: ts.Node): void => {\n if (ts.isCallExpression(n)) {\n const nm = ts.isIdentifier(n.expression) ? n.expression.text : propAccessName(n)\n if (SUBSCRIBE_CALLS.test(nm)) subscribes = true\n if (ts.isIdentifier(n.expression) && n.expression.text === 'onCleanup') hasCleanup = true\n }\n // A `return <function>` OR `return <identifier>` is a teardown: effect() registers ANY returned\n // function value as cleanup (reactive.ts), so `const t = …; return t` / `return unsub` count.\n if (\n ts.isReturnStatement(n) &&\n n.expression &&\n (ts.isArrowFunction(n.expression) ||\n ts.isFunctionExpression(n.expression) ||\n ts.isIdentifier(n.expression))\n )\n hasCleanup = true\n ts.forEachChild(n, look)\n }\n look(fn.body)\n if (subscribes && !hasCleanup) {\n emit(\n node,\n 'MDC_PERF_005',\n 'effect() subscribes to an external source but returns no cleanup — the listener leaks ' +\n 'when the scope disposes. Return a teardown function or call onCleanup(() => …).',\n )\n }\n }\n\n const ruleConstFunctionStyle = (node: ts.Node): void => {\n if (!ts.isJsxAttribute(node) || !node.initializer) return\n if (!ts.isJsxExpression(node.initializer) || !node.initializer.expression) return\n const expr = node.initializer.expression\n if (!ts.isArrowFunction(expr) || ts.isBlock(expr.body)) return\n let body: ts.Expression = expr.body\n while (ts.isParenthesizedExpression(body)) body = body.expression // `() => ({...})` parens\n if (!ts.isObjectLiteralExpression(body) && !ts.isArrayLiteralExpression(body)) return\n // Only flag a PROVABLY-STATIC literal — one with NO call expression at all. A body that calls\n // anything (theme(), props.active(), select()/destructured/param accessors, helpers) may read a\n // live signal; flagging it and \"passing the literal directly\" would BREAK reactivity. Erring to\n // silence here keeps the rule from firing on the #1 styled-component pattern.\n if (!containsAnyCall(body)) {\n emit(\n node,\n 'MDC_PERF_006',\n 'A function-valued prop that returns a constant object/array (no reads) allocates a reactive ' +\n 'binding for a value that never changes. Pass the literal directly (style={{…}}).',\n )\n }\n }\n\n const ruleLargeLiteralList = (node: ts.Node): void => {\n if (ts.isJsxElement(node) || ts.isJsxFragment(node)) {\n const count = node.children.filter(\n (c) => ts.isJsxElement(c) || ts.isJsxSelfClosingElement(c) || ts.isJsxFragment(c),\n ).length\n if (count >= threshold)\n emit(\n node,\n 'MDC_PERF_007',\n `${count} static child elements rendered inline — consider List({...}) virtualization for large lists.`,\n )\n } else if (ts.isArrayLiteralExpression(node)) {\n const count = node.elements.filter(\n (c) => ts.isJsxElement(c) || ts.isJsxSelfClosingElement(c) || ts.isJsxFragment(c),\n ).length\n if (count >= threshold)\n emit(\n node,\n 'MDC_PERF_007',\n `${count} JSX elements in an array literal — consider For/List for large lists.`,\n )\n }\n }\n\n const visit = (node: ts.Node): void => {\n ruleMapJsxChild(node)\n ruleMissingKey(node)\n ruleHeavyEffect(node)\n ruleRepeatedReadInLoop(node)\n ruleEffectNoCleanup(node)\n ruleConstFunctionStyle(node)\n ruleLargeLiteralList(node)\n ts.forEachChild(node, visit)\n }\n visit(sf)\n return out\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"perf-lint.js","names":[],"sources":["../src/perf-lint.ts"],"sourcesContent":["/**\n * Build-time **perf-lint** — an opt-in pass that flags real performance footguns in the\n * fine-grained reactive + Helix render model, as `warning` {@link Diagnostic}s (it NEVER blocks the\n * build). Honest by design: every rule reports a concrete structural fact and *why* it's slow in\n * THIS model — no invented frame-time numbers. A diagnostic neither React Native nor Flutter ships.\n *\n * Enable via `compileChecked(src, { perf: true })` (or `{ perf: { rules, listSizeThreshold } }`).\n * Suppress a finding with a leading `// mdc-perf-ignore` (all) or `// mdc-perf-ignore MDC_PERF_001`\n * (one code) comment, or `rules: { MDC_PERF_001: 'off' }`.\n *\n * @module\n */\n\nimport ts from 'typescript'\nimport { scriptKindForFile } from './typecheck'\nimport type { Diagnostic } from './types'\n\n/** Per-call configuration for {@link perfLint}. */\nexport interface PerfLintOptions {\n /** Turn individual rules on/off (default: all on except `MDC_PERF_007`). */\n readonly rules?: Record<string, 'off' | 'warning'>\n /** Element count at/above which `MDC_PERF_007` fires (default 50). */\n readonly listSizeThreshold?: number\n /** Identifiers treated as keyed list builders (default For/keyedRegion/List/createList/SectionList/createSectionList). */\n readonly keyedNames?: readonly string[]\n}\n\nconst DEFAULT_KEYED = [\n 'For',\n 'keyedRegion',\n 'List',\n 'createList',\n 'SectionList',\n 'createSectionList',\n]\nconst OFF_BY_DEFAULT = new Set(['MDC_PERF_007'])\n/** A `const x = signal()|computed()|memo()|use<Capital>()` binds a reactive accessor. */\nconst ACCESSOR_FACTORY = /^(signal|computed|memo|use[A-Z])/\nconst HEAVY_METHODS = new Set([\n 'map',\n 'filter',\n 'reduce',\n 'forEach',\n 'sort',\n 'flatMap',\n 'reduceRight',\n])\nconst SUBSCRIBE_CALLS =\n /(addEventListener|setInterval|setTimeout|subscribe|^on$|observe|addListener)/\n\n/** Run the perf-lint over one module's source; returns `warning` diagnostics (possibly empty). */\nexport function perfLint(\n source: string,\n fileName = 'module.tsx',\n options: PerfLintOptions = {},\n): Diagnostic[] {\n const sf = ts.createSourceFile(\n fileName,\n source,\n ts.ScriptTarget.ES2023,\n /* setParentNodes */ true,\n scriptKindForFile(fileName),\n )\n const keyed = new Set(options.keyedNames ?? DEFAULT_KEYED)\n const threshold = options.listSizeThreshold ?? 50\n const out: Diagnostic[] = []\n\n const isEnabled = (code: string): boolean => {\n const setting = options.rules?.[code]\n if (setting) return setting !== 'off'\n return !OFF_BY_DEFAULT.has(code)\n }\n\n // --- pre-pass: the set of identifiers bound to reactive accessors (signal/computed/memo/use*) ---\n const accessors = new Set<string>()\n const addParamName = (param: ts.ParameterDeclaration | undefined): void => {\n if (param && ts.isIdentifier(param.name)) accessors.add(param.name.text)\n }\n const collectAccessors = (node: ts.Node): void => {\n if (\n ts.isVariableDeclaration(node) &&\n ts.isIdentifier(node.name) &&\n node.initializer &&\n ts.isCallExpression(node.initializer)\n ) {\n const callee = node.initializer.expression\n const name = ts.isIdentifier(callee)\n ? callee.text\n : ts.isPropertyAccessExpression(callee)\n ? callee.name.text\n : ''\n if (ACCESSOR_FACTORY.test(name)) accessors.add(node.name.text)\n }\n // A keyed builder's row callback receives accessors: For/List({ children|renderItem: (item, index) => … })\n // where item()/index() are reads. Seed those param names so rules 003/004 see them.\n if (\n ts.isCallExpression(node) &&\n keyed.has(ts.isIdentifier(node.expression) ? node.expression.text : '')\n ) {\n const arg = node.arguments[0]\n if (arg && ts.isObjectLiteralExpression(arg)) {\n for (const p of arg.properties) {\n if (\n ts.isPropertyAssignment(p) &&\n ts.isIdentifier(p.name) &&\n (p.name.text === 'children' || p.name.text === 'renderItem') &&\n (ts.isArrowFunction(p.initializer) || ts.isFunctionExpression(p.initializer))\n ) {\n addParamName(p.initializer.parameters[0]) // item accessor\n addParamName(p.initializer.parameters[1]) // index accessor\n }\n }\n }\n }\n ts.forEachChild(node, collectAccessors)\n }\n collectAccessors(sf)\n\n const positionOf = (node: ts.Node) => {\n const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf))\n return { line: line + 1, column: character + 1 }\n }\n // Line-based suppression: an `mdc-perf-ignore [CODE]` comment on the finding's line or the line\n // above. This works uniformly for `//` comments AND JSX `{/* … */}` comment children (whose comment\n // text isn't in the flagged node's leading trivia).\n const sourceLines = source.split('\\n')\n const isSuppressed = (node: ts.Node, code: string): boolean => {\n const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf))\n for (const ln of [line, line - 1]) {\n const m = /mdc-perf-ignore(?:\\s+(MDC_PERF_\\d+))?/.exec(sourceLines[ln] ?? '')\n if (m && (!m[1] || m[1] === code)) return true\n }\n return false\n }\n const emit = (node: ts.Node, code: string, message: string): void => {\n if (!isEnabled(code) || isSuppressed(node, code)) return\n out.push({ severity: 'warning', code, message, file: fileName, position: positionOf(node) })\n }\n\n // --- shared AST helpers ---\n const unwrapArrow = (expr: ts.Expression): ts.Expression => {\n // `() => X` → X (the arrow's expression body), else the node itself.\n if (ts.isArrowFunction(expr) && !ts.isBlock(expr.body)) return expr.body\n return expr\n }\n const returnsJsx = (fn: ts.Expression): boolean => {\n if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return false\n let found = false\n const look = (n: ts.Node): void => {\n if (found) return\n if (ts.isJsxElement(n) || ts.isJsxFragment(n) || ts.isJsxSelfClosingElement(n)) {\n found = true\n return\n }\n // don't descend into a NESTED function (its JSX isn't this callback's return)\n if (\n n !== fn &&\n (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n))\n )\n return\n ts.forEachChild(n, look)\n }\n look(fn.body)\n return found\n }\n const calleeName = (call: ts.CallExpression): string =>\n ts.isIdentifier(call.expression) ? call.expression.text : ''\n const propAccessName = (call: ts.CallExpression): string =>\n ts.isPropertyAccessExpression(call.expression) ? call.expression.name.text : ''\n /** Does the subtree contain ANY call expression? (A returned literal with no call is provably static.) */\n const containsAnyCall = (root: ts.Node): boolean => {\n let found = false\n const look = (n: ts.Node): void => {\n if (found) return\n if (ts.isCallExpression(n)) {\n found = true\n return\n }\n ts.forEachChild(n, look)\n }\n look(root)\n return found\n }\n /** Does the subtree reactively read a known accessor (a zero-arg call `acc()`)? */\n const readsAccessor = (root: ts.Node): boolean => {\n let found = false\n const look = (n: ts.Node): void => {\n if (found) return\n if (\n ts.isCallExpression(n) &&\n n.arguments.length === 0 &&\n ts.isIdentifier(n.expression) &&\n accessors.has(n.expression.text)\n ) {\n found = true\n return\n }\n ts.forEachChild(n, look)\n }\n look(root)\n return found\n }\n /** Does the subtree contain a heavy synchronous construct (loop / array-method chain / JSON)? */\n const hasHeavyWork = (root: ts.Node): boolean => {\n let found = false\n const look = (n: ts.Node): void => {\n if (found) return\n if (\n ts.isForStatement(n) ||\n ts.isForOfStatement(n) ||\n ts.isForInStatement(n) ||\n ts.isWhileStatement(n)\n ) {\n found = true\n return\n }\n if (ts.isCallExpression(n) && HEAVY_METHODS.has(propAccessName(n))) {\n found = true\n return\n }\n if (\n ts.isCallExpression(n) &&\n ts.isPropertyAccessExpression(n.expression) &&\n ts.isIdentifier(n.expression.expression) &&\n n.expression.expression.text === 'JSON'\n ) {\n found = true\n return\n }\n ts.forEachChild(n, look)\n }\n look(root)\n return found\n }\n\n // --- rule matchers ---\n const ruleMapJsxChild = (node: ts.Node): void => {\n if (!ts.isJsxExpression(node) || !node.expression) return\n const parent = node.parent\n if (!parent || !(ts.isJsxElement(parent) || ts.isJsxFragment(parent))) return\n const expr = unwrapArrow(node.expression)\n if (!ts.isCallExpression(expr) || propAccessName(expr) !== 'map') return\n const cb = expr.arguments[0]\n if (!cb || !returnsJsx(cb)) return\n emit(\n node,\n 'MDC_PERF_001',\n 'List rendered with a bare .map() — on any change this region re-mounts EVERY row ' +\n '(losing focus/scroll/state). Use For({ each, key, children }) (keyed: only the diff is ' +\n 'created/moved/disposed) or List({...}) for large lists.',\n )\n }\n\n const ruleMissingKey = (node: ts.Node): void => {\n if (!ts.isCallExpression(node)) return\n const name = calleeName(node)\n if (!keyed.has(name)) return\n const arg = node.arguments[0]\n if (!arg || !ts.isObjectLiteralExpression(arg)) return\n let hasKey = false\n let hasSpread = false\n let hasEach = false\n for (const p of arg.properties) {\n if (ts.isSpreadAssignment(p)) hasSpread = true\n if (\n (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&\n p.name &&\n ts.isIdentifier(p.name)\n ) {\n if (p.name.text === 'key') hasKey = true\n if (p.name.text === 'each') hasEach = true\n }\n }\n // Only the `{ each, children }` keyed-region shape (For/keyedRegion) defaults to identity keying;\n // List/createList key differently (e.g. keyExtractor) and use a different option shape — requiring\n // `each` avoids false-positives on those.\n if (hasKey || hasSpread || !hasEach) return\n emit(\n node,\n 'MDC_PERF_002',\n `${name}() has no \\`key\\` — it defaults to object identity, so freshly-built rows never ` +\n 'match and every row is disposed + recreated (or throws on a duplicate primitive). Add ' +\n 'key: (item) => item.id (a stable id).',\n )\n }\n\n const ruleHeavyEffect = (node: ts.Node): void => {\n if (!ts.isCallExpression(node) || calleeName(node) !== 'effect') return\n // effect(fn, { priority: 'normal' }) is already deferred — exempt.\n if (node.arguments.length >= 2) return\n const fn = node.arguments[0]\n if (!fn || (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn))) return\n if (hasHeavyWork(fn.body) && readsAccessor(fn.body)) {\n emit(\n node,\n 'MDC_PERF_003',\n 'Heavy synchronous work in a default (sync-lane) effect — it runs inline on every ' +\n 'dependency write, blocking the interaction. Move derived values into computed()/memo() ' +\n \"(lazy + cached), or run heavy work on the deferred lane: effect(fn, { priority: 'normal' }).\",\n )\n }\n }\n\n const ruleRepeatedReadInLoop = (node: ts.Node): void => {\n let body: ts.Node | undefined\n if (\n ts.isForStatement(node) ||\n ts.isForOfStatement(node) ||\n ts.isForInStatement(node) ||\n ts.isWhileStatement(node)\n ) {\n body = node.statement\n } else if (\n ts.isCallExpression(node) &&\n (propAccessName(node) === 'map' || propAccessName(node) === 'forEach')\n ) {\n const cb = node.arguments[0]\n if (cb && (ts.isArrowFunction(cb) || ts.isFunctionExpression(cb))) body = cb.body\n }\n if (!body) return\n // Count zero-arg reads of each known accessor inside the loop body. A nested function resets the\n // scope (its reads run later, not per-iteration), so don't descend into one.\n const occ = new Map<string, { count: number; first: ts.Node }>()\n const walk = (n: ts.Node): void => {\n if (\n n !== body &&\n (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n))\n )\n return\n if (\n ts.isCallExpression(n) &&\n n.arguments.length === 0 &&\n ts.isIdentifier(n.expression) &&\n accessors.has(n.expression.text)\n ) {\n const id = n.expression.text\n const e = occ.get(id)\n if (e) e.count++\n else occ.set(id, { count: 1, first: n })\n }\n ts.forEachChild(n, walk)\n }\n walk(body)\n for (const [id, e] of occ) {\n if (e.count >= 2) {\n emit(\n e.first,\n 'MDC_PERF_004',\n `\\`${id}()\\` is read ${e.count}× inside a loop — each call re-reads the signal. Hoist ` +\n `it once above the loop: const v = ${id}().`,\n )\n }\n }\n }\n\n const ruleEffectNoCleanup = (node: ts.Node): void => {\n if (!ts.isCallExpression(node) || calleeName(node) !== 'effect') return\n const fn = node.arguments[0]\n if (!fn || (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn))) return\n let subscribes = false\n let hasCleanup = false\n const look = (n: ts.Node): void => {\n if (ts.isCallExpression(n)) {\n const nm = ts.isIdentifier(n.expression) ? n.expression.text : propAccessName(n)\n if (SUBSCRIBE_CALLS.test(nm)) subscribes = true\n if (ts.isIdentifier(n.expression) && n.expression.text === 'onCleanup') hasCleanup = true\n }\n // A `return <function>` OR `return <identifier>` is a teardown: effect() registers ANY returned\n // function value as cleanup (reactive.ts), so `const t = …; return t` / `return unsub` count.\n if (\n ts.isReturnStatement(n) &&\n n.expression &&\n (ts.isArrowFunction(n.expression) ||\n ts.isFunctionExpression(n.expression) ||\n ts.isIdentifier(n.expression))\n )\n hasCleanup = true\n ts.forEachChild(n, look)\n }\n look(fn.body)\n if (subscribes && !hasCleanup) {\n emit(\n node,\n 'MDC_PERF_005',\n 'effect() subscribes to an external source but returns no cleanup — the listener leaks ' +\n 'when the scope disposes. Return a teardown function or call onCleanup(() => …).',\n )\n }\n }\n\n const ruleConstFunctionStyle = (node: ts.Node): void => {\n if (!ts.isJsxAttribute(node) || !node.initializer) return\n if (!ts.isJsxExpression(node.initializer) || !node.initializer.expression) return\n const expr = node.initializer.expression\n if (!ts.isArrowFunction(expr) || ts.isBlock(expr.body)) return\n let body: ts.Expression = expr.body\n while (ts.isParenthesizedExpression(body)) body = body.expression // `() => ({...})` parens\n if (!ts.isObjectLiteralExpression(body) && !ts.isArrayLiteralExpression(body)) return\n // Only flag a PROVABLY-STATIC literal — one with NO call expression at all. A body that calls\n // anything (theme(), props.active(), select()/destructured/param accessors, helpers) may read a\n // live signal; flagging it and \"passing the literal directly\" would BREAK reactivity. Erring to\n // silence here keeps the rule from firing on the #1 styled-component pattern.\n if (!containsAnyCall(body)) {\n emit(\n node,\n 'MDC_PERF_006',\n 'A function-valued prop that returns a constant object/array (no reads) allocates a reactive ' +\n 'binding for a value that never changes. Pass the literal directly (style={{…}}).',\n )\n }\n }\n\n const ruleLargeLiteralList = (node: ts.Node): void => {\n if (ts.isJsxElement(node) || ts.isJsxFragment(node)) {\n const count = node.children.filter(\n (c) => ts.isJsxElement(c) || ts.isJsxSelfClosingElement(c) || ts.isJsxFragment(c),\n ).length\n if (count >= threshold)\n emit(\n node,\n 'MDC_PERF_007',\n `${count} static child elements rendered inline — consider List({...}) virtualization for large lists.`,\n )\n } else if (ts.isArrayLiteralExpression(node)) {\n const count = node.elements.filter(\n (c) => ts.isJsxElement(c) || ts.isJsxSelfClosingElement(c) || ts.isJsxFragment(c),\n ).length\n if (count >= threshold)\n emit(\n node,\n 'MDC_PERF_007',\n `${count} JSX elements in an array literal — consider For/List for large lists.`,\n )\n }\n }\n\n const visit = (node: ts.Node): void => {\n ruleMapJsxChild(node)\n ruleMissingKey(node)\n ruleHeavyEffect(node)\n ruleRepeatedReadInLoop(node)\n ruleEffectNoCleanup(node)\n ruleConstFunctionStyle(node)\n ruleLargeLiteralList(node)\n ts.forEachChild(node, visit)\n }\n visit(sf)\n return out\n}\n"],"mappings":"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"}
|
package/dist/transform.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transform.d.ts","names":[],"sources":["../src/transform.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"transform.d.ts","names":[],"sources":["../src/transform.ts"],"mappings":";;;AAgGoF;AAuDpF;;;;;;;AAvDoF,iBAApE,OAAA,CAAQ,MAAA,UAAgB,OAAA,GAAS,cAAA,GAAsB,aAAa;;AAuDO;;;iBAA3E,cAAA,CAAe,MAAA,UAAgB,OAAA,GAAS,cAAA,GAAsB,aAAa"}
|
package/dist/transform.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkBudget } from "./budget.js";
|
|
1
2
|
import { createFlattenTransformer } from "./flatten.js";
|
|
2
3
|
import { hasErrors, typecheck } from "./typecheck.js";
|
|
3
4
|
import { perfLint } from "./perf-lint.js";
|
|
@@ -115,13 +116,24 @@ function compileChecked(source, options = {}) {
|
|
|
115
116
|
};
|
|
116
117
|
const compiled = compile(source, options);
|
|
117
118
|
const perfDiagnostics = options.perf ? perfLint(source, fileName, typeof options.perf === "object" ? options.perf : {}) : [];
|
|
119
|
+
const budgetDiagnostics = options.budget ? checkBudget(compiled, options.budget).map((d) => ({
|
|
120
|
+
...d,
|
|
121
|
+
file: fileName
|
|
122
|
+
})) : [];
|
|
123
|
+
const allDiagnostics = [
|
|
124
|
+
...diagnostics,
|
|
125
|
+
...perfDiagnostics,
|
|
126
|
+
...budgetDiagnostics,
|
|
127
|
+
...compiled.diagnostics
|
|
128
|
+
];
|
|
129
|
+
if (hasErrors(budgetDiagnostics)) return {
|
|
130
|
+
code: "",
|
|
131
|
+
diagnostics: allDiagnostics,
|
|
132
|
+
stats: compiled.stats
|
|
133
|
+
};
|
|
118
134
|
return {
|
|
119
135
|
...compiled,
|
|
120
|
-
diagnostics:
|
|
121
|
-
...diagnostics,
|
|
122
|
-
...perfDiagnostics,
|
|
123
|
-
...compiled.diagnostics
|
|
124
|
-
]
|
|
136
|
+
diagnostics: allDiagnostics
|
|
125
137
|
};
|
|
126
138
|
}
|
|
127
139
|
//#endregion
|
package/dist/transform.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transform.js","names":[],"sources":["../src/transform.ts"],"sourcesContent":["/**\n * The MDC transform/compile pipeline.\n *\n * `compile()` lowers TSX → `createElement(...)` (matching `@mindees/core`'s\n * factory), runs the built-in optimizer passes (tree-flattening) plus any user\n * plugins, and emits JavaScript + a source map. It does **not** type-check\n * (that's {@link typecheck}); `compileChecked()` runs the gate first and refuses\n * to emit on `error` diagnostics.\n *\n * @module\n */\n\nimport ts from 'typescript'\nimport { createFlattenTransformer } from './flatten'\nimport { perfLint } from './perf-lint'\nimport { hasErrors, typecheck } from './typecheck'\nimport type { CompileOptions, CompileResult, CompileStats } from './types'\n\n/** Compiler options for emit (JSX → `createElement`/`Fragment`, which the optimizer matches). */\nfunction emitOptions(sourceMap: boolean): ts.CompilerOptions {\n return {\n jsx: ts.JsxEmit.React,\n jsxFactory: 'createElement',\n jsxFragmentFactory: 'Fragment',\n target: ts.ScriptTarget.ES2023,\n module: ts.ModuleKind.ESNext,\n sourceMap,\n }\n}\n\n/** Runtime names the JSX desugar references; injected from `@mindees/core` if unbound. */\nconst RUNTIME_NAMES = ['createElement', 'Fragment'] as const\n\n/**\n * Ensure the JSX runtime is in scope. Idiomatic components use **automatic JSX** and import\n * nothing, but we emit classic `createElement(...)`/`Fragment` (so the tree-flatten optimizer\n * can match them) — which would be unbound at runtime. This transformer prepends\n * `import { createElement, Fragment } from '@mindees/core'` for any runtime name that is\n * referenced but not already imported, so emitted modules run. Runs LAST (after flatten/plugins),\n * so names the optimizer removed don't get a needless import.\n */\nfunction createRuntimeImportTransformer(tsmod: typeof ts): ts.TransformerFactory<ts.SourceFile> {\n return (context) => (sourceFile) => {\n const imported = new Set<string>()\n for (const stmt of sourceFile.statements) {\n if (\n tsmod.isImportDeclaration(stmt) &&\n tsmod.isStringLiteral(stmt.moduleSpecifier) &&\n stmt.moduleSpecifier.text === '@mindees/core'\n ) {\n const named = stmt.importClause?.namedBindings\n if (named && tsmod.isNamedImports(named)) {\n for (const el of named.elements) imported.add((el.propertyName ?? el.name).text)\n }\n }\n }\n const referenced = new Set<string>()\n const visit = (node: ts.Node): void => {\n if (tsmod.isIdentifier(node) && (RUNTIME_NAMES as readonly string[]).includes(node.text)) {\n referenced.add(node.text)\n }\n tsmod.forEachChild(node, visit)\n }\n visit(sourceFile)\n const missing = RUNTIME_NAMES.filter((n) => referenced.has(n) && !imported.has(n))\n if (missing.length === 0) return sourceFile\n const importDecl = tsmod.factory.createImportDeclaration(\n undefined,\n tsmod.factory.createImportClause(\n false,\n undefined,\n tsmod.factory.createNamedImports(\n missing.map((n) =>\n tsmod.factory.createImportSpecifier(\n false,\n undefined,\n tsmod.factory.createIdentifier(n),\n ),\n ),\n ),\n ),\n tsmod.factory.createStringLiteral('@mindees/core'),\n )\n return context.factory.updateSourceFile(sourceFile, [importDecl, ...sourceFile.statements])\n }\n}\n\n/**\n * Compile a single TSX/TS module to JavaScript.\n *\n * Pipeline: JSX desugar → tree-flatten (optional) → user plugins → emit.\n * Returns emitted code, an optional source map, any (transpile-level)\n * diagnostics, and optimizer stats. Use {@link compileChecked} to gate on the\n * full type checker.\n */\nexport function compile(source: string, options: CompileOptions = {}): CompileResult {\n const { fileName = 'module.tsx', sourceMap = true, flatten = true, plugins = [] } = options\n\n // IMPORTANT: our optimizer + plugins operate on the desugared\n // `createElement(...)` call form, but `transpileModule` runs `before`\n // transformers on the *pre-desugar* JSX AST. JSX is lowered during the\n // `after` phase, so flatten/plugins must run there to see the calls.\n const after: ts.TransformerFactory<ts.SourceFile>[] = []\n let stats: CompileStats = { flattenedNodes: 0, totalElements: 0 }\n\n if (flatten) {\n const flattener = createFlattenTransformer(ts)\n after.push(flattener.factory)\n stats = flattener.stats // live object, updated during emit\n }\n\n for (const plugin of plugins) {\n after.push(plugin.transformer(ts) as ts.TransformerFactory<ts.SourceFile>)\n }\n\n // LAST: bind the JSX runtime (automatic-JSX components import nothing) so output runs.\n after.push(createRuntimeImportTransformer(ts))\n\n const output = ts.transpileModule(source, {\n compilerOptions: emitOptions(sourceMap),\n fileName,\n reportDiagnostics: true,\n transformers: { after },\n })\n\n // transpileModule only surfaces a few syntactic diagnostics; semantic ones\n // come from the type-check gate. Map each to our structured form.\n const diagnostics = (output.diagnostics ?? []).map((d) => {\n const message = ts.flattenDiagnosticMessageText(d.messageText, '\\n')\n return {\n severity:\n d.category === ts.DiagnosticCategory.Error ? ('error' as const) : ('warning' as const),\n code: `TS${d.code}`,\n message,\n }\n })\n\n const result: CompileResult = {\n code: output.outputText,\n diagnostics,\n stats,\n }\n if (sourceMap && output.sourceMapText) result.map = output.sourceMapText\n return result\n}\n\n/**\n * Type-check then compile. If the gate finds any `error` diagnostic, returns it\n * WITHOUT emitting code (`code: ''`) — the build must not ship type errors.\n */\nexport function compileChecked(source: string, options: CompileOptions = {}): CompileResult {\n const fileName = options.fileName ?? 'module.tsx'\n const diagnostics = typecheck(source, fileName)\n if (hasErrors(diagnostics)) {\n return { code: '', diagnostics, stats: { flattenedNodes: 0, totalElements: 0 } }\n }\n const compiled = compile(source, options)\n // Opt-in build-time perf-lint: warnings only (never blocks — the gate above already returned on\n // errors, and every perf diagnostic is severity 'warning').\n const perfDiagnostics = options.perf\n ? perfLint(source, fileName, typeof options.perf === 'object' ? options.perf : {})\n : []\n //
|
|
1
|
+
{"version":3,"file":"transform.js","names":[],"sources":["../src/transform.ts"],"sourcesContent":["/**\n * The MDC transform/compile pipeline.\n *\n * `compile()` lowers TSX → `createElement(...)` (matching `@mindees/core`'s\n * factory), runs the built-in optimizer passes (tree-flattening) plus any user\n * plugins, and emits JavaScript + a source map. It does **not** type-check\n * (that's {@link typecheck}); `compileChecked()` runs the gate first and refuses\n * to emit on `error` diagnostics.\n *\n * @module\n */\n\nimport ts from 'typescript'\nimport { checkBudget } from './budget'\nimport { createFlattenTransformer } from './flatten'\nimport { perfLint } from './perf-lint'\nimport { hasErrors, typecheck } from './typecheck'\nimport type { CompileOptions, CompileResult, CompileStats } from './types'\n\n/** Compiler options for emit (JSX → `createElement`/`Fragment`, which the optimizer matches). */\nfunction emitOptions(sourceMap: boolean): ts.CompilerOptions {\n return {\n jsx: ts.JsxEmit.React,\n jsxFactory: 'createElement',\n jsxFragmentFactory: 'Fragment',\n target: ts.ScriptTarget.ES2023,\n module: ts.ModuleKind.ESNext,\n sourceMap,\n }\n}\n\n/** Runtime names the JSX desugar references; injected from `@mindees/core` if unbound. */\nconst RUNTIME_NAMES = ['createElement', 'Fragment'] as const\n\n/**\n * Ensure the JSX runtime is in scope. Idiomatic components use **automatic JSX** and import\n * nothing, but we emit classic `createElement(...)`/`Fragment` (so the tree-flatten optimizer\n * can match them) — which would be unbound at runtime. This transformer prepends\n * `import { createElement, Fragment } from '@mindees/core'` for any runtime name that is\n * referenced but not already imported, so emitted modules run. Runs LAST (after flatten/plugins),\n * so names the optimizer removed don't get a needless import.\n */\nfunction createRuntimeImportTransformer(tsmod: typeof ts): ts.TransformerFactory<ts.SourceFile> {\n return (context) => (sourceFile) => {\n const imported = new Set<string>()\n for (const stmt of sourceFile.statements) {\n if (\n tsmod.isImportDeclaration(stmt) &&\n tsmod.isStringLiteral(stmt.moduleSpecifier) &&\n stmt.moduleSpecifier.text === '@mindees/core'\n ) {\n const named = stmt.importClause?.namedBindings\n if (named && tsmod.isNamedImports(named)) {\n for (const el of named.elements) imported.add((el.propertyName ?? el.name).text)\n }\n }\n }\n const referenced = new Set<string>()\n const visit = (node: ts.Node): void => {\n if (tsmod.isIdentifier(node) && (RUNTIME_NAMES as readonly string[]).includes(node.text)) {\n referenced.add(node.text)\n }\n tsmod.forEachChild(node, visit)\n }\n visit(sourceFile)\n const missing = RUNTIME_NAMES.filter((n) => referenced.has(n) && !imported.has(n))\n if (missing.length === 0) return sourceFile\n const importDecl = tsmod.factory.createImportDeclaration(\n undefined,\n tsmod.factory.createImportClause(\n false,\n undefined,\n tsmod.factory.createNamedImports(\n missing.map((n) =>\n tsmod.factory.createImportSpecifier(\n false,\n undefined,\n tsmod.factory.createIdentifier(n),\n ),\n ),\n ),\n ),\n tsmod.factory.createStringLiteral('@mindees/core'),\n )\n return context.factory.updateSourceFile(sourceFile, [importDecl, ...sourceFile.statements])\n }\n}\n\n/**\n * Compile a single TSX/TS module to JavaScript.\n *\n * Pipeline: JSX desugar → tree-flatten (optional) → user plugins → emit.\n * Returns emitted code, an optional source map, any (transpile-level)\n * diagnostics, and optimizer stats. Use {@link compileChecked} to gate on the\n * full type checker.\n */\nexport function compile(source: string, options: CompileOptions = {}): CompileResult {\n const { fileName = 'module.tsx', sourceMap = true, flatten = true, plugins = [] } = options\n\n // IMPORTANT: our optimizer + plugins operate on the desugared\n // `createElement(...)` call form, but `transpileModule` runs `before`\n // transformers on the *pre-desugar* JSX AST. JSX is lowered during the\n // `after` phase, so flatten/plugins must run there to see the calls.\n const after: ts.TransformerFactory<ts.SourceFile>[] = []\n let stats: CompileStats = { flattenedNodes: 0, totalElements: 0 }\n\n if (flatten) {\n const flattener = createFlattenTransformer(ts)\n after.push(flattener.factory)\n stats = flattener.stats // live object, updated during emit\n }\n\n for (const plugin of plugins) {\n after.push(plugin.transformer(ts) as ts.TransformerFactory<ts.SourceFile>)\n }\n\n // LAST: bind the JSX runtime (automatic-JSX components import nothing) so output runs.\n after.push(createRuntimeImportTransformer(ts))\n\n const output = ts.transpileModule(source, {\n compilerOptions: emitOptions(sourceMap),\n fileName,\n reportDiagnostics: true,\n transformers: { after },\n })\n\n // transpileModule only surfaces a few syntactic diagnostics; semantic ones\n // come from the type-check gate. Map each to our structured form.\n const diagnostics = (output.diagnostics ?? []).map((d) => {\n const message = ts.flattenDiagnosticMessageText(d.messageText, '\\n')\n return {\n severity:\n d.category === ts.DiagnosticCategory.Error ? ('error' as const) : ('warning' as const),\n code: `TS${d.code}`,\n message,\n }\n })\n\n const result: CompileResult = {\n code: output.outputText,\n diagnostics,\n stats,\n }\n if (sourceMap && output.sourceMapText) result.map = output.sourceMapText\n return result\n}\n\n/**\n * Type-check then compile. If the gate finds any `error` diagnostic, returns it\n * WITHOUT emitting code (`code: ''`) — the build must not ship type errors.\n */\nexport function compileChecked(source: string, options: CompileOptions = {}): CompileResult {\n const fileName = options.fileName ?? 'module.tsx'\n const diagnostics = typecheck(source, fileName)\n if (hasErrors(diagnostics)) {\n return { code: '', diagnostics, stats: { flattenedNodes: 0, totalElements: 0 } }\n }\n const compiled = compile(source, options)\n // Opt-in build-time perf-lint: warnings only (never blocks — the gate above already returned on\n // errors, and every perf diagnostic is severity 'warning').\n const perfDiagnostics = options.perf\n ? perfLint(source, fileName, typeof options.perf === 'object' ? options.perf : {})\n : []\n // Performance budget (spec §12): violations are ERRORS that refuse to emit — \"100% optimized,\n // enforced.\" Attach the budget errors to a file so editors surface them.\n const budgetDiagnostics = options.budget\n ? checkBudget(compiled, options.budget).map((d) => ({ ...d, file: fileName }))\n : []\n const allDiagnostics = [\n ...diagnostics,\n ...perfDiagnostics,\n ...budgetDiagnostics,\n ...compiled.diagnostics,\n ]\n if (hasErrors(budgetDiagnostics)) {\n // Over budget → refuse to emit (same contract as the type-check gate above).\n return { code: '', diagnostics: allDiagnostics, stats: compiled.stats }\n }\n return { ...compiled, diagnostics: allDiagnostics }\n}\n"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PerfLintOptions } from "./perf-lint.js";
|
|
2
|
+
import { BudgetOptions } from "./budget.js";
|
|
2
3
|
|
|
3
4
|
//#region src/types.d.ts
|
|
4
5
|
/** Severity of a {@link Diagnostic}. */
|
|
@@ -55,6 +56,11 @@ interface CompileOptions {
|
|
|
55
56
|
* render footguns; never blocks the build. `true` for defaults, or pass {@link PerfLintOptions}.
|
|
56
57
|
*/
|
|
57
58
|
perf?: boolean | PerfLintOptions;
|
|
59
|
+
/**
|
|
60
|
+
* Enforce a performance budget (`compileChecked` only): a violation is an **error** that refuses to
|
|
61
|
+
* emit (spec §12 — "100% optimized, enforced"). See {@link BudgetOptions}.
|
|
62
|
+
*/
|
|
63
|
+
budget?: BudgetOptions;
|
|
58
64
|
}
|
|
59
65
|
/**
|
|
60
66
|
* A transform plugin. Plugins operate on the desugared `createElement(...)` call
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;KAUY,kBAAA;AAGZ;AAAA,UAAiB,cAAA;;EAEf,IAAA;EAEM;EAAN,MAAM;AAAA;;UAIS,UAAA;EACf,QAAA,EAAU,kBAAA;EAAA;EAEV,IAAA;EAEA;EAAA,OAAA;EAIA;EAFA,IAAA;EAEyB;EAAzB,QAAA,GAAW,cAAc;AAAA;;UAIV,aAAA;EAQI;EANnB,IAAA;EAEA;EAAA,GAAA;EAEa;EAAb,WAAA,EAAa,UAAA;EAEN;EAAP,KAAA,EAAO,YAAY;AAAA;AAIrB;AAAA,UAAiB,YAAA;;EAEf,cAAA;EAEa;EAAb,aAAa;AAAA;;UAIE,cAAA;EAaE;EAXjB,QAAA;EAgBsB;EAdtB,SAAA;EAFA;EAIA,OAAA;EAAA;EAEA,OAAA,GAAU,SAAA;EAAA;;;;EAKV,IAAA,aAAiB,eAAA;EAKK;AAAA;AAYxB;;EAZE,MAAA,GAAS,aAAA;AAAA;;;;;AAoBoC;;;;;UAR9B,SAAA;;EAEf,IAAA;;;;;;EAMA,WAAA,GAAc,EAAA;AAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindees/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "MindeesNative Compiler (MDC) — build-time optimizer: type-check gate, TSX→createElement transform, tree-flattening, per-route code-splitting, and a plugin API. TS→native AOT is a research track.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"typescript": "6.0.3",
|
|
27
|
-
"@mindees/core": "0.
|
|
27
|
+
"@mindees/core": "0.10.0"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsdown",
|