@starci/eslint-canon-fe 9.8.0 → 9.8.2

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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 9.8.2 - 2026-11-01
4
+
5
+ - Changed: the bundled canon pins name @starci/grammar 0.19.1, eslint-grammar-core and eslint-grammar-offset-pop 0.19.1, eslint-canon-be 3.11.3, eslint-canon-fe 9.8.2, stylelint-canon 2.4.0 and hfs 4.16.2 (RELEASE_VERSION_NOT_BUMPED: the previous versions are on npm).
6
+ - Changed: the bundled hfs `database-migrations` follows hfs 4.16.2 (an alter of a column the migration added is not destructive; an entity names its table by its decorator).
7
+
8
+ ## 9.8.1 - 2026-10-31
9
+
10
+ - Changed: the bundled canon pins name @starci/grammar, eslint-grammar-core and eslint-grammar-offset-pop 0.19.0, eslint-canon-be 3.11.2, eslint-canon-fe 9.8.1, stylelint-canon 2.4.0 and hfs 4.16.1 (RELEASE_VERSION_NOT_BUMPED: 9.8.0 is on npm).
11
+
3
12
  ## 9.8.0 - 2026-10-31
4
13
 
5
14
  - Added: `no-spacing-utility` (R350, `FE_SPACING_UTILITY`): no padding, margin, gap or space utility (variants and arbitrary values included) in an app class string; the message names the role prop to use. It ships in the closed counting cohort (`COUNTING` of `src/lib/config.ts`) at warn for this release.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "9.8.0",
3
+ "version": "9.8.2",
4
4
  "description": "Universal front-end lint laws for TypeScript and Next.js, independent of the selected Grammar family.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,35 +31,35 @@ pins:
31
31
  source: packages/event-stream/package.json
32
32
  why: 'the one server-sent-events client of every front end (fe-stack event-stream, owner:event-stream-package): the transport client layer (modules/api/sse/clients) creates one instance for the app with the bearer token source and the stream operations follow on it; it carries Last-Event-ID resume, the capped doubling backoff, the heartbeat timeout, the single 401 renewal and the useSWRSubscription adapter, so no app keeps a hand-written reader.'
33
33
  '@starci/grammar':
34
- version: 0.18.0
34
+ version: 0.19.1
35
35
  group: starci
36
36
  install: registry
37
37
  side: fe
38
38
  source: packages/grammar/package.json
39
39
  why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
40
40
  '@starci/eslint-canon-be':
41
- version: 3.11.1
41
+ version: 3.11.3
42
42
  group: starci
43
43
  install: registry
44
44
  side: be
45
45
  source: packages/eslint-canon-be/package.json
46
46
  why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
47
47
  '@starci/eslint-canon-fe':
48
- version: 9.8.0
48
+ version: 9.8.2
49
49
  group: starci
50
50
  install: registry
51
51
  side: fe
52
52
  source: packages/eslint-canon-fe/package.json
53
53
  why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
54
54
  '@starci/eslint-grammar-core':
55
- version: 0.18.0
55
+ version: 0.19.1
56
56
  group: starci
57
57
  install: registry
58
58
  side: fe
59
59
  source: packages/eslint-grammar-core/package.json
60
60
  why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
61
61
  '@starci/eslint-grammar-offset-pop':
62
- version: 0.18.0
62
+ version: 0.19.1
63
63
  group: starci
64
64
  install: registry
65
65
  side: fe
@@ -106,7 +106,7 @@ pins:
106
106
  source: packages/ai-router/package.json
107
107
  why: 'the AI pool router of a back end: a NestJS module that routes OpenAI-compatible chat, stream and embeddings calls across keys, models and providers, with @starci/ai-router/testing for its specs; @starci/jest-preset compiles it as an ESM-only dependency.'
108
108
  '@starci/hfs':
109
- version: 4.16.0
109
+ version: 4.16.2
110
110
  group: starci
111
111
  install: registry
112
112
  side: both
@@ -369,7 +369,7 @@ function columnAlterObject(tokens, index, table, line) {
369
369
  && sqlWord(tokens[at + 2 + Number(sqlWord(tokens[at + 1], 'column'))], 'set') && sqlWord(tokens[at + 3 + Number(sqlWord(tokens[at + 1], 'column'))], 'default'));
370
370
  if (!type && !(notNull && !defaultSet))
371
371
  return null;
372
- return { name: `${table.name}.${column.name}`, leaf: column.leaf, table: table.leaf, kind: 'column', line };
372
+ return { name: `${table.name}.${column.name}`, leaf: column.leaf, table: table.leaf, kind: 'column', alters: true, line };
373
373
  }
374
374
  function alteredColumn(table, column, line) {
375
375
  return { name: `${table.name}.${column.name}`, leaf: column.leaf, table: table.leaf, kind: 'column', line };
@@ -467,11 +467,32 @@ function globalConstraintNames(statement) {
467
467
  }
468
468
  return names;
469
469
  }
470
+ const ENTITY_DECORATORS = new Set(['Entity', 'Table', 'ViewEntity']);
471
+ /** Adds the table names a class decorator `@Entity(...)`, `@Table(...)` or `@ViewEntity(...)` spells to `tables`; true when such a decorator spells none. */
472
+ function noteEntityTables(ts, decorator, tables) {
473
+ const call = decorator.expression;
474
+ const callee = ts.isCallExpression(call) ? call.expression : call;
475
+ if (!ts.isIdentifier(callee) || !ENTITY_DECORATORS.has(callee.text))
476
+ return false;
477
+ const before = tables.size;
478
+ const collect = (node) => {
479
+ if (ts.isStringLiteralLike(node))
480
+ for (const part of [node.text, node.text.split('.').at(-1)])
481
+ tables.add(part);
482
+ ts.forEachChild(node, collect);
483
+ };
484
+ if (ts.isCallExpression(call))
485
+ for (const argument of call.arguments)
486
+ collect(argument);
487
+ return tables.size === before;
488
+ }
470
489
  function sourceNameScopes(ts, text, file) {
471
490
  const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
472
491
  const checker = lexicalChecker(ts, source);
473
492
  const scopes = [];
474
493
  const entityNames = /\.entity\.[cm]?tsx?$/.test(file) ? new Set() : null;
494
+ const entityTables = new Set();
495
+ let implicitTable = false;
475
496
  const addSql = (sql) => {
476
497
  for (const statement of databaseSqlStatements(databaseSqlTokens(sql))) {
477
498
  const names = new Set(statement.filter((token) => sqlName(token) !== null).map((token) => token.v));
@@ -490,10 +511,15 @@ function sourceNameScopes(ts, text, file) {
490
511
  addSql(sql);
491
512
  if (entityNames && ts.isIdentifier(node))
492
513
  entityNames.add(node.text);
514
+ if (entityNames && ts.isDecorator(node))
515
+ implicitTable = noteEntityTables(ts, node, entityTables) || implicitTable;
493
516
  ts.forEachChild(node, visit);
494
517
  };
495
518
  visit(source);
496
- return entityNames ? [{ names: entityNames, constraints: entityNames, globalConstraints: new Set() }] : scopes;
519
+ // An entity names its table by the string of its @Entity/@Table decorator; a column property of the same word (`queue`) is no table reference.
520
+ // An entity that names none (or has no such decorator) keeps the conservative identifier match.
521
+ const tables = entityTables.size > 0 && !implicitTable ? entityTables : undefined;
522
+ return entityNames ? [{ names: entityNames, tables, constraints: entityNames, globalConstraints: new Set() }] : scopes;
497
523
  }
498
524
  /**
499
525
  * Whether a be/src source references a destroyed object. A column (`<schema>.<table>.<column>`) is referenced only by a source that
@@ -501,9 +527,10 @@ function sourceNameScopes(ts, text, file) {
501
527
  * SQL constraints require explicit constraint names, while entities retain their table/decorator association.
502
528
  * Other object kinds match their leaf name conservatively.
503
529
  */
504
- const referencesObject = (scopes, object) => scopes.some(({ names, constraints, globalConstraints }) => (object.kind === 'constraint' && globalConstraints.has(object.leaf))
505
- || ((object.kind === 'constraint' ? constraints : names).has(object.leaf)
506
- && (!['column', 'constraint'].includes(object.kind) || !object.table || names.has(object.table))));
530
+ const referencesObject = (scopes, object) => scopes.some(({ names, tables, constraints, globalConstraints }) => (object.kind === 'constraint' && globalConstraints.has(object.leaf))
531
+ || (object.kind === 'table' && tables !== undefined ? tables.has(object.leaf)
532
+ : (object.kind === 'constraint' ? constraints : names).has(object.leaf)
533
+ && (!['column', 'constraint'].includes(object.kind) || !object.table || (tables ?? names).has(object.table))));
507
534
  function destructiveObjectFinding(file, object, contract, references) {
508
535
  const reference = references.find(({ scopes }) => referencesObject(scopes, object));
509
536
  if (contract && !reference)
@@ -512,10 +539,50 @@ function destructiveObjectFinding(file, object, contract, references) {
512
539
  : 'use a *_contract_* migration after the application stops using this object';
513
540
  return found(DB_MIGRATION_DESTRUCTIVE, file, `${file}:${object.line} destructively changes ${object.name}; ${reason}`, { line: object.line, object: object.name, ...(reference ? { reference: reference.file } : {}) });
514
541
  }
542
+ /** The `<table leaf>.<column leaf>` keys an ALTER TABLE ... ADD COLUMN statement adds, and `<table leaf>.*` for a CREATE TABLE. */
543
+ function addedColumns(statement) {
544
+ const added = [];
545
+ for (let index = 0; index < statement.length - 1; index += 1) {
546
+ if (sqlWord(statement[index], 'create')) {
547
+ const at = skipWords(statement, index + 1, ['temp', 'temporary', 'unlogged']);
548
+ const created = sqlWord(statement[at], 'table') ? nameAt(statement, skipWords(statement, at + 1, ['if', 'not', 'exists'])) : null;
549
+ if (created)
550
+ added.push(`${created.leaf}.*`);
551
+ }
552
+ if (!(sqlWord(statement[index], 'alter') && sqlWord(statement[index + 1], 'table')))
553
+ continue;
554
+ const table = nameAt(statement, skipWords(statement, index + 2, ['if', 'exists', 'only']));
555
+ if (!table)
556
+ continue;
557
+ const body = statement.slice(table.next);
558
+ body.forEach((token, at) => {
559
+ if (!sqlWord(token, 'add'))
560
+ return;
561
+ const start = skipWords(body, at + 1, ['column', 'if', 'not', 'exists']);
562
+ if (['constraint', 'primary', 'foreign', 'unique', 'check', 'exclude'].some((word) => sqlWord(body[start], word)))
563
+ return;
564
+ const column = nameAt(body, start);
565
+ if (column)
566
+ added.push(`${table.leaf}.${column.leaf}`);
567
+ });
568
+ }
569
+ return added;
570
+ }
571
+ /** An ALTER of a column (SET NOT NULL, TYPE) the same migration added earlier changes no existing data: it is not destructive. */
572
+ const alterOfNewColumn = (object, added) => object.alters === true && (added.has(`${object.table}.${object.leaf}`) || added.has(`${object.table}.*`));
573
+ /** The destructive objects of one migration's SQL, in order, leaving out the alters of columns and tables the migration created itself. */
574
+ function migrationDestructiveObjects(text) {
575
+ const added = new Set();
576
+ return databaseSqlStatements(databaseExecutableTokens(text)).flatMap((statement) => {
577
+ for (const key of addedColumns(statement))
578
+ added.add(key);
579
+ return destructiveObjects(statement).filter((object) => !alterOfNewColumn(object, added));
580
+ });
581
+ }
515
582
  function destructiveFileFindings(file, text, references) {
516
583
  const contract = /_contract_[a-z0-9]+(?:[-_][a-z0-9]+)*\.sql$/.test(file);
517
584
  const seen = new Set();
518
- const objects = databaseSqlStatements(databaseExecutableTokens(text)).flatMap(destructiveObjects);
585
+ const objects = migrationDestructiveObjects(text);
519
586
  return objects.flatMap((object) => {
520
587
  const key = `${object.line}:${object.kind}:${object.name}`;
521
588
  if (seen.has(key))
@@ -564,7 +631,7 @@ export async function contractReleaseFindings({ repoRoot, migrations, git }) {
564
631
  const text = readText(repoRoot, file);
565
632
  if (text === null)
566
633
  return [];
567
- return databaseSqlStatements(databaseExecutableTokens(text)).flatMap(destructiveObjects).flatMap((object) => {
634
+ return migrationDestructiveObjects(text).flatMap((object) => {
568
635
  const reference = references.find(({ scopes }) => referencesObject(scopes, object));
569
636
  return reference ? [found(DB_MIGRATION_DESTRUCTIVE, file, `${file}:${object.line} destroys ${object.name}, which the previous release ${release} still references in ${reference.file}; the contract migration runs right after the accept while that release is the rollback target: ship the code change in one release and this contract migration in the next`, { line: object.line, object: object.name, release, reference: reference.file })] : [];
570
637
  });