uniweb 0.7.6 → 0.7.8
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 +50 -42
- package/package.json +4 -4
- package/src/commands/add.js +158 -37
- package/src/commands/doctor.js +2 -2
- package/src/index.js +19 -14
- package/src/utils/config.js +39 -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,60 @@ 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
|
-
|
|
13
|
+
The interactive prompt asks for a project name and template. Pick one, then:
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
```bash
|
|
16
|
+
cd my-project
|
|
17
|
+
npm install
|
|
18
|
+
npm run dev
|
|
19
|
+
```
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
Edit files in `site/pages/` and `foundation/src/sections/` to see changes instantly.
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
> **pnpm ready** — `pnpm create uniweb` works out of the box. Projects include both `pnpm-workspace.yaml` and npm workspaces.
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
pnpm dev # Start development server
|
|
26
|
-
pnpm build # Build foundation + site for production
|
|
27
|
-
pnpm preview # Preview the production build
|
|
28
|
-
```
|
|
25
|
+
### Templates
|
|
29
26
|
|
|
30
|
-
|
|
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` |
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
**See them live:** [View all template demos](https://uniweb.github.io/templates/)
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
You can also skip the interactive prompt with `--template`:
|
|
35
42
|
|
|
36
43
|
```bash
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
npm create uniweb my-site -- --template docs
|
|
45
|
+
```
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
pnpm create uniweb my-site --template academic
|
|
47
|
+
or
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
```bash
|
|
44
50
|
pnpm create uniweb my-site --template docs
|
|
51
|
+
```
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
pnpm create uniweb my-site --template store
|
|
48
|
-
|
|
49
|
-
# Dynamic content (data sources, API-driven pages)
|
|
50
|
-
pnpm create uniweb my-site --template dynamic
|
|
51
|
-
|
|
52
|
-
# Extensions demo (primary foundation + effects extension)
|
|
53
|
-
pnpm create uniweb my-site --template extensions
|
|
53
|
+
### Development Commands
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
pnpm create uniweb my-site --template starter
|
|
55
|
+
Run these from the **project root**:
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
```bash
|
|
58
|
+
npm run dev # Start development server
|
|
59
|
+
npm run build # Build foundation + site for production
|
|
60
|
+
npm run preview # Preview the production build
|
|
60
61
|
```
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
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
64
|
|
|
64
65
|
## What You Get
|
|
65
66
|
|
|
@@ -173,7 +174,7 @@ After creating your project:
|
|
|
173
174
|
|
|
174
175
|
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
176
|
|
|
176
|
-
2. **Generate component docs** — Run `
|
|
177
|
+
2. **Generate component docs** — Run `npx uniweb docs` to create `COMPONENTS.md` with all available components, their parameters, and presets.
|
|
177
178
|
|
|
178
179
|
3. **Learn the configuration** — Run `uniweb docs site` or `uniweb docs page` for quick reference on configuration options.
|
|
179
180
|
|
|
@@ -221,7 +222,7 @@ The `foundation/` folder ships with your project as a convenience, but a foundat
|
|
|
221
222
|
| Mode | How it works | Best for |
|
|
222
223
|
| ---------------- | ---------------------------------- | -------------------------------------------------- |
|
|
223
224
|
| **Local folder** | Foundation lives in your workspace | Developing site and components together |
|
|
224
|
-
| **npm package** | `
|
|
225
|
+
| **npm package** | `npm add @acme/foundation` | Distributing via standard package tooling |
|
|
225
226
|
| **Runtime link** | Foundation loads from a URL | Independent release cycles, platform-managed sites |
|
|
226
227
|
|
|
227
228
|
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 +259,13 @@ The workspace grows organically. `add` handles placement, wires dependencies, up
|
|
|
258
259
|
**Or start blank and build up:**
|
|
259
260
|
|
|
260
261
|
```bash
|
|
261
|
-
|
|
262
|
+
npm create uniweb acme -- --template blank
|
|
262
263
|
cd acme
|
|
264
|
+
npm install
|
|
263
265
|
npx uniweb add foundation
|
|
264
266
|
npx uniweb add site
|
|
265
|
-
|
|
267
|
+
npm install
|
|
268
|
+
npm run dev
|
|
266
269
|
```
|
|
267
270
|
|
|
268
271
|
## The Bigger Picture
|
|
@@ -352,7 +355,7 @@ export default defineFoundationConfig({
|
|
|
352
355
|
|
|
353
356
|
### Workspace Configuration
|
|
354
357
|
|
|
355
|
-
A default project has two packages
|
|
358
|
+
A default project has two packages, listed in both `pnpm-workspace.yaml` and `package.json`:
|
|
356
359
|
|
|
357
360
|
```yaml
|
|
358
361
|
# pnpm-workspace.yaml
|
|
@@ -361,7 +364,14 @@ packages:
|
|
|
361
364
|
- site
|
|
362
365
|
```
|
|
363
366
|
|
|
364
|
-
|
|
367
|
+
```json
|
|
368
|
+
// package.json (for npm compatibility)
|
|
369
|
+
{
|
|
370
|
+
"workspaces": ["foundation", "site"]
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
When you `add` more packages, the CLI adds the appropriate globs to both files automatically:
|
|
365
375
|
|
|
366
376
|
```yaml
|
|
367
377
|
# After: uniweb add foundation blog → adds foundations/*
|
|
@@ -373,8 +383,6 @@ packages:
|
|
|
373
383
|
- extensions/*
|
|
374
384
|
```
|
|
375
385
|
|
|
376
|
-
The `package.json` `workspaces` field is kept in sync for npm compatibility.
|
|
377
|
-
|
|
378
386
|
## FAQ
|
|
379
387
|
|
|
380
388
|
**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.8",
|
|
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/
|
|
44
|
+
"@uniweb/build": "0.7.5",
|
|
45
|
+
"@uniweb/kit": "0.6.1",
|
|
45
46
|
"@uniweb/runtime": "0.6.1",
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/kit": "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,36 @@ 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
|
+
// Only set name if user chose something other than the default —
|
|
182
|
+
// null name tells resolveFoundationTarget to use default placement (./foundation/)
|
|
183
|
+
if (!hasDefault || response.name !== 'foundation') {
|
|
184
|
+
name = response.name
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
136
188
|
const target = await resolveFoundationTarget(rootDir, name, opts)
|
|
137
189
|
const fullPath = join(rootDir, target)
|
|
138
190
|
|
|
@@ -161,18 +213,46 @@ async function addFoundation(rootDir, projectName, opts) {
|
|
|
161
213
|
|
|
162
214
|
// Update root scripts
|
|
163
215
|
const sites = await discoverSites(rootDir)
|
|
164
|
-
await updateRootScripts(rootDir, sites)
|
|
216
|
+
await updateRootScripts(rootDir, sites, pm)
|
|
165
217
|
|
|
166
218
|
success(`Created foundation '${name || 'foundation'}' at ${target}/`)
|
|
167
219
|
log('')
|
|
168
|
-
log(`Next: ${colors.cyan}
|
|
220
|
+
log(`Next: ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
169
221
|
}
|
|
170
222
|
|
|
171
223
|
/**
|
|
172
224
|
* Add a site to the workspace
|
|
173
225
|
*/
|
|
174
|
-
async function addSite(rootDir, projectName, opts) {
|
|
175
|
-
|
|
226
|
+
async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
227
|
+
let name = opts.name
|
|
228
|
+
|
|
229
|
+
// Interactive name prompt when name not provided and no --path
|
|
230
|
+
if (!name && !opts.path) {
|
|
231
|
+
const existingSites = await discoverSites(rootDir)
|
|
232
|
+
const hasDefault = existingSites.length === 0 && !existsSync(join(rootDir, 'site'))
|
|
233
|
+
const response = await prompts({
|
|
234
|
+
type: 'text',
|
|
235
|
+
name: 'name',
|
|
236
|
+
message: 'Site name:',
|
|
237
|
+
initial: hasDefault ? 'site' : undefined,
|
|
238
|
+
validate: (value) => {
|
|
239
|
+
if (!value) return 'Name is required'
|
|
240
|
+
if (!/^[a-z0-9-]+$/.test(value)) return 'Use lowercase letters, numbers, and hyphens'
|
|
241
|
+
return true
|
|
242
|
+
},
|
|
243
|
+
}, {
|
|
244
|
+
onCancel: () => {
|
|
245
|
+
log('\nCancelled.')
|
|
246
|
+
process.exit(0)
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
// Only set name if user chose something other than the default —
|
|
250
|
+
// null name tells resolveSiteTarget to use default placement (./site/)
|
|
251
|
+
if (!hasDefault || response.name !== 'site') {
|
|
252
|
+
name = response.name
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
176
256
|
const target = await resolveSiteTarget(rootDir, name, opts)
|
|
177
257
|
const fullPath = join(rootDir, target)
|
|
178
258
|
|
|
@@ -183,25 +263,34 @@ async function addSite(rootDir, projectName, opts) {
|
|
|
183
263
|
|
|
184
264
|
// Resolve foundation
|
|
185
265
|
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
266
|
const siteName = name || 'site'
|
|
194
267
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
268
|
+
if (foundation) {
|
|
269
|
+
// Compute relative path from site to foundation
|
|
270
|
+
const foundationPath = computeFoundationPath(target, foundation.path)
|
|
271
|
+
|
|
272
|
+
// Scaffold
|
|
273
|
+
await scaffoldSite(fullPath, {
|
|
274
|
+
name: siteName,
|
|
275
|
+
projectName,
|
|
276
|
+
foundationName: foundation.name,
|
|
277
|
+
foundationPath,
|
|
278
|
+
foundationRef: foundation.name,
|
|
279
|
+
}, {
|
|
280
|
+
onProgress: (msg) => info(` ${msg}`),
|
|
281
|
+
})
|
|
282
|
+
} else {
|
|
283
|
+
// No foundation — scaffold without wiring
|
|
284
|
+
await scaffoldSite(fullPath, {
|
|
285
|
+
name: siteName,
|
|
286
|
+
projectName,
|
|
287
|
+
foundationName: '',
|
|
288
|
+
foundationPath: '',
|
|
289
|
+
}, {
|
|
290
|
+
onProgress: (msg) => info(` ${msg}`),
|
|
291
|
+
})
|
|
292
|
+
log(` ${colors.yellow}⚠ No foundation wired. Add one later with: npx uniweb add foundation${colors.reset}`)
|
|
293
|
+
}
|
|
205
294
|
|
|
206
295
|
// Apply template content if --from specified
|
|
207
296
|
if (opts.from) {
|
|
@@ -218,22 +307,41 @@ async function addSite(rootDir, projectName, opts) {
|
|
|
218
307
|
if (!sites.find(s => s.path === target)) {
|
|
219
308
|
sites.push({ name: siteName, path: target })
|
|
220
309
|
}
|
|
221
|
-
await updateRootScripts(rootDir, sites)
|
|
310
|
+
await updateRootScripts(rootDir, sites, pm)
|
|
222
311
|
|
|
223
|
-
|
|
312
|
+
if (foundation) {
|
|
313
|
+
success(`Created site '${siteName}' at ${target}/ → foundation '${foundation.name}'`)
|
|
314
|
+
} else {
|
|
315
|
+
success(`Created site '${siteName}' at ${target}/`)
|
|
316
|
+
}
|
|
224
317
|
log('')
|
|
225
|
-
log(`Next: ${colors.cyan}
|
|
318
|
+
log(`Next: ${colors.cyan}${installCmd(pm)} && ${filterCmd(pm, siteName, 'dev')}${colors.reset}`)
|
|
226
319
|
}
|
|
227
320
|
|
|
228
321
|
/**
|
|
229
322
|
* Add an extension to the workspace
|
|
230
323
|
*/
|
|
231
|
-
async function addExtension(rootDir, projectName, opts) {
|
|
232
|
-
|
|
324
|
+
async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
325
|
+
let name = opts.name
|
|
233
326
|
|
|
327
|
+
// Interactive name prompt when name not provided
|
|
234
328
|
if (!name) {
|
|
235
|
-
|
|
236
|
-
|
|
329
|
+
const response = await prompts({
|
|
330
|
+
type: 'text',
|
|
331
|
+
name: 'name',
|
|
332
|
+
message: 'Extension name:',
|
|
333
|
+
validate: (value) => {
|
|
334
|
+
if (!value) return 'Name is required'
|
|
335
|
+
if (!/^[a-z0-9-]+$/.test(value)) return 'Use lowercase letters, numbers, and hyphens'
|
|
336
|
+
return true
|
|
337
|
+
},
|
|
338
|
+
}, {
|
|
339
|
+
onCancel: () => {
|
|
340
|
+
log('\nCancelled.')
|
|
341
|
+
process.exit(0)
|
|
342
|
+
},
|
|
343
|
+
})
|
|
344
|
+
name = response.name
|
|
237
345
|
}
|
|
238
346
|
|
|
239
347
|
// Determine target
|
|
@@ -281,7 +389,7 @@ async function addExtension(rootDir, projectName, opts) {
|
|
|
281
389
|
|
|
282
390
|
// Update root scripts
|
|
283
391
|
const sites = await discoverSites(rootDir)
|
|
284
|
-
await updateRootScripts(rootDir, sites)
|
|
392
|
+
await updateRootScripts(rootDir, sites, pm)
|
|
285
393
|
|
|
286
394
|
let msg = `Created extension '${name}' at ${target}/`
|
|
287
395
|
if (wiredSite) {
|
|
@@ -289,7 +397,7 @@ async function addExtension(rootDir, projectName, opts) {
|
|
|
289
397
|
}
|
|
290
398
|
success(msg)
|
|
291
399
|
log('')
|
|
292
|
-
log(`Next: ${colors.cyan}
|
|
400
|
+
log(`Next: ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
293
401
|
}
|
|
294
402
|
|
|
295
403
|
/**
|
|
@@ -367,6 +475,19 @@ async function resolveFoundation(rootDir, foundationFlag) {
|
|
|
367
475
|
}
|
|
368
476
|
|
|
369
477
|
if (foundations.length === 0) {
|
|
478
|
+
const response = await prompts({
|
|
479
|
+
type: 'select',
|
|
480
|
+
name: 'choice',
|
|
481
|
+
message: 'No foundations found. Proceed without one?',
|
|
482
|
+
choices: [
|
|
483
|
+
{ title: 'None', value: 'none', description: 'Proceed without a foundation' },
|
|
484
|
+
],
|
|
485
|
+
}, {
|
|
486
|
+
onCancel: () => {
|
|
487
|
+
log('\nCancelled.')
|
|
488
|
+
process.exit(0)
|
|
489
|
+
},
|
|
490
|
+
})
|
|
370
491
|
return null
|
|
371
492
|
}
|
|
372
493
|
|
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,16 @@ 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
|
+
pkg.workspaces = []
|
|
65
|
+
}
|
|
66
|
+
if (!pkg.workspaces.includes(glob)) {
|
|
67
|
+
pkg.workspaces.push(glob)
|
|
68
|
+
await writeRootPackageJson(rootDir, pkg)
|
|
69
|
+
}
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
/**
|
|
@@ -75,9 +95,10 @@ export async function writeRootPackageJson(rootDir, pkg) {
|
|
|
75
95
|
/**
|
|
76
96
|
* Compute root scripts based on discovered sites
|
|
77
97
|
* @param {Array<{name: string, path: string}>} sites - Discovered sites
|
|
98
|
+
* @param {'pnpm' | 'npm'} [pm='pnpm'] - Package manager
|
|
78
99
|
* @returns {Object} Scripts object for package.json
|
|
79
100
|
*/
|
|
80
|
-
export function computeRootScripts(sites) {
|
|
101
|
+
export function computeRootScripts(sites, pm = 'pnpm') {
|
|
81
102
|
const scripts = {
|
|
82
103
|
build: 'uniweb build',
|
|
83
104
|
}
|
|
@@ -87,17 +108,17 @@ export function computeRootScripts(sites) {
|
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
if (sites.length === 1) {
|
|
90
|
-
scripts.dev =
|
|
91
|
-
scripts.preview =
|
|
111
|
+
scripts.dev = filterCmd(pm, sites[0].name, 'dev')
|
|
112
|
+
scripts.preview = filterCmd(pm, sites[0].name, 'preview')
|
|
92
113
|
} else {
|
|
93
114
|
// First site gets unqualified dev/preview
|
|
94
|
-
scripts.dev =
|
|
95
|
-
scripts.preview =
|
|
115
|
+
scripts.dev = filterCmd(pm, sites[0].name, 'dev')
|
|
116
|
+
scripts.preview = filterCmd(pm, sites[0].name, 'preview')
|
|
96
117
|
|
|
97
118
|
// Subsequent sites get qualified dev:{name}/preview:{name}
|
|
98
119
|
for (let i = 1; i < sites.length; i++) {
|
|
99
|
-
scripts[`dev:${sites[i].name}`] =
|
|
100
|
-
scripts[`preview:${sites[i].name}`] =
|
|
120
|
+
scripts[`dev:${sites[i].name}`] = filterCmd(pm, sites[i].name, 'dev')
|
|
121
|
+
scripts[`preview:${sites[i].name}`] = filterCmd(pm, sites[i].name, 'preview')
|
|
101
122
|
}
|
|
102
123
|
}
|
|
103
124
|
|
|
@@ -108,16 +129,17 @@ export function computeRootScripts(sites) {
|
|
|
108
129
|
* Update root scripts after adding a new site
|
|
109
130
|
* @param {string} rootDir - Workspace root directory
|
|
110
131
|
* @param {Array<{name: string, path: string}>} sites - All sites (including new one)
|
|
132
|
+
* @param {'pnpm' | 'npm'} [pm='pnpm'] - Package manager
|
|
111
133
|
*/
|
|
112
|
-
export async function updateRootScripts(rootDir, sites) {
|
|
134
|
+
export async function updateRootScripts(rootDir, sites, pm = 'pnpm') {
|
|
113
135
|
const pkg = await readRootPackageJson(rootDir)
|
|
114
|
-
const newScripts = computeRootScripts(sites)
|
|
136
|
+
const newScripts = computeRootScripts(sites, pm)
|
|
115
137
|
|
|
116
138
|
// If we're adding a second site, rename existing dev/preview to dev:{firstName}
|
|
117
139
|
if (sites.length === 2 && pkg.scripts?.dev) {
|
|
118
140
|
const firstName = sites[0].name
|
|
119
|
-
// Only rename if the existing dev
|
|
120
|
-
if (pkg.scripts.dev
|
|
141
|
+
// Only rename if the existing dev script references the first site's name
|
|
142
|
+
if (pkg.scripts.dev.includes(firstName)) {
|
|
121
143
|
pkg.scripts[`dev:${firstName}`] = pkg.scripts.dev
|
|
122
144
|
pkg.scripts[`preview:${firstName}`] = pkg.scripts.preview
|
|
123
145
|
}
|
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"}}"
|