@zaaxch/tailframe 2.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,6 +37,7 @@ export interface RpcErrorEnvelope {
37
37
  export const uiHttpSource = `import axios, { type AxiosError } from "axios";
38
38
  import type { ServiceError } from "@/core/errors";
39
39
  import type { RpcErrorEnvelope, RpcResponse } from "@/core/rpc";
40
+ import { apiBaseUrl } from "@/platform/config";
40
41
 
41
42
  const failureKind = (status?: number): ServiceError["kind"] => {
42
43
  if (!status) return "network";
@@ -52,7 +53,7 @@ const failureKind = (status?: number): ServiceError["kind"] => {
52
53
  };
53
54
 
54
55
  export const http = axios.create({
55
- baseURL: \`\${window.location.origin}/api/v1\`,
56
+ baseURL: apiBaseUrl,
56
57
  headers: {
57
58
  "Content-Type": "application/json",
58
59
  Accept: "application/json",
@@ -98,6 +99,139 @@ export async function rpc<T>(operation: string, payload: unknown = {}): Promise<
98
99
  }
99
100
  `;
100
101
 
102
+ export const uiErrorMessagesSource = `import { isServiceError } from "@/core/errors";
103
+
104
+ export type ErrorMessageMap = Record<string, string>;
105
+
106
+ export function getErrorMessage(
107
+ error: unknown,
108
+ fallback = "Please try again.",
109
+ messages: ErrorMessageMap = {}
110
+ ): string {
111
+ if (isServiceError(error)) return messages[error.code] ?? fallback;
112
+ return fallback;
113
+ }
114
+
115
+ export const hasErrorCode = (error: unknown, code: string) => isServiceError(error) && error.code === code;
116
+ `;
117
+
118
+ export const notificationStoreSource = `import { defineStore } from "pinia";
119
+ import { computed, ref } from "vue";
120
+
121
+ export type NotificationKind = "info" | "success" | "warning" | "error";
122
+
123
+ export interface NotificationInput {
124
+ message: string;
125
+ kind?: NotificationKind;
126
+ durationMs?: number;
127
+ }
128
+
129
+ export interface AppNotification {
130
+ id: number;
131
+ message: string;
132
+ kind: NotificationKind;
133
+ durationMs: number;
134
+ }
135
+
136
+ export const useNotificationStore = defineStore("notification", () => {
137
+ const queue = ref<AppNotification[]>([]);
138
+ const active = computed<AppNotification | undefined>(() => queue.value[0]);
139
+ let nextId = 1;
140
+
141
+ function notify({ message, kind = "info", durationMs = 4000 }: NotificationInput) {
142
+ const id = nextId++;
143
+ queue.value.push({ id, message, kind, durationMs });
144
+ return id;
145
+ }
146
+
147
+ function dismiss(id: number) {
148
+ queue.value = queue.value.filter((notification) => notification.id !== id);
149
+ }
150
+
151
+ function clear() {
152
+ queue.value = [];
153
+ }
154
+
155
+ return { queue, active, notify, dismiss, clear };
156
+ });
157
+ `;
158
+
159
+ export const notificationHostSource = `<template>
160
+ <div class="tailframe-notifications" aria-live="polite" aria-atomic="true">
161
+ <div v-if="active" :class="['tailframe-notification', \`is-\${active.kind}\`]" role="status">
162
+ <span>{{ active.message }}</span>
163
+ <button type="button" aria-label="Dismiss notification" @click="notifications.dismiss(active.id)">×</button>
164
+ </div>
165
+ </div>
166
+ </template>
167
+
168
+ <script setup lang="ts">
169
+ import { computed, onBeforeUnmount, watch } from "vue";
170
+ import { useNotificationStore } from "@/app/stores/notification.store";
171
+
172
+ const notifications = useNotificationStore();
173
+ const active = computed(() => notifications.active);
174
+ let timer: number | undefined;
175
+
176
+ watch(
177
+ active,
178
+ (notification) => {
179
+ if (timer !== undefined) window.clearTimeout(timer);
180
+ timer = undefined;
181
+ if (notification && notification.durationMs > 0) {
182
+ timer = window.setTimeout(() => notifications.dismiss(notification.id), notification.durationMs);
183
+ }
184
+ },
185
+ { immediate: true }
186
+ );
187
+
188
+ onBeforeUnmount(() => {
189
+ if (timer !== undefined) window.clearTimeout(timer);
190
+ });
191
+ </script>
192
+
193
+ <style scoped>
194
+ .tailframe-notifications {
195
+ position: fixed;
196
+ inset: auto 1rem 1rem;
197
+ z-index: 10000;
198
+ display: grid;
199
+ justify-items: end;
200
+ pointer-events: none;
201
+ }
202
+ .tailframe-notification {
203
+ display: flex;
204
+ max-width: min(32rem, calc(100vw - 2rem));
205
+ align-items: center;
206
+ gap: 0.75rem;
207
+ border-radius: 0.75rem;
208
+ background: #1e293b;
209
+ color: white;
210
+ padding: 0.75rem 1rem;
211
+ box-shadow: 0 12px 30px rgb(15 23 42 / 0.28);
212
+ pointer-events: auto;
213
+ }
214
+ .tailframe-notification.is-success {
215
+ background: #166534;
216
+ }
217
+ .tailframe-notification.is-warning {
218
+ background: #92400e;
219
+ }
220
+ .tailframe-notification.is-error {
221
+ background: #991b1b;
222
+ }
223
+ .tailframe-notification button {
224
+ border: 0;
225
+ background: transparent;
226
+ color: inherit;
227
+ cursor: pointer;
228
+ font: inherit;
229
+ font-size: 1.25rem;
230
+ line-height: 1;
231
+ }
232
+ </style>
233
+ `;
234
+
101
235
  export const configureHttpSource = `import type { Pinia } from "pinia";
102
236
  import router from "@/app/router";
103
237
  import { useAuthStore } from "@/app/stores/auth.store";
package/src/validate.mjs CHANGED
@@ -1,8 +1,13 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import { validateArchitecture } from "./architecture.mjs";
4
+ import { appRoot, loadConfig } from "./config.mjs";
2
5
  import { validateConventions } from "./conventions.mjs";
3
6
  import { isExcepted, loadExceptions } from "./exceptions.mjs";
7
+ import { runSync } from "./sync.mjs";
4
8
 
5
9
  const NON_EXCEPTABLE_RULES = new Set(["S8", "S9", "U5", "U6", "U7"]);
10
+ const PNPM_VERSION = "11.22.0";
6
11
 
7
12
  export function runValidate(root, kind) {
8
13
  const structural = validateArchitecture(root, kind);
@@ -15,3 +20,88 @@ export function runValidate(root, kind) {
15
20
  .map((violation) => `${violation.rule}: ${violation.message}`);
16
21
  return [...structural, ...conventions];
17
22
  }
23
+
24
+ function escapeRegex(value) {
25
+ return value.replace(/[.*+?^$()|[\]\\{}]/g, "\\$&");
26
+ }
27
+
28
+ function validateVersionMetadata(root, runningVersion) {
29
+ const errors = [];
30
+ const packageFile = path.join(root, "package.json");
31
+ const lockFile = path.join(root, "pnpm-lock.yaml");
32
+ const workspaceFile = path.join(root, "pnpm-workspace.yaml");
33
+ if (!fs.existsSync(packageFile)) return ["root package.json is required to pin the Tailframe build-time contract"];
34
+ let manifest;
35
+ try { manifest = JSON.parse(fs.readFileSync(packageFile, "utf8")); }
36
+ catch { return ["root package.json is not valid JSON"]; }
37
+ if (manifest.private !== true) errors.push("root package.json must be private");
38
+ if (manifest.packageManager !== `pnpm@${PNPM_VERSION}`) {
39
+ errors.push(`root package.json packageManager must be pnpm@${PNPM_VERSION}`);
40
+ }
41
+ if (manifest.devDependencies?.["@zaaxch/tailframe"] !== runningVersion) {
42
+ errors.push(`root package.json must pin @zaaxch/tailframe exactly to ${runningVersion}`);
43
+ }
44
+ if (!fs.existsSync(workspaceFile)) errors.push("pnpm-workspace.yaml is required at the product root");
45
+ else {
46
+ const workspace = fs.readFileSync(workspaceFile, "utf8");
47
+ if (!/^\s*-\s*["']?apps\/\*["']?\s*$/mu.test(workspace)) {
48
+ errors.push("pnpm-workspace.yaml must include apps/*");
49
+ }
50
+ if (!/^injectWorkspacePackages:\s*true\s*$/mu.test(workspace)) {
51
+ errors.push("pnpm-workspace.yaml must enable injectWorkspacePackages");
52
+ }
53
+ }
54
+ if (!fs.existsSync(lockFile)) return [...errors, "pnpm-lock.yaml is required to lock the Tailframe contract"];
55
+ const lock = fs.readFileSync(lockFile, "utf8");
56
+ const version = escapeRegex(runningVersion);
57
+ const pin = new RegExp(`['"]?@zaaxch/tailframe['"]?:\\s*\\n\\s*specifier:\\s*['"]?${version}['"]?\\s*\\n\\s*version:\\s*['"]?${version}['"]?`, "u");
58
+ const resolution = new RegExp(`['"]?@zaaxch/tailframe@${version}['"]?:`, "u");
59
+ if (!pin.test(lock)) errors.push(`pnpm-lock.yaml root importer must pin @zaaxch/tailframe exactly to ${runningVersion}`);
60
+ if (!resolution.test(lock)) errors.push(`pnpm-lock.yaml does not resolve @zaaxch/tailframe ${runningVersion}`);
61
+ return errors;
62
+ }
63
+
64
+ function validateAppMetadata(productRoot, app) {
65
+ const errors = [];
66
+ const root = appRoot(productRoot, app);
67
+ if (!fs.existsSync(root)) return [`Missing configured application root: ${app.path}`];
68
+ if (fs.existsSync(path.join(root, "tailframe.json"))) {
69
+ errors.push(`${app.path}/tailframe.json is forbidden; the product root owns Tailframe metadata`);
70
+ }
71
+ if (fs.existsSync(path.join(root, "package-lock.json")) || fs.existsSync(path.join(root, "pnpm-lock.yaml"))) {
72
+ errors.push(`${app.path} must not contain an application lockfile`);
73
+ }
74
+ const packageFile = path.join(root, "package.json");
75
+ if (!fs.existsSync(packageFile)) errors.push(`${app.path}/package.json is required`);
76
+ else {
77
+ try {
78
+ const manifest = JSON.parse(fs.readFileSync(packageFile, "utf8"));
79
+ if (manifest.devDependencies?.["@zaaxch/tailframe"] || manifest.dependencies?.["@zaaxch/tailframe"]) {
80
+ errors.push(`${app.path}/package.json must not depend on @zaaxch/tailframe; pin it at the product root`);
81
+ }
82
+ } catch {
83
+ errors.push(`${app.path}/package.json is not valid JSON`);
84
+ }
85
+ }
86
+ return errors;
87
+ }
88
+ export function runConfiguredValidate(rootArgument, runningVersion, selectedKind) {
89
+ const loaded = loadConfig(rootArgument);
90
+ if (loaded.errors.length) return loaded.errors;
91
+ const errors = [];
92
+ if (loaded.config.contractVersion !== runningVersion) {
93
+ errors.push(`tailframe.json requires ${loaded.config.contractVersion}, but this CLI is ${runningVersion}`);
94
+ }
95
+ if (selectedKind && !loaded.config.apps.some((app) => app.kind === selectedKind)) {
96
+ errors.push(`tailframe.json does not declare a ${selectedKind} app`);
97
+ }
98
+ errors.push(...validateVersionMetadata(loaded.root, runningVersion));
99
+ for (const app of loaded.config.apps) errors.push(...validateAppMetadata(loaded.root, app));
100
+ if (errors.length) return errors;
101
+ const apps = selectedKind ? loaded.config.apps.filter((app) => app.kind === selectedKind) : loaded.config.apps;
102
+ for (const app of apps) {
103
+ for (const error of runValidate(appRoot(loaded.root, app), app.kind)) errors.push(`${app.path}: ${error}`);
104
+ }
105
+ errors.push(...runSync(loaded.root, "check", runningVersion, selectedKind).errors);
106
+ return errors;
107
+ }