@zaaxch/tailframe 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/tailframe.mjs CHANGED
@@ -1,30 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
3
  import { createRequire } from "node:module";
4
- import { runValidate } from "../src/validate.mjs";
4
+ import { runConfiguredValidate } from "../src/validate.mjs";
5
5
  import { runGenerate, GenerateError } from "../src/generate.mjs";
6
6
  import { createProject } from "../src/new.mjs";
7
+ import { runSync } from "../src/sync.mjs";
7
8
 
8
9
  const require = createRequire(import.meta.url);
9
10
 
10
11
  function usage() {
11
- console.error("Usage: tailframe validate --kind service|ui [root]");
12
- console.error(" tailframe new <name> [--path <parent>] [--auth none|firebase] [--ui] [--redis] [--worker]");
12
+ console.error("Usage: tailframe validate [root]");
13
+ console.error(" tailframe sync --check|--write [root]");
14
+ console.error(" tailframe new <name> [--path <parent>] [--db mongo|postgres] [--auth none|firebase] [--ui] [--redis] [--worker]");
13
15
  console.error(" tailframe generate <schematic> <args...> [--options]");
14
16
  console.error(" service schematics: module <name> <VerbNoun> [--no-http], use-case <module> <VerbNoun>,");
15
17
  console.error(" http <module> <VerbNoun>, port <module> <Name>, adapter <module> <PortName> --db <technology>,");
16
18
  console.error(" identifiers <module> <NameId...>, integration <provider>");
17
19
  console.error(" ui schematics: module <name> --api|--view <Name>|--component <Name>|--store <name>|--composable <name>|--public <name>|--public-component <Name>,");
18
20
  console.error(" api <module>, view <module> <Name>, component <module> <Name>, store <module> <name>, composable <module> <name>,");
19
- console.error(" public <module> <name>, public-component <module> <Name>, app-store auth, app-store theme --storage-key <key>, app-component ThemeToggle");
21
+ console.error(" public <module> <name>, public-component <module> <Name>, app-store auth|notification, app-store theme --storage-key <key>, app-component ThemeToggle");
20
22
  process.exit(2);
21
23
  }
22
24
 
23
25
  const args = process.argv.slice(2);
24
26
  const command = args.shift();
27
+ const contractVersion = require("../package.json").version;
25
28
 
26
29
  if (command === "--version" || command === "-v") {
27
- console.log(require("../package.json").version);
30
+ console.log(contractVersion);
31
+ process.exit(0);
32
+ }
33
+ if (command === "sync") {
34
+ let mode;
35
+ let root = ".";
36
+ while (args.length) {
37
+ const value = args.shift();
38
+ if (value === "--check") mode = "check";
39
+ else if (value === "--write") mode = "write";
40
+ else if (value.startsWith("-")) usage();
41
+ else root = value;
42
+ }
43
+ if (!mode) usage();
44
+ const result = runSync(root, mode, contractVersion);
45
+ for (const error of result.errors) console.error(error);
46
+ for (const file of result.changed) console.log(`Updated ${file}`);
47
+ if (result.errors.length) process.exit(1);
48
+ if (mode === "check") console.log(`Canonical Tailframe sources match ${contractVersion}: ${path.resolve(root)}`);
28
49
  process.exit(0);
29
50
  }
30
51
  if (command === "new") {
@@ -33,7 +54,7 @@ if (command === "new") {
33
54
  const rest = [];
34
55
  for (let index = 0; index < args.length; index += 1) {
35
56
  const value = args[index];
36
- if (value === "--path" || value === "--auth") rest.push(value, args[++index]);
57
+ if (value === "--path" || value === "--auth" || value === "--db") rest.push(value, args[++index]);
37
58
  else if (value.startsWith("--")) rest.push(value);
38
59
  else if (name === undefined) name = value;
39
60
  else rest.push(value);
@@ -68,17 +89,14 @@ if (command === "generate") {
68
89
  }
69
90
  if (command !== "validate") usage();
70
91
 
71
- let kind;
72
92
  let root = ".";
73
93
  while (args.length) {
74
94
  const value = args.shift();
75
- if (value === "--kind") kind = args.shift();
76
- else if (value.startsWith("-")) usage();
95
+ if (value.startsWith("-")) usage();
77
96
  else root = value;
78
97
  }
79
- if (!["service", "ui"].includes(kind)) usage();
80
98
 
81
- const errors = runValidate(root, kind);
99
+ const errors = runConfiguredValidate(root, contractVersion);
82
100
  for (const error of errors) console.error(error);
83
101
  if (errors.length) process.exit(1);
84
- console.log(`Validated ${kind} contract ${require("../package.json").version}: ${path.resolve(root)}`);
102
+ console.log(`Validated Tailframe contract ${contractVersion}: ${path.resolve(root)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaaxch/tailframe",
3
- "version": "2.2.0",
3
+ "version": "3.0.0",
4
4
  "description": "Tailframe architecture toolkit: validates the Tailframe structure, import-boundary, and file-convention contracts. The package version is the contract version.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,9 +4,13 @@ import path from "node:path";
4
4
 
5
5
 
6
6
  const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".vue"]);
7
- const APP_STORE_FILES = ["auth.store.ts", "theme.store.ts"];
7
+ const APP_STORE_FILES = ["auth.store.ts", "notification.store.ts", "theme.store.ts"];
8
8
  const APP_PUBLIC_FILES = ["ThemeToggle.vue"];
9
- const APP_STORE_TARGETS = new Set(["src/app/stores/auth.store", "src/app/stores/theme.store"]);
9
+ const APP_STORE_TARGETS = new Set([
10
+ "src/app/stores/auth.store",
11
+ "src/app/stores/notification.store",
12
+ "src/app/stores/theme.store"
13
+ ]);
10
14
  const APP_PUBLIC_TARGETS = new Set(["src/app/public/ThemeToggle"]);
11
15
 
12
16
  function withoutSourceExtension(target) {
@@ -118,7 +122,7 @@ function validateStructure(root, kind, fail) {
118
122
  checkEntries("src/core", "directory", []);
119
123
  } else {
120
124
  checkEntries("src", "directory", ["app", "core", "modules", "platform"]);
121
- checkEntries("src", "file", ["main.ts"]);
125
+ checkEntries("src", "file", kind === "extension" ? ["background.ts", "content.ts"] : ["main.ts"]);
122
126
  checkEntries("src/app", "directory", ["__tests__", "components", "public", "stores", "views"]);
123
127
  checkEntries("src/core", "directory", []);
124
128
  checkOptionalEntries("src/app/public", "directory", []);
@@ -138,8 +142,8 @@ function validateStructure(root, kind, fail) {
138
142
  ? ["__tests__", "domain", "http", "persistence", "use-cases"]
139
143
  : ["__tests__", "api", "components", "composables", "public", "routes", "stores", "types", "views"];
140
144
  for (const moduleName of listEntries(modules, "directory")) {
141
- if (kind === "ui" && ["auth", "theme"].includes(moduleName)) {
142
- fail(`Application ${moduleName === "auth" ? "authentication" : "theme"} must live under src/app, not src/modules/${moduleName}`);
145
+ if (kind !== "service" && ["auth", "notification", "theme"].includes(moduleName)) {
146
+ fail(`Application ${moduleName} capability must live under src/app, not src/modules/${moduleName}`);
143
147
  }
144
148
  checkEntries(`src/modules/${moduleName}`, "directory", allowedModuleDirectories);
145
149
  checkEntries(`src/modules/${moduleName}`, "file", []);
@@ -223,9 +227,10 @@ function validateServiceImport(source, target, external, fail, graph) {
223
227
  }
224
228
  }
225
229
 
226
- function validateUiImport(source, target, external, fail, graph) {
227
- if (source === "src/main.ts") {
228
- if (external || !target.startsWith("src/app/")) fail(`src/main.ts may import only app bootstrap code, not ${external ?? target}`);
230
+ function validateUiImport(source, target, external, fail, graph, kind) {
231
+ const bootstraps = kind === "extension" ? ["src/background.ts", "src/content.ts"] : ["src/main.ts"];
232
+ if (bootstraps.includes(source)) {
233
+ if (external || !target.startsWith("src/app/")) fail(`${source} may import only app bootstrap code, not ${external ?? target}`);
229
234
  return;
230
235
  }
231
236
  if (source.startsWith("src/core/")) {
@@ -294,7 +299,7 @@ export function validateArchitecture(rootArgument, kind) {
294
299
  const root = path.resolve(rootArgument);
295
300
  const errors = [];
296
301
  const fail = (message) => errors.push(message);
297
- if (!["service", "ui"].includes(kind)) return [`Architecture kind must be service or ui, received ${kind ?? "nothing"}`];
302
+ if (!["service", "ui", "extension"].includes(kind)) return [`Architecture kind must be service, ui, or extension, received ${kind ?? "nothing"}`];
298
303
  if (!fs.existsSync(root)) return [`Architecture root does not exist: ${root}`];
299
304
  validateStructure(root, kind, fail);
300
305
  const src = path.join(root, "src");
@@ -313,7 +318,7 @@ export function validateArchitecture(rootArgument, kind) {
313
318
  continue;
314
319
  }
315
320
  if (kind === "service") validateServiceImport(relative, target, target ? undefined : specifier, fail, graph);
316
- else validateUiImport(relative, target, target ? undefined : specifier, fail, graph);
321
+ else validateUiImport(relative, target, target ? undefined : specifier, fail, graph, kind);
317
322
  }
318
323
  }
319
324
  detectCycles(graph, fail, kind === "service" ? "Cross-module use-case dependency cycle" : "Cross-module UI public dependency cycle");
package/src/config.mjs ADDED
@@ -0,0 +1,59 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export const CONFIG_FILE = "tailframe.json";
5
+ export const CONFIG_SCHEMA_VERSION = 1;
6
+ export const KINDS = new Set(["service", "ui", "extension", "flutter"]);
7
+ export const PROFILES = new Set([
8
+ "firebase",
9
+ "mongo",
10
+ "postgres",
11
+ "redis",
12
+ "worker",
13
+ "rate-limit",
14
+ "ui-host",
15
+ "notifications"
16
+ ]);
17
+
18
+ export function loadConfig(rootArgument) {
19
+ const root = path.resolve(rootArgument);
20
+ const file = path.join(root, CONFIG_FILE);
21
+ if (!fs.existsSync(file)) return { root, errors: [`Missing ${CONFIG_FILE}`] };
22
+ let config;
23
+ try {
24
+ config = JSON.parse(fs.readFileSync(file, "utf8"));
25
+ } catch (error) {
26
+ return { root, errors: [`Invalid ${CONFIG_FILE}: ${error instanceof Error ? error.message : error}`] };
27
+ }
28
+ const errors = [];
29
+ if (config.schemaVersion !== CONFIG_SCHEMA_VERSION) {
30
+ errors.push(`${CONFIG_FILE} schemaVersion must be ${CONFIG_SCHEMA_VERSION}`);
31
+ }
32
+ if (typeof config.contractVersion !== "string" || !/^\d+\.\d+\.\d+$/.test(config.contractVersion)) {
33
+ errors.push(`${CONFIG_FILE} contractVersion must be an exact semantic version`);
34
+ }
35
+ if (!KINDS.has(config.kind)) errors.push(`${CONFIG_FILE} kind must be service, ui, extension, or flutter`);
36
+ if (!Array.isArray(config.profiles) || config.profiles.some((profile) => !PROFILES.has(profile))) {
37
+ errors.push(`${CONFIG_FILE} profiles contain an unsupported value`);
38
+ } else if (new Set(config.profiles).size !== config.profiles.length) {
39
+ errors.push(`${CONFIG_FILE} profiles must not contain duplicates`);
40
+ }
41
+ const profiles = new Set(config.profiles ?? []);
42
+ if (config.kind === "service") {
43
+ if (profiles.has("mongo") === profiles.has("postgres")) {
44
+ errors.push("Service profiles must select exactly one of mongo or postgres");
45
+ }
46
+ if (profiles.has("worker") && !profiles.has("redis")) errors.push("The worker profile requires redis");
47
+ if (profiles.has("rate-limit") && !profiles.has("redis")) errors.push("The rate-limit profile requires redis");
48
+ if (profiles.has("notifications")) errors.push("The notifications profile is client-only");
49
+ } else {
50
+ for (const profile of ["mongo", "postgres", "redis", "worker", "rate-limit", "ui-host"]) {
51
+ if (profiles.has(profile)) errors.push(`${profile} is a service-only profile`);
52
+ }
53
+ }
54
+ return { root, config, errors };
55
+ }
56
+
57
+ export function configSource({ kind, profiles, contractVersion }) {
58
+ return `${JSON.stringify({ schemaVersion: CONFIG_SCHEMA_VERSION, contractVersion, kind, profiles }, null, "\t")}\n`;
59
+ }
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { toPosix, walk } from "./architecture.mjs";
4
4
  import { authAppStoreSource, themeAppStoreSource, themeToggleSource } from "./generate.mjs";
5
+ import { extensionAuthStoreSource } from "./ui-templates.mjs";
5
6
 
6
7
  const PASCAL = /^[A-Z][A-Za-z0-9]*$/;
7
8
  const CAMEL = /^[a-z][A-Za-z0-9]*$/;
@@ -114,8 +115,10 @@ export function validateConventions(rootArgument, kind) {
114
115
  }
115
116
  if (kind === "ui") {
116
117
  validateRouteNames(root, src, flag);
117
- validateCanonicalShell(root, flag);
118
- } else {
118
+ validateCanonicalShell(root, flag, kind);
119
+ } else if (kind === "extension") {
120
+ validateCanonicalShell(root, flag, kind);
121
+ } else if (kind === "service") {
119
122
  validateServiceComposition(root, flag);
120
123
  }
121
124
  return violations;
@@ -129,6 +132,12 @@ function validateServiceSource(relative, moduleName, layer, remainder, name, sou
129
132
  } else if (exportedClasses.length === 1 && exportedClasses[0] !== name.stem) {
130
133
  flag("G1", relative, `${relative} must be named ${exportedClasses[0]}.ts after its exported use-case class`);
131
134
  }
135
+ if (/^\s*(?:async\s+)?execute\s*\(/m.test(source) && !/^\s*(?:async\s+)?execute\s*\(\s*[_a-zA-Z][A-Za-z0-9]*\s*:\s*RequestContext\s*,\s*[_a-zA-Z][A-Za-z0-9]*\s*:/m.test(source)) {
136
+ flag("S1", relative, `${relative} execute must receive RequestContext first and an explicit input second; internal policies use a descriptive method instead`);
137
+ }
138
+ }
139
+ if (["domain", "use-cases"].includes(layer) && /\b_id\b/.test(source)) {
140
+ flag("S5", relative, `${relative} exposes MongoDB _id outside persistence; domain and use-case code use id`);
132
141
  }
133
142
 
134
143
  if (layer !== "http") return;
@@ -143,6 +152,11 @@ function validateServiceSource(relative, moduleName, layer, remainder, name, sou
143
152
  if (postCount > rpcHandlerCount) {
144
153
  flag("S8", relative, `${relative} must translate every RPC POST through rpcHandler; found ${postCount} POST routes and ${rpcHandlerCount} rpcHandler calls`);
145
154
  }
155
+ for (const match of source.matchAll(/\brouter\.post\s*\(\s*["']\/([^"']+)["']/g)) {
156
+ if (!match[1].startsWith(`${moduleName}.`)) {
157
+ flag("S8", relative, `${relative} exposes ${match[1]}; RPC operations use the owning module namespace ${moduleName}.*`);
158
+ }
159
+ }
146
160
  }
147
161
  if (name.role === "schemas" && name.subject === moduleName) {
148
162
  const schemas = [...source.matchAll(/^export\s+const\s+([A-Za-z][A-Za-z0-9]*)\s*=/gm)].map((match) => match[1]);
@@ -199,10 +213,11 @@ function normalizeGeneratedSource(source) {
199
213
  return source.replace(/\r\n/g, "\n");
200
214
  }
201
215
 
202
- function validateCanonicalShell(root, flag) {
216
+ function validateCanonicalShell(root, flag, kind = "ui") {
203
217
  const authRelative = "src/app/stores/auth.store.ts";
204
218
  const authFile = path.join(root, authRelative);
205
- if (fs.existsSync(authFile) && normalizeGeneratedSource(fs.readFileSync(authFile, "utf8")) !== authAppStoreSource()) {
219
+ const expectedAuth = kind === "extension" ? extensionAuthStoreSource : authAppStoreSource();
220
+ if (fs.existsSync(authFile) && normalizeGeneratedSource(fs.readFileSync(authFile, "utf8")) !== expectedAuth) {
206
221
  flag("U7", authRelative, `${authRelative} differs from the canonical Tailframe auth store; regenerate it instead of maintaining a project-local variant`);
207
222
  }
208
223
 
@@ -279,7 +294,9 @@ function validateShared(relative, name, flag) {
279
294
  return;
280
295
  }
281
296
  if (relative.startsWith("src/app/stores/") && name.extension === ".ts") {
282
- flag("U3", relative, `${relative} is not a canonical app store; app/stores is closed to auth.store.ts and theme.store.ts`);
297
+ if (!new Set(["auth.store", "notification.store", "theme.store"]).has(name.stem)) {
298
+ flag("U3", relative, `${relative} is not a canonical app store; app/stores is closed to auth.store.ts, notification.store.ts, and theme.store.ts`);
299
+ }
283
300
  return;
284
301
  }
285
302
  if (!PASCAL.test(name.stem) && !CAMEL.test(name.stem)) {
@@ -0,0 +1,111 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const ALLOWED_ROOTS = new Set(["app", "core", "modules", "platform"]);
5
+ const ALLOWED_APP_DIRECTORIES = new Set(["components", "public", "state", "theme", "views"]);
6
+ const ALLOWED_MODULE_DIRECTORIES = new Set(["api", "components", "public", "state", "types", "views"]);
7
+ const IMPORT_PATTERN = /(?:import|export)\s+["']([^"']+)["']/g;
8
+
9
+ const toPosix = (value) => value.split(path.sep).join("/");
10
+
11
+ function walk(target) {
12
+ const results = [];
13
+ for (const entry of fs.readdirSync(target, { withFileTypes: true })) {
14
+ if ([".dart_tool", "build", ".git"].includes(entry.name)) continue;
15
+ const absolute = path.join(target, entry.name);
16
+ if (entry.isDirectory()) results.push(...walk(absolute));
17
+ else if (entry.name.endsWith(".dart")) results.push(absolute);
18
+ }
19
+ return results;
20
+ }
21
+
22
+ function resolveImport(source, specifier, packageName) {
23
+ if (specifier.startsWith(`package:${packageName}/`)) return specifier.slice(`package:${packageName}/`.length);
24
+ if (specifier.startsWith(".")) return path.posix.normalize(path.posix.join(path.posix.dirname(source), specifier));
25
+ return undefined;
26
+ }
27
+
28
+ export function readFlutterPackageName(rootArgument) {
29
+ try {
30
+ return fs.readFileSync(path.join(path.resolve(rootArgument), "pubspec.yaml"), "utf8").match(/^name:\s*([a-zA-Z0-9_]+)/m)?.[1];
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
35
+
36
+ export function validateFlutter(rootArgument) {
37
+ const root = path.resolve(rootArgument);
38
+ const lib = path.join(root, "lib");
39
+ const errors = [];
40
+ if (!fs.existsSync(lib)) return ["Missing Flutter architecture directory: lib"];
41
+ const packageName = readFlutterPackageName(root);
42
+ if (!packageName) errors.push("pubspec.yaml must declare a package name");
43
+ for (const entry of fs.readdirSync(lib, { withFileTypes: true })) {
44
+ if (entry.isDirectory() && !ALLOWED_ROOTS.has(entry.name)) errors.push(`lib/${entry.name} is not a recognized architecture root`);
45
+ if (entry.isFile() && entry.name !== "main.dart") errors.push(`lib/${entry.name} is not the root bootstrap lib/main.dart`);
46
+ }
47
+ const files = walk(lib);
48
+ const relativeFiles = new Set(files.map((file) => toPosix(path.relative(lib, file))));
49
+ const graph = new Map();
50
+ for (const relative of relativeFiles) {
51
+ const parts = relative.split("/");
52
+ if (relative === "main.dart") {
53
+ // Checked after imports resolve below.
54
+ } else if (!ALLOWED_ROOTS.has(parts[0])) {
55
+ errors.push(`lib/${relative} is outside an architecture root`);
56
+ }
57
+ if (parts[0] === "core" && parts.length !== 2) errors.push(`lib/${relative} violates the flat core contract`);
58
+ if (parts[0] === "app" && parts.length > 2 && !ALLOWED_APP_DIRECTORIES.has(parts[1])) {
59
+ errors.push(`lib/${relative} uses an unsupported app directory`);
60
+ }
61
+ if (parts[0] === "modules") {
62
+ if (parts.length < 4 || !ALLOWED_MODULE_DIRECTORIES.has(parts[2])) errors.push(`lib/${relative} is not in a canonical module layer`);
63
+ if (parts[2] === "public" && (parts.length !== 4 || parts.at(-1) === "index.dart")) errors.push(`lib/${relative} is not a named direct module public entry`);
64
+ }
65
+ const source = fs.readFileSync(path.join(lib, relative), "utf8");
66
+ for (const match of source.matchAll(IMPORT_PATTERN)) {
67
+ const specifier = match[1];
68
+ const target = packageName ? resolveImport(relative, specifier, packageName) : undefined;
69
+ if (!target) {
70
+ if (parts[0] === "core" && specifier.startsWith("package:")) {
71
+ errors.push(`lib/${relative} violates core purity by importing ${specifier}`);
72
+ }
73
+ continue;
74
+ }
75
+ if (!relativeFiles.has(target)) {
76
+ errors.push(`lib/${relative} references missing lib/${target}`);
77
+ continue;
78
+ }
79
+ if (relative === "main.dart" && !target.startsWith("app/")) errors.push(`lib/main.dart may import only application bootstrap code, not lib/${target}`);
80
+ if (parts[0] === "core" && !target.startsWith("core/")) errors.push(`lib/${relative} crosses outward from core to lib/${target}`);
81
+ if (parts[0] === "platform" && (target.startsWith("app/") || target.startsWith("modules/"))) errors.push(`lib/${relative} crosses from platform to lib/${target}`);
82
+ if (parts[0] === "app" && target.startsWith("modules/")) {
83
+ const targetParts = target.split("/");
84
+ if (targetParts[2] !== "public" || targetParts.length !== 4) errors.push(`lib/${relative} imports private module file lib/${target}`);
85
+ }
86
+ if (parts[0] !== "modules") continue;
87
+ if (target.startsWith("app/") && !target.startsWith("app/public/")) errors.push(`lib/${relative} imports private app file lib/${target}`);
88
+ if (!target.startsWith("modules/")) continue;
89
+ const targetParts = target.split("/");
90
+ if (targetParts[1] === parts[1]) continue;
91
+ if (targetParts[2] !== "public" || targetParts.length !== 4) errors.push(`lib/${relative} imports private sibling file lib/${target}`);
92
+ else {
93
+ if (!graph.has(parts[1])) graph.set(parts[1], new Set());
94
+ graph.get(parts[1]).add(targetParts[1]);
95
+ }
96
+ }
97
+ }
98
+ const visiting = new Set();
99
+ const visited = new Set();
100
+ const visit = (module) => {
101
+ if (visiting.has(module)) return true;
102
+ if (visited.has(module)) return false;
103
+ visiting.add(module);
104
+ for (const dependency of graph.get(module) ?? []) if (visit(dependency)) return true;
105
+ visiting.delete(module);
106
+ visited.add(module);
107
+ return false;
108
+ };
109
+ for (const module of graph.keys()) if (visit(module)) errors.push(`Flutter module dependency graph contains a cycle involving ${module}`);
110
+ return [...new Set(errors)];
111
+ }
package/src/generate.mjs CHANGED
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { loadExceptions, isExcepted } from "./exceptions.mjs";
4
4
  import { serviceHttpFiles, serviceOperationName } from "./service-templates.mjs";
5
- import { moduleApiSource } from "./ui-templates.mjs";
5
+ import { moduleApiSource, notificationStoreSource } from "./ui-templates.mjs";
6
6
 
7
7
  const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
8
8
  const PASCAL = /^[A-Z][A-Za-z0-9]*$/;
@@ -470,7 +470,8 @@ function appStoreFiles(name, options) {
470
470
  return { "src/app/stores/theme.store.ts": themeAppStoreSource(storageKey) };
471
471
  }
472
472
  if (name === "auth") return { "src/app/stores/auth.store.ts": authAppStoreSource() };
473
- fail(`App store must be one of auth or theme, received "${name ?? ""}"`);
473
+ if (name === "notification") return { "src/app/stores/notification.store.ts": notificationStoreSource };
474
+ fail(`App store must be one of auth, notification, or theme, received "${name ?? ""}"`);
474
475
  }
475
476
 
476
477
  function appPublicComponentFiles(name) {