@starci/hfs 2.0.2 → 4.0.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 +25 -0
- package/README.md +33 -33
- package/bin/hfs.mjs +94 -68
- package/lint/run.mjs +161 -0
- package/package.json +3 -2
- package/report/sonar.mjs +14 -29
- package/runtime/engine/admission.mjs +3 -3
- package/runtime/engine/ledger-db.mjs +2 -2
- package/runtime/engine/machine-db.mjs +90 -9
- package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +13 -0
- package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +93 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +32 -13
- package/runtime/knowledge/hfs/peer-integrations.yaml +18 -0
- package/runtime/knowledge/hfs/slots.yaml +193 -128
- package/runtime/knowledge/patterns/fe/folder.yaml +36 -36
- package/runtime/knowledge/sonar-gate.yaml +8 -7
- package/runtime/modules/kernel/failure-codes.yaml +31 -52
- package/runtime/scripts/checks/architecture/backend.mjs +1 -1
- package/runtime/scripts/checks/architecture/config.mjs +31 -11
- package/runtime/scripts/checks/architecture/contracts.mjs +4 -4
- package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +7 -3
- package/runtime/scripts/checks/architecture/framework-pinned.mjs +5 -47
- package/runtime/scripts/checks/architecture/frontend.mjs +6 -4
- package/runtime/scripts/checks/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/checks/architecture/hfs.mjs +105 -67
- package/runtime/scripts/checks/architecture/index.mjs +18 -13
- package/runtime/scripts/checks/architecture/next-data.mjs +3 -2
- package/runtime/scripts/checks/architecture/owners.mjs +12 -6
- package/runtime/scripts/checks/architecture/registration.mjs +1 -1
- package/runtime/scripts/checks/architecture/surface.mjs +91 -0
- package/runtime/scripts/checks/architecture/symbols.mjs +13 -2
- package/runtime/scripts/checks/architecture/test-world-files.mjs +83 -45
- package/runtime/scripts/checks/architecture/typescript.mjs +127 -29
- package/runtime/scripts/checks/typescript-programs.mjs +2 -2
- package/runtime/scripts/lib/hfs-check.mjs +160 -209
- package/runtime/scripts/lib/hfs-path-findings.mjs +95 -0
- package/runtime/scripts/lib/hfs-rules/contract.mjs +15 -42
- package/runtime/scripts/lib/hfs-rules/frontend.mjs +37 -36
- package/runtime/scripts/lib/hfs-rules/peer-integrations.mjs +44 -0
- package/runtime/scripts/lib/hfs-rules/pipeline.mjs +12 -13
- package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +11 -8
- package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +1 -3
- package/runtime/scripts/lib/hfs-slots.mjs +244 -61
- package/runtime/scripts/lib/hfs-view.mjs +9 -7
- package/runtime/scripts/lib/language.mjs +11 -1
- package/runtime/scripts/lib/safe-remove.mjs +95 -10
- package/scaffold/app.mjs +179 -0
- package/scaffold/service.mjs +26 -16
- package/sync/cli.mjs +1 -1
- package/sync/hygiene.mjs +11 -8
- package/sync/index.mjs +109 -111
- package/sync/managed.mjs +9 -8
- package/sync/sonar-key.mjs +20 -22
- package/templates/{be → app}/ci-workflows/github/workflows/ci.yml +5 -11
- package/templates/app/gitignore +6 -0
- package/templates/app/hooks/husky/pre-commit +25 -0
- package/templates/app/hooks/husky/pre-push +7 -0
- package/templates/app/package-scripts/package.json +22 -0
- package/templates/{be → app}/quality-config/sonar-project.properties +4 -3
- package/templates/app/skeleton/.editorconfig +15 -0
- package/templates/app/skeleton/.gitattributes +2 -0
- package/templates/app/skeleton/.nvmrc +1 -0
- package/templates/app/skeleton/.starciwork/features/index.yaml +7 -0
- package/templates/app/skeleton/.starciwork/workspace.yaml +9 -0
- package/templates/app/skeleton/README.md +36 -0
- package/templates/app/skeleton/scripts/codegen.mjs +4 -0
- package/templates/{fe → app}/tool-config/prettierignore +4 -1
- package/templates/be/skeleton/.sops.yaml +2 -0
- package/templates/be/skeleton/.starcistacks/application-stacks.yaml +10 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +27 -6
- package/templates/be/skeleton/apps/__app__/src/main.ts +4 -1
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +11 -0
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +25 -0
- package/templates/be/skeleton/src/modules/domain/identity/errors/identity.error.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +11 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +14 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +6 -0
- package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -1
- package/templates/be/skeleton/src/modules/platform/composition/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +71 -23
- package/templates/be/skeleton/src/modules/platform/config/errors/config.error.ts +14 -16
- package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/server.config.ts +2 -12
- package/templates/be/skeleton/src/modules/platform/errors/domain.error.ts +16 -7
- package/templates/be/skeleton/src/modules/platform/errors/errors/errors.error.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.contracts.ts +33 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.decorators.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.options.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.service.spec.ts +94 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.service.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/errors/http-status.policy.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/errors/index.ts +4 -1
- package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +17 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +31 -0
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.ts +68 -0
- package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.spec.ts +66 -0
- package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.contracts.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.decorators.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module.ts +24 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.options.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.port.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.spec.ts +45 -0
- package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +85 -68
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +14 -10
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +0 -1
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.contracts.ts +25 -0
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.ts +24 -0
- package/templates/fe/skeleton/apps/__app__/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +5 -17
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +11 -22
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +3 -12
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +6 -24
- package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +4 -18
- package/templates/fe/skeleton/apps/__app__/src/app/globals.css +5 -0
- package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +28 -0
- package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +35 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +21 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +17 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +26 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +25 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +27 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +32 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/config/index.ts +12 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +2 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/request.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +4 -0
- package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/proxy.ts +1 -1
- package/runtime/scripts/checks/architecture/size-growth.mjs +0 -73
- package/sync/skeleton.mjs +0 -76
- package/templates/be/gitignore +0 -2
- package/templates/be/hooks/husky/pre-commit +0 -13
- package/templates/be/hooks/husky/pre-push +0 -7
- package/templates/be/package-scripts/package.json +0 -22
- package/templates/be/skeleton/scripts/.gitkeep +0 -0
- package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +0 -9
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +0 -20
- package/templates/be/tool-config/prettierignore +0 -8
- package/templates/fe/ci-workflows/github/workflows/ci.yml +0 -54
- package/templates/fe/gitignore +0 -3
- package/templates/fe/hooks/husky/pre-commit +0 -16
- package/templates/fe/hooks/husky/pre-push +0 -6
- package/templates/fe/package-scripts/package.json +0 -17
- package/templates/fe/parts/api-client.ts +0 -44
- package/templates/fe/parts/api-outcome.ts +0 -7
- package/templates/fe/quality-config/sonar-project.properties +0 -8
- package/templates/fe/skeleton/scripts/.gitkeep +0 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +0 -1
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +0 -3
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +0 -1
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +0 -4
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/navigation.ts +0 -5
- package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +0 -12
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +0 -2
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +0 -9
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +0 -5
- package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +0 -5
- package/templates/fe/skeleton-shared/packages/__family__-api/package.json +0 -12
- package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +0 -1
- package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +0 -3
- package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +0 -1
- package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +0 -5
- package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +0 -18
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +0 -19
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +0 -2
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +0 -12
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +0 -15
- package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +0 -5
- package/templates/fe/tool-config/prettierrc +0 -1
- /package/templates/{be → app}/ci-workflows/github/workflows/e2e.yml +0 -0
- /package/templates/{be → app}/starciwork.gitignore +0 -0
- /package/templates/{be → app}/tool-config/prettierrc +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/next.config.ts +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/config.ts +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/routing.ts +0 -0
package/sync/index.mjs
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
// hfs sync: render, verify and write the files
|
|
1
|
+
// hfs sync: render, verify and write the files an app cannot `extends`.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
3
|
+
// An app is one repository: its root holds the one package.json (all dependencies, all scripts), the lockfile, the hfs.json of
|
|
4
|
+
// kind app, CI, the hooks, the formatter and Sonar configuration and .starciwork; its sides be/ and fe/ hold each side's tool
|
|
5
|
+
// configuration. The managed set is read from the slot manifest (knowledge/hfs/slots.yaml): every slot that names `managedBy`
|
|
6
|
+
// lists, in its `path`, the whole files sync renders, and `managedBy` names the template directory they are rendered from:
|
|
7
|
+
// templates/<scope or common>/<managedBy>/<file path with each leading dot dropped>, the scope being app for a root slot and the
|
|
8
|
+
// side (be, fe) for a side slot, whose files land under that side's folder. Two targets are not whole files of a managedBy slot
|
|
9
|
+
// and are listed in this module: the marked block of the root .gitignore, and .starciwork/.gitignore, which lives inside the
|
|
10
|
+
// .starciwork directory slot. Every file is rendered with the app's hfs.json (both sides and their apps) and, for the Sonar
|
|
11
|
+
// exclusions, the jest preset the app installs for its be side. `--check` compares the sha256 of the rendered content with the
|
|
12
|
+
// file on disk and fails on any drift; `--write` rewrites the drifted files. `.gitignore` is the one shared file: only the marked
|
|
13
|
+
// block is managed and the app's own lines around it are left alone. The root package.json is managed by its `scripts` block
|
|
14
|
+
// only (mode scripts, compared as parsed JSON): the rest of the file (dependencies, npm workspaces of fe/packages/*) is the app's.
|
|
13
15
|
import { createHash } from 'node:crypto';
|
|
14
16
|
import { createRequire } from 'node:module';
|
|
15
17
|
import fs from 'node:fs';
|
|
16
18
|
import path from 'node:path';
|
|
17
19
|
import { braceVariants } from '../runtime/scripts/lib/glob.mjs';
|
|
18
|
-
import { loadSlotManifest, resolveRepoDeclaration } from '../runtime/scripts/lib/hfs-slots.mjs';
|
|
20
|
+
import { APP_SCOPE, SIDES, loadSlotManifest, resolveRepoDeclaration } from '../runtime/scripts/lib/hfs-slots.mjs';
|
|
19
21
|
import { readDeclaredSonarKey } from './sonar-key.mjs';
|
|
20
22
|
|
|
21
23
|
export const TEMPLATES_DIR = path.join(import.meta.dirname, '..', 'templates');
|
|
22
24
|
export const NODE_MAJOR = 22;
|
|
23
|
-
|
|
25
|
+
/** The scopes a target is rendered for: the app root, then each side. */
|
|
26
|
+
export const SCOPES = Object.freeze([APP_SCOPE, ...SIDES]);
|
|
24
27
|
export const BLOCK_BEGIN = '# >>> hfs sync (managed block; do not edit) >>>';
|
|
25
28
|
export const BLOCK_END = '# <<< hfs sync <<<';
|
|
26
|
-
const HEADER =
|
|
29
|
+
const HEADER = scope => `# Generated by hfs sync (${scope === APP_SCOPE ? 'app root' : `${scope} side`}). Do not edit: run "npx hfs sync --write".`;
|
|
27
30
|
|
|
28
31
|
export class SyncError extends Error {
|
|
29
32
|
constructor(code, message) {
|
|
@@ -32,69 +35,54 @@ export class SyncError extends Error {
|
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
// mode: file = whole file, block = the marked block inside a file the
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
+
// mode: file = whole file, block = the marked block inside a file the app also writes to, scripts = the `scripts` block of
|
|
39
|
+
// package.json. A template that can hold a comment opens with {{header}}; a file whose exact content is specified (JSON, the
|
|
40
|
+
// eslint one-liner) has no header.
|
|
38
41
|
const MODE_OF_GROUP = Object.freeze({ 'package-scripts': 'scripts' });
|
|
39
42
|
const LITERAL_FILE = /^[^*?<>{}[\]]+[^/*?<>{}[\]]$/;
|
|
40
43
|
|
|
41
|
-
/** The targets no managedBy slot lists as a whole file (see the header of this module). */
|
|
44
|
+
/** The targets no managedBy slot lists as a whole file (see the header of this module); both belong to the app root. */
|
|
42
45
|
export const UNLISTED_TARGETS = Object.freeze([
|
|
43
|
-
{ path: '.gitignore', template:
|
|
44
|
-
{ path: '.starciwork/.gitignore', template:
|
|
46
|
+
{ path: '.gitignore', template: 'app/gitignore', mode: 'block' },
|
|
47
|
+
{ path: '.starciwork/.gitignore', template: 'app/starciwork.gitignore', mode: 'file' },
|
|
45
48
|
].map(Object.freeze));
|
|
46
49
|
|
|
47
|
-
const templateName = (
|
|
50
|
+
const templateName = (scope, group, file) => `${scope}/${group}/${file.split('/').map(segment => segment.replace(/^\./, '')).join('/')}`;
|
|
51
|
+
const onSide = (scope, file) => (scope === APP_SCOPE ? file : `${scope}/${file}`);
|
|
48
52
|
|
|
49
53
|
/**
|
|
50
|
-
* Every target of `
|
|
51
|
-
* managedBy lists literal files only, and each has a template under
|
|
54
|
+
* Every target of `scope` (app, be or fe), its path relative to that scope's folder: the files the manifest's managedBy slots
|
|
55
|
+
* list, then (app root) the unlisted ones. A slot that names managedBy lists literal files only, and each has a template under
|
|
56
|
+
* its scope or under common.
|
|
52
57
|
*/
|
|
53
|
-
export function targetsOf(
|
|
58
|
+
export function targetsOf(scope, { manifest = loadSlotManifest(), hasTemplate = name => fs.existsSync(path.join(TEMPLATES_DIR, name)) } = {}) {
|
|
54
59
|
const listed = [];
|
|
55
60
|
for (const slot of manifest.slots) {
|
|
56
|
-
if (slot.managedBy === undefined || !slot.profiles.includes(
|
|
61
|
+
if (slot.managedBy === undefined || !slot.profiles.includes(scope)) continue;
|
|
57
62
|
for (const file of braceVariants(slot.path)) {
|
|
58
63
|
if (!LITERAL_FILE.test(file)) throw new SyncError('HFS_SYNC_MANIFEST_MANAGED', `slot ${slot.id} names managedBy but its path ${slot.path} is not a list of literal files (${file})`);
|
|
59
|
-
const template = [
|
|
60
|
-
if (!template) throw new SyncError('HFS_SYNC_TEMPLATE_MISSING', `slot ${slot.id} manages ${file} for ${
|
|
64
|
+
const template = [scope, 'common'].map(dir => templateName(dir, slot.managedBy, file)).find(hasTemplate);
|
|
65
|
+
if (!template) throw new SyncError('HFS_SYNC_TEMPLATE_MISSING', `slot ${slot.id} manages ${file} for ${scope}, but templates/{${scope},common}/${slot.managedBy} has no template for it`);
|
|
61
66
|
listed.push({ path: file, template, mode: MODE_OF_GROUP[slot.managedBy] ?? 'file', slot: slot.id });
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
|
-
return [...listed, ...
|
|
69
|
+
return [...listed, ...(scope === APP_SCOPE ? UNLISTED_TARGETS.map(target => ({ ...target })) : [])];
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
const KEBAB = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
68
72
|
const lf = text => text.replace(/\r\n/g, '\n');
|
|
69
73
|
export const hashOf = text => createHash('sha256').update(lf(text)).digest('hex');
|
|
70
74
|
|
|
71
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* The hfs.json sync reads: an app declaration valid for the manifest it pins (both sides, their apps, connections, optional slots,
|
|
77
|
+
* reads). Sync never renders configs for a declaration the eslint canons and hfs check would refuse, so a pin bump cannot leave
|
|
78
|
+
* lint unable to start. Answers the resolved app.
|
|
79
|
+
*/
|
|
72
80
|
export function validateHfs(hfs, manifest = loadSlotManifest()) {
|
|
73
|
-
const bad = message => { throw new SyncError('HFS_SYNC_HFS_INVALID', message); };
|
|
74
|
-
if (!hfs || typeof hfs !== 'object') bad('hfs.json is not an object');
|
|
75
|
-
if (hfs.hfs !== manifest.major) bad(`hfs must be ${manifest.major}, the major of the slot manifest`);
|
|
76
|
-
if (!PROFILES.includes(hfs.profile)) bad(`profile must be one of ${PROFILES.join(', ')}`);
|
|
77
|
-
if (typeof hfs.project !== 'string' || !KEBAB.test(hfs.project)) bad('project must be a kebab-case name');
|
|
78
|
-
if (!Array.isArray(hfs.apps) || hfs.apps.length === 0) bad('apps must list at least one app');
|
|
79
|
-
if (hfs.stacks !== undefined) {
|
|
80
|
-
if (hfs.profile !== 'fe') bad('stacks is a front-end field: a back-end repository owns its own .starcistacks declaration');
|
|
81
|
-
if (typeof hfs.stacks !== 'string' || !hfs.stacks || path.isAbsolute(hfs.stacks)) bad('stacks must be a relative path to the sibling back-end repository that owns .starcistacks');
|
|
82
|
-
}
|
|
83
|
-
const seen = new Set();
|
|
84
|
-
for (const app of hfs.apps) {
|
|
85
|
-
if (!app || typeof app.name !== 'string' || !KEBAB.test(app.name)) bad('every app needs a kebab-case name');
|
|
86
|
-
if (seen.has(app.name)) bad(`app ${app.name} is listed twice`);
|
|
87
|
-
if (hfs.profile === 'be' && !manifest.appKinds.be.includes(app.kind)) bad(`app ${app.name} needs a kind, one of ${manifest.appKinds.be.join(', ')}`);
|
|
88
|
-
seen.add(app.name);
|
|
89
|
-
}
|
|
90
|
-
// The whole declaration must be valid for the manifest it pins (connections, optional slots, ...): sync never renders
|
|
91
|
-
// configs for a declaration the eslint canons and hfs check would refuse, so a pin bump cannot leave lint unable to start.
|
|
92
81
|
try {
|
|
93
|
-
resolveRepoDeclaration(manifest, hfs);
|
|
82
|
+
return resolveRepoDeclaration(manifest, hfs);
|
|
94
83
|
} catch (error) {
|
|
95
|
-
|
|
84
|
+
throw new SyncError('HFS_SYNC_HFS_INVALID', `hfs.json is not a valid app declaration for manifest ${manifest.version}: ${error.message}`);
|
|
96
85
|
}
|
|
97
|
-
return hfs;
|
|
98
86
|
}
|
|
99
87
|
|
|
100
88
|
function readBundled(name) {
|
|
@@ -110,9 +98,8 @@ export function render(text, vars, readTemplate = readBundled) {
|
|
|
110
98
|
});
|
|
111
99
|
}
|
|
112
100
|
|
|
113
|
-
/** The Sonar exclusions from the jest preset
|
|
114
|
-
export async function loadPresets(root
|
|
115
|
-
if (profile === 'fe') return null;
|
|
101
|
+
/** The Sonar exclusions from the jest preset the app installs for its be side: { sonarExclusions }. */
|
|
102
|
+
export async function loadPresets(root) {
|
|
116
103
|
const name = '@starci/jest-preset';
|
|
117
104
|
const require = createRequire(path.join(root, 'package.json'));
|
|
118
105
|
let resolved;
|
|
@@ -126,38 +113,50 @@ export async function loadPresets(root, profile) {
|
|
|
126
113
|
}
|
|
127
114
|
|
|
128
115
|
/**
|
|
129
|
-
* The
|
|
130
|
-
*
|
|
131
|
-
*
|
|
116
|
+
* The script lines the apps add to the root package.json, each ending with a comma (the template puts them mid-object). Every
|
|
117
|
+
* script runs a side from the app root: a be script from be/ (`cd be && ...`, the folder its tsconfig and jest configuration
|
|
118
|
+
* are relative to), an fe script from fe/. be: `dev:be` runs the one api app from source and restarts it on change (`dev:be:<app>`
|
|
119
|
+
* each, with several), `start:<app>` runs a built api, worker or cli app, `migrate` the migrate app (`migrate:<app>` with several).
|
|
120
|
+
* fe: `dev:fe` runs the one Next app in development (`dev:fe:<app>` each, with several), `start:<app>` serves a built one.
|
|
132
121
|
*/
|
|
133
|
-
export function appScripts(
|
|
122
|
+
export function appScripts(app) {
|
|
134
123
|
const line = (name, command) => `${JSON.stringify(name)}: ${JSON.stringify(command)},`;
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
124
|
+
const be = app.sides.be.apps;
|
|
125
|
+
const fe = app.sides.fe.apps;
|
|
126
|
+
const apis = be.filter(entry => entry.kind === 'api');
|
|
127
|
+
const migrates = be.filter(entry => entry.kind === 'migrate');
|
|
128
|
+
const watch = entry => `cd be && ts-node-dev --respawn -r tsconfig-paths/register apps/${entry.name}/src/main.ts`;
|
|
129
|
+
return [
|
|
130
|
+
...(apis.length === 1 ? [line('dev:be', watch(apis[0]))] : apis.map(entry => line(`dev:be:${entry.name}`, watch(entry)))),
|
|
131
|
+
...(fe.length === 1 ? [line('dev:fe', `npm run codegen --silent && cd fe && next dev apps/${fe[0].name}`)] : fe.map(entry => line(`dev:fe:${entry.name}`, `npm run codegen --silent && cd fe && next dev apps/${entry.name}`))),
|
|
132
|
+
...be.map(entry => line(entry.kind === 'migrate' ? (migrates.length === 1 ? 'migrate' : `migrate:${entry.name}`) : `start:${entry.name}`, `node be/dist/apps/${entry.name}/src/main.js`)),
|
|
133
|
+
...fe.map(entry => line(`start:${entry.name}`, `cd fe && next start apps/${entry.name}`)),
|
|
134
|
+
].join('\n ');
|
|
141
135
|
}
|
|
142
136
|
|
|
143
|
-
/** True when
|
|
144
|
-
export const opensPackages =
|
|
137
|
+
/** True when the fe side opts into a workspace package (repo.packages or any fe.package.* slot): the sources then include fe/packages/. */
|
|
138
|
+
export const opensPackages = side => (side.optionalSlots ?? []).some(id => id === 'repo.packages' || String(id).startsWith('fe.package.'));
|
|
145
139
|
|
|
146
|
-
/** The stylesheets of
|
|
140
|
+
/** The stylesheets of the fe side the CSS canon judges, relative to fe/: every app and every workspace package (slot fe.route, fe.package.*). */
|
|
147
141
|
export const STYLE_GLOB = '{apps,packages}/*/src/**/*.css';
|
|
148
142
|
|
|
149
|
-
/** Every value a template can name, derived from hfs.json and the presets. */
|
|
150
|
-
export function variables(
|
|
151
|
-
const
|
|
143
|
+
/** Every value a template of `scope` can name, derived from hfs.json and the presets. */
|
|
144
|
+
export function variables(app, scope, presets, sonarKey) {
|
|
145
|
+
const fe = app.sides.fe;
|
|
146
|
+
const packages = opensPackages(fe);
|
|
147
|
+
const feApps = fe.apps.map(entry => `fe/apps/${entry.name}`);
|
|
148
|
+
const feTsconfigs = [...feApps.map(dir => `${dir}/tsconfig.json`), ...(packages ? ['fe/packages/*/tsconfig.json'] : [])];
|
|
152
149
|
return {
|
|
153
|
-
header: HEADER(
|
|
154
|
-
appScripts: appScripts(
|
|
155
|
-
|
|
150
|
+
header: HEADER(scope),
|
|
151
|
+
appScripts: appScripts(app),
|
|
152
|
+
buildFe: ['npm run codegen --silent', ...(packages ? ['npm run build --workspaces --if-present'] : []), `cd fe && ${fe.apps.map(entry => `next build apps/${entry.name}`).join(' && ')}`].join(' && '),
|
|
153
|
+
// The fe apps import the workspace packages from their dist/, so the packages are built before the apps are type-checked.
|
|
154
|
+
typecheck: ['npm run codegen --silent', 'tsc -p be/tsconfig.json', ...(packages ? ['npm run build --workspaces --if-present'] : []), ...feTsconfigs.filter(file => !file.includes('*')).map(file => `tsc -p ${file} --noEmit`), ...(packages ? ['npm run typecheck --workspaces --if-present'] : [])].join(' && '),
|
|
156
155
|
nodeMajor: String(NODE_MAJOR),
|
|
157
|
-
sonarKey: sonarKey ??
|
|
158
|
-
sonarExclusions: presets?.sonarExclusions
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
sonarKey: sonarKey ?? app.project,
|
|
157
|
+
sonarExclusions: [presets?.sonarExclusions, '**/.next/**', '**/node_modules/**', '**/src/messages/**'].filter(Boolean).join(','),
|
|
158
|
+
sonarSources: ['be/apps', 'be/src', 'fe/apps', ...(packages ? ['fe/packages'] : [])].join(','),
|
|
159
|
+
tsconfigPaths: ['be/tsconfig.json', ...feTsconfigs].join(','),
|
|
161
160
|
styleGlob: STYLE_GLOB,
|
|
162
161
|
};
|
|
163
162
|
}
|
|
@@ -175,23 +174,27 @@ function parseScripts(text) {
|
|
|
175
174
|
}
|
|
176
175
|
|
|
177
176
|
/**
|
|
178
|
-
* The managed content of every target
|
|
179
|
-
* `scripts`, the parsed block
|
|
180
|
-
*
|
|
177
|
+
* The managed content of every target of the app: [{ path, mode, content, hash, scope }], paths app-relative (a side's targets under
|
|
178
|
+
* its folder); a scripts target also carries `scripts`, the parsed block. `sonarKey` is the key the be side's stack declaration
|
|
179
|
+
* names; without one the key is the project.
|
|
181
180
|
*/
|
|
182
181
|
export function renderTargets(hfs, presets, { sonarKey, readTemplate = readBundled, manifest = loadSlotManifest() } = {}) {
|
|
183
|
-
validateHfs(hfs, manifest);
|
|
184
|
-
const
|
|
185
|
-
return
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
182
|
+
const app = validateHfs(hfs, manifest);
|
|
183
|
+
const hasTemplate = name => { try { return readTemplate(name) !== undefined; } catch { return false; } };
|
|
184
|
+
return SCOPES.flatMap(scope => {
|
|
185
|
+
const vars = variables(app, scope, presets, sonarKey);
|
|
186
|
+
return targetsOf(scope, { manifest, hasTemplate }).map(target => {
|
|
187
|
+
const body = render(readTemplate(target.template), vars, readTemplate);
|
|
188
|
+
const where = onSide(scope, target.path);
|
|
189
|
+
if (target.mode === 'scripts') {
|
|
190
|
+
const scripts = parseScripts(body);
|
|
191
|
+
if (!scripts) throw new SyncError('HFS_SYNC_TEMPLATE_VARIABLE', `the rendered ${target.template} has no scripts object`);
|
|
192
|
+
const content = scriptsText(scripts);
|
|
193
|
+
return { path: where, scope, mode: 'scripts', content, hash: hashOf(content), scripts };
|
|
194
|
+
}
|
|
195
|
+
const managed = target.mode === 'block' ? `${BLOCK_BEGIN}\n${body.replace(/\n*$/, '\n')}${BLOCK_END}\n` : body;
|
|
196
|
+
return { path: where, scope, mode: target.mode, content: managed, hash: hashOf(managed) };
|
|
197
|
+
});
|
|
195
198
|
});
|
|
196
199
|
}
|
|
197
200
|
|
|
@@ -256,42 +259,37 @@ export function writeTargets(root, targets) {
|
|
|
256
259
|
return written;
|
|
257
260
|
}
|
|
258
261
|
|
|
262
|
+
/** The hfs.json object of the app at `root` (validated), or a SyncError. */
|
|
259
263
|
export function loadHfs(root) {
|
|
260
264
|
const file = path.join(root, 'hfs.json');
|
|
261
|
-
if (!fs.existsSync(file)) throw new SyncError('HFS_SYNC_HFS_INVALID', `${file} does not exist`);
|
|
265
|
+
if (!fs.existsSync(file)) throw new SyncError('HFS_SYNC_HFS_INVALID', `${file} does not exist; hfs sync runs at the app root`);
|
|
266
|
+
let hfs;
|
|
262
267
|
try {
|
|
263
|
-
|
|
268
|
+
hfs = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
264
269
|
} catch (error) {
|
|
265
|
-
if (error instanceof SyncError) throw error;
|
|
266
270
|
throw new SyncError('HFS_SYNC_HFS_INVALID', `${file} is not valid JSON: ${error.message}`);
|
|
267
271
|
}
|
|
272
|
+
validateHfs(hfs);
|
|
273
|
+
return hfs;
|
|
268
274
|
}
|
|
269
275
|
|
|
270
|
-
/** The rendered targets of the
|
|
276
|
+
/** The rendered targets of the app at `root`: its hfs.json, the Sonar key the be side's stack declaration names, the presets it installs. */
|
|
271
277
|
export async function renderRepo(root, { presets, parseYaml } = {}) {
|
|
272
278
|
const hfs = loadHfs(root);
|
|
273
|
-
const sonarKey = await readDeclaredSonarKey(root, { parseYaml,
|
|
274
|
-
return { hfs, targets: renderTargets(hfs, presets ?? await loadPresets(root
|
|
279
|
+
const sonarKey = await readDeclaredSonarKey(root, { parseYaml, fail: message => { throw new SyncError('HFS_SYNC_SONAR_KEY', message); } });
|
|
280
|
+
return { hfs, targets: renderTargets(hfs, presets ?? await loadPresets(root), { sonarKey }) };
|
|
275
281
|
}
|
|
276
282
|
|
|
277
|
-
/** `hfs sync --check | --write [--root <dir>]
|
|
283
|
+
/** `hfs sync --check | --write [--root <dir>]` at the app root; returns the exit code (0 clean, 1 drift or error, 2 usage). */
|
|
278
284
|
export async function runSync(argv, { cwd = process.cwd(), out = line => process.stdout.write(`${line}\n`), presets, parseYaml } = {}) {
|
|
279
|
-
const check = argv.includes('--check'), write = argv.includes('--write')
|
|
285
|
+
const check = argv.includes('--check'), write = argv.includes('--write');
|
|
280
286
|
const rootAt = argv.indexOf('--root');
|
|
281
|
-
if ([check, write
|
|
282
|
-
out('usage: hfs sync (--check | --write
|
|
287
|
+
if ([check, write].filter(Boolean).length !== 1) {
|
|
288
|
+
out('usage: hfs sync (--check | --write) [--root <dir>]');
|
|
283
289
|
return 2;
|
|
284
290
|
}
|
|
285
291
|
const root = rootAt >= 0 ? path.resolve(cwd, argv[rootAt + 1] ?? '') : cwd;
|
|
286
292
|
try {
|
|
287
|
-
const hfs = loadHfs(root);
|
|
288
|
-
if (init) {
|
|
289
|
-
const { initSkeleton } = await import('./skeleton.mjs');
|
|
290
|
-
const { created, skipped } = initSkeleton(root, hfs);
|
|
291
|
-
for (const file of created) out(`created ${file}`);
|
|
292
|
-
out(`hfs sync --init: ${created.length} created, ${skipped.length} already exist and were left alone`);
|
|
293
|
-
return 0;
|
|
294
|
-
}
|
|
295
293
|
const { targets } = await renderRepo(root, { presets, parseYaml });
|
|
296
294
|
if (write) {
|
|
297
295
|
const written = writeTargets(root, targets);
|
package/sync/managed.mjs
CHANGED
|
@@ -72,14 +72,15 @@ function driftFindings(repoRoot, targets) {
|
|
|
72
72
|
for (const result of checkTargets(repoRoot, targets)) {
|
|
73
73
|
const target = targets.find(candidate => candidate.path === result.path);
|
|
74
74
|
if (!fs.existsSync(path.join(repoRoot, result.path)) || result.status === 'ok') continue;
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// A side's tsconfig.json (be/tsconfig.json, fe/tsconfig.json: the side's own root) is judged by flag (R22).
|
|
76
|
+
if (target.scope !== 'app' && result.path === `${target.scope}/${TS_STRICT_FILE}`) {
|
|
77
|
+
const flags = tsStrictFindings(fs.readFileSync(path.join(repoRoot, result.path), 'utf8'), target.content, result.path);
|
|
77
78
|
if (flags.length) {
|
|
78
79
|
findings.push(...flags);
|
|
79
80
|
continue;
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
|
-
const code = target.mode === 'block' ? 'HFS_GITIGNORE_BLOCK_DRIFT' : ONE_LINER_FILES.has(result.path) ? 'HFS_RULE_OFF_WITHOUT_REPLACEMENT' : result.path === SONAR_PROPERTIES_FILE ? 'HFS_SONAR_CONFIG' : 'HFS_MANAGED_FILE_DRIFT';
|
|
83
|
+
const code = target.mode === 'block' ? 'HFS_GITIGNORE_BLOCK_DRIFT' : ONE_LINER_FILES.has(path.posix.basename(result.path)) ? 'HFS_RULE_OFF_WITHOUT_REPLACEMENT' : result.path === SONAR_PROPERTIES_FILE ? 'HFS_SONAR_CONFIG' : 'HFS_MANAGED_FILE_DRIFT';
|
|
83
84
|
const where = result.difference ? `; line ${result.difference.line} expected ${JSON.stringify(result.difference.expected)}, found ${JSON.stringify(result.difference.actual)}` : '';
|
|
84
85
|
const what = target.mode === 'block' ? `the managed block of ${result.path} is ${result.status === 'missing' ? 'missing' : 'not its render'}` : target.mode === 'scripts' ? 'the scripts block of package.json is not the rendered one' : `${result.path} is not its render${code === 'HFS_RULE_OFF_WITHOUT_REPLACEMENT' ? ', so a rule can be off or redefined in it' : ''}`;
|
|
85
86
|
findings.push({ code, level: 'error', path: result.path, mode: target.mode, expectedHash: result.expectedHash, ...(result.actualHash ? { actualHash: result.actualHash } : {}), message: `${what} (expected sha256 ${result.expectedHash.slice(0, 12)}${result.actualHash ? `, found ${result.actualHash.slice(0, 12)}` : ''}${where}); run "npx hfs sync --write"` });
|
|
@@ -87,9 +88,9 @@ function driftFindings(repoRoot, targets) {
|
|
|
87
88
|
return findings;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
/** R11: the quality gate the stack declaration names is the one gate of the bundled knowledge/sonar-gate.yaml. */
|
|
91
|
-
function sonarGateFindings(repoRoot,
|
|
92
|
-
const declared = readDeclaredSonarGate(repoRoot, { parseYaml
|
|
91
|
+
/** R11: the quality gate the be side's stack declaration names is the one gate of the bundled knowledge/sonar-gate.yaml. */
|
|
92
|
+
function sonarGateFindings(repoRoot, parseYaml) {
|
|
93
|
+
const declared = readDeclaredSonarGate(repoRoot, { parseYaml });
|
|
93
94
|
if (declared === null) return [];
|
|
94
95
|
const gate = (parseYaml ?? bundledParseYaml)(fs.readFileSync(SONAR_GATE_FILE, 'utf8'))?.gate?.name;
|
|
95
96
|
if (declared.qualityGate === gate) return [];
|
|
@@ -160,9 +161,9 @@ export async function managedFindings({ repoRoot, tracked, presets, parseYaml })
|
|
|
160
161
|
if (error instanceof SyncError && error.code === 'HFS_SYNC_HFS_INVALID') return [];
|
|
161
162
|
throw error;
|
|
162
163
|
}
|
|
163
|
-
const {
|
|
164
|
+
const { targets } = rendered;
|
|
164
165
|
const findings = driftFindings(repoRoot, targets);
|
|
165
|
-
findings.push(...sonarGateFindings(repoRoot,
|
|
166
|
+
findings.push(...sonarGateFindings(repoRoot, parseYaml));
|
|
166
167
|
findings.push(...ruleFileFindings(repoRoot, tracked));
|
|
167
168
|
findings.push(...toolKeyFindings(repoRoot, tracked));
|
|
168
169
|
findings.push(...flagFindings(repoRoot, tracked, new Set(targets.map(target => target.path))));
|
package/sync/sonar-key.mjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
// The Sonar project key of
|
|
2
|
-
// services.sonar.projects[]) when one
|
|
3
|
-
//
|
|
1
|
+
// The Sonar project key of an app: read from its stack declaration (be/.starcistacks/application-stacks.yaml,
|
|
2
|
+
// services.sonar.projects[]) when one names the app, so there is one source of the key. Only when no declaration names the app
|
|
3
|
+
// does sync derive the key from the project name.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { parseYaml as bundledParseYaml } from '../runtime/engine/yaml.mjs';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/** The side folder that holds an app's stack declaration. */
|
|
9
|
+
export const STACKS_SIDE = 'be';
|
|
10
|
+
/** The stack declaration, app-relative. */
|
|
11
|
+
export const DECLARATION = path.join(STACKS_SIDE, '.starcistacks', 'application-stacks.yaml');
|
|
9
12
|
|
|
10
|
-
/** The repository name the declaration lists its projects under: package.json name, else the folder name. */
|
|
13
|
+
/** The repository name the declaration lists its projects under: the app package.json name, else the folder name. */
|
|
11
14
|
export function repositoryName(root) {
|
|
12
15
|
try {
|
|
13
16
|
const name = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name;
|
|
@@ -26,31 +29,26 @@ export function declaredSonarKeys(declaration, repository) {
|
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
|
-
* The declared key
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* is refused, because the key would silently fall back to a derived one.
|
|
33
|
-
* `parseYaml(text)` is injected; the default is the YAML parser bundled in this package (the package installs with no node_modules).
|
|
32
|
+
* The declared key of the app at `root`, or null when the app has no stack declaration or the declaration names none for it.
|
|
33
|
+
* Two keys for one app are refused through `fail`. `parseYaml(text)` is injected; the default is the YAML parser bundled in this
|
|
34
|
+
* package (the package installs with no node_modules).
|
|
34
35
|
*/
|
|
35
|
-
export async function readDeclaredSonarKey(root, { parseYaml, fail
|
|
36
|
-
const file = path.join(
|
|
37
|
-
if (!fs.existsSync(file))
|
|
38
|
-
if (stacks !== undefined) fail(`hfs.json stacks points at ${stacks}, which has no ${DECLARATION}`);
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
36
|
+
export async function readDeclaredSonarKey(root, { parseYaml, fail }) {
|
|
37
|
+
const file = path.join(root, DECLARATION);
|
|
38
|
+
if (!fs.existsSync(file)) return null;
|
|
41
39
|
const parse = parseYaml ?? bundledParseYaml;
|
|
42
40
|
const keys = declaredSonarKeys(parse(fs.readFileSync(file, 'utf8')), repositoryName(root));
|
|
43
|
-
if (keys.length > 1) fail(`${DECLARATION} declares ${keys.length} Sonar projects for ${repositoryName(root)} (${keys.join(', ')}); one
|
|
41
|
+
if (keys.length > 1) fail(`${DECLARATION.split(path.sep).join('/')} declares ${keys.length} Sonar projects for ${repositoryName(root)} (${keys.join(', ')}); one app has one key`);
|
|
44
42
|
return keys[0] ?? null;
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
/**
|
|
48
|
-
* The quality gate the declaration names for Sonar: `{ file, qualityGate }
|
|
49
|
-
* disabled in it, or it declares no Sonar service. The one gate is knowledge/sonar-gate.yaml;
|
|
50
|
-
* never states thresholds of its own.
|
|
46
|
+
* The quality gate the declaration names for Sonar: `{ file, qualityGate }` (file app-relative), or null when there is no
|
|
47
|
+
* declaration, Sonar is disabled in it, or it declares no Sonar service. The one gate is knowledge/sonar-gate.yaml; an app names it
|
|
48
|
+
* and never states thresholds of its own.
|
|
51
49
|
*/
|
|
52
|
-
export function readDeclaredSonarGate(root, { parseYaml
|
|
53
|
-
const file = path.join(
|
|
50
|
+
export function readDeclaredSonarGate(root, { parseYaml } = {}) {
|
|
51
|
+
const file = path.join(root, DECLARATION);
|
|
54
52
|
if (!fs.existsSync(file)) return null;
|
|
55
53
|
const sonar = (parseYaml ?? bundledParseYaml)(fs.readFileSync(file, 'utf8'))?.services?.sonar;
|
|
56
54
|
if (!sonar || sonar.mode === 'disabled') return null;
|
|
@@ -21,24 +21,18 @@ jobs:
|
|
|
21
21
|
node-version: {{nodeMajor}}
|
|
22
22
|
cache: npm
|
|
23
23
|
- run: npm ci
|
|
24
|
-
- name: hfs check
|
|
25
|
-
run: npm run hfs:report
|
|
26
24
|
- name: lint
|
|
27
|
-
run: npm run lint
|
|
28
|
-
- name: lint report
|
|
29
|
-
if: ${{ !cancelled() }}
|
|
30
|
-
run: npm run lint:report
|
|
31
|
-
- name: eslint sonar import
|
|
32
|
-
if: ${{ !cancelled() }}
|
|
33
|
-
run: npx hfs report eslint reports/eslint.json reports/eslint.sonar.json
|
|
25
|
+
run: npm run lint -- --sonar reports/lint.sonar.json
|
|
34
26
|
- name: format
|
|
35
27
|
run: npm run format:check
|
|
36
28
|
- name: typecheck
|
|
37
29
|
run: npm run typecheck
|
|
38
30
|
- name: unit
|
|
39
31
|
run: npm test -- --ci
|
|
40
|
-
- name: build
|
|
41
|
-
run: npm run build
|
|
32
|
+
- name: build be
|
|
33
|
+
run: npm run build:be
|
|
34
|
+
- name: build fe
|
|
35
|
+
run: npm run build:fe
|
|
42
36
|
- uses: SonarSource/sonarqube-scan-action@v7
|
|
43
37
|
if: ${{ !cancelled() && env.SONAR_TOKEN != '' }}
|
|
44
38
|
env:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{{header}}
|
|
2
|
+
# Commit gate: the secrets guard and work hygiene of the staged files, types, lint and format of the staged files of each side,
|
|
3
|
+
# stylelint over the staged CSS of fe, the unit specs of be the staged files touch. Never integration, e2e or contract.
|
|
4
|
+
npx hfs work-hygiene
|
|
5
|
+
npm run typecheck
|
|
6
|
+
be=$(git diff --cached --name-only --diff-filter=ACMR --relative=be -- '*.ts' '*.mts' '*.cts' '*.js' '*.mjs' '*.cjs' || true)
|
|
7
|
+
if [ -n "$be" ]; then
|
|
8
|
+
(cd be && npx eslint $be)
|
|
9
|
+
fi
|
|
10
|
+
fe=$(git diff --cached --name-only --diff-filter=ACMR --relative=fe -- '*.ts' '*.tsx' '*.mts' '*.cts' '*.js' '*.jsx' '*.mjs' '*.cjs' || true)
|
|
11
|
+
if [ -n "$fe" ]; then
|
|
12
|
+
(cd fe && npx eslint --max-warnings=0 --no-warn-ignored $fe)
|
|
13
|
+
fi
|
|
14
|
+
styles=$(git diff --cached --name-only --diff-filter=ACMR --relative=fe -- '*.css' || true)
|
|
15
|
+
if [ -n "$styles" ]; then
|
|
16
|
+
(cd fe && npx stylelint $styles)
|
|
17
|
+
fi
|
|
18
|
+
formatted=$(git diff --cached --name-only --diff-filter=ACMR || true)
|
|
19
|
+
if [ -n "$formatted" ]; then
|
|
20
|
+
npx prettier --check --ignore-unknown $formatted
|
|
21
|
+
fi
|
|
22
|
+
specs=$(git diff --cached --name-only --diff-filter=ACMR --relative=be -- '*.ts' | grep -v -E '(^|/)src/tests/(world|integration|e2e|contract)/|\.(integration|e2e|contract)-spec\.ts$' || true)
|
|
23
|
+
if [ -n "$specs" ]; then
|
|
24
|
+
npm run test:affected -- --findRelatedTests $specs
|
|
25
|
+
fi
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{{header}}
|
|
2
|
+
# Push gate: types, lint (one `hfs lint`: eslint per side, the app checks, stylelint over fe), format, the unit specs of be affected
|
|
3
|
+
# since main. Never integration, e2e or contract.
|
|
4
|
+
npm run typecheck
|
|
5
|
+
npm run lint
|
|
6
|
+
npm run format:check
|
|
7
|
+
npm run test:affected -- --changedSince=origin/main
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scripts": {
|
|
3
|
+
"prepare": "husky",
|
|
4
|
+
{{appScripts}}
|
|
5
|
+
"build:be": "cd be && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
6
|
+
"build:fe": "{{buildFe}}",
|
|
7
|
+
"codegen": "node scripts/codegen.mjs",
|
|
8
|
+
"typecheck": "{{typecheck}}",
|
|
9
|
+
"typecheck:tests": "tsc -p be/src/tests/tsconfig.json",
|
|
10
|
+
"lint": "npm run codegen --silent && hfs lint",
|
|
11
|
+
"lint:fix": "npm run codegen --silent && hfs lint --fix",
|
|
12
|
+
"format": "prettier --write .",
|
|
13
|
+
"format:check": "prettier --check .",
|
|
14
|
+
"test": "cd be && jest --selectProjects unit --coverage",
|
|
15
|
+
"test:affected": "cd be && jest --selectProjects unit --passWithNoTests",
|
|
16
|
+
"test:integration": "npm run typecheck:tests && cd be && jest --selectProjects integration",
|
|
17
|
+
"test:e2e": "npm run typecheck:tests && cd be && jest --selectProjects e2e",
|
|
18
|
+
"test:contract": "npm run typecheck:tests && cd be && jest --selectProjects contract",
|
|
19
|
+
"test:stack": "cd be && starci-test-stack",
|
|
20
|
+
"contract:emit": "hfs emit-contracts"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{{header}}
|
|
2
2
|
sonar.projectKey={{sonarKey}}
|
|
3
3
|
sonar.sourceEncoding=UTF-8
|
|
4
|
-
sonar.sources=
|
|
5
|
-
sonar.tests=apps,src
|
|
4
|
+
sonar.sources={{sonarSources}}
|
|
5
|
+
sonar.tests=be/apps,be/src
|
|
6
6
|
sonar.exclusions={{sonarExclusions}}
|
|
7
7
|
sonar.test.inclusions=**/*.spec.ts
|
|
8
|
-
sonar.
|
|
8
|
+
sonar.typescript.tsconfigPaths={{tsconfigPaths}}
|
|
9
|
+
sonar.externalIssuesReportPaths=reports/lint.sonar.json
|
|
9
10
|
sonar.nodejs.maxspace=8192
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
[*]
|
|
4
|
+
charset = utf-8
|
|
5
|
+
end_of_line = lf
|
|
6
|
+
indent_style = space
|
|
7
|
+
indent_size = 4
|
|
8
|
+
insert_final_newline = true
|
|
9
|
+
trim_trailing_whitespace = true
|
|
10
|
+
|
|
11
|
+
[*.{json,yml,yaml}]
|
|
12
|
+
indent_size = 2
|
|
13
|
+
|
|
14
|
+
[*.md]
|
|
15
|
+
trim_trailing_whitespace = false
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
22
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
schema: work/catalog@1
|
|
2
|
+
id: {{project}}
|
|
3
|
+
description: Product catalog of the features directory. One entry per feature; every record of a feature lives under its directory.
|
|
4
|
+
features:
|
|
5
|
+
- id: system-health
|
|
6
|
+
directory: features/system-health
|
|
7
|
+
description: The liveness probe the platform asks before it routes traffic to the app.
|