Skip to content

Commit 4924a9a

Browse files
SoliEstreclaude
andcommitted
feat: create-estreuv scaffolding CLI (pure + pair modes)
packages/create-estreuv — npm create estreuv my-app: - bin/estreuv.js: commander CLI (init/dev/update/add/remove, default->init, estreui-style arg routing) - init: mode select (pure | pair), --pure/--pair/--no-install flags - pure: self-contained templates/pure (import map + tiles, no build) - pair: delegates EstreUI shell to create-estreui, then applies EstreUV overlay (estreuv-tiles.js + import map injection + deps + ESTREUV-PAIR.md handler snippet) - dev: reused from create-estreui verbatim (zero drift, PM 008 R5) - update/add/remove: pure=npm + import-map guidance, pair=delegate - templates/pure + templates/pair-overlay - test/smoke.mjs: module load + pure scaffold E2E (--no-install) - CI: create-estreuv smoke step Design: max-share with create-estreui (depend, not fork); common scaffold core is a later follow-up (PM 008 R5). Pure mode E2E verified (all smoke checks pass). create-estreuv stays 0.1.0 for first publish (no changeset — changesets only bumps). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d6424b2 commit 4924a9a

18 files changed

Lines changed: 786 additions & 54 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ jobs:
2222
run: npm run measure
2323
- name: Type declarations (D4 — .d.ts emit must stay green)
2424
run: npm run types --workspace=packages/estreuv
25+
- name: create-estreuv smoke test
26+
run: npm run test --workspace=packages/create-estreuv

package-lock.json

Lines changed: 19 additions & 54 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/create-estreuv/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# create-estreuv
2+
3+
Scaffolding tool for [EstreUV.js](https://github.com/SoliEstre/EstreUV.js) projects.
4+
5+
```sh
6+
npm create estreuv my-app
7+
# or
8+
npm create estreuv my-app -- --pure # pure EstreUV app
9+
npm create estreuv my-app -- --pair # EstreUI + EstreUV pair app
10+
```
11+
12+
## Project types
13+
14+
| Type | What you get |
15+
| --- | --- |
16+
| **pure** | Minimal EstreUV app — import map (lit + estreuv), tile demo, `estreuv dev` server. No build, no EstreUI. |
17+
| **pair** | An EstreUI app (scaffolded via `create-estreui`) + the EstreUV overlay (import map, `scripts/estreuv-tiles.js`, deps) and `ESTREUV-PAIR.md` with the one page-handler wiring step. |
18+
19+
## CLI (`estreuv`)
20+
21+
The package also installs an `estreuv` bin in scaffolded projects:
22+
23+
| Command | Behavior |
24+
| --- | --- |
25+
| `estreuv dev` | HTTPS dev server (shared with `create-estreui` — mkcert/openssl auto-cert). |
26+
| `estreuv update` | `npm update estreuv lit @lit/context`; a pair app also refreshes the EstreUI shell. |
27+
| `estreuv add <pkg>` | Pure: `npm install` + import-map guidance. Pair: delegates to `create-estreui` (vendored). |
28+
| `estreuv remove <pkg>` | Symmetric to `add`. |
29+
30+
## Design
31+
32+
`dev` is reused from `create-estreui` verbatim (zero EstreUI-specific logic →
33+
no fork drift). Pair mode delegates the EstreUI shell to `create-estreui`
34+
rather than duplicating its vendored-asset logic (PM 008 R5: maximize
35+
sharing; a common scaffold core is a later follow-up).
36+
37+
## License
38+
39+
MIT © SoliEstre
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env node
2+
3+
const { Command } = require('commander');
4+
const program = new Command();
5+
const packageJson = require('../package.json');
6+
7+
const initCommand = require('../lib/commands/init');
8+
const devCommand = require('../lib/commands/dev');
9+
const updateCommand = require('../lib/commands/update');
10+
const addCommand = require('../lib/commands/add');
11+
const removeCommand = require('../lib/commands/remove');
12+
13+
program
14+
.name('estreuv')
15+
.description('CLI for EstreUV.js — micro-Rimwork (Lit class primitive), sister to EstreUI.js')
16+
.version(packageJson.version);
17+
18+
program.addCommand(initCommand);
19+
program.addCommand(devCommand);
20+
program.addCommand(updateCommand);
21+
program.addCommand(addCommand);
22+
program.addCommand(removeCommand);
23+
24+
// If no args, default to init
25+
const args = process.argv.slice(2);
26+
if (!args.length) {
27+
program.parse([...process.argv, 'init']);
28+
} else {
29+
const knownCommands = ['init', 'update', 'dev', 'add', 'remove', 'help'];
30+
const firstArg = args[0];
31+
32+
// If first arg is not a known command and not a flag, treat it as a project name for init
33+
if (!knownCommands.includes(firstArg) && !firstArg.startsWith('-')) {
34+
program.parse([...process.argv.slice(0, 2), 'init', ...args]);
35+
} else {
36+
program.parse(process.argv);
37+
}
38+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const { Command } = require('commander');
2+
const { execSync } = require('child_process');
3+
const path = require('path');
4+
const fs = require('fs');
5+
6+
const program = new Command('add');
7+
8+
function isPairProject(cwd) {
9+
return fs.existsSync(path.join(cwd, 'scripts/estreUi-main.js'))
10+
|| fs.existsSync(path.join(cwd, 'scripts/estreUi-core.js'));
11+
}
12+
13+
program
14+
.description('Add a frontend package (npm install; pair app vendors it via create-estreui)')
15+
.argument('<package>', 'Package name')
16+
.action(async (packageName) => {
17+
const cwd = process.cwd();
18+
try {
19+
if (isPairProject(cwd)) {
20+
// Pair app uses the EstreUI vendored model — delegate.
21+
const bin = require.resolve('create-estreui/bin/estreui.js');
22+
execSync(`node "${bin}" add ${packageName}`, { cwd, stdio: 'inherit' });
23+
return;
24+
}
25+
// Pure EstreUV app: npm install + import-map guidance (ESM path
26+
// varies per package, so we don't guess the map entry).
27+
console.log(`Installing ${packageName}...`);
28+
execSync(`npm install ${packageName}`, { cwd, stdio: 'inherit' });
29+
console.log(`✓ Installed ${packageName}`);
30+
console.log(`\nNext: add an import-map entry in index.html, e.g.`);
31+
console.log(` "${packageName}": "/node_modules/${packageName}/<esm-entry>.js"`);
32+
console.log(`then import it from scripts/tiles.js.`);
33+
} catch (err) {
34+
console.error('Error adding package:', err.message);
35+
process.exit(1);
36+
}
37+
});
38+
39+
module.exports = program;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// `estreuv dev` — reuse create-estreui's dev server verbatim.
2+
//
3+
// The dev command in create-estreui is a generic HTTPS static server
4+
// (mkcert/openssl auto-cert, SPA-friendly, Service-Worker-Allowed header).
5+
// It has zero EstreUI-specific logic, so create-estreuv shares it directly
6+
// rather than forking — minimizes drift (PM 008 R5). Works for both the
7+
// pure EstreUV importmap app and the EstreUI + EstreUV pair app.
8+
9+
let devCommand;
10+
try {
11+
devCommand = require('create-estreui/lib/commands/dev');
12+
} catch (e) {
13+
const { Command } = require('commander');
14+
devCommand = new Command('dev')
15+
.description('Start local HTTPS development server')
16+
.action(() => {
17+
console.error('❌ `create-estreui` is required for `estreuv dev` but could not be loaded.');
18+
console.error(' Install it: npm i -D create-estreui');
19+
console.error(' (' + e.message + ')');
20+
process.exit(1);
21+
});
22+
}
23+
24+
module.exports = devCommand;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
const { Command } = require('commander');
2+
const inquirer = require('inquirer');
3+
const path = require('path');
4+
const fs = require('fs');
5+
const { copyDir, writeProjectPackageJson, applyEstreuvOverlay, runCreateEstreui } = require('../utils');
6+
7+
const program = new Command('init');
8+
9+
program
10+
.description('Initialize a new EstreUV.js project')
11+
.argument('[project-name]', 'Project name')
12+
.option('--pure', 'Pure EstreUV app (no EstreUI)')
13+
.option('--pair', 'EstreUI + EstreUV pair app')
14+
.option('--no-install', 'Skip npm install')
15+
.action(async (projectName, options) => {
16+
// 1. Project name
17+
let { name } = { name: projectName };
18+
if (!name) {
19+
({ name } = await inquirer.prompt([{
20+
type: 'input', name: 'name',
21+
message: 'Project name:', default: 'my-estreuv-app'
22+
}]));
23+
}
24+
25+
// 2. Mode
26+
let mode = options.pure ? 'pure' : options.pair ? 'pair' : null;
27+
if (!mode) {
28+
({ mode } = await inquirer.prompt([{
29+
type: 'list', name: 'mode', message: 'Project type:',
30+
choices: [
31+
{ name: 'Pure EstreUV app (Lit micro-Rimwork, no build)', value: 'pure' },
32+
{ name: 'EstreUI + EstreUV pair app (EstreUI shell + EstreUV tiles)', value: 'pair' }
33+
],
34+
default: 'pure'
35+
}]));
36+
}
37+
38+
const projectPath = path.resolve(process.cwd(), name === '.' ? '' : name);
39+
const pkgName = name === '.' ? path.basename(process.cwd()) : name;
40+
41+
try {
42+
if (mode === 'pure') {
43+
await scaffoldPure(projectPath, pkgName, options.install);
44+
} else {
45+
await scaffoldPair(projectPath, pkgName, name, options.install);
46+
}
47+
console.log('\n✓ Project initialized.');
48+
console.log(`\n cd ${name}\n npm run dev\n`);
49+
} catch (error) {
50+
console.error('Error initializing project:', error);
51+
process.exit(1);
52+
}
53+
});
54+
55+
async function scaffoldPure(projectPath, pkgName, install) {
56+
console.log(`Initializing pure EstreUV app in ${projectPath}...`);
57+
fs.mkdirSync(projectPath, { recursive: true });
58+
59+
const templateDir = path.resolve(__dirname, '../../templates/pure');
60+
await copyDir(templateDir, projectPath);
61+
console.log('✓ Copied template');
62+
63+
writeProjectPackageJson(projectPath, pkgName, { pair: false });
64+
console.log('✓ Created package.json');
65+
66+
if (install) require('../utils').installDependencies(projectPath);
67+
}
68+
69+
async function scaffoldPair(projectPath, pkgName, rawName, install) {
70+
console.log(`Initializing EstreUI + EstreUV pair app in ${projectPath}...`);
71+
72+
// Delegate the EstreUI shell to create-estreui (max share — PM 008 R5).
73+
runCreateEstreui(rawName);
74+
75+
if (!fs.existsSync(projectPath)) {
76+
throw new Error('create-estreui did not produce the project directory. Aborting overlay.');
77+
}
78+
79+
// Apply the EstreUV overlay on top of the EstreUI scaffold.
80+
const overlayDir = path.resolve(__dirname, '../../templates/pair-overlay');
81+
applyEstreuvOverlay(projectPath, overlayDir);
82+
83+
writeProjectPackageJson(projectPath, pkgName, { pair: true });
84+
console.log('✓ Added estreuv / lit / @lit/context to package.json');
85+
86+
if (install) require('../utils').installDependencies(projectPath);
87+
console.log('✓ See ESTREUV-PAIR.md for the one manual page-handler step.');
88+
}
89+
90+
module.exports = program;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const { Command } = require('commander');
2+
const { execSync } = require('child_process');
3+
const path = require('path');
4+
const fs = require('fs');
5+
6+
const program = new Command('remove');
7+
8+
function isPairProject(cwd) {
9+
return fs.existsSync(path.join(cwd, 'scripts/estreUi-main.js'))
10+
|| fs.existsSync(path.join(cwd, 'scripts/estreUi-core.js'));
11+
}
12+
13+
program
14+
.description('Remove a frontend package (npm uninstall; pair app delegates to create-estreui)')
15+
.argument('<package>', 'Package name')
16+
.action(async (packageName) => {
17+
const cwd = process.cwd();
18+
try {
19+
if (isPairProject(cwd)) {
20+
const bin = require.resolve('create-estreui/bin/estreui.js');
21+
execSync(`node "${bin}" remove ${packageName}`, { cwd, stdio: 'inherit' });
22+
return;
23+
}
24+
console.log(`Uninstalling ${packageName}...`);
25+
execSync(`npm uninstall ${packageName}`, { cwd, stdio: 'inherit' });
26+
console.log(`✓ Removed ${packageName}`);
27+
console.log(`\nRemember to remove its import-map entry from index.html and any imports.`);
28+
} catch (err) {
29+
console.error('Error removing package:', err.message);
30+
process.exit(1);
31+
}
32+
});
33+
34+
module.exports = program;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const { Command } = require('commander');
2+
const { execSync } = require('child_process');
3+
const path = require('path');
4+
const fs = require('fs');
5+
6+
const program = new Command('update');
7+
8+
function isPairProject(cwd) {
9+
// create-estreui scaffolds vendored estreui core assets.
10+
return fs.existsSync(path.join(cwd, 'scripts/estreUi-main.js'))
11+
|| fs.existsSync(path.join(cwd, 'scripts/estreUi-core.js'));
12+
}
13+
14+
program
15+
.description('Update EstreUV (and, for a pair app, the EstreUI shell) to latest')
16+
.action(async () => {
17+
const cwd = process.cwd();
18+
try {
19+
console.log('🔄 Updating estreuv / lit / @lit/context...');
20+
execSync('npm update estreuv lit @lit/context', { cwd, stdio: 'inherit' });
21+
22+
if (isPairProject(cwd)) {
23+
console.log('🔄 Pair app detected — refreshing EstreUI shell via create-estreui...');
24+
const bin = require.resolve('create-estreui/bin/estreui.js');
25+
execSync(`node "${bin}" update`, { cwd, stdio: 'inherit' });
26+
}
27+
console.log('🎉 Update complete.');
28+
} catch (err) {
29+
console.error('❌ Update failed:', err.message);
30+
process.exit(1);
31+
}
32+
});
33+
34+
module.exports = program;

0 commit comments

Comments
 (0)