@zaaxch/tailframe 0.1.0 → 0.1.1

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/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@zaaxch/tailframe",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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": {
7
7
  "tailframe": "bin/tailframe.mjs"
8
8
  },
9
9
  "files": [
10
- "assets",
11
10
  "bin",
12
11
  "src"
13
12
  ],
package/src/new.mjs CHANGED
@@ -33,7 +33,9 @@ const svc = `${options.name}-svc`;
33
33
  const ui = `${options.name}-ui`;
34
34
  const files = {};
35
35
  const add = (relative, content) => { files[relative] = content.endsWith("\n") ? content : `${content}\n`; };
36
- const architectureValidatorSource = fs.readFileSync(new URL("../assets/validate-architecture.mjs", import.meta.url), "utf8");
36
+ // Generated repositories depend on the published toolkit rather than copying a validator, and they
37
+ // pin the exact contract version they were generated against.
38
+ const contractVersion = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
37
39
  const prettierConfig = {
38
40
  useTabs: true,
39
41
  tabWidth: 4,
@@ -158,6 +160,7 @@ const svcDeps = {
158
160
  };
159
161
  const svcDevDeps = {
160
162
  "@types/cors": "^2.8.17", "@types/express": "^5.0.1", "@types/jest": "^30.0.0", "@types/node": "^22.13.14", "@types/supertest": "^7.2.1",
163
+ "@zaaxch/tailframe": contractVersion,
161
164
  jest: "^30.0.0", nodemon: "^3.1.9", prettier: "3.5.3", "ts-jest": "^29.4.11", "ts-node": "^10.9.2",
162
165
  "tsc-alias": "^1.8.16", "tsconfig-paths": "^4.2.0", typescript: "^5.8.2", supertest: "^7.2.2"
163
166
  };
@@ -169,14 +172,13 @@ add(`${svc}/package.json`, JSON.stringify({
169
172
  ...(options.worker ? { "dev:worker": "nodemon --legacy-watch --exec ts-node -r tsconfig-paths/register src/worker.ts" } : {}),
170
173
  build: "tsc && tsc-alias", start: "node dist/server.js",
171
174
  ...(options.worker ? { worker: "node dist/worker.js" } : {}),
172
- test: "npm run test:unit && npm run test:integration", "test:unit": "jest --selectProjects unit --runInBand", "test:integration": "jest --selectProjects integration --runInBand", "validate:architecture": "node scripts/validate-architecture.mjs --kind service .", format: "prettier --write src/",
175
+ test: "npm run test:unit && npm run test:integration", "test:unit": "jest --selectProjects unit --runInBand", "test:integration": "jest --selectProjects integration --runInBand", "validate:architecture": "tailframe validate --kind service .", format: "prettier --write src/",
173
176
  "test:db:up": "docker compose -f docker-compose.test.yml up -d", "test:db:down": "docker compose -f docker-compose.test.yml down -v",
174
177
  "docker:dev": "docker compose -f docker-compose.dev.yml up",
175
178
  "docker:prod": "docker compose -f docker-compose.yml up -d",
176
179
  "docker:push": "bash scripts/build_and_push.sh"
177
180
  }, dependencies: svcDeps, devDependencies: svcDevDeps
178
181
  }, null, "\t"));
179
- add(`${svc}/scripts/validate-architecture.mjs`, architectureValidatorSource);
180
182
  add(`${svc}/tsconfig.json`, JSON.stringify({
181
183
  compilerOptions: { target: "ES2022", module: "commonjs", rootDir: "src", outDir: "dist", strict: true, esModuleInterop: true, experimentalDecorators: true, emitDecoratorMetadata: true, baseUrl: ".", paths: { "@/*": ["src/*"] }, skipLibCheck: true },
182
184
  include: ["src/**/*.ts"]
@@ -442,9 +444,8 @@ Run \`npm run validate:architecture\` after changing files or imports. The gener
442
444
 
443
445
  if (options.ui) {
444
446
  const uiDeps = { "@tailwindcss/vite": "^4.1.18", "@vueuse/core": "^14.3.0", axios: "^1.9.0", pinia: "^3.0.1", primevue: "^4.2.5", vue: "^3.5.13", "vue-router": "^4.5.0", ...(options.auth === "firebase" ? { firebase: "^11.0.0" } : {}) };
445
- const uiDevDeps = { "@tsconfig/node22": "^22.0.1", "@types/node": "^22.13.14", "@vitejs/plugin-vue": "^5.2.3", "@vue/test-utils": "^2.4.10", "@vue/tsconfig": "^0.7.0", jsdom: "^29.1.1", "npm-run-all2": "^7.0.2", prettier: "3.5.3", typescript: "~5.8.0", vite: "^6.2.4", vitest: "^4.1.7", "vue-tsc": "^2.2.8" };
446
- add(`${ui}/package.json`, JSON.stringify({ name: ui, version: "0.1.0", private: true, type: "module", scripts: { dev: "vite --host 0.0.0.0", build: "run-p type-check \"build-only {@}\" --", "build-only": "vite build", "type-check": "vue-tsc --build", test: "vitest", "validate:architecture": "node scripts/validate-architecture.mjs --kind ui .", format: "prettier --write src/" }, dependencies: uiDeps, devDependencies: uiDevDeps }, null, "\t"));
447
- add(`${ui}/scripts/validate-architecture.mjs`, architectureValidatorSource);
447
+ const uiDevDeps = { "@tsconfig/node22": "^22.0.1", "@zaaxch/tailframe": contractVersion, "@types/node": "^22.13.14", "@vitejs/plugin-vue": "^5.2.3", "@vue/test-utils": "^2.4.10", "@vue/tsconfig": "^0.7.0", jsdom: "^29.1.1", "npm-run-all2": "^7.0.2", prettier: "3.5.3", typescript: "~5.8.0", vite: "^6.2.4", vitest: "^4.1.7", "vue-tsc": "^2.2.8" };
448
+ add(`${ui}/package.json`, JSON.stringify({ name: ui, version: "0.1.0", private: true, type: "module", scripts: { dev: "vite --host 0.0.0.0", build: "run-p type-check \"build-only {@}\" --", "build-only": "vite build", "type-check": "vue-tsc --build", test: "vitest", "validate:architecture": "tailframe validate --kind ui .", format: "prettier --write src/" }, dependencies: uiDeps, devDependencies: uiDevDeps }, null, "\t"));
448
449
  add(`${ui}/.prettierrc.json`, JSON.stringify(prettierConfig, null, "\t"));
449
450
  add(`${ui}/tsconfig.json`, JSON.stringify({ files: [], references: [{ path: "./tsconfig.app.json" }, { path: "./tsconfig.node.json" }] }, null, "\t"));
450
451
  add(`${ui}/tsconfig.app.json`, JSON.stringify({ extends: "@vue/tsconfig/tsconfig.dom.json", include: ["env.d.ts", "src/**/*", "src/**/*.vue"], compilerOptions: { composite: true, baseUrl: ".", paths: { "@/*": ["./src/*"] } } }, null, "\t"));
@@ -1,279 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".vue"]);
7
-
8
- function toPosix(value) {
9
- return value.split(path.sep).join("/");
10
- }
11
-
12
- function listEntries(target, kind) {
13
- return fs.readdirSync(target, { withFileTypes: true })
14
- .filter((entry) => kind === "directory" ? entry.isDirectory() : entry.isFile())
15
- .map((entry) => entry.name)
16
- .sort();
17
- }
18
-
19
- function walk(target) {
20
- const results = [];
21
- for (const entry of fs.readdirSync(target, { withFileTypes: true })) {
22
- if (["node_modules", "dist", ".git"].includes(entry.name)) continue;
23
- const absolute = path.join(target, entry.name);
24
- if (entry.isDirectory()) results.push(...walk(absolute));
25
- else if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) results.push(absolute);
26
- }
27
- return results;
28
- }
29
-
30
- function extractImports(source, relative) {
31
- const code = relative.endsWith(".vue")
32
- ? [...source.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)].map((match) => match[1]).join("\n")
33
- : source;
34
- const script = code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
35
- const imports = [];
36
- const staticPattern = /(?:^|[;\n])\s*(?:import|export)\s+(?:type\s+)?(?:[^"'`;]*?\s+from\s+)?["']([^"']+)["']/g;
37
- const dynamicPattern = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
38
- for (const pattern of [staticPattern, dynamicPattern]) {
39
- for (const match of script.matchAll(pattern)) imports.push(match[1]);
40
- }
41
- return [...new Set(imports)];
42
- }
43
-
44
- function resolveInternal(relativeFile, specifier) {
45
- if (specifier.startsWith("@/")) return path.posix.normalize(`src/${specifier.slice(2)}`);
46
- if (!specifier.startsWith(".")) return undefined;
47
- return path.posix.normalize(path.posix.join(path.posix.dirname(relativeFile), specifier));
48
- }
49
-
50
- function isTestFile(relative) {
51
- return relative.includes("/__tests__/") || /\.(?:test|spec)\.[^.]+$/.test(relative);
52
- }
53
-
54
- function moduleParts(relative) {
55
- const match = relative.match(/^src\/modules\/([^/]+)\/([^/]+)(?:\/(.*))?$/);
56
- if (!match) return undefined;
57
- return { module: match[1], layer: match[2], remainder: match[3] ?? "" };
58
- }
59
-
60
- function isNamedUseCase(relative, expectedModule) {
61
- const target = moduleParts(relative);
62
- return Boolean(
63
- target &&
64
- target.module === expectedModule &&
65
- target.layer === "use-cases" &&
66
- target.remainder &&
67
- !target.remainder.includes("/") &&
68
- !isTestFile(relative)
69
- );
70
- }
71
-
72
- function validateStructure(root, kind, fail) {
73
- const src = path.join(root, "src");
74
- if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) {
75
- fail("Missing architecture directory: src");
76
- return;
77
- }
78
-
79
- const checkEntries = (relative, entryKind, allowed) => {
80
- const target = path.join(root, relative);
81
- if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
82
- fail(`Missing architecture directory: ${relative}`);
83
- return;
84
- }
85
- for (const name of listEntries(target, entryKind)) {
86
- if (!allowed.includes(name)) fail(`Unexpected architecture ${entryKind}: ${relative}/${name}`);
87
- }
88
- };
89
-
90
- if (kind === "service") {
91
- checkEntries("src", "directory", ["__tests__", "app", "core", "modules", "platform"]);
92
- checkEntries("src", "file", ["server.ts", "worker.ts"]);
93
- checkEntries("src/app", "directory", ["__tests__", "cli", "jobs", "mcp", "scheduled-tasks", "workers"]);
94
- checkEntries("src/core", "directory", []);
95
- } else {
96
- checkEntries("src", "directory", ["app", "core", "modules", "platform"]);
97
- checkEntries("src", "file", ["main.ts"]);
98
- checkEntries("src/app", "directory", ["__tests__", "components", "stores", "views"]);
99
- checkEntries("src/core", "directory", []);
100
- }
101
-
102
- const platform = path.join(root, "src/platform");
103
- if (!fs.existsSync(platform) || !fs.statSync(platform).isDirectory()) fail("Missing architecture directory: src/platform");
104
- const modules = path.join(root, "src/modules");
105
- if (!fs.existsSync(modules) || !fs.statSync(modules).isDirectory()) {
106
- fail("Missing architecture directory: src/modules");
107
- return;
108
- }
109
- const allowedModuleDirectories = kind === "service"
110
- ? ["__tests__", "domain", "http", "persistence", "use-cases"]
111
- : ["__tests__", "api", "components", "composables", "routes", "stores", "types", "views"];
112
- for (const moduleName of listEntries(modules, "directory")) {
113
- if (kind === "ui" && moduleName === "theme") fail("Application theme must live under src/app, not src/modules/theme");
114
- checkEntries(`src/modules/${moduleName}`, "directory", allowedModuleDirectories);
115
- checkEntries(`src/modules/${moduleName}`, "file", []);
116
- }
117
- }
118
-
119
- function validateServiceImport(source, target, external, fail, graph) {
120
- if (source === "src/server.ts" || source === "src/worker.ts") {
121
- if (external) {
122
- if (external !== "reflect-metadata") fail(`${source} may import only reflect-metadata and app bootstrap code, not ${external}`);
123
- } else {
124
- const expectedAssembly = source === "src/server.ts" ? "src/app/server" : "src/app/workers/";
125
- if (!target.startsWith(expectedAssembly)) fail(`${source} may import only its matching app assembly, not ${target}`);
126
- }
127
- return;
128
- }
129
- if (source.startsWith("src/core/")) {
130
- if (external || !target.startsWith("src/core/")) fail(`${source} violates core purity by importing ${external ?? target}`);
131
- return;
132
- }
133
- if (source.startsWith("src/app/") || source.startsWith("src/__tests__/")) return;
134
- if (source.startsWith("src/platform/")) {
135
- if (external) return;
136
- if (target.startsWith("src/core/") || target.startsWith("src/platform/")) return;
137
- if (source.startsWith("src/platform/integrations/") && /^src\/modules\/[^/]+\/use-cases\/ports\//.test(target)) return;
138
- fail(`${source} may import only core, platform, or a module-owned integration port, not ${target}`);
139
- return;
140
- }
141
-
142
- const sourceModule = moduleParts(source);
143
- if (!sourceModule) return;
144
- const moduleTest = isTestFile(source) || sourceModule.layer === "__tests__";
145
- if (external) {
146
- if (!moduleTest && ["domain", "use-cases"].includes(sourceModule.layer)) {
147
- fail(`${source} in ${sourceModule.layer} may not import framework or vendor package ${external}`);
148
- }
149
- return;
150
- }
151
- if (target.startsWith("src/core/")) return;
152
- const targetModule = moduleParts(target);
153
- if (target.startsWith("src/platform/")) {
154
- if (["http", "persistence"].includes(sourceModule.layer) || moduleTest) return;
155
- fail(`${source} cannot import platform code from ${sourceModule.layer}`);
156
- return;
157
- }
158
- if (target.startsWith("src/app/") || target.startsWith("src/__tests__/")) {
159
- if (moduleTest) return;
160
- fail(`${source} cannot import application assembly or cross-boundary test support`);
161
- return;
162
- }
163
- if (!targetModule) {
164
- fail(`${source} imports unsupported internal path ${target}`);
165
- return;
166
- }
167
- if (targetModule.module !== sourceModule.module) {
168
- if ((sourceModule.layer === "use-cases" || moduleTest) && isNamedUseCase(target, targetModule.module)) {
169
- if (!moduleTest) {
170
- if (!graph.has(sourceModule.module)) graph.set(sourceModule.module, new Set());
171
- graph.get(sourceModule.module).add(targetModule.module);
172
- }
173
- return;
174
- }
175
- fail(`${source} may import another module only through a named use-case file, not ${target}`);
176
- return;
177
- }
178
-
179
- if (moduleTest) return;
180
- const allowedLayers = {
181
- domain: ["domain"],
182
- "use-cases": ["domain", "use-cases"],
183
- http: ["domain", "http", "use-cases"],
184
- persistence: ["domain", "persistence", "use-cases"]
185
- }[sourceModule.layer] ?? [];
186
- if (!allowedLayers.includes(targetModule.layer)) {
187
- fail(`${source} in ${sourceModule.layer} cannot import its module's ${targetModule.layer} layer`);
188
- }
189
- }
190
-
191
- function validateUiImport(source, target, external, fail) {
192
- if (source === "src/main.ts") {
193
- if (external || !target.startsWith("src/app/")) fail(`src/main.ts may import only app bootstrap code, not ${external ?? target}`);
194
- return;
195
- }
196
- if (source.startsWith("src/core/")) {
197
- if (external || !target.startsWith("src/core/")) fail(`${source} violates core purity by importing ${external ?? target}`);
198
- return;
199
- }
200
- if (source.startsWith("src/app/")) return;
201
- if (source.startsWith("src/platform/")) {
202
- if (external || target.startsWith("src/core/") || target.startsWith("src/platform/")) return;
203
- fail(`${source} may not import app or module code: ${target}`);
204
- return;
205
- }
206
- const sourceModule = moduleParts(source);
207
- if (!sourceModule || external) return;
208
- if (target.startsWith("src/core/") || target.startsWith("src/platform/")) return;
209
- const targetModule = moduleParts(target);
210
- if (targetModule?.module === sourceModule.module) return;
211
- fail(`${source} may import only its own UI module, core, or platform, not ${target}`);
212
- }
213
-
214
- function detectCycles(graph, fail) {
215
- const visited = new Set();
216
- const active = [];
217
- const activeSet = new Set();
218
- const visit = (moduleName) => {
219
- if (activeSet.has(moduleName)) {
220
- const start = active.indexOf(moduleName);
221
- fail(`Cross-module use-case dependency cycle: ${[...active.slice(start), moduleName].join(" -> ")}`);
222
- return;
223
- }
224
- if (visited.has(moduleName)) return;
225
- visited.add(moduleName);
226
- active.push(moduleName);
227
- activeSet.add(moduleName);
228
- for (const dependency of graph.get(moduleName) ?? []) visit(dependency);
229
- active.pop();
230
- activeSet.delete(moduleName);
231
- };
232
- for (const moduleName of graph.keys()) visit(moduleName);
233
- }
234
-
235
- export function validateArchitecture(rootArgument, kind) {
236
- const root = path.resolve(rootArgument);
237
- const errors = [];
238
- const fail = (message) => errors.push(message);
239
- if (!["service", "ui"].includes(kind)) return [`Architecture kind must be service or ui, received ${kind ?? "nothing"}`];
240
- if (!fs.existsSync(root)) return [`Architecture root does not exist: ${root}`];
241
- validateStructure(root, kind, fail);
242
- const src = path.join(root, "src");
243
- if (!fs.existsSync(src)) return errors;
244
- const graph = new Map();
245
- for (const absolute of walk(src)) {
246
- const relative = toPosix(path.relative(root, absolute));
247
- for (const specifier of extractImports(fs.readFileSync(absolute, "utf8"), relative)) {
248
- const target = resolveInternal(relative, specifier);
249
- if (target && !target.startsWith("src/")) {
250
- fail(`${relative} has a relative import outside src: ${specifier}`);
251
- continue;
252
- }
253
- if (kind === "service") validateServiceImport(relative, target, target ? undefined : specifier, fail, graph);
254
- else validateUiImport(relative, target, target ? undefined : specifier, fail);
255
- }
256
- }
257
- if (kind === "service") detectCycles(graph, fail);
258
- return errors;
259
- }
260
-
261
- function parseCli(argv) {
262
- const args = [...argv];
263
- let kind;
264
- let root = ".";
265
- while (args.length) {
266
- const value = args.shift();
267
- if (value === "--kind") kind = args.shift();
268
- else root = value;
269
- }
270
- return { kind, root };
271
- }
272
-
273
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
274
- const { kind, root } = parseCli(process.argv.slice(2));
275
- const errors = validateArchitecture(root, kind);
276
- for (const error of errors) console.error(error);
277
- if (errors.length) process.exit(1);
278
- console.log(`Validated ${kind} architecture: ${path.resolve(root)}`);
279
- }