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 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
- pnpm create uniweb my-site --template marketing
11
- cd my-site
12
- pnpm install
13
- pnpm dev
10
+ npm create uniweb
14
11
  ```
15
12
 
16
- Open http://localhost:5173 to see your site. Edit files in `site/pages/` and `foundation/src/sections/` to see changes instantly.
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
- ### Development Commands
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
- pnpm dev # Start development server
26
- pnpm build # Build foundation + site for production
27
- pnpm preview # Preview the production build
18
+ cd my-project
19
+ npm install
20
+ npm run dev
28
21
  ```
29
22
 
30
- 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.
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
- ```bash
37
- # Multilingual business site (English, Spanish, French)
38
- pnpm create uniweb my-site --template international
25
+ ### Templates
39
26
 
40
- # Academic site (researcher portfolios, lab pages)
41
- pnpm create uniweb my-site --template academic
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
- # Documentation site
44
- pnpm create uniweb my-site --template docs
39
+ **See them live:** [View all template demos](https://uniweb.github.io/templates/)
45
40
 
46
- # Online store
47
- pnpm create uniweb my-site --template store
41
+ You can also skip the interactive prompt with `--template`:
48
42
 
49
- # Dynamic content (data sources, API-driven pages)
50
- pnpm create uniweb my-site --template dynamic
43
+ ```bash
44
+ npm create uniweb my-site -- --template docs
45
+ ```
51
46
 
52
- # Extensions demo (primary foundation + effects extension)
53
- pnpm create uniweb my-site --template extensions
47
+ ### Development Commands
54
48
 
55
- # Default starter (foundation + site + sample content)
56
- pnpm create uniweb my-site --template starter
49
+ Run these from the **project root**:
57
50
 
58
- # Blank workspace (grow with `add`)
59
- pnpm create uniweb my-site --template blank
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
- **See them live:** [View all template demos](https://uniweb.github.io/templates/)
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 `pnpm uniweb docs` to create `COMPONENTS.md` with all available components, their parameters, and presets.
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** | `pnpm add @acme/foundation` | Distributing via standard package tooling |
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
- pnpm create uniweb acme --template blank
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
- pnpm install && pnpm dev
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
- When you `add` more packages, the CLI adds the appropriate globs automatically:
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.5",
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/build": "0.7.3",
45
- "@uniweb/runtime": "0.6.0",
46
- "@uniweb/core": "0.5.0",
47
- "@uniweb/kit": "0.6.0"
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
  }
@@ -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 (!args.length || args[0] === '--help' || args[0] === '-h') {
94
+ if (args[0] === '--help' || args[0] === '-h') {
94
95
  showAddHelp()
95
96
  return
96
97
  }
97
98
 
98
- const parsed = parseArgs(args)
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
- const name = opts.name
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}pnpm install${colors.reset}`)
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
- const name = opts.name
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
- // Scaffold
196
- await scaffoldSite(fullPath, {
197
- name: siteName,
198
- projectName,
199
- foundationName: foundation.name,
200
- foundationPath,
201
- foundationRef: foundation.name,
202
- }, {
203
- onProgress: (msg) => info(` ${msg}`),
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
- success(`Created site '${siteName}' at ${target}/ → foundation '${foundation.name}'`)
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}pnpm install && pnpm --filter ${siteName} dev${colors.reset}`)
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
- const name = opts.name
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
- error('Extension name is required: uniweb add extension <name>')
236
- process.exit(1)
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}pnpm install${colors.reset}`)
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
 
@@ -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: pnpm --filter ${matchingFoundation.name} build${colors.reset}`)
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: pnpm --filter ${ext.name} build${colors.reset}`)
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: 'pnpm --filter site dev',
86
+ dev: filterCmd(pm, 'site', 'dev'),
86
87
  build: 'uniweb build',
87
- preview: 'pnpm --filter site 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 = `pnpm --filter ${sites[0].name} dev`
160
- scripts.preview = `pnpm --filter ${sites[0].name} 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}`] = `pnpm --filter ${s.name} dev`
164
- scripts[`preview:${s.name}`] = `pnpm --filter ${s.name} preview`
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 = `pnpm --filter ${sites[0].name} dev`
169
- scripts.preview = `pnpm --filter ${sites[0].name} 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}pnpm install${colors.reset}`)
503
- log(` ${colors.cyan}pnpm dev${colors.reset}`)
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}pnpm install${colors.reset}`)
508
- log(` ${colors.cyan}pnpm dev${colors.reset}`)
512
+ log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
513
+ log(` ${colors.cyan}${runCmd(pm, 'dev')}${colors.reset}`)
509
514
  }
510
515
  log('')
511
516
  }
@@ -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 pnpm-workspace.yaml
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 (!existsSync(configPath)) {
21
- return { packages: [] }
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
- const content = await readFile(configPath, 'utf-8')
24
- const config = yaml.load(content)
25
- return { packages: config?.packages || [] }
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 = `pnpm --filter ${sites[0].name} dev`
91
- scripts.preview = `pnpm --filter ${sites[0].name} 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 = `pnpm --filter ${sites[0].name} dev`
95
- scripts.preview = `pnpm --filter ${sites[0].name} 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}`] = `pnpm --filter ${sites[i].name} dev`
100
- scripts[`preview:${sites[i].name}`] = `pnpm --filter ${sites[i].name} preview`
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 matches the first site
120
- if (pkg.scripts.dev === `pnpm --filter ${firstName} 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
  }
@@ -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
+ }
@@ -1,24 +1,45 @@
1
1
  /**
2
2
  * Workspace Detection Utilities
3
3
  *
4
- * Detects pnpm workspace structure and classifies packages as foundations or sites.
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
- * Find workspace root by looking for pnpm-workspace.yaml
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 (existsSync(join(dir, 'pnpm-workspace.yaml'))) {
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
- const content = await readFile(configPath, 'utf-8')
81
- const config = yaml.load(content)
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
- if (!config?.packages || !Array.isArray(config.packages)) {
84
- return []
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 resolvePatterns(config.packages, workspaceRoot)
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 existsSync(join(dir, 'pnpm-workspace.yaml'))
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"}}"