create-lego-box 0.1.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/README.md +70 -0
- package/index.js +169 -0
- package/package.json +31 -0
- package/templates/packages/create-pilet/index.js +200 -0
- package/templates/packages/create-pilet/package.json +14 -0
- package/templates/packages/create-pilet/templates/.krasrc +1 -0
- package/templates/packages/create-pilet/templates/package.json +1 -0
- package/templates/packages/create-pilet/templates/src/index.tsx +85 -0
- package/templates/packages/create-pilet/templates/src/pages/Test1Page.tsx +23 -0
- package/templates/packages/create-pilet/templates/src/pages/Test2Page.tsx +22 -0
- package/templates/packages/create-pilet/templates/src/pages/demo.tsx +220 -0
- package/templates/packages/create-pilet/templates/src/pages/home.tsx +20 -0
- package/templates/packages/create-pilet/templates/tailwind.config.js +54 -0
- package/templates/packages/create-pilet/templates/tsconfig.json +1 -0
- package/templates/packages/create-pilet/templates/webpack.config.js +34 -0
- package/templates/packages/create-pilet/templates.js +117 -0
- package/templates/packages/ui-kit/package.json +42 -0
- package/templates/packages/ui-kit/postcss.config.js +6 -0
- package/templates/packages/ui-kit/src/components/example-custom-component.tsx +21 -0
- package/templates/packages/ui-kit/src/components/index.ts +6 -0
- package/templates/packages/ui-kit/src/index.css +75 -0
- package/templates/packages/ui-kit/src/index.ts +10 -0
- package/templates/packages/ui-kit/tailwind.config.js +26 -0
- package/templates/packages/ui-kit/tsconfig.json +8 -0
- package/templates/packages/ui-kit/tsup.config.ts +15 -0
- package/templates/pilets/my-pilet/.krasrc +1 -0
- package/templates/pilets/my-pilet/package.json +32 -0
- package/templates/pilets/my-pilet/src/index.tsx +85 -0
- package/templates/pilets/my-pilet/src/pages/Test1Page.tsx +23 -0
- package/templates/pilets/my-pilet/src/pages/Test2Page.tsx +25 -0
- package/templates/pilets/my-pilet/src/pages/demo.tsx +220 -0
- package/templates/pilets/my-pilet/src/pages/home.tsx +20 -0
- package/templates/pilets/my-pilet/tailwind.config.js +26 -0
- package/templates/pilets/my-pilet/tsconfig.json +1 -0
- package/templates/pilets/my-pilet/webpack.config.js +27 -0
- package/templates/root/env.example +5 -0
- package/templates/root/package.json +19 -0
- package/templates/root/pnpm-workspace.yaml +3 -0
- package/templates/root/tsconfig.base.json +17 -0
- package/templates/scripts/dev-multi-pilet.js +97 -0
- package/templates/scripts/discover-pilets.js +76 -0
- package/templates/scripts/run-shell.js +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# create-lego-box
|
|
2
|
+
|
|
3
|
+
Scaffolds a new Lego Box microfrontend app with ui-kit, pilets, and shell from npm.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
### From npm (recommended for external developers)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm create lego-box
|
|
11
|
+
# or
|
|
12
|
+
npm create lego-box
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### From monorepo (local development)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm create:app
|
|
19
|
+
# or
|
|
20
|
+
node packages/create-lego-box/index.js
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### With app name
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm create lego-box my-awesome-app
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## What it creates
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
/my-app
|
|
33
|
+
package.json
|
|
34
|
+
pnpm-workspace.yaml
|
|
35
|
+
tsconfig.base.json
|
|
36
|
+
.env.example
|
|
37
|
+
scripts/
|
|
38
|
+
dev-multi-pilet.js
|
|
39
|
+
discover-pilets.js
|
|
40
|
+
run-shell.js
|
|
41
|
+
packages/
|
|
42
|
+
ui-kit/ # Extensible – re-exports @lego-box/ui-kit, add custom components here
|
|
43
|
+
create-pilet/ # CLI to scaffold new pilets
|
|
44
|
+
pilets/
|
|
45
|
+
my-pilet/ # Initial pilet
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Architecture
|
|
49
|
+
|
|
50
|
+
- **Shell**: `@lego-box/shell` from npm (maintained by Lego Box team)
|
|
51
|
+
- **ui-kit**: Local workspace package – extends `@lego-box/ui-kit`, add your components
|
|
52
|
+
- **Pilets**: Create with `pnpm create:pilet` – auto-saved to `pilets/`
|
|
53
|
+
|
|
54
|
+
## Next steps after creation
|
|
55
|
+
|
|
56
|
+
1. `cd my-app`
|
|
57
|
+
2. `pnpm install` (if not run automatically)
|
|
58
|
+
3. Start PocketBase: `pb serve` (or your PocketBase setup)
|
|
59
|
+
4. `pnpm dev` – runs shell + ui-kit + pilets
|
|
60
|
+
5. Add pilets: `pnpm create:pilet`
|
|
61
|
+
6. Extend ui-kit: edit `packages/ui-kit/src/components/`
|
|
62
|
+
|
|
63
|
+
## Publishing
|
|
64
|
+
|
|
65
|
+
To publish to npm for `pnpm create lego-box`:
|
|
66
|
+
|
|
67
|
+
1. Add to changesets
|
|
68
|
+
2. Publish as `create-lego-box` (unscoped) or `@lego-box/create` (scoped)
|
|
69
|
+
|
|
70
|
+
The `create-*` convention: `pnpm create foo` runs the `create-foo` package.
|
package/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import prompts from 'prompts';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import fse from 'fs-extra';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const TEMPLATES_DIR = path.resolve(__dirname, 'templates');
|
|
13
|
+
|
|
14
|
+
function isValidAppName(name) {
|
|
15
|
+
if (!name || typeof name !== 'string') return false;
|
|
16
|
+
const sanitized = name.toLowerCase().replace(/\s+/g, '-');
|
|
17
|
+
return /^[a-z0-9][a-z0-9._-]*$/.test(sanitized);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sanitizeAppName(name) {
|
|
21
|
+
return name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9._-]/g, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getAllFiles(dir, base = '') {
|
|
25
|
+
const entries = fs.readdirSync(path.join(dir, base), { withFileTypes: true });
|
|
26
|
+
const files = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
31
|
+
files.push(...getAllFiles(dir, rel));
|
|
32
|
+
} else {
|
|
33
|
+
files.push(rel);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return files;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function processContent(content, variables) {
|
|
40
|
+
let result = content;
|
|
41
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
42
|
+
if (value !== undefined && value !== null) {
|
|
43
|
+
result = result.replace(new RegExp(`{{${key}}}`, 'g'), String(value));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function main() {
|
|
50
|
+
const args = process.argv.slice(2).filter((a) => !a.startsWith('-'));
|
|
51
|
+
const appNameArg = args[0];
|
|
52
|
+
const suggestedName = appNameArg ? sanitizeAppName(appNameArg) : 'my-app';
|
|
53
|
+
const isNonInteractive = process.argv.includes('--yes') || process.argv.includes('-y');
|
|
54
|
+
|
|
55
|
+
console.log(chalk.cyan('\n create-lego-box – Scaffold a new Lego Box microfrontend app\n'));
|
|
56
|
+
console.log(chalk.gray('Creates: app structure with ui-kit, pilets, and shell from npm.\n'));
|
|
57
|
+
|
|
58
|
+
const response = isNonInteractive
|
|
59
|
+
? { appName: suggestedName, runInstall: !process.argv.includes('--no-install') }
|
|
60
|
+
: await prompts([
|
|
61
|
+
{
|
|
62
|
+
type: 'text',
|
|
63
|
+
name: 'appName',
|
|
64
|
+
message: 'App name:',
|
|
65
|
+
initial: suggestedName,
|
|
66
|
+
validate: (v) => (isValidAppName(v || suggestedName) ? true : 'Use lowercase, no spaces (e.g. my-app)'),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: 'confirm',
|
|
70
|
+
name: 'runInstall',
|
|
71
|
+
message: 'Run pnpm install after creating the app?',
|
|
72
|
+
initial: true,
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
if (Object.keys(response).length === 0) {
|
|
77
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const appName = sanitizeAppName(response.appName || suggestedName) || 'my-app';
|
|
82
|
+
const runInstall = response.runInstall ?? true;
|
|
83
|
+
|
|
84
|
+
const targetDir = path.resolve(process.cwd(), appName);
|
|
85
|
+
|
|
86
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
87
|
+
const { overwrite } = await prompts({
|
|
88
|
+
type: 'confirm',
|
|
89
|
+
name: 'overwrite',
|
|
90
|
+
message: `Directory ${targetDir} already exists. Overwrite?`,
|
|
91
|
+
initial: false,
|
|
92
|
+
});
|
|
93
|
+
if (!overwrite) {
|
|
94
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
fse.removeSync(targetDir);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const variables = { APP_NAME: appName };
|
|
101
|
+
|
|
102
|
+
console.log(chalk.gray(`Creating app at ${targetDir}...`));
|
|
103
|
+
|
|
104
|
+
// Root files
|
|
105
|
+
const rootFiles = ['package.json', 'pnpm-workspace.yaml', 'tsconfig.base.json', 'env.example'];
|
|
106
|
+
for (const file of rootFiles) {
|
|
107
|
+
const srcPath = path.join(TEMPLATES_DIR, 'root', file);
|
|
108
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
109
|
+
const destPath = path.join(targetDir, file === 'env.example' ? '.env.example' : file);
|
|
110
|
+
fse.ensureDirSync(path.dirname(destPath));
|
|
111
|
+
let content = fs.readFileSync(srcPath, 'utf-8');
|
|
112
|
+
content = processContent(content, variables);
|
|
113
|
+
fs.writeFileSync(destPath, content, 'utf-8');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Scripts
|
|
117
|
+
const scriptsDir = path.join(TEMPLATES_DIR, 'scripts');
|
|
118
|
+
const scriptFiles = fs.readdirSync(scriptsDir);
|
|
119
|
+
const scriptsDest = path.join(targetDir, 'scripts');
|
|
120
|
+
fse.ensureDirSync(scriptsDest);
|
|
121
|
+
for (const file of scriptFiles) {
|
|
122
|
+
const content = fs.readFileSync(path.join(scriptsDir, file), 'utf-8');
|
|
123
|
+
fs.writeFileSync(path.join(scriptsDest, file), content, 'utf-8');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// packages/ui-kit
|
|
127
|
+
const uiKitSrc = path.join(TEMPLATES_DIR, 'packages', 'ui-kit');
|
|
128
|
+
const uiKitDest = path.join(targetDir, 'packages', 'ui-kit');
|
|
129
|
+
fse.copySync(uiKitSrc, uiKitDest);
|
|
130
|
+
|
|
131
|
+
// packages/create-pilet
|
|
132
|
+
const createPiletSrc = path.join(TEMPLATES_DIR, 'packages', 'create-pilet');
|
|
133
|
+
const createPiletDest = path.join(targetDir, 'packages', 'create-pilet');
|
|
134
|
+
fse.copySync(createPiletSrc, createPiletDest);
|
|
135
|
+
|
|
136
|
+
// pilets/my-pilet
|
|
137
|
+
const piletSrc = path.join(TEMPLATES_DIR, 'pilets', 'my-pilet');
|
|
138
|
+
const piletDest = path.join(targetDir, 'pilets', 'my-pilet');
|
|
139
|
+
fse.copySync(piletSrc, piletDest);
|
|
140
|
+
|
|
141
|
+
if (runInstall) {
|
|
142
|
+
console.log(chalk.gray('Running pnpm install...'));
|
|
143
|
+
try {
|
|
144
|
+
execSync('pnpm install', {
|
|
145
|
+
cwd: targetDir,
|
|
146
|
+
stdio: 'inherit',
|
|
147
|
+
});
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.warn(chalk.yellow('pnpm install failed. Run it manually from the app directory.'));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log(chalk.green('\n App created successfully!\n'));
|
|
154
|
+
console.log(chalk.white('Next steps:'));
|
|
155
|
+
console.log(chalk.cyan(` cd ${appName}`));
|
|
156
|
+
if (!runInstall) {
|
|
157
|
+
console.log(chalk.cyan(' pnpm install'));
|
|
158
|
+
}
|
|
159
|
+
console.log(chalk.cyan(' pnpm dev'));
|
|
160
|
+
console.log(chalk.gray('\nStart PocketBase (e.g. pb serve) for full functionality.'));
|
|
161
|
+
console.log(chalk.gray('Add pilets with: pnpm create:pilet'));
|
|
162
|
+
console.log(chalk.gray('Extend ui-kit in packages/ui-kit/src/components/'));
|
|
163
|
+
console.log('');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
main().catch((err) => {
|
|
167
|
+
console.error(chalk.red('Error:'), err.message);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-lego-box",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a new Lego Box microfrontend app with ui-kit and pilets",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-lego-box": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"lego-box",
|
|
15
|
+
"piral",
|
|
16
|
+
"microfrontend",
|
|
17
|
+
"scaffold",
|
|
18
|
+
"create"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/lego-box/lego-box.git",
|
|
24
|
+
"directory": "packages/create-lego-box"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"fs-extra": "^11.2.0",
|
|
29
|
+
"prompts": "^2.4.2"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import prompts from 'prompts';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import fse from 'fs-extra';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { getTemplateFiles, generateFileContent } from './templates.js';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const WORKSPACE_ROOT = path.resolve(__dirname, '../..');
|
|
14
|
+
const PILETS_DIR = path.join(WORKSPACE_ROOT, 'pilets');
|
|
15
|
+
|
|
16
|
+
function isValidPiletName(name) {
|
|
17
|
+
if (!name || typeof name !== 'string') return false;
|
|
18
|
+
const sanitized = name.toLowerCase().replace(/\s+/g, '-');
|
|
19
|
+
return /^[a-z0-9][a-z0-9._-]*$/.test(sanitized);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sanitizePiletName(name) {
|
|
23
|
+
return name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9._-]/g, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getNextAvailablePort() {
|
|
27
|
+
if (!fs.existsSync(PILETS_DIR)) return 9000;
|
|
28
|
+
const entries = fs.readdirSync(PILETS_DIR, { withFileTypes: true });
|
|
29
|
+
const usedPorts = new Set([9000]);
|
|
30
|
+
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
const krasrcPath = path.join(PILETS_DIR, entry.name, '.krasrc');
|
|
34
|
+
if (fs.existsSync(krasrcPath)) {
|
|
35
|
+
try {
|
|
36
|
+
const krasrc = JSON.parse(fs.readFileSync(krasrcPath, 'utf-8'));
|
|
37
|
+
const url = krasrc?.injectors?.pilet?.assetUrl || '';
|
|
38
|
+
const match = url.match(/localhost:(\d+)/);
|
|
39
|
+
if (match) usedPorts.add(parseInt(match[1], 10));
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let port = 9000;
|
|
46
|
+
while (usedPorts.has(port)) port++;
|
|
47
|
+
return port;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getShellDep() {
|
|
51
|
+
const shellPath = path.join(WORKSPACE_ROOT, 'packages', 'app-shell', 'package.json');
|
|
52
|
+
return fs.existsSync(shellPath) ? 'workspace:*' : '^1.0.0';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getUiKitDep() {
|
|
56
|
+
const uiKitPath = path.join(WORKSPACE_ROOT, 'packages', 'ui-kit', 'package.json');
|
|
57
|
+
return fs.existsSync(uiKitPath) ? 'workspace:*' : '^0.1.0';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
const args = process.argv.slice(2);
|
|
62
|
+
const targetDirArg = args[0];
|
|
63
|
+
const defaultName = targetDirArg ? path.basename(path.resolve(targetDirArg)) : 'my-pilet';
|
|
64
|
+
const suggestedName = sanitizePiletName(defaultName) || 'my-pilet';
|
|
65
|
+
const nextPort = getNextAvailablePort();
|
|
66
|
+
|
|
67
|
+
console.log(chalk.cyan('\n create-pilet – Scaffold a new Piral pilet\n'));
|
|
68
|
+
|
|
69
|
+
const response = await prompts([
|
|
70
|
+
{
|
|
71
|
+
type: 'text',
|
|
72
|
+
name: 'piletName',
|
|
73
|
+
message: 'Pilet name (package name):',
|
|
74
|
+
initial: suggestedName,
|
|
75
|
+
validate: (v) => (isValidPiletName(v || suggestedName) ? true : 'Use lowercase, no spaces (e.g. my-pilet)'),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: 'text',
|
|
79
|
+
name: 'description',
|
|
80
|
+
message: 'Description:',
|
|
81
|
+
initial: '',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: 'text',
|
|
85
|
+
name: 'author',
|
|
86
|
+
message: 'Author:',
|
|
87
|
+
initial: '',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: 'select',
|
|
91
|
+
name: 'mode',
|
|
92
|
+
message: 'Package mode:',
|
|
93
|
+
choices: [
|
|
94
|
+
{ title: 'Workspace (monorepo development, workspace:* deps)', value: 'workspace' },
|
|
95
|
+
{ title: 'Published (external use, versioned deps)', value: 'published' },
|
|
96
|
+
],
|
|
97
|
+
initial: 0,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'number',
|
|
101
|
+
name: 'port',
|
|
102
|
+
message: 'Development port:',
|
|
103
|
+
initial: nextPort,
|
|
104
|
+
min: 1024,
|
|
105
|
+
max: 65535,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: 'confirm',
|
|
109
|
+
name: 'runInstall',
|
|
110
|
+
message: 'Run pnpm install after creating the pilet?',
|
|
111
|
+
initial: true,
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
if (Object.keys(response).length === 0) {
|
|
116
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const piletName = sanitizePiletName(response.piletName || suggestedName) || 'my-pilet';
|
|
121
|
+
const description = response.description || '';
|
|
122
|
+
const author = response.author || '';
|
|
123
|
+
const mode = response.mode || 'workspace';
|
|
124
|
+
const port = response.port ?? nextPort;
|
|
125
|
+
const runInstall = response.runInstall ?? true;
|
|
126
|
+
|
|
127
|
+
const isMonorepo = fs.existsSync(path.join(WORKSPACE_ROOT, 'pnpm-workspace.yaml'));
|
|
128
|
+
const targetDir = isMonorepo
|
|
129
|
+
? path.join(PILETS_DIR, piletName)
|
|
130
|
+
: targetDirArg
|
|
131
|
+
? path.resolve(process.cwd(), targetDirArg)
|
|
132
|
+
: path.resolve(process.cwd(), piletName);
|
|
133
|
+
|
|
134
|
+
const shellDep = mode === 'workspace' ? getShellDep() : '^1.0.0';
|
|
135
|
+
const uiKitDep = mode === 'workspace' ? getUiKitDep() : '^0.1.0';
|
|
136
|
+
|
|
137
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
138
|
+
const { overwrite } = await prompts({
|
|
139
|
+
type: 'confirm',
|
|
140
|
+
name: 'overwrite',
|
|
141
|
+
message: `Directory ${targetDir} already exists. Overwrite?`,
|
|
142
|
+
initial: false,
|
|
143
|
+
});
|
|
144
|
+
if (!overwrite) {
|
|
145
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
fse.removeSync(targetDir);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const variables = {
|
|
152
|
+
PILET_NAME: piletName,
|
|
153
|
+
DESCRIPTION: description,
|
|
154
|
+
AUTHOR: author,
|
|
155
|
+
PORT: port,
|
|
156
|
+
SHELL_DEPENDENCY: shellDep,
|
|
157
|
+
UI_KIT_DEPENDENCY: uiKitDep,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
console.log(chalk.gray(`Creating pilet at ${targetDir}...`));
|
|
161
|
+
|
|
162
|
+
fse.ensureDirSync(path.join(targetDir, 'src', 'pages'));
|
|
163
|
+
|
|
164
|
+
const templateFiles = getTemplateFiles();
|
|
165
|
+
for (const templatePath of templateFiles) {
|
|
166
|
+
const content = generateFileContent(templatePath, variables, mode);
|
|
167
|
+
const destPath = path.join(targetDir, templatePath);
|
|
168
|
+
fse.ensureDirSync(path.dirname(destPath));
|
|
169
|
+
fs.writeFileSync(destPath, content, 'utf-8');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (runInstall) {
|
|
173
|
+
console.log(chalk.gray('Running pnpm install...'));
|
|
174
|
+
try {
|
|
175
|
+
execSync('pnpm install', {
|
|
176
|
+
cwd: isMonorepo ? WORKSPACE_ROOT : targetDir,
|
|
177
|
+
stdio: 'inherit',
|
|
178
|
+
});
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.warn(chalk.yellow('pnpm install failed. Run it manually.'));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(chalk.green('\n Pilet created successfully!\n'));
|
|
185
|
+
console.log(chalk.white('Next steps:'));
|
|
186
|
+
console.log(chalk.cyan(` cd ${path.relative(process.cwd(), targetDir) || targetDir}`));
|
|
187
|
+
if (isMonorepo) {
|
|
188
|
+
console.log(chalk.cyan(' pnpm dev'));
|
|
189
|
+
console.log(chalk.gray('\nRun from app root to start shell + pilets.'));
|
|
190
|
+
} else {
|
|
191
|
+
console.log(chalk.cyan(' pnpm start'));
|
|
192
|
+
console.log(chalk.gray('\nConfigure the feed URL in your shell to point to this pilet.'));
|
|
193
|
+
}
|
|
194
|
+
console.log('');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
main().catch((err) => {
|
|
198
|
+
console.error(chalk.red('Error:'), err.message);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"injectors":{"pilet":{"assetUrl":"http://localhost:9000"}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"pilet-demo","version":"1.0.0","private":true,"piral":{"name":"@lego-box/shell","importmap":{"inherit":["@lego-box/shell"]},"sharedDependencies":["@lego-box/ui-kit","piral","pocketbase","react","react-dom"]},"peerDependencies":{"react":"^18.2.0","react-dom":"^18.2.0"},"devDependencies":{"@lego-box/shell":"workspace:*","@lego-box/ui-kit":"workspace:*","@types/react":"^18.2.0","@types/react-dom":"^18.2.0","piral":"1.9.2","piral-cli":"1.9.2","piral-cli-webpack5":"^1.9.2","strip-ansi":"^6.0.1","react":"^18.2.0","react-dom":"^18.2.0","tailwindcss":"^3.4.0","typescript":"^5.3.3"},"scripts":{"start":"pilet debug","build":"pilet build"}}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { PiletApi } from '@lego-box/shell/pilet';
|
|
3
|
+
import { HomePage } from './pages/home';
|
|
4
|
+
import { DemoPage } from './pages/demo';
|
|
5
|
+
import { Test1Page } from './pages/Test1Page';
|
|
6
|
+
import { Test2Page } from './pages/Test2Page';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pilet demonstrating extended API, UI Kit, and dynamic menu registration.
|
|
10
|
+
* Shows getUser, getUserId, getUserType, test.getTesting(), and sidebar menu registration.
|
|
11
|
+
*/
|
|
12
|
+
export function setup(context: PiletApi) {
|
|
13
|
+
console.log('PocketBase plugin test:', context.test.getTesting());
|
|
14
|
+
|
|
15
|
+
// Register demo page with route-level permission guard (read:users)
|
|
16
|
+
context.registerProtectedPage(
|
|
17
|
+
'/demo',
|
|
18
|
+
() => <DemoPage api={context} />,
|
|
19
|
+
{ action: 'read', subject: 'demo' }
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Register test pages with route-level permission guards (read:audit_logs)
|
|
23
|
+
context.registerProtectedPage('/testing/test1', () => <Test1Page />, {
|
|
24
|
+
action: 'read',
|
|
25
|
+
subject: 'demo',
|
|
26
|
+
});
|
|
27
|
+
context.registerProtectedPage('/testing/test2', () => <Test2Page />, {
|
|
28
|
+
action: 'read',
|
|
29
|
+
subject: 'demo',
|
|
30
|
+
});
|
|
31
|
+
// Register a single-level menu item with permission guard (read:users)
|
|
32
|
+
context.registerSidebarMenu({
|
|
33
|
+
id: 'pilet-demo',
|
|
34
|
+
label: 'Pilet Demo',
|
|
35
|
+
href: '/demo',
|
|
36
|
+
action: 'read',
|
|
37
|
+
subject: 'demo',
|
|
38
|
+
icon: (
|
|
39
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
40
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
41
|
+
</svg>
|
|
42
|
+
),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Register a multilevel menu with permission guards (children require read:audit_logs)
|
|
46
|
+
context.registerSidebarMenu({
|
|
47
|
+
id: 'testing-menu',
|
|
48
|
+
label: 'Testing',
|
|
49
|
+
action: 'read',
|
|
50
|
+
subject: 'demo',
|
|
51
|
+
icon: (
|
|
52
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
53
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
|
54
|
+
</svg>
|
|
55
|
+
),
|
|
56
|
+
children: [
|
|
57
|
+
{
|
|
58
|
+
id: 'test-1',
|
|
59
|
+
label: 'Test 1',
|
|
60
|
+
href: '/testing/test1',
|
|
61
|
+
action: 'read',
|
|
62
|
+
subject: 'demo',
|
|
63
|
+
icon: (
|
|
64
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
65
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
66
|
+
</svg>
|
|
67
|
+
),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'test-2',
|
|
71
|
+
label: 'Test 2',
|
|
72
|
+
href: '/testing/test2',
|
|
73
|
+
action: 'read',
|
|
74
|
+
subject: 'demo',
|
|
75
|
+
icon: (
|
|
76
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
77
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
78
|
+
</svg>
|
|
79
|
+
),
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
console.log('[pilet-demo] Registered pages and sidebar menus');
|
|
85
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Card } from '@lego-box/ui-kit';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Test1Page - Sample page for "Test 1" menu item
|
|
6
|
+
* Demonstrates pilet-registered menu with working navigation
|
|
7
|
+
*/
|
|
8
|
+
export function Test1Page() {
|
|
9
|
+
return (
|
|
10
|
+
<Card title="Test 1 Page">
|
|
11
|
+
<div className="p-6">
|
|
12
|
+
<h1 className="text-2xl font-bold mb-4">Hello from Test 1 menu!</h1>
|
|
13
|
+
<p className="text-muted-foreground">
|
|
14
|
+
This page is registered by the pilet-demo pilet and accessible via the sidebar menu.
|
|
15
|
+
</p>
|
|
16
|
+
<p className="mt-4 text-muted-foreground">
|
|
17
|
+
The menu item was dynamically registered using{' '}
|
|
18
|
+
<code className="bg-muted px-2 py-1 rounded text-sm">context.registerSidebarMenu()</code>.
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
</Card>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Card } from '@lego-box/ui-kit';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Test2Page - Sample page for "Test 2" menu item
|
|
6
|
+
* Demonstrates pilet-registered menu with working navigation
|
|
7
|
+
*/
|
|
8
|
+
export function Test2Page() {
|
|
9
|
+
return (
|
|
10
|
+
<Card title="Test 2 Page">
|
|
11
|
+
<div className="p-6">
|
|
12
|
+
<h1 className="text-2xl font-bold mb-4">Hello from Test 2 menu!</h1>
|
|
13
|
+
<p className="text-muted-foreground">
|
|
14
|
+
This is another sample page registered by pilet-demo via the dynamic menu system.
|
|
15
|
+
</p>
|
|
16
|
+
<p className="mt-4 text-muted-foreground">
|
|
17
|
+
This page is part of a multilevel menu structure under "Testing" menu.
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
</Card>
|
|
21
|
+
);
|
|
22
|
+
}
|