@mettlecast/domain-cli 0.2.2 → 0.2.4
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/dist/cli.js
CHANGED
|
@@ -30,6 +30,7 @@ import { runPowerTune } from './commands/power-tune.js';
|
|
|
30
30
|
import { runReseedPage } from './commands/reseed-page.js';
|
|
31
31
|
import { runAddSeedPage } from './commands/add-seed-page.js';
|
|
32
32
|
import { runShowDns } from './commands/show-dns.js';
|
|
33
|
+
import { runBuildUi } from './commands/build-ui.js';
|
|
33
34
|
/**
|
|
34
35
|
* Root CLI program for the TIB Domain Module local developer toolchain.
|
|
35
36
|
* Provides build, validate, test, dev, add-domain, add-api, and add-subscriber subcommands.
|
|
@@ -174,7 +175,7 @@ program
|
|
|
174
175
|
});
|
|
175
176
|
program
|
|
176
177
|
.command('add-page <pageId>')
|
|
177
|
-
.description('Scaffold a
|
|
178
|
+
.description('Scaffold a .page.json definition (default) or a pattern-based .tsx page component (--pattern)')
|
|
178
179
|
.option('--output <dir>', 'Override the output directory (default: src/pages/<category>)')
|
|
179
180
|
.option('--pattern <pattern>', 'Scaffold with a page pattern template (list, detail, form, wizard, dashboard, settings)')
|
|
180
181
|
.option('--layout <layout>', 'Layout to use: app (default) or auth', 'app')
|
|
@@ -331,6 +332,13 @@ program
|
|
|
331
332
|
.action(async (opts) => {
|
|
332
333
|
await runShowDns(opts);
|
|
333
334
|
});
|
|
335
|
+
program
|
|
336
|
+
.command('build-ui')
|
|
337
|
+
.description('Scan frontend components and page definitions, write .mc/component-manifest.json and .mc/page-registry.json')
|
|
338
|
+
.option('--project-root <path>', 'Root of the project (defaults to cwd)')
|
|
339
|
+
.action(async (opts) => {
|
|
340
|
+
await runBuildUi({ projectRoot: opts.projectRoot });
|
|
341
|
+
});
|
|
334
342
|
program.parseAsync(process.argv).catch((err) => {
|
|
335
343
|
// eslint-disable-next-line no-console
|
|
336
344
|
console.error(err);
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join, resolve
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { Readable } from 'node:stream';
|
|
5
|
-
import { Parser as TarParser } from 'tar';
|
|
1
|
+
import { writeFile, mkdir, access } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
6
3
|
import { cliLogger } from '../utils/logger.js';
|
|
7
|
-
import { readScaffoldConfig } from '../utils/scaffold-config.js';
|
|
8
|
-
import { fetchModulesJson, fetchModuleTarball } from '../utils/s3-fetch.js';
|
|
9
4
|
import { PAGE_PATTERNS, listPageTemplate, detailPageTemplate, formPageTemplate, wizardPageTemplate, dashboardPageTemplate, settingsPageTemplate, } from '../templates/patterns/page/index.js';
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const TEMPLATES_DIR = resolve(__dirname, '..', 'templates', 'dashboard-pages');
|
|
12
5
|
/** Map from pattern name to its template function. */
|
|
13
6
|
const PATTERN_TEMPLATES = {
|
|
14
7
|
list: listPageTemplate,
|
|
@@ -19,137 +12,59 @@ const PATTERN_TEMPLATES = {
|
|
|
19
12
|
settings: settingsPageTemplate,
|
|
20
13
|
};
|
|
21
14
|
const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
|
|
22
|
-
async function loadRegistry() {
|
|
23
|
-
// Dynamic import avoids tsx compilation issues with .tsx templates
|
|
24
|
-
const mod = await import('../templates/dashboard-pages/index.js');
|
|
25
|
-
return [...mod.PAGE_REGISTRY];
|
|
26
|
-
}
|
|
27
|
-
function getCategoryForPageId(pageId) {
|
|
28
|
-
const authPages = ['login', 'signup', 'verify-email', 'forgot-password', 'reset-password', 'mfa-setup', 'mfa-verify', 'choose-org', 'accept-invitation', 'sign-out'];
|
|
29
|
-
const accountPages = ['profile', 'workspace-settings', 'members', 'api-keys', 'audit-log'];
|
|
30
|
-
if (authPages.includes(pageId))
|
|
31
|
-
return 'auth';
|
|
32
|
-
if (accountPages.includes(pageId))
|
|
33
|
-
return 'account';
|
|
34
|
-
return 'shell';
|
|
35
|
-
}
|
|
36
|
-
async function fetchPageFromS3(scaffoldBucket, scaffoldVersion, pageId) {
|
|
37
|
-
try {
|
|
38
|
-
const modulesJson = await fetchModulesJson(scaffoldBucket, scaffoldVersion);
|
|
39
|
-
const frontendModule = modulesJson.modules.find(m => m.id === 'frontend');
|
|
40
|
-
if (!frontendModule) {
|
|
41
|
-
throw new Error(`Frontend module not found in modules.json for version ${scaffoldVersion}`);
|
|
42
|
-
}
|
|
43
|
-
const tarballBuffer = await fetchModuleTarball(scaffoldBucket, scaffoldVersion, frontendModule.tarball, frontendModule.sha256);
|
|
44
|
-
// Extract the page file from the tarball
|
|
45
|
-
const pageFileName = `${pageId}.tsx`;
|
|
46
|
-
const category = getCategoryForPageId(pageId);
|
|
47
|
-
const expectedPath = `frontend/src/pages/${category}/${pageFileName}`;
|
|
48
|
-
let content = '';
|
|
49
|
-
const chunks = [];
|
|
50
|
-
await new Promise((resolve, reject) => {
|
|
51
|
-
const parser = new TarParser({
|
|
52
|
-
onentry: (entry) => {
|
|
53
|
-
if (entry.path === expectedPath) {
|
|
54
|
-
entry.on('data', (chunk) => chunks.push(chunk));
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
entry.resume();
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
parser.on('finish', resolve);
|
|
62
|
-
parser.on('error', reject);
|
|
63
|
-
Readable.from(tarballBuffer).pipe(parser);
|
|
64
|
-
});
|
|
65
|
-
if (chunks.length === 0) {
|
|
66
|
-
throw new Error(`Page template not found at ${expectedPath} in frontend tarball`);
|
|
67
|
-
}
|
|
68
|
-
content = Buffer.concat(chunks).toString('utf-8');
|
|
69
|
-
return content;
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
cliLogger.warn(err, 'Failed to fetch from S3, falling back to local templates');
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
async function loadFromLocal(pageId, entry) {
|
|
77
|
-
const templatePath = join(TEMPLATES_DIR, entry.path);
|
|
78
|
-
try {
|
|
79
|
-
const content = await readFile(templatePath, 'utf8');
|
|
80
|
-
cliLogger.warn({ pageId }, 'Using local fallback template (scaffold version not available)');
|
|
81
|
-
return content;
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
throw new Error(`Template file not found at ${templatePath}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
15
|
export async function runAddPage(opts) {
|
|
88
16
|
const { pageId, pattern, layout = 'app' } = opts;
|
|
89
17
|
if (!KEBAB_REGEX.test(pageId)) {
|
|
90
18
|
throw new Error(`Invalid page id "${pageId}" — must be kebab-case`);
|
|
91
19
|
}
|
|
92
|
-
// ──
|
|
93
|
-
if (pattern) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
20
|
+
// ── .page.json output mode (default when no pattern specified) ──
|
|
21
|
+
if (!pattern) {
|
|
22
|
+
const pageJsonContent = JSON.stringify({
|
|
23
|
+
id: pageId,
|
|
24
|
+
type: 'page',
|
|
25
|
+
route: `/${pageId}`,
|
|
26
|
+
tenancy: layout === 'auth' ? 'none' : 'required',
|
|
27
|
+
title: pageId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
28
|
+
layout: layout,
|
|
29
|
+
tokens: {},
|
|
30
|
+
regions: [],
|
|
31
|
+
}, null, 2) + '\n';
|
|
32
|
+
const pageJsonOutDir = opts.outputDir
|
|
104
33
|
? resolve(opts.outputDir)
|
|
105
|
-
: resolve(process.cwd(), '
|
|
106
|
-
await mkdir(
|
|
107
|
-
const
|
|
108
|
-
const outPath = join(outDir, fileName);
|
|
109
|
-
// Refuse if target already exists
|
|
34
|
+
: resolve(process.cwd(), 'pages');
|
|
35
|
+
await mkdir(pageJsonOutDir, { recursive: true });
|
|
36
|
+
const pageJsonPath = join(pageJsonOutDir, `${pageId}.page.json`);
|
|
110
37
|
try {
|
|
111
|
-
await access(
|
|
112
|
-
throw new Error(`File already exists at ${
|
|
38
|
+
await access(pageJsonPath);
|
|
39
|
+
throw new Error(`File already exists at ${pageJsonPath}. Remove it first or use a different --output directory.`);
|
|
113
40
|
}
|
|
114
41
|
catch (err) {
|
|
115
42
|
if (err.code !== 'ENOENT')
|
|
116
43
|
throw err;
|
|
117
44
|
}
|
|
118
|
-
await writeFile(
|
|
119
|
-
cliLogger.info({ pageId,
|
|
45
|
+
await writeFile(pageJsonPath, pageJsonContent, 'utf8');
|
|
46
|
+
cliLogger.info({ pageId, layout, outPath: pageJsonPath }, 'Page .page.json scaffolded');
|
|
120
47
|
// eslint-disable-next-line no-console
|
|
121
|
-
console.log(`\n✓ Page "${pageId}" (
|
|
122
|
-
` Next: add
|
|
48
|
+
console.log(`\n✓ Page "${pageId}" (layout: ${layout}) added at ${pageJsonPath}\n` +
|
|
49
|
+
` Next: add the page in the page builder UI or edit the .page.json directly.\n` +
|
|
50
|
+
` To add a route, update src/router.tsx.tpl.\n`);
|
|
123
51
|
return;
|
|
124
52
|
}
|
|
125
|
-
// ──
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const available = registry.map(p => p.id).join(', ');
|
|
130
|
-
throw new Error(`Unknown page id "${pageId}".\n\nAvailable pages: ${available}\n` +
|
|
131
|
-
`Tip: use --pattern <pattern> to scaffold with a page pattern. Available patterns: ${PAGE_PATTERNS.join(', ')}`);
|
|
132
|
-
}
|
|
133
|
-
let content = null;
|
|
134
|
-
// Try S3 fetch first
|
|
135
|
-
try {
|
|
136
|
-
const config = await readScaffoldConfig(process.cwd());
|
|
137
|
-
if (config.scaffoldVersion && config.scaffoldBucket) {
|
|
138
|
-
content = await fetchPageFromS3(config.scaffoldBucket, config.scaffoldVersion, pageId);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
catch (err) {
|
|
142
|
-
cliLogger.debug(err, 'Could not read scaffold config');
|
|
143
|
-
}
|
|
144
|
-
// Fall back to local templates
|
|
145
|
-
if (!content) {
|
|
146
|
-
content = await loadFromLocal(pageId, entry);
|
|
53
|
+
// ── Pattern mode ──
|
|
54
|
+
if (!PAGE_PATTERNS.includes(pattern)) {
|
|
55
|
+
const available = PAGE_PATTERNS.join(', ');
|
|
56
|
+
throw new Error(`Unknown page pattern "${pattern}".\n\nAvailable patterns: ${available}`);
|
|
147
57
|
}
|
|
58
|
+
const templateFn = PATTERN_TEMPLATES[pattern];
|
|
59
|
+
let content = templateFn(pageId);
|
|
60
|
+
// Prepend layout comment
|
|
61
|
+
const layoutComment = `// Layout: ${layout === 'auth' ? 'AuthLayout' : 'AppLayout'} — import and use in your router config\n\n`;
|
|
62
|
+
content = layoutComment + content;
|
|
148
63
|
const outDir = opts.outputDir
|
|
149
64
|
? resolve(opts.outputDir)
|
|
150
|
-
: resolve(process.cwd(), 'src', 'pages',
|
|
65
|
+
: resolve(process.cwd(), 'src', 'pages', pageId);
|
|
151
66
|
await mkdir(outDir, { recursive: true });
|
|
152
|
-
const fileName = `${
|
|
67
|
+
const fileName = `${pageId}.tsx`;
|
|
153
68
|
const outPath = join(outDir, fileName);
|
|
154
69
|
// Refuse if target already exists
|
|
155
70
|
try {
|
|
@@ -161,10 +76,8 @@ export async function runAddPage(opts) {
|
|
|
161
76
|
throw err;
|
|
162
77
|
}
|
|
163
78
|
await writeFile(outPath, content, 'utf8');
|
|
164
|
-
cliLogger.info({ pageId, layout, outPath }, 'Page scaffolded');
|
|
79
|
+
cliLogger.info({ pageId, pattern, layout, outPath }, 'Page scaffolded from pattern');
|
|
165
80
|
// eslint-disable-next-line no-console
|
|
166
|
-
console.log(`\n✓ Page "${pageId}" (layout: ${layout}) added at ${outPath}\n` +
|
|
167
|
-
`
|
|
168
|
-
` Next: import { ${entry.name} } from './${entry.category}/${basename(entry.path, '.tsx')}'\n` +
|
|
169
|
-
` Then add a route in src/router.tsx\n`);
|
|
81
|
+
console.log(`\n✓ Page "${pageId}" (pattern: ${pattern}, layout: ${layout}) added at ${outPath}\n` +
|
|
82
|
+
` Next: add a route in src/router.tsx for this page\n`);
|
|
170
83
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ComponentManifestEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
displayName: string;
|
|
4
|
+
filePath: string;
|
|
5
|
+
propsSchema: Record<string, unknown>;
|
|
6
|
+
fixture?: unknown;
|
|
7
|
+
category: 'ui' | 'custom';
|
|
8
|
+
}
|
|
9
|
+
export declare function scanComponents(opts: {
|
|
10
|
+
uiDir: string;
|
|
11
|
+
customDir: string;
|
|
12
|
+
}): Promise<ComponentManifestEntry[]>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
function kebabToPascal(kebab) {
|
|
4
|
+
return kebab
|
|
5
|
+
.split('-')
|
|
6
|
+
.map(segment => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
7
|
+
.join('');
|
|
8
|
+
}
|
|
9
|
+
async function scanDir(dir, category) {
|
|
10
|
+
const entries = [];
|
|
11
|
+
let files;
|
|
12
|
+
try {
|
|
13
|
+
files = await readdir(dir);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
if (err.code === 'ENOENT') {
|
|
17
|
+
return entries;
|
|
18
|
+
}
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
if (file.endsWith('.tsx')) {
|
|
23
|
+
const id = file.slice(0, -4);
|
|
24
|
+
const displayName = kebabToPascal(id);
|
|
25
|
+
const filePath = join(dir, file);
|
|
26
|
+
const entry = {
|
|
27
|
+
id,
|
|
28
|
+
displayName,
|
|
29
|
+
filePath,
|
|
30
|
+
propsSchema: {},
|
|
31
|
+
category,
|
|
32
|
+
};
|
|
33
|
+
const fixtureFilePath = join(dir, `${id}.fixture.json`);
|
|
34
|
+
try {
|
|
35
|
+
const fixtureContent = await readFile(fixtureFilePath, 'utf8');
|
|
36
|
+
entry.fixture = JSON.parse(fixtureContent);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
// If fixture doesn't exist or can't be parsed, omit the field
|
|
40
|
+
}
|
|
41
|
+
entries.push(entry);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return entries;
|
|
45
|
+
}
|
|
46
|
+
export async function scanComponents(opts) {
|
|
47
|
+
const uiEntries = await scanDir(opts.uiDir, 'ui');
|
|
48
|
+
const customEntries = await scanDir(opts.customDir, 'custom');
|
|
49
|
+
return [...uiEntries, ...customEntries];
|
|
50
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface PageRegistryEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
route: string | null;
|
|
4
|
+
tenancy: string | null;
|
|
5
|
+
title: string | null;
|
|
6
|
+
tier: 1 | 2 | 3;
|
|
7
|
+
filePath: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function indexPages(opts: {
|
|
10
|
+
pagesDir: string;
|
|
11
|
+
customDir: string;
|
|
12
|
+
frontendPagesDir: string;
|
|
13
|
+
}): Promise<PageRegistryEntry[]>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
async function readPageJsonDir(dir) {
|
|
4
|
+
const entries = [];
|
|
5
|
+
let files;
|
|
6
|
+
try {
|
|
7
|
+
files = await readdir(dir);
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
if (err.code === 'ENOENT') {
|
|
11
|
+
return entries;
|
|
12
|
+
}
|
|
13
|
+
throw err;
|
|
14
|
+
}
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
if (file.endsWith('.page.json')) {
|
|
17
|
+
const filePath = join(dir, file);
|
|
18
|
+
try {
|
|
19
|
+
const content = await readFile(filePath, 'utf8');
|
|
20
|
+
const pageData = JSON.parse(content);
|
|
21
|
+
entries.push({
|
|
22
|
+
id: pageData.id,
|
|
23
|
+
route: pageData.route ?? null,
|
|
24
|
+
tenancy: pageData.tenancy ?? null,
|
|
25
|
+
title: pageData.title ?? null,
|
|
26
|
+
tier: 1,
|
|
27
|
+
filePath,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
// Skip malformed JSON files
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return entries;
|
|
36
|
+
}
|
|
37
|
+
async function readCustomComponentsDir(dir) {
|
|
38
|
+
const entries = [];
|
|
39
|
+
let files;
|
|
40
|
+
try {
|
|
41
|
+
files = await readdir(dir);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err.code === 'ENOENT') {
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
if (file.endsWith('.tsx')) {
|
|
51
|
+
const id = file.slice(0, -4);
|
|
52
|
+
const filePath = join(dir, file);
|
|
53
|
+
entries.push({
|
|
54
|
+
id,
|
|
55
|
+
route: null,
|
|
56
|
+
tenancy: null,
|
|
57
|
+
title: null,
|
|
58
|
+
tier: 2,
|
|
59
|
+
filePath,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return entries;
|
|
64
|
+
}
|
|
65
|
+
async function walkFrontendPages(dir, baseDir) {
|
|
66
|
+
const entries = [];
|
|
67
|
+
let items;
|
|
68
|
+
try {
|
|
69
|
+
items = await readdir(dir, { withFileTypes: true });
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err.code === 'ENOENT') {
|
|
73
|
+
return entries;
|
|
74
|
+
}
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
// Skip directories named _errors and shell
|
|
79
|
+
if (item.isDirectory() && (item.name === '_errors' || item.name === 'shell')) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const itemPath = join(dir, item.name);
|
|
83
|
+
if (item.isDirectory()) {
|
|
84
|
+
const subEntries = await walkFrontendPages(itemPath, baseDir);
|
|
85
|
+
entries.push(...subEntries);
|
|
86
|
+
}
|
|
87
|
+
else if (item.name.endsWith('.tsx')) {
|
|
88
|
+
const relPath = relative(baseDir, itemPath);
|
|
89
|
+
const id = relPath.slice(0, -4);
|
|
90
|
+
entries.push({
|
|
91
|
+
id,
|
|
92
|
+
route: null,
|
|
93
|
+
tenancy: null,
|
|
94
|
+
title: null,
|
|
95
|
+
tier: 3,
|
|
96
|
+
filePath: itemPath,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return entries;
|
|
101
|
+
}
|
|
102
|
+
export async function indexPages(opts) {
|
|
103
|
+
const tier1 = await readPageJsonDir(opts.pagesDir);
|
|
104
|
+
const tier2 = await readCustomComponentsDir(opts.customDir);
|
|
105
|
+
const tier3 = await walkFrontendPages(opts.frontendPagesDir, opts.frontendPagesDir);
|
|
106
|
+
return [...tier1, ...tier2, ...tier3];
|
|
107
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
export async function runBuildUi(opts) {
|
|
6
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
7
|
+
const components = await scanComponents({
|
|
8
|
+
uiDir: join(projectRoot, 'frontend/src/components/ui'),
|
|
9
|
+
customDir: join(projectRoot, 'frontend/src/components/custom'),
|
|
10
|
+
});
|
|
11
|
+
const componentManifestPath = join(projectRoot, '.mc/component-manifest.json');
|
|
12
|
+
await mkdir(join(componentManifestPath, '..'), { recursive: true });
|
|
13
|
+
await writeFile(componentManifestPath, JSON.stringify(components, null, 2), 'utf8');
|
|
14
|
+
const pages = await indexPages({
|
|
15
|
+
pagesDir: join(projectRoot, 'pages'),
|
|
16
|
+
customDir: join(projectRoot, 'frontend/src/components/custom'),
|
|
17
|
+
frontendPagesDir: join(projectRoot, 'frontend/src/pages'),
|
|
18
|
+
});
|
|
19
|
+
const pageRegistryPath = join(projectRoot, '.mc/page-manifest.json');
|
|
20
|
+
await mkdir(join(pageRegistryPath, '..'), { recursive: true });
|
|
21
|
+
await writeFile(pageRegistryPath, JSON.stringify(pages, null, 2), 'utf8');
|
|
22
|
+
process.stderr.write(`[build-ui] Wrote ${components.length} components and ${pages.length} pages.\n`);
|
|
23
|
+
}
|
|
@@ -19,7 +19,7 @@ function camelCase(s) {
|
|
|
19
19
|
export function apiSkeletonTemplate(domainId, apiId, tenancy) {
|
|
20
20
|
const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
|
|
21
21
|
return `import { z } from 'zod';
|
|
22
|
-
import { defineApi } from '
|
|
22
|
+
import { defineApi } from '@mettlecast/domain-runtime';
|
|
23
23
|
|
|
24
24
|
export const ${camelCase(apiId)} = defineApi({
|
|
25
25
|
id: '${apiId}',
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export function eventsSkeletonTemplate(domainId) {
|
|
10
10
|
return `import { z } from 'zod';
|
|
11
|
-
import { defineEvent } from '
|
|
11
|
+
import { defineEvent } from '@mettlecast/domain-runtime';
|
|
12
12
|
|
|
13
13
|
export const exampleEvent = defineEvent({
|
|
14
14
|
id: '${domainId}.example',
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @returns TypeScript source code as a string.
|
|
10
10
|
*/
|
|
11
11
|
export function subscriberSkeletonTemplate(domainId, subscriberId, eventId) {
|
|
12
|
-
return `import { defineSubscriber } from '
|
|
12
|
+
return `import { defineSubscriber } from '@mettlecast/domain-runtime';
|
|
13
13
|
|
|
14
14
|
export const ${subscriberId.replace(/-([a-z])/g, (_, c) => c.toUpperCase())} = defineSubscriber({
|
|
15
15
|
id: '${domainId}.${subscriberId}',
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { runPowerTune } from './commands/power-tune.js';
|
|
|
30
30
|
import { runReseedPage } from './commands/reseed-page.js';
|
|
31
31
|
import { runAddSeedPage } from './commands/add-seed-page.js';
|
|
32
32
|
import { runShowDns } from './commands/show-dns.js';
|
|
33
|
+
import { runBuildUi } from './commands/build-ui.js';
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Root CLI program for the TIB Domain Module local developer toolchain.
|
|
@@ -198,7 +199,7 @@ program
|
|
|
198
199
|
|
|
199
200
|
program
|
|
200
201
|
.command('add-page <pageId>')
|
|
201
|
-
.description('Scaffold a
|
|
202
|
+
.description('Scaffold a .page.json definition (default) or a pattern-based .tsx page component (--pattern)')
|
|
202
203
|
.option('--output <dir>', 'Override the output directory (default: src/pages/<category>)')
|
|
203
204
|
.option('--pattern <pattern>', 'Scaffold with a page pattern template (list, detail, form, wizard, dashboard, settings)')
|
|
204
205
|
.option('--layout <layout>', 'Layout to use: app (default) or auth', 'app')
|
|
@@ -380,6 +381,14 @@ program
|
|
|
380
381
|
await runShowDns(opts);
|
|
381
382
|
});
|
|
382
383
|
|
|
384
|
+
program
|
|
385
|
+
.command('build-ui')
|
|
386
|
+
.description('Scan frontend components and page definitions, write .mc/component-manifest.json and .mc/page-registry.json')
|
|
387
|
+
.option('--project-root <path>', 'Root of the project (defaults to cwd)')
|
|
388
|
+
.action(async (opts: { projectRoot?: string }) => {
|
|
389
|
+
await runBuildUi({ projectRoot: opts.projectRoot });
|
|
390
|
+
});
|
|
391
|
+
|
|
383
392
|
program.parseAsync(process.argv).catch((err: unknown) => {
|
|
384
393
|
// eslint-disable-next-line no-console
|
|
385
394
|
console.error(err);
|