@starci/hfs 4.2.13 → 4.2.15
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 +18 -0
- package/README.md +7 -3
- package/emit/db-types.mjs +2 -1
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +200 -8
- package/runtime/knowledge/hfs/fe-stack.yaml +221 -0
- package/runtime/knowledge/hfs/rules.yaml +50 -47
- package/runtime/knowledge/hfs/slots.yaml +307 -179
- package/runtime/knowledge/patterns/be/event-bus.yaml +15 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +10 -0
- package/runtime/knowledge/patterns/be/projections.yaml +10 -0
- package/runtime/knowledge/patterns/be/queues.yaml +15 -0
- package/runtime/knowledge/patterns/be/reactors.yaml +8 -0
- package/runtime/knowledge/patterns/be/realtime.yaml +17 -0
- package/runtime/knowledge/patterns/be/saga.yaml +13 -0
- package/runtime/knowledge/patterns/fe/folder.yaml +24 -18
- package/runtime/knowledge/sonar-gate.yaml +10 -2
- package/runtime/modules/kernel/failure-codes.yaml +40 -60
- package/runtime/modules/kernel/removed-vocabulary.yaml +43 -0
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +0 -2
- package/runtime/scripts/hfs/architecture/config.mjs +2 -1
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +6 -2
- package/runtime/scripts/hfs/architecture/error-masked.mjs +10 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +10 -3
- package/runtime/scripts/hfs/architecture/hfs.mjs +6 -3
- package/runtime/scripts/hfs/architecture/index.mjs +2 -0
- package/runtime/scripts/hfs/architecture/owners.mjs +19 -16
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +4 -1
- package/runtime/scripts/hfs/architecture/server-action.mjs +75 -20
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +2 -2
- package/runtime/scripts/hfs/architecture/supabase.mjs +36 -210
- package/runtime/scripts/hfs/architecture/tiers.mjs +1 -33
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +110 -54
- package/runtime/scripts/hfs/architecture/type-context.mjs +4 -1
- package/runtime/scripts/hfs/check.mjs +10 -6
- package/runtime/scripts/hfs/edition-slots.mjs +13 -4
- package/runtime/scripts/hfs/manifest-shape.mjs +3 -3
- package/runtime/scripts/hfs/rule-catalog.mjs +21 -1
- package/runtime/scripts/hfs/rules/database-constants.mjs +4 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +12 -7
- package/runtime/scripts/hfs/rules/database.mjs +4 -3
- package/runtime/scripts/hfs/rules/edition.mjs +49 -37
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +4 -5
- package/runtime/scripts/hfs/rules/fe-stack.mjs +71 -0
- package/scaffold/add-app.mjs +24 -18
- package/scaffold/add-cli-lite.mjs +33 -3
- package/scaffold/add-inbox-lite.mjs +24 -8
- package/scaffold/add-table.mjs +222 -74
- package/scaffold/add.mjs +85 -35
- package/scaffold/app.mjs +140 -36
- package/scaffold/edition-gate.mjs +21 -2
- package/scaffold/lite-exports.mjs +30 -1
- package/scaffold/service.mjs +5 -8
- package/src/main.mjs +2 -2
- package/sync/index.mjs +19 -15
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +14 -0
- package/templates/app/codegen-graphql.mjs.partial +34 -0
- package/templates/app/codegen-lite/scripts/codegen.mjs +18 -13
- package/templates/app/package-scripts/core.json +1 -0
- package/templates/app/package-scripts-lite/package.json +4 -0
- package/templates/app/skeleton/README.md +1 -1
- package/templates/app/skeleton/scripts/codegen.mjs +4 -1
- package/templates/app/skeleton-lite/README.md +7 -4
- package/templates/app/skeleton-lite/supabase/config.toml +1 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +13 -14
- package/templates/be/inbox/database-client.spec.ts.tpl +47 -0
- package/templates/be/inbox/service.spec.ts.tpl +46 -0
- package/templates/be/inbox/webhook-door.spec.ts.tpl +76 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +2 -2
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +6 -3
- package/templates/be/platform/errors-graphql/graphql-error.mapper.spec.ts +52 -0
- package/templates/be/platform/errors-graphql/graphql-error.mapper.ts +31 -0
- package/templates/be/platform/graphql/graphql-depth.policy.spec.ts +43 -0
- package/templates/be/platform/graphql/graphql-depth.policy.ts +21 -0
- package/templates/be/platform/graphql/graphql.contracts.ts +12 -0
- package/templates/be/platform/graphql/graphql.module-definition.ts +7 -0
- package/templates/be/platform/graphql/graphql.module.ts +49 -0
- package/templates/be/platform/graphql/graphql.options.ts +2 -0
- package/templates/be/platform/graphql/index.ts +1 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +114 -0
- package/templates/be/platform/graphql/localize-errors.mapper.ts +51 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +13 -1
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +11 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +21 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +94 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +2 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +2 -2
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +2 -6
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +15 -0
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +1 -1
- package/templates/be/table/api/create.command.ts.tpl +10 -0
- package/templates/be/table/api/create.contracts.ts.tpl +8 -0
- package/templates/be/table/api/create.handler.ts.tpl +22 -0
- package/templates/be/table/api/create.mapper.ts.tpl +5 -0
- package/templates/be/table/api/create.resolver.ts.tpl +21 -0
- package/templates/be/table/api/create.type.ts.tpl +9 -0
- package/templates/be/table/api/feature.index.ts.tpl +1 -0
- package/templates/be/table/api/feature.module.ts.tpl +7 -0
- package/templates/be/table/api/get.contracts.ts.tpl +11 -0
- package/templates/be/table/api/get.handler.ts.tpl +22 -0
- package/templates/be/table/api/get.input.ts.tpl +12 -0
- package/templates/be/table/api/get.mapper.ts.tpl +9 -0
- package/templates/be/table/api/get.query.ts.tpl +10 -0
- package/templates/be/table/api/get.resolver.ts.tpl +27 -0
- package/templates/be/table/api/get.type.ts.tpl +9 -0
- package/templates/be/table/api/graphql.module.ts.tpl +8 -0
- package/templates/be/table/error.api.ts.tpl +19 -0
- package/templates/be/table/migration.sql.tpl +13 -21
- package/templates/be/table/service.api.ts.tpl +30 -0
- package/templates/be/table/sql.api.ts.tpl +7 -0
- package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignInForm/index.tsx +16 -18
- package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignOutButton/component.tsx +7 -3
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignOutButton/index.tsx +23 -0
- package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +49 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +4 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +29 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +27 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/effects/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/effects/useAuthSessionEffect.ts +14 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/swr/SwrSideEffects.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/swr/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/api/graphql/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/api/graphql/types.ts +26 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/api/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/client.ts +21 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/contracts.ts +27 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/index.ts +2 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/api.ts +52 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +33 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +32 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/types/ambient/styles.d.ts +2 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/ReduxProvider.tsx +11 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +8 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +3 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +26 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +14 -0
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +5 -2
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/component.tsx +10 -2
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +1 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +23 -0
- package/templates/fe/skeleton/apps/landing/src/modules/types/ambient/styles.d.ts +2 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +51 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/providers.tsx +10 -13
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +8 -5
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -17
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +3 -10
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +0 -6
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +11 -8
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +0 -3
- package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +9 -25
- package/templates/fe/table/graphql/mutation.ts.tpl +25 -0
- package/templates/fe/table/graphql/mutation.types.ts.tpl +7 -0
- package/templates/fe/table/graphql/query.ts.tpl +25 -0
- package/templates/fe/table/graphql/query.types.ts.tpl +7 -0
- package/templates/fe/table/graphql/useMutateSwr.ts.tpl +21 -0
- package/templates/fe/table/graphql/useQuerySwr.ts.tpl +17 -0
- package/templates/fe/transport/graphql/clients/clients/auth.ts +39 -0
- package/templates/fe/transport/graphql/clients/clients/index.ts +2 -0
- package/templates/fe/transport/graphql/clients/clients/no-auth.ts +33 -0
- package/templates/fe/transport/graphql/clients/index.ts +2 -0
- package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +9 -0
- package/templates/fe/transport/graphql/clients/links/error.ts +30 -0
- package/templates/fe/transport/graphql/clients/links/http.ts +16 -0
- package/templates/fe/transport/graphql/clients/links/index.ts +8 -0
- package/templates/fe/transport/graphql/clients/links/locale.ts +13 -0
- package/templates/fe/transport/graphql/clients/links/options.ts +17 -0
- package/templates/fe/transport/graphql/clients/links/refresh-token.ts +36 -0
- package/templates/fe/transport/graphql/clients/links/retry.ts +16 -0
- package/templates/fe/transport/graphql/clients/links/timeout.ts +28 -0
- package/templates/fe/transport/graphql/clients/response.ts +43 -0
- package/upgrade/check.mjs +1 -1
- package/upgrade/index.mjs +3 -2
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +0 -182
- package/templates/app/quality-config-lite/sonar-project.properties +0 -5
- package/templates/be/patterns/api/feature.index.http.ts.tpl +0 -1
- package/templates/be/patterns/api/http.controller.ts.tpl +0 -27
- package/templates/be/patterns/api/http.mapper.ts.tpl +0 -9
- package/templates/be/patterns/api/http.module.ts.tpl +0 -7
- package/templates/be/patterns/api/http.request.ts.tpl +0 -10
- package/templates/be/patterns/api/http.response.ts.tpl +0 -5
- package/templates/fe/skeleton-lite/apps/web/src/app/auth/callback/route.ts +0 -24
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/index.tsx +0 -38
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/read-session.ts +0 -25
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-in.ts +0 -25
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +0 -17
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/browser.ts +0 -14
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/index.ts +0 -3
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/outcome.ts +0 -59
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/principal.ts +0 -29
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/server.ts +0 -70
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +0 -58
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/safe-next-path.ts +0 -26
- package/templates/fe/table/read.ts.tpl +0 -16
- package/templates/fe/table/write.ts.tpl +0 -16
- /package/templates/be/patterns/api/{application.command.http.ts.tpl → application.command.principal.ts.tpl} +0 -0
- /package/templates/be/patterns/api/{application.handler.http.ts.tpl → application.handler.principal.ts.tpl} +0 -0
- /package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignInForm/component.tsx +0 -0
package/scaffold/app.mjs
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// The selected edition owns the app root and its be/fe sides under knowledge/hfs/slots.yaml.
|
|
3
3
|
//
|
|
4
4
|
// The skeleton files are written once from templates/<app|be|fe>/skeleton ({{project}}, {{app}} and {{appPascal}} filled). Path
|
|
5
|
-
// variables: a `__app__` folder is written once per app of the side, named after it
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// variables: a `__app__` folder is written once per app of the side, named after it (on the fe side: once per product app, every
|
|
6
|
+
// declared Next app but the marketing landing, which calls no back end: the shared app core of both editions, modules/api, auth,
|
|
7
|
+
// toast, redux, the providers and the hooks); an fe `apps/<name>/` folder is the skeleton of the declared fe app <name> alone
|
|
8
|
+
// (landing and app differ); `__project__` in a path is the project name (packages/__project__-ui is packages/<name>-ui). The workspace manifests and every tsconfig.json are written from code, never kept as template files (a
|
|
8
9
|
// tsconfig.json under templates/ would be a TypeScript project of this repository); the managed files are the render of `starci app sync` (sync/index.mjs), so a fresh app
|
|
9
10
|
// is in sync by construction. The lockfile is never written by hand: once the files are written, npm resolves the real one
|
|
10
11
|
// (`npm install --package-lock-only`, no node_modules, no scripts), so `npm ci` installs the new app as it is. When npm cannot
|
|
@@ -33,9 +34,58 @@ const APP_DIR = '__app__';
|
|
|
33
34
|
const PROJECT_DIR = '__project__';
|
|
34
35
|
const TIMESTAMP = '__timestamp__';
|
|
35
36
|
const PINS_FILE = path.join(import.meta.dirname, '..', 'runtime', 'knowledge', 'hfs', 'canon-pins.yaml');
|
|
37
|
+
/**
|
|
38
|
+
* The front-end stack catalog (knowledge/hfs/fe-stack.yaml): the bundled runtime copy, else the runtime checkout's own file (the
|
|
39
|
+
* package inside the StarCi repository before the runtime copy carries it).
|
|
40
|
+
*/
|
|
41
|
+
const FE_STACK_FILES = [
|
|
42
|
+
path.join(import.meta.dirname, '..', 'runtime', 'knowledge', 'hfs', 'fe-stack.yaml'),
|
|
43
|
+
path.join(import.meta.dirname, '..', '..', '..', 'knowledge', 'hfs', 'fe-stack.yaml'),
|
|
44
|
+
];
|
|
36
45
|
const SONAR_GATE_FILE = path.join(import.meta.dirname, '..', 'runtime', 'knowledge', 'sonar-gate.yaml');
|
|
46
|
+
/** The runtime-peer catalog (knowledge/hfs/peer-integrations.yaml, R111) the package carries. */
|
|
47
|
+
const PEERS_FILE = path.join(import.meta.dirname, '..', 'runtime', 'knowledge', 'hfs', 'peer-integrations.yaml');
|
|
37
48
|
const pascal = name => name.split('-').map(part => part[0].toUpperCase() + part.slice(1)).join('');
|
|
38
|
-
|
|
49
|
+
/** The canon pins (knowledge/hfs/canon-pins.yaml) the package carries. */
|
|
50
|
+
export const bundledPins = () => parseYaml(fs.readFileSync(PINS_FILE, 'utf8')).pins;
|
|
51
|
+
|
|
52
|
+
/** The parsed front-end stack catalog; a package without it cannot know what every front end installs. */
|
|
53
|
+
function bundledFeStack() {
|
|
54
|
+
const file = FE_STACK_FILES.find(candidate => fs.existsSync(candidate));
|
|
55
|
+
if (!file) throw new ScaffoldError('HFS_SCAFFOLD_STACK_MISSING', 'knowledge/hfs/fe-stack.yaml is not carried by this package; regenerate the runtime copy');
|
|
56
|
+
return parseYaml(fs.readFileSync(file, 'utf8'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The marketing fe app of the full starter: it calls no back end, so the shared app core (`apps/__app__/`) is not written into
|
|
61
|
+
* it and the repository keeps one transport client folder (FE_TRANSPORT_OWNER).
|
|
62
|
+
*/
|
|
63
|
+
const MARKETING_FE_APPS = Object.freeze(new Set(['landing']));
|
|
64
|
+
/** The fe apps that carry the shared app core: every declared Next app but the marketing one. */
|
|
65
|
+
export const coreFeApps = apps => apps.filter(entry => entry.kind === 'next' && !MARKETING_FE_APPS.has(entry.name));
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The tool packages of the fe stack, installed once at the app root (devDependencies) and never in an fe workspace: the
|
|
69
|
+
* concerns whose packages only the root scripts run (the GraphQL codegen, lint and format), plus the compiler and the CSS engine.
|
|
70
|
+
*/
|
|
71
|
+
const FE_ROOT_TOOL_CONCERNS = Object.freeze(new Set(['codegen', 'lint']));
|
|
72
|
+
const FE_ROOT_TOOL_PACKAGES = Object.freeze(new Set(['typescript', 'tailwindcss']));
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The `tier: core` packages of the fe stack every scaffold installs, read from fe-stack.yaml (never listed here), split into
|
|
76
|
+
* `app` (the runtime imports of each product fe app workspace) and `root` (the tools at the app root). Both editions alike.
|
|
77
|
+
*/
|
|
78
|
+
export function coreFeStackPackages(stack = bundledFeStack()) {
|
|
79
|
+
const app = new Set();
|
|
80
|
+
const root = new Set();
|
|
81
|
+
for (const concern of stack.concerns ?? []) {
|
|
82
|
+
if (concern.tier !== 'core') continue;
|
|
83
|
+
for (const name of concern.packages ?? []) {
|
|
84
|
+
(FE_ROOT_TOOL_CONCERNS.has(concern.id) || FE_ROOT_TOOL_PACKAGES.has(name) ? root : app).add(name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { app: [...app].sort(byCodeUnit), root: [...root].sort(byCodeUnit) };
|
|
88
|
+
}
|
|
39
89
|
|
|
40
90
|
/**
|
|
41
91
|
* A JSON file as prettier prints it (the app's format check judges every file the scaffold writes): an object one key per line, an
|
|
@@ -87,7 +137,7 @@ const LITE_STARTER_SUPABASE = Object.freeze({
|
|
|
87
137
|
enableSignup: false,
|
|
88
138
|
jwtExpiry: 3600,
|
|
89
139
|
siteUrl: 'http://127.0.0.1:3000',
|
|
90
|
-
redirectUrls: ['http://127.0.0.1:3000/
|
|
140
|
+
redirectUrls: ['http://127.0.0.1:3000/'],
|
|
91
141
|
});
|
|
92
142
|
|
|
93
143
|
/**
|
|
@@ -97,8 +147,8 @@ const LITE_STARTER_SUPABASE = Object.freeze({
|
|
|
97
147
|
*/
|
|
98
148
|
const STARTER_DEPENDENCIES = Object.freeze({
|
|
99
149
|
dependencies: {
|
|
100
|
-
'@nestjs/common': null, '@nestjs/core': null, '@nestjs/cqrs': null,
|
|
101
|
-
'@nestjs/platform-express': null, '@nestjs/typeorm': null, 'nest-commander': null, pg: null,
|
|
150
|
+
'@apollo/server': null, '@nestjs/apollo': null, '@nestjs/common': null, '@nestjs/core': null, '@nestjs/cqrs': null, '@nestjs/graphql': null,
|
|
151
|
+
'@nestjs/platform-express': null, '@nestjs/typeorm': null, 'nest-commander': null, pg: null, graphql: null,
|
|
102
152
|
'class-transformer': null, 'class-validator': null, 'reflect-metadata': null, rxjs: null, tslib: null, typeorm: null,
|
|
103
153
|
},
|
|
104
154
|
devDependencies: {
|
|
@@ -110,37 +160,45 @@ const STARTER_DEPENDENCIES = Object.freeze({
|
|
|
110
160
|
},
|
|
111
161
|
});
|
|
112
162
|
|
|
113
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* Lite keeps the production stack, the canon tools and the back end's unit run (owner ruling 2026-10-12); it runs no test world of
|
|
165
|
+
* its own (no @starci/test-world). Both editions serve GraphQL from the api app (@nestjs/graphql over Apollo, owner ruling
|
|
166
|
+
* 2026-10-13); the fe stack's root tools are added from fe-stack.yaml by packageManifest.
|
|
167
|
+
*/
|
|
114
168
|
const LITE_STARTER_DEPENDENCIES = Object.freeze({
|
|
115
169
|
dependencies: {
|
|
116
|
-
'@nestjs/common': null, '@nestjs/core': null, '@nestjs/cqrs': null,
|
|
170
|
+
'@apollo/server': null, '@nestjs/apollo': null, '@nestjs/common': null, '@nestjs/core': null, '@nestjs/cqrs': null, '@nestjs/graphql': null,
|
|
117
171
|
'@nestjs/platform-express': null, '@nestjs/typeorm': null, '@supabase/supabase-js': null,
|
|
118
|
-
'class-transformer': null, 'class-validator': null, jose: null, pg: null,
|
|
172
|
+
'class-transformer': null, 'class-validator': null, graphql: null, jose: null, pg: null,
|
|
119
173
|
'reflect-metadata': null, rxjs: null, tslib: null, typeorm: null,
|
|
120
174
|
},
|
|
121
175
|
devDependencies: {
|
|
122
|
-
turbo: null, '@starci/eslint-canon-be': null, '@starci/eslint-canon-fe': null, '@starci/cli': null,
|
|
123
|
-
'@starci/prettier-config': null, '@starci/stylelint-canon': null, '@starci/tsconfig': null, '@tailwindcss/postcss': null,
|
|
124
|
-
'@types/express': null, '@types/node': null, '@types/react': null, '@types/react-dom': null,
|
|
125
|
-
eslint: null, 'eslint-plugin-react-hooks': null, husky: null, 'postcss-value-parser': null, prettier: null,
|
|
126
|
-
stylelint: null, supabase: null, tailwindcss: null, 'ts-node-dev': null, 'tsc-alias': null,
|
|
176
|
+
turbo: null, '@nestjs/testing': null, '@starci/eslint-canon-be': null, '@starci/eslint-canon-fe': null, '@starci/cli': null,
|
|
177
|
+
'@starci/jest-preset': null, '@starci/prettier-config': null, '@starci/stylelint-canon': null, '@starci/tsconfig': null, '@tailwindcss/postcss': null,
|
|
178
|
+
'@types/express': null, '@types/jest': null, '@types/node': null, '@types/react': null, '@types/react-dom': null,
|
|
179
|
+
eslint: null, 'eslint-plugin-react-hooks': null, husky: null, jest: null, 'postcss-value-parser': null, prettier: null,
|
|
180
|
+
stylelint: null, supabase: null, tailwindcss: null, 'ts-jest': null, 'ts-node-dev': null, 'tsc-alias': null,
|
|
127
181
|
'tsconfig-paths': null, typescript: null,
|
|
128
182
|
},
|
|
129
183
|
});
|
|
130
184
|
|
|
131
185
|
/**
|
|
132
|
-
* The runtime dependencies of every fe app workspace of the starter
|
|
133
|
-
*
|
|
134
|
-
*
|
|
186
|
+
* The runtime dependencies of every fe app workspace of the starter beyond the fe stack's core packages (fe-stack.yaml, added by
|
|
187
|
+
* feAppManifest for a product app): the two workspace packages at "*", the grammar and its required peers (@heroui/react,
|
|
188
|
+
* @xyflow/react: the grammar contract), next with next-intl, react and react-dom (next's peer). `@<project>/<pkg>` names are
|
|
189
|
+
* filled with the project. The marketing landing takes exactly these.
|
|
135
190
|
*/
|
|
136
191
|
const FE_APP_DEPENDENCIES = Object.freeze({
|
|
137
192
|
'@<project>/i18n': '*', '@<project>/ui': '*',
|
|
138
193
|
'@heroui/react': null, '@heroui/styles': null, '@starci/grammar': null, '@xyflow/react': null, next: null, 'next-intl': null, react: null, 'react-dom': null,
|
|
139
194
|
});
|
|
140
195
|
|
|
141
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* The lite fe workspace: the same base without the shared packages, plus its infra adapter (owner ruling 2026-10-13: Supabase on
|
|
198
|
+
* the front end is the Auth client of modules/auth only) and server-only for its own next-intl request config.
|
|
199
|
+
*/
|
|
142
200
|
const LITE_FE_APP_DEPENDENCIES = Object.freeze({
|
|
143
|
-
'@heroui/react': null, '@heroui/styles': null, '@starci/grammar': null, '@xyflow/react': null,
|
|
201
|
+
'@heroui/react': null, '@heroui/styles': null, '@starci/grammar': null, '@xyflow/react': null,
|
|
144
202
|
'@supabase/supabase-js': null, next: null, 'next-intl': null, react: null, 'react-dom': null, 'server-only': null,
|
|
145
203
|
});
|
|
146
204
|
|
|
@@ -178,18 +236,51 @@ function pinnedSection(entries, pins) {
|
|
|
178
236
|
return Object.fromEntries(Object.entries(entries).sort(([a], [b]) => a.localeCompare(b)).map(([dependency, range]) => [dependency, pinned(dependency, range)]));
|
|
179
237
|
}
|
|
180
238
|
|
|
181
|
-
/**
|
|
182
|
-
|
|
239
|
+
/**
|
|
240
|
+
* The runtime peers (R111, knowledge/hfs/peer-integrations.yaml) a root package.json needs and does not declare in its
|
|
241
|
+
* dependencies: the `requires` of every pair whose `when` packages it declares (at the named major, read from the declared
|
|
242
|
+
* exact pin), each at its canon pin. Both editions alike: @nestjs/apollo on @nestjs/platform-express 11 needs
|
|
243
|
+
* @as-integrations/express5.
|
|
244
|
+
*/
|
|
245
|
+
export function peerDependencies(pkg, pins, pairs = parseYaml(fs.readFileSync(PEERS_FILE, 'utf8')).pairs) {
|
|
246
|
+
const runtime = pkg.dependencies ?? {};
|
|
247
|
+
const declared = { ...pkg.devDependencies, ...runtime };
|
|
248
|
+
const major = version => Number(/^\D*(\d+)/.exec(String(version))?.[1]);
|
|
249
|
+
const needed = pairs.filter(pair => typeof runtime[pair.requires] !== 'string'
|
|
250
|
+
&& pair.when.every(entry => typeof declared[entry.package] === 'string' && (entry.major === undefined || major(declared[entry.package]) === entry.major)));
|
|
251
|
+
return pinnedSection(Object.fromEntries(needed.map(pair => [pair.requires, null])), pins);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The fe-stack core packages one version of which the whole install resolves (npm `overrides`, at their canon pins): a
|
|
256
|
+
* transitive dependency otherwise brings its own (zustand 4 under @xyflow/react, dayjs under typeorm, a hoisted newer zod
|
|
257
|
+
* that breaks the zodResolver types), and @xyflow/react itself stays the grammar's one version.
|
|
258
|
+
*/
|
|
259
|
+
const ROOT_OVERRIDES = Object.freeze(['@xyflow/react', 'dayjs', 'zod', 'zustand']);
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The root package.json of a new app, the monorepo root: name, workspaces, npm, its edition's dependencies with their runtime
|
|
263
|
+
* peers, the fe stack's root tools and the one-version overrides, at canon pins.
|
|
264
|
+
*/
|
|
265
|
+
function packageManifest(name, pins, edition, stack = coreFeStackPackages()) {
|
|
183
266
|
const dependencies = edition === 'lite' ? LITE_STARTER_DEPENDENCIES : STARTER_DEPENDENCIES;
|
|
184
|
-
|
|
267
|
+
const devDependencies = { ...dependencies.devDependencies, ...Object.fromEntries(stack.root.map(dependency => [dependency, null])) };
|
|
268
|
+
const pkg = { name, version: '0.0.0', private: true, packageManager: PACKAGE_MANAGER, workspaces: [...WORKSPACES], dependencies: pinnedSection(dependencies.dependencies, pins), devDependencies: pinnedSection(devDependencies, pins) };
|
|
269
|
+
pkg.dependencies = pinnedSection({ ...pkg.dependencies, ...peerDependencies(pkg, pins) }, pins);
|
|
270
|
+
pkg.overrides = pinnedSection(Object.fromEntries(ROOT_OVERRIDES.map(dependency => [dependency, null])), pins);
|
|
271
|
+
return pkg;
|
|
185
272
|
}
|
|
186
273
|
|
|
187
274
|
/** A dependency section of an fe workspace with `@<project>/` names filled and the canon pins taken. */
|
|
188
275
|
const workspaceSection = (entries, project, pins) => pinnedSection(Object.fromEntries(Object.entries(entries).map(([name, range]) => [name.replace('@<project>/', `@${project}/`), range])), pins);
|
|
189
276
|
|
|
190
|
-
/**
|
|
191
|
-
|
|
192
|
-
|
|
277
|
+
/**
|
|
278
|
+
* The package.json of the fe app workspace `app` of the app `project`: @<project>/<app>, private, the workspace scripts, its own
|
|
279
|
+
* dependencies. A product app (`core`, it carries the shared app core) also takes every core runtime package of the fe stack.
|
|
280
|
+
*/
|
|
281
|
+
function feAppManifest(project, app, pins, edition, { core = true, stack = coreFeStackPackages() } = {}) {
|
|
282
|
+
const base = edition === 'lite' ? LITE_FE_APP_DEPENDENCIES : FE_APP_DEPENDENCIES;
|
|
283
|
+
const dependencies = { ...base, ...(core ? Object.fromEntries(stack.app.filter(name => !Object.hasOwn(base, name)).map(name => [name, null])) : {}) };
|
|
193
284
|
return { name: feAppPackageName(project, app), version: '0.0.0', private: true, scripts: { ...FE_APP_SCRIPTS }, dependencies: workspaceSection(dependencies, project, pins) };
|
|
194
285
|
}
|
|
195
286
|
|
|
@@ -253,7 +344,7 @@ function fill(source, vars, file) {
|
|
|
253
344
|
}
|
|
254
345
|
|
|
255
346
|
/** Let sync's renderer expand partials first while leaving every source placeholder for `fill` to validate or preserve. */
|
|
256
|
-
const PARTIAL_VARIABLES = new Proxy(Object.create(null), {
|
|
347
|
+
export const PARTIAL_VARIABLES = new Proxy(Object.create(null), {
|
|
257
348
|
get: (_target, key) => `{{${String(key)}}}`,
|
|
258
349
|
getOwnPropertyDescriptor: (_target, key) => ({ configurable: true, value: `{{${String(key)}}}` }),
|
|
259
350
|
});
|
|
@@ -269,10 +360,7 @@ const LITE_DEFERRED_GENERATOR_FILES = new Set([
|
|
|
269
360
|
'src/modules/platform/database/migration-runner.service.ts',
|
|
270
361
|
'src/modules/platform/database/seed-runner.service.ts',
|
|
271
362
|
]);
|
|
272
|
-
const
|
|
273
|
-
if (scope === 'app') return LITE_APP_REUSED.has(rel);
|
|
274
|
-
if (scope === 'fe') return false;
|
|
275
|
-
return !rel.startsWith('apps/cli/')
|
|
363
|
+
const liteBaseSource = (rel) => !rel.startsWith('apps/cli/')
|
|
276
364
|
&& !rel.startsWith('src/features/cli/')
|
|
277
365
|
&& !rel.startsWith('src/modules/domain/note/')
|
|
278
366
|
&& rel !== 'src/modules/platform/database/connection-source.client.ts'
|
|
@@ -280,8 +368,23 @@ const liteBaseFile = (scope, rel) => {
|
|
|
280
368
|
&& rel !== 'src/modules/platform/database/seed-connections.client.ts'
|
|
281
369
|
&& rel !== 'src/modules/platform/primitives/sequence.policy.ts'
|
|
282
370
|
&& !LITE_DEFERRED_GENERATOR_FILES.has(rel)
|
|
283
|
-
&& !rel.startsWith('src/tests/')
|
|
284
|
-
|
|
371
|
+
&& !rel.startsWith('src/tests/');
|
|
372
|
+
/** The be files the lite overlay replaces: a base unit spec of such a file tests the full variant, so lite carries its own. */
|
|
373
|
+
const liteOverlaid = (() => {
|
|
374
|
+
let set = null;
|
|
375
|
+
return () => (set ??= new Set(listFiles(path.join(TEMPLATES_DIR, 'be', 'skeleton-lite'))));
|
|
376
|
+
})();
|
|
377
|
+
/**
|
|
378
|
+
* A base be file lite reuses: a source file of the shared shape, and the unit spec beside a reused source file that the lite overlay
|
|
379
|
+
* does not replace (owner ruling 2026-10-12: a lite back end keeps full's unit specs).
|
|
380
|
+
*/
|
|
381
|
+
const liteBaseFile = (scope, rel) => {
|
|
382
|
+
if (scope === 'app') return LITE_APP_REUSED.has(rel);
|
|
383
|
+
// The shared fe app core (apps/__app__/) is one tree for both editions; the lite overlay replaces only modules/auth/client.ts.
|
|
384
|
+
if (scope === 'fe') return rel.startsWith(`apps/${APP_DIR}/`);
|
|
385
|
+
if (!rel.endsWith('.spec.ts')) return liteBaseSource(rel);
|
|
386
|
+
const subject = rel.replace(/\.spec\.ts$/, '.ts');
|
|
387
|
+
return liteBaseSource(subject) && !liteOverlaid().has(subject);
|
|
285
388
|
};
|
|
286
389
|
const liteOverlayFile = (scope, rel) => scope !== 'be' || !LITE_DEFERRED_GENERATOR_FILES.has(rel);
|
|
287
390
|
|
|
@@ -290,6 +393,7 @@ function skeletonDirectory(scope, directory, app, vars, include = () => true) {
|
|
|
290
393
|
const dir = path.join(TEMPLATES_DIR, scope, directory);
|
|
291
394
|
if (!fs.existsSync(dir)) throw new ScaffoldError('HFS_SCAFFOLD_SKELETON_MISSING', `no skeleton templates in templates/${scope}/${directory}`);
|
|
292
395
|
const apps = scope === 'app' ? [] : app.sides[scope].apps.filter(entry => scope === 'fe' || entry.kind === 'api');
|
|
396
|
+
const appsOf = rel => (scope === 'fe' && rel.includes(APP_DIR) ? coreFeApps(apps) : apps);
|
|
293
397
|
const prefix = scope === 'app' ? '' : `${scope}/`;
|
|
294
398
|
const files = [];
|
|
295
399
|
for (const rel of listFiles(dir).filter(include)) {
|
|
@@ -304,7 +408,7 @@ function skeletonDirectory(scope, directory, app, vars, include = () => true) {
|
|
|
304
408
|
files.push({ path: `${prefix}${output(rel)}`, content: fill(source, { ...once, ...appVars }, `${directory}/${rel}`) });
|
|
305
409
|
continue;
|
|
306
410
|
}
|
|
307
|
-
for (const entry of
|
|
411
|
+
for (const entry of appsOf(rel)) files.push({ path: `${prefix}${output(rel.split(APP_DIR).join(entry.name))}`, content: fill(source, { ...once, app: entry.name, appPascal: pascal(entry.name) }, `${directory}/${rel}`) });
|
|
308
412
|
}
|
|
309
413
|
return files;
|
|
310
414
|
}
|
|
@@ -317,7 +421,7 @@ export function scaffoldWorkSeeds(project) {
|
|
|
317
421
|
|
|
318
422
|
/**
|
|
319
423
|
* The skeleton files of one scope (app root, be, fe): [{ path, content }], app-relative; a `__app__` file once per api app of the
|
|
320
|
-
* be side (once per app of the fe side), the be `apps/cli/` folder once when the cli app is declared, an fe `apps/<name>/` folder
|
|
424
|
+
* be side (once per product app of the fe side, coreFeApps), the be `apps/cli/` folder once when the cli app is declared, an fe `apps/<name>/` folder
|
|
321
425
|
* once for the declared fe app <name> (none other), `__project__` in a path replaced by the project name.
|
|
322
426
|
*/
|
|
323
427
|
function skeletonOf(scope, app, vars) {
|
|
@@ -465,7 +569,7 @@ export function scaffoldApp({ name, into, presets, edition = 'full', manifest =
|
|
|
465
569
|
path: `fe/apps/${entry.name}/src/modules/types/ambient/styles.d.ts`,
|
|
466
570
|
content: '/** Declares stylesheets loaded by the Next bundler; non-CSS side-effect imports remain checked. */\ndeclare module "*.css" {}\n',
|
|
467
571
|
})),
|
|
468
|
-
...app.sides.fe.apps.map(entry => ({ path: `fe/apps/${entry.name}/package.json`, content: packageJsonText(feAppManifest(name, entry.name, pins, app.edition)) })),
|
|
572
|
+
...app.sides.fe.apps.map(entry => ({ path: `fe/apps/${entry.name}/package.json`, content: packageJsonText(feAppManifest(name, entry.name, pins, app.edition, { core: coreFeApps([entry]).length === 1 })) })),
|
|
469
573
|
...imageFiles(app),
|
|
470
574
|
...(app.edition === 'lite' ? [] : Object.keys(FE_PACKAGES).flatMap(pkg => [
|
|
471
575
|
{ path: `fe/packages/${name}-${pkg}/package.json`, content: packageJsonText(fePackageManifest(name, pkg, pins)) },
|
|
@@ -7,12 +7,31 @@
|
|
|
7
7
|
|
|
8
8
|
/** The refusal of a verb a non-full edition does not carry: printed verbatim by the CLI, exit 2, nothing written. */
|
|
9
9
|
export class EditionRefusal extends Error {
|
|
10
|
-
constructor(command) {
|
|
11
|
-
super(
|
|
10
|
+
constructor(command, message = `${command}: full edition only; run starci app upgrade --edition full`) {
|
|
11
|
+
super(message);
|
|
12
12
|
this.name = 'EditionRefusal';
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The refusal of a pattern noun in a lite app whose topic carries a `liteAdapter` (owner ruling 2026-10-12): lite HAS the pattern,
|
|
18
|
+
* but the package's templates are the full adapter (BullMQ, Kafka, sockets, TypeORM entities, e2e proofs), so generating them would
|
|
19
|
+
* write what lite forbids. The pattern is written by hand on its Supabase adapter until the package carries a lite template.
|
|
20
|
+
*/
|
|
21
|
+
export class LiteAdapterRefusal extends EditionRefusal {
|
|
22
|
+
constructor(command, topics) {
|
|
23
|
+
super(command, `${command}: the package carries only the full adapter of ${topics.join(', ')}; in a lite app write it on its Supabase adapter (knowledge/patterns/be/${topics[0]}.yaml liteAdapter, knowledge/patterns/be/supabase-queues.yaml)`);
|
|
24
|
+
this.name = 'LiteAdapterRefusal';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Refuses `command` in a lite app when one of `topics` (pattern topic ids with their parsed documents) carries a `liteAdapter`. */
|
|
29
|
+
export function refuseFullAdapterInLite({ edition, topics, command }) {
|
|
30
|
+
if (edition !== 'lite') return;
|
|
31
|
+
const adapted = topics.filter(({ doc }) => doc?.liteAdapter !== undefined).map(({ id }) => id).sort();
|
|
32
|
+
if (adapted.length) throw new LiteAdapterRefusal(command, adapted);
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
/**
|
|
17
36
|
* Refuses `command` (`add <noun>`, `new <noun>`) when one of `slotIds` - the slots the verb writes into - does not
|
|
18
37
|
* exist for the edition of `resolver`'s repository or is forbidden under it. `resolver` is the
|
|
@@ -43,9 +43,10 @@ const REGISTRATIONS = Object.freeze({
|
|
|
43
43
|
beforeSource: "./errors/database.error",
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
|
+
// PRIMARY_ENTITY_MANAGER: the token the inbox service's unit spec provides its EntityManager double under.
|
|
46
47
|
file: "be/src/modules/platform/database/index.ts",
|
|
47
48
|
source: "./primary.decorators",
|
|
48
|
-
names: ["InjectPrimaryEntityManager"],
|
|
49
|
+
names: ["InjectPrimaryEntityManager", "PRIMARY_ENTITY_MANAGER"],
|
|
49
50
|
},
|
|
50
51
|
{
|
|
51
52
|
file: "be/src/modules/platform/http-security/index.ts",
|
|
@@ -90,6 +91,31 @@ const REGISTRATIONS = Object.freeze({
|
|
|
90
91
|
names: ["WebhookSignatureService"],
|
|
91
92
|
},
|
|
92
93
|
],
|
|
94
|
+
// The GraphQL server capability (be/src/modules/platform/graphql, written by the first GraphQL feature): the errors service and
|
|
95
|
+
// the request locale it formats and localizes errors with.
|
|
96
|
+
graphql: [
|
|
97
|
+
{
|
|
98
|
+
file: "be/src/modules/platform/errors/index.ts",
|
|
99
|
+
source: "./errors.decorators",
|
|
100
|
+
names: ["ERRORS_SERVICE"],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
file: "be/src/modules/platform/errors/index.ts",
|
|
104
|
+
source: "./graphql-error.mapper",
|
|
105
|
+
names: ["formatGraphqlError"],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
file: "be/src/modules/platform/errors/index.ts",
|
|
109
|
+
source: "./errors.service",
|
|
110
|
+
names: ["ErrorsService"],
|
|
111
|
+
type: true,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
file: "be/src/modules/platform/i18n/index.ts",
|
|
115
|
+
source: "./i18n.decorators",
|
|
116
|
+
names: ["REQUEST_LOCALE"],
|
|
117
|
+
},
|
|
118
|
+
],
|
|
93
119
|
cli: [
|
|
94
120
|
{
|
|
95
121
|
file: "be/src/modules/platform/database/index.ts",
|
|
@@ -138,6 +164,9 @@ function addExport(text, registration) {
|
|
|
138
164
|
return `${text.replace(/\n+$/, "")}\n${added}\n`
|
|
139
165
|
}
|
|
140
166
|
|
|
167
|
+
/** The app-relative barrels one generator's registrations edit (a caller snapshots them before registerLiteExports). */
|
|
168
|
+
export const registrationFiles = (generator) => [...new Set((REGISTRATIONS[generator] ?? []).map((registration) => registration.file))]
|
|
169
|
+
|
|
141
170
|
/** Adds only the barrel exports first consumed by one generator; full webhooks share the same optional platform API. */
|
|
142
171
|
export function registerLiteExports({ root, generator }) {
|
|
143
172
|
const registrations = REGISTRATIONS[generator]
|
package/scaffold/service.mjs
CHANGED
|
@@ -15,8 +15,7 @@ import fs from 'node:fs';
|
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import { createRequire } from 'node:module';
|
|
17
17
|
import { explainPath } from '../runtime/scripts/hfs/check.mjs';
|
|
18
|
-
import {
|
|
19
|
-
import { refuseInEdition } from './edition-gate.mjs';
|
|
18
|
+
import { loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
20
19
|
import { byCodeUnit } from '../report/order.mjs';
|
|
21
20
|
|
|
22
21
|
export class ScaffoldError extends Error {
|
|
@@ -320,9 +319,8 @@ const writeNew = (repoRoot, relative, text) => {
|
|
|
320
319
|
|
|
321
320
|
/** `starci app new spec be/<file>.service.ts` at the app root: writes the spec skeleton of an existing service; returns the created app-relative paths. */
|
|
322
321
|
export function newSpec({ repoRoot: appRoot, file, ts, manifest = loadSlotManifest() }) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
refuseInEdition({ resolver: createSlotResolver(manifest, repo), slotIds: ['be.tests.world'], command: 'new spec' });
|
|
322
|
+
// A unit spec belongs to both editions (owner ruling 2026-10-12): a lite back end keeps full's unit specs.
|
|
323
|
+
const { beRoot: repoRoot } = backEndOf(appRoot, manifest);
|
|
326
324
|
const relative = belowBackEnd(file);
|
|
327
325
|
if (!relative.endsWith('.service.ts')) throw new ScaffoldError('HFS_NEW_NOT_A_SERVICE', `${relative} is not a *.service.ts file: only services have a unit spec`);
|
|
328
326
|
const absolute = path.join(repoRoot, relative);
|
|
@@ -335,9 +333,8 @@ export function newSpec({ repoRoot: appRoot, file, ts, manifest = loadSlotManife
|
|
|
335
333
|
|
|
336
334
|
/** `starci app new service be/<dir> <name> [--inject ...]` at the app root: writes the service and its spec skeleton; returns the created app-relative paths. */
|
|
337
335
|
export function newService({ repoRoot: appRoot, dir, name, inject = [], ts, manifest = loadSlotManifest() }) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
refuseInEdition({ resolver: createSlotResolver(manifest, repo), slotIds: ['be.tests.world'], command: 'new service' });
|
|
336
|
+
// The service and its unit spec belong to both editions (owner ruling 2026-10-12).
|
|
337
|
+
const { beRoot: repoRoot } = backEndOf(appRoot, manifest);
|
|
341
338
|
if (!SERVICE_NAME.test(name)) throw new ScaffoldError('HFS_NEW_NAME_INVALID', `the service name ${name} must be kebab-case (member-profile), without the .service suffix`);
|
|
342
339
|
const relative = path.posix.join(belowBackEnd(dir), `${name}.service.ts`);
|
|
343
340
|
requireSlot(repoRoot, relative);
|
package/src/main.mjs
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
// a back-end `<name>.service.ts` and its `<name>.service.spec.ts` skeleton (scaffold/service.mjs; <dir> is
|
|
34
34
|
// relative to be/): the spec is built
|
|
35
35
|
// with Test.createTestingModule, one provider per constructor dependency (kit doubles from @starci/jest-preset),
|
|
36
|
-
// one placeholder it per public method. Never overwrites a file.
|
|
36
|
+
// one placeholder it per public method. Never overwrites a file. Both editions: a lite back end keeps full's unit specs (owner ruling 2026-10-12).
|
|
37
37
|
// starci app new image [--cwd <dir>] the Dockerfile of every declared app that has none, from the image canon (scaffold/image.mjs); never overwrites
|
|
38
38
|
// starci app new spec <file>.service.ts [--cwd <dir>] the spec skeleton of an existing service, read from its constructor with the repository's TypeScript
|
|
39
39
|
// starci app secret list | show <slug> | set <slug> | gen <slug> [--key NAME] [--env NAME] [--age RECIPIENT] [--bytes N] [--cwd DIR]
|
|
@@ -309,7 +309,7 @@ export async function main(argv, {
|
|
|
309
309
|
const [name, ...extra] = opts.positional;
|
|
310
310
|
if (!name || extra.length) throw new Error('starci app scaffold takes `<name> [--into <dir>] [--edition full|lite]`');
|
|
311
311
|
const edition = global.edition ?? 'full';
|
|
312
|
-
const { root: created, files } = scaffoldApp({ name, into: path.resolve(global.cwd, opts.into ?? '.'), edition, presets: presets ??
|
|
312
|
+
const { root: created, files } = scaffoldApp({ name, into: path.resolve(global.cwd, opts.into ?? '.'), edition, presets: presets ?? await scaffoldPresets(), ...(lock ? { lock } : {}) });
|
|
313
313
|
out(`starci app scaffold: created ${created} (${files.length} files); next: npm ci, then starci app lint\n`);
|
|
314
314
|
return 0;
|
|
315
315
|
}
|
package/sync/index.mjs
CHANGED
|
@@ -106,18 +106,20 @@ function readBundled(name) {
|
|
|
106
106
|
return lf(fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf8'));
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Fill `{{name}}` from vars and expand `{{> partial}}` lines. A partial line may be written as a line comment (`// {{> partial}}`)
|
|
111
|
+
* so a JavaScript template stays valid source before rendering. An unknown name is an error, never an empty string.
|
|
112
|
+
*/
|
|
110
113
|
export function render(text, vars, readTemplate = readBundled) {
|
|
111
|
-
const withPartials = text.replace(
|
|
114
|
+
const withPartials = text.replace(/^(?:\/\/ )?\{\{> ([\w./-]+)\}\}\n/gm, (_, name) => render(readTemplate(name), vars, readTemplate).replace(/\n*$/, '\n'));
|
|
112
115
|
return withPartials.replace(/\{\{([A-Za-z]\w*)\}\}/g, (_, key) => {
|
|
113
116
|
if (!Object.hasOwn(vars, key)) throw new SyncError('HFS_SYNC_TEMPLATE_VARIABLE', `template names {{${key}}}, which sync does not provide`);
|
|
114
117
|
return vars[key];
|
|
115
118
|
});
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
/** What the app's sync reads from the jest preset it installs for its be side: the Sonar exclusions ({ sonarExclusions }); a lite
|
|
119
|
-
export async function loadPresets(root
|
|
120
|
-
if (edition === 'lite') return {};
|
|
121
|
+
/** What the app's sync reads from the jest preset it installs for its be side: the Sonar exclusions ({ sonarExclusions }); both editions install it (a lite back end keeps full's unit run, owner ruling 2026-10-12). */
|
|
122
|
+
export async function loadPresets(root) {
|
|
121
123
|
const name = '@starci/jest-preset';
|
|
122
124
|
const require = createRequire(path.join(root, 'package.json'));
|
|
123
125
|
let resolved;
|
|
@@ -190,7 +192,8 @@ export function appScripts(app) {
|
|
|
190
192
|
const watch = entry => `cd be && ts-node-dev --respawn -r tsconfig-paths/register apps/${entry.name}/src/main.ts`;
|
|
191
193
|
const built = entry => `node be/dist/apps/${entry.name}/src/main.js`;
|
|
192
194
|
const workspace = entry => feAppPackageName(app.project, entry.name);
|
|
193
|
-
|
|
195
|
+
// Both editions generate before they run (owner ruling 2026-10-13): the fe's GraphQL wire types, and under lite the Supabase types check.
|
|
196
|
+
const dev = entry => `npm run codegen --silent && turbo run dev --filter=${workspace(entry)}`;
|
|
194
197
|
const images = imageEntries(app);
|
|
195
198
|
const imageCommand = ({ side, entry }) => `docker build -f ${dockerfilePath(side, entry.name)} -t ${app.project}/${entry.name}:dev .`;
|
|
196
199
|
return [
|
|
@@ -264,19 +267,20 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
264
267
|
appBinary: APP_BINARY,
|
|
265
268
|
appScripts: appScripts(app),
|
|
266
269
|
// turbo builds every fe app workspace; ^build (turbo.json) builds the packages each app imports first, and caches both.
|
|
267
|
-
buildFe:
|
|
270
|
+
buildFe: 'npm run codegen --silent && turbo run build --filter=./fe/apps/*',
|
|
268
271
|
// The be side with its tsc; every fe workspace with its own typecheck script through turbo (the packages are built first: ^build).
|
|
269
|
-
typecheck:
|
|
270
|
-
lintPrefix:
|
|
272
|
+
typecheck: 'npm run codegen --silent && tsc -p be/tsconfig.json && turbo run typecheck',
|
|
273
|
+
lintPrefix: 'npm run codegen --silent && ',
|
|
271
274
|
nodeMajor: String(NODE_MAJOR),
|
|
272
275
|
sonarKey: sonarKey ?? app.project,
|
|
273
|
-
sonarExclusions:
|
|
276
|
+
sonarExclusions: [presets?.sonarExclusions, '**/.next/**', '**/node_modules/**', '**/src/messages/**', ...(lite ? ['supabase/types/**'] : [])].filter(Boolean).join(','),
|
|
274
277
|
sonarSources: ['be/apps', 'be/src', 'fe/apps', ...(packages ? ['fe/packages'] : [])].join(','),
|
|
275
278
|
lcovReport: LCOV_REPORT,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
279
|
+
// The coverage scope is the same in both editions: lite measures exactly what full measures (owner ruling 2026-10-12).
|
|
280
|
+
jestCoverage: scope === 'be' ? jestCoverageSource(jestCoverage(manifest, providers)) : '',
|
|
281
|
+
coverageExclusions: scope === APP_SCOPE ? sonarCoverageExclusions(manifest, providers).join(',') : '',
|
|
282
|
+
codecovPaths: scope === APP_SCOPE ? codecovPaths(manifest, providers).map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
|
|
283
|
+
codecovComponents: scope === APP_SCOPE ? componentsYaml(coverageComponents(manifest, { ...source, apps: app.sides.be.apps, providers })) : '',
|
|
280
284
|
tsconfigPaths: ['be/tsconfig.json', ...fe.apps.map(entry => `fe/apps/${entry.name}/tsconfig.json`), ...(packages ? ['fe/packages/*/tsconfig.json'] : [])].join(','),
|
|
281
285
|
styleGlob: STYLE_GLOB,
|
|
282
286
|
imageMatrix: imageMatrix(app),
|
|
@@ -400,7 +404,7 @@ export function loadHfs(root, manifest = loadSlotManifest()) {
|
|
|
400
404
|
export async function renderRepo(root, { presets, parseYaml, manifest = loadSlotManifest() } = {}) {
|
|
401
405
|
const hfs = loadHfs(root, manifest);
|
|
402
406
|
const sonarKey = await readDeclaredSonarKey(root, { parseYaml, fail: message => { throw new SyncError('HFS_SYNC_SONAR_KEY', message); } });
|
|
403
|
-
return { hfs, targets: renderTargets(hfs, presets ?? await loadPresets(root
|
|
407
|
+
return { hfs, targets: renderTargets(hfs, presets ?? await loadPresets(root), { sonarKey, manifest, source: appSource(root) }) };
|
|
404
408
|
}
|
|
405
409
|
|
|
406
410
|
/** `starci app sync --check | --write [--cwd <dir>]` at the app root; returns 0 clean, 1 drift, 2 usage. */
|
|
@@ -17,6 +17,10 @@ concurrency:
|
|
|
17
17
|
jobs:
|
|
18
18
|
ci:
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
|
+
# id-token: write lets the Codecov upload authenticate with GitHub's OIDC token: no repository secret, no silent skip.
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
id-token: write
|
|
20
24
|
env:
|
|
21
25
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
22
26
|
steps:
|
|
@@ -36,6 +40,16 @@ jobs:
|
|
|
36
40
|
run: npm run format:check
|
|
37
41
|
- name: typecheck
|
|
38
42
|
run: npm run typecheck
|
|
43
|
+
- name: unit
|
|
44
|
+
run: npm test -- --ci
|
|
45
|
+
- name: coverage upload
|
|
46
|
+
uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5
|
|
47
|
+
if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/v') }}
|
|
48
|
+
with:
|
|
49
|
+
use_oidc: true
|
|
50
|
+
files: {{lcovReport}}
|
|
51
|
+
disable_search: true
|
|
52
|
+
fail_ci_if_error: true
|
|
39
53
|
- name: build be
|
|
40
54
|
run: npm run build:be
|
|
41
55
|
- name: build fe
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// GraphQL wire types, the same in both editions (owner ruling 2026-10-13): every fe app with operations under
|
|
2
|
+
// src/modules/api/graphql/{queries,mutations} gets its typed documents generated from the contract snapshots the back end emits
|
|
3
|
+
// (be/contracts/<app>/schema.graphql, `npm run contract:emit`) into the gitignored src/modules/api/graphql/__generated__/.
|
|
4
|
+
// Nothing to generate (no snapshot yet, or no fe app with a modules/api/graphql folder) is not an error.
|
|
5
|
+
const CONTRACTS = path.join("be", "contracts")
|
|
6
|
+
const FE_APPS = path.join("fe", "apps")
|
|
7
|
+
const posix = (file) => file.split(path.sep).join("/")
|
|
8
|
+
const schemas = (fs.existsSync(CONTRACTS) ? fs.readdirSync(CONTRACTS) : [])
|
|
9
|
+
.map((app) => path.join(CONTRACTS, app, "schema.graphql"))
|
|
10
|
+
.filter((file) => fs.existsSync(file))
|
|
11
|
+
.sort()
|
|
12
|
+
.map(posix)
|
|
13
|
+
const operationRoots = (fs.existsSync(FE_APPS) ? fs.readdirSync(FE_APPS) : [])
|
|
14
|
+
.map((app) => path.join(FE_APPS, app, "src", "modules", "api", "graphql"))
|
|
15
|
+
.filter((dir) => fs.existsSync(dir))
|
|
16
|
+
.sort()
|
|
17
|
+
.map(posix)
|
|
18
|
+
if (schemas.length === 0 || operationRoots.length === 0) {
|
|
19
|
+
process.stdout.write("codegen: no GraphQL contract under be/contracts yet\n")
|
|
20
|
+
} else {
|
|
21
|
+
const { generate } = await import("@graphql-codegen/cli")
|
|
22
|
+
const generates = Object.fromEntries(
|
|
23
|
+
operationRoots.map((root) => [
|
|
24
|
+
`${root}/__generated__/`,
|
|
25
|
+
{
|
|
26
|
+
preset: "client",
|
|
27
|
+
documents: [`${root}/queries/*.ts`, `${root}/mutations/*.ts`],
|
|
28
|
+
presetConfig: { fragmentMasking: false },
|
|
29
|
+
config: { useTypeImports: true, enumsAsTypes: true, avoidOptionals: true },
|
|
30
|
+
},
|
|
31
|
+
]),
|
|
32
|
+
)
|
|
33
|
+
await generate({ schema: schemas, generates, ignoreNoDocuments: true, silent: true }, true)
|
|
34
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Database types hook, offline: the app compiles against the committed supabase/types/database.types.ts, which `npm run contract:emit`
|
|
3
3
|
// regenerates from the local stack and stamps with the digest of the migrations it was generated from. This refuses a missing or
|
|
4
4
|
// stale file, so lint, typecheck, dev and build never run against types older than the migrations; it starts nothing and needs no stack.
|
|
5
|
+
// `--graphql` runs only the GraphQL wire types below (`npm run codegen:graphql`).
|
|
5
6
|
import { createHash } from "node:crypto"
|
|
6
7
|
import fs from "node:fs"
|
|
7
8
|
import path from "node:path"
|
|
@@ -15,18 +16,22 @@ const fail = (message) => {
|
|
|
15
16
|
process.exit(1)
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
if (!process.argv.includes("--graphql")) {
|
|
20
|
+
const hash = createHash("sha256")
|
|
21
|
+
for (const name of (fs.existsSync(MIGRATIONS) ? fs.readdirSync(MIGRATIONS) : [])
|
|
22
|
+
.filter((file) => file.endsWith(".sql"))
|
|
23
|
+
.sort()) {
|
|
24
|
+
hash.update(`${name}\0${fs.readFileSync(path.join(MIGRATIONS, name), "utf8").replace(/\r\n/g, "\n")}\0`)
|
|
25
|
+
}
|
|
26
|
+
const digest = hash.digest("hex")
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(TYPES))
|
|
29
|
+
fail(`${TYPES} is missing; start the local stack and run \`npm run contract:emit\`, then commit it`)
|
|
30
|
+
const stamped = STAMP.exec(fs.readFileSync(TYPES, "utf8").split(/\r?\n/, 1)[0])?.[1]
|
|
31
|
+
if (stamped !== digest)
|
|
32
|
+
fail(
|
|
33
|
+
`${TYPES} is ${stamped === undefined ? "not stamped" : "older than the migrations"}; run \`npm run contract:emit\` and commit the regenerated types`,
|
|
34
|
+
)
|
|
23
35
|
}
|
|
24
|
-
const digest = hash.digest("hex")
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
fail(`${TYPES} is missing; start the local stack and run \`npm run contract:emit\`, then commit it`)
|
|
28
|
-
const stamped = STAMP.exec(fs.readFileSync(TYPES, "utf8").split(/\r?\n/, 1)[0])?.[1]
|
|
29
|
-
if (stamped !== digest)
|
|
30
|
-
fail(
|
|
31
|
-
`${TYPES} is ${stamped === undefined ? "not stamped" : "older than the migrations"}; run \`npm run contract:emit\` and commit the regenerated types`,
|
|
32
|
-
)
|
|
37
|
+
// {{> app/codegen-graphql.mjs.partial}}
|