@starci/eslint-canon-fe 9.12.0 → 9.13.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 +10 -0
- package/dist/lib/hfs-view.d.ts +12 -0
- package/dist/lib/scope.d.ts +5 -0
- package/dist/lib/scope.js +9 -0
- package/dist/next-conventions.d.ts +1 -1
- package/dist/transport.d.ts +3 -1
- package/dist/transport.js +6 -3
- package/docs/transport.md +1 -1
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +12 -12
- package/runtime/knowledge/hfs/slots.yaml +3 -3
- package/runtime/scripts/hfs/architecture/root-packages.mjs +1 -1
- package/runtime/scripts/hfs/declaration-shape.mjs +3 -1
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/hfs/view.mjs +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 9.13.0 - 2026-11-03
|
|
4
|
+
|
|
5
|
+
- Changed: `one-outcome-union` accepts in a root workspace package only the result union its `package.json` names (`starci.outcome: { name, file }`); every other result union stays refused. Specs: `src/packages-config.spec.ts`.
|
|
6
|
+
- Changed: the runtime copies carry the root-packages view, the `modules` declaration and the canon pins of this release (no release-age wording).
|
|
7
|
+
- Changed: the package's slot manifest places any root workspace package `packages/<name>/` in `app.packages` (a block library such as `packages/blocks-core` beside the agent SDK and the module packages), so the root packages config lints it.
|
|
8
|
+
|
|
9
|
+
## 9.12.1 - 2026-11-03
|
|
10
|
+
|
|
11
|
+
- Changed: the runtime copies carry the canon pins of `@starci/auth` 0.2.1, `@starci/auth-oidc` 0.1.1, the Grammar family 0.21.16 and `@starci/hfs` 5.4.0.
|
|
12
|
+
|
|
3
13
|
## 9.12.0 - 2026-11-03
|
|
4
14
|
|
|
5
15
|
- Changed: the runtime copies carry R368's acceptance of the module-definition helpers, the module-per-transport confinement of the helpers and the `@starci/nest-kit` and `@starci/next-i18n` pins; `i18n-stack-in-one-module` refuses every product-source call of a next-intl layer builder (the stack is `@starci/next-i18n`'s).
|
package/dist/lib/hfs-view.d.ts
CHANGED
|
@@ -71,4 +71,16 @@ export interface HfsView {
|
|
|
71
71
|
ownerOf(file: string): string | null;
|
|
72
72
|
allows(file: string): HfsAllowance | null;
|
|
73
73
|
slot(id: string): HfsSlot | null;
|
|
74
|
+
/** The vocabulary the root package of a file declares in the `starci` key of its manifest (the view of the root packages only), or null outside a package. */
|
|
75
|
+
readonly packageContract?: (file: string) => HfsPackageContract | null;
|
|
76
|
+
}
|
|
77
|
+
/** A type a root package declares: its name and the app-root relative file that declares it. */
|
|
78
|
+
export interface HfsPackageDeclaredType {
|
|
79
|
+
readonly name: string;
|
|
80
|
+
readonly file: string;
|
|
81
|
+
}
|
|
82
|
+
/** The error base and the outcome union a root package declares for itself, since it cannot import the app's `platform/errors` or `GraphQLResponse<T>`. */
|
|
83
|
+
export interface HfsPackageContract {
|
|
84
|
+
readonly errorBase?: HfsPackageDeclaredType;
|
|
85
|
+
readonly outcome?: HfsPackageDeclaredType;
|
|
74
86
|
}
|
package/dist/lib/scope.d.ts
CHANGED
|
@@ -55,6 +55,11 @@ export declare const isApiLayer: (context: AnyContext) => boolean;
|
|
|
55
55
|
export declare const isApiRest: (context: AnyContext) => boolean;
|
|
56
56
|
/** The file that may declare the one result vocabulary (`GraphQLResponse`): the file of the slot the manifest marks `outcomeHome`. */
|
|
57
57
|
export declare const isOutcomeModule: (context: AnyContext) => boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The one result union a root workspace package declares for itself (`starci.outcome` of its manifest: a package cannot import the app's
|
|
60
|
+
* `GraphQLResponse<T>`) when the linted file is its declared file, or null: only the alias of that name in that file is its outcome home.
|
|
61
|
+
*/
|
|
62
|
+
export declare const packageOutcomeOf: (context: AnyContext) => string | null;
|
|
58
63
|
/** The basename of a path without directories. */
|
|
59
64
|
export declare const baseName: (filename: string) => string;
|
|
60
65
|
/** The file name with its extension removed. */
|
package/dist/lib/scope.js
CHANGED
|
@@ -77,6 +77,15 @@ export const isOutcomeModule = (context) => {
|
|
|
77
77
|
const slot = slotOfFile(context);
|
|
78
78
|
return slot !== null && hfs.slot(slot)?.outcomeHome === true;
|
|
79
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* The one result union a root workspace package declares for itself (`starci.outcome` of its manifest: a package cannot import the app's
|
|
82
|
+
* `GraphQLResponse<T>`) when the linted file is its declared file, or null: only the alias of that name in that file is its outcome home.
|
|
83
|
+
*/
|
|
84
|
+
export const packageOutcomeOf = (context) => {
|
|
85
|
+
const hfs = hfsOf(context);
|
|
86
|
+
const outcome = hfs.packageContract?.(context.filename)?.outcome;
|
|
87
|
+
return outcome !== undefined && hfs.relative(context.filename) === outcome.file ? outcome.name : null;
|
|
88
|
+
};
|
|
80
89
|
/** The basename of a path without directories. */
|
|
81
90
|
export const baseName = (filename) => normalizePath(filename).split("/").pop() ?? "";
|
|
82
91
|
/** The file name with its extension removed. */
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { type Recommended, type Rules } from "./lib/rule.js";
|
|
14
14
|
/** Next 16 names the request interceptor `proxy.ts`; `middleware.ts` is forbidden. */
|
|
15
|
-
export declare const noMiddlewareFile: import("./lib/rule.js").Rule<"
|
|
15
|
+
export declare const noMiddlewareFile: import("./lib/rule.js").Rule<"file" | "export", []>;
|
|
16
16
|
/** The locale segment is `[locale]`. */
|
|
17
17
|
export declare const localeSegmentIsLocale: import("./lib/rule.js").Rule<"segment", []>;
|
|
18
18
|
/** Exactly one i18n stack: next-intl. */
|
package/dist/transport.d.ts
CHANGED
|
@@ -56,7 +56,9 @@ export declare const noHandTypedWire: import("./lib/rule.js").Rule<"document" |
|
|
|
56
56
|
export declare const outcomeKindsExhaustive: import("./lib/rule.js").Rule<"missing", []>;
|
|
57
57
|
/**
|
|
58
58
|
* A result vocabulary is declared once per repository: `GraphQLResponse` in the file of the slot the manifest marks `outcomeHome`
|
|
59
|
-
* (`fe.transport.outcome`: `modules/api/graphql/types.ts`, or `fe.package.api.outcome`: the api package's `src/graphql/types.ts`).
|
|
59
|
+
* (`fe.transport.outcome`: `modules/api/graphql/types.ts`, or `fe.package.api.outcome`: the api package's `src/graphql/types.ts`). A root
|
|
60
|
+
* workspace package, which cannot import it, declares its one union in its manifest (`starci.outcome`: `{ name, file }`): that alias, in
|
|
61
|
+
* that file, is its outcome home and every other result union of the package is refused.
|
|
60
62
|
*
|
|
61
63
|
* A union type alias is a result union when its members are object types that ALL carry the same literal-typed discriminant, in the
|
|
62
64
|
* result vocabulary: `success` or `ok` (both `true` and `false` among the members) or `kind` (a member of kind `"ok"` beside at least one failure
|
package/dist/transport.js
CHANGED
|
@@ -23,7 +23,7 @@ import { isNode } from "./lib/ast.js";
|
|
|
23
23
|
import { hfsOf } from "./lib/hfs.js";
|
|
24
24
|
import { forbiddingConcern, packageNameOf } from "./lib/fe-stack.js";
|
|
25
25
|
import { defineRule } from "./lib/rule.js";
|
|
26
|
-
import { globalReferences, inSlot, isApiClient, isApiLayer, isApiRest, isOutcomeModule } from "./lib/scope.js";
|
|
26
|
+
import { globalReferences, inSlot, isApiClient, isApiLayer, isApiRest, isOutcomeModule, packageOutcomeOf } from "./lib/scope.js";
|
|
27
27
|
import { typed } from "./lib/types.js";
|
|
28
28
|
/**
|
|
29
29
|
* The canonical transport libraries and the one place each may be imported (fe-stack graphql, rest and realtime): Apollo in
|
|
@@ -811,7 +811,9 @@ const aliasDeclaration = (tsNode) => {
|
|
|
811
811
|
};
|
|
812
812
|
/**
|
|
813
813
|
* A result vocabulary is declared once per repository: `GraphQLResponse` in the file of the slot the manifest marks `outcomeHome`
|
|
814
|
-
* (`fe.transport.outcome`: `modules/api/graphql/types.ts`, or `fe.package.api.outcome`: the api package's `src/graphql/types.ts`).
|
|
814
|
+
* (`fe.transport.outcome`: `modules/api/graphql/types.ts`, or `fe.package.api.outcome`: the api package's `src/graphql/types.ts`). A root
|
|
815
|
+
* workspace package, which cannot import it, declares its one union in its manifest (`starci.outcome`: `{ name, file }`): that alias, in
|
|
816
|
+
* that file, is its outcome home and every other result union of the package is refused.
|
|
815
817
|
*
|
|
816
818
|
* A union type alias is a result union when its members are object types that ALL carry the same literal-typed discriminant, in the
|
|
817
819
|
* result vocabulary: `success` or `ok` (both `true` and `false` among the members) or `kind` (a member of kind `"ok"` beside at least one failure
|
|
@@ -834,11 +836,12 @@ export const oneOutcomeUnion = defineRule({
|
|
|
834
836
|
create(context) {
|
|
835
837
|
if (isOutcomeModule(context))
|
|
836
838
|
return {};
|
|
839
|
+
const declaredOutcome = packageOutcomeOf(context);
|
|
837
840
|
const { checker, toTs } = typed(context);
|
|
838
841
|
return {
|
|
839
842
|
TSTypeAliasDeclaration(node) {
|
|
840
843
|
// typescript-eslint 6 and later produce no TSParenthesizedType node: a parenthesised type is its inner node.
|
|
841
|
-
if (node.typeAnnotation.type !== "TSUnionType")
|
|
844
|
+
if (node.typeAnnotation.type !== "TSUnionType" || node.id.name === declaredOutcome)
|
|
842
845
|
return;
|
|
843
846
|
const mapped = toTs(node);
|
|
844
847
|
if (!mapped)
|
package/docs/transport.md
CHANGED
|
@@ -171,7 +171,7 @@ switch (outcome.kind) {
|
|
|
171
171
|
|
|
172
172
|
## `starci-fe/one-outcome-union`
|
|
173
173
|
|
|
174
|
-
A result union (`success` / `ok` / `kind` discriminant) is declared only in the outcome-home slot (`outcomeHome: true` in the slot manifest: `modules/api/graphql/types.ts`, or the api package's `src/graphql/types.ts`); elsewhere the code composes `GraphQLResponse<T>`.
|
|
174
|
+
A result union (`success` / `ok` / `kind` discriminant) is declared only in the outcome-home slot (`outcomeHome: true` in the slot manifest: `modules/api/graphql/types.ts`, or the api package's `src/graphql/types.ts`); elsewhere the code composes `GraphQLResponse<T>`. A root workspace package, which cannot import it, names its one union in its `package.json` (`"starci": { "outcome": { "name": "AgentResult", "file": "src/workbench/outcome.ts" } }`): that alias, in that file, is its outcome home, and every other result union of the package is refused.
|
|
175
175
|
|
|
176
176
|
A union type alias is a result union when its resolved members are all object types that carry the same literal-typed discriminant in the result vocabulary: `success` or `ok` (each member pins it to one value, `true` and `false` both present) or `kind` (at least one literal of the Outcome vocabulary: `ok`, `refused`, `forbidden`, `invalid`, `not-found`, `unavailable`). The members are read through the type checker, so `Ok<T> | Failure` and intersections are seen as what they are; no name (`*Outcome`, `*Result`) decides anything. UI state unions (`status`, `state`, `type`, a `kind` of tree nodes or menu items) are not result vocabulary and pass; so does an alias that only composes the one union (`type Read = Outcome<Course>`, `Exclude<Outcome<T>, ...>`). `Outcome<T> | { ok: false; kind: "conflict" }` adds an arm to the one union and is refused.
|
|
177
177
|
|
package/package.json
CHANGED
|
@@ -38,7 +38,7 @@ pins:
|
|
|
38
38
|
source: packages/next-data/package.json
|
|
39
39
|
why: 'the SWR data helpers of every front end (fe-stack data, owner:next-data-package): pollingRestart, the viewer and tenant key helpers, viewerSwrConfig and the viewer cache; each operation hook stays in the app and the package holds no component.'
|
|
40
40
|
'@starci/grammar':
|
|
41
|
-
version: 0.21.
|
|
41
|
+
version: 0.21.17
|
|
42
42
|
group: starci
|
|
43
43
|
install: registry
|
|
44
44
|
side: fe
|
|
@@ -52,28 +52,28 @@ pins:
|
|
|
52
52
|
source: packages/next-i18n/package.json
|
|
53
53
|
why: 'the next-intl stack of every front end (fe-stack i18n, owner:next-i18n-package): defineI18n over the locales of the app, the locale-negotiating proxy, the locale-aware navigation, the request config that loads the catalogs of the app and the locale read of the [locale] layout; the app keeps its message catalogs and writes none of the stack.'
|
|
54
54
|
'@starci/eslint-canon-be':
|
|
55
|
-
version: 3.
|
|
55
|
+
version: 3.15.0
|
|
56
56
|
group: starci
|
|
57
57
|
install: registry
|
|
58
58
|
side: be
|
|
59
59
|
source: packages/eslint-canon-be/package.json
|
|
60
60
|
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.'
|
|
61
61
|
'@starci/eslint-canon-fe':
|
|
62
|
-
version: 9.
|
|
62
|
+
version: 9.13.0
|
|
63
63
|
group: starci
|
|
64
64
|
install: registry
|
|
65
65
|
side: fe
|
|
66
66
|
source: packages/eslint-canon-fe/package.json
|
|
67
67
|
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.'
|
|
68
68
|
'@starci/eslint-grammar-core':
|
|
69
|
-
version: 0.21.
|
|
69
|
+
version: 0.21.17
|
|
70
70
|
group: starci
|
|
71
71
|
install: registry
|
|
72
72
|
side: fe
|
|
73
73
|
source: packages/eslint-grammar-core/package.json
|
|
74
74
|
why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
|
|
75
75
|
'@starci/eslint-grammar-offset-pop':
|
|
76
|
-
version: 0.21.
|
|
76
|
+
version: 0.21.17
|
|
77
77
|
group: starci
|
|
78
78
|
install: registry
|
|
79
79
|
side: fe
|
|
@@ -99,7 +99,7 @@ pins:
|
|
|
99
99
|
side: both
|
|
100
100
|
source: packages/prettier-config/package.json
|
|
101
101
|
'@starci/jest-preset':
|
|
102
|
-
version: 2.
|
|
102
|
+
version: 2.7.0
|
|
103
103
|
group: starci
|
|
104
104
|
install: registry
|
|
105
105
|
side: be
|
|
@@ -127,14 +127,14 @@ pins:
|
|
|
127
127
|
source: packages/nest-kit/package.json
|
|
128
128
|
why: 'the module definition of every back-end module: defineConfigurableModule and defineCapabilityModule return the module base class, the options token and the options types with their types stated, so a <x>.module-definition.ts is one call that the lint on stated export types (R368) and the duplicate-code check (R21) both accept; @starci/jest-preset compiles it as an ESM-only dependency.'
|
|
129
129
|
'@starci/auth':
|
|
130
|
-
version: 0.
|
|
130
|
+
version: 0.3.0
|
|
131
131
|
group: starci
|
|
132
132
|
install: registry
|
|
133
133
|
side: both
|
|
134
134
|
source: packages/auth/package.json
|
|
135
135
|
why: 'the lite sign-in: the NestJS module (EdDSA access tokens with a multi-key JWKS, passwords, Google OAuth with state and PKCE, rotating refresh sessions, the sign-in budget, the refresh cookie), the front-end client and its React bindings, the door contracts and test doubles; the one importer of the identity libraries, reached by a back end only through integrations/passport; @starci/jest-preset compiles it as an ESM-only dependency.'
|
|
136
136
|
'@starci/auth-oidc':
|
|
137
|
-
version: 0.1.
|
|
137
|
+
version: 0.1.2
|
|
138
138
|
group: starci
|
|
139
139
|
install: registry
|
|
140
140
|
side: both
|
|
@@ -155,7 +155,7 @@ pins:
|
|
|
155
155
|
source: packages/mail/package.json
|
|
156
156
|
why: 'outgoing mail of a back end: a NestJS module and the Mailer port over SMTP (connect, greeting, socket and total timeouts, STARTTLS required by default, recipient, message and attachment checks, a shared SMTP target policy, coded errors, typed escaping render helpers returning { subject, text, html }), with @starci/mail/testing for its specs; the only importer of nodemailer (R90).'
|
|
157
157
|
'@starci/hfs':
|
|
158
|
-
version: 5.
|
|
158
|
+
version: 5.5.0
|
|
159
159
|
group: starci
|
|
160
160
|
install: registry
|
|
161
161
|
side: both
|
|
@@ -592,7 +592,7 @@ pins:
|
|
|
592
592
|
version: 3.1150.0
|
|
593
593
|
group: framework
|
|
594
594
|
side: be
|
|
595
|
-
why: 'the S3 transport inside @starci/object-storage, the one ObjectStorage adapter of both editions (MinIO, S3, Spaces), which depends on it at exactly this version and which no app file imports; 3.1150.0 is the release of 2026-10-12
|
|
595
|
+
why: 'the S3 transport inside @starci/object-storage, the one ObjectStorage adapter of both editions (MinIO, S3, Spaces), which depends on it at exactly this version and which no app file imports; 3.1150.0 is the release of 2026-10-12.'
|
|
596
596
|
'@aws-sdk/s3-request-presigner':
|
|
597
597
|
version: 3.1150.0
|
|
598
598
|
group: framework
|
|
@@ -602,7 +602,7 @@ pins:
|
|
|
602
602
|
version: 10.1.7
|
|
603
603
|
group: framework
|
|
604
604
|
side: be
|
|
605
|
-
why: 'the SMTP client inside @starci/mail, which depends on it at exactly this version and which no app file imports; 10.1.7 is the release of 2026-10-18
|
|
605
|
+
why: 'the SMTP client inside @starci/mail, which depends on it at exactly this version and which no app file imports; 10.1.7 is the release of 2026-10-18.'
|
|
606
606
|
'@apollo/server':
|
|
607
607
|
version: 5.5.1
|
|
608
608
|
group: framework
|
|
@@ -652,7 +652,7 @@ pins:
|
|
|
652
652
|
version: 6.29.0
|
|
653
653
|
group: framework
|
|
654
654
|
side: be
|
|
655
|
-
why: 'the Agent of platform/http (its one owner, R90): FetchHttpClient hands fetch a pool that trusts HttpOptions.trustedCa beside the bundled roots (a test world''s per-run fake CA); the 6 line is the undici the Node 22 runtime of the app images bundles, and its Agent also serves the fetch of Node 24; 6.29.0 is the release of 2026-09-25
|
|
655
|
+
why: 'the Agent of platform/http (its one owner, R90): FetchHttpClient hands fetch a pool that trusts HttpOptions.trustedCa beside the bundled roots (a test world''s per-run fake CA); the 6 line is the undici the Node 22 runtime of the app images bundles, and its Agent also serves the fetch of Node 24; 6.29.0 is the release of 2026-09-25.'
|
|
656
656
|
# --- lite identity (integrations/passport, ruling lite-self-hosted-postgres-passport-minio)
|
|
657
657
|
'@nestjs/passport':
|
|
658
658
|
version: 11.0.5
|
|
@@ -859,10 +859,10 @@ slots:
|
|
|
859
859
|
- id: app.packages
|
|
860
860
|
since: 3.17.0
|
|
861
861
|
profiles: [app]
|
|
862
|
-
editionWhy: Lite and full share the slot; a product
|
|
863
|
-
# A workspace package of the app ROOT (packages
|
|
862
|
+
editionWhy: Lite and full share the slot; every root workspace package of a product (its agent SDK, a block library, a module package, a reviewed plugin package) is one in both.
|
|
863
|
+
# A workspace package of the app ROOT (packages/<name>: the agent SDK, a block library, packages/module-<name>): first-party source that both sides import through its
|
|
864
864
|
# package entry points and never past them (ARCH_INTERNAL_IMPORT_OUTSIDE), linted by the root eslint config by subpath.
|
|
865
|
-
path: "packages
|
|
865
|
+
path: "packages/<name>/"
|
|
866
866
|
presence: optional
|
|
867
867
|
tracked: tracked
|
|
868
868
|
tier: none
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// root-packages.mjs - the workspace packages of an app ROOT (packages
|
|
1
|
+
// root-packages.mjs - the workspace packages of an app ROOT (packages/<name>: the agent SDK, a block library, a module package, the managed packages/modules).
|
|
2
2
|
// They lie outside both side folders and belong to no side; both sides import them by package name, through their package entry points
|
|
3
3
|
// (the `exports` of the manifest), so the import graph reads them as first-party packages and the back-end canon never reads them as
|
|
4
4
|
// a third-party library. The app root's one package.json lists them in `workspaces`; the patterns that start with a side folder
|
|
@@ -140,7 +140,7 @@ function observeBlockProblems(observe) {
|
|
|
140
140
|
bad.push('observe.reporter must name the optional error reporter');
|
|
141
141
|
return bad;
|
|
142
142
|
}
|
|
143
|
-
const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'db', 'sides', 'browser', 'lexicon', 'observe']);
|
|
143
|
+
const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'db', 'sides', 'browser', 'modules', 'lexicon', 'observe']);
|
|
144
144
|
const RUNTIME_DECLARATION_KEYS = new Set(['hfs', 'kind', 'project']);
|
|
145
145
|
function productDeclarationProblems(declaration, profiles) {
|
|
146
146
|
const bad = [];
|
|
@@ -150,6 +150,8 @@ function productDeclarationProblems(declaration, profiles) {
|
|
|
150
150
|
bad.push(...dbBlockProblems(declaration.db), ...lexiconBlockProblems(declaration.lexicon), ...observeBlockProblems(declaration.observe));
|
|
151
151
|
if (declaration.browser !== undefined && declaration.browser !== true)
|
|
152
152
|
bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
|
|
153
|
+
if (declaration.modules !== undefined && declaration.modules !== 'data')
|
|
154
|
+
bad.push('modules is `"data"` when the app keeps its modules as data (seeds, no rendered registry package), and is left out otherwise');
|
|
153
155
|
if (declaration.kind !== APP_KIND)
|
|
154
156
|
bad.push(`kind must be ${APP_KIND} (a product is one app repository with a be and an fe side) or ${RUNTIME_KIND} (the StarCi runtime repository)`);
|
|
155
157
|
if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort(byCodeUnit).join() !== profiles.join()) {
|
|
@@ -177,6 +177,7 @@ export function resolveRepoDeclaration(manifest, declaration, { file = HFS_DECLA
|
|
|
177
177
|
// tsconfig.json that type-checks it, both switched on by `browser: true`.
|
|
178
178
|
apps: Object.freeze([]),
|
|
179
179
|
optionalSlots: Object.freeze(declaration.browser === true ? ['app.browser', 'app.browser-config'] : []),
|
|
180
|
+
modules: declaration.modules === 'data' ? 'data' : 'packages',
|
|
180
181
|
connections: Object.freeze([]),
|
|
181
182
|
providers,
|
|
182
183
|
reads: Object.freeze([]),
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// their rules (`settings.starci.hfs`). Both @starci/eslint-canon-be and @starci/eslint-canon-fe ship a byte copy of this file in their
|
|
3
3
|
// runtime/ bundle (scripts/hfs/sync-runtime.mjs), so a path-scoped rule in either package asks the SAME resolver the
|
|
4
4
|
// architecture machine and `starci app check` use, and never tests a path with a regular expression of its own.
|
|
5
|
+
import fs from 'node:fs';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { allowsFile } from './allows.mjs';
|
|
7
8
|
import { openHfs } from './slots.mjs';
|
|
@@ -70,6 +71,36 @@ export const openHfsView = ({ runtimeRoot, repoRoot }) => hfsView(openHfs({ root
|
|
|
70
71
|
* @returns {object} The frozen view.
|
|
71
72
|
*/
|
|
72
73
|
export const declaredHfsView = ({ runtimeRoot, declaration, repoRoot, side }) => hfsView(openHfs({ root: runtimeRoot, declaration, side }), repoRoot);
|
|
74
|
+
const CONTRACT_KEYS = ['errorBase', 'outcome'];
|
|
75
|
+
/**
|
|
76
|
+
* The contract of the package at `packageDir` (app-root relative, `packages/<pkg>`), read from the `starci` key of its manifest. A
|
|
77
|
+
* declaration that is not `{ name, file }` with an identifier name and a file inside `src/` stops the lint, because a rule must not guess.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} repoRoot - The absolute app root.
|
|
80
|
+
* @param {string} packageDir - The package folder, relative to the app root.
|
|
81
|
+
* @returns {object} The declared error base and outcome union, each with its file relative to the app root.
|
|
82
|
+
*/
|
|
83
|
+
export const readPackageContract = (repoRoot, packageDir) => {
|
|
84
|
+
const manifestPath = path.join(repoRoot, packageDir, 'package.json');
|
|
85
|
+
const manifest = (fs.existsSync(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, 'utf8')) : null);
|
|
86
|
+
const declared = manifest?.starci;
|
|
87
|
+
if (declared === undefined)
|
|
88
|
+
return {};
|
|
89
|
+
if (typeof declared !== 'object' || declared === null)
|
|
90
|
+
throw new Error(`${packageDir}/package.json: "starci" is an object ({ errorBase, outcome })`);
|
|
91
|
+
const contract = {};
|
|
92
|
+
for (const key of CONTRACT_KEYS) {
|
|
93
|
+
const entry = declared[key];
|
|
94
|
+
if (entry === undefined)
|
|
95
|
+
continue;
|
|
96
|
+
const { name, file } = (typeof entry === 'object' && entry !== null ? entry : {});
|
|
97
|
+
if (typeof name !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(name) || typeof file !== 'string' || !/^src\/(?:[\w.-]+\/)*[\w.-]+\.ts$/.test(file)) {
|
|
98
|
+
throw new Error(`${packageDir}/package.json: "starci.${key}" is { "name": "<Identifier>", "file": "src/<path>.ts" }`);
|
|
99
|
+
}
|
|
100
|
+
contract[key] = Object.freeze({ name, file: `${packageDir}/${file}` });
|
|
101
|
+
}
|
|
102
|
+
return Object.freeze(contract);
|
|
103
|
+
};
|
|
73
104
|
/**
|
|
74
105
|
* The view of the root workspace packages of an app (`packages/<pkg>/src/**`, slots app.packages and app.module-registry), as the lint of one
|
|
75
106
|
* side sees them: the rule parameters, the apps and the Grammar of `side` (a package is judged by the laws of the side whose subpath a file
|
|
@@ -103,6 +134,15 @@ function packagesView(opened, repoRoot, side) {
|
|
|
103
134
|
const relative = rel(file);
|
|
104
135
|
return slot !== undefined && ROOT_PACKAGE_SLOTS.has(slot) ? relative.split('/').slice(0, 2).join('/') : null;
|
|
105
136
|
};
|
|
137
|
+
const contracts = new Map();
|
|
138
|
+
const packageContract = (file) => {
|
|
139
|
+
const dir = packageOf(file);
|
|
140
|
+
if (dir === null)
|
|
141
|
+
return null;
|
|
142
|
+
if (!contracts.has(dir))
|
|
143
|
+
contracts.set(dir, readPackageContract(repoRoot, dir));
|
|
144
|
+
return contracts.get(dir);
|
|
145
|
+
};
|
|
106
146
|
return Object.freeze({
|
|
107
147
|
repoRoot,
|
|
108
148
|
profile: side,
|
|
@@ -121,6 +161,7 @@ function packagesView(opened, repoRoot, side) {
|
|
|
121
161
|
ownerOf: () => null,
|
|
122
162
|
allows: (file) => allowsFile(opened, rel(file)),
|
|
123
163
|
slot: (id) => opened.slot(id),
|
|
164
|
+
packageContract,
|
|
124
165
|
});
|
|
125
166
|
}
|
|
126
167
|
/**
|