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 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
- 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.
13
+ The interactive prompt asks for a project name and template. Pick one, then:
17
14
 
18
- > **Need pnpm?** Run `npm install -g pnpm` or see [pnpm installation](https://pnpm.io/installation).
15
+ ```bash
16
+ cd my-project
17
+ npm install
18
+ npm run dev
19
+ ```
19
20
 
20
- ### Development Commands
21
+ Edit files in `site/pages/` and `foundation/src/sections/` to see changes instantly.
21
22
 
22
- Run these from the **project root** (where `pnpm-workspace.yaml` is):
23
+ > **pnpm ready** — `pnpm create uniweb` works out of the box. Projects include both `pnpm-workspace.yaml` and npm workspaces.
23
24
 
24
- ```bash
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
- 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.
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
- The `marketing` template includes real components (Hero, Features, Pricing, Testimonials, FAQ, and more) with sample content—a working site you can explore and modify.
39
+ **See them live:** [View all template demos](https://uniweb.github.io/templates/)
33
40
 
34
- **Other templates:**
41
+ You can also skip the interactive prompt with `--template`:
35
42
 
36
43
  ```bash
37
- # Multilingual business site (English, Spanish, French)
38
- pnpm create uniweb my-site --template international
44
+ npm create uniweb my-site -- --template docs
45
+ ```
39
46
 
40
- # Academic site (researcher portfolios, lab pages)
41
- pnpm create uniweb my-site --template academic
47
+ or
42
48
 
43
- # Documentation site
49
+ ```bash
44
50
  pnpm create uniweb my-site --template docs
51
+ ```
45
52
 
46
- # Online store
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
- # Default starter (foundation + site + sample content)
56
- pnpm create uniweb my-site --template starter
55
+ Run these from the **project root**:
57
56
 
58
- # Blank workspace (grow with `add`)
59
- pnpm create uniweb my-site --template blank
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
- **See them live:** [View all template demos](https://uniweb.github.io/templates/)
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 `pnpm uniweb docs` to create `COMPONENTS.md` with all available components, their parameters, and presets.
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** | `pnpm add @acme/foundation` | Distributing via standard package tooling |
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
- pnpm create uniweb acme --template blank
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
- pnpm install && pnpm dev
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
- When you `add` more packages, the CLI adds the appropriate globs automatically:
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.6",
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/core": "0.5.1",
44
+ "@uniweb/build": "0.7.5",
45
+ "@uniweb/kit": "0.6.1",
45
46
  "@uniweb/runtime": "0.6.1",
46
- "@uniweb/build": "0.7.4",
47
- "@uniweb/kit": "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,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
- 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
+ // 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}pnpm install${colors.reset}`)
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
- const name = opts.name
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
- // 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
- })
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
- success(`Created site '${siteName}' at ${target}/ → foundation '${foundation.name}'`)
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}pnpm install && pnpm --filter ${siteName} dev${colors.reset}`)
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
- const name = opts.name
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
- error('Extension name is required: uniweb add extension <name>')
236
- process.exit(1)
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}pnpm install${colors.reset}`)
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
 
@@ -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,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 = `pnpm --filter ${sites[0].name} dev`
91
- scripts.preview = `pnpm --filter ${sites[0].name} 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 = `pnpm --filter ${sites[0].name} dev`
95
- scripts.preview = `pnpm --filter ${sites[0].name} 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}`] = `pnpm --filter ${sites[i].name} dev`
100
- scripts[`preview:${sites[i].name}`] = `pnpm --filter ${sites[i].name} preview`
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 matches the first site
120
- if (pkg.scripts.dev === `pnpm --filter ${firstName} 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
  }
@@ -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"}}"