uniweb 0.8.3 → 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 +4 -4
- package/partials/agents.md +16 -6
- package/src/commands/add.js +268 -75
- package/src/commands/docs.js +7 -0
- package/src/index.js +71 -22
- package/src/templates/resolver.js +1 -1
- package/src/utils/interactive.js +53 -0
- package/templates/site/theme.yml +99 -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/runtime": "0.6.4",
|
|
45
|
-
"@uniweb/build": "0.8.3",
|
|
46
44
|
"@uniweb/core": "0.5.4",
|
|
47
|
-
"@uniweb/kit": "0.7.3"
|
|
45
|
+
"@uniweb/kit": "0.7.3",
|
|
46
|
+
"@uniweb/runtime": "0.6.4",
|
|
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,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
import { validatePackageName, getExistingPackageNames, resolveUniqueName } from '../utils/names.js'
|
|
26
27
|
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
27
28
|
import { detectPackageManager, filterCmd, installCmd } from '../utils/pm.js'
|
|
29
|
+
import { isNonInteractive, getCliPrefix, stripNonInteractiveFlag, formatOptions } from '../utils/interactive.js'
|
|
28
30
|
import { resolveTemplate } from '../templates/index.js'
|
|
29
31
|
import { validateTemplate } from '../templates/validator.js'
|
|
30
32
|
import { getVersionsForTemplates } from '../versions.js'
|
|
@@ -91,13 +93,17 @@ function parseArgs(args) {
|
|
|
91
93
|
/**
|
|
92
94
|
* Main add command handler
|
|
93
95
|
*/
|
|
94
|
-
export async function add(
|
|
96
|
+
export async function add(rawArgs) {
|
|
97
|
+
const nonInteractive = isNonInteractive(rawArgs)
|
|
98
|
+
const args = stripNonInteractiveFlag(rawArgs)
|
|
99
|
+
|
|
95
100
|
if (args[0] === '--help' || args[0] === '-h') {
|
|
96
101
|
showAddHelp()
|
|
97
102
|
return
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
const pm = detectPackageManager()
|
|
106
|
+
const prefix = getCliPrefix()
|
|
101
107
|
|
|
102
108
|
// Find workspace root
|
|
103
109
|
const rootDir = findWorkspaceRoot()
|
|
@@ -110,11 +116,25 @@ export async function add(args) {
|
|
|
110
116
|
// Interactive subcommand chooser when no args given
|
|
111
117
|
let parsed
|
|
112
118
|
if (!args.length || (args[0] && args[0].startsWith('--'))) {
|
|
119
|
+
if (nonInteractive) {
|
|
120
|
+
error(`Missing subcommand.\n`)
|
|
121
|
+
log(formatOptions([
|
|
122
|
+
{ label: 'project', description: 'Co-located foundation + site pair' },
|
|
123
|
+
{ label: 'foundation', description: 'Component library' },
|
|
124
|
+
{ label: 'site', description: 'Content site' },
|
|
125
|
+
{ label: 'extension', description: 'Additional component package' },
|
|
126
|
+
]))
|
|
127
|
+
log('')
|
|
128
|
+
log(`Usage: ${prefix} add <project|foundation|site|extension> [name]`)
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
|
131
|
+
|
|
113
132
|
const response = await prompts({
|
|
114
133
|
type: 'select',
|
|
115
134
|
name: 'subcommand',
|
|
116
135
|
message: 'What would you like to add?',
|
|
117
136
|
choices: [
|
|
137
|
+
{ title: 'Project', value: 'project', description: 'Co-located foundation + site pair' },
|
|
118
138
|
{ title: 'Foundation', value: 'foundation', description: 'Component library' },
|
|
119
139
|
{ title: 'Site', value: 'site', description: 'Content site' },
|
|
120
140
|
{ title: 'Extension', value: 'extension', description: 'Additional component package' },
|
|
@@ -137,6 +157,9 @@ export async function add(args) {
|
|
|
137
157
|
const projectName = rootPkg.name || 'my-project'
|
|
138
158
|
|
|
139
159
|
switch (parsed.subcommand) {
|
|
160
|
+
case 'project':
|
|
161
|
+
await addProject(rootDir, projectName, parsed, pm)
|
|
162
|
+
break
|
|
140
163
|
case 'foundation':
|
|
141
164
|
await addFoundation(rootDir, projectName, parsed, pm)
|
|
142
165
|
break
|
|
@@ -148,7 +171,7 @@ export async function add(args) {
|
|
|
148
171
|
break
|
|
149
172
|
default:
|
|
150
173
|
error(`Unknown subcommand: ${parsed.subcommand}`)
|
|
151
|
-
log(`Valid subcommands: foundation, site, extension`)
|
|
174
|
+
log(`Valid subcommands: project, foundation, site, extension`)
|
|
152
175
|
process.exit(1)
|
|
153
176
|
}
|
|
154
177
|
}
|
|
@@ -171,25 +194,28 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
171
194
|
|
|
172
195
|
// Interactive name prompt when name not provided and no --path
|
|
173
196
|
if (!name && !opts.path) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
}
|
|
192
217
|
}
|
|
218
|
+
// Non-interactive without name: defaults to 'foundation' — resolveFoundationTarget handles it
|
|
193
219
|
}
|
|
194
220
|
|
|
195
221
|
const target = await resolveFoundationTarget(rootDir, name, opts)
|
|
@@ -200,10 +226,12 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
200
226
|
process.exit(1)
|
|
201
227
|
}
|
|
202
228
|
|
|
203
|
-
//
|
|
204
|
-
|
|
229
|
+
// Package name = name or 'foundation'
|
|
230
|
+
const packageName = name || 'foundation'
|
|
205
231
|
if (existingNames.has(packageName)) {
|
|
206
|
-
|
|
232
|
+
error(`Package name '${packageName}' already exists in this workspace.`)
|
|
233
|
+
log(`Choose a different name: ${getCliPrefix()} add foundation <name>`)
|
|
234
|
+
process.exit(1)
|
|
207
235
|
}
|
|
208
236
|
|
|
209
237
|
// Scaffold
|
|
@@ -251,25 +279,28 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
251
279
|
|
|
252
280
|
// Interactive name prompt when name not provided and no --path
|
|
253
281
|
if (!name && !opts.path) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
}
|
|
272
302
|
}
|
|
303
|
+
// Non-interactive without name: defaults to 'site' — resolveSiteTarget handles it
|
|
273
304
|
}
|
|
274
305
|
|
|
275
306
|
const target = await resolveSiteTarget(rootDir, name, opts)
|
|
@@ -282,17 +313,13 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
282
313
|
|
|
283
314
|
// Resolve foundation
|
|
284
315
|
const foundation = await resolveFoundation(rootDir, opts.foundation)
|
|
285
|
-
let siteName
|
|
286
|
-
if (opts.project) {
|
|
287
|
-
// Co-located: convention is {project}-site (e.g., io-site)
|
|
288
|
-
siteName = (!name || name === opts.project) ? `${opts.project}-site` : name
|
|
289
|
-
} else {
|
|
290
|
-
siteName = name || 'site'
|
|
291
|
-
}
|
|
292
316
|
|
|
293
|
-
//
|
|
317
|
+
// Package name = name or 'site'
|
|
318
|
+
const siteName = name || 'site'
|
|
294
319
|
if (existingNames.has(siteName)) {
|
|
295
|
-
|
|
320
|
+
error(`Package name '${siteName}' already exists in this workspace.`)
|
|
321
|
+
log(`Choose a different name: ${getCliPrefix()} add site <name>`)
|
|
322
|
+
process.exit(1)
|
|
296
323
|
}
|
|
297
324
|
|
|
298
325
|
if (foundation) {
|
|
@@ -371,6 +398,12 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
371
398
|
|
|
372
399
|
// Interactive name prompt when name not provided
|
|
373
400
|
if (!name) {
|
|
401
|
+
if (isNonInteractive(process.argv)) {
|
|
402
|
+
error(`Missing extension name.\n`)
|
|
403
|
+
log(`Usage: ${getCliPrefix()} add extension <name>`)
|
|
404
|
+
process.exit(1)
|
|
405
|
+
}
|
|
406
|
+
|
|
374
407
|
const response = await prompts({
|
|
375
408
|
type: 'text',
|
|
376
409
|
name: 'name',
|
|
@@ -446,8 +479,117 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
446
479
|
log(`Next: ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
447
480
|
}
|
|
448
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
|
+
|
|
449
583
|
/**
|
|
450
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
|
|
451
593
|
*/
|
|
452
594
|
async function resolveFoundationTarget(rootDir, name, opts) {
|
|
453
595
|
if (opts.path) return opts.path
|
|
@@ -460,23 +602,43 @@ async function resolveFoundationTarget(rootDir, name, opts) {
|
|
|
460
602
|
const { packages } = await readWorkspaceConfig(rootDir)
|
|
461
603
|
const hasColocated = packages.some(p => p.includes('*/foundation'))
|
|
462
604
|
const hasFoundationsGlob = packages.some(p => p.startsWith('foundations/'))
|
|
463
|
-
const hasSingleFoundation = existsSync(join(rootDir, 'foundation'))
|
|
464
605
|
|
|
465
|
-
|
|
466
|
-
|
|
606
|
+
// Respect existing co-located layout
|
|
607
|
+
if (hasColocated && name) {
|
|
608
|
+
return `${name}/foundation`
|
|
467
609
|
}
|
|
468
610
|
|
|
469
|
-
//
|
|
470
|
-
if (
|
|
471
|
-
return 'foundation'
|
|
611
|
+
// Respect existing segregated layout
|
|
612
|
+
if (hasFoundationsGlob) {
|
|
613
|
+
return `foundations/${name || 'foundation'}`
|
|
472
614
|
}
|
|
473
615
|
|
|
474
|
-
//
|
|
475
|
-
|
|
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
|
|
476
636
|
}
|
|
477
637
|
|
|
478
638
|
/**
|
|
479
639
|
* Resolve placement for a site
|
|
640
|
+
*
|
|
641
|
+
* Same rules as resolveFoundationTarget, adapted for sites.
|
|
480
642
|
*/
|
|
481
643
|
async function resolveSiteTarget(rootDir, name, opts) {
|
|
482
644
|
if (opts.path) return opts.path
|
|
@@ -488,19 +650,37 @@ async function resolveSiteTarget(rootDir, name, opts) {
|
|
|
488
650
|
const { packages } = await readWorkspaceConfig(rootDir)
|
|
489
651
|
const hasColocated = packages.some(p => p.includes('*/site'))
|
|
490
652
|
const hasSitesGlob = packages.some(p => p.startsWith('sites/'))
|
|
491
|
-
const hasSingleSite = existsSync(join(rootDir, 'site'))
|
|
492
653
|
|
|
493
|
-
|
|
494
|
-
|
|
654
|
+
// Respect existing co-located layout
|
|
655
|
+
if (hasColocated && name) {
|
|
656
|
+
return `${name}/site`
|
|
657
|
+
}
|
|
658
|
+
|
|
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
|
|
495
670
|
}
|
|
496
671
|
|
|
497
|
-
//
|
|
498
|
-
if (
|
|
499
|
-
|
|
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)
|
|
500
680
|
}
|
|
501
681
|
|
|
502
|
-
//
|
|
503
|
-
return
|
|
682
|
+
// Interactive: the existsSync check in addSite will catch it
|
|
683
|
+
return dirName
|
|
504
684
|
}
|
|
505
685
|
|
|
506
686
|
/**
|
|
@@ -529,7 +709,18 @@ async function resolveFoundation(rootDir, foundationFlag) {
|
|
|
529
709
|
return foundations[0]
|
|
530
710
|
}
|
|
531
711
|
|
|
532
|
-
// Multiple foundations — prompt
|
|
712
|
+
// Multiple foundations — prompt (or fail in non-interactive mode)
|
|
713
|
+
if (isNonInteractive(process.argv)) {
|
|
714
|
+
error(`Multiple foundations found. Specify which to use:\n`)
|
|
715
|
+
log(formatOptions(foundations.map(f => ({
|
|
716
|
+
label: f.name,
|
|
717
|
+
description: f.path,
|
|
718
|
+
}))))
|
|
719
|
+
log('')
|
|
720
|
+
log(`Usage: ${getCliPrefix()} add site <name> --foundation <name>`)
|
|
721
|
+
process.exit(1)
|
|
722
|
+
}
|
|
723
|
+
|
|
533
724
|
const response = await prompts({
|
|
534
725
|
type: 'select',
|
|
535
726
|
name: 'foundation',
|
|
@@ -691,9 +882,10 @@ function showAddHelp() {
|
|
|
691
882
|
log(`
|
|
692
883
|
${colors.cyan}${colors.bright}Uniweb Add${colors.reset}
|
|
693
884
|
|
|
694
|
-
Add foundations, sites, or extensions to your workspace.
|
|
885
|
+
Add projects, foundations, sites, or extensions to your workspace.
|
|
695
886
|
|
|
696
887
|
${colors.bright}Usage:${colors.reset}
|
|
888
|
+
uniweb add project [name] [options]
|
|
697
889
|
uniweb add foundation [name] [options]
|
|
698
890
|
uniweb add site [name] [options]
|
|
699
891
|
uniweb add extension <name> [options]
|
|
@@ -713,11 +905,12 @@ ${colors.bright}Extension Options:${colors.reset}
|
|
|
713
905
|
--site <name> Site to wire extension URL into
|
|
714
906
|
|
|
715
907
|
${colors.bright}Examples:${colors.reset}
|
|
716
|
-
uniweb add
|
|
717
|
-
uniweb add
|
|
718
|
-
uniweb add foundation
|
|
719
|
-
uniweb add
|
|
720
|
-
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
|
|
721
914
|
uniweb add extension effects --site site # Create ./extensions/effects/
|
|
722
915
|
uniweb add foundation --project docs # Create ./docs/foundation/ (co-located)
|
|
723
916
|
uniweb add site --project docs # Create ./docs/site/ (co-located)
|
package/src/commands/docs.js
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
findFoundations,
|
|
30
30
|
promptSelect,
|
|
31
31
|
} from '../utils/workspace.js'
|
|
32
|
+
import { isNonInteractive, getCliPrefix, formatOptions } from '../utils/interactive.js'
|
|
32
33
|
|
|
33
34
|
// Colors for terminal output
|
|
34
35
|
const colors = {
|
|
@@ -394,6 +395,12 @@ async function generateComponentDocs(args) {
|
|
|
394
395
|
if (foundations.length === 1) {
|
|
395
396
|
targetFoundation = foundations[0]
|
|
396
397
|
info(`Found foundation: ${targetFoundation}`)
|
|
398
|
+
} else if (isNonInteractive(process.argv)) {
|
|
399
|
+
error(`Multiple foundations found. Specify which to target:\n`)
|
|
400
|
+
log(formatOptions(foundations.map(f => ({ label: f, description: '' }))))
|
|
401
|
+
log('')
|
|
402
|
+
log(`Usage: ${getCliPrefix()} docs --target <path>`)
|
|
403
|
+
process.exit(1)
|
|
397
404
|
} else {
|
|
398
405
|
log(`${colors.dim}Multiple foundations found in workspace.${colors.reset}\n`)
|
|
399
406
|
targetFoundation = await promptSelect('Select foundation:', foundations)
|
package/src/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import { validateTemplate } from './templates/validator.js'
|
|
30
30
|
import { scaffoldWorkspace, scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies } from './utils/scaffold.js'
|
|
31
31
|
import { detectPackageManager, filterCmd, installCmd, runCmd } from './utils/pm.js'
|
|
32
|
+
import { isNonInteractive, getCliPrefix, stripNonInteractiveFlag, formatOptions } from './utils/interactive.js'
|
|
32
33
|
|
|
33
34
|
// Colors for terminal output
|
|
34
35
|
const colors = {
|
|
@@ -43,8 +44,8 @@ const colors = {
|
|
|
43
44
|
|
|
44
45
|
// Template choices for interactive prompt
|
|
45
46
|
const TEMPLATE_CHOICES = [
|
|
47
|
+
{ title: 'None', value: 'none', description: 'Foundation + site with no content' },
|
|
46
48
|
{ title: 'Starter', value: 'starter', description: 'Foundation + site + sample content' },
|
|
47
|
-
{ title: 'Blank', value: 'blank', description: 'Empty workspace — grow with uniweb add' },
|
|
48
49
|
{ title: 'Marketing', value: 'marketing', description: 'Landing page, features, pricing, testimonials' },
|
|
49
50
|
{ title: 'Docs', value: 'docs', description: 'Documentation with sidebar and search' },
|
|
50
51
|
{ title: 'Academic', value: 'academic', description: 'Research site with publications and team' },
|
|
@@ -52,6 +53,7 @@ const TEMPLATE_CHOICES = [
|
|
|
52
53
|
{ title: 'International', value: 'international', description: 'Multilingual site with i18n and blog' },
|
|
53
54
|
{ title: 'Store', value: 'store', description: 'E-commerce with product grid' },
|
|
54
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' },
|
|
55
57
|
]
|
|
56
58
|
|
|
57
59
|
function log(message) {
|
|
@@ -74,7 +76,7 @@ function title(message) {
|
|
|
74
76
|
* Create a project using the new package template flow (default)
|
|
75
77
|
*/
|
|
76
78
|
async function createFromPackageTemplates(projectDir, projectName, options = {}) {
|
|
77
|
-
const { onProgress, onWarning, pm = 'pnpm' } = options
|
|
79
|
+
const { onProgress, onWarning, pm = 'pnpm', includeStarter = true } = options
|
|
78
80
|
|
|
79
81
|
onProgress?.('Setting up workspace...')
|
|
80
82
|
|
|
@@ -106,9 +108,11 @@ async function createFromPackageTemplates(projectDir, projectName, options = {})
|
|
|
106
108
|
foundationPath: 'file:../foundation',
|
|
107
109
|
}, { onProgress, onWarning })
|
|
108
110
|
|
|
109
|
-
// 4. Apply starter content
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|
|
112
116
|
|
|
113
117
|
success(`Created project: ${projectName}`)
|
|
114
118
|
}
|
|
@@ -288,7 +292,9 @@ function computeFoundationFilePath(sitePath, foundationPath) {
|
|
|
288
292
|
}
|
|
289
293
|
|
|
290
294
|
async function main() {
|
|
291
|
-
const
|
|
295
|
+
const rawArgs = process.argv.slice(2)
|
|
296
|
+
const nonInteractive = isNonInteractive(rawArgs)
|
|
297
|
+
const args = stripNonInteractiveFlag(rawArgs)
|
|
292
298
|
const command = args[0]
|
|
293
299
|
const pm = detectPackageManager()
|
|
294
300
|
|
|
@@ -361,6 +367,15 @@ async function main() {
|
|
|
361
367
|
displayName = args[nameIndex + 1]
|
|
362
368
|
}
|
|
363
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
|
+
|
|
364
379
|
// Check for --no-git flag
|
|
365
380
|
const noGit = args.includes('--no-git')
|
|
366
381
|
|
|
@@ -369,6 +384,20 @@ async function main() {
|
|
|
369
384
|
projectName = null
|
|
370
385
|
}
|
|
371
386
|
|
|
387
|
+
const prefix = getCliPrefix()
|
|
388
|
+
|
|
389
|
+
// Non-interactive: fail with actionable message instead of prompting
|
|
390
|
+
if (nonInteractive && !projectName) {
|
|
391
|
+
error(`Missing project name.\n`)
|
|
392
|
+
log(`Usage: ${prefix} create <project-name> [--template <name>] [--blank]`)
|
|
393
|
+
process.exit(1)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Non-interactive: default to starter when no template specified
|
|
397
|
+
if (nonInteractive && !templateType && !isBlank) {
|
|
398
|
+
templateType = 'starter'
|
|
399
|
+
}
|
|
400
|
+
|
|
372
401
|
// Interactive prompts
|
|
373
402
|
const response = await prompts([
|
|
374
403
|
{
|
|
@@ -398,13 +427,14 @@ async function main() {
|
|
|
398
427
|
process.exit(1)
|
|
399
428
|
}
|
|
400
429
|
|
|
401
|
-
// Prompt for template if not specified via --template
|
|
402
|
-
if (!templateType) {
|
|
430
|
+
// Prompt for template if not specified via --template or --blank
|
|
431
|
+
if (!templateType && !isBlank) {
|
|
403
432
|
const templateResponse = await prompts({
|
|
404
433
|
type: 'select',
|
|
405
434
|
name: 'template',
|
|
406
435
|
message: 'Template:',
|
|
407
436
|
choices: TEMPLATE_CHOICES,
|
|
437
|
+
initial: 1,
|
|
408
438
|
}, {
|
|
409
439
|
onCancel: () => {
|
|
410
440
|
log('\nScaffolding cancelled.')
|
|
@@ -412,6 +442,11 @@ async function main() {
|
|
|
412
442
|
},
|
|
413
443
|
})
|
|
414
444
|
templateType = templateResponse.template
|
|
445
|
+
// Handle "blank" selection from interactive prompt
|
|
446
|
+
if (templateType === 'blank') {
|
|
447
|
+
isBlank = true
|
|
448
|
+
templateType = null
|
|
449
|
+
}
|
|
415
450
|
}
|
|
416
451
|
|
|
417
452
|
const effectiveName = displayName || projectName
|
|
@@ -428,13 +463,22 @@ async function main() {
|
|
|
428
463
|
const progressCb = (msg) => log(` ${colors.dim}${msg}${colors.reset}`)
|
|
429
464
|
const warningCb = (msg) => log(` ${colors.yellow}Warning: ${msg}${colors.reset}`)
|
|
430
465
|
|
|
431
|
-
if (
|
|
432
|
-
// Blank workspace
|
|
466
|
+
if (isBlank) {
|
|
467
|
+
// Blank workspace (--blank or --template blank)
|
|
433
468
|
log('\nCreating blank workspace...')
|
|
434
469
|
await createBlankWorkspace(projectDir, effectiveName, {
|
|
435
470
|
onProgress: progressCb,
|
|
436
471
|
onWarning: warningCb,
|
|
437
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
|
+
})
|
|
438
482
|
} else if (templateType === 'starter') {
|
|
439
483
|
// Starter: foundation + site + sample content
|
|
440
484
|
log('\nCreating project...')
|
|
@@ -498,11 +542,10 @@ async function main() {
|
|
|
498
542
|
// Success message
|
|
499
543
|
title('Project created successfully!')
|
|
500
544
|
|
|
501
|
-
if (
|
|
545
|
+
if (isBlank) {
|
|
502
546
|
log(`Next steps:\n`)
|
|
503
547
|
log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
|
|
504
|
-
log(` ${colors.cyan}
|
|
505
|
-
log(` ${colors.cyan}npx uniweb add site${colors.reset}`)
|
|
548
|
+
log(` ${colors.cyan}${prefix} add project${colors.reset}`)
|
|
506
549
|
log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
507
550
|
log(` ${colors.cyan}${runCmd(pm, 'dev')}${colors.reset}`)
|
|
508
551
|
} else {
|
|
@@ -530,15 +573,21 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
530
573
|
i18n <cmd> Internationalization (extract, sync, status)
|
|
531
574
|
|
|
532
575
|
${colors.bright}Create Options:${colors.reset}
|
|
533
|
-
--template <type> Project template (
|
|
576
|
+
--template <type> Project template (default: starter)
|
|
577
|
+
--blank Create an empty workspace (grow with uniweb add)
|
|
534
578
|
--name <name> Project display name
|
|
535
579
|
--no-git Skip git repository initialization
|
|
536
580
|
|
|
537
581
|
${colors.bright}Add Subcommands:${colors.reset}
|
|
582
|
+
add project [name] Add a co-located foundation + site pair
|
|
538
583
|
add foundation [name] Add a foundation (--from, --path, --project)
|
|
539
584
|
add site [name] Add a site (--from, --foundation, --path, --project)
|
|
540
585
|
add extension <name> Add an extension (--from, --site, --path)
|
|
541
586
|
|
|
587
|
+
${colors.bright}Global Options:${colors.reset}
|
|
588
|
+
--non-interactive Fail with usage info instead of prompting
|
|
589
|
+
Auto-detected when CI=true or no TTY (pipes, agents)
|
|
590
|
+
|
|
542
591
|
${colors.bright}Build Options:${colors.reset}
|
|
543
592
|
--target <type> Build target (foundation, site) - auto-detected if not specified
|
|
544
593
|
--prerender Force pre-rendering (overrides site.yml)
|
|
@@ -567,7 +616,7 @@ ${colors.bright}i18n Commands:${colors.reset}
|
|
|
567
616
|
|
|
568
617
|
${colors.bright}Template Types:${colors.reset}
|
|
569
618
|
starter Foundation + site + sample content (default)
|
|
570
|
-
|
|
619
|
+
none Foundation + site with no content
|
|
571
620
|
marketing Official marketing template
|
|
572
621
|
./path/to/template Local directory
|
|
573
622
|
@scope/template-name npm package
|
|
@@ -575,17 +624,17 @@ ${colors.bright}Template Types:${colors.reset}
|
|
|
575
624
|
https://github.com/user/repo GitHub URL
|
|
576
625
|
|
|
577
626
|
${colors.bright}Examples:${colors.reset}
|
|
578
|
-
npx uniweb create my-project #
|
|
579
|
-
npx uniweb create my-project --template
|
|
580
|
-
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
|
|
581
630
|
npx uniweb create my-project --template marketing # Official template
|
|
582
631
|
npx uniweb create my-project --template ./my-template # Local template
|
|
583
632
|
|
|
584
633
|
cd my-project
|
|
585
|
-
npx uniweb add
|
|
586
|
-
npx uniweb add
|
|
587
|
-
npx uniweb add
|
|
588
|
-
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
|
|
589
638
|
npx uniweb add extension effects --site site # Add extensions/effects/
|
|
590
639
|
|
|
591
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']
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-Interactive Mode Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects when the CLI is running without a TTY (AI agents, CI, piped input)
|
|
5
|
+
* and provides helpers for printing actionable error messages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detect if the CLI is running in non-interactive mode.
|
|
10
|
+
* True when --non-interactive flag, CI env var, or no TTY.
|
|
11
|
+
* @param {string[]} args - Command line arguments
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
export function isNonInteractive(args) {
|
|
15
|
+
if (args.includes('--non-interactive')) return true
|
|
16
|
+
if (process.env.CI) return true
|
|
17
|
+
if (!process.stdin.isTTY) return true
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the CLI invocation prefix to use in suggested commands.
|
|
23
|
+
* Mirrors however the user actually ran the CLI.
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function getCliPrefix() {
|
|
27
|
+
const ua = process.env.npm_config_user_agent || ''
|
|
28
|
+
if (ua.startsWith('pnpm/')) return 'pnpm uniweb'
|
|
29
|
+
if (ua.startsWith('npm/')) return 'npx uniweb'
|
|
30
|
+
return 'uniweb'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Strip --non-interactive from an args array so it doesn't interfere
|
|
35
|
+
* with positional argument parsing.
|
|
36
|
+
* @param {string[]} args
|
|
37
|
+
* @returns {string[]}
|
|
38
|
+
*/
|
|
39
|
+
export function stripNonInteractiveFlag(args) {
|
|
40
|
+
return args.filter(a => a !== '--non-interactive')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Format a list of options with aligned descriptions for terminal output.
|
|
45
|
+
* @param {{ label: string, description: string }[]} options
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function formatOptions(options) {
|
|
49
|
+
const maxLen = Math.max(...options.map(o => o.label.length))
|
|
50
|
+
return options
|
|
51
|
+
.map(o => ` ${o.label.padEnd(maxLen + 3)}${o.description}`)
|
|
52
|
+
.join('\n')
|
|
53
|
+
}
|
package/templates/site/theme.yml
CHANGED
|
@@ -1 +1,99 @@
|
|
|
1
|
-
|
|
1
|
+
# Theme Configuration
|
|
2
|
+
# Controls colors, typography, and visual style for the site.
|
|
3
|
+
# Only set what you want to change — everything has sensible defaults.
|
|
4
|
+
|
|
5
|
+
# ─── Colors ────────────────────────────────────────────────────────────────────
|
|
6
|
+
# Palette colors generate shades (50–950) used by semantic tokens.
|
|
7
|
+
# Values: any CSS color (hex, rgb, hsl, oklch).
|
|
8
|
+
|
|
9
|
+
colors:
|
|
10
|
+
primary: '#3b82f6' # Brand color — buttons, links, focus rings
|
|
11
|
+
# secondary: '#64748b' # Secondary actions
|
|
12
|
+
# accent: '#8b5cf6' # Highlights, decorative elements
|
|
13
|
+
# neutral: stone # Gray family — stone, zinc, gray, slate, neutral
|
|
14
|
+
|
|
15
|
+
# ─── Fonts ─────────────────────────────────────────────────────────────────────
|
|
16
|
+
# Font families for text, headings, and code blocks.
|
|
17
|
+
# Default: system-ui stack. Uncomment to use custom fonts.
|
|
18
|
+
|
|
19
|
+
# fonts:
|
|
20
|
+
# heading: '"Inter", system-ui, sans-serif'
|
|
21
|
+
# body: '"Inter", system-ui, sans-serif'
|
|
22
|
+
# mono: '"JetBrains Mono", ui-monospace, monospace'
|
|
23
|
+
# import:
|
|
24
|
+
# - url: https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap
|
|
25
|
+
# - url: https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap
|
|
26
|
+
|
|
27
|
+
# ─── Page Background ──────────────────────────────────────────────────────────
|
|
28
|
+
# Background applied to the page body. Any CSS background value.
|
|
29
|
+
|
|
30
|
+
# background: '#fafaf9'
|
|
31
|
+
# background: 'linear-gradient(to bottom, #fafaf9, white)'
|
|
32
|
+
|
|
33
|
+
# ─── Appearance ────────────────────────────────────────────────────────────────
|
|
34
|
+
# Color scheme support. Simple value: light, dark, or system.
|
|
35
|
+
# Or use the expanded form for more control.
|
|
36
|
+
|
|
37
|
+
# appearance: light
|
|
38
|
+
# appearance:
|
|
39
|
+
# default: light
|
|
40
|
+
# schemes: [light, dark]
|
|
41
|
+
# allowToggle: true
|
|
42
|
+
# respectSystemPreference: true
|
|
43
|
+
|
|
44
|
+
# ─── Context Overrides ─────────────────────────────────────────────────────────
|
|
45
|
+
# Sections declare a context (light, medium, dark) in frontmatter.
|
|
46
|
+
# Each context maps semantic tokens to palette values.
|
|
47
|
+
# Override individual tokens here — unlisted tokens keep their defaults.
|
|
48
|
+
#
|
|
49
|
+
# Available tokens:
|
|
50
|
+
# Surfaces: section, card, muted
|
|
51
|
+
# Text: body, heading, subtle
|
|
52
|
+
# Border: border, ring
|
|
53
|
+
# Links: link, link-hover
|
|
54
|
+
# Actions: primary, primary-foreground, primary-hover
|
|
55
|
+
# secondary, secondary-foreground, secondary-hover
|
|
56
|
+
# Status: success, warning, error, info (+ -subtle variants)
|
|
57
|
+
|
|
58
|
+
# contexts:
|
|
59
|
+
# light:
|
|
60
|
+
# section: white
|
|
61
|
+
# medium:
|
|
62
|
+
# section: 'var(--neutral-100)'
|
|
63
|
+
# dark:
|
|
64
|
+
# heading: white
|
|
65
|
+
|
|
66
|
+
# ─── Inline Text Styles ───────────────────────────────────────────────────────
|
|
67
|
+
# Named styles for inline text in markdown: [text]{emphasis}
|
|
68
|
+
# Each name maps to CSS properties.
|
|
69
|
+
# Defaults: emphasis (colored + bold), muted (subtle).
|
|
70
|
+
|
|
71
|
+
# inline:
|
|
72
|
+
# emphasis:
|
|
73
|
+
# color: 'var(--link)'
|
|
74
|
+
# font-weight: '600'
|
|
75
|
+
# muted:
|
|
76
|
+
# color: 'var(--subtle)'
|
|
77
|
+
|
|
78
|
+
# ─── Code Blocks ───────────────────────────────────────────────────────────────
|
|
79
|
+
# Syntax highlighting colors for code blocks (uses Shiki).
|
|
80
|
+
|
|
81
|
+
# code:
|
|
82
|
+
# background: '#1e1e2e'
|
|
83
|
+
# foreground: '#cdd6f4'
|
|
84
|
+
# keyword: '#cba6f7'
|
|
85
|
+
# string: '#a6e3a1'
|
|
86
|
+
# number: '#fab387'
|
|
87
|
+
# comment: '#6c7086'
|
|
88
|
+
# function: '#89b4fa'
|
|
89
|
+
# variable: '#f5e0dc'
|
|
90
|
+
# type: '#f9e2af'
|
|
91
|
+
# property: '#94e2d5'
|
|
92
|
+
# tag: '#89b4fa'
|
|
93
|
+
|
|
94
|
+
# ─── Foundation Variables ──────────────────────────────────────────────────────
|
|
95
|
+
# Override variables declared by the foundation (e.g., header-height, max-width).
|
|
96
|
+
# Available variables depend on the foundation — check its foundation.js.
|
|
97
|
+
|
|
98
|
+
# vars:
|
|
99
|
+
# header-height: 5rem
|