uniweb 0.7.5 → 0.7.7
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 +46 -44
- package/package.json +5 -5
- package/src/commands/add.js +150 -37
- package/src/commands/doctor.js +2 -2
- package/src/index.js +19 -14
- package/src/utils/config.js +38 -17
- package/src/utils/pm.js +51 -0
- package/src/utils/workspace.js +47 -11
- package/templates/workspace/package.json.hbs +7 -0
package/README.md
CHANGED
|
@@ -7,59 +7,54 @@ Create well-structured Vite + React projects with file-based routing, localizati
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
11
|
-
cd my-site
|
|
12
|
-
pnpm install
|
|
13
|
-
pnpm dev
|
|
10
|
+
npm create uniweb
|
|
14
11
|
```
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
> **Need pnpm?** Run `npm install -g pnpm` or see [pnpm installation](https://pnpm.io/installation).
|
|
13
|
+
> **pnpm ready** — `pnpm create uniweb` works out of the box. Projects include both `pnpm-workspace.yaml` and npm workspaces.
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Run these from the **project root** (where `pnpm-workspace.yaml` is):
|
|
15
|
+
The interactive prompt asks for a project name and template. Pick one, then:
|
|
23
16
|
|
|
24
17
|
```bash
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
cd my-project
|
|
19
|
+
npm install
|
|
20
|
+
npm run dev
|
|
28
21
|
```
|
|
29
22
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
The `marketing` template includes real components (Hero, Features, Pricing, Testimonials, FAQ, and more) with sample content—a working site you can explore and modify.
|
|
33
|
-
|
|
34
|
-
**Other templates:**
|
|
23
|
+
Edit files in `site/pages/` and `foundation/src/sections/` to see changes instantly.
|
|
35
24
|
|
|
36
|
-
|
|
37
|
-
# Multilingual business site (English, Spanish, French)
|
|
38
|
-
pnpm create uniweb my-site --template international
|
|
25
|
+
### Templates
|
|
39
26
|
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
| Template | Description |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| **Starter** | Foundation + site + sample content (default) |
|
|
30
|
+
| **Marketing** | Landing page, features, pricing, testimonials |
|
|
31
|
+
| **Docs** | Documentation with sidebar and search |
|
|
32
|
+
| **Academic** | Research site with publications and team |
|
|
33
|
+
| **International** | Multilingual site with i18n and blog |
|
|
34
|
+
| **Dynamic** | Live API data fetching with loading states |
|
|
35
|
+
| **Store** | E-commerce with product grid |
|
|
36
|
+
| **Extensions** | Multi-foundation with visual effects extension |
|
|
37
|
+
| **Blank** | Empty workspace — grow with `uniweb add` |
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
pnpm create uniweb my-site --template docs
|
|
39
|
+
**See them live:** [View all template demos](https://uniweb.github.io/templates/)
|
|
45
40
|
|
|
46
|
-
|
|
47
|
-
pnpm create uniweb my-site --template store
|
|
41
|
+
You can also skip the interactive prompt with `--template`:
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
```bash
|
|
44
|
+
npm create uniweb my-site -- --template docs
|
|
45
|
+
```
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
pnpm create uniweb my-site --template extensions
|
|
47
|
+
### Development Commands
|
|
54
48
|
|
|
55
|
-
|
|
56
|
-
pnpm create uniweb my-site --template starter
|
|
49
|
+
Run these from the **project root**:
|
|
57
50
|
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
```bash
|
|
52
|
+
npm run dev # Start development server
|
|
53
|
+
npm run build # Build foundation + site for production
|
|
54
|
+
npm run preview # Preview the production build
|
|
60
55
|
```
|
|
61
56
|
|
|
62
|
-
|
|
57
|
+
The `build` command outputs to `site/dist/`. With pre-rendering enabled (the default for official templates), you get static HTML files ready to deploy anywhere.
|
|
63
58
|
|
|
64
59
|
## What You Get
|
|
65
60
|
|
|
@@ -173,7 +168,7 @@ After creating your project:
|
|
|
173
168
|
|
|
174
169
|
1. **Explore the structure** — Browse `site/pages/` to see how content is organized. Each page folder contains `page.yml` (metadata) and `.md` files (sections).
|
|
175
170
|
|
|
176
|
-
2. **Generate component docs** — Run `
|
|
171
|
+
2. **Generate component docs** — Run `npx uniweb docs` to create `COMPONENTS.md` with all available components, their parameters, and presets.
|
|
177
172
|
|
|
178
173
|
3. **Learn the configuration** — Run `uniweb docs site` or `uniweb docs page` for quick reference on configuration options.
|
|
179
174
|
|
|
@@ -221,7 +216,7 @@ The `foundation/` folder ships with your project as a convenience, but a foundat
|
|
|
221
216
|
| Mode | How it works | Best for |
|
|
222
217
|
| ---------------- | ---------------------------------- | -------------------------------------------------- |
|
|
223
218
|
| **Local folder** | Foundation lives in your workspace | Developing site and components together |
|
|
224
|
-
| **npm package** | `
|
|
219
|
+
| **npm package** | `npm add @acme/foundation` | Distributing via standard package tooling |
|
|
225
220
|
| **Runtime link** | Foundation loads from a URL | Independent release cycles, platform-managed sites |
|
|
226
221
|
|
|
227
222
|
You can delete the `foundation/` folder entirely and point your site at a published foundation. Or develop a foundation locally, then publish it for other sites to consume. The site doesn't care where its components come from.
|
|
@@ -258,11 +253,13 @@ The workspace grows organically. `add` handles placement, wires dependencies, up
|
|
|
258
253
|
**Or start blank and build up:**
|
|
259
254
|
|
|
260
255
|
```bash
|
|
261
|
-
|
|
256
|
+
npm create uniweb acme -- --template blank
|
|
262
257
|
cd acme
|
|
258
|
+
npm install
|
|
263
259
|
npx uniweb add foundation
|
|
264
260
|
npx uniweb add site
|
|
265
|
-
|
|
261
|
+
npm install
|
|
262
|
+
npm run dev
|
|
266
263
|
```
|
|
267
264
|
|
|
268
265
|
## The Bigger Picture
|
|
@@ -352,7 +349,7 @@ export default defineFoundationConfig({
|
|
|
352
349
|
|
|
353
350
|
### Workspace Configuration
|
|
354
351
|
|
|
355
|
-
A default project has two packages
|
|
352
|
+
A default project has two packages, listed in both `pnpm-workspace.yaml` and `package.json`:
|
|
356
353
|
|
|
357
354
|
```yaml
|
|
358
355
|
# pnpm-workspace.yaml
|
|
@@ -361,7 +358,14 @@ packages:
|
|
|
361
358
|
- site
|
|
362
359
|
```
|
|
363
360
|
|
|
364
|
-
|
|
361
|
+
```json
|
|
362
|
+
// package.json (for npm compatibility)
|
|
363
|
+
{
|
|
364
|
+
"workspaces": ["foundation", "site"]
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
When you `add` more packages, the CLI adds the appropriate globs to both files automatically:
|
|
365
369
|
|
|
366
370
|
```yaml
|
|
367
371
|
# After: uniweb add foundation blog → adds foundations/*
|
|
@@ -373,8 +377,6 @@ packages:
|
|
|
373
377
|
- extensions/*
|
|
374
378
|
```
|
|
375
379
|
|
|
376
|
-
The `package.json` `workspaces` field is kept in sync for npm compatibility.
|
|
377
|
-
|
|
378
380
|
## FAQ
|
|
379
381
|
|
|
380
382
|
**How is this different from MDX?**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/
|
|
44
|
+
"@uniweb/kit": "0.6.1",
|
|
45
|
+
"@uniweb/build": "0.7.5",
|
|
46
|
+
"@uniweb/runtime": "0.6.1",
|
|
47
|
+
"@uniweb/core": "0.5.1"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/commands/add.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
updateRootScripts,
|
|
24
24
|
} from '../utils/config.js'
|
|
25
25
|
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
26
|
+
import { detectPackageManager, filterCmd, installCmd } from '../utils/pm.js'
|
|
26
27
|
import { resolveTemplate } from '../templates/index.js'
|
|
27
28
|
import { validateTemplate } from '../templates/validator.js'
|
|
28
29
|
import { getVersionsForTemplates } from '../versions.js'
|
|
@@ -90,12 +91,12 @@ function parseArgs(args) {
|
|
|
90
91
|
* Main add command handler
|
|
91
92
|
*/
|
|
92
93
|
export async function add(args) {
|
|
93
|
-
if (
|
|
94
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
94
95
|
showAddHelp()
|
|
95
96
|
return
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
const
|
|
99
|
+
const pm = detectPackageManager()
|
|
99
100
|
|
|
100
101
|
// Find workspace root
|
|
101
102
|
const rootDir = findWorkspaceRoot()
|
|
@@ -105,6 +106,29 @@ export async function add(args) {
|
|
|
105
106
|
process.exit(1)
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
// Interactive subcommand chooser when no args given
|
|
110
|
+
let parsed
|
|
111
|
+
if (!args.length || (args[0] && args[0].startsWith('--'))) {
|
|
112
|
+
const response = await prompts({
|
|
113
|
+
type: 'select',
|
|
114
|
+
name: 'subcommand',
|
|
115
|
+
message: 'What would you like to add?',
|
|
116
|
+
choices: [
|
|
117
|
+
{ title: 'Foundation', value: 'foundation', description: 'Component library' },
|
|
118
|
+
{ title: 'Site', value: 'site', description: 'Content site' },
|
|
119
|
+
{ title: 'Extension', value: 'extension', description: 'Additional component package' },
|
|
120
|
+
],
|
|
121
|
+
}, {
|
|
122
|
+
onCancel: () => {
|
|
123
|
+
log('\nCancelled.')
|
|
124
|
+
process.exit(0)
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
parsed = parseArgs([response.subcommand, ...args])
|
|
128
|
+
} else {
|
|
129
|
+
parsed = parseArgs(args)
|
|
130
|
+
}
|
|
131
|
+
|
|
108
132
|
// Read root package.json for project name
|
|
109
133
|
const rootPkg = JSON.parse(
|
|
110
134
|
await readFile(join(rootDir, 'package.json'), 'utf-8').catch(() => '{}')
|
|
@@ -113,13 +137,13 @@ export async function add(args) {
|
|
|
113
137
|
|
|
114
138
|
switch (parsed.subcommand) {
|
|
115
139
|
case 'foundation':
|
|
116
|
-
await addFoundation(rootDir, projectName, parsed)
|
|
140
|
+
await addFoundation(rootDir, projectName, parsed, pm)
|
|
117
141
|
break
|
|
118
142
|
case 'site':
|
|
119
|
-
await addSite(rootDir, projectName, parsed)
|
|
143
|
+
await addSite(rootDir, projectName, parsed, pm)
|
|
120
144
|
break
|
|
121
145
|
case 'extension':
|
|
122
|
-
await addExtension(rootDir, projectName, parsed)
|
|
146
|
+
await addExtension(rootDir, projectName, parsed, pm)
|
|
123
147
|
break
|
|
124
148
|
default:
|
|
125
149
|
error(`Unknown subcommand: ${parsed.subcommand}`)
|
|
@@ -131,8 +155,32 @@ export async function add(args) {
|
|
|
131
155
|
/**
|
|
132
156
|
* Add a foundation to the workspace
|
|
133
157
|
*/
|
|
134
|
-
async function addFoundation(rootDir, projectName, opts) {
|
|
135
|
-
|
|
158
|
+
async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
159
|
+
let name = opts.name
|
|
160
|
+
|
|
161
|
+
// Interactive name prompt when name not provided and no --path
|
|
162
|
+
if (!name && !opts.path) {
|
|
163
|
+
const foundations = await discoverFoundations(rootDir)
|
|
164
|
+
const hasDefault = foundations.length === 0 && !existsSync(join(rootDir, 'foundation'))
|
|
165
|
+
const response = await prompts({
|
|
166
|
+
type: 'text',
|
|
167
|
+
name: 'name',
|
|
168
|
+
message: 'Foundation name:',
|
|
169
|
+
initial: hasDefault ? 'foundation' : undefined,
|
|
170
|
+
validate: (value) => {
|
|
171
|
+
if (!value) return 'Name is required'
|
|
172
|
+
if (!/^[a-z0-9-]+$/.test(value)) return 'Use lowercase letters, numbers, and hyphens'
|
|
173
|
+
return true
|
|
174
|
+
},
|
|
175
|
+
}, {
|
|
176
|
+
onCancel: () => {
|
|
177
|
+
log('\nCancelled.')
|
|
178
|
+
process.exit(0)
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
name = response.name
|
|
182
|
+
}
|
|
183
|
+
|
|
136
184
|
const target = await resolveFoundationTarget(rootDir, name, opts)
|
|
137
185
|
const fullPath = join(rootDir, target)
|
|
138
186
|
|
|
@@ -161,18 +209,42 @@ async function addFoundation(rootDir, projectName, opts) {
|
|
|
161
209
|
|
|
162
210
|
// Update root scripts
|
|
163
211
|
const sites = await discoverSites(rootDir)
|
|
164
|
-
await updateRootScripts(rootDir, sites)
|
|
212
|
+
await updateRootScripts(rootDir, sites, pm)
|
|
165
213
|
|
|
166
214
|
success(`Created foundation '${name || 'foundation'}' at ${target}/`)
|
|
167
215
|
log('')
|
|
168
|
-
log(`Next: ${colors.cyan}
|
|
216
|
+
log(`Next: ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
169
217
|
}
|
|
170
218
|
|
|
171
219
|
/**
|
|
172
220
|
* Add a site to the workspace
|
|
173
221
|
*/
|
|
174
|
-
async function addSite(rootDir, projectName, opts) {
|
|
175
|
-
|
|
222
|
+
async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
223
|
+
let name = opts.name
|
|
224
|
+
|
|
225
|
+
// Interactive name prompt when name not provided and no --path
|
|
226
|
+
if (!name && !opts.path) {
|
|
227
|
+
const existingSites = await discoverSites(rootDir)
|
|
228
|
+
const hasDefault = existingSites.length === 0 && !existsSync(join(rootDir, 'site'))
|
|
229
|
+
const response = await prompts({
|
|
230
|
+
type: 'text',
|
|
231
|
+
name: 'name',
|
|
232
|
+
message: 'Site name:',
|
|
233
|
+
initial: hasDefault ? 'site' : undefined,
|
|
234
|
+
validate: (value) => {
|
|
235
|
+
if (!value) return 'Name is required'
|
|
236
|
+
if (!/^[a-z0-9-]+$/.test(value)) return 'Use lowercase letters, numbers, and hyphens'
|
|
237
|
+
return true
|
|
238
|
+
},
|
|
239
|
+
}, {
|
|
240
|
+
onCancel: () => {
|
|
241
|
+
log('\nCancelled.')
|
|
242
|
+
process.exit(0)
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
name = response.name
|
|
246
|
+
}
|
|
247
|
+
|
|
176
248
|
const target = await resolveSiteTarget(rootDir, name, opts)
|
|
177
249
|
const fullPath = join(rootDir, target)
|
|
178
250
|
|
|
@@ -183,25 +255,34 @@ async function addSite(rootDir, projectName, opts) {
|
|
|
183
255
|
|
|
184
256
|
// Resolve foundation
|
|
185
257
|
const foundation = await resolveFoundation(rootDir, opts.foundation)
|
|
186
|
-
if (!foundation) {
|
|
187
|
-
error('No foundation found. Add a foundation first: uniweb add foundation')
|
|
188
|
-
process.exit(1)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Compute relative path from site to foundation
|
|
192
|
-
const foundationPath = computeFoundationPath(target, foundation.path)
|
|
193
258
|
const siteName = name || 'site'
|
|
194
259
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
260
|
+
if (foundation) {
|
|
261
|
+
// Compute relative path from site to foundation
|
|
262
|
+
const foundationPath = computeFoundationPath(target, foundation.path)
|
|
263
|
+
|
|
264
|
+
// Scaffold
|
|
265
|
+
await scaffoldSite(fullPath, {
|
|
266
|
+
name: siteName,
|
|
267
|
+
projectName,
|
|
268
|
+
foundationName: foundation.name,
|
|
269
|
+
foundationPath,
|
|
270
|
+
foundationRef: foundation.name,
|
|
271
|
+
}, {
|
|
272
|
+
onProgress: (msg) => info(` ${msg}`),
|
|
273
|
+
})
|
|
274
|
+
} else {
|
|
275
|
+
// No foundation — scaffold without wiring
|
|
276
|
+
await scaffoldSite(fullPath, {
|
|
277
|
+
name: siteName,
|
|
278
|
+
projectName,
|
|
279
|
+
foundationName: '',
|
|
280
|
+
foundationPath: '',
|
|
281
|
+
}, {
|
|
282
|
+
onProgress: (msg) => info(` ${msg}`),
|
|
283
|
+
})
|
|
284
|
+
log(` ${colors.yellow}⚠ No foundation wired. Add one later with: npx uniweb add foundation${colors.reset}`)
|
|
285
|
+
}
|
|
205
286
|
|
|
206
287
|
// Apply template content if --from specified
|
|
207
288
|
if (opts.from) {
|
|
@@ -218,22 +299,41 @@ async function addSite(rootDir, projectName, opts) {
|
|
|
218
299
|
if (!sites.find(s => s.path === target)) {
|
|
219
300
|
sites.push({ name: siteName, path: target })
|
|
220
301
|
}
|
|
221
|
-
await updateRootScripts(rootDir, sites)
|
|
302
|
+
await updateRootScripts(rootDir, sites, pm)
|
|
222
303
|
|
|
223
|
-
|
|
304
|
+
if (foundation) {
|
|
305
|
+
success(`Created site '${siteName}' at ${target}/ → foundation '${foundation.name}'`)
|
|
306
|
+
} else {
|
|
307
|
+
success(`Created site '${siteName}' at ${target}/`)
|
|
308
|
+
}
|
|
224
309
|
log('')
|
|
225
|
-
log(`Next: ${colors.cyan}
|
|
310
|
+
log(`Next: ${colors.cyan}${installCmd(pm)} && ${filterCmd(pm, siteName, 'dev')}${colors.reset}`)
|
|
226
311
|
}
|
|
227
312
|
|
|
228
313
|
/**
|
|
229
314
|
* Add an extension to the workspace
|
|
230
315
|
*/
|
|
231
|
-
async function addExtension(rootDir, projectName, opts) {
|
|
232
|
-
|
|
316
|
+
async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
317
|
+
let name = opts.name
|
|
233
318
|
|
|
319
|
+
// Interactive name prompt when name not provided
|
|
234
320
|
if (!name) {
|
|
235
|
-
|
|
236
|
-
|
|
321
|
+
const response = await prompts({
|
|
322
|
+
type: 'text',
|
|
323
|
+
name: 'name',
|
|
324
|
+
message: 'Extension name:',
|
|
325
|
+
validate: (value) => {
|
|
326
|
+
if (!value) return 'Name is required'
|
|
327
|
+
if (!/^[a-z0-9-]+$/.test(value)) return 'Use lowercase letters, numbers, and hyphens'
|
|
328
|
+
return true
|
|
329
|
+
},
|
|
330
|
+
}, {
|
|
331
|
+
onCancel: () => {
|
|
332
|
+
log('\nCancelled.')
|
|
333
|
+
process.exit(0)
|
|
334
|
+
},
|
|
335
|
+
})
|
|
336
|
+
name = response.name
|
|
237
337
|
}
|
|
238
338
|
|
|
239
339
|
// Determine target
|
|
@@ -281,7 +381,7 @@ async function addExtension(rootDir, projectName, opts) {
|
|
|
281
381
|
|
|
282
382
|
// Update root scripts
|
|
283
383
|
const sites = await discoverSites(rootDir)
|
|
284
|
-
await updateRootScripts(rootDir, sites)
|
|
384
|
+
await updateRootScripts(rootDir, sites, pm)
|
|
285
385
|
|
|
286
386
|
let msg = `Created extension '${name}' at ${target}/`
|
|
287
387
|
if (wiredSite) {
|
|
@@ -289,7 +389,7 @@ async function addExtension(rootDir, projectName, opts) {
|
|
|
289
389
|
}
|
|
290
390
|
success(msg)
|
|
291
391
|
log('')
|
|
292
|
-
log(`Next: ${colors.cyan}
|
|
392
|
+
log(`Next: ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
293
393
|
}
|
|
294
394
|
|
|
295
395
|
/**
|
|
@@ -367,6 +467,19 @@ async function resolveFoundation(rootDir, foundationFlag) {
|
|
|
367
467
|
}
|
|
368
468
|
|
|
369
469
|
if (foundations.length === 0) {
|
|
470
|
+
const response = await prompts({
|
|
471
|
+
type: 'select',
|
|
472
|
+
name: 'choice',
|
|
473
|
+
message: 'No foundations found. Proceed without one?',
|
|
474
|
+
choices: [
|
|
475
|
+
{ title: 'None', value: 'none', description: 'Proceed without a foundation' },
|
|
476
|
+
],
|
|
477
|
+
}, {
|
|
478
|
+
onCancel: () => {
|
|
479
|
+
log('\nCancelled.')
|
|
480
|
+
process.exit(0)
|
|
481
|
+
},
|
|
482
|
+
})
|
|
370
483
|
return null
|
|
371
484
|
}
|
|
372
485
|
|
package/src/commands/doctor.js
CHANGED
|
@@ -379,7 +379,7 @@ export async function doctor(args = []) {
|
|
|
379
379
|
message: `Foundation not built: ${matchingFoundation.name}`
|
|
380
380
|
})
|
|
381
381
|
warn(`Foundation not built yet`)
|
|
382
|
-
log(` ${colors.dim}Run:
|
|
382
|
+
log(` ${colors.dim}Run: npx uniweb build${colors.reset}`)
|
|
383
383
|
} else {
|
|
384
384
|
success(`Foundation built: dist/foundation.js exists`)
|
|
385
385
|
}
|
|
@@ -431,7 +431,7 @@ export async function doctor(args = []) {
|
|
|
431
431
|
message: `Extension not built: ${ext.name}`
|
|
432
432
|
})
|
|
433
433
|
warn(`Extension not built yet`)
|
|
434
|
-
log(` ${colors.dim}Run:
|
|
434
|
+
log(` ${colors.dim}Run: npx uniweb build${colors.reset}`)
|
|
435
435
|
} else {
|
|
436
436
|
success(`Extension built: dist/foundation.js exists`)
|
|
437
437
|
}
|
package/src/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from './templates/index.js'
|
|
29
29
|
import { validateTemplate } from './templates/validator.js'
|
|
30
30
|
import { scaffoldWorkspace, scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies } from './utils/scaffold.js'
|
|
31
|
+
import { detectPackageManager, filterCmd, installCmd, runCmd } from './utils/pm.js'
|
|
31
32
|
|
|
32
33
|
// Colors for terminal output
|
|
33
34
|
const colors = {
|
|
@@ -73,7 +74,7 @@ function title(message) {
|
|
|
73
74
|
* Create a project using the new package template flow (default)
|
|
74
75
|
*/
|
|
75
76
|
async function createFromPackageTemplates(projectDir, projectName, options = {}) {
|
|
76
|
-
const { onProgress, onWarning } = options
|
|
77
|
+
const { onProgress, onWarning, pm = 'pnpm' } = options
|
|
77
78
|
|
|
78
79
|
onProgress?.('Setting up workspace...')
|
|
79
80
|
|
|
@@ -82,9 +83,9 @@ async function createFromPackageTemplates(projectDir, projectName, options = {})
|
|
|
82
83
|
projectName,
|
|
83
84
|
workspaceGlobs: ['foundation', 'site'],
|
|
84
85
|
scripts: {
|
|
85
|
-
dev: '
|
|
86
|
+
dev: filterCmd(pm, 'site', 'dev'),
|
|
86
87
|
build: 'uniweb build',
|
|
87
|
-
preview: '
|
|
88
|
+
preview: filterCmd(pm, 'site', 'preview'),
|
|
88
89
|
},
|
|
89
90
|
}, { onProgress, onWarning })
|
|
90
91
|
|
|
@@ -138,7 +139,7 @@ async function createBlankWorkspace(projectDir, projectName, options = {}) {
|
|
|
138
139
|
* content (sections, pages, theme) from the content template.
|
|
139
140
|
*/
|
|
140
141
|
async function createFromContentTemplate(projectDir, projectName, metadata, templateRootPath, options = {}) {
|
|
141
|
-
const { onProgress, onWarning } = options
|
|
142
|
+
const { onProgress, onWarning, pm = 'pnpm' } = options
|
|
142
143
|
|
|
143
144
|
// Determine packages to create
|
|
144
145
|
const packages = metadata.packages || [
|
|
@@ -156,17 +157,17 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
|
|
|
156
157
|
build: 'uniweb build',
|
|
157
158
|
}
|
|
158
159
|
if (sites.length === 1) {
|
|
159
|
-
scripts.dev =
|
|
160
|
-
scripts.preview =
|
|
160
|
+
scripts.dev = filterCmd(pm, sites[0].name, 'dev')
|
|
161
|
+
scripts.preview = filterCmd(pm, sites[0].name, 'preview')
|
|
161
162
|
} else {
|
|
162
163
|
for (const s of sites) {
|
|
163
|
-
scripts[`dev:${s.name}`] =
|
|
164
|
-
scripts[`preview:${s.name}`] =
|
|
164
|
+
scripts[`dev:${s.name}`] = filterCmd(pm, s.name, 'dev')
|
|
165
|
+
scripts[`preview:${s.name}`] = filterCmd(pm, s.name, 'preview')
|
|
165
166
|
}
|
|
166
167
|
// First site gets unqualified aliases
|
|
167
168
|
if (sites.length > 0) {
|
|
168
|
-
scripts.dev =
|
|
169
|
-
scripts.preview =
|
|
169
|
+
scripts.dev = filterCmd(pm, sites[0].name, 'dev')
|
|
170
|
+
scripts.preview = filterCmd(pm, sites[0].name, 'preview')
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
@@ -289,6 +290,7 @@ function computeFoundationFilePath(sitePath, foundationPath) {
|
|
|
289
290
|
async function main() {
|
|
290
291
|
const args = process.argv.slice(2)
|
|
291
292
|
const command = args[0]
|
|
293
|
+
const pm = detectPackageManager()
|
|
292
294
|
|
|
293
295
|
// Show help
|
|
294
296
|
if (!command || command === '--help' || command === '-h') {
|
|
@@ -439,6 +441,7 @@ async function main() {
|
|
|
439
441
|
await createFromPackageTemplates(projectDir, effectiveName, {
|
|
440
442
|
onProgress: progressCb,
|
|
441
443
|
onWarning: warningCb,
|
|
444
|
+
pm,
|
|
442
445
|
})
|
|
443
446
|
} else {
|
|
444
447
|
// External: official/npm/github/local
|
|
@@ -458,6 +461,7 @@ async function main() {
|
|
|
458
461
|
await createFromContentTemplate(projectDir, effectiveName, metadata, resolved.path, {
|
|
459
462
|
onProgress: progressCb,
|
|
460
463
|
onWarning: warningCb,
|
|
464
|
+
pm,
|
|
461
465
|
})
|
|
462
466
|
} finally {
|
|
463
467
|
if (resolved.cleanup) await resolved.cleanup()
|
|
@@ -497,15 +501,16 @@ async function main() {
|
|
|
497
501
|
if (templateType === 'blank') {
|
|
498
502
|
log(`Next steps:\n`)
|
|
499
503
|
log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
|
|
504
|
+
log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
500
505
|
log(` ${colors.cyan}npx uniweb add foundation${colors.reset}`)
|
|
501
506
|
log(` ${colors.cyan}npx uniweb add site${colors.reset}`)
|
|
502
|
-
log(` ${colors.cyan}
|
|
503
|
-
log(` ${colors.cyan}
|
|
507
|
+
log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
508
|
+
log(` ${colors.cyan}${runCmd(pm, 'dev')}${colors.reset}`)
|
|
504
509
|
} else {
|
|
505
510
|
log(`Next steps:\n`)
|
|
506
511
|
log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
|
|
507
|
-
log(` ${colors.cyan}
|
|
508
|
-
log(` ${colors.cyan}
|
|
512
|
+
log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
513
|
+
log(` ${colors.cyan}${runCmd(pm, 'dev')}${colors.reset}`)
|
|
509
514
|
}
|
|
510
515
|
log('')
|
|
511
516
|
}
|
package/src/utils/config.js
CHANGED
|
@@ -9,20 +9,30 @@ import { existsSync } from 'node:fs'
|
|
|
9
9
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
10
10
|
import { join } from 'node:path'
|
|
11
11
|
import yaml from 'js-yaml'
|
|
12
|
+
import { filterCmd } from './pm.js'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* Read
|
|
15
|
+
* Read workspace package globs.
|
|
16
|
+
* Tries pnpm-workspace.yaml first, falls back to package.json workspaces.
|
|
15
17
|
* @param {string} rootDir - Workspace root directory
|
|
16
18
|
* @returns {Promise<{packages: string[]}>}
|
|
17
19
|
*/
|
|
18
20
|
export async function readWorkspaceConfig(rootDir) {
|
|
21
|
+
// Try pnpm-workspace.yaml first
|
|
19
22
|
const configPath = join(rootDir, 'pnpm-workspace.yaml')
|
|
20
|
-
if (
|
|
21
|
-
|
|
23
|
+
if (existsSync(configPath)) {
|
|
24
|
+
const content = await readFile(configPath, 'utf-8')
|
|
25
|
+
const config = yaml.load(content)
|
|
26
|
+
return { packages: config?.packages || [] }
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
|
|
29
|
+
// Fall back to package.json workspaces
|
|
30
|
+
const pkg = await readRootPackageJson(rootDir)
|
|
31
|
+
if (Array.isArray(pkg.workspaces)) {
|
|
32
|
+
return { packages: pkg.workspaces }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { packages: [] }
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
/**
|
|
@@ -47,6 +57,15 @@ export async function addWorkspaceGlob(rootDir, glob) {
|
|
|
47
57
|
config.packages.push(glob)
|
|
48
58
|
await writeWorkspaceConfig(rootDir, config)
|
|
49
59
|
}
|
|
60
|
+
|
|
61
|
+
// Sync workspaces array in package.json (for npm compatibility)
|
|
62
|
+
const pkg = await readRootPackageJson(rootDir)
|
|
63
|
+
if (pkg.workspaces) {
|
|
64
|
+
if (!pkg.workspaces.includes(glob)) {
|
|
65
|
+
pkg.workspaces.push(glob)
|
|
66
|
+
await writeRootPackageJson(rootDir, pkg)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
/**
|
|
@@ -75,9 +94,10 @@ export async function writeRootPackageJson(rootDir, pkg) {
|
|
|
75
94
|
/**
|
|
76
95
|
* Compute root scripts based on discovered sites
|
|
77
96
|
* @param {Array<{name: string, path: string}>} sites - Discovered sites
|
|
97
|
+
* @param {'pnpm' | 'npm'} [pm='pnpm'] - Package manager
|
|
78
98
|
* @returns {Object} Scripts object for package.json
|
|
79
99
|
*/
|
|
80
|
-
export function computeRootScripts(sites) {
|
|
100
|
+
export function computeRootScripts(sites, pm = 'pnpm') {
|
|
81
101
|
const scripts = {
|
|
82
102
|
build: 'uniweb build',
|
|
83
103
|
}
|
|
@@ -87,17 +107,17 @@ export function computeRootScripts(sites) {
|
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
if (sites.length === 1) {
|
|
90
|
-
scripts.dev =
|
|
91
|
-
scripts.preview =
|
|
110
|
+
scripts.dev = filterCmd(pm, sites[0].name, 'dev')
|
|
111
|
+
scripts.preview = filterCmd(pm, sites[0].name, 'preview')
|
|
92
112
|
} else {
|
|
93
113
|
// First site gets unqualified dev/preview
|
|
94
|
-
scripts.dev =
|
|
95
|
-
scripts.preview =
|
|
114
|
+
scripts.dev = filterCmd(pm, sites[0].name, 'dev')
|
|
115
|
+
scripts.preview = filterCmd(pm, sites[0].name, 'preview')
|
|
96
116
|
|
|
97
117
|
// Subsequent sites get qualified dev:{name}/preview:{name}
|
|
98
118
|
for (let i = 1; i < sites.length; i++) {
|
|
99
|
-
scripts[`dev:${sites[i].name}`] =
|
|
100
|
-
scripts[`preview:${sites[i].name}`] =
|
|
119
|
+
scripts[`dev:${sites[i].name}`] = filterCmd(pm, sites[i].name, 'dev')
|
|
120
|
+
scripts[`preview:${sites[i].name}`] = filterCmd(pm, sites[i].name, 'preview')
|
|
101
121
|
}
|
|
102
122
|
}
|
|
103
123
|
|
|
@@ -108,16 +128,17 @@ export function computeRootScripts(sites) {
|
|
|
108
128
|
* Update root scripts after adding a new site
|
|
109
129
|
* @param {string} rootDir - Workspace root directory
|
|
110
130
|
* @param {Array<{name: string, path: string}>} sites - All sites (including new one)
|
|
131
|
+
* @param {'pnpm' | 'npm'} [pm='pnpm'] - Package manager
|
|
111
132
|
*/
|
|
112
|
-
export async function updateRootScripts(rootDir, sites) {
|
|
133
|
+
export async function updateRootScripts(rootDir, sites, pm = 'pnpm') {
|
|
113
134
|
const pkg = await readRootPackageJson(rootDir)
|
|
114
|
-
const newScripts = computeRootScripts(sites)
|
|
135
|
+
const newScripts = computeRootScripts(sites, pm)
|
|
115
136
|
|
|
116
137
|
// If we're adding a second site, rename existing dev/preview to dev:{firstName}
|
|
117
138
|
if (sites.length === 2 && pkg.scripts?.dev) {
|
|
118
139
|
const firstName = sites[0].name
|
|
119
|
-
// Only rename if the existing dev
|
|
120
|
-
if (pkg.scripts.dev
|
|
140
|
+
// Only rename if the existing dev script references the first site's name
|
|
141
|
+
if (pkg.scripts.dev.includes(firstName)) {
|
|
121
142
|
pkg.scripts[`dev:${firstName}`] = pkg.scripts.dev
|
|
122
143
|
pkg.scripts[`preview:${firstName}`] = pkg.scripts.preview
|
|
123
144
|
}
|
package/src/utils/pm.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Manager Detection
|
|
3
|
+
*
|
|
4
|
+
* Detect whether the user ran the CLI via npm or pnpm,
|
|
5
|
+
* and generate PM-appropriate commands for output messages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detect which package manager invoked the CLI.
|
|
10
|
+
* Uses the standard npm_config_user_agent env var (same technique as create-vite, create-next-app).
|
|
11
|
+
* @returns {'pnpm' | 'npm'}
|
|
12
|
+
*/
|
|
13
|
+
export function detectPackageManager() {
|
|
14
|
+
const ua = process.env.npm_config_user_agent || ''
|
|
15
|
+
if (ua.startsWith('pnpm/')) return 'pnpm'
|
|
16
|
+
return 'npm'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate a workspace-filtered command.
|
|
21
|
+
* pnpm: "pnpm --filter site dev"
|
|
22
|
+
* npm: "npm -w site run dev"
|
|
23
|
+
* @param {'pnpm' | 'npm'} pm
|
|
24
|
+
* @param {string} pkg - Package name to filter to
|
|
25
|
+
* @param {string} cmd - Script name to run
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
export function filterCmd(pm, pkg, cmd) {
|
|
29
|
+
return pm === 'pnpm'
|
|
30
|
+
? `pnpm --filter ${pkg} ${cmd}`
|
|
31
|
+
: `npm -w ${pkg} run ${cmd}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate an install command.
|
|
36
|
+
* @param {'pnpm' | 'npm'} pm
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function installCmd(pm) {
|
|
40
|
+
return pm === 'pnpm' ? 'pnpm install' : 'npm install'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generate a run-script command.
|
|
45
|
+
* @param {'pnpm' | 'npm'} pm
|
|
46
|
+
* @param {string} script - Script name
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function runCmd(pm, script) {
|
|
50
|
+
return pm === 'pnpm' ? `pnpm ${script}` : `npm run ${script}`
|
|
51
|
+
}
|
package/src/utils/workspace.js
CHANGED
|
@@ -1,24 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workspace Detection Utilities
|
|
3
3
|
*
|
|
4
|
-
* Detects
|
|
4
|
+
* Detects workspace structure (pnpm-workspace.yaml or package.json workspaces)
|
|
5
|
+
* and classifies packages as foundations or sites.
|
|
5
6
|
* Used by commands to auto-detect targets when run from workspace root.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import { existsSync, readdirSync } from 'node:fs'
|
|
9
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
9
10
|
import { readFile } from 'node:fs/promises'
|
|
10
11
|
import { resolve, dirname, join } from 'node:path'
|
|
11
12
|
import yaml from 'js-yaml'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
+
* Check if a directory is a workspace root.
|
|
16
|
+
* Recognizes pnpm-workspace.yaml or package.json with workspaces field.
|
|
17
|
+
* @param {string} dir - Directory to check
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function hasWorkspaceConfig(dir) {
|
|
21
|
+
if (existsSync(join(dir, 'pnpm-workspace.yaml'))) return true
|
|
22
|
+
const pkgPath = join(dir, 'package.json')
|
|
23
|
+
if (existsSync(pkgPath)) {
|
|
24
|
+
try {
|
|
25
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
26
|
+
if (Array.isArray(pkg.workspaces)) return true
|
|
27
|
+
} catch {
|
|
28
|
+
// ignore
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Find workspace root by looking for pnpm-workspace.yaml or package.json workspaces
|
|
15
36
|
* @param {string} startDir - Directory to start searching from
|
|
16
37
|
* @returns {string|null} - Workspace root path or null
|
|
17
38
|
*/
|
|
18
39
|
export function findWorkspaceRoot(startDir = process.cwd()) {
|
|
19
40
|
let dir = startDir
|
|
20
41
|
while (dir !== dirname(dir)) {
|
|
21
|
-
if (
|
|
42
|
+
if (hasWorkspaceConfig(dir)) {
|
|
22
43
|
return dir
|
|
23
44
|
}
|
|
24
45
|
dir = dirname(dir)
|
|
@@ -71,20 +92,35 @@ function resolvePatterns(patterns, workspaceRoot) {
|
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
/**
|
|
74
|
-
* Get workspace packages from pnpm-workspace.yaml
|
|
95
|
+
* Get workspace packages from pnpm-workspace.yaml or package.json workspaces
|
|
75
96
|
* @param {string} workspaceRoot
|
|
76
97
|
* @returns {Promise<string[]>} - Array of package directories (relative paths)
|
|
77
98
|
*/
|
|
78
99
|
export async function getWorkspacePackages(workspaceRoot) {
|
|
100
|
+
// Try pnpm-workspace.yaml first
|
|
79
101
|
const configPath = join(workspaceRoot, 'pnpm-workspace.yaml')
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
if (existsSync(configPath)) {
|
|
103
|
+
const content = await readFile(configPath, 'utf-8')
|
|
104
|
+
const config = yaml.load(content)
|
|
105
|
+
if (config?.packages && Array.isArray(config.packages)) {
|
|
106
|
+
return resolvePatterns(config.packages, workspaceRoot)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
82
109
|
|
|
83
|
-
|
|
84
|
-
|
|
110
|
+
// Fall back to package.json workspaces
|
|
111
|
+
const pkgPath = join(workspaceRoot, 'package.json')
|
|
112
|
+
if (existsSync(pkgPath)) {
|
|
113
|
+
try {
|
|
114
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'))
|
|
115
|
+
if (Array.isArray(pkg.workspaces)) {
|
|
116
|
+
return resolvePatterns(pkg.workspaces, workspaceRoot)
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// ignore
|
|
120
|
+
}
|
|
85
121
|
}
|
|
86
122
|
|
|
87
|
-
return
|
|
123
|
+
return []
|
|
88
124
|
}
|
|
89
125
|
|
|
90
126
|
/**
|
|
@@ -166,7 +202,7 @@ export async function findSites(workspaceRoot) {
|
|
|
166
202
|
* @returns {boolean}
|
|
167
203
|
*/
|
|
168
204
|
export function isWorkspaceRoot(dir = process.cwd()) {
|
|
169
|
-
return
|
|
205
|
+
return hasWorkspaceConfig(dir)
|
|
170
206
|
}
|
|
171
207
|
|
|
172
208
|
/**
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
"{{@key}}": "{{this}}"{{#unless @last}},{{/unless}}
|
|
9
9
|
{{/each}}
|
|
10
10
|
},
|
|
11
|
+
{{#if workspaceGlobs.length}}
|
|
12
|
+
"workspaces": [
|
|
13
|
+
{{#each workspaceGlobs}}
|
|
14
|
+
"{{this}}"{{#unless @last}},{{/unless}}
|
|
15
|
+
{{/each}}
|
|
16
|
+
],
|
|
17
|
+
{{/if}}
|
|
11
18
|
"devDependencies": {
|
|
12
19
|
"@types/node": "^22.0.0",
|
|
13
20
|
"uniweb": "{{version "uniweb"}}"
|