@mettlecast/domain-cli 0.2.2 → 0.2.3
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/dist/cli.js +9 -1
- package/dist/commands/add-page.js +39 -126
- package/dist/commands/build-ui/component-scanner.d.ts +12 -0
- package/dist/commands/build-ui/component-scanner.js +50 -0
- package/dist/commands/build-ui/page-indexer.d.ts +13 -0
- package/dist/commands/build-ui/page-indexer.js +107 -0
- package/dist/commands/build-ui.d.ts +3 -0
- package/dist/commands/build-ui.js +23 -0
- package/dist/templates/api-skeleton.js +1 -1
- package/dist/templates/events-skeleton.js +1 -1
- package/dist/templates/subscriber-skeleton.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +10 -1
- package/src/commands/add-page.ts +40 -156
- package/src/commands/build-ui/component-scanner.ts +70 -0
- package/src/commands/build-ui/page-indexer.ts +134 -0
- package/src/commands/build-ui.ts +29 -0
- package/src/templates/api-skeleton.ts +1 -1
- package/src/templates/events-skeleton.ts +1 -1
- package/src/templates/subscriber-skeleton.ts +1 -1
- package/dist/templates/dashboard-pages/index.d.ts +0 -101
- package/dist/templates/dashboard-pages/index.js +0 -22
- package/src/templates/dashboard-pages/account/api-keys.tsx +0 -107
- package/src/templates/dashboard-pages/account/audit-log.tsx +0 -60
- package/src/templates/dashboard-pages/account/index.ts +0 -5
- package/src/templates/dashboard-pages/account/members.tsx +0 -107
- package/src/templates/dashboard-pages/account/profile.tsx +0 -53
- package/src/templates/dashboard-pages/account/workspace-settings.tsx +0 -64
- package/src/templates/dashboard-pages/auth/accept-invitation.tsx +0 -62
- package/src/templates/dashboard-pages/auth/choose-org.tsx +0 -66
- package/src/templates/dashboard-pages/auth/forgot-password.tsx +0 -69
- package/src/templates/dashboard-pages/auth/index.ts +0 -10
- package/src/templates/dashboard-pages/auth/login.tsx +0 -75
- package/src/templates/dashboard-pages/auth/mfa-setup.tsx +0 -56
- package/src/templates/dashboard-pages/auth/mfa-verify.tsx +0 -49
- package/src/templates/dashboard-pages/auth/reset-password.tsx +0 -64
- package/src/templates/dashboard-pages/auth/sign-out.tsx +0 -20
- package/src/templates/dashboard-pages/auth/signup.tsx +0 -71
- package/src/templates/dashboard-pages/auth/verify-email.tsx +0 -53
- package/src/templates/dashboard-pages/index.ts +0 -22
- package/src/templates/dashboard-pages/shell/dashboard-home.tsx +0 -41
- package/src/templates/dashboard-pages/shell/forbidden.tsx +0 -46
- package/src/templates/dashboard-pages/shell/index.ts +0 -5
- package/src/templates/dashboard-pages/shell/maintenance.tsx +0 -27
- package/src/templates/dashboard-pages/shell/not-found.tsx +0 -34
- package/src/templates/dashboard-pages/shell/server-error.tsx +0 -44
package/src/commands/add-page.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join, resolve, dirname
|
|
1
|
+
import { writeFile, mkdir, access } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve, dirname } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { Readable } from 'node:stream';
|
|
5
|
-
import { Parser as TarParser } from 'tar';
|
|
6
4
|
import { cliLogger } from '../utils/logger.js';
|
|
7
|
-
import { readScaffoldConfig } from '../utils/scaffold-config.js';
|
|
8
|
-
import { fetchModulesJson, fetchModuleTarball } from '../utils/s3-fetch.js';
|
|
9
5
|
import {
|
|
10
6
|
type PagePattern,
|
|
11
7
|
PAGE_PATTERNS,
|
|
@@ -17,9 +13,6 @@ import {
|
|
|
17
13
|
settingsPageTemplate,
|
|
18
14
|
} from '../templates/patterns/page/index.js';
|
|
19
15
|
|
|
20
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
const TEMPLATES_DIR = resolve(__dirname, '..', 'templates', 'dashboard-pages');
|
|
22
|
-
|
|
23
16
|
/** Map from pattern name to its template function. */
|
|
24
17
|
const PATTERN_TEMPLATES: Record<PagePattern, (name: string) => string> = {
|
|
25
18
|
list: listPageTemplate,
|
|
@@ -44,94 +37,6 @@ export interface AddPageOptions {
|
|
|
44
37
|
layout?: 'app' | 'auth';
|
|
45
38
|
}
|
|
46
39
|
|
|
47
|
-
interface PageEntry {
|
|
48
|
-
id: string;
|
|
49
|
-
category: string;
|
|
50
|
-
path: string;
|
|
51
|
-
name: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function loadRegistry(): Promise<PageEntry[]> {
|
|
55
|
-
// Dynamic import avoids tsx compilation issues with .tsx templates
|
|
56
|
-
const mod = await import('../templates/dashboard-pages/index.js') as { PAGE_REGISTRY: readonly PageEntry[] };
|
|
57
|
-
return [...mod.PAGE_REGISTRY];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function getCategoryForPageId(pageId: string): string {
|
|
61
|
-
const authPages = ['login', 'signup', 'verify-email', 'forgot-password', 'reset-password', 'mfa-setup', 'mfa-verify', 'choose-org', 'accept-invitation', 'sign-out'];
|
|
62
|
-
const accountPages = ['profile', 'workspace-settings', 'members', 'api-keys', 'audit-log'];
|
|
63
|
-
|
|
64
|
-
if (authPages.includes(pageId)) return 'auth';
|
|
65
|
-
if (accountPages.includes(pageId)) return 'account';
|
|
66
|
-
return 'shell';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function fetchPageFromS3(
|
|
70
|
-
scaffoldBucket: string,
|
|
71
|
-
scaffoldVersion: string,
|
|
72
|
-
pageId: string
|
|
73
|
-
): Promise<string | null> {
|
|
74
|
-
try {
|
|
75
|
-
const modulesJson = await fetchModulesJson(scaffoldBucket, scaffoldVersion);
|
|
76
|
-
const frontendModule = modulesJson.modules.find(m => m.id === 'frontend');
|
|
77
|
-
|
|
78
|
-
if (!frontendModule) {
|
|
79
|
-
throw new Error(`Frontend module not found in modules.json for version ${scaffoldVersion}`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const tarballBuffer = await fetchModuleTarball(
|
|
83
|
-
scaffoldBucket,
|
|
84
|
-
scaffoldVersion,
|
|
85
|
-
frontendModule.tarball,
|
|
86
|
-
frontendModule.sha256
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// Extract the page file from the tarball
|
|
90
|
-
const pageFileName = `${pageId}.tsx`;
|
|
91
|
-
const category = getCategoryForPageId(pageId);
|
|
92
|
-
const expectedPath = `frontend/src/pages/${category}/${pageFileName}`;
|
|
93
|
-
|
|
94
|
-
let content = '';
|
|
95
|
-
const chunks: Buffer[] = [];
|
|
96
|
-
|
|
97
|
-
await new Promise<void>((resolve, reject) => {
|
|
98
|
-
const parser = new TarParser({
|
|
99
|
-
onentry: (entry) => {
|
|
100
|
-
if (entry.path === expectedPath) {
|
|
101
|
-
entry.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
102
|
-
} else {
|
|
103
|
-
entry.resume();
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
parser.on('finish', resolve);
|
|
108
|
-
parser.on('error', reject);
|
|
109
|
-
Readable.from(tarballBuffer).pipe(parser);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
if (chunks.length === 0) {
|
|
113
|
-
throw new Error(`Page template not found at ${expectedPath} in frontend tarball`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
content = Buffer.concat(chunks).toString('utf-8');
|
|
117
|
-
return content;
|
|
118
|
-
} catch (err) {
|
|
119
|
-
cliLogger.warn(err, 'Failed to fetch from S3, falling back to local templates');
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function loadFromLocal(pageId: string, entry: PageEntry): Promise<string> {
|
|
125
|
-
const templatePath = join(TEMPLATES_DIR, entry.path);
|
|
126
|
-
try {
|
|
127
|
-
const content = await readFile(templatePath, 'utf8');
|
|
128
|
-
cliLogger.warn({ pageId }, 'Using local fallback template (scaffold version not available)');
|
|
129
|
-
return content;
|
|
130
|
-
} catch {
|
|
131
|
-
throw new Error(`Template file not found at ${templatePath}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
40
|
export async function runAddPage(opts: AddPageOptions): Promise<void> {
|
|
136
41
|
const { pageId, pattern, layout = 'app' } = opts;
|
|
137
42
|
|
|
@@ -139,86 +44,67 @@ export async function runAddPage(opts: AddPageOptions): Promise<void> {
|
|
|
139
44
|
throw new Error(`Invalid page id "${pageId}" — must be kebab-case`);
|
|
140
45
|
}
|
|
141
46
|
|
|
142
|
-
// ──
|
|
143
|
-
if (pattern) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
content = layoutComment + content;
|
|
157
|
-
|
|
158
|
-
const outDir = opts.outputDir
|
|
47
|
+
// ── .page.json output mode (default when no pattern specified) ──
|
|
48
|
+
if (!pattern) {
|
|
49
|
+
const pageJsonContent = JSON.stringify({
|
|
50
|
+
id: pageId,
|
|
51
|
+
type: 'page',
|
|
52
|
+
route: `/${pageId}`,
|
|
53
|
+
tenancy: layout === 'auth' ? 'none' : 'required',
|
|
54
|
+
title: pageId.replace(/-/g, ' ').replace(/\b\w/g, (c: string) => c.toUpperCase()),
|
|
55
|
+
layout: layout,
|
|
56
|
+
tokens: {},
|
|
57
|
+
regions: [],
|
|
58
|
+
}, null, 2) + '\n';
|
|
59
|
+
|
|
60
|
+
const pageJsonOutDir = opts.outputDir
|
|
159
61
|
? resolve(opts.outputDir)
|
|
160
|
-
: resolve(process.cwd(), '
|
|
62
|
+
: resolve(process.cwd(), 'pages');
|
|
161
63
|
|
|
162
|
-
await mkdir(
|
|
64
|
+
await mkdir(pageJsonOutDir, { recursive: true });
|
|
65
|
+
const pageJsonPath = join(pageJsonOutDir, `${pageId}.page.json`);
|
|
163
66
|
|
|
164
|
-
const fileName = `${pageId}.tsx`;
|
|
165
|
-
const outPath = join(outDir, fileName);
|
|
166
|
-
|
|
167
|
-
// Refuse if target already exists
|
|
168
67
|
try {
|
|
169
|
-
await access(
|
|
170
|
-
throw new Error(`File already exists at ${
|
|
68
|
+
await access(pageJsonPath);
|
|
69
|
+
throw new Error(`File already exists at ${pageJsonPath}. Remove it first or use a different --output directory.`);
|
|
171
70
|
} catch (err) {
|
|
172
71
|
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
173
72
|
}
|
|
174
73
|
|
|
175
|
-
await writeFile(
|
|
74
|
+
await writeFile(pageJsonPath, pageJsonContent, 'utf8');
|
|
176
75
|
|
|
177
|
-
cliLogger.info({ pageId,
|
|
76
|
+
cliLogger.info({ pageId, layout, outPath: pageJsonPath }, 'Page .page.json scaffolded');
|
|
178
77
|
// eslint-disable-next-line no-console
|
|
179
78
|
console.log(
|
|
180
|
-
`\n✓ Page "${pageId}" (
|
|
181
|
-
` Next: add
|
|
79
|
+
`\n✓ Page "${pageId}" (layout: ${layout}) added at ${pageJsonPath}\n` +
|
|
80
|
+
` Next: add the page in the page builder UI or edit the .page.json directly.\n` +
|
|
81
|
+
` To add a route, update src/router.tsx.tpl.\n`
|
|
182
82
|
);
|
|
183
83
|
return;
|
|
184
84
|
}
|
|
185
85
|
|
|
186
|
-
// ──
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (!entry) {
|
|
191
|
-
const available = registry.map(p => p.id).join(', ');
|
|
86
|
+
// ── Pattern mode ──
|
|
87
|
+
if (!PAGE_PATTERNS.includes(pattern)) {
|
|
88
|
+
const available = PAGE_PATTERNS.join(', ');
|
|
192
89
|
throw new Error(
|
|
193
|
-
`Unknown page
|
|
194
|
-
`Tip: use --pattern <pattern> to scaffold with a page pattern. Available patterns: ${PAGE_PATTERNS.join(', ')}`
|
|
90
|
+
`Unknown page pattern "${pattern}".\n\nAvailable patterns: ${available}`
|
|
195
91
|
);
|
|
196
92
|
}
|
|
197
93
|
|
|
198
|
-
|
|
94
|
+
const templateFn = PATTERN_TEMPLATES[pattern];
|
|
95
|
+
let content = templateFn(pageId);
|
|
199
96
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (config.scaffoldVersion && config.scaffoldBucket) {
|
|
204
|
-
content = await fetchPageFromS3(config.scaffoldBucket, config.scaffoldVersion, pageId);
|
|
205
|
-
}
|
|
206
|
-
} catch (err) {
|
|
207
|
-
cliLogger.debug(err, 'Could not read scaffold config');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Fall back to local templates
|
|
211
|
-
if (!content) {
|
|
212
|
-
content = await loadFromLocal(pageId, entry);
|
|
213
|
-
}
|
|
97
|
+
// Prepend layout comment
|
|
98
|
+
const layoutComment = `// Layout: ${layout === 'auth' ? 'AuthLayout' : 'AppLayout'} — import and use in your router config\n\n`;
|
|
99
|
+
content = layoutComment + content;
|
|
214
100
|
|
|
215
101
|
const outDir = opts.outputDir
|
|
216
102
|
? resolve(opts.outputDir)
|
|
217
|
-
: resolve(process.cwd(), 'src', 'pages',
|
|
103
|
+
: resolve(process.cwd(), 'src', 'pages', pageId);
|
|
218
104
|
|
|
219
105
|
await mkdir(outDir, { recursive: true });
|
|
220
106
|
|
|
221
|
-
const fileName = `${
|
|
107
|
+
const fileName = `${pageId}.tsx`;
|
|
222
108
|
const outPath = join(outDir, fileName);
|
|
223
109
|
|
|
224
110
|
// Refuse if target already exists
|
|
@@ -231,12 +117,10 @@ export async function runAddPage(opts: AddPageOptions): Promise<void> {
|
|
|
231
117
|
|
|
232
118
|
await writeFile(outPath, content, 'utf8');
|
|
233
119
|
|
|
234
|
-
cliLogger.info({ pageId, layout, outPath }, 'Page scaffolded');
|
|
120
|
+
cliLogger.info({ pageId, pattern, layout, outPath }, 'Page scaffolded from pattern');
|
|
235
121
|
// eslint-disable-next-line no-console
|
|
236
122
|
console.log(
|
|
237
|
-
`\n✓ Page "${pageId}" (layout: ${layout}) added at ${outPath}\n` +
|
|
238
|
-
`
|
|
239
|
-
` Next: import { ${entry.name} } from './${entry.category}/${basename(entry.path, '.tsx')}'\n` +
|
|
240
|
-
` Then add a route in src/router.tsx\n`
|
|
123
|
+
`\n✓ Page "${pageId}" (pattern: ${pattern}, layout: ${layout}) added at ${outPath}\n` +
|
|
124
|
+
` Next: add a route in src/router.tsx for this page\n`
|
|
241
125
|
);
|
|
242
126
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface ComponentManifestEntry {
|
|
5
|
+
id: string;
|
|
6
|
+
displayName: string;
|
|
7
|
+
filePath: string;
|
|
8
|
+
propsSchema: Record<string, unknown>;
|
|
9
|
+
fixture?: unknown;
|
|
10
|
+
category: 'ui' | 'custom';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function kebabToPascal(kebab: string): string {
|
|
14
|
+
return kebab
|
|
15
|
+
.split('-')
|
|
16
|
+
.map(segment => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
17
|
+
.join('');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function scanDir(dir: string, category: 'ui' | 'custom'): Promise<ComponentManifestEntry[]> {
|
|
21
|
+
const entries: ComponentManifestEntry[] = [];
|
|
22
|
+
|
|
23
|
+
let files: string[];
|
|
24
|
+
try {
|
|
25
|
+
files = await readdir(dir);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
28
|
+
return entries;
|
|
29
|
+
}
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
if (file.endsWith('.tsx')) {
|
|
35
|
+
const id = file.slice(0, -4);
|
|
36
|
+
const displayName = kebabToPascal(id);
|
|
37
|
+
const filePath = join(dir, file);
|
|
38
|
+
|
|
39
|
+
const entry: ComponentManifestEntry = {
|
|
40
|
+
id,
|
|
41
|
+
displayName,
|
|
42
|
+
filePath,
|
|
43
|
+
propsSchema: {},
|
|
44
|
+
category,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const fixtureFilePath = join(dir, `${id}.fixture.json`);
|
|
48
|
+
try {
|
|
49
|
+
const fixtureContent = await readFile(fixtureFilePath, 'utf8');
|
|
50
|
+
entry.fixture = JSON.parse(fixtureContent);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// If fixture doesn't exist or can't be parsed, omit the field
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
entries.push(entry);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return entries;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function scanComponents(opts: {
|
|
63
|
+
uiDir: string;
|
|
64
|
+
customDir: string;
|
|
65
|
+
}): Promise<ComponentManifestEntry[]> {
|
|
66
|
+
const uiEntries = await scanDir(opts.uiDir, 'ui');
|
|
67
|
+
const customEntries = await scanDir(opts.customDir, 'custom');
|
|
68
|
+
|
|
69
|
+
return [...uiEntries, ...customEntries];
|
|
70
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface PageRegistryEntry {
|
|
5
|
+
id: string;
|
|
6
|
+
route: string | null;
|
|
7
|
+
tenancy: string | null;
|
|
8
|
+
title: string | null;
|
|
9
|
+
tier: 1 | 2 | 3;
|
|
10
|
+
filePath: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function readPageJsonDir(dir: string): Promise<PageRegistryEntry[]> {
|
|
14
|
+
const entries: PageRegistryEntry[] = [];
|
|
15
|
+
|
|
16
|
+
let files: string[];
|
|
17
|
+
try {
|
|
18
|
+
files = await readdir(dir);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
21
|
+
return entries;
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
if (file.endsWith('.page.json')) {
|
|
28
|
+
const filePath = join(dir, file);
|
|
29
|
+
try {
|
|
30
|
+
const content = await readFile(filePath, 'utf8');
|
|
31
|
+
const pageData = JSON.parse(content);
|
|
32
|
+
|
|
33
|
+
entries.push({
|
|
34
|
+
id: pageData.id,
|
|
35
|
+
route: pageData.route ?? null,
|
|
36
|
+
tenancy: pageData.tenancy ?? null,
|
|
37
|
+
title: pageData.title ?? null,
|
|
38
|
+
tier: 1,
|
|
39
|
+
filePath,
|
|
40
|
+
});
|
|
41
|
+
} catch (err) {
|
|
42
|
+
// Skip malformed JSON files
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return entries;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function readCustomComponentsDir(dir: string): Promise<PageRegistryEntry[]> {
|
|
51
|
+
const entries: PageRegistryEntry[] = [];
|
|
52
|
+
|
|
53
|
+
let files: string[];
|
|
54
|
+
try {
|
|
55
|
+
files = await readdir(dir);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
58
|
+
return entries;
|
|
59
|
+
}
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
if (file.endsWith('.tsx')) {
|
|
65
|
+
const id = file.slice(0, -4);
|
|
66
|
+
const filePath = join(dir, file);
|
|
67
|
+
|
|
68
|
+
entries.push({
|
|
69
|
+
id,
|
|
70
|
+
route: null,
|
|
71
|
+
tenancy: null,
|
|
72
|
+
title: null,
|
|
73
|
+
tier: 2,
|
|
74
|
+
filePath,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return entries;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function walkFrontendPages(dir: string, baseDir: string): Promise<PageRegistryEntry[]> {
|
|
83
|
+
const entries: PageRegistryEntry[] = [];
|
|
84
|
+
|
|
85
|
+
let items: { name: string; isDirectory(): boolean }[];
|
|
86
|
+
try {
|
|
87
|
+
items = await readdir(dir, { withFileTypes: true });
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
90
|
+
return entries;
|
|
91
|
+
}
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const item of items) {
|
|
96
|
+
// Skip directories named _errors and shell
|
|
97
|
+
if (item.isDirectory() && (item.name === '_errors' || item.name === 'shell')) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const itemPath = join(dir, item.name);
|
|
102
|
+
|
|
103
|
+
if (item.isDirectory()) {
|
|
104
|
+
const subEntries = await walkFrontendPages(itemPath, baseDir);
|
|
105
|
+
entries.push(...subEntries);
|
|
106
|
+
} else if (item.name.endsWith('.tsx')) {
|
|
107
|
+
const relPath = relative(baseDir, itemPath);
|
|
108
|
+
const id = relPath.slice(0, -4);
|
|
109
|
+
|
|
110
|
+
entries.push({
|
|
111
|
+
id,
|
|
112
|
+
route: null,
|
|
113
|
+
tenancy: null,
|
|
114
|
+
title: null,
|
|
115
|
+
tier: 3,
|
|
116
|
+
filePath: itemPath,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return entries;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function indexPages(opts: {
|
|
125
|
+
pagesDir: string;
|
|
126
|
+
customDir: string;
|
|
127
|
+
frontendPagesDir: string;
|
|
128
|
+
}): Promise<PageRegistryEntry[]> {
|
|
129
|
+
const tier1 = await readPageJsonDir(opts.pagesDir);
|
|
130
|
+
const tier2 = await readCustomComponentsDir(opts.customDir);
|
|
131
|
+
const tier3 = await walkFrontendPages(opts.frontendPagesDir, opts.frontendPagesDir);
|
|
132
|
+
|
|
133
|
+
return [...tier1, ...tier2, ...tier3];
|
|
134
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { scanComponents } from './build-ui/component-scanner.js';
|
|
4
|
+
import { indexPages } from './build-ui/page-indexer.js';
|
|
5
|
+
|
|
6
|
+
export async function runBuildUi(opts: { projectRoot?: string }): Promise<void> {
|
|
7
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
8
|
+
|
|
9
|
+
const components = await scanComponents({
|
|
10
|
+
uiDir: join(projectRoot, 'frontend/src/components/ui'),
|
|
11
|
+
customDir: join(projectRoot, 'frontend/src/components/custom'),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const componentManifestPath = join(projectRoot, '.mc/component-manifest.json');
|
|
15
|
+
await mkdir(join(componentManifestPath, '..'), { recursive: true });
|
|
16
|
+
await writeFile(componentManifestPath, JSON.stringify(components, null, 2), 'utf8');
|
|
17
|
+
|
|
18
|
+
const pages = await indexPages({
|
|
19
|
+
pagesDir: join(projectRoot, 'pages'),
|
|
20
|
+
customDir: join(projectRoot, 'frontend/src/components/custom'),
|
|
21
|
+
frontendPagesDir: join(projectRoot, 'frontend/src/pages'),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const pageRegistryPath = join(projectRoot, '.mc/page-manifest.json');
|
|
25
|
+
await mkdir(join(pageRegistryPath, '..'), { recursive: true });
|
|
26
|
+
await writeFile(pageRegistryPath, JSON.stringify(pages, null, 2), 'utf8');
|
|
27
|
+
|
|
28
|
+
process.stderr.write(`[build-ui] Wrote ${components.length} components and ${pages.length} pages.\n`);
|
|
29
|
+
}
|
|
@@ -21,7 +21,7 @@ function camelCase(s: string): string {
|
|
|
21
21
|
export function apiSkeletonTemplate(domainId: string, apiId: string, tenancy: string): string {
|
|
22
22
|
const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
|
|
23
23
|
return `import { z } from 'zod';
|
|
24
|
-
import { defineApi } from '
|
|
24
|
+
import { defineApi } from '@mettlecast/domain-runtime';
|
|
25
25
|
|
|
26
26
|
export const ${camelCase(apiId)} = defineApi({
|
|
27
27
|
id: '${apiId}',
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export function eventsSkeletonTemplate(domainId: string): string {
|
|
11
11
|
return `import { z } from 'zod';
|
|
12
|
-
import { defineEvent } from '
|
|
12
|
+
import { defineEvent } from '@mettlecast/domain-runtime';
|
|
13
13
|
|
|
14
14
|
export const exampleEvent = defineEvent({
|
|
15
15
|
id: '${domainId}.example',
|
|
@@ -14,7 +14,7 @@ export function subscriberSkeletonTemplate(
|
|
|
14
14
|
subscriberId: string,
|
|
15
15
|
eventId: string
|
|
16
16
|
): string {
|
|
17
|
-
return `import { defineSubscriber } from '
|
|
17
|
+
return `import { defineSubscriber } from '@mettlecast/domain-runtime';
|
|
18
18
|
|
|
19
19
|
export const ${subscriberId.replace(/-([a-z])/g, (_, c) => c.toUpperCase())} = defineSubscriber({
|
|
20
20
|
id: '${domainId}.${subscriberId}',
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
export declare const PAGE_REGISTRY: readonly [{
|
|
2
|
-
readonly id: "login";
|
|
3
|
-
readonly category: "auth";
|
|
4
|
-
readonly path: "auth/login.tsx";
|
|
5
|
-
readonly name: "LoginPage";
|
|
6
|
-
}, {
|
|
7
|
-
readonly id: "signup";
|
|
8
|
-
readonly category: "auth";
|
|
9
|
-
readonly path: "auth/signup.tsx";
|
|
10
|
-
readonly name: "SignupPage";
|
|
11
|
-
}, {
|
|
12
|
-
readonly id: "verify-email";
|
|
13
|
-
readonly category: "auth";
|
|
14
|
-
readonly path: "auth/verify-email.tsx";
|
|
15
|
-
readonly name: "VerifyEmailPage";
|
|
16
|
-
}, {
|
|
17
|
-
readonly id: "forgot-password";
|
|
18
|
-
readonly category: "auth";
|
|
19
|
-
readonly path: "auth/forgot-password.tsx";
|
|
20
|
-
readonly name: "ForgotPasswordPage";
|
|
21
|
-
}, {
|
|
22
|
-
readonly id: "reset-password";
|
|
23
|
-
readonly category: "auth";
|
|
24
|
-
readonly path: "auth/reset-password.tsx";
|
|
25
|
-
readonly name: "ResetPasswordPage";
|
|
26
|
-
}, {
|
|
27
|
-
readonly id: "mfa-setup";
|
|
28
|
-
readonly category: "auth";
|
|
29
|
-
readonly path: "auth/mfa-setup.tsx";
|
|
30
|
-
readonly name: "MfaSetupPage";
|
|
31
|
-
}, {
|
|
32
|
-
readonly id: "mfa-verify";
|
|
33
|
-
readonly category: "auth";
|
|
34
|
-
readonly path: "auth/mfa-verify.tsx";
|
|
35
|
-
readonly name: "MfaVerifyPage";
|
|
36
|
-
}, {
|
|
37
|
-
readonly id: "accept-invitation";
|
|
38
|
-
readonly category: "auth";
|
|
39
|
-
readonly path: "auth/accept-invitation.tsx";
|
|
40
|
-
readonly name: "AcceptInvitationPage";
|
|
41
|
-
}, {
|
|
42
|
-
readonly id: "choose-org";
|
|
43
|
-
readonly category: "auth";
|
|
44
|
-
readonly path: "auth/choose-org.tsx";
|
|
45
|
-
readonly name: "ChooseOrgPage";
|
|
46
|
-
}, {
|
|
47
|
-
readonly id: "sign-out";
|
|
48
|
-
readonly category: "auth";
|
|
49
|
-
readonly path: "auth/sign-out.tsx";
|
|
50
|
-
readonly name: "SignOutPage";
|
|
51
|
-
}, {
|
|
52
|
-
readonly id: "dashboard-home";
|
|
53
|
-
readonly category: "shell";
|
|
54
|
-
readonly path: "shell/dashboard-home.tsx";
|
|
55
|
-
readonly name: "DashboardHomePage";
|
|
56
|
-
}, {
|
|
57
|
-
readonly id: "not-found";
|
|
58
|
-
readonly category: "shell";
|
|
59
|
-
readonly path: "shell/not-found.tsx";
|
|
60
|
-
readonly name: "NotFoundPage";
|
|
61
|
-
}, {
|
|
62
|
-
readonly id: "forbidden";
|
|
63
|
-
readonly category: "shell";
|
|
64
|
-
readonly path: "shell/forbidden.tsx";
|
|
65
|
-
readonly name: "ForbiddenPage";
|
|
66
|
-
}, {
|
|
67
|
-
readonly id: "server-error";
|
|
68
|
-
readonly category: "shell";
|
|
69
|
-
readonly path: "shell/server-error.tsx";
|
|
70
|
-
readonly name: "ServerErrorPage";
|
|
71
|
-
}, {
|
|
72
|
-
readonly id: "maintenance";
|
|
73
|
-
readonly category: "shell";
|
|
74
|
-
readonly path: "shell/maintenance.tsx";
|
|
75
|
-
readonly name: "MaintenancePage";
|
|
76
|
-
}, {
|
|
77
|
-
readonly id: "profile";
|
|
78
|
-
readonly category: "account";
|
|
79
|
-
readonly path: "account/profile.tsx";
|
|
80
|
-
readonly name: "ProfilePage";
|
|
81
|
-
}, {
|
|
82
|
-
readonly id: "workspace-settings";
|
|
83
|
-
readonly category: "account";
|
|
84
|
-
readonly path: "account/workspace-settings.tsx";
|
|
85
|
-
readonly name: "WorkspaceSettingsPage";
|
|
86
|
-
}, {
|
|
87
|
-
readonly id: "members";
|
|
88
|
-
readonly category: "account";
|
|
89
|
-
readonly path: "account/members.tsx";
|
|
90
|
-
readonly name: "MembersPage";
|
|
91
|
-
}, {
|
|
92
|
-
readonly id: "api-keys";
|
|
93
|
-
readonly category: "account";
|
|
94
|
-
readonly path: "account/api-keys.tsx";
|
|
95
|
-
readonly name: "ApiKeysPage";
|
|
96
|
-
}, {
|
|
97
|
-
readonly id: "audit-log";
|
|
98
|
-
readonly category: "account";
|
|
99
|
-
readonly path: "account/audit-log.tsx";
|
|
100
|
-
readonly name: "AuditLogPage";
|
|
101
|
-
}];
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export const PAGE_REGISTRY = [
|
|
2
|
-
{ id: 'login', category: 'auth', path: 'auth/login.tsx', name: 'LoginPage' },
|
|
3
|
-
{ id: 'signup', category: 'auth', path: 'auth/signup.tsx', name: 'SignupPage' },
|
|
4
|
-
{ id: 'verify-email', category: 'auth', path: 'auth/verify-email.tsx', name: 'VerifyEmailPage' },
|
|
5
|
-
{ id: 'forgot-password', category: 'auth', path: 'auth/forgot-password.tsx', name: 'ForgotPasswordPage' },
|
|
6
|
-
{ id: 'reset-password', category: 'auth', path: 'auth/reset-password.tsx', name: 'ResetPasswordPage' },
|
|
7
|
-
{ id: 'mfa-setup', category: 'auth', path: 'auth/mfa-setup.tsx', name: 'MfaSetupPage' },
|
|
8
|
-
{ id: 'mfa-verify', category: 'auth', path: 'auth/mfa-verify.tsx', name: 'MfaVerifyPage' },
|
|
9
|
-
{ id: 'accept-invitation', category: 'auth', path: 'auth/accept-invitation.tsx', name: 'AcceptInvitationPage' },
|
|
10
|
-
{ id: 'choose-org', category: 'auth', path: 'auth/choose-org.tsx', name: 'ChooseOrgPage' },
|
|
11
|
-
{ id: 'sign-out', category: 'auth', path: 'auth/sign-out.tsx', name: 'SignOutPage' },
|
|
12
|
-
{ id: 'dashboard-home', category: 'shell', path: 'shell/dashboard-home.tsx', name: 'DashboardHomePage' },
|
|
13
|
-
{ id: 'not-found', category: 'shell', path: 'shell/not-found.tsx', name: 'NotFoundPage' },
|
|
14
|
-
{ id: 'forbidden', category: 'shell', path: 'shell/forbidden.tsx', name: 'ForbiddenPage' },
|
|
15
|
-
{ id: 'server-error', category: 'shell', path: 'shell/server-error.tsx', name: 'ServerErrorPage' },
|
|
16
|
-
{ id: 'maintenance', category: 'shell', path: 'shell/maintenance.tsx', name: 'MaintenancePage' },
|
|
17
|
-
{ id: 'profile', category: 'account', path: 'account/profile.tsx', name: 'ProfilePage' },
|
|
18
|
-
{ id: 'workspace-settings', category: 'account', path: 'account/workspace-settings.tsx', name: 'WorkspaceSettingsPage' },
|
|
19
|
-
{ id: 'members', category: 'account', path: 'account/members.tsx', name: 'MembersPage' },
|
|
20
|
-
{ id: 'api-keys', category: 'account', path: 'account/api-keys.tsx', name: 'ApiKeysPage' },
|
|
21
|
-
{ id: 'audit-log', category: 'account', path: 'account/audit-log.tsx', name: 'AuditLogPage' },
|
|
22
|
-
];
|