@starci/hfs 4.0.10 → 4.2.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 +5 -1
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +65 -24
- package/package.json +1 -1
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +194 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +222 -239
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/release-config.mjs +28 -0
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +35 -31
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +161 -65
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/knowledge/patterns/be/api.yaml +7 -24
- package/runtime/knowledge/patterns/be/cli.yaml +3 -12
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
- package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
- package/runtime/modules/kernel/failure-codes.yaml +1 -41
- package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +126 -265
- package/runtime/modules/models/tiers.yaml +66 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +34 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
- package/runtime/scripts/api/process/run-program.mjs +2 -1
- package/runtime/scripts/api/sops/lib.mjs +198 -120
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +164 -104
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/architecture.mjs +3 -4
- package/runtime/scripts/hfs/catalog-once.mjs +15 -0
- package/runtime/scripts/hfs/check.mjs +63 -71
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +21 -4
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +59 -48
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +74 -695
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/env.mjs +10 -0
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/graphql-contract.mjs +158 -322
- package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/in-order.mjs +71 -0
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/path-key.mjs +38 -5
- package/runtime/scripts/lib/pid-alive.mjs +7 -0
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/same-text.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sleep-sync.mjs +2 -2
- package/runtime/scripts/lib/sops-envelope.mjs +95 -48
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/runtime/scripts/lib/yaml-cached.mjs +14 -0
- package/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
- package/runtime/engine/admission.mjs +0 -308
- package/runtime/engine/canonical-json.mjs +0 -10
- package/runtime/engine/db/blob.mjs +0 -315
- package/runtime/engine/db/ledger-paths.mjs +0 -83
- package/runtime/engine/db/ledger.mjs +0 -1118
- package/runtime/engine/db/machine-connection.mjs +0 -135
- package/runtime/engine/db/machine-schema.mjs +0 -84
- package/runtime/engine/db/machine.mjs +0 -1367
- package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
- package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
- package/runtime/engine/db/provider-reservations.mjs +0 -101
- package/runtime/engine/digest.mjs +0 -16
- package/runtime/engine/refuse.mjs +0 -11
- package/runtime/modules/ops/_labels.yaml +0 -53
- package/runtime/scripts/api/node/lib.mjs +0 -14
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/process/lib.mjs +0 -111
- package/runtime/scripts/api/process/owned-process.mjs +0 -78
- package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
- package/runtime/scripts/connectors/lib.mjs +0 -488
- package/runtime/scripts/lib/clip.mjs +0 -19
- package/runtime/scripts/lib/display-names.mjs +0 -259
- package/runtime/scripts/lib/example-refs.mjs +0 -158
- package/runtime/scripts/lib/json-schema.mjs +0 -52
- package/runtime/scripts/lib/process-identity.mjs +0 -6
- package/runtime/scripts/lib/read-yaml.mjs +0 -18
- package/runtime/scripts/lib/redact.mjs +0 -161
- package/runtime/scripts/lib/sleep.mjs +0 -7
- package/runtime/scripts/lib/source-phrases.mjs +0 -34
- package/runtime/scripts/lib/sqlite.mjs +0 -21
|
@@ -13,18 +13,38 @@
|
|
|
13
13
|
// and `starci app check` compares the files with this derivation. Nothing here lists a path of a product: it all comes from slots.yaml.
|
|
14
14
|
import { braceVariants, globExpression } from '../lib/glob.mjs';
|
|
15
15
|
import { logicRolesOf } from './manifest-shape.mjs';
|
|
16
|
+
import { byCodeUnit } from '../lib/list.mjs';
|
|
17
|
+
import { trimTrailingSlashes } from './trailing-slashes.mjs';
|
|
16
18
|
|
|
17
19
|
/** The component that holds the shared infrastructure: the platform tier and every capability more than one service app composes. */
|
|
18
20
|
export const PLATFORM_COMPONENT = 'platform';
|
|
19
21
|
const SERVICE_APP_KINDS = new Set(['api', 'worker']);
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
+
/** `text` with every `<placeholder>` (a `<`, at least one character, the next `>`) replaced by `*`; a `<` with no closing `>` stays. */
|
|
23
|
+
export const star = (text) => {
|
|
24
|
+
const source = String(text);
|
|
25
|
+
let result = '';
|
|
26
|
+
let copied = 0;
|
|
27
|
+
let open = source.indexOf('<');
|
|
28
|
+
while (open !== -1) {
|
|
29
|
+
const close = source.indexOf('>', open + 1);
|
|
30
|
+
if (close === -1) break;
|
|
31
|
+
if (close === open + 1) {
|
|
32
|
+
open = source.indexOf('<', close);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
result += `${source.slice(copied, open)}*`;
|
|
36
|
+
copied = close + 1;
|
|
37
|
+
open = source.indexOf('<', copied);
|
|
38
|
+
}
|
|
39
|
+
return result + source.slice(copied);
|
|
40
|
+
};
|
|
41
|
+
export const segments = (dir, trimSlashes = false) => (trimSlashes ? trimTrailingSlashes(dir) : dir).split('/').filter(Boolean);
|
|
22
42
|
const isDir = (path) => path.endsWith('/');
|
|
23
43
|
|
|
24
44
|
/** True when directory pattern `inner` lies inside `outer` (an outer `*` segment matches any one inner segment) or is the same. */
|
|
25
|
-
function nested(inner, outer) {
|
|
26
|
-
const a = segments(inner);
|
|
27
|
-
const b = segments(outer);
|
|
45
|
+
export function nested(inner, outer) {
|
|
46
|
+
const a = segments(inner, true);
|
|
47
|
+
const b = segments(outer, true);
|
|
28
48
|
return a.length >= b.length && b.every((segment, index) => segment === '*' ? true : segment === a[index]);
|
|
29
49
|
}
|
|
30
50
|
|
|
@@ -37,6 +57,24 @@ const variantsOf = (slot) => braceVariants(slot.path).map(star);
|
|
|
37
57
|
*/
|
|
38
58
|
const beSlots = (manifest, providers) => manifest.slots.filter((slot) => slot.profiles.includes('be') && slot.tracked === 'tracked' && (slot.provider === undefined || providers.includes(slot.provider)));
|
|
39
59
|
|
|
60
|
+
function addRequiredPatterns(sonar, required, suffixes) {
|
|
61
|
+
const escapedSuffixes = suffixes.map((suffix) => suffix.replaceAll('-', String.raw`\-`)).join('|');
|
|
62
|
+
const isRole = new RegExp(String.raw`\.(?:${escapedSuffixes})\.ts$`);
|
|
63
|
+
for (const slot of required) {
|
|
64
|
+
for (const entry of [...(slot.requires ?? []), ...(slot.allows ?? [])]) {
|
|
65
|
+
addEntryPatterns(sonar, slot, entry, isRole);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function addEntryPatterns(sonar, slot, entry, isRole) {
|
|
71
|
+
if (!entry.endsWith('.ts') || entry.includes('<role>')) return;
|
|
72
|
+
for (const name of braceVariants(entry)) {
|
|
73
|
+
if (isRole.test(star(name).replaceAll('*', 'x'))) continue;
|
|
74
|
+
for (const dir of variantsOf(slot)) sonar.add(`be/${dir}${star(name)}`.replace(/\/\/+/g, '/'));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
40
78
|
/** The directory a path pattern lives in (itself when it is one). */
|
|
41
79
|
const dirOf = (path) => (isDir(path) ? path : path.slice(0, path.lastIndexOf('/') + 1));
|
|
42
80
|
|
|
@@ -64,31 +102,26 @@ export function coverageScope(manifest, providers = []) {
|
|
|
64
102
|
if (!isDir(dir)) throw new Error(`slot ${slot.id} is coverage: required but its path ${slot.path} is not a directory`);
|
|
65
103
|
return dir;
|
|
66
104
|
}));
|
|
67
|
-
const roots = minimal(requiredDirs).sort();
|
|
105
|
+
const roots = minimal(requiredDirs).sort(byCodeUnit);
|
|
68
106
|
const nonePaths = none.flatMap((slot) => variantsOf(slot));
|
|
69
107
|
for (const dir of nonePaths.filter(isDir)) if (requiredDirs.some((other) => nested(other, dir))) throw new Error(`a coverage: none directory ${dir} contains a coverage: required slot`);
|
|
70
108
|
const noneUnique = [...new Set(nonePaths)];
|
|
71
109
|
const noneMinimal = noneUnique.filter((path) => !noneUnique.some((other) => other !== path && isDir(other) && nested(dirOf(path), other)));
|
|
72
|
-
const excludes = noneMinimal.filter((path) => isDir(path) && roots.some((root) => nested(path, root))).sort();
|
|
110
|
+
const excludes = noneMinimal.filter((path) => isDir(path) && roots.some((root) => nested(path, root))).sort(byCodeUnit);
|
|
73
111
|
const suffixes = manifest.ruleParams.be.suffixes;
|
|
74
|
-
const isRole = new RegExp(`\\.(?:${suffixes.map((s) => s.replace(/-/g, '\\-')).join('|')})\\.ts$`);
|
|
75
112
|
const sonar = new Set(noneMinimal.map((path) => `be/${path}${isDir(path) ? '**' : ''}`));
|
|
76
113
|
for (const role of suffixes.filter((suffix) => !roles.includes(suffix))) sonar.add(`be/**/*.${role}.ts`);
|
|
77
|
-
|
|
78
|
-
for (const entry of [...(slot.requires ?? []), ...(slot.allows ?? [])]) {
|
|
79
|
-
if (!/\.ts$/.test(entry) || entry.includes('<role>')) continue;
|
|
80
|
-
for (const name of braceVariants(entry)) {
|
|
81
|
-
if (isRole.test(star(name).replace(/\*/g, 'x'))) continue;
|
|
82
|
-
for (const dir of variantsOf(slot)) sonar.add(`be/${dir}${star(name)}`.replace(/\/\/+/g, '/'));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
114
|
+
addRequiredPatterns(sonar, required, suffixes);
|
|
86
115
|
sonar.add('fe/**');
|
|
87
|
-
|
|
116
|
+
noneMinimal.sort(byCodeUnit);
|
|
117
|
+
return { roles, roots, excludes, none: noneMinimal, sonar: [...sonar].sort(byCodeUnit), codecovPaths: roots.map((root) => `be/${root}**`) };
|
|
88
118
|
}
|
|
89
119
|
|
|
90
120
|
/** The glob a root measures: every file of a logic role below it (`src/modules/domain/*\/**\/*.{service,policy}.ts`). */
|
|
91
|
-
export const rootGlob = (root, roles) =>
|
|
121
|
+
export const rootGlob = (root, roles) => {
|
|
122
|
+
const roleGlob = roles.length === 1 ? roles[0] : `{${roles.join(',')}}`;
|
|
123
|
+
return `${root}**/*.${roleGlob}.ts`;
|
|
124
|
+
};
|
|
92
125
|
|
|
93
126
|
/** The jest coverage options of the rendered be/jest.config.js: { roots, roles, excludes }. */
|
|
94
127
|
export function jestCoverage(manifest, providers = []) {
|
|
@@ -107,13 +140,17 @@ export const codecovPaths = (manifest, providers = []) => coverageScope(manifest
|
|
|
107
140
|
* (`src/modules/domain/*\/` and `src/modules/domain/order/order.service.ts` give `order`).
|
|
108
141
|
*/
|
|
109
142
|
function capabilityOf(root, file) {
|
|
110
|
-
const parts = segments(root);
|
|
143
|
+
const parts = segments(root, true);
|
|
111
144
|
const at = parts.length - 1;
|
|
112
145
|
if (parts[at] !== '*') return null;
|
|
113
146
|
const own = file.split('/');
|
|
114
147
|
return parts.slice(0, at).every((part, index) => part === own[index]) && own.length > at + 1 ? own[at] : null;
|
|
115
148
|
}
|
|
116
149
|
|
|
150
|
+
function escapeImportPart(part) {
|
|
151
|
+
return part.replace(/[/.]/g, String.raw`\$&`);
|
|
152
|
+
}
|
|
153
|
+
|
|
117
154
|
/**
|
|
118
155
|
* The Codecov components of an app, [{ id, name, paths }] (paths repository-relative from the app root): one per service app (api or worker)
|
|
119
156
|
* that composes the capabilities nothing else composes, then `platform`. A capability of a root whose last segment is a placeholder
|
|
@@ -130,28 +167,37 @@ export function coverageComponents(manifest, { files, read, apps, providers = []
|
|
|
130
167
|
const imports = new Map(services.map((app) => [app.name, files.filter((file) => file.startsWith(`apps/${app.name}/`) && /\.[cm]?ts$/.test(file)).map((file) => read(file)).join('\n')]));
|
|
131
168
|
const platformPaths = [];
|
|
132
169
|
const ownedBy = new Map(services.map((app) => [app.name, []]));
|
|
133
|
-
for (const root of roots) {
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
for (const capability of capabilities) {
|
|
142
|
-
const spec = new RegExp(`from\\s+["']@modules/${module.replace(/[/.]/g, '\\$&')}/${capability.replace(/[/.]/g, '\\$&')}["']`);
|
|
143
|
-
const importers = services.filter((app) => spec.test(imports.get(app.name)));
|
|
144
|
-
const owners = importers.length === 1 ? importers : importers.filter((app) => app.name === capability);
|
|
145
|
-
if (owners.length === 1) ownedBy.get(owners[0].name).push(`be/${base}${capability}/**`);
|
|
146
|
-
else platformPaths.push(`be/${base}${capability}/**`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
170
|
+
for (const root of roots) assignRoot({ root, files, services, imports, platformPaths, ownedBy });
|
|
171
|
+
const components = services.filter((app) => ownedBy.get(app.name).length).map((app) => {
|
|
172
|
+
const paths = ownedBy.get(app.name);
|
|
173
|
+
paths.sort(byCodeUnit);
|
|
174
|
+
return { id: app.name, name: app.name, paths };
|
|
175
|
+
});
|
|
176
|
+
if (platformPaths.length) platformPaths.sort(byCodeUnit);
|
|
149
177
|
return [
|
|
150
|
-
...
|
|
151
|
-
...(platformPaths.length ? [{ id: PLATFORM_COMPONENT, name: PLATFORM_COMPONENT, paths: platformPaths
|
|
178
|
+
...components,
|
|
179
|
+
...(platformPaths.length ? [{ id: PLATFORM_COMPONENT, name: PLATFORM_COMPONENT, paths: platformPaths }] : []),
|
|
152
180
|
];
|
|
153
181
|
}
|
|
154
182
|
|
|
183
|
+
/** Puts the measured root, or each capability below it, into the platform paths or the paths of the one service app that composes it. */
|
|
184
|
+
function assignRoot({ root, files, services, imports, platformPaths, ownedBy }) {
|
|
185
|
+
const module = /^src\/modules\/(.+)\/\*\/$/.exec(root)?.[1] ?? null;
|
|
186
|
+
const capabilities = [...new Set(files.map((file) => capabilityOf(root, file)).filter(Boolean))].sort(byCodeUnit);
|
|
187
|
+
const base = root.slice(0, -2);
|
|
188
|
+
if (module === null || module === 'platform' || !capabilities.length) {
|
|
189
|
+
platformPaths.push(`be/${root}**`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const capability of capabilities) {
|
|
193
|
+
const spec = new RegExp(String.raw`from\s+["']@modules/${escapeImportPart(module)}/${escapeImportPart(capability)}["']`);
|
|
194
|
+
const importers = services.filter((app) => spec.test(imports.get(app.name)));
|
|
195
|
+
const owners = importers.length === 1 ? importers : importers.filter((app) => app.name === capability);
|
|
196
|
+
if (owners.length === 1) ownedBy.get(owners[0].name).push(`be/${base}${capability}/**`);
|
|
197
|
+
else platformPaths.push(`be/${base}${capability}/**`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
155
201
|
/** True when the be-relative `file` is measured: it is `<name>.<role>.ts` of a logic role below a measured root and inside no none directory of it. */
|
|
156
202
|
export function isMeasured(manifest, file) {
|
|
157
203
|
const { roots, roles, excludes } = coverageScope(manifest);
|
|
@@ -3,17 +3,88 @@
|
|
|
3
3
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
4
4
|
import { APP_KIND, connectionShapeProblems, NAME, RUNTIME_KIND, SLOT_ID } from './manifest-shape.mjs';
|
|
5
5
|
import { kindShapeProblems, patternShapeProblems } from './declaration-slots.mjs';
|
|
6
|
+
import { byCodeUnit } from '../lib/list.mjs';
|
|
6
7
|
|
|
7
8
|
/** Shape problems of the optional hfs.json `supabase` block whose auth posture DB_CONFIG_POLICY checks. */
|
|
9
|
+
const isSupabaseStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string' && entry.trim());
|
|
10
|
+
|
|
11
|
+
function supabaseValueProblems(block) {
|
|
12
|
+
const bad = [];
|
|
13
|
+
if (block.enableSignup !== undefined && typeof block.enableSignup !== 'boolean') bad.push('supabase.enableSignup must be true or false');
|
|
14
|
+
if (block.jwtExpiry !== undefined && !(Number.isInteger(block.jwtExpiry) && block.jwtExpiry > 0 && block.jwtExpiry <= 3600)) bad.push('supabase.jwtExpiry must be an integer of seconds, 3600 at most');
|
|
15
|
+
if (block.siteUrl !== undefined && (typeof block.siteUrl !== 'string' || !block.siteUrl.trim())) bad.push('supabase.siteUrl must be a URL string');
|
|
16
|
+
for (const key of ['redirectUrls', 'forceRls']) if (block[key] !== undefined && !isSupabaseStringList(block[key])) bad.push(`supabase.${key} must be a list of strings`);
|
|
17
|
+
return bad;
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
function supabaseBlockProblems(block) {
|
|
9
21
|
if (block === undefined) return [];
|
|
10
22
|
if (!isPlainObject(block)) return ['supabase must be an object'];
|
|
11
23
|
const bad = [];
|
|
12
24
|
for (const key of Object.keys(block)) if (!['enableSignup', 'jwtExpiry', 'siteUrl', 'redirectUrls', 'forceRls'].includes(key)) bad.push(`supabase has unknown key ${key}`);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
bad.push(...supabaseValueProblems(block));
|
|
26
|
+
return bad;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function appShapeProblems(apps, at, side, names) {
|
|
30
|
+
const bad = [];
|
|
31
|
+
if (!Array.isArray(apps) || !apps.length) {
|
|
32
|
+
bad.push(`${at}.apps must list every ${side}/apps/<name> with its kind`);
|
|
33
|
+
return bad;
|
|
34
|
+
}
|
|
35
|
+
apps.forEach((app, index) => {
|
|
36
|
+
if (!isPlainObject(app) || !NAME.test(String(app.name)) || !NAME.test(String(app.kind)) || Object.keys(app).some((key) => key !== 'name' && key !== 'kind')) bad.push(`${at}.apps[${index}] must be {name, kind}`);
|
|
37
|
+
else if (names.has(app.name)) bad.push(`app ${app.name} is declared twice (${names.get(app.name)} and ${side})`);
|
|
38
|
+
else names.set(app.name, side);
|
|
39
|
+
});
|
|
40
|
+
return bad;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function connectionProblems(side, value) {
|
|
44
|
+
const bad = [];
|
|
45
|
+
if (side !== 'be') {
|
|
46
|
+
bad.push('connections belong to the be side');
|
|
47
|
+
return bad;
|
|
48
|
+
}
|
|
49
|
+
const list = Array.isArray(value.connections) ? value.connections : null;
|
|
50
|
+
const shape = connectionShapeProblems(list, value.apps);
|
|
51
|
+
if (shape.length) {
|
|
52
|
+
bad.push(...shape);
|
|
53
|
+
return bad;
|
|
54
|
+
}
|
|
55
|
+
if (new Set(list.map((connection) => connection.name)).size !== list.length) bad.push('connections names must be unique');
|
|
56
|
+
for (const a of list) for (const b of list) if (a !== b && `${b.envPrefix}_`.startsWith(`${a.envPrefix}_`)) bad.push(`connections ${a.name} and ${b.name} share env keys (${a.envPrefix}_ covers ${b.envPrefix}_)`);
|
|
57
|
+
return bad;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether a declared list is present but not a list of distinct entries that all satisfy `valid`. */
|
|
61
|
+
const isBadUniqueList = (list, valid) => list !== undefined && (!Array.isArray(list) || !list.every((entry) => valid(entry)) || new Set(list).size !== list.length);
|
|
62
|
+
|
|
63
|
+
function declarationSideProblems(side, value, names) {
|
|
64
|
+
const bad = [];
|
|
65
|
+
const at = `sides.${side}`;
|
|
66
|
+
if (!isPlainObject(value)) return [`${at} must be an object`];
|
|
67
|
+
for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads'].includes(key)) bad.push(`${at} has unknown key ${key}`);
|
|
68
|
+
bad.push(...appShapeProblems(value.apps, at, side, names));
|
|
69
|
+
if (isBadUniqueList(value.optionalSlots, (slot) => SLOT_ID.test(String(slot)))) bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
|
|
70
|
+
bad.push(...patternShapeProblems(value, at, NAME), ...kindShapeProblems(value, at, NAME));
|
|
71
|
+
if (isBadUniqueList(value.reads, (read) => typeof read === 'string' && read.length > 0)) bad.push(`${at}.reads must be a unique list of paths`);
|
|
72
|
+
if (value.connections !== undefined) bad.push(...connectionProblems(side, value));
|
|
73
|
+
return bad;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'supabase', 'sides', 'browser']);
|
|
77
|
+
const RUNTIME_DECLARATION_KEYS = new Set(['hfs', 'kind', 'project']);
|
|
78
|
+
|
|
79
|
+
function productDeclarationProblems(declaration, profiles) {
|
|
80
|
+
const bad = [];
|
|
81
|
+
for (const key of Object.keys(declaration)) if (!DECLARATION_KEYS.has(key)) bad.push(`unknown key ${key}`);
|
|
82
|
+
bad.push(...supabaseBlockProblems(declaration.supabase));
|
|
83
|
+
if (declaration.browser !== undefined && declaration.browser !== true) bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
|
|
84
|
+
if (declaration.kind !== APP_KIND) 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)`);
|
|
85
|
+
if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort(byCodeUnit).join() !== profiles.join()) { bad.push('sides must declare exactly be and fe'); return bad; }
|
|
86
|
+
const names = new Map();
|
|
87
|
+
for (const side of profiles) bad.push(...declarationSideProblems(side, declaration.sides[side], names));
|
|
17
88
|
return bad;
|
|
18
89
|
}
|
|
19
90
|
|
|
@@ -24,39 +95,9 @@ export function declarationShapeProblems(declaration, profiles) {
|
|
|
24
95
|
if (!(Number.isInteger(declaration.hfs) && declaration.hfs >= 1)) bad.push('hfs must be the pinned manifest major (an integer, 1 or more)');
|
|
25
96
|
if (!NAME.test(String(declaration.project))) bad.push('project must be a project name');
|
|
26
97
|
if (declaration.kind === RUNTIME_KIND) {
|
|
27
|
-
for (const key of Object.keys(declaration)) if (!
|
|
98
|
+
for (const key of Object.keys(declaration)) if (!RUNTIME_DECLARATION_KEYS.has(key)) bad.push(`unknown key ${key} (a runtime declaration is {hfs, kind, project})`);
|
|
28
99
|
return bad;
|
|
29
100
|
}
|
|
30
|
-
|
|
31
|
-
bad.push(...supabaseBlockProblems(declaration.supabase));
|
|
32
|
-
if (declaration.browser !== undefined && declaration.browser !== true) bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
|
|
33
|
-
if (declaration.kind !== APP_KIND) 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)`);
|
|
34
|
-
if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort().join() !== profiles.join()) { bad.push('sides must declare exactly be and fe'); return bad; }
|
|
35
|
-
const names = new Map();
|
|
36
|
-
for (const side of profiles) {
|
|
37
|
-
const value = declaration.sides[side];
|
|
38
|
-
const at = `sides.${side}`;
|
|
39
|
-
if (!isPlainObject(value)) { bad.push(`${at} must be an object`); continue; }
|
|
40
|
-
for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads'].includes(key)) bad.push(`${at} has unknown key ${key}`);
|
|
41
|
-
if (!Array.isArray(value.apps) || !value.apps.length) bad.push(`${at}.apps must list every ${side}/apps/<name> with its kind`);
|
|
42
|
-
else value.apps.forEach((app, index) => {
|
|
43
|
-
if (!isPlainObject(app) || !NAME.test(String(app.name)) || !NAME.test(String(app.kind)) || Object.keys(app).some((key) => key !== 'name' && key !== 'kind')) bad.push(`${at}.apps[${index}] must be {name, kind}`);
|
|
44
|
-
else if (names.has(app.name)) bad.push(`app ${app.name} is declared twice (${names.get(app.name)} and ${side})`);
|
|
45
|
-
else names.set(app.name, side);
|
|
46
|
-
});
|
|
47
|
-
if (value.optionalSlots !== undefined && (!Array.isArray(value.optionalSlots) || !value.optionalSlots.every((slot) => SLOT_ID.test(String(slot))) || new Set(value.optionalSlots).size !== value.optionalSlots.length)) bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
|
|
48
|
-
bad.push(...patternShapeProblems(value, at, NAME), ...kindShapeProblems(value, at, NAME));
|
|
49
|
-
if (value.reads !== undefined && (!Array.isArray(value.reads) || !value.reads.every((read) => typeof read === 'string' && read.length > 0) || new Set(value.reads).size !== value.reads.length)) bad.push(`${at}.reads must be a unique list of paths`);
|
|
50
|
-
if (value.connections !== undefined) {
|
|
51
|
-
if (side !== 'be') { bad.push('connections belong to the be side'); continue; }
|
|
52
|
-
const list = Array.isArray(value.connections) ? value.connections : null;
|
|
53
|
-
const shape = connectionShapeProblems(list, value.apps);
|
|
54
|
-
if (shape.length) bad.push(...shape);
|
|
55
|
-
else {
|
|
56
|
-
if (new Set(list.map((connection) => connection.name)).size !== list.length) bad.push('connections names must be unique');
|
|
57
|
-
for (const a of list) for (const b of list) if (a !== b && `${b.envPrefix}_`.startsWith(`${a.envPrefix}_`)) bad.push(`connections ${a.name} and ${b.name} share env keys (${a.envPrefix}_ covers ${b.envPrefix}_)`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
101
|
+
bad.push(...productDeclarationProblems(declaration, profiles));
|
|
61
102
|
return bad;
|
|
62
103
|
}
|
|
@@ -8,7 +8,7 @@ export function optionalSlotProblems(manifest, side, s) {
|
|
|
8
8
|
const bad = [];
|
|
9
9
|
for (const id of s.optionalSlots ?? []) {
|
|
10
10
|
const slot = manifest.slots.find((candidate) => candidate.id === id);
|
|
11
|
-
if (!slot
|
|
11
|
+
if (!slot?.profiles.includes(side)) bad.push(`sides.${side}.optionalSlots names ${id}, which is not a ${side} slot`);
|
|
12
12
|
else if (slot.presence !== 'opt-in') bad.push(`sides.${side}.optionalSlots names ${id}, which is ${slot.presence}, not opt-in`);
|
|
13
13
|
else if (slot.appKind !== undefined) bad.push(`sides.${side}.optionalSlots names ${id}; an app of kind ${slot.appKind} enables it`);
|
|
14
14
|
}
|
|
@@ -20,8 +20,10 @@ export function effectiveSlot(slot, profile, edition) {
|
|
|
20
20
|
if (edition !== 'lite') return slot;
|
|
21
21
|
const { lite, litePresence, ...base } = slot;
|
|
22
22
|
const presence = litePresenceOf(slot, profile);
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
let tracked = slot.tracked;
|
|
24
|
+
if (presence === 'forbidden') tracked = 'external';
|
|
25
|
+
else if (tracked === 'external') tracked = 'tracked';
|
|
26
|
+
return { ...base, ...lite, presence, tracked, tests: 'none' };
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** The template group starci app sync renders `slot` from under `edition`: `liteManagedBy` (else `managedBy`) in lite, `managedBy` otherwise; undefined when unmanaged. */
|
|
@@ -61,6 +63,6 @@ export function enforcerJudgedInEdition(rules, kind, id, edition = 'full') {
|
|
|
61
63
|
export function editionRuleParams(params, edition) {
|
|
62
64
|
const { lite, ...base } = params;
|
|
63
65
|
const merged = edition === 'lite' && lite ? { ...base, ...lite } : base;
|
|
64
|
-
const deepFreeze = (value) => { if (value && typeof value === 'object') Object.values(value).forEach(deepFreeze); return Object.freeze(value); };
|
|
66
|
+
const deepFreeze = (value) => { if (value && typeof value === 'object') { Object.values(value).forEach(deepFreeze); } return Object.freeze(value); };
|
|
65
67
|
return deepFreeze(structuredClone(merged));
|
|
66
68
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Linear-time text trimming and `<placeholder>` replacement for the HFS path and prose checks.
|
|
2
|
+
|
|
3
|
+
/** `text` without its trailing characters that are in `chars`. */
|
|
4
|
+
export function trimTrailingChars(text, chars) {
|
|
5
|
+
let last = text.length - 1;
|
|
6
|
+
while (last >= 0 && chars.includes(text[last])) last -= 1;
|
|
7
|
+
return text.slice(0, last + 1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** `text` with every `<name>` placeholder (a `<`, one or more characters other than `>`, a `>`) replaced by `replacement`. */
|
|
11
|
+
export function replacePlaceholders(text, replacement) {
|
|
12
|
+
let out = '';
|
|
13
|
+
let from = 0;
|
|
14
|
+
let open = text.indexOf('<');
|
|
15
|
+
while (open >= 0) {
|
|
16
|
+
const close = text.indexOf('>', open + 1);
|
|
17
|
+
if (close < 0) break;
|
|
18
|
+
if (close === open + 1) {
|
|
19
|
+
open = text.indexOf('<', close);
|
|
20
|
+
} else {
|
|
21
|
+
out += text.slice(from, open) + replacement;
|
|
22
|
+
from = close + 1;
|
|
23
|
+
open = text.indexOf('<', from);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out + text.slice(from);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Whether `text` holds a `<name>` placeholder. */
|
|
30
|
+
export const hasPlaceholder = (text) => replacePlaceholders(text, '') !== text;
|