@starci/eslint-canon-fe 9.8.1 → 9.8.3
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.3 - 2026-11-01
|
|
4
|
+
|
|
5
|
+
- Changed: the bundled canon pins name @starci/grammar 0.20.0, eslint-grammar-core and eslint-grammar-offset-pop 0.20.0, eslint-canon-be 3.11.4, eslint-canon-fe 9.8.3, stylelint-canon 2.4.0 and hfs 4.16.3 (RELEASE_VERSION_NOT_BUMPED: 9.8.2 is on npm).
|
|
6
|
+
|
|
7
|
+
## 9.8.2 - 2026-11-01
|
|
8
|
+
|
|
9
|
+
- 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).
|
|
10
|
+
- 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).
|
|
11
|
+
|
|
3
12
|
## 9.8.1 - 2026-10-31
|
|
4
13
|
|
|
5
14
|
- 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).
|
package/package.json
CHANGED
|
@@ -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.
|
|
34
|
+
version: 0.20.0
|
|
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.
|
|
41
|
+
version: 3.11.4
|
|
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.
|
|
48
|
+
version: 9.8.3
|
|
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.
|
|
55
|
+
version: 0.20.0
|
|
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.
|
|
62
|
+
version: 0.20.0
|
|
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.
|
|
109
|
+
version: 4.16.3
|
|
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
|
-
|
|
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
|
-
|| (
|
|
506
|
-
|
|
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 =
|
|
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
|
|
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
|
});
|