uniweb 0.8.4 → 0.8.5
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 +14 -11
- package/package.json +3 -3
- package/partials/agents.md +16 -6
- package/src/commands/add.js +233 -86
- package/src/index.js +53 -31
- package/src/templates/resolver.js +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ Edit files in `site/pages/` and `foundation/src/sections/` to see changes instan
|
|
|
27
27
|
| Template | Description |
|
|
28
28
|
| --- | --- |
|
|
29
29
|
| **Starter** | Foundation + site + sample content (default) |
|
|
30
|
+
| **None** | Foundation + site with no content |
|
|
30
31
|
| **Marketing** | Landing page, features, pricing, testimonials |
|
|
31
32
|
| **Docs** | Documentation with sidebar and search |
|
|
32
33
|
| **Academic** | Research site with publications and team |
|
|
@@ -34,7 +35,8 @@ Edit files in `site/pages/` and `foundation/src/sections/` to see changes instan
|
|
|
34
35
|
| **Dynamic** | Live API data fetching with loading states |
|
|
35
36
|
| **Store** | E-commerce with product grid |
|
|
36
37
|
| **Extensions** | Multi-foundation with visual effects extension |
|
|
37
|
-
|
|
38
|
+
|
|
39
|
+
Use `--blank` for an empty workspace (no packages) — grow with `uniweb add`.
|
|
38
40
|
|
|
39
41
|
**See them live:** [View all template demos](https://uniweb.github.io/templates/)
|
|
40
42
|
|
|
@@ -234,31 +236,32 @@ Start simple. Add what you need, when you need it:
|
|
|
234
236
|
```bash
|
|
235
237
|
cd my-site
|
|
236
238
|
|
|
237
|
-
# Add a
|
|
238
|
-
npx uniweb add
|
|
239
|
+
# Add a co-located foundation + site pair
|
|
240
|
+
npx uniweb add project blog
|
|
241
|
+
|
|
242
|
+
# Add a second foundation at root
|
|
243
|
+
npx uniweb add foundation ui
|
|
239
244
|
|
|
240
|
-
# Add a site wired to
|
|
241
|
-
npx uniweb add site
|
|
245
|
+
# Add a site wired to a specific foundation
|
|
246
|
+
npx uniweb add site docs --foundation ui
|
|
242
247
|
|
|
243
248
|
# Add an extension (auto-wired to the only site)
|
|
244
249
|
npx uniweb add extension effects
|
|
245
250
|
|
|
246
251
|
# Scaffold + apply content from an official template
|
|
247
|
-
npx uniweb add
|
|
248
|
-
npx uniweb add site main --from marketing --foundation marketing
|
|
252
|
+
npx uniweb add project marketing --from marketing
|
|
249
253
|
```
|
|
250
254
|
|
|
251
|
-
The workspace grows organically. `add` handles placement, wires dependencies, updates workspace globs, and generates root scripts. Use `--path` to override default placement, or `--project` for co-located layouts
|
|
255
|
+
The workspace grows organically. `add` handles placement, wires dependencies, updates workspace globs, and generates root scripts. The name you provide becomes both the directory name and the package name. Use `--path` to override default placement, or `--project` for explicit co-located layouts.
|
|
252
256
|
|
|
253
257
|
> `npx uniweb` works before and after install. Once dependencies are installed, you can also use `pnpm uniweb` directly since `uniweb` is a project dependency.
|
|
254
258
|
|
|
255
259
|
**Or start blank and build up:**
|
|
256
260
|
|
|
257
261
|
```bash
|
|
258
|
-
pnpm create uniweb acme --
|
|
262
|
+
pnpm create uniweb acme --blank
|
|
259
263
|
cd acme
|
|
260
|
-
npx uniweb add
|
|
261
|
-
npx uniweb add site
|
|
264
|
+
npx uniweb add project main
|
|
262
265
|
pnpm install
|
|
263
266
|
pnpm dev
|
|
264
267
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
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.8.4",
|
|
45
44
|
"@uniweb/core": "0.5.4",
|
|
45
|
+
"@uniweb/kit": "0.7.3",
|
|
46
46
|
"@uniweb/runtime": "0.6.4",
|
|
47
|
-
"@uniweb/
|
|
47
|
+
"@uniweb/build": "0.8.4"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/partials/agents.md
CHANGED
|
@@ -27,17 +27,27 @@ pnpm create uniweb my-project
|
|
|
27
27
|
cd my-project && pnpm install
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
This creates a workspace with foundation + site + starter content — two commands to a dev server. Use `--template <name>` for an official template (`marketing`, `docs`, `academic`, etc.), `--template none` for foundation + site with no content, or `--blank` for an empty workspace.
|
|
31
31
|
|
|
32
|
-
### Adding
|
|
32
|
+
### Adding a co-located project
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
pnpm uniweb add
|
|
36
|
-
pnpm uniweb add site myname --project myname
|
|
35
|
+
pnpm uniweb add project docs
|
|
37
36
|
pnpm install
|
|
38
37
|
```
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
This creates `docs/foundation/` + `docs/site/` with package names `docs-foundation` and `docs-site`. Use `--from <template>` to apply template content to both packages.
|
|
40
|
+
|
|
41
|
+
### Adding individual packages
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pnpm uniweb add foundation # First foundation → ./foundation/
|
|
45
|
+
pnpm uniweb add foundation ui # Named → ./ui/
|
|
46
|
+
pnpm uniweb add site # First site → ./site/
|
|
47
|
+
pnpm uniweb add site blog # Named → ./blog/
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The name is both the directory name and the package name. Use `--project <name>` to co-locate under a project directory (e.g., `--project docs` → `docs/foundation/`).
|
|
41
51
|
|
|
42
52
|
### What the CLI generates
|
|
43
53
|
|
|
@@ -805,7 +815,7 @@ Uniweb section types do more with less because the framework handles concerns th
|
|
|
805
815
|
|
|
806
816
|
1. **Check if you're inside an existing Uniweb workspace** (look for `pnpm-workspace.yaml` and a `package.json` with `uniweb` as a dependency). If yes, use `pnpm uniweb add` to create projects inside it. If no, create a new workspace:
|
|
807
817
|
```bash
|
|
808
|
-
pnpm create uniweb my-project --template
|
|
818
|
+
pnpm create uniweb my-project --template none
|
|
809
819
|
```
|
|
810
820
|
|
|
811
821
|
3. **Use named layouts** for different page groups — a marketing layout for landing pages, a docs layout for `/docs/*`. One site, multiple layouts, each with its own header/footer/sidebar content.
|
package/src/commands/add.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Add Command
|
|
3
3
|
*
|
|
4
|
-
* Adds foundations, sites, or
|
|
4
|
+
* Adds foundations, sites, extensions, or co-located projects to an existing workspace.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
+
* uniweb add project [name] [--from <template>]
|
|
7
8
|
* uniweb add foundation [name] [--from <template>] [--path <dir>] [--project <name>]
|
|
8
9
|
* uniweb add site [name] [--from <template>] [--foundation <name>] [--path <dir>] [--project <name>]
|
|
9
10
|
* uniweb add extension [name] [--from <template>] [--site <name>] [--path <dir>]
|
|
@@ -14,7 +15,7 @@ import { readFile, writeFile } from 'node:fs/promises'
|
|
|
14
15
|
import { join, relative } from 'node:path'
|
|
15
16
|
import prompts from 'prompts'
|
|
16
17
|
import yaml from 'js-yaml'
|
|
17
|
-
import { scaffoldFoundation, scaffoldSite, applyContent, mergeTemplateDependencies } from '../utils/scaffold.js'
|
|
18
|
+
import { scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies } from '../utils/scaffold.js'
|
|
18
19
|
import {
|
|
19
20
|
readWorkspaceConfig,
|
|
20
21
|
addWorkspaceGlob,
|
|
@@ -118,12 +119,13 @@ export async function add(rawArgs) {
|
|
|
118
119
|
if (nonInteractive) {
|
|
119
120
|
error(`Missing subcommand.\n`)
|
|
120
121
|
log(formatOptions([
|
|
122
|
+
{ label: 'project', description: 'Co-located foundation + site pair' },
|
|
121
123
|
{ label: 'foundation', description: 'Component library' },
|
|
122
124
|
{ label: 'site', description: 'Content site' },
|
|
123
125
|
{ label: 'extension', description: 'Additional component package' },
|
|
124
126
|
]))
|
|
125
127
|
log('')
|
|
126
|
-
log(`Usage: ${prefix} add <foundation|site|extension> [name]`)
|
|
128
|
+
log(`Usage: ${prefix} add <project|foundation|site|extension> [name]`)
|
|
127
129
|
process.exit(1)
|
|
128
130
|
}
|
|
129
131
|
|
|
@@ -132,6 +134,7 @@ export async function add(rawArgs) {
|
|
|
132
134
|
name: 'subcommand',
|
|
133
135
|
message: 'What would you like to add?',
|
|
134
136
|
choices: [
|
|
137
|
+
{ title: 'Project', value: 'project', description: 'Co-located foundation + site pair' },
|
|
135
138
|
{ title: 'Foundation', value: 'foundation', description: 'Component library' },
|
|
136
139
|
{ title: 'Site', value: 'site', description: 'Content site' },
|
|
137
140
|
{ title: 'Extension', value: 'extension', description: 'Additional component package' },
|
|
@@ -154,6 +157,9 @@ export async function add(rawArgs) {
|
|
|
154
157
|
const projectName = rootPkg.name || 'my-project'
|
|
155
158
|
|
|
156
159
|
switch (parsed.subcommand) {
|
|
160
|
+
case 'project':
|
|
161
|
+
await addProject(rootDir, projectName, parsed, pm)
|
|
162
|
+
break
|
|
157
163
|
case 'foundation':
|
|
158
164
|
await addFoundation(rootDir, projectName, parsed, pm)
|
|
159
165
|
break
|
|
@@ -165,7 +171,7 @@ export async function add(rawArgs) {
|
|
|
165
171
|
break
|
|
166
172
|
default:
|
|
167
173
|
error(`Unknown subcommand: ${parsed.subcommand}`)
|
|
168
|
-
log(`Valid subcommands: foundation, site, extension`)
|
|
174
|
+
log(`Valid subcommands: project, foundation, site, extension`)
|
|
169
175
|
process.exit(1)
|
|
170
176
|
}
|
|
171
177
|
}
|
|
@@ -188,31 +194,28 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
188
194
|
|
|
189
195
|
// Interactive name prompt when name not provided and no --path
|
|
190
196
|
if (!name && !opts.path) {
|
|
191
|
-
if (isNonInteractive(process.argv)) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// Only set name if user chose something other than the default —
|
|
212
|
-
// null name tells resolveFoundationTarget to use default placement (./foundation/)
|
|
213
|
-
if (!hasDefault || response.name !== 'foundation') {
|
|
214
|
-
name = response.name
|
|
197
|
+
if (!isNonInteractive(process.argv)) {
|
|
198
|
+
const foundations = await discoverFoundations(rootDir)
|
|
199
|
+
const hasDefault = foundations.length === 0 && !existsSync(join(rootDir, 'foundation'))
|
|
200
|
+
const response = await prompts({
|
|
201
|
+
type: 'text',
|
|
202
|
+
name: 'name',
|
|
203
|
+
message: 'Foundation name:',
|
|
204
|
+
initial: hasDefault ? 'foundation' : undefined,
|
|
205
|
+
validate: (value) => validatePackageName(value),
|
|
206
|
+
}, {
|
|
207
|
+
onCancel: () => {
|
|
208
|
+
log('\nCancelled.')
|
|
209
|
+
process.exit(0)
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
// Only set name if user chose something other than the default —
|
|
213
|
+
// null name tells resolveFoundationTarget to use default placement (./foundation/)
|
|
214
|
+
if (!hasDefault || response.name !== 'foundation') {
|
|
215
|
+
name = response.name
|
|
216
|
+
}
|
|
215
217
|
}
|
|
218
|
+
// Non-interactive without name: defaults to 'foundation' — resolveFoundationTarget handles it
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
const target = await resolveFoundationTarget(rootDir, name, opts)
|
|
@@ -223,10 +226,12 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
223
226
|
process.exit(1)
|
|
224
227
|
}
|
|
225
228
|
|
|
226
|
-
//
|
|
227
|
-
|
|
229
|
+
// Package name = name or 'foundation'
|
|
230
|
+
const packageName = name || 'foundation'
|
|
228
231
|
if (existingNames.has(packageName)) {
|
|
229
|
-
|
|
232
|
+
error(`Package name '${packageName}' already exists in this workspace.`)
|
|
233
|
+
log(`Choose a different name: ${getCliPrefix()} add foundation <name>`)
|
|
234
|
+
process.exit(1)
|
|
230
235
|
}
|
|
231
236
|
|
|
232
237
|
// Scaffold
|
|
@@ -274,31 +279,28 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
274
279
|
|
|
275
280
|
// Interactive name prompt when name not provided and no --path
|
|
276
281
|
if (!name && !opts.path) {
|
|
277
|
-
if (isNonInteractive(process.argv)) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// Only set name if user chose something other than the default —
|
|
298
|
-
// null name tells resolveSiteTarget to use default placement (./site/)
|
|
299
|
-
if (!hasDefault || response.name !== 'site') {
|
|
300
|
-
name = response.name
|
|
282
|
+
if (!isNonInteractive(process.argv)) {
|
|
283
|
+
const existingSites = await discoverSites(rootDir)
|
|
284
|
+
const hasDefault = existingSites.length === 0 && !existsSync(join(rootDir, 'site'))
|
|
285
|
+
const response = await prompts({
|
|
286
|
+
type: 'text',
|
|
287
|
+
name: 'name',
|
|
288
|
+
message: 'Site name:',
|
|
289
|
+
initial: hasDefault ? 'site' : undefined,
|
|
290
|
+
validate: (value) => validatePackageName(value),
|
|
291
|
+
}, {
|
|
292
|
+
onCancel: () => {
|
|
293
|
+
log('\nCancelled.')
|
|
294
|
+
process.exit(0)
|
|
295
|
+
},
|
|
296
|
+
})
|
|
297
|
+
// Only set name if user chose something other than the default —
|
|
298
|
+
// null name tells resolveSiteTarget to use default placement (./site/)
|
|
299
|
+
if (!hasDefault || response.name !== 'site') {
|
|
300
|
+
name = response.name
|
|
301
|
+
}
|
|
301
302
|
}
|
|
303
|
+
// Non-interactive without name: defaults to 'site' — resolveSiteTarget handles it
|
|
302
304
|
}
|
|
303
305
|
|
|
304
306
|
const target = await resolveSiteTarget(rootDir, name, opts)
|
|
@@ -311,17 +313,13 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
311
313
|
|
|
312
314
|
// Resolve foundation
|
|
313
315
|
const foundation = await resolveFoundation(rootDir, opts.foundation)
|
|
314
|
-
let siteName
|
|
315
|
-
if (opts.project) {
|
|
316
|
-
// Co-located: convention is {project}-site (e.g., io-site)
|
|
317
|
-
siteName = (!name || name === opts.project) ? `${opts.project}-site` : name
|
|
318
|
-
} else {
|
|
319
|
-
siteName = name || 'site'
|
|
320
|
-
}
|
|
321
316
|
|
|
322
|
-
//
|
|
317
|
+
// Package name = name or 'site'
|
|
318
|
+
const siteName = name || 'site'
|
|
323
319
|
if (existingNames.has(siteName)) {
|
|
324
|
-
|
|
320
|
+
error(`Package name '${siteName}' already exists in this workspace.`)
|
|
321
|
+
log(`Choose a different name: ${getCliPrefix()} add site <name>`)
|
|
322
|
+
process.exit(1)
|
|
325
323
|
}
|
|
326
324
|
|
|
327
325
|
if (foundation) {
|
|
@@ -481,8 +479,117 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
481
479
|
log(`Next: ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
482
480
|
}
|
|
483
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Add a co-located foundation + site pair to the workspace
|
|
484
|
+
*/
|
|
485
|
+
async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
|
|
486
|
+
let name = opts.name
|
|
487
|
+
const existingNames = await getExistingPackageNames(rootDir)
|
|
488
|
+
|
|
489
|
+
// Validate name format
|
|
490
|
+
if (name) {
|
|
491
|
+
const valid = validatePackageName(name)
|
|
492
|
+
if (valid !== true) {
|
|
493
|
+
error(valid)
|
|
494
|
+
process.exit(1)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Interactive name prompt when name not provided
|
|
499
|
+
if (!name) {
|
|
500
|
+
if (isNonInteractive(process.argv)) {
|
|
501
|
+
error(`Missing project name.\n`)
|
|
502
|
+
log(`Usage: ${getCliPrefix()} add project <name>`)
|
|
503
|
+
process.exit(1)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const response = await prompts({
|
|
507
|
+
type: 'text',
|
|
508
|
+
name: 'name',
|
|
509
|
+
message: 'Project name:',
|
|
510
|
+
validate: (value) => validatePackageName(value),
|
|
511
|
+
}, {
|
|
512
|
+
onCancel: () => {
|
|
513
|
+
log('\nCancelled.')
|
|
514
|
+
process.exit(0)
|
|
515
|
+
},
|
|
516
|
+
})
|
|
517
|
+
name = response.name
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Check directory doesn't already exist
|
|
521
|
+
const projectDir = join(rootDir, name)
|
|
522
|
+
if (existsSync(projectDir)) {
|
|
523
|
+
error(`Directory already exists: ${name}/`)
|
|
524
|
+
process.exit(1)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Compute package names
|
|
528
|
+
const foundationPkgName = `${name}-foundation`
|
|
529
|
+
const sitePkgName = `${name}-site`
|
|
530
|
+
|
|
531
|
+
// Check package name collisions
|
|
532
|
+
for (const pkgName of [foundationPkgName, sitePkgName]) {
|
|
533
|
+
if (existingNames.has(pkgName)) {
|
|
534
|
+
error(`Package name '${pkgName}' already exists in this workspace.`)
|
|
535
|
+
process.exit(1)
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const progressCb = (msg) => info(` ${msg}`)
|
|
540
|
+
|
|
541
|
+
// Scaffold foundation
|
|
542
|
+
info(`Creating foundation: ${foundationPkgName}...`)
|
|
543
|
+
await scaffoldFoundation(join(projectDir, 'foundation'), {
|
|
544
|
+
name: foundationPkgName,
|
|
545
|
+
projectName,
|
|
546
|
+
isExtension: false,
|
|
547
|
+
}, { onProgress: progressCb })
|
|
548
|
+
|
|
549
|
+
// Scaffold site
|
|
550
|
+
info(`Creating site: ${sitePkgName}...`)
|
|
551
|
+
await scaffoldSite(join(projectDir, 'site'), {
|
|
552
|
+
name: sitePkgName,
|
|
553
|
+
projectName,
|
|
554
|
+
foundationName: foundationPkgName,
|
|
555
|
+
foundationPath: 'file:../foundation',
|
|
556
|
+
foundationRef: foundationPkgName,
|
|
557
|
+
}, { onProgress: progressCb })
|
|
558
|
+
|
|
559
|
+
// Apply template content if --from specified
|
|
560
|
+
if (opts.from) {
|
|
561
|
+
await applyFromTemplate(opts.from, 'foundation', join(projectDir, 'foundation'), projectName)
|
|
562
|
+
await applyFromTemplate(opts.from, 'site', join(projectDir, 'site'), projectName)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Update workspace globs for co-located layout
|
|
566
|
+
await addWorkspaceGlob(rootDir, '*/foundation')
|
|
567
|
+
await addWorkspaceGlob(rootDir, '*/site')
|
|
568
|
+
|
|
569
|
+
// Update root scripts
|
|
570
|
+
const sites = await discoverSites(rootDir)
|
|
571
|
+
if (!sites.find(s => s.path === `${name}/site`)) {
|
|
572
|
+
sites.push({ name: sitePkgName, path: `${name}/site` })
|
|
573
|
+
}
|
|
574
|
+
await updateRootScripts(rootDir, sites, pm)
|
|
575
|
+
|
|
576
|
+
success(`Created project '${name}' at ${name}/`)
|
|
577
|
+
log(` ${colors.dim}Foundation: ${name}/foundation/ (${foundationPkgName})${colors.reset}`)
|
|
578
|
+
log(` ${colors.dim}Site: ${name}/site/ (${sitePkgName})${colors.reset}`)
|
|
579
|
+
log('')
|
|
580
|
+
log(`Next: ${colors.cyan}${installCmd(pm)} && ${filterCmd(pm, sitePkgName, 'dev')}${colors.reset}`)
|
|
581
|
+
}
|
|
582
|
+
|
|
484
583
|
/**
|
|
485
584
|
* Resolve placement for a foundation
|
|
585
|
+
*
|
|
586
|
+
* Rules:
|
|
587
|
+
* - --path: use it directly
|
|
588
|
+
* - --project: {project}/foundation (co-located)
|
|
589
|
+
* - Existing co-located glob: follow pattern
|
|
590
|
+
* - Existing segregated glob: follow pattern
|
|
591
|
+
* - First foundation: dir name is the name (default: 'foundation')
|
|
592
|
+
* - Already have one: error in non-interactive, ask in interactive
|
|
486
593
|
*/
|
|
487
594
|
async function resolveFoundationTarget(rootDir, name, opts) {
|
|
488
595
|
if (opts.path) return opts.path
|
|
@@ -495,23 +602,43 @@ async function resolveFoundationTarget(rootDir, name, opts) {
|
|
|
495
602
|
const { packages } = await readWorkspaceConfig(rootDir)
|
|
496
603
|
const hasColocated = packages.some(p => p.includes('*/foundation'))
|
|
497
604
|
const hasFoundationsGlob = packages.some(p => p.startsWith('foundations/'))
|
|
498
|
-
const hasSingleFoundation = existsSync(join(rootDir, 'foundation'))
|
|
499
605
|
|
|
500
|
-
|
|
501
|
-
|
|
606
|
+
// Respect existing co-located layout
|
|
607
|
+
if (hasColocated && name) {
|
|
608
|
+
return `${name}/foundation`
|
|
502
609
|
}
|
|
503
610
|
|
|
504
|
-
//
|
|
505
|
-
if (
|
|
506
|
-
return 'foundation'
|
|
611
|
+
// Respect existing segregated layout
|
|
612
|
+
if (hasFoundationsGlob) {
|
|
613
|
+
return `foundations/${name || 'foundation'}`
|
|
507
614
|
}
|
|
508
615
|
|
|
509
|
-
//
|
|
510
|
-
|
|
616
|
+
// dir name = name or 'foundation'
|
|
617
|
+
const dirName = name || 'foundation'
|
|
618
|
+
|
|
619
|
+
// Check if target already exists
|
|
620
|
+
if (!existsSync(join(rootDir, dirName))) {
|
|
621
|
+
return dirName
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Already have one at the target path — error with guidance
|
|
625
|
+
if (isNonInteractive(process.argv)) {
|
|
626
|
+
error(`Directory '${dirName}' already exists.`)
|
|
627
|
+
log(`\nTo add another foundation, specify a name:`)
|
|
628
|
+
log(` ${getCliPrefix()} add foundation <name>`)
|
|
629
|
+
log(`\nOr use --path for explicit placement:`)
|
|
630
|
+
log(` ${getCliPrefix()} add foundation --path <dir>`)
|
|
631
|
+
process.exit(1)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Interactive: the existsSync check in addFoundation will catch it
|
|
635
|
+
return dirName
|
|
511
636
|
}
|
|
512
637
|
|
|
513
638
|
/**
|
|
514
639
|
* Resolve placement for a site
|
|
640
|
+
*
|
|
641
|
+
* Same rules as resolveFoundationTarget, adapted for sites.
|
|
515
642
|
*/
|
|
516
643
|
async function resolveSiteTarget(rootDir, name, opts) {
|
|
517
644
|
if (opts.path) return opts.path
|
|
@@ -523,19 +650,37 @@ async function resolveSiteTarget(rootDir, name, opts) {
|
|
|
523
650
|
const { packages } = await readWorkspaceConfig(rootDir)
|
|
524
651
|
const hasColocated = packages.some(p => p.includes('*/site'))
|
|
525
652
|
const hasSitesGlob = packages.some(p => p.startsWith('sites/'))
|
|
526
|
-
const hasSingleSite = existsSync(join(rootDir, 'site'))
|
|
527
653
|
|
|
528
|
-
|
|
529
|
-
|
|
654
|
+
// Respect existing co-located layout
|
|
655
|
+
if (hasColocated && name) {
|
|
656
|
+
return `${name}/site`
|
|
530
657
|
}
|
|
531
658
|
|
|
532
|
-
//
|
|
533
|
-
if (
|
|
534
|
-
return 'site'
|
|
659
|
+
// Respect existing segregated layout
|
|
660
|
+
if (hasSitesGlob) {
|
|
661
|
+
return `sites/${name || 'site'}`
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// dir name = name or 'site'
|
|
665
|
+
const dirName = name || 'site'
|
|
666
|
+
|
|
667
|
+
// Check if target already exists
|
|
668
|
+
if (!existsSync(join(rootDir, dirName))) {
|
|
669
|
+
return dirName
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Already have one at the target path — error with guidance
|
|
673
|
+
if (isNonInteractive(process.argv)) {
|
|
674
|
+
error(`Directory '${dirName}' already exists.`)
|
|
675
|
+
log(`\nTo add another site, specify a name:`)
|
|
676
|
+
log(` ${getCliPrefix()} add site <name>`)
|
|
677
|
+
log(`\nOr use --path for explicit placement:`)
|
|
678
|
+
log(` ${getCliPrefix()} add site --path <dir>`)
|
|
679
|
+
process.exit(1)
|
|
535
680
|
}
|
|
536
681
|
|
|
537
|
-
//
|
|
538
|
-
return
|
|
682
|
+
// Interactive: the existsSync check in addSite will catch it
|
|
683
|
+
return dirName
|
|
539
684
|
}
|
|
540
685
|
|
|
541
686
|
/**
|
|
@@ -737,9 +882,10 @@ function showAddHelp() {
|
|
|
737
882
|
log(`
|
|
738
883
|
${colors.cyan}${colors.bright}Uniweb Add${colors.reset}
|
|
739
884
|
|
|
740
|
-
Add foundations, sites, or extensions to your workspace.
|
|
885
|
+
Add projects, foundations, sites, or extensions to your workspace.
|
|
741
886
|
|
|
742
887
|
${colors.bright}Usage:${colors.reset}
|
|
888
|
+
uniweb add project [name] [options]
|
|
743
889
|
uniweb add foundation [name] [options]
|
|
744
890
|
uniweb add site [name] [options]
|
|
745
891
|
uniweb add extension <name> [options]
|
|
@@ -759,11 +905,12 @@ ${colors.bright}Extension Options:${colors.reset}
|
|
|
759
905
|
--site <name> Site to wire extension URL into
|
|
760
906
|
|
|
761
907
|
${colors.bright}Examples:${colors.reset}
|
|
762
|
-
uniweb add
|
|
763
|
-
uniweb add
|
|
764
|
-
uniweb add foundation
|
|
765
|
-
uniweb add
|
|
766
|
-
uniweb add site
|
|
908
|
+
uniweb add project docs # Create docs/foundation/ + docs/site/
|
|
909
|
+
uniweb add project docs --from academic # Co-located pair + academic content
|
|
910
|
+
uniweb add foundation # Create ./foundation/ at root
|
|
911
|
+
uniweb add foundation ui # Create ./ui/ at root
|
|
912
|
+
uniweb add site # Create ./site/ at root
|
|
913
|
+
uniweb add site blog --foundation marketing # Create ./blog/ wired to marketing
|
|
767
914
|
uniweb add extension effects --site site # Create ./extensions/effects/
|
|
768
915
|
uniweb add foundation --project docs # Create ./docs/foundation/ (co-located)
|
|
769
916
|
uniweb add site --project docs # Create ./docs/site/ (co-located)
|
package/src/index.js
CHANGED
|
@@ -44,8 +44,8 @@ const colors = {
|
|
|
44
44
|
|
|
45
45
|
// Template choices for interactive prompt
|
|
46
46
|
const TEMPLATE_CHOICES = [
|
|
47
|
+
{ title: 'None', value: 'none', description: 'Foundation + site with no content' },
|
|
47
48
|
{ title: 'Starter', value: 'starter', description: 'Foundation + site + sample content' },
|
|
48
|
-
{ title: 'Blank', value: 'blank', description: 'Empty workspace — grow with uniweb add' },
|
|
49
49
|
{ title: 'Marketing', value: 'marketing', description: 'Landing page, features, pricing, testimonials' },
|
|
50
50
|
{ title: 'Docs', value: 'docs', description: 'Documentation with sidebar and search' },
|
|
51
51
|
{ title: 'Academic', value: 'academic', description: 'Research site with publications and team' },
|
|
@@ -53,6 +53,7 @@ const TEMPLATE_CHOICES = [
|
|
|
53
53
|
{ title: 'International', value: 'international', description: 'Multilingual site with i18n and blog' },
|
|
54
54
|
{ title: 'Store', value: 'store', description: 'E-commerce with product grid' },
|
|
55
55
|
{ title: 'Extensions', value: 'extensions', description: 'Multi-foundation with visual effects extension' },
|
|
56
|
+
{ title: 'Blank workspace', value: 'blank', description: 'Empty workspace — grow with uniweb add' },
|
|
56
57
|
]
|
|
57
58
|
|
|
58
59
|
function log(message) {
|
|
@@ -75,7 +76,7 @@ function title(message) {
|
|
|
75
76
|
* Create a project using the new package template flow (default)
|
|
76
77
|
*/
|
|
77
78
|
async function createFromPackageTemplates(projectDir, projectName, options = {}) {
|
|
78
|
-
const { onProgress, onWarning, pm = 'pnpm' } = options
|
|
79
|
+
const { onProgress, onWarning, pm = 'pnpm', includeStarter = true } = options
|
|
79
80
|
|
|
80
81
|
onProgress?.('Setting up workspace...')
|
|
81
82
|
|
|
@@ -107,9 +108,11 @@ async function createFromPackageTemplates(projectDir, projectName, options = {})
|
|
|
107
108
|
foundationPath: 'file:../foundation',
|
|
108
109
|
}, { onProgress, onWarning })
|
|
109
110
|
|
|
110
|
-
// 4. Apply starter content
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
// 4. Apply starter content (unless creating a "none" project)
|
|
112
|
+
if (includeStarter) {
|
|
113
|
+
onProgress?.('Adding starter content...')
|
|
114
|
+
await applyStarter(projectDir, { projectName }, { onProgress, onWarning })
|
|
115
|
+
}
|
|
113
116
|
|
|
114
117
|
success(`Created project: ${projectName}`)
|
|
115
118
|
}
|
|
@@ -364,6 +367,15 @@ async function main() {
|
|
|
364
367
|
displayName = args[nameIndex + 1]
|
|
365
368
|
}
|
|
366
369
|
|
|
370
|
+
// Check for --blank flag
|
|
371
|
+
let isBlank = args.includes('--blank')
|
|
372
|
+
|
|
373
|
+
// Handle --template blank as alias for --blank
|
|
374
|
+
if (templateType === 'blank') {
|
|
375
|
+
isBlank = true
|
|
376
|
+
templateType = null
|
|
377
|
+
}
|
|
378
|
+
|
|
367
379
|
// Check for --no-git flag
|
|
368
380
|
const noGit = args.includes('--no-git')
|
|
369
381
|
|
|
@@ -377,19 +389,13 @@ async function main() {
|
|
|
377
389
|
// Non-interactive: fail with actionable message instead of prompting
|
|
378
390
|
if (nonInteractive && !projectName) {
|
|
379
391
|
error(`Missing project name.\n`)
|
|
380
|
-
log(`Usage: ${prefix} create <project-name> [--template <name>]`)
|
|
392
|
+
log(`Usage: ${prefix} create <project-name> [--template <name>] [--blank]`)
|
|
381
393
|
process.exit(1)
|
|
382
394
|
}
|
|
383
395
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
label: c.value,
|
|
388
|
-
description: c.description,
|
|
389
|
-
}))))
|
|
390
|
-
log('')
|
|
391
|
-
log(`Usage: ${prefix} create ${projectName || '<project-name>'} --template <name>`)
|
|
392
|
-
process.exit(1)
|
|
396
|
+
// Non-interactive: default to starter when no template specified
|
|
397
|
+
if (nonInteractive && !templateType && !isBlank) {
|
|
398
|
+
templateType = 'starter'
|
|
393
399
|
}
|
|
394
400
|
|
|
395
401
|
// Interactive prompts
|
|
@@ -421,13 +427,14 @@ async function main() {
|
|
|
421
427
|
process.exit(1)
|
|
422
428
|
}
|
|
423
429
|
|
|
424
|
-
// Prompt for template if not specified via --template
|
|
425
|
-
if (!templateType) {
|
|
430
|
+
// Prompt for template if not specified via --template or --blank
|
|
431
|
+
if (!templateType && !isBlank) {
|
|
426
432
|
const templateResponse = await prompts({
|
|
427
433
|
type: 'select',
|
|
428
434
|
name: 'template',
|
|
429
435
|
message: 'Template:',
|
|
430
436
|
choices: TEMPLATE_CHOICES,
|
|
437
|
+
initial: 1,
|
|
431
438
|
}, {
|
|
432
439
|
onCancel: () => {
|
|
433
440
|
log('\nScaffolding cancelled.')
|
|
@@ -435,6 +442,11 @@ async function main() {
|
|
|
435
442
|
},
|
|
436
443
|
})
|
|
437
444
|
templateType = templateResponse.template
|
|
445
|
+
// Handle "blank" selection from interactive prompt
|
|
446
|
+
if (templateType === 'blank') {
|
|
447
|
+
isBlank = true
|
|
448
|
+
templateType = null
|
|
449
|
+
}
|
|
438
450
|
}
|
|
439
451
|
|
|
440
452
|
const effectiveName = displayName || projectName
|
|
@@ -451,13 +463,22 @@ async function main() {
|
|
|
451
463
|
const progressCb = (msg) => log(` ${colors.dim}${msg}${colors.reset}`)
|
|
452
464
|
const warningCb = (msg) => log(` ${colors.yellow}Warning: ${msg}${colors.reset}`)
|
|
453
465
|
|
|
454
|
-
if (
|
|
455
|
-
// Blank workspace
|
|
466
|
+
if (isBlank) {
|
|
467
|
+
// Blank workspace (--blank or --template blank)
|
|
456
468
|
log('\nCreating blank workspace...')
|
|
457
469
|
await createBlankWorkspace(projectDir, effectiveName, {
|
|
458
470
|
onProgress: progressCb,
|
|
459
471
|
onWarning: warningCb,
|
|
460
472
|
})
|
|
473
|
+
} else if (templateType === 'none') {
|
|
474
|
+
// Foundation + site with no content
|
|
475
|
+
log('\nCreating project...')
|
|
476
|
+
await createFromPackageTemplates(projectDir, effectiveName, {
|
|
477
|
+
onProgress: progressCb,
|
|
478
|
+
onWarning: warningCb,
|
|
479
|
+
pm,
|
|
480
|
+
includeStarter: false,
|
|
481
|
+
})
|
|
461
482
|
} else if (templateType === 'starter') {
|
|
462
483
|
// Starter: foundation + site + sample content
|
|
463
484
|
log('\nCreating project...')
|
|
@@ -521,11 +542,10 @@ async function main() {
|
|
|
521
542
|
// Success message
|
|
522
543
|
title('Project created successfully!')
|
|
523
544
|
|
|
524
|
-
if (
|
|
545
|
+
if (isBlank) {
|
|
525
546
|
log(`Next steps:\n`)
|
|
526
547
|
log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
|
|
527
|
-
log(` ${colors.cyan}
|
|
528
|
-
log(` ${colors.cyan}npx uniweb add site${colors.reset}`)
|
|
548
|
+
log(` ${colors.cyan}${prefix} add project${colors.reset}`)
|
|
529
549
|
log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
530
550
|
log(` ${colors.cyan}${runCmd(pm, 'dev')}${colors.reset}`)
|
|
531
551
|
} else {
|
|
@@ -553,11 +573,13 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
553
573
|
i18n <cmd> Internationalization (extract, sync, status)
|
|
554
574
|
|
|
555
575
|
${colors.bright}Create Options:${colors.reset}
|
|
556
|
-
--template <type> Project template (
|
|
576
|
+
--template <type> Project template (default: starter)
|
|
577
|
+
--blank Create an empty workspace (grow with uniweb add)
|
|
557
578
|
--name <name> Project display name
|
|
558
579
|
--no-git Skip git repository initialization
|
|
559
580
|
|
|
560
581
|
${colors.bright}Add Subcommands:${colors.reset}
|
|
582
|
+
add project [name] Add a co-located foundation + site pair
|
|
561
583
|
add foundation [name] Add a foundation (--from, --path, --project)
|
|
562
584
|
add site [name] Add a site (--from, --foundation, --path, --project)
|
|
563
585
|
add extension <name> Add an extension (--from, --site, --path)
|
|
@@ -594,7 +616,7 @@ ${colors.bright}i18n Commands:${colors.reset}
|
|
|
594
616
|
|
|
595
617
|
${colors.bright}Template Types:${colors.reset}
|
|
596
618
|
starter Foundation + site + sample content (default)
|
|
597
|
-
|
|
619
|
+
none Foundation + site with no content
|
|
598
620
|
marketing Official marketing template
|
|
599
621
|
./path/to/template Local directory
|
|
600
622
|
@scope/template-name npm package
|
|
@@ -602,17 +624,17 @@ ${colors.bright}Template Types:${colors.reset}
|
|
|
602
624
|
https://github.com/user/repo GitHub URL
|
|
603
625
|
|
|
604
626
|
${colors.bright}Examples:${colors.reset}
|
|
605
|
-
npx uniweb create my-project #
|
|
606
|
-
npx uniweb create my-project --template
|
|
607
|
-
npx uniweb create my-project --
|
|
627
|
+
npx uniweb create my-project # Foundation + site + starter content
|
|
628
|
+
npx uniweb create my-project --template none # Foundation + site, no content
|
|
629
|
+
npx uniweb create my-project --blank # Empty workspace
|
|
608
630
|
npx uniweb create my-project --template marketing # Official template
|
|
609
631
|
npx uniweb create my-project --template ./my-template # Local template
|
|
610
632
|
|
|
611
633
|
cd my-project
|
|
612
|
-
npx uniweb add
|
|
613
|
-
npx uniweb add
|
|
614
|
-
npx uniweb add
|
|
615
|
-
npx uniweb add site blog --
|
|
634
|
+
npx uniweb add project docs # Add docs/foundation/ + docs/site/
|
|
635
|
+
npx uniweb add project docs --from academic # Co-located pair + academic content
|
|
636
|
+
npx uniweb add foundation # Add foundation at root
|
|
637
|
+
npx uniweb add site blog --foundation marketing # Add site wired to marketing
|
|
616
638
|
npx uniweb add extension effects --site site # Add extensions/effects/
|
|
617
639
|
|
|
618
640
|
npx uniweb build
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
// Built-in templates (programmatic, not file-based)
|
|
6
|
-
export const BUILTIN_TEMPLATES = ['blank', 'starter']
|
|
6
|
+
export const BUILTIN_TEMPLATES = ['blank', 'starter', 'none']
|
|
7
7
|
|
|
8
8
|
// Official templates from @uniweb/templates package (downloaded from GitHub releases)
|
|
9
9
|
export const OFFICIAL_TEMPLATES = ['marketing', 'academic', 'docs', 'international', 'dynamic', 'store', 'extensions']
|