uniweb 0.10.13 → 0.12.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 +20 -18
- package/package.json +2 -2
- package/partials/agents.md +163 -39
- package/src/commands/add.js +152 -233
- package/src/commands/build.js +14 -42
- package/src/commands/deploy.js +262 -3
- package/src/commands/docs.js +5 -6
- package/src/commands/doctor.js +21 -22
- package/src/commands/publish.js +255 -34
- package/src/framework-index.json +3 -3
- package/src/index.js +27 -14
- package/src/utils/auth.js +82 -6
- package/src/utils/names.js +9 -2
- package/src/utils/registry.js +88 -16
- package/src/utils/scaffold.js +8 -2
- package/src/utils/workspace.js +8 -46
- package/starter/site/pages/home/1-welcome.md.hbs +1 -1
- package/templates/foundation/{src/foundation.js.hbs → main.js.hbs} +1 -1
- package/templates/foundation/package.json.hbs +6 -6
- package/templates/foundation/{src/styles.css → styles.css} +1 -1
- package/templates/site/index.html.hbs +1 -1
- package/templates/site/theme.yml +1 -1
- package/templates/workspace/README.md.hbs +9 -7
- /package/starter/foundation/{src/foundation.js → main.js} +0 -0
- /package/starter/foundation/{src/sections → sections}/Section/index.jsx +0 -0
- /package/starter/foundation/{src/sections → sections}/Section/meta.js +0 -0
- /package/templates/foundation/{src/components → components}/.gitkeep +0 -0
- /package/templates/foundation/{src/sections → sections}/.gitkeep +0 -0
- /package/templates/site/{main.js → entry.js} +0 -0
package/src/commands/add.js
CHANGED
|
@@ -16,6 +16,7 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
|
16
16
|
import { join, relative } from 'node:path'
|
|
17
17
|
import prompts from 'prompts'
|
|
18
18
|
import yaml from 'js-yaml'
|
|
19
|
+
import { resolveFoundationSrcPath } from '@uniweb/build'
|
|
19
20
|
import { scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies } from '../utils/scaffold.js'
|
|
20
21
|
import {
|
|
21
22
|
readWorkspaceConfig,
|
|
@@ -186,57 +187,45 @@ export async function add(rawArgs) {
|
|
|
186
187
|
* Add a foundation to the workspace
|
|
187
188
|
*/
|
|
188
189
|
async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
189
|
-
|
|
190
|
+
const name = opts.name
|
|
190
191
|
const existingNames = await getExistingPackageNames(rootDir)
|
|
191
192
|
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
// Resolve placement first (path + package name) so we have everything we
|
|
194
|
+
// need to validate before scaffolding. Note: `name` here may be a bare
|
|
195
|
+
// name (`ui`) or a path (`foundations/ui`); resolvePlacement handles
|
|
196
|
+
// both. Format validation runs on the derived package name below, not
|
|
197
|
+
// on the raw input — slashes in the input are intentional path syntax.
|
|
198
|
+
const FOUNDATION_KIND = { defaultDir: 'src', defaultPkg: 'src', projectSub: 'src' }
|
|
199
|
+
const { relativePath, packageName } = resolvePlacement(rootDir, name, opts, FOUNDATION_KIND)
|
|
200
|
+
const fullPath = join(rootDir, relativePath)
|
|
201
|
+
|
|
202
|
+
// Validate the derived package name (format + reserved-name check). The
|
|
203
|
+
// auto-derived `src` default is grandfathered in (`src` IS reserved
|
|
204
|
+
// but `src` is the convention for "the package that lives in src/").
|
|
205
|
+
if (packageName !== 'src') {
|
|
206
|
+
const valid = validatePackageName(packageName)
|
|
195
207
|
if (valid !== true) {
|
|
196
208
|
error(valid)
|
|
197
209
|
process.exit(1)
|
|
198
210
|
}
|
|
199
211
|
}
|
|
200
212
|
|
|
201
|
-
//
|
|
202
|
-
if (!name && !opts.path) {
|
|
203
|
-
if (!isNonInteractive(process.argv)) {
|
|
204
|
-
const foundations = await discoverFoundations(rootDir)
|
|
205
|
-
const hasDefault = foundations.length === 0 && !existsSync(join(rootDir, 'foundation'))
|
|
206
|
-
const response = await prompts({
|
|
207
|
-
type: 'text',
|
|
208
|
-
name: 'name',
|
|
209
|
-
message: 'Foundation name:',
|
|
210
|
-
initial: hasDefault ? 'foundation' : undefined,
|
|
211
|
-
validate: (value) => validatePackageName(value),
|
|
212
|
-
}, {
|
|
213
|
-
onCancel: () => {
|
|
214
|
-
log('\nCancelled.')
|
|
215
|
-
process.exit(0)
|
|
216
|
-
},
|
|
217
|
-
})
|
|
218
|
-
// Only set name if user chose something other than the default —
|
|
219
|
-
// null name tells resolveFoundationTarget to use default placement (./foundation/)
|
|
220
|
-
if (!hasDefault || response.name !== 'foundation') {
|
|
221
|
-
name = response.name
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
// Non-interactive without name: defaults to 'foundation' — resolveFoundationTarget handles it
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const target = await resolveFoundationTarget(rootDir, name, opts)
|
|
228
|
-
const fullPath = join(rootDir, target)
|
|
229
|
-
|
|
213
|
+
// Collision check 1: target folder already exists.
|
|
230
214
|
if (existsSync(fullPath)) {
|
|
231
|
-
error(`
|
|
215
|
+
error(`Cannot create foundation: ${colors.bright}${relativePath}/${colors.reset} already exists.`)
|
|
216
|
+
log('')
|
|
217
|
+
log(`Pick a different name, or pass --path to choose a different folder:`)
|
|
218
|
+
log(` ${colors.cyan}${getCliPrefix()} add foundation <name>${colors.reset}`)
|
|
219
|
+
log(` ${colors.cyan}${getCliPrefix()} add foundation <name> --path <parent-dir>${colors.reset}`)
|
|
232
220
|
process.exit(1)
|
|
233
221
|
}
|
|
234
222
|
|
|
235
|
-
//
|
|
236
|
-
|
|
223
|
+
// Collision check 2: a package with the same name already exists somewhere
|
|
224
|
+
// in the workspace.
|
|
237
225
|
if (existingNames.has(packageName)) {
|
|
238
|
-
error(`
|
|
239
|
-
log(`
|
|
226
|
+
error(`Cannot create foundation: a package named ${colors.bright}${packageName}${colors.reset} already exists in this workspace.`)
|
|
227
|
+
log(`Pick a different name:`)
|
|
228
|
+
log(` ${colors.cyan}${getCliPrefix()} add foundation <other-name>${colors.reset}`)
|
|
240
229
|
process.exit(1)
|
|
241
230
|
}
|
|
242
231
|
|
|
@@ -254,15 +243,16 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
254
243
|
await applyFromTemplate(opts.from, 'foundation', fullPath, projectName)
|
|
255
244
|
}
|
|
256
245
|
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
246
|
+
// Register the package in pnpm-workspace.yaml — by exact path, not by glob.
|
|
247
|
+
// No glob inference: if the user wants `foundations/*` they can edit the
|
|
248
|
+
// workspace file themselves. This matches the "no assumptions" rule.
|
|
249
|
+
await addWorkspaceGlob(rootDir, relativePath)
|
|
260
250
|
|
|
261
251
|
// Update root scripts
|
|
262
252
|
const sites = await discoverSites(rootDir)
|
|
263
253
|
await updateRootScripts(rootDir, sites, pm)
|
|
264
254
|
|
|
265
|
-
success(`Created foundation
|
|
255
|
+
success(`Created foundation ${colors.bright}${packageName}${colors.reset} at ${relativePath}/`)
|
|
266
256
|
log('')
|
|
267
257
|
log(`Next: ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
268
258
|
}
|
|
@@ -271,66 +261,47 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
271
261
|
* Add a site to the workspace
|
|
272
262
|
*/
|
|
273
263
|
async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
274
|
-
|
|
264
|
+
const name = opts.name
|
|
275
265
|
const existingNames = await getExistingPackageNames(rootDir)
|
|
276
266
|
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
267
|
+
// Resolve placement first (path + package name); see notes in addFoundation.
|
|
268
|
+
const SITE_KIND = { defaultDir: 'site', defaultPkg: 'site', projectSub: 'site' }
|
|
269
|
+
const { relativePath, packageName: siteName } = resolvePlacement(rootDir, name, opts, SITE_KIND)
|
|
270
|
+
const fullPath = join(rootDir, relativePath)
|
|
271
|
+
|
|
272
|
+
// Validate the package name (skip for the auto-derived 'site' default).
|
|
273
|
+
if (siteName !== 'site') {
|
|
274
|
+
const valid = validatePackageName(siteName)
|
|
280
275
|
if (valid !== true) {
|
|
281
276
|
error(valid)
|
|
282
277
|
process.exit(1)
|
|
283
278
|
}
|
|
284
279
|
}
|
|
285
280
|
|
|
286
|
-
//
|
|
287
|
-
if (!name && !opts.path) {
|
|
288
|
-
if (!isNonInteractive(process.argv)) {
|
|
289
|
-
const existingSites = await discoverSites(rootDir)
|
|
290
|
-
const hasDefault = existingSites.length === 0 && !existsSync(join(rootDir, 'site'))
|
|
291
|
-
const response = await prompts({
|
|
292
|
-
type: 'text',
|
|
293
|
-
name: 'name',
|
|
294
|
-
message: 'Site name:',
|
|
295
|
-
initial: hasDefault ? 'site' : undefined,
|
|
296
|
-
validate: (value) => validatePackageName(value),
|
|
297
|
-
}, {
|
|
298
|
-
onCancel: () => {
|
|
299
|
-
log('\nCancelled.')
|
|
300
|
-
process.exit(0)
|
|
301
|
-
},
|
|
302
|
-
})
|
|
303
|
-
// Only set name if user chose something other than the default —
|
|
304
|
-
// null name tells resolveSiteTarget to use default placement (./site/)
|
|
305
|
-
if (!hasDefault || response.name !== 'site') {
|
|
306
|
-
name = response.name
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
// Non-interactive without name: defaults to 'site' — resolveSiteTarget handles it
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const target = await resolveSiteTarget(rootDir, name, opts)
|
|
313
|
-
const fullPath = join(rootDir, target)
|
|
314
|
-
|
|
281
|
+
// Collision check 1: target folder exists.
|
|
315
282
|
if (existsSync(fullPath)) {
|
|
316
|
-
error(`
|
|
283
|
+
error(`Cannot create site: ${colors.bright}${relativePath}/${colors.reset} already exists.`)
|
|
284
|
+
log('')
|
|
285
|
+
log(`Pick a different name, or pass --path to choose a different folder:`)
|
|
286
|
+
log(` ${colors.cyan}${getCliPrefix()} add site <name>${colors.reset}`)
|
|
287
|
+
log(` ${colors.cyan}${getCliPrefix()} add site <name> --path <parent-dir>${colors.reset}`)
|
|
317
288
|
process.exit(1)
|
|
318
289
|
}
|
|
319
290
|
|
|
320
|
-
//
|
|
321
|
-
const foundation = await resolveFoundation(rootDir, opts.foundation)
|
|
322
|
-
|
|
323
|
-
// Package name = name or 'site'
|
|
324
|
-
const siteName = name || 'site'
|
|
291
|
+
// Collision check 2: package name already in workspace.
|
|
325
292
|
if (existingNames.has(siteName)) {
|
|
326
|
-
error(`
|
|
327
|
-
log(`
|
|
293
|
+
error(`Cannot create site: a package named ${colors.bright}${siteName}${colors.reset} already exists in this workspace.`)
|
|
294
|
+
log(`Pick a different name:`)
|
|
295
|
+
log(` ${colors.cyan}${getCliPrefix()} add site <other-name>${colors.reset}`)
|
|
328
296
|
process.exit(1)
|
|
329
297
|
}
|
|
330
298
|
|
|
299
|
+
// Resolve foundation
|
|
300
|
+
const foundation = await resolveFoundation(rootDir, opts.foundation)
|
|
301
|
+
|
|
331
302
|
if (foundation) {
|
|
332
303
|
// Compute relative path from site to foundation
|
|
333
|
-
const foundationPath = computeFoundationPath(
|
|
304
|
+
const foundationPath = computeFoundationPath(relativePath, foundation.path)
|
|
334
305
|
|
|
335
306
|
// Scaffold
|
|
336
307
|
await scaffoldSite(fullPath, {
|
|
@@ -360,28 +331,26 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
360
331
|
await applyFromTemplate(opts.from, 'site', fullPath, projectName)
|
|
361
332
|
}
|
|
362
333
|
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
await addWorkspaceGlob(rootDir, glob)
|
|
334
|
+
// Register the package by exact path. No glob inference.
|
|
335
|
+
await addWorkspaceGlob(rootDir, relativePath)
|
|
366
336
|
|
|
367
|
-
// Update root scripts (discover sites after
|
|
337
|
+
// Update root scripts (discover sites after registration — includes the new one)
|
|
368
338
|
const sites = await discoverSites(rootDir)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
sites.push({ name: siteName, path: target })
|
|
339
|
+
if (!sites.find(s => s.path === relativePath)) {
|
|
340
|
+
sites.push({ name: siteName, path: relativePath })
|
|
372
341
|
}
|
|
373
342
|
await updateRootScripts(rootDir, sites, pm)
|
|
374
343
|
|
|
375
344
|
if (foundation) {
|
|
376
|
-
success(`Created site
|
|
345
|
+
success(`Created site ${colors.bright}${siteName}${colors.reset} at ${relativePath}/ → foundation '${foundation.name}'`)
|
|
377
346
|
} else {
|
|
378
|
-
success(`Created site
|
|
347
|
+
success(`Created site ${colors.bright}${siteName}${colors.reset} at ${relativePath}/`)
|
|
379
348
|
}
|
|
380
349
|
log('')
|
|
381
350
|
log(`Next: ${colors.cyan}${installCmd(pm)} && ${filterCmd(pm, siteName, 'dev')}${colors.reset}`)
|
|
382
351
|
if (!opts.from) {
|
|
383
352
|
log('')
|
|
384
|
-
log(`${colors.dim}To add your first page, create ${
|
|
353
|
+
log(`${colors.dim}To add your first page, create ${relativePath}/pages/home/page.yml and a .md file.${colors.reset}`)
|
|
385
354
|
log(`${colors.dim}Or use --from to start with template content: uniweb add site --from starter${colors.reset}`)
|
|
386
355
|
}
|
|
387
356
|
}
|
|
@@ -530,8 +499,11 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
530
499
|
process.exit(1)
|
|
531
500
|
}
|
|
532
501
|
|
|
533
|
-
// Compute package names
|
|
534
|
-
|
|
502
|
+
// Compute package names. Co-located projects use the `-src` / `-site`
|
|
503
|
+
// suffix convention so package names are unique within the workspace.
|
|
504
|
+
// The folder structure inside the project is `src/` + `site/`, mirroring
|
|
505
|
+
// the single-project default layout.
|
|
506
|
+
const foundationPkgName = `${name}-src`
|
|
535
507
|
const sitePkgName = `${name}-site`
|
|
536
508
|
|
|
537
509
|
// Check package name collisions
|
|
@@ -544,9 +516,9 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
544
516
|
|
|
545
517
|
const progressCb = (msg) => info(` ${msg}`)
|
|
546
518
|
|
|
547
|
-
// Scaffold foundation
|
|
519
|
+
// Scaffold foundation (folder: src/, package name: <project>-src)
|
|
548
520
|
info(`Creating foundation: ${foundationPkgName}...`)
|
|
549
|
-
await scaffoldFoundation(join(projectDir, '
|
|
521
|
+
await scaffoldFoundation(join(projectDir, 'src'), {
|
|
550
522
|
name: foundationPkgName,
|
|
551
523
|
projectName,
|
|
552
524
|
isExtension: false,
|
|
@@ -558,18 +530,18 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
558
530
|
name: sitePkgName,
|
|
559
531
|
projectName,
|
|
560
532
|
foundationName: foundationPkgName,
|
|
561
|
-
foundationPath: 'file:../
|
|
533
|
+
foundationPath: 'file:../src',
|
|
562
534
|
foundationRef: foundationPkgName,
|
|
563
535
|
}, { onProgress: progressCb })
|
|
564
536
|
|
|
565
537
|
// Apply template content if --from specified
|
|
566
538
|
if (opts.from) {
|
|
567
|
-
await applyFromTemplate(opts.from, 'foundation', join(projectDir, '
|
|
539
|
+
await applyFromTemplate(opts.from, 'foundation', join(projectDir, 'src'), projectName)
|
|
568
540
|
await applyFromTemplate(opts.from, 'site', join(projectDir, 'site'), projectName)
|
|
569
541
|
}
|
|
570
542
|
|
|
571
543
|
// Update workspace globs for co-located layout
|
|
572
|
-
await addWorkspaceGlob(rootDir, '*/
|
|
544
|
+
await addWorkspaceGlob(rootDir, '*/src')
|
|
573
545
|
await addWorkspaceGlob(rootDir, '*/site')
|
|
574
546
|
|
|
575
547
|
// Update root scripts
|
|
@@ -580,124 +552,96 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
580
552
|
await updateRootScripts(rootDir, sites, pm)
|
|
581
553
|
|
|
582
554
|
success(`Created project '${name}' at ${name}/`)
|
|
583
|
-
log(` ${colors.dim}Foundation: ${name}/
|
|
555
|
+
log(` ${colors.dim}Foundation: ${name}/src/ (${foundationPkgName})${colors.reset}`)
|
|
584
556
|
log(` ${colors.dim}Site: ${name}/site/ (${sitePkgName})${colors.reset}`)
|
|
585
557
|
log('')
|
|
586
558
|
log(`Next: ${colors.cyan}${installCmd(pm)} && ${filterCmd(pm, sitePkgName, 'dev')}${colors.reset}`)
|
|
587
559
|
}
|
|
588
560
|
|
|
589
561
|
/**
|
|
590
|
-
* Resolve
|
|
562
|
+
* Resolve where a foundation or site should be placed, given the user's input.
|
|
591
563
|
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
* -
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
return `${name}/foundation`
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// Respect existing segregated layout
|
|
619
|
-
if (hasFoundationsGlob) {
|
|
620
|
-
return `foundations/${name || 'foundation'}`
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// Named foundation → segregated layout (foundations/{name})
|
|
624
|
-
if (name) {
|
|
625
|
-
return `foundations/${name}`
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Unnamed → root-level 'foundation'
|
|
629
|
-
const dirName = 'foundation'
|
|
630
|
-
|
|
631
|
-
// Check if target already exists
|
|
632
|
-
if (!existsSync(join(rootDir, dirName))) {
|
|
633
|
-
return dirName
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// Already have one at the target path — error with guidance
|
|
637
|
-
if (isNonInteractive(process.argv)) {
|
|
638
|
-
error(`Directory '${dirName}' already exists.`)
|
|
639
|
-
log(`\nTo add another foundation, specify a name:`)
|
|
640
|
-
log(` ${getCliPrefix()} add foundation <name>`)
|
|
641
|
-
log(`\nOr use --path for explicit placement:`)
|
|
642
|
-
log(` ${getCliPrefix()} add foundation --path <dir>`)
|
|
643
|
-
process.exit(1)
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// Interactive: the existsSync check in addFoundation will catch it
|
|
647
|
-
return dirName
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* Resolve placement for a site
|
|
564
|
+
* The rule: **the user names a folder, and we create exactly that folder.**
|
|
565
|
+
* No silent nesting under `foundations/` / `sites/`, no inferring layout from
|
|
566
|
+
* pre-existing globs. The framework doesn't require any particular folder
|
|
567
|
+
* structure (the build classifies packages by their contents, not their
|
|
568
|
+
* location), so the CLI shouldn't impose one.
|
|
569
|
+
*
|
|
570
|
+
* Resolution priority (foundation example, same shape for site):
|
|
571
|
+
*
|
|
572
|
+
* 1. `--path <dir>` → explicit folder. Name is the path's
|
|
573
|
+
* last segment (used as the package
|
|
574
|
+
* name unless `name` was also given).
|
|
575
|
+
* 2. `name` contains `/` → treat as a path (e.g., `foundations/ui`).
|
|
576
|
+
* Folder = the path, package name =
|
|
577
|
+
* the last segment.
|
|
578
|
+
* 3. `name` (no slash) → folder = `<name>/`, package name = `<name>`.
|
|
579
|
+
* 4. `--project <project>` → folder = `<project>/<defaultSub>` and
|
|
580
|
+
* package name = `<project>-<defaultSub>`
|
|
581
|
+
* (the co-located convention; only this
|
|
582
|
+
* one uses the `-src` / `-site` suffix).
|
|
583
|
+
* 5. (no input) → folder = `<defaultDir>/`, package name
|
|
584
|
+
* = `<defaultPkg>` (`src/` + `src`
|
|
585
|
+
* for foundations; `site/` + `site` for
|
|
586
|
+
* sites).
|
|
652
587
|
*
|
|
653
|
-
*
|
|
588
|
+
* @param {string} rootDir
|
|
589
|
+
* @param {string|null} name - Either a bare name or a path-with-slash.
|
|
590
|
+
* @param {{ path?: string, project?: string }} opts
|
|
591
|
+
* @param {{ defaultDir: string, defaultPkg: string, projectSub: string }} kind
|
|
592
|
+
* @returns {{ relativePath: string, packageName: string }}
|
|
654
593
|
*/
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
594
|
+
function resolvePlacement(rootDir, name, opts, kind) {
|
|
595
|
+
// 1. --path is a PARENT directory. The folder is `<path>/<name>` if a
|
|
596
|
+
// name was given, or `<path>` itself if not (the path's last segment
|
|
597
|
+
// is then taken as the package name).
|
|
598
|
+
if (opts.path) {
|
|
599
|
+
const parent = opts.path.replace(/\/+$/, '')
|
|
600
|
+
if (name) {
|
|
601
|
+
const last = name.split('/').filter(Boolean).pop()
|
|
602
|
+
return {
|
|
603
|
+
relativePath: `${parent}/${name}`.replace(/\/+/g, '/'),
|
|
604
|
+
packageName: last,
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const lastSegment = parent.split('/').filter(Boolean).pop() || parent
|
|
608
|
+
return {
|
|
609
|
+
relativePath: parent,
|
|
610
|
+
packageName: lastSegment,
|
|
611
|
+
}
|
|
669
612
|
}
|
|
670
613
|
|
|
671
|
-
//
|
|
672
|
-
if (
|
|
673
|
-
|
|
614
|
+
// 2. name contains a slash → treat as a path.
|
|
615
|
+
if (name && name.includes('/')) {
|
|
616
|
+
const relativePath = name.replace(/\/+$/, '')
|
|
617
|
+
const lastSegment = relativePath.split('/').filter(Boolean).pop()
|
|
618
|
+
return {
|
|
619
|
+
relativePath,
|
|
620
|
+
packageName: lastSegment,
|
|
621
|
+
}
|
|
674
622
|
}
|
|
675
623
|
|
|
676
|
-
//
|
|
624
|
+
// 3. Bare name.
|
|
677
625
|
if (name) {
|
|
678
|
-
return
|
|
626
|
+
return {
|
|
627
|
+
relativePath: name,
|
|
628
|
+
packageName: name,
|
|
629
|
+
}
|
|
679
630
|
}
|
|
680
631
|
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
632
|
+
// 4. --project (co-located convention with -src / -site suffix).
|
|
633
|
+
if (opts.project) {
|
|
634
|
+
return {
|
|
635
|
+
relativePath: `${opts.project}/${kind.projectSub}`,
|
|
636
|
+
packageName: `${opts.project}-${kind.projectSub}`,
|
|
637
|
+
}
|
|
687
638
|
}
|
|
688
639
|
|
|
689
|
-
//
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
log(` ${getCliPrefix()} add site <name>`)
|
|
694
|
-
log(`\nOr use --path for explicit placement:`)
|
|
695
|
-
log(` ${getCliPrefix()} add site --path <dir>`)
|
|
696
|
-
process.exit(1)
|
|
640
|
+
// 5. Default placement.
|
|
641
|
+
return {
|
|
642
|
+
relativePath: kind.defaultDir,
|
|
643
|
+
packageName: kind.defaultPkg,
|
|
697
644
|
}
|
|
698
|
-
|
|
699
|
-
// Interactive: the existsSync check in addSite will catch it
|
|
700
|
-
return dirName
|
|
701
645
|
}
|
|
702
646
|
|
|
703
647
|
/**
|
|
@@ -766,35 +710,6 @@ function computeFoundationPath(sitePath, foundationPath) {
|
|
|
766
710
|
return `file:${rel}`
|
|
767
711
|
}
|
|
768
712
|
|
|
769
|
-
/**
|
|
770
|
-
* Compute the appropriate glob pattern for a target directory
|
|
771
|
-
*/
|
|
772
|
-
function computeGlob(target, type) {
|
|
773
|
-
// e.g., "foundation" → "foundation"
|
|
774
|
-
// e.g., "foundations/marketing" → "foundations/*"
|
|
775
|
-
// e.g., "docs/foundation" → "*/foundation"
|
|
776
|
-
// e.g., "lib/mktg" → "lib/mktg"
|
|
777
|
-
|
|
778
|
-
const parts = target.split('/')
|
|
779
|
-
|
|
780
|
-
if (parts.length === 1) {
|
|
781
|
-
// Direct: "foundation", "site"
|
|
782
|
-
return target
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (parts.length === 2) {
|
|
786
|
-
// Could be "foundations/marketing" or "docs/foundation"
|
|
787
|
-
if (parts[1] === type) {
|
|
788
|
-
// Co-located: "docs/foundation" → "*/foundation"
|
|
789
|
-
return `*/${type}`
|
|
790
|
-
}
|
|
791
|
-
// Plural container: "foundations/marketing" → "foundations/*"
|
|
792
|
-
return `${parts[0]}/*`
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// Custom path — return as-is
|
|
796
|
-
return target
|
|
797
|
-
}
|
|
798
713
|
|
|
799
714
|
/**
|
|
800
715
|
* Apply content from a template to a scaffolded package
|
|
@@ -965,12 +880,16 @@ async function addSection(rootDir, opts) {
|
|
|
965
880
|
foundation = response.foundation
|
|
966
881
|
}
|
|
967
882
|
|
|
968
|
-
// Resolve sections directory
|
|
969
|
-
|
|
883
|
+
// Resolve sections directory — source root comes from package.json::main
|
|
884
|
+
// (works for both nested `src/` layouts and flat layouts).
|
|
885
|
+
const foundationDir = join(rootDir, foundation.path)
|
|
886
|
+
const foundationSrc = resolveFoundationSrcPath(foundationDir)
|
|
887
|
+
const sectionsDir = join(foundationSrc, 'sections')
|
|
970
888
|
const sectionDir = join(sectionsDir, name)
|
|
889
|
+
const relSectionPath = relative(foundationDir, sectionDir)
|
|
971
890
|
|
|
972
891
|
if (existsSync(sectionDir)) {
|
|
973
|
-
error(`Section '${name}' already exists at ${foundation.path}
|
|
892
|
+
error(`Section '${name}' already exists at ${foundation.path}/${relSectionPath}/`)
|
|
974
893
|
process.exit(1)
|
|
975
894
|
}
|
|
976
895
|
|
|
@@ -1015,7 +934,7 @@ export default function ${name}({ content, params }) {
|
|
|
1015
934
|
await writeFile(join(sectionDir, 'index.jsx'), componentContent)
|
|
1016
935
|
await writeFile(join(sectionDir, 'meta.js'), metaContent)
|
|
1017
936
|
|
|
1018
|
-
success(`Created section '${name}' at ${foundation.path}
|
|
937
|
+
success(`Created section '${name}' at ${foundation.path}/${relSectionPath}/`)
|
|
1019
938
|
log(` ${colors.dim}index.jsx${colors.reset} — component (customize the JSX)`)
|
|
1020
939
|
log(` ${colors.dim}meta.js${colors.reset} — metadata (add content expectations, params, presets)`)
|
|
1021
940
|
if (foundations.length === 1) {
|
package/src/commands/build.js
CHANGED
|
@@ -20,6 +20,9 @@ import { createRequire } from 'node:module'
|
|
|
20
20
|
import {
|
|
21
21
|
generateEntryPoint,
|
|
22
22
|
discoverComponents,
|
|
23
|
+
resolveFoundationSrcPath,
|
|
24
|
+
classifyPackage,
|
|
25
|
+
isExtensionPackage,
|
|
23
26
|
} from '@uniweb/build'
|
|
24
27
|
import { readSiteConfig } from '@uniweb/build/site'
|
|
25
28
|
import { readWorkspaceConfig, resolveGlob } from '../utils/config.js'
|
|
@@ -62,30 +65,15 @@ function info(message) {
|
|
|
62
65
|
* 5. pnpm-workspace.yaml or package.json workspaces → workspace
|
|
63
66
|
*/
|
|
64
67
|
function detectProjectType(projectDir) {
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (existsSync(join(projectDir, 'site.yml'))) {
|
|
71
|
-
return 'site'
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Fallback detection: directory structure
|
|
75
|
-
if (existsSync(join(projectDir, 'src', 'components'))) {
|
|
76
|
-
return 'foundation'
|
|
77
|
-
}
|
|
68
|
+
// Foundation vs site: delegate to the canonical classifier in @uniweb/build.
|
|
69
|
+
const kind = classifyPackage(projectDir)
|
|
70
|
+
if (kind) return kind
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
return 'site'
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Workspace: has pnpm-workspace.yaml or package.json with workspaces
|
|
72
|
+
// Workspace: has pnpm-workspace.yaml or package.json with workspaces field.
|
|
84
73
|
if (existsSync(join(projectDir, 'pnpm-workspace.yaml'))) {
|
|
85
74
|
return 'workspace'
|
|
86
75
|
}
|
|
87
76
|
|
|
88
|
-
// Check package.json for workspaces field
|
|
89
77
|
const packageJsonPath = join(projectDir, 'package.json')
|
|
90
78
|
if (existsSync(packageJsonPath)) {
|
|
91
79
|
try {
|
|
@@ -168,7 +156,7 @@ async function runLocalVite(projectDir, args) {
|
|
|
168
156
|
* Build a foundation
|
|
169
157
|
*/
|
|
170
158
|
async function buildFoundation(projectDir, options = {}) {
|
|
171
|
-
const srcDir =
|
|
159
|
+
const srcDir = resolveFoundationSrcPath(projectDir)
|
|
172
160
|
|
|
173
161
|
info('Building foundation...')
|
|
174
162
|
|
|
@@ -180,7 +168,7 @@ async function buildFoundation(projectDir, options = {}) {
|
|
|
180
168
|
|
|
181
169
|
if (componentNames.length === 0) {
|
|
182
170
|
error('No components found with meta.js files')
|
|
183
|
-
error(
|
|
171
|
+
error(`Make sure components are in ${srcDir}/components/[Name]/ with a meta.js file`)
|
|
184
172
|
process.exit(1)
|
|
185
173
|
}
|
|
186
174
|
|
|
@@ -504,38 +492,22 @@ async function buildSite(projectDir, options = {}) {
|
|
|
504
492
|
* Check if a directory is a foundation
|
|
505
493
|
*/
|
|
506
494
|
function isFoundation(dir) {
|
|
507
|
-
|
|
508
|
-
if (existsSync(join(dir, 'src', 'foundation.js'))) return true
|
|
509
|
-
// Fallback: has src/sections/
|
|
510
|
-
if (existsSync(join(dir, 'src', 'sections'))) return true
|
|
511
|
-
// Legacy fallback: has src/components/
|
|
512
|
-
if (existsSync(join(dir, 'src', 'components'))) return true
|
|
513
|
-
return false
|
|
495
|
+
return classifyPackage(dir) === 'foundation'
|
|
514
496
|
}
|
|
515
497
|
|
|
516
498
|
/**
|
|
517
|
-
* Check if a foundation directory declares extension: true in
|
|
499
|
+
* Check if a foundation directory declares extension: true in its
|
|
500
|
+
* authored declarations file (main.js or legacy foundation.js).
|
|
518
501
|
*/
|
|
519
502
|
function isExtensionDir(dir) {
|
|
520
|
-
|
|
521
|
-
if (!existsSync(filePath)) return false
|
|
522
|
-
try {
|
|
523
|
-
const content = readFileSync(filePath, 'utf8')
|
|
524
|
-
return /extension\s*:\s*true/.test(content)
|
|
525
|
-
} catch {
|
|
526
|
-
return false
|
|
527
|
-
}
|
|
503
|
+
return isExtensionPackage(dir)
|
|
528
504
|
}
|
|
529
505
|
|
|
530
506
|
/**
|
|
531
507
|
* Check if a directory is a site
|
|
532
508
|
*/
|
|
533
509
|
function isSite(dir) {
|
|
534
|
-
|
|
535
|
-
if (existsSync(join(dir, 'site.yml'))) return true
|
|
536
|
-
// Fallback: has pages/
|
|
537
|
-
if (existsSync(join(dir, 'pages'))) return true
|
|
538
|
-
return false
|
|
510
|
+
return classifyPackage(dir) === 'site'
|
|
539
511
|
}
|
|
540
512
|
|
|
541
513
|
/**
|