uniweb 0.13.17 → 0.14.0
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 +1 -1
- package/package.json +7 -7
- package/partials/agents.md +51 -0
- package/src/backend/client.js +173 -40
- package/src/backend/data-bundle.js +15 -3
- package/src/backend/foundation-bring-along.js +76 -21
- package/src/backend/payment-handoff.js +28 -9
- package/src/backend/site-media.js +147 -19
- package/src/backend/site-sync.js +362 -40
- package/src/commands/add.js +575 -273
- package/src/commands/build.js +160 -57
- package/src/commands/clone.js +79 -26
- package/src/commands/content.js +11 -7
- package/src/commands/deploy.js +80 -32
- package/src/commands/dev.js +39 -17
- package/src/commands/docs.js +44 -16
- package/src/commands/doctor.js +338 -82
- package/src/commands/export.js +15 -7
- package/src/commands/handoff.js +6 -2
- package/src/commands/i18n.js +248 -99
- package/src/commands/inspect.js +48 -19
- package/src/commands/invite.js +9 -3
- package/src/commands/org.js +19 -5
- package/src/commands/publish.js +229 -60
- package/src/commands/pull.js +157 -48
- package/src/commands/push.js +144 -42
- package/src/commands/refresh.js +37 -10
- package/src/commands/register.js +235 -64
- package/src/commands/rename.js +126 -46
- package/src/commands/runtime.js +74 -24
- package/src/commands/status.js +48 -15
- package/src/commands/sync.js +7 -2
- package/src/commands/template.js +12 -4
- package/src/commands/update.js +263 -87
- package/src/commands/validate.js +115 -32
- package/src/framework-index.json +15 -13
- package/src/index.js +298 -145
- package/src/templates/fetchers/github.js +7 -7
- package/src/templates/fetchers/npm.js +3 -3
- package/src/templates/fetchers/release.js +21 -18
- package/src/templates/index.js +34 -12
- package/src/templates/processor.js +44 -12
- package/src/templates/resolver.js +42 -15
- package/src/templates/validator.js +14 -7
- package/src/utils/asset-upload.js +90 -22
- package/src/utils/code-upload.js +62 -37
- package/src/utils/config.js +31 -10
- package/src/utils/dep-survey.js +33 -7
- package/src/utils/destination-prompt.js +27 -17
- package/src/utils/git.js +66 -22
- package/src/utils/host-prompt.js +4 -3
- package/src/utils/install-integrity.js +8 -4
- package/src/utils/interactive.js +3 -3
- package/src/utils/json-file.js +6 -2
- package/src/utils/names.js +15 -6
- package/src/utils/placement.js +16 -8
- package/src/utils/registry-auth.js +185 -57
- package/src/utils/registry-orgs.js +142 -53
- package/src/utils/runtime-upload.js +52 -14
- package/src/utils/scaffold.js +55 -14
- package/src/utils/site-content-refs.js +3 -1
- package/src/utils/update-check.js +43 -17
- package/src/utils/workspace.js +13 -7
- package/src/versions.js +18 -7
- package/templates/site/_gitignore +5 -0
package/src/commands/add.js
CHANGED
|
@@ -17,21 +17,50 @@ import { join, relative, basename } from 'node:path'
|
|
|
17
17
|
import prompts from 'prompts'
|
|
18
18
|
import yaml from 'js-yaml'
|
|
19
19
|
import { resolveFoundationSrcPath } from '@uniweb/build'
|
|
20
|
-
import {
|
|
21
|
-
|
|
20
|
+
import {
|
|
21
|
+
scaffoldFoundation,
|
|
22
|
+
scaffoldSite,
|
|
23
|
+
applyContent,
|
|
24
|
+
applyStarter,
|
|
25
|
+
mergeTemplateDependencies
|
|
26
|
+
} from '../utils/scaffold.js'
|
|
27
|
+
import {
|
|
28
|
+
resolvePlacement,
|
|
29
|
+
SITE_KIND,
|
|
30
|
+
FOUNDATION_KIND
|
|
31
|
+
} from '../utils/placement.js'
|
|
22
32
|
import {
|
|
23
33
|
readWorkspaceConfig,
|
|
24
34
|
addWorkspaceGlob,
|
|
25
|
-
updateRootScripts
|
|
35
|
+
updateRootScripts
|
|
26
36
|
} from '../utils/config.js'
|
|
27
37
|
import { discoverFoundations, discoverSites } from '../utils/discover.js'
|
|
28
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
validatePackageName,
|
|
40
|
+
getExistingPackageNames,
|
|
41
|
+
resolveUniqueName
|
|
42
|
+
} from '../utils/names.js'
|
|
29
43
|
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
30
|
-
import {
|
|
31
|
-
|
|
44
|
+
import {
|
|
45
|
+
detectPackageManager,
|
|
46
|
+
detectWorkspacePm,
|
|
47
|
+
detectInstalledPnpmVersion,
|
|
48
|
+
filterCmd,
|
|
49
|
+
installCmd
|
|
50
|
+
} from '../utils/pm.js'
|
|
51
|
+
import {
|
|
52
|
+
isNonInteractive,
|
|
53
|
+
getCliPrefix,
|
|
54
|
+
stripNonInteractiveFlag,
|
|
55
|
+
formatOptions
|
|
56
|
+
} from '../utils/interactive.js'
|
|
32
57
|
import { resolveTemplate } from '../templates/index.js'
|
|
33
58
|
import { validateTemplate } from '../templates/validator.js'
|
|
34
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
getVersionsForTemplates,
|
|
61
|
+
resolveCiPnpmVersion,
|
|
62
|
+
resolveCiNodeVersion
|
|
63
|
+
} from '../versions.js'
|
|
35
64
|
|
|
36
65
|
// Colors for terminal output
|
|
37
66
|
const colors = {
|
|
@@ -41,20 +70,28 @@ const colors = {
|
|
|
41
70
|
cyan: '\x1b[36m',
|
|
42
71
|
green: '\x1b[32m',
|
|
43
72
|
yellow: '\x1b[33m',
|
|
44
|
-
red: '\x1b[31m'
|
|
73
|
+
red: '\x1b[31m'
|
|
45
74
|
}
|
|
46
75
|
|
|
47
|
-
function log(message) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
function
|
|
76
|
+
function log(message) {
|
|
77
|
+
console.log(message)
|
|
78
|
+
}
|
|
79
|
+
function success(message) {
|
|
80
|
+
console.log(`${colors.green}✓${colors.reset} ${message}`)
|
|
81
|
+
}
|
|
82
|
+
function error(message) {
|
|
83
|
+
console.error(`${colors.red}✗${colors.reset} ${message}`)
|
|
84
|
+
}
|
|
85
|
+
function info(message) {
|
|
86
|
+
console.log(`${colors.dim}${message}${colors.reset}`)
|
|
87
|
+
}
|
|
51
88
|
|
|
52
89
|
/**
|
|
53
90
|
* Parse add command arguments
|
|
54
91
|
*/
|
|
55
92
|
function parseArgs(args) {
|
|
56
93
|
const result = {
|
|
57
|
-
subcommand: args[0],
|
|
94
|
+
subcommand: args[0], // foundation, site, extension, ci, …
|
|
58
95
|
name: null,
|
|
59
96
|
path: null,
|
|
60
97
|
project: null,
|
|
@@ -69,7 +106,7 @@ function parseArgs(args) {
|
|
|
69
106
|
// scaffold PR-preview workflows.
|
|
70
107
|
target: null,
|
|
71
108
|
projectName: null,
|
|
72
|
-
previews: true
|
|
109
|
+
previews: true
|
|
73
110
|
}
|
|
74
111
|
|
|
75
112
|
// Booleans (no value) consumed up-front so the value-flag loop below
|
|
@@ -94,7 +131,7 @@ function parseArgs(args) {
|
|
|
94
131
|
'--host': 'host',
|
|
95
132
|
'--domain': 'domain',
|
|
96
133
|
'--target': 'target',
|
|
97
|
-
'--project-name': 'projectName'
|
|
134
|
+
'--project-name': 'projectName'
|
|
98
135
|
}
|
|
99
136
|
|
|
100
137
|
/** Split `--flag=value` into [flag, value]; `--flag` into [flag, null]. */
|
|
@@ -156,7 +193,9 @@ export async function add(rawArgs) {
|
|
|
156
193
|
// Find workspace root
|
|
157
194
|
const rootDir = findWorkspaceRoot()
|
|
158
195
|
if (!rootDir) {
|
|
159
|
-
error(
|
|
196
|
+
error(
|
|
197
|
+
'Not in a Uniweb workspace. Run this command from a project directory.'
|
|
198
|
+
)
|
|
160
199
|
error('Use "uniweb create" to create a new project first.')
|
|
161
200
|
process.exit(1)
|
|
162
201
|
}
|
|
@@ -166,37 +205,73 @@ export async function add(rawArgs) {
|
|
|
166
205
|
if (!args.length || (args[0] && args[0].startsWith('--'))) {
|
|
167
206
|
if (nonInteractive) {
|
|
168
207
|
error(`Missing subcommand.\n`)
|
|
169
|
-
log(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
208
|
+
log(
|
|
209
|
+
formatOptions([
|
|
210
|
+
{
|
|
211
|
+
label: 'project',
|
|
212
|
+
description: 'Co-located foundation + site pair'
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
label: 'foundation',
|
|
216
|
+
description: 'Component system for content authors'
|
|
217
|
+
},
|
|
218
|
+
{ label: 'site', description: 'Content site' },
|
|
219
|
+
{ label: 'extension', description: 'Additional component package' },
|
|
220
|
+
{ label: 'section', description: 'Section type in a foundation' },
|
|
221
|
+
{
|
|
222
|
+
label: 'ci',
|
|
223
|
+
description: 'CI deploy workflow for a host (e.g., GitHub Pages)'
|
|
224
|
+
}
|
|
225
|
+
])
|
|
226
|
+
)
|
|
177
227
|
log('')
|
|
178
|
-
log(
|
|
228
|
+
log(
|
|
229
|
+
`Usage: ${prefix} add <project|foundation|site|extension|section|ci> [name]`
|
|
230
|
+
)
|
|
179
231
|
process.exit(1)
|
|
180
232
|
}
|
|
181
233
|
|
|
182
|
-
const response = await prompts(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
234
|
+
const response = await prompts(
|
|
235
|
+
{
|
|
236
|
+
type: 'select',
|
|
237
|
+
name: 'subcommand',
|
|
238
|
+
message: 'What would you like to add?',
|
|
239
|
+
choices: [
|
|
240
|
+
{
|
|
241
|
+
title: 'Project',
|
|
242
|
+
value: 'project',
|
|
243
|
+
description: 'Co-located foundation + site pair'
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
title: 'Foundation',
|
|
247
|
+
value: 'foundation',
|
|
248
|
+
description: 'Component system for content authors'
|
|
249
|
+
},
|
|
250
|
+
{ title: 'Site', value: 'site', description: 'Content site' },
|
|
251
|
+
{
|
|
252
|
+
title: 'Extension',
|
|
253
|
+
value: 'extension',
|
|
254
|
+
description: 'Additional component package'
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
title: 'Section',
|
|
258
|
+
value: 'section',
|
|
259
|
+
description: 'Section type in a foundation'
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
title: 'CI workflow',
|
|
263
|
+
value: 'ci',
|
|
264
|
+
description: 'Deploy workflow for a host (e.g., GitHub Pages)'
|
|
265
|
+
}
|
|
266
|
+
]
|
|
198
267
|
},
|
|
199
|
-
|
|
268
|
+
{
|
|
269
|
+
onCancel: () => {
|
|
270
|
+
log('\nCancelled.')
|
|
271
|
+
process.exit(0)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
)
|
|
200
275
|
parsed = parseArgs([response.subcommand, ...args])
|
|
201
276
|
} else {
|
|
202
277
|
parsed = parseArgs(args)
|
|
@@ -238,7 +313,9 @@ export async function add(rawArgs) {
|
|
|
238
313
|
break
|
|
239
314
|
default:
|
|
240
315
|
error(`Unknown subcommand: ${parsed.subcommand}`)
|
|
241
|
-
log(
|
|
316
|
+
log(
|
|
317
|
+
`Valid subcommands: project, foundation, site, extension, section, ci`
|
|
318
|
+
)
|
|
242
319
|
process.exit(1)
|
|
243
320
|
}
|
|
244
321
|
}
|
|
@@ -273,11 +350,17 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
273
350
|
|
|
274
351
|
// Collision check 1: target folder already exists.
|
|
275
352
|
if (existsSync(fullPath)) {
|
|
276
|
-
error(
|
|
353
|
+
error(
|
|
354
|
+
`Cannot create foundation: ${colors.bright}${relativePath}/${colors.reset} already exists.`
|
|
355
|
+
)
|
|
277
356
|
log('')
|
|
278
357
|
log(`Pick a different name, or pass --path to choose a different folder:`)
|
|
279
|
-
log(
|
|
280
|
-
|
|
358
|
+
log(
|
|
359
|
+
` ${colors.cyan}${getCliPrefix()} add foundation <name>${colors.reset}`
|
|
360
|
+
)
|
|
361
|
+
log(
|
|
362
|
+
` ${colors.cyan}${getCliPrefix()} add foundation <name> --path <parent-dir>${colors.reset}`
|
|
363
|
+
)
|
|
281
364
|
process.exit(1)
|
|
282
365
|
}
|
|
283
366
|
|
|
@@ -293,33 +376,47 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
293
376
|
// situation, not a disambiguation case.
|
|
294
377
|
if (existingNames.has(packageName)) {
|
|
295
378
|
const sites = await discoverSites(rootDir)
|
|
296
|
-
const isSiteCollision = sites.some(s => s.name === packageName)
|
|
379
|
+
const isSiteCollision = sites.some((s) => s.name === packageName)
|
|
297
380
|
if (isSiteCollision) {
|
|
298
381
|
const suffixed = `${packageName}-src`
|
|
299
382
|
if (existingNames.has(suffixed)) {
|
|
300
|
-
error(
|
|
383
|
+
error(
|
|
384
|
+
`Cannot create foundation: both ${colors.bright}${packageName}${colors.reset} and ${colors.bright}${suffixed}${colors.reset} are taken in this workspace.`
|
|
385
|
+
)
|
|
301
386
|
log(`Pick a different name:`)
|
|
302
|
-
log(
|
|
387
|
+
log(
|
|
388
|
+
` ${colors.cyan}${getCliPrefix()} add foundation <other-name>${colors.reset}`
|
|
389
|
+
)
|
|
303
390
|
process.exit(1)
|
|
304
391
|
}
|
|
305
|
-
info(
|
|
392
|
+
info(
|
|
393
|
+
`Package "${packageName}" is taken by a site; using "${suffixed}" for this foundation.`
|
|
394
|
+
)
|
|
306
395
|
packageName = suffixed
|
|
307
396
|
} else {
|
|
308
|
-
error(
|
|
397
|
+
error(
|
|
398
|
+
`Cannot create foundation: a foundation named ${colors.bright}${packageName}${colors.reset} already exists in this workspace.`
|
|
399
|
+
)
|
|
309
400
|
log(`Pick a different name:`)
|
|
310
|
-
log(
|
|
401
|
+
log(
|
|
402
|
+
` ${colors.cyan}${getCliPrefix()} add foundation <other-name>${colors.reset}`
|
|
403
|
+
)
|
|
311
404
|
process.exit(1)
|
|
312
405
|
}
|
|
313
406
|
}
|
|
314
407
|
|
|
315
408
|
// Scaffold
|
|
316
|
-
await scaffoldFoundation(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
409
|
+
await scaffoldFoundation(
|
|
410
|
+
fullPath,
|
|
411
|
+
{
|
|
412
|
+
name: packageName,
|
|
413
|
+
projectName,
|
|
414
|
+
isExtension: false
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
onProgress: (msg) => info(` ${msg}`)
|
|
418
|
+
}
|
|
419
|
+
)
|
|
323
420
|
|
|
324
421
|
// Apply template content if --from specified
|
|
325
422
|
if (opts.from) {
|
|
@@ -335,7 +432,9 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
335
432
|
const sites = await discoverSites(rootDir)
|
|
336
433
|
await updateRootScripts(rootDir, sites, pm)
|
|
337
434
|
|
|
338
|
-
success(
|
|
435
|
+
success(
|
|
436
|
+
`Created foundation ${colors.bright}${packageName}${colors.reset} at ${relativePath}/`
|
|
437
|
+
)
|
|
339
438
|
log('')
|
|
340
439
|
log(`Next: ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
341
440
|
}
|
|
@@ -364,11 +463,15 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
364
463
|
|
|
365
464
|
// Collision check 1: target folder exists.
|
|
366
465
|
if (existsSync(fullPath)) {
|
|
367
|
-
error(
|
|
466
|
+
error(
|
|
467
|
+
`Cannot create site: ${colors.bright}${relativePath}/${colors.reset} already exists.`
|
|
468
|
+
)
|
|
368
469
|
log('')
|
|
369
470
|
log(`Pick a different name, or pass --path to choose a different folder:`)
|
|
370
471
|
log(` ${colors.cyan}${getCliPrefix()} add site <name>${colors.reset}`)
|
|
371
|
-
log(
|
|
472
|
+
log(
|
|
473
|
+
` ${colors.cyan}${getCliPrefix()} add site <name> --path <parent-dir>${colors.reset}`
|
|
474
|
+
)
|
|
372
475
|
process.exit(1)
|
|
373
476
|
}
|
|
374
477
|
|
|
@@ -377,21 +480,31 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
377
480
|
// addFoundation for the rationale.
|
|
378
481
|
if (existingNames.has(siteName)) {
|
|
379
482
|
const foundations = await discoverFoundations(rootDir)
|
|
380
|
-
const isFoundationCollision = foundations.some(f => f.name === siteName)
|
|
483
|
+
const isFoundationCollision = foundations.some((f) => f.name === siteName)
|
|
381
484
|
if (isFoundationCollision) {
|
|
382
485
|
const suffixed = `${siteName}-site`
|
|
383
486
|
if (existingNames.has(suffixed)) {
|
|
384
|
-
error(
|
|
487
|
+
error(
|
|
488
|
+
`Cannot create site: both ${colors.bright}${siteName}${colors.reset} and ${colors.bright}${suffixed}${colors.reset} are taken in this workspace.`
|
|
489
|
+
)
|
|
385
490
|
log(`Pick a different name:`)
|
|
386
|
-
log(
|
|
491
|
+
log(
|
|
492
|
+
` ${colors.cyan}${getCliPrefix()} add site <other-name>${colors.reset}`
|
|
493
|
+
)
|
|
387
494
|
process.exit(1)
|
|
388
495
|
}
|
|
389
|
-
info(
|
|
496
|
+
info(
|
|
497
|
+
`Package "${siteName}" is taken by a foundation; using "${suffixed}" for this site.`
|
|
498
|
+
)
|
|
390
499
|
siteName = suffixed
|
|
391
500
|
} else {
|
|
392
|
-
error(
|
|
501
|
+
error(
|
|
502
|
+
`Cannot create site: a site named ${colors.bright}${siteName}${colors.reset} already exists in this workspace.`
|
|
503
|
+
)
|
|
393
504
|
log(`Pick a different name:`)
|
|
394
|
-
log(
|
|
505
|
+
log(
|
|
506
|
+
` ${colors.cyan}${getCliPrefix()} add site <other-name>${colors.reset}`
|
|
507
|
+
)
|
|
395
508
|
process.exit(1)
|
|
396
509
|
}
|
|
397
510
|
}
|
|
@@ -404,26 +517,36 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
404
517
|
const foundationPath = computeFoundationPath(relativePath, foundation.path)
|
|
405
518
|
|
|
406
519
|
// Scaffold
|
|
407
|
-
await scaffoldSite(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
520
|
+
await scaffoldSite(
|
|
521
|
+
fullPath,
|
|
522
|
+
{
|
|
523
|
+
name: siteName,
|
|
524
|
+
projectName,
|
|
525
|
+
foundationName: foundation.name,
|
|
526
|
+
foundationPath,
|
|
527
|
+
foundationRef: foundation.name
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
onProgress: (msg) => info(` ${msg}`)
|
|
531
|
+
}
|
|
532
|
+
)
|
|
416
533
|
} else {
|
|
417
534
|
// No foundation — scaffold without wiring
|
|
418
|
-
await scaffoldSite(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
535
|
+
await scaffoldSite(
|
|
536
|
+
fullPath,
|
|
537
|
+
{
|
|
538
|
+
name: siteName,
|
|
539
|
+
projectName,
|
|
540
|
+
foundationName: '',
|
|
541
|
+
foundationPath: ''
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
onProgress: (msg) => info(` ${msg}`)
|
|
545
|
+
}
|
|
546
|
+
)
|
|
547
|
+
log(
|
|
548
|
+
` ${colors.yellow}⚠ No foundation wired. Add one later with: uniweb add foundation${colors.reset}`
|
|
549
|
+
)
|
|
427
550
|
}
|
|
428
551
|
|
|
429
552
|
// Apply template content if --from specified
|
|
@@ -436,22 +559,32 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
436
559
|
|
|
437
560
|
// Update root scripts (discover sites after registration — includes the new one)
|
|
438
561
|
const sites = await discoverSites(rootDir)
|
|
439
|
-
if (!sites.find(s => s.path === relativePath)) {
|
|
562
|
+
if (!sites.find((s) => s.path === relativePath)) {
|
|
440
563
|
sites.push({ name: siteName, path: relativePath })
|
|
441
564
|
}
|
|
442
565
|
await updateRootScripts(rootDir, sites, pm)
|
|
443
566
|
|
|
444
567
|
if (foundation) {
|
|
445
|
-
success(
|
|
568
|
+
success(
|
|
569
|
+
`Created site ${colors.bright}${siteName}${colors.reset} at ${relativePath}/ → foundation '${foundation.name}'`
|
|
570
|
+
)
|
|
446
571
|
} else {
|
|
447
|
-
success(
|
|
572
|
+
success(
|
|
573
|
+
`Created site ${colors.bright}${siteName}${colors.reset} at ${relativePath}/`
|
|
574
|
+
)
|
|
448
575
|
}
|
|
449
576
|
log('')
|
|
450
|
-
log(
|
|
577
|
+
log(
|
|
578
|
+
`Next: ${colors.cyan}${installCmd(pm)} && uniweb dev ${siteName}${colors.reset}`
|
|
579
|
+
)
|
|
451
580
|
if (!opts.from) {
|
|
452
581
|
log('')
|
|
453
|
-
log(
|
|
454
|
-
|
|
582
|
+
log(
|
|
583
|
+
`${colors.dim}To add your first page, create ${relativePath}/pages/home/page.yml and a .md file.${colors.reset}`
|
|
584
|
+
)
|
|
585
|
+
log(
|
|
586
|
+
`${colors.dim}Or use --from to start with template content: uniweb add site --from starter${colors.reset}`
|
|
587
|
+
)
|
|
455
588
|
}
|
|
456
589
|
}
|
|
457
590
|
|
|
@@ -479,17 +612,20 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
479
612
|
process.exit(1)
|
|
480
613
|
}
|
|
481
614
|
|
|
482
|
-
const response = await prompts(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
onCancel: () => {
|
|
489
|
-
log('\nCancelled.')
|
|
490
|
-
process.exit(0)
|
|
615
|
+
const response = await prompts(
|
|
616
|
+
{
|
|
617
|
+
type: 'text',
|
|
618
|
+
name: 'name',
|
|
619
|
+
message: 'Extension name:',
|
|
620
|
+
validate: (value) => validatePackageName(value)
|
|
491
621
|
},
|
|
492
|
-
|
|
622
|
+
{
|
|
623
|
+
onCancel: () => {
|
|
624
|
+
log('\nCancelled.')
|
|
625
|
+
process.exit(0)
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
)
|
|
493
629
|
name = response.name
|
|
494
630
|
}
|
|
495
631
|
|
|
@@ -514,13 +650,17 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
514
650
|
}
|
|
515
651
|
|
|
516
652
|
// Scaffold foundation with extension flag
|
|
517
|
-
await scaffoldFoundation(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
653
|
+
await scaffoldFoundation(
|
|
654
|
+
fullPath,
|
|
655
|
+
{
|
|
656
|
+
name: extensionPackageName,
|
|
657
|
+
projectName,
|
|
658
|
+
isExtension: true
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
onProgress: (msg) => info(` ${msg}`)
|
|
662
|
+
}
|
|
663
|
+
)
|
|
524
664
|
|
|
525
665
|
// Apply template content if --from specified
|
|
526
666
|
if (opts.from) {
|
|
@@ -546,25 +686,44 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
546
686
|
} else {
|
|
547
687
|
const sites = await discoverSites(rootDir)
|
|
548
688
|
if (sites.length === 1) {
|
|
549
|
-
wiredSite = await wireExtensionToSite(
|
|
689
|
+
wiredSite = await wireExtensionToSite(
|
|
690
|
+
rootDir,
|
|
691
|
+
sites[0].name,
|
|
692
|
+
name,
|
|
693
|
+
target
|
|
694
|
+
)
|
|
550
695
|
} else if (sites.length > 1) {
|
|
551
696
|
if (isNonInteractive(process.argv)) {
|
|
552
697
|
unwiredReason = `Multiple sites in workspace; extension not wired. Re-run with --site <name>, or edit <site>/site.yml::extensions: manually.`
|
|
553
698
|
} else {
|
|
554
|
-
const sortedSites = [...sites].sort((a, b) =>
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
699
|
+
const sortedSites = [...sites].sort((a, b) =>
|
|
700
|
+
a.name.localeCompare(b.name)
|
|
701
|
+
)
|
|
702
|
+
const response = await prompts(
|
|
703
|
+
{
|
|
704
|
+
type: 'select',
|
|
705
|
+
name: 'site',
|
|
706
|
+
message: 'Which site is this extension for?',
|
|
707
|
+
choices: sortedSites.map((s) => ({
|
|
708
|
+
title: s.name,
|
|
709
|
+
description: s.path,
|
|
710
|
+
value: s.name
|
|
711
|
+
}))
|
|
564
712
|
},
|
|
565
|
-
|
|
713
|
+
{
|
|
714
|
+
onCancel: () => {
|
|
715
|
+
log('\nCancelled.')
|
|
716
|
+
process.exit(0)
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
)
|
|
566
720
|
if (response.site) {
|
|
567
|
-
wiredSite = await wireExtensionToSite(
|
|
721
|
+
wiredSite = await wireExtensionToSite(
|
|
722
|
+
rootDir,
|
|
723
|
+
response.site,
|
|
724
|
+
name,
|
|
725
|
+
target
|
|
726
|
+
)
|
|
568
727
|
}
|
|
569
728
|
}
|
|
570
729
|
} else {
|
|
@@ -612,17 +771,20 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
612
771
|
process.exit(1)
|
|
613
772
|
}
|
|
614
773
|
|
|
615
|
-
const response = await prompts(
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
onCancel: () => {
|
|
622
|
-
log('\nCancelled.')
|
|
623
|
-
process.exit(0)
|
|
774
|
+
const response = await prompts(
|
|
775
|
+
{
|
|
776
|
+
type: 'text',
|
|
777
|
+
name: 'name',
|
|
778
|
+
message: 'Project name:',
|
|
779
|
+
validate: (value) => validatePackageName(value)
|
|
624
780
|
},
|
|
625
|
-
|
|
781
|
+
{
|
|
782
|
+
onCancel: () => {
|
|
783
|
+
log('\nCancelled.')
|
|
784
|
+
process.exit(0)
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
)
|
|
626
788
|
name = response.name
|
|
627
789
|
}
|
|
628
790
|
|
|
@@ -652,26 +814,44 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
652
814
|
|
|
653
815
|
// Scaffold foundation (folder: src/, package name: <project>-src)
|
|
654
816
|
info(`Creating foundation: ${foundationPkgName}...`)
|
|
655
|
-
await scaffoldFoundation(
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
817
|
+
await scaffoldFoundation(
|
|
818
|
+
join(projectDir, 'src'),
|
|
819
|
+
{
|
|
820
|
+
name: foundationPkgName,
|
|
821
|
+
projectName,
|
|
822
|
+
isExtension: false
|
|
823
|
+
},
|
|
824
|
+
{ onProgress: progressCb }
|
|
825
|
+
)
|
|
660
826
|
|
|
661
827
|
// Scaffold site
|
|
662
828
|
info(`Creating site: ${sitePkgName}...`)
|
|
663
|
-
await scaffoldSite(
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
829
|
+
await scaffoldSite(
|
|
830
|
+
join(projectDir, 'site'),
|
|
831
|
+
{
|
|
832
|
+
name: sitePkgName,
|
|
833
|
+
projectName,
|
|
834
|
+
foundationName: foundationPkgName,
|
|
835
|
+
foundationPath: 'file:../src',
|
|
836
|
+
foundationRef: foundationPkgName
|
|
837
|
+
},
|
|
838
|
+
{ onProgress: progressCb }
|
|
839
|
+
)
|
|
670
840
|
|
|
671
841
|
// Apply template content if --from specified
|
|
672
842
|
if (opts.from) {
|
|
673
|
-
await applyFromTemplate(
|
|
674
|
-
|
|
843
|
+
await applyFromTemplate(
|
|
844
|
+
opts.from,
|
|
845
|
+
'foundation',
|
|
846
|
+
join(projectDir, 'src'),
|
|
847
|
+
projectName
|
|
848
|
+
)
|
|
849
|
+
await applyFromTemplate(
|
|
850
|
+
opts.from,
|
|
851
|
+
'site',
|
|
852
|
+
join(projectDir, 'site'),
|
|
853
|
+
projectName
|
|
854
|
+
)
|
|
675
855
|
}
|
|
676
856
|
|
|
677
857
|
// Update workspace globs for co-located layout
|
|
@@ -680,19 +860,22 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
680
860
|
|
|
681
861
|
// Update root scripts
|
|
682
862
|
const sites = await discoverSites(rootDir)
|
|
683
|
-
if (!sites.find(s => s.path === `${name}/site`)) {
|
|
863
|
+
if (!sites.find((s) => s.path === `${name}/site`)) {
|
|
684
864
|
sites.push({ name: sitePkgName, path: `${name}/site` })
|
|
685
865
|
}
|
|
686
866
|
await updateRootScripts(rootDir, sites, pm)
|
|
687
867
|
|
|
688
868
|
success(`Created project '${name}' at ${name}/`)
|
|
689
|
-
log(
|
|
869
|
+
log(
|
|
870
|
+
` ${colors.dim}Foundation: ${name}/src/ (${foundationPkgName})${colors.reset}`
|
|
871
|
+
)
|
|
690
872
|
log(` ${colors.dim}Site: ${name}/site/ (${sitePkgName})${colors.reset}`)
|
|
691
873
|
log('')
|
|
692
|
-
log(
|
|
874
|
+
log(
|
|
875
|
+
`Next: ${colors.cyan}${installCmd(pm)} && uniweb dev ${sitePkgName}${colors.reset}`
|
|
876
|
+
)
|
|
693
877
|
}
|
|
694
878
|
|
|
695
|
-
|
|
696
879
|
/**
|
|
697
880
|
* Resolve which foundation to wire a site to
|
|
698
881
|
*/
|
|
@@ -701,12 +884,14 @@ async function resolveFoundation(rootDir, foundationFlag) {
|
|
|
701
884
|
|
|
702
885
|
if (foundationFlag) {
|
|
703
886
|
// Find by name
|
|
704
|
-
const found = foundations.find(f => f.name === foundationFlag)
|
|
887
|
+
const found = foundations.find((f) => f.name === foundationFlag)
|
|
705
888
|
if (found) return found
|
|
706
889
|
|
|
707
890
|
// Not found — could be a URL or new foundation
|
|
708
891
|
error(`Foundation '${foundationFlag}' not found in workspace.`)
|
|
709
|
-
log(
|
|
892
|
+
log(
|
|
893
|
+
`Available foundations: ${foundations.map((f) => f.name).join(', ') || 'none'}`
|
|
894
|
+
)
|
|
710
895
|
process.exit(1)
|
|
711
896
|
}
|
|
712
897
|
|
|
@@ -722,30 +907,37 @@ async function resolveFoundation(rootDir, foundationFlag) {
|
|
|
722
907
|
// Multiple foundations — prompt (or fail in non-interactive mode)
|
|
723
908
|
if (isNonInteractive(process.argv)) {
|
|
724
909
|
error(`Multiple foundations found. Specify which to use:\n`)
|
|
725
|
-
log(
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
910
|
+
log(
|
|
911
|
+
formatOptions(
|
|
912
|
+
foundations.map((f) => ({
|
|
913
|
+
label: f.name,
|
|
914
|
+
description: f.path
|
|
915
|
+
}))
|
|
916
|
+
)
|
|
917
|
+
)
|
|
729
918
|
log('')
|
|
730
919
|
log(`Usage: ${getCliPrefix()} add site <name> --foundation <name>`)
|
|
731
920
|
process.exit(1)
|
|
732
921
|
}
|
|
733
922
|
|
|
734
|
-
const response = await prompts(
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
onCancel: () => {
|
|
745
|
-
log('\nCancelled.')
|
|
746
|
-
process.exit(0)
|
|
923
|
+
const response = await prompts(
|
|
924
|
+
{
|
|
925
|
+
type: 'select',
|
|
926
|
+
name: 'foundation',
|
|
927
|
+
message: 'Which foundation should this site use?',
|
|
928
|
+
choices: foundations.map((f) => ({
|
|
929
|
+
title: f.name,
|
|
930
|
+
description: f.path,
|
|
931
|
+
value: f
|
|
932
|
+
}))
|
|
747
933
|
},
|
|
748
|
-
|
|
934
|
+
{
|
|
935
|
+
onCancel: () => {
|
|
936
|
+
log('\nCancelled.')
|
|
937
|
+
process.exit(0)
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
)
|
|
749
941
|
|
|
750
942
|
return response.foundation
|
|
751
943
|
}
|
|
@@ -759,7 +951,6 @@ function computeFoundationPath(sitePath, foundationPath) {
|
|
|
759
951
|
return `file:${rel}`
|
|
760
952
|
}
|
|
761
953
|
|
|
762
|
-
|
|
763
954
|
/**
|
|
764
955
|
* Apply content from a template to a scaffolded package
|
|
765
956
|
*
|
|
@@ -768,34 +959,47 @@ function computeFoundationPath(sitePath, foundationPath) {
|
|
|
768
959
|
* @param {string} targetDir - Absolute path to the scaffolded package
|
|
769
960
|
* @param {string} projectName - Project name for template context
|
|
770
961
|
*/
|
|
771
|
-
async function applyFromTemplate(
|
|
962
|
+
async function applyFromTemplate(
|
|
963
|
+
templateId,
|
|
964
|
+
packageType,
|
|
965
|
+
targetDir,
|
|
966
|
+
projectName
|
|
967
|
+
) {
|
|
772
968
|
info(`Resolving template: ${templateId}...`)
|
|
773
969
|
|
|
774
970
|
const resolved = await resolveTemplate(templateId, {
|
|
775
|
-
onProgress: (msg) => info(` ${msg}`)
|
|
971
|
+
onProgress: (msg) => info(` ${msg}`)
|
|
776
972
|
})
|
|
777
973
|
|
|
778
974
|
try {
|
|
779
975
|
const metadata = await validateTemplate(resolved.path, {})
|
|
780
976
|
|
|
781
977
|
// Look in contentDirs for matching package type
|
|
782
|
-
const match =
|
|
783
|
-
|
|
978
|
+
const match =
|
|
979
|
+
metadata.contentDirs.find((d) => d.type === packageType) ||
|
|
980
|
+
metadata.contentDirs.find((d) => d.name === packageType)
|
|
784
981
|
const contentDir = match ? match.dir : null
|
|
785
982
|
|
|
786
983
|
if (contentDir) {
|
|
787
984
|
info(`Applying ${metadata.name} content...`)
|
|
788
|
-
await applyContent(
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
985
|
+
await applyContent(
|
|
986
|
+
contentDir,
|
|
987
|
+
targetDir,
|
|
988
|
+
{
|
|
989
|
+
projectName,
|
|
990
|
+
versions: getVersionsForTemplates()
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
onProgress: (msg) => info(` ${msg}`),
|
|
994
|
+
renames: match.renames
|
|
995
|
+
}
|
|
996
|
+
)
|
|
795
997
|
|
|
796
998
|
// Merge template dependencies
|
|
797
999
|
if (metadata.dependencies) {
|
|
798
|
-
const deps =
|
|
1000
|
+
const deps =
|
|
1001
|
+
metadata.dependencies[packageType] ||
|
|
1002
|
+
metadata.dependencies[match?.name]
|
|
799
1003
|
if (deps) {
|
|
800
1004
|
await mergeTemplateDependencies(join(targetDir, 'package.json'), deps)
|
|
801
1005
|
}
|
|
@@ -804,11 +1008,15 @@ async function applyFromTemplate(templateId, packageType, targetDir, projectName
|
|
|
804
1008
|
// If site content applied, inform about expected section types
|
|
805
1009
|
if (packageType === 'site' && metadata.components) {
|
|
806
1010
|
log('')
|
|
807
|
-
info(
|
|
1011
|
+
info(
|
|
1012
|
+
`This template expects section types: ${metadata.components.join(', ')}`
|
|
1013
|
+
)
|
|
808
1014
|
info(`Make sure your foundation provides them.`)
|
|
809
1015
|
}
|
|
810
1016
|
} else {
|
|
811
|
-
info(
|
|
1017
|
+
info(
|
|
1018
|
+
`Template '${metadata.name}' has no ${packageType} content to apply.`
|
|
1019
|
+
)
|
|
812
1020
|
}
|
|
813
1021
|
} finally {
|
|
814
1022
|
if (resolved.cleanup) await resolved.cleanup()
|
|
@@ -818,10 +1026,15 @@ async function applyFromTemplate(templateId, packageType, targetDir, projectName
|
|
|
818
1026
|
/**
|
|
819
1027
|
* Wire an extension URL to a site's site.yml
|
|
820
1028
|
*/
|
|
821
|
-
async function wireExtensionToSite(
|
|
1029
|
+
async function wireExtensionToSite(
|
|
1030
|
+
rootDir,
|
|
1031
|
+
siteName,
|
|
1032
|
+
extensionName,
|
|
1033
|
+
extensionPath
|
|
1034
|
+
) {
|
|
822
1035
|
// Find the site directory
|
|
823
1036
|
const sites = await discoverSites(rootDir)
|
|
824
|
-
const site = sites.find(s => s.name === siteName)
|
|
1037
|
+
const site = sites.find((s) => s.name === siteName)
|
|
825
1038
|
if (!site) {
|
|
826
1039
|
info(`Could not find site '${siteName}' to wire extension.`)
|
|
827
1040
|
return null
|
|
@@ -846,7 +1059,10 @@ async function wireExtensionToSite(rootDir, siteName, extensionName, extensionPa
|
|
|
846
1059
|
config.extensions.push(extensionUrl)
|
|
847
1060
|
}
|
|
848
1061
|
|
|
849
|
-
await writeFile(
|
|
1062
|
+
await writeFile(
|
|
1063
|
+
siteYmlPath,
|
|
1064
|
+
yaml.dump(config, { flowLevel: -1, quotingType: "'" })
|
|
1065
|
+
)
|
|
850
1066
|
return siteName
|
|
851
1067
|
} catch (err) {
|
|
852
1068
|
info(`Warning: Could not update site.yml: ${err.message}`)
|
|
@@ -869,23 +1085,30 @@ async function addSection(rootDir, opts) {
|
|
|
869
1085
|
process.exit(1)
|
|
870
1086
|
}
|
|
871
1087
|
|
|
872
|
-
const response = await prompts(
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
process.exit(0)
|
|
1088
|
+
const response = await prompts(
|
|
1089
|
+
{
|
|
1090
|
+
type: 'text',
|
|
1091
|
+
name: 'name',
|
|
1092
|
+
message: 'Section name (PascalCase):',
|
|
1093
|
+
validate: (value) =>
|
|
1094
|
+
/^[A-Z][a-zA-Z0-9]*$/.test(value) ||
|
|
1095
|
+
'Use PascalCase: Hero, FeatureGrid, CallToAction'
|
|
881
1096
|
},
|
|
882
|
-
|
|
1097
|
+
{
|
|
1098
|
+
onCancel: () => {
|
|
1099
|
+
log('\nCancelled.')
|
|
1100
|
+
process.exit(0)
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
)
|
|
883
1104
|
name = response.name
|
|
884
1105
|
}
|
|
885
1106
|
|
|
886
1107
|
// Validate PascalCase
|
|
887
1108
|
if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
|
|
888
|
-
error(
|
|
1109
|
+
error(
|
|
1110
|
+
`Section name must be PascalCase (e.g., Hero, FeatureGrid, CallToAction).`
|
|
1111
|
+
)
|
|
889
1112
|
process.exit(1)
|
|
890
1113
|
}
|
|
891
1114
|
|
|
@@ -900,30 +1123,41 @@ async function addSection(rootDir, opts) {
|
|
|
900
1123
|
} else if (foundations.length === 1) {
|
|
901
1124
|
foundation = foundations[0]
|
|
902
1125
|
} else if (opts.foundation) {
|
|
903
|
-
foundation = foundations.find(f => f.name === opts.foundation)
|
|
1126
|
+
foundation = foundations.find((f) => f.name === opts.foundation)
|
|
904
1127
|
if (!foundation) {
|
|
905
1128
|
error(`Foundation '${opts.foundation}' not found.`)
|
|
906
|
-
log(`Available: ${foundations.map(f => f.name).join(', ')}`)
|
|
1129
|
+
log(`Available: ${foundations.map((f) => f.name).join(', ')}`)
|
|
907
1130
|
process.exit(1)
|
|
908
1131
|
}
|
|
909
1132
|
} else if (isNonInteractive(process.argv)) {
|
|
910
1133
|
error(`Multiple foundations found. Specify which to use:\n`)
|
|
911
|
-
log(
|
|
1134
|
+
log(
|
|
1135
|
+
formatOptions(
|
|
1136
|
+
foundations.map((f) => ({ label: f.name, description: f.path }))
|
|
1137
|
+
)
|
|
1138
|
+
)
|
|
912
1139
|
log('')
|
|
913
1140
|
log(`Usage: ${getCliPrefix()} add section ${name} --foundation <name>`)
|
|
914
1141
|
process.exit(1)
|
|
915
1142
|
} else {
|
|
916
|
-
const response = await prompts(
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1143
|
+
const response = await prompts(
|
|
1144
|
+
{
|
|
1145
|
+
type: 'select',
|
|
1146
|
+
name: 'foundation',
|
|
1147
|
+
message: 'Which foundation?',
|
|
1148
|
+
choices: foundations.map((f) => ({
|
|
1149
|
+
title: f.name,
|
|
1150
|
+
description: f.path,
|
|
1151
|
+
value: f
|
|
1152
|
+
}))
|
|
925
1153
|
},
|
|
926
|
-
|
|
1154
|
+
{
|
|
1155
|
+
onCancel: () => {
|
|
1156
|
+
log('\nCancelled.')
|
|
1157
|
+
process.exit(0)
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
)
|
|
927
1161
|
foundation = response.foundation
|
|
928
1162
|
}
|
|
929
1163
|
|
|
@@ -936,7 +1170,9 @@ async function addSection(rootDir, opts) {
|
|
|
936
1170
|
const relSectionPath = relative(foundationDir, sectionDir)
|
|
937
1171
|
|
|
938
1172
|
if (existsSync(sectionDir)) {
|
|
939
|
-
error(
|
|
1173
|
+
error(
|
|
1174
|
+
`Section '${name}' already exists at ${foundation.path}/${relSectionPath}/`
|
|
1175
|
+
)
|
|
940
1176
|
process.exit(1)
|
|
941
1177
|
}
|
|
942
1178
|
|
|
@@ -982,11 +1218,17 @@ export default function ${name}({ content, params }) {
|
|
|
982
1218
|
await writeFile(join(sectionDir, 'meta.js'), metaContent)
|
|
983
1219
|
|
|
984
1220
|
success(`Created section '${name}' at ${foundation.path}/${relSectionPath}/`)
|
|
985
|
-
log(
|
|
986
|
-
|
|
1221
|
+
log(
|
|
1222
|
+
` ${colors.dim}index.jsx${colors.reset} — component (customize the JSX)`
|
|
1223
|
+
)
|
|
1224
|
+
log(
|
|
1225
|
+
` ${colors.dim}meta.js${colors.reset} — metadata (add content expectations, params, presets)`
|
|
1226
|
+
)
|
|
987
1227
|
if (foundations.length === 1) {
|
|
988
1228
|
log('')
|
|
989
|
-
log(
|
|
1229
|
+
log(
|
|
1230
|
+
`${colors.dim}The dev server will pick it up automatically.${colors.reset}`
|
|
1231
|
+
)
|
|
990
1232
|
}
|
|
991
1233
|
}
|
|
992
1234
|
|
|
@@ -1006,7 +1248,7 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
|
|
|
1006
1248
|
// every add invocation.
|
|
1007
1249
|
let getAdapter, listAdapters
|
|
1008
1250
|
try {
|
|
1009
|
-
({ getAdapter, listAdapters } = await import('@uniweb/build/hosts'))
|
|
1251
|
+
;({ getAdapter, listAdapters } = await import('@uniweb/build/hosts'))
|
|
1010
1252
|
} catch {
|
|
1011
1253
|
error('Failed to load host adapter registry from @uniweb/build/hosts.')
|
|
1012
1254
|
process.exit(1)
|
|
@@ -1026,9 +1268,11 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
|
|
|
1026
1268
|
// capability, never a hardcoded list — an adapter that grows an initCi
|
|
1027
1269
|
// shows up here with no edit to this file.
|
|
1028
1270
|
const ciHosts = listAdapters()
|
|
1029
|
-
.map(name => getAdapter(name))
|
|
1030
|
-
.filter(a => typeof a.initCi === 'function')
|
|
1031
|
-
.filter(a =>
|
|
1271
|
+
.map((name) => getAdapter(name))
|
|
1272
|
+
.filter((a) => typeof a.initCi === 'function')
|
|
1273
|
+
.filter((a) =>
|
|
1274
|
+
target === 'foundation' ? a.display?.foundationCi === true : true
|
|
1275
|
+
)
|
|
1032
1276
|
.sort((a, b) => (a.display?.order ?? 999) - (b.display?.order ?? 999))
|
|
1033
1277
|
|
|
1034
1278
|
if (ciHosts.length === 0) {
|
|
@@ -1042,24 +1286,30 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
|
|
|
1042
1286
|
host = ciHosts[0].name
|
|
1043
1287
|
} else if (isNonInteractive(process.argv)) {
|
|
1044
1288
|
error('`uniweb add ci` needs a host.')
|
|
1045
|
-
log(`Available: ${ciHosts.map(a => a.name).join(', ')}`)
|
|
1289
|
+
log(`Available: ${ciHosts.map((a) => a.name).join(', ')}`)
|
|
1046
1290
|
process.exit(1)
|
|
1047
1291
|
} else {
|
|
1048
|
-
const choice = await prompts(
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1292
|
+
const choice = await prompts(
|
|
1293
|
+
{
|
|
1294
|
+
type: 'select',
|
|
1295
|
+
name: 'host',
|
|
1296
|
+
message: 'Which host should the workflow deploy to?',
|
|
1297
|
+
choices: ciHosts.map((a) => ({
|
|
1298
|
+
title:
|
|
1299
|
+
`${a.display?.title || a.name} · ${a.display?.qualifier || ''}`
|
|
1300
|
+
.trim()
|
|
1301
|
+
.replace(/ ·\s*$/, ''),
|
|
1302
|
+
description: a.display?.summary,
|
|
1303
|
+
value: a.name
|
|
1304
|
+
}))
|
|
1061
1305
|
},
|
|
1062
|
-
|
|
1306
|
+
{
|
|
1307
|
+
onCancel: () => {
|
|
1308
|
+
log('\nCancelled.')
|
|
1309
|
+
process.exit(0)
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
)
|
|
1063
1313
|
host = choice.host
|
|
1064
1314
|
}
|
|
1065
1315
|
}
|
|
@@ -1074,8 +1324,10 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
|
|
|
1074
1324
|
|
|
1075
1325
|
if (typeof adapter.initCi !== 'function') {
|
|
1076
1326
|
error(`Host '${host}' does not provide a CI workflow.`)
|
|
1077
|
-
log(`Hosts that do: ${ciHosts.map(a => a.name).join(', ')}`)
|
|
1078
|
-
log(
|
|
1327
|
+
log(`Hosts that do: ${ciHosts.map((a) => a.name).join(', ')}`)
|
|
1328
|
+
log(
|
|
1329
|
+
`Others are dashboard-driven — connect the repo in the host's UI instead.`
|
|
1330
|
+
)
|
|
1079
1331
|
process.exit(1)
|
|
1080
1332
|
}
|
|
1081
1333
|
|
|
@@ -1084,7 +1336,7 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
|
|
|
1084
1336
|
// scaffold a SITE workflow — the wrong artifact, with no error.
|
|
1085
1337
|
if (target === 'foundation' && adapter.display?.foundationCi !== true) {
|
|
1086
1338
|
error(`Host '${host}' cannot publish a foundation.`)
|
|
1087
|
-
log(`Hosts that can: ${ciHosts.map(a => a.name).join(', ')}`)
|
|
1339
|
+
log(`Hosts that can: ${ciHosts.map((a) => a.name).join(', ')}`)
|
|
1088
1340
|
log('')
|
|
1089
1341
|
log('Foundation publishing needs permanent versioned URLs, which the')
|
|
1090
1342
|
log('gh-pages branch layout provides. Other hosts overwrite on deploy.')
|
|
@@ -1100,7 +1352,9 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
|
|
|
1100
1352
|
// it (writes a CNAME, switches UNIWEB_BASE to root).
|
|
1101
1353
|
if (opts.domain && !isLikelyDomain(opts.domain)) {
|
|
1102
1354
|
error(`Invalid --domain value: '${opts.domain}'`)
|
|
1103
|
-
log(
|
|
1355
|
+
log(
|
|
1356
|
+
`Expected a bare hostname (e.g., 'mysite.com' or 'docs.mysite.com'). No scheme, no path.`
|
|
1357
|
+
)
|
|
1104
1358
|
process.exit(1)
|
|
1105
1359
|
}
|
|
1106
1360
|
|
|
@@ -1114,37 +1368,46 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
|
|
|
1114
1368
|
// Resolve site: --site flag, single site auto, prompt, or error.
|
|
1115
1369
|
const sites = await discoverSites(rootDir)
|
|
1116
1370
|
if (sites.length === 0) {
|
|
1117
|
-
error(
|
|
1371
|
+
error(
|
|
1372
|
+
'No site found in this workspace. Add one with `uniweb add site` first.'
|
|
1373
|
+
)
|
|
1118
1374
|
process.exit(1)
|
|
1119
1375
|
}
|
|
1120
1376
|
|
|
1121
1377
|
let site
|
|
1122
1378
|
if (opts.site) {
|
|
1123
|
-
site = sites.find(s => s.name === opts.site)
|
|
1379
|
+
site = sites.find((s) => s.name === opts.site)
|
|
1124
1380
|
if (!site) {
|
|
1125
1381
|
error(`Site '${opts.site}' not found.`)
|
|
1126
|
-
log(`Available sites: ${sites.map(s => s.name).join(', ')}`)
|
|
1382
|
+
log(`Available sites: ${sites.map((s) => s.name).join(', ')}`)
|
|
1127
1383
|
process.exit(1)
|
|
1128
1384
|
}
|
|
1129
1385
|
} else if (sites.length === 1) {
|
|
1130
1386
|
site = sites[0]
|
|
1131
1387
|
} else if (isNonInteractive(process.argv)) {
|
|
1132
1388
|
error(`Multiple sites in workspace. Specify --site <name>.`)
|
|
1133
|
-
log(`Available sites: ${sites.map(s => s.name).join(', ')}`)
|
|
1389
|
+
log(`Available sites: ${sites.map((s) => s.name).join(', ')}`)
|
|
1134
1390
|
process.exit(1)
|
|
1135
1391
|
} else {
|
|
1136
1392
|
const sortedSites = [...sites].sort((a, b) => a.name.localeCompare(b.name))
|
|
1137
|
-
const response = await prompts(
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1393
|
+
const response = await prompts(
|
|
1394
|
+
{
|
|
1395
|
+
type: 'select',
|
|
1396
|
+
name: 'site',
|
|
1397
|
+
message: 'Which site should the workflow build?',
|
|
1398
|
+
choices: sortedSites.map((s) => ({
|
|
1399
|
+
title: s.name,
|
|
1400
|
+
description: s.path,
|
|
1401
|
+
value: s
|
|
1402
|
+
}))
|
|
1146
1403
|
},
|
|
1147
|
-
|
|
1404
|
+
{
|
|
1405
|
+
onCancel: () => {
|
|
1406
|
+
log('\nCancelled.')
|
|
1407
|
+
process.exit(0)
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
)
|
|
1148
1411
|
site = response.site
|
|
1149
1412
|
}
|
|
1150
1413
|
|
|
@@ -1158,10 +1421,20 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
|
|
|
1158
1421
|
// project's floor, raised if that pnpm needs more.
|
|
1159
1422
|
const installedPnpm = detectInstalledPnpmVersion(rootDir)
|
|
1160
1423
|
const pnpmVersion = resolveCiPnpmVersion(rootPkg, installedPnpm)
|
|
1161
|
-
const nodeVersion = resolveCiNodeVersion(
|
|
1424
|
+
const nodeVersion = resolveCiNodeVersion(
|
|
1425
|
+
rootPkg.engines?.node,
|
|
1426
|
+
pm,
|
|
1427
|
+
pnpmVersion
|
|
1428
|
+
)
|
|
1162
1429
|
reportCiToolchain({
|
|
1163
|
-
pm,
|
|
1164
|
-
|
|
1430
|
+
pm,
|
|
1431
|
+
pnpmVersion,
|
|
1432
|
+
nodeVersion,
|
|
1433
|
+
source: rootPkg?.packageManager
|
|
1434
|
+
? 'declared'
|
|
1435
|
+
: installedPnpm
|
|
1436
|
+
? 'installed'
|
|
1437
|
+
: 'fallback'
|
|
1165
1438
|
})
|
|
1166
1439
|
|
|
1167
1440
|
const siteDir = join(rootDir, site.path)
|
|
@@ -1187,7 +1460,13 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
|
|
|
1187
1460
|
pnpmVersion,
|
|
1188
1461
|
domain: resolvedDomain,
|
|
1189
1462
|
previews: opts.previews !== false,
|
|
1190
|
-
projectName: resolveHostProjectName(
|
|
1463
|
+
projectName: resolveHostProjectName(
|
|
1464
|
+
rootDir,
|
|
1465
|
+
rootPkg,
|
|
1466
|
+
site,
|
|
1467
|
+
sites.length,
|
|
1468
|
+
opts
|
|
1469
|
+
)
|
|
1191
1470
|
})
|
|
1192
1471
|
|
|
1193
1472
|
await writeCiFiles(rootDir, result.files, opts.force)
|
|
@@ -1206,7 +1485,7 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
|
|
|
1206
1485
|
const { recordTarget } = await import('@uniweb/build/site')
|
|
1207
1486
|
const writeResult = await recordTarget(siteDir, {
|
|
1208
1487
|
targetName: host,
|
|
1209
|
-
targetConfig: result.targetConfig
|
|
1488
|
+
targetConfig: result.targetConfig
|
|
1210
1489
|
})
|
|
1211
1490
|
success(
|
|
1212
1491
|
writeResult.action === 'scaffold'
|
|
@@ -1252,7 +1531,12 @@ const GENERIC_SITE_NAMES = new Set(['site', 'sites', 'www', 'web', 'app'])
|
|
|
1252
1531
|
* Prefer a meaningful package/directory name; fall back to the workspace
|
|
1253
1532
|
* name; disambiguate only when several foundations would collide.
|
|
1254
1533
|
*/
|
|
1255
|
-
const GENERIC_FOUNDATION_NAMES = new Set([
|
|
1534
|
+
const GENERIC_FOUNDATION_NAMES = new Set([
|
|
1535
|
+
'src',
|
|
1536
|
+
'foundation',
|
|
1537
|
+
'foundations',
|
|
1538
|
+
'lib'
|
|
1539
|
+
])
|
|
1256
1540
|
|
|
1257
1541
|
function resolveFoundationPublicNames(rootDir, rootPkg, foundations) {
|
|
1258
1542
|
const workspaceName = stripScope(rootPkg?.name) || basename(rootDir)
|
|
@@ -1302,14 +1586,17 @@ function reportCiToolchain({ pm, pnpmVersion, nodeVersion, source }) {
|
|
|
1302
1586
|
info(`CI will use ${pm} + Node ${nodeVersion}`)
|
|
1303
1587
|
return
|
|
1304
1588
|
}
|
|
1305
|
-
const origin =
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1589
|
+
const origin =
|
|
1590
|
+
{
|
|
1591
|
+
declared: '',
|
|
1592
|
+
installed: ' (matching your install)',
|
|
1593
|
+
fallback: ' (no pnpm detected — using the default)'
|
|
1594
|
+
}[source] ?? ''
|
|
1310
1595
|
info(`CI will use pnpm ${pnpmVersion} + Node ${nodeVersion}${origin}`)
|
|
1311
1596
|
if (source === 'fallback') {
|
|
1312
|
-
info(
|
|
1597
|
+
info(
|
|
1598
|
+
`Pin with "packageManager": "pnpm@<version>" in package.json to choose a major.`
|
|
1599
|
+
)
|
|
1313
1600
|
}
|
|
1314
1601
|
}
|
|
1315
1602
|
|
|
@@ -1356,10 +1643,12 @@ async function addFoundationCi(rootDir, opts, adapter, pm) {
|
|
|
1356
1643
|
// its own versioned directory, so there's no collision).
|
|
1357
1644
|
let foundations = all
|
|
1358
1645
|
if (opts.foundation) {
|
|
1359
|
-
const match = all.find(
|
|
1646
|
+
const match = all.find(
|
|
1647
|
+
(f) => f.name === opts.foundation || basename(f.path) === opts.foundation
|
|
1648
|
+
)
|
|
1360
1649
|
if (!match) {
|
|
1361
1650
|
error(`Foundation '${opts.foundation}' not found.`)
|
|
1362
|
-
log(`Available: ${all.map(f => f.name).join(', ')}`)
|
|
1651
|
+
log(`Available: ${all.map((f) => f.name).join(', ')}`)
|
|
1363
1652
|
process.exit(1)
|
|
1364
1653
|
}
|
|
1365
1654
|
foundations = [match]
|
|
@@ -1373,10 +1662,20 @@ async function addFoundationCi(rootDir, opts, adapter, pm) {
|
|
|
1373
1662
|
// project's floor, raised if that pnpm needs more.
|
|
1374
1663
|
const installedPnpm = detectInstalledPnpmVersion(rootDir)
|
|
1375
1664
|
const pnpmVersion = resolveCiPnpmVersion(rootPkg, installedPnpm)
|
|
1376
|
-
const nodeVersion = resolveCiNodeVersion(
|
|
1665
|
+
const nodeVersion = resolveCiNodeVersion(
|
|
1666
|
+
rootPkg.engines?.node,
|
|
1667
|
+
pm,
|
|
1668
|
+
pnpmVersion
|
|
1669
|
+
)
|
|
1377
1670
|
reportCiToolchain({
|
|
1378
|
-
pm,
|
|
1379
|
-
|
|
1671
|
+
pm,
|
|
1672
|
+
pnpmVersion,
|
|
1673
|
+
nodeVersion,
|
|
1674
|
+
source: rootPkg?.packageManager
|
|
1675
|
+
? 'declared'
|
|
1676
|
+
: installedPnpm
|
|
1677
|
+
? 'installed'
|
|
1678
|
+
: 'fallback'
|
|
1380
1679
|
})
|
|
1381
1680
|
|
|
1382
1681
|
let result
|
|
@@ -1387,7 +1686,7 @@ async function addFoundationCi(rootDir, opts, adapter, pm) {
|
|
|
1387
1686
|
target: 'foundation',
|
|
1388
1687
|
packageManager: pm,
|
|
1389
1688
|
nodeVersion,
|
|
1390
|
-
pnpmVersion
|
|
1689
|
+
pnpmVersion
|
|
1391
1690
|
})
|
|
1392
1691
|
} catch (err) {
|
|
1393
1692
|
error(err.message)
|
|
@@ -1406,11 +1705,14 @@ async function addFoundationCi(rootDir, opts, adapter, pm) {
|
|
|
1406
1705
|
}
|
|
1407
1706
|
|
|
1408
1707
|
function isLikelyDomain(value) {
|
|
1409
|
-
if (typeof value !== 'string' || value.length === 0 || value.length > 253)
|
|
1708
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 253)
|
|
1709
|
+
return false
|
|
1410
1710
|
// Reject schemes, paths, ports, whitespace, leading/trailing dots/hyphens.
|
|
1411
1711
|
// Each label is 1–63 chars of [a-z0-9-], no leading/trailing hyphen.
|
|
1412
1712
|
// The TLD must be at least 2 characters of letters.
|
|
1413
|
-
return /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i.test(
|
|
1713
|
+
return /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i.test(
|
|
1714
|
+
value
|
|
1715
|
+
)
|
|
1414
1716
|
}
|
|
1415
1717
|
|
|
1416
1718
|
function parseNodeMajor(engines) {
|