@starci/eslint-canon-fe 8.2.18 → 8.3.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 +9 -0
- package/README.md +19 -1
- package/docs/size-and-state-budget.md +6 -6
- package/drawing.mjs +65 -28
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
- package/runtime/knowledge/hfs/slots.yaml +99 -43
- package/runtime/knowledge/patterns/fe/folder.yaml +3 -0
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/git/diff.mjs +5 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +110 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +20 -3
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
- package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
- package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
- package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
- package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +14 -9
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +21 -12
- package/runtime/scripts/hfs/architecture/register-once.mjs +13 -5
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +3 -6
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/tiers.mjs +8 -1
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +1 -11
- package/runtime/scripts/hfs/manifest-shape.mjs +27 -2
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
- package/runtime/scripts/hfs/slot-imports.mjs +3 -1
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/lib/ts-analysis.mjs +20 -0
- package/size-and-state-budget.mjs +11 -8
|
@@ -323,6 +323,12 @@ function runtimeParamProblems(rp) {
|
|
|
323
323
|
];
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
const folderKindsValid = list => strList(list) && list.length > 0 && new Set(list).size === list.length && list.every(kind => NAME.test(kind));
|
|
327
|
+
const kindPairValid = pair => isPlainObject(pair) && Object.keys(pair).sort(byCodeUnit).join() === 'from,to'
|
|
328
|
+
&& ['from', 'to'].every(side => folderKindsValid(pair[side]));
|
|
329
|
+
const kindRestrictionValid = kinds => isPlainObject(kinds) && Object.keys(kinds).length > 0
|
|
330
|
+
&& Object.entries(kinds).every(([target, pair]) => NAME.test(target) && kindPairValid(pair));
|
|
331
|
+
|
|
326
332
|
/** Shape problems of the tier map of one profile. */
|
|
327
333
|
export function tierMapProblems(profile, tiers) {
|
|
328
334
|
const bad = [];
|
|
@@ -330,14 +336,33 @@ export function tierMapProblems(profile, tiers) {
|
|
|
330
336
|
for (const [tier, def] of Object.entries(tiers)) {
|
|
331
337
|
if (!NAME.test(tier)) bad.push(`tiers.${profile}.${tier} is not a tier name`);
|
|
332
338
|
if (!isPlainObject(def) || !Array.isArray(def.mayImport) || !def.mayImport.every((t) => NAME.test(String(t)))) bad.push(`tiers.${profile}.${tier}.mayImport must be a list of tier names`);
|
|
333
|
-
|
|
339
|
+
if (def?.mayImportKinds !== undefined && !kindRestrictionValid(def.mayImportKinds)) bad.push(`tiers.${profile}.${tier}.mayImportKinds must map tier names to {from, to} non-empty lists of unique folder kinds`);
|
|
340
|
+
for (const key of Object.keys(def ?? {})) if (!['mayImport', 'mayImportKinds', 'acyclic', 'lowerLayerOnly'].includes(key)) bad.push(`tiers.${profile}.${tier}.${key} is not a tier field`);
|
|
341
|
+
}
|
|
342
|
+
return bad;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function tierFolderKindProblems(manifest, profile, tier, kinds, at) {
|
|
346
|
+
const known = new Set(manifest.slots.filter((slot) => slot.profiles.includes(profile) && slot.tier === tier).flatMap((slot) => [...(slot.kinds ?? []), ...(slot.layers ?? [])]));
|
|
347
|
+
return kinds.filter(kind => !known.has(kind)).map(kind => `${at} names ${kind}, which is not a folder kind of the ${tier} tier`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Kind restrictions narrow an allowed tier edge and name kinds that its source and target slots actually declare. */
|
|
351
|
+
export function tierKindProblems(manifest, profile) {
|
|
352
|
+
const bad = [];
|
|
353
|
+
for (const [tier, def] of Object.entries(manifest.tiers[profile])) {
|
|
354
|
+
for (const [target, kinds] of Object.entries(def.mayImportKinds ?? {})) {
|
|
355
|
+
const at = `tiers.${profile}.${tier}.mayImportKinds.${target}`;
|
|
356
|
+
if (!def.mayImport.includes(target)) bad.push(`${at} restricts a tier that mayImport does not allow`);
|
|
357
|
+
bad.push(...tierFolderKindProblems(manifest, profile, tier, kinds.from, `${at}.from`), ...tierFolderKindProblems(manifest, profile, target, kinds.to, `${at}.to`));
|
|
358
|
+
}
|
|
334
359
|
}
|
|
335
360
|
return bad;
|
|
336
361
|
}
|
|
337
362
|
|
|
338
363
|
/** The semantic rules of a runtime manifest beyond the slot rules both kinds share: tier names and the schema major. */
|
|
339
364
|
export function runtimeSemanticProblems(m) {
|
|
340
|
-
const bad =
|
|
365
|
+
const bad = tierKindProblems(m, RUNTIME_KIND);
|
|
341
366
|
const tiers = m.tiers.runtime;
|
|
342
367
|
for (const [tier, def] of Object.entries(tiers)) {
|
|
343
368
|
for (const target of def.mayImport) if (!(target in tiers)) bad.push(`tiers.runtime.${tier}.mayImport names ${target}, which is not a runtime tier`);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// (scripts/hfs/project-graph.mjs) runs it over the same tree and serves the findings on a TypeScript file as ESLint reports.
|
|
4
4
|
// HFS_SLOT_UNDECLARED / HFS_SLOT_AMBIGUOUS / HFS_SLOT_NOT_ENABLED / HFS_FORBIDDEN_PRESENT / HFS_TOOL_CONFIG_LOCAL / HFS_TRACKED_MUST_BE_IGNORED (R01, R16)
|
|
5
5
|
// BE_SOURCE_FORM (R89, the file name), BE_SPEC_PLACEMENT (R102)
|
|
6
|
+
import fs from 'node:fs';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { allowsFile } from './allows.mjs';
|
|
8
9
|
import { isFeTestPath } from './rules/fe-no-tests.mjs';
|
|
@@ -30,8 +31,11 @@ function sourceFormContext(file, resolver, suffixes, bannedSuffixes) {
|
|
|
30
31
|
const slot = resolver.slot(c.slot);
|
|
31
32
|
if ([...(slot?.requires ?? []), ...(slot?.allows ?? [])].includes(base)) return null;
|
|
32
33
|
const admitted = allowsFile(resolver, file);
|
|
34
|
+
// Environment parsing is an app slot role, not a source suffix admitted outside that matching app file.
|
|
35
|
+
if (c.slot.startsWith('be.app.') && admitted?.allowed && admitted.entry === '<app>.env.ts') return null;
|
|
33
36
|
if (admitted?.allowed && admitted.entry?.includes('/') && path.posix.basename(admitted.entry) === base) return null;
|
|
34
|
-
|
|
37
|
+
// A plain entry names its file by a variable only (`<x>.ts`, or `compose/<area>.ts` of an app slot): a kebab name is its form.
|
|
38
|
+
if (admitted?.allowed && PLAIN_ENTRY.test(path.posix.basename(admitted.entry ?? '')) && KEBAB.test(base.slice(0, -'.ts'.length))) return null;
|
|
35
39
|
if (c.slot === 'be.persistence' && path.posix.basename(path.posix.dirname(file)) === 'migrations') return null;
|
|
36
40
|
return { c, base, suffixes, bannedSuffixes };
|
|
37
41
|
}
|
|
@@ -54,7 +58,7 @@ function sourceFormFinding(file, { c, base, suffixes, bannedSuffixes }, boundSuf
|
|
|
54
58
|
|
|
55
59
|
/**
|
|
56
60
|
* BE_SOURCE_FORM (R89): every tracked src/ or apps/ TypeScript file of a back end is index.ts, main.ts, a migration of
|
|
57
|
-
* be.persistence, or <kebab-name>.<suffix>.ts with <suffix> in the closed vocabulary ruleParams.be.suffixes (a name such
|
|
61
|
+
* be.persistence, the matching <app>.env.ts explicitly admitted by its app slot, or <kebab-name>.<suffix>.ts with <suffix> in the closed vocabulary ruleParams.be.suffixes (a name such
|
|
58
62
|
* as order.service.spec.ts keeps its inner words kebab-case). A suffix of ruleParams.be.bannedSuffixes anywhere in the
|
|
59
63
|
* name is refused by name. Paths no slot owns are HFS_SLOT_UNDECLARED's, not this code's.
|
|
60
64
|
*/
|
|
@@ -86,6 +90,8 @@ function forbiddenPathFinding(file, c, resolver) {
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
function pathStatusFinding(file, resolver) {
|
|
93
|
+
const liteWork = liteStarciworkFinding(file, resolver);
|
|
94
|
+
if (liteWork) return liteWork;
|
|
89
95
|
const c = resolver.classifyPath(file);
|
|
90
96
|
if (c.status === 'no-slot') {
|
|
91
97
|
const nearestNote = c.nearest ? `; nearest slot ${c.nearest.slot} (${c.nearest.pattern}), matched ${c.nearest.matchedPrefix || '.'} then expected ${c.nearest.expectedNext ?? 'nothing'}` : '';
|
|
@@ -98,6 +104,30 @@ function pathStatusFinding(file, resolver) {
|
|
|
98
104
|
return null;
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
/** The lite Work boundary, read from app.starciwork's edition-specific admission list, including paths a narrower slot owns. */
|
|
108
|
+
export function liteStarciworkFinding(file, resolver, { dir = false } = {}) {
|
|
109
|
+
if (resolver.repo?.edition !== 'lite' || !resolver.slot('app.starciwork')) return null;
|
|
110
|
+
const p = resolver.classifyPath(file).path;
|
|
111
|
+
if (!p.startsWith('.starciwork/')) return null;
|
|
112
|
+
const admitted = allowsFile(resolver, dir ? `${p}/.probe` : p);
|
|
113
|
+
if (admitted?.slot === 'app.starciwork' && admitted.allowed) return null;
|
|
114
|
+
return { code: 'HFS_STARCIWORK_LITE_FORBIDDEN', level: 'error', path: p, slot: 'app.starciwork', message: `${p}: lite .starciwork holds only workspace.yaml and brand/ (plus .gitignore); remove this path or use the full edition` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Local lite Work leftovers, including metadata ignored by an old full-edition .gitignore. */
|
|
118
|
+
export function liteStarciworkTreeFindings(repoRoot, resolver, tracked) {
|
|
119
|
+
if (resolver.repo?.edition !== 'lite' || !resolver.slot('app.starciwork')) return [];
|
|
120
|
+
let entries = [];
|
|
121
|
+
try { entries = fs.readdirSync(path.join(repoRoot, '.starciwork'), { withFileTypes: true }); } catch { /* Work is optional. */ }
|
|
122
|
+
const trackedSet = new Set(tracked);
|
|
123
|
+
return entries.flatMap(entry => {
|
|
124
|
+
const file = `.starciwork/${entry.name}`;
|
|
125
|
+
if (trackedSet.has(file)) return [];
|
|
126
|
+
const finding = liteStarciworkFinding(file, resolver, { dir: entry.isDirectory() });
|
|
127
|
+
return finding ? [finding] : [];
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
101
131
|
/** The findings of the slot manifest over `files` (repository-relative tracked paths) of a repository of `profile`. */
|
|
102
132
|
export function pathFindings({ files, resolver, profile }) {
|
|
103
133
|
const findings = [];
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// - a tracked `.enc` that is not a sops envelope (an encrypted-looking file that holds plaintext);
|
|
6
6
|
// - a line of any other tracked text file that matches a secret pattern.
|
|
7
7
|
// A finding names the file, the line and the pattern, never the value.
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
8
10
|
import { FORBIDDEN_FILES, secretHits } from '../../lib/secret-patterns.mjs';
|
|
9
11
|
import { isSopsEnvelope } from '../../lib/sops-envelope.mjs';
|
|
10
12
|
import { found, readText } from './read.mjs';
|
|
@@ -16,6 +18,17 @@ const NOT_SCANNED = /(^|\/)package-lock\.json$/;
|
|
|
16
18
|
/** True when the slot `id` of the manifest reports HFS_PLAINTEXT_SECRET for the paths it forbids. */
|
|
17
19
|
export const slotOwnsSecrets = (slot) => slot?.rules?.includes(PLAINTEXT_SECRET) === true;
|
|
18
20
|
|
|
21
|
+
/** A legacy lite root secrets.env is reported even when ignored; its presence is enough, so no value is read. */
|
|
22
|
+
export function untrackedSecretFindings({ repoRoot, files, repo }) {
|
|
23
|
+
if (repo.profile !== 'app' || repo.edition !== 'lite' || files.includes('secrets.env')) return [];
|
|
24
|
+
try {
|
|
25
|
+
if (!fs.statSync(path.join(repoRoot, 'secrets.env')).isFile()) return [];
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return [found(PLAINTEXT_SECRET, 'secrets.env', 'secrets.env is an untracked plaintext secret at the lite app root; seal it with starci app secret set into .starcistacks/<env>/secrets/<slug>.enc and remove the plaintext file; read materialized values by *_FILE', { level: 'warning', pattern: 'untracked-secrets-env' })];
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
/**
|
|
20
33
|
* The findings of R06 for one tracked file that no forbidden slot claims: a secret by being (an env file, a key file), an `.enc` that is
|
|
21
34
|
* no sops envelope, or a line that matches a secret pattern. `text` is the file's text, or null when it cannot be read (binary, too
|
|
@@ -65,8 +65,10 @@ export function importAllowedIn(scope, fromPath, toPath) {
|
|
|
65
65
|
const fromOwner = ownerIn(scope, from.path);
|
|
66
66
|
const toOwner = ownerIn(scope, to.path);
|
|
67
67
|
if (sameOwnerUnit(from, fromOwner) === sameOwnerUnit(to, toOwner)) return { allowed: true, reason: 'sameOwner' };
|
|
68
|
-
const { mayImport } = scope.manifest.tiers[scope.profile][fromTier] ?? {};
|
|
68
|
+
const { mayImport, mayImportKinds } = scope.manifest.tiers[scope.profile][fromTier] ?? {};
|
|
69
69
|
if (!mayImport?.includes(toTier)) return { allowed: false, reason: 'tierDirection', fromTier, toTier, mayImport: mayImport ?? [] };
|
|
70
|
+
const kindRestriction = mayImportKinds?.[toTier];
|
|
71
|
+
if (kindRestriction && (!kindRestriction.from.includes(from.kind) || !kindRestriction.to.includes(to.kind))) return { allowed: false, reason: 'tierDirection', fromTier, toTier, mayImport, kindRestriction, fromKind: from.kind ?? null, toKind: to.kind ?? null };
|
|
70
72
|
const layerProblem = lowerLayerProblem(scope, from, to, fromTier, toTier, fromOwner, toOwner);
|
|
71
73
|
if (layerProblem) return layerProblem;
|
|
72
74
|
// A runtime owner is imported file by file (crossOwner of knowledge/hfs/runtime-slots.yaml): no public entry.
|
|
@@ -5,6 +5,19 @@ import { braceVariants } from '../lib/glob.mjs';
|
|
|
5
5
|
import { classifyIn, slotEnabled } from './slot-classify.mjs';
|
|
6
6
|
import { fillVars, varsOf } from './slot-match.mjs';
|
|
7
7
|
|
|
8
|
+
/** A populated fe.modules instance needs its index.ts only when it contains TypeScript, including nested files. */
|
|
9
|
+
export function requiredFileApplies(slotId, target, files) {
|
|
10
|
+
if (!/^fe\.modules(?:\.|$)/u.test(slotId) || path.posix.basename(target) !== 'index.ts') return true;
|
|
11
|
+
const prefix = `${path.posix.dirname(target)}/`;
|
|
12
|
+
let populated = false;
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
if (!file.startsWith(prefix)) continue;
|
|
15
|
+
populated = true;
|
|
16
|
+
if (/\.tsx?$/u.test(file)) return true;
|
|
17
|
+
}
|
|
18
|
+
return !populated;
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
/** One `requires` entry of a slot placed under `root`: a leading / makes it repository-relative, the variables are filled from `bindings`. */
|
|
9
22
|
function requiredEntry(entry, bindings, root) {
|
|
10
23
|
const rooted = entry.startsWith('/');
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
3
3
|
import { triggerProblems } from './declaration-slots.mjs';
|
|
4
4
|
import { litePresenceOf, slotInEdition } from './edition-slots.mjs';
|
|
5
|
-
import { manifestKind, RUNTIME_KIND, runtimeSemanticProblems } from './manifest-shape.mjs';
|
|
5
|
+
import { manifestKind, RUNTIME_KIND, runtimeSemanticProblems, tierKindProblems } from './manifest-shape.mjs';
|
|
6
6
|
import { APP_SCOPE, PROFILES } from './slot-manifest-shape.mjs';
|
|
7
7
|
|
|
8
8
|
function checkAppRootSlot(slot, bad) {
|
|
@@ -113,6 +113,7 @@ function checkSideReads(manifest, bad, braceVariants) {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
function checkProfileTierTargets(manifest, profile, bad) {
|
|
116
|
+
bad.push(...tierKindProblems(manifest, profile));
|
|
116
117
|
for (const [tier, def] of Object.entries(manifest.tiers[profile])) {
|
|
117
118
|
for (const target of def.mayImport) {
|
|
118
119
|
if (!(target in manifest.tiers[profile])) bad.push(`tiers.${profile}.${tier}.mayImport names ${target}, which is not a ${profile} tier`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** The expression under the wrappers selected by an AST check's contract. */
|
|
2
|
+
export function unwrapExpression(ts, expression, { awaitExpressions = false, typeAssertions = true, satisfiesExpressions = true } = {}) {
|
|
3
|
+
while (expression && (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression)
|
|
4
|
+
|| (typeAssertions && ts.isTypeAssertionExpression(expression)) || ts.isNonNullExpression(expression)
|
|
5
|
+
|| (satisfiesExpressions && (ts.isSatisfiesExpression?.(expression) ?? false))
|
|
6
|
+
|| (awaitExpressions && ts.isAwaitExpression(expression)))) expression = expression.expression;
|
|
7
|
+
return expression;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Local name -> imported name and module; callers select their existing module key and literal policy. */
|
|
11
|
+
export function importsOf(ts, sourceFile, { moduleKey = 'module', stringLiteralsOnly = false } = {}) {
|
|
12
|
+
const imports = new Map();
|
|
13
|
+
for (const statement of sourceFile.statements) {
|
|
14
|
+
if (!ts.isImportDeclaration(statement) || (stringLiteralsOnly && !ts.isStringLiteralLike(statement.moduleSpecifier))) continue;
|
|
15
|
+
const bindings = statement.importClause?.namedBindings;
|
|
16
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
17
|
+
for (const binding of bindings.elements) imports.set(binding.name.text, { name: (binding.propertyName ?? binding.name).text, [moduleKey]: statement.moduleSpecifier.text });
|
|
18
|
+
}
|
|
19
|
+
return imports;
|
|
20
|
+
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { functionOf, isFn } from "./lib/bindings.mjs"
|
|
22
22
|
import { hfsOf } from "./lib/hfs.mjs"
|
|
23
23
|
import { isPromiseValued } from "./lib/types.mjs"
|
|
24
|
-
import { fileOf, roleOfFile, slotOfFile } from "./lib/scope.mjs"
|
|
24
|
+
import { baseName, fileOf, roleOfFile, slotOfFile } from "./lib/scope.mjs"
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* The budget the slot of the linted file states (`budget` in knowledge/hfs/slots.yaml), or an empty one when no slot owns the file
|
|
@@ -34,13 +34,17 @@ const budgetOf = (context) => {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* The line limit of the linted file: the budget keyed by the file name its role has in the slot (`component.tsx` for the drawing,
|
|
37
|
-
* `index.tsx` for the entry), else the slot's `file` budget (hooks 200, modules 400), else null.
|
|
37
|
+
* `index.tsx` for the entry), else the slot's `file` budget (hooks 200, modules 400), else null. A pure re-export `index.ts`
|
|
38
|
+
* uses the slot's separate `barrel` budget when stated: every statement must be an `export ... from` declaration.
|
|
38
39
|
*/
|
|
39
|
-
const lineBudgetOf = (context) => {
|
|
40
|
+
const lineBudgetOf = (context, node) => {
|
|
40
41
|
const role = roleOfFile(context)
|
|
41
42
|
const id = slotOfFile(context)
|
|
42
43
|
const fileName = role === null || id === null ? undefined : hfsOf(context).slot(id)?.roles?.[role]
|
|
43
44
|
const budget = budgetOf(context)
|
|
45
|
+
const pureBarrel = baseName(fileOf(context)) === "index.ts" && node.body.length > 0 && node.body.every((statement) =>
|
|
46
|
+
["ExportNamedDeclaration", "ExportAllDeclaration"].includes(statement.type) && Boolean(statement.source))
|
|
47
|
+
if (pureBarrel && typeof budget.barrel === "number") return budget.barrel
|
|
44
48
|
const limit = fileName !== undefined && typeof budget[fileName] === "number" ? budget[fileName] : budget.file
|
|
45
49
|
return typeof limit === "number" ? limit : null
|
|
46
50
|
}
|
|
@@ -73,11 +77,11 @@ const isUnitName = (name) => Boolean(name) && (/^[A-Z]/.test(name) || /^use[A-Z0
|
|
|
73
77
|
|
|
74
78
|
// -- BUDGET-1 --------------------------------------------------------------------------------------
|
|
75
79
|
|
|
76
|
-
/** A file is within the line budget its slot states for its role (`
|
|
80
|
+
/** A file is within the line budget its slot states for its role, pure re-export barrel (`barrel`) or every file (`file`). */
|
|
77
81
|
export const componentLineBudget = {
|
|
78
82
|
meta: {
|
|
79
83
|
type: "suggestion",
|
|
80
|
-
docs: { description: "A file stays within the line budget its slot states (`component.tsx`, `index.tsx`, `file`)." },
|
|
84
|
+
docs: { description: "A file stays within the line budget its slot states (`component.tsx`, `index.tsx`, `barrel`, `file`)." },
|
|
81
85
|
schema: [],
|
|
82
86
|
messages: {
|
|
83
87
|
lines:
|
|
@@ -85,12 +89,11 @@ export const componentLineBudget = {
|
|
|
85
89
|
},
|
|
86
90
|
},
|
|
87
91
|
create(context) {
|
|
88
|
-
// A spec is not held to a budget; a file has one when its role or its slot states it.
|
|
89
|
-
const max = lineBudgetOf(context)
|
|
90
|
-
if (max === null) return {}
|
|
91
92
|
const source = context.sourceCode || context.getSourceCode()
|
|
92
93
|
return {
|
|
93
94
|
Program(node) {
|
|
95
|
+
const max = lineBudgetOf(context, node)
|
|
96
|
+
if (max === null) return
|
|
94
97
|
// A trailing newline is not a line of code.
|
|
95
98
|
const count = source.lines.length - (source.lines[source.lines.length - 1] === "" ? 1 : 0)
|
|
96
99
|
if (count > max) context.report({ node, messageId: "lines", data: { count, max } })
|