uniweb 0.11.0 → 0.12.1

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.
@@ -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
- let name = opts.name
190
+ const name = opts.name
190
191
  const existingNames = await getExistingPackageNames(rootDir)
191
192
 
192
- // Reject reserved names (format + reserved check only collisions handled at package name level)
193
- if (name) {
194
- const valid = validatePackageName(name)
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
- // Interactive name prompt when name not provided and no --path
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(`Directory already exists: ${target}`)
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
- // Package name = name or 'foundation'
236
- const packageName = name || 'foundation'
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(`Package name '${packageName}' already exists in this workspace.`)
239
- log(`Choose a different name: ${getCliPrefix()} add foundation <name>`)
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
- // Update workspace globs
258
- const glob = computeGlob(target, 'foundation')
259
- await addWorkspaceGlob(rootDir, glob)
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 '${packageName}' at ${target}/`)
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
- let name = opts.name
264
+ const name = opts.name
275
265
  const existingNames = await getExistingPackageNames(rootDir)
276
266
 
277
- // Reject reserved names (format + reserved check only collisions handled at package name level)
278
- if (name) {
279
- const valid = validatePackageName(name)
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
- // Interactive name prompt when name not provided and no --path
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(`Directory already exists: ${target}`)
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
- // Resolve foundation
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(`Package name '${siteName}' already exists in this workspace.`)
327
- log(`Choose a different name: ${getCliPrefix()} add site <name>`)
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(target, foundation.path)
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
- // Update workspace globs
364
- const glob = computeGlob(target, 'site')
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 glob is added — includes the new one)
337
+ // Update root scripts (discover sites after registration — includes the new one)
368
338
  const sites = await discoverSites(rootDir)
369
- // If the new site wasn't discovered (glob may not match yet), add it
370
- if (!sites.find(s => s.path === target)) {
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 '${siteName}' at ${target}/ → foundation '${foundation.name}'`)
345
+ success(`Created site ${colors.bright}${siteName}${colors.reset} at ${relativePath}/ → foundation '${foundation.name}'`)
377
346
  } else {
378
- success(`Created site '${siteName}' at ${target}/`)
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 ${target}/pages/home/page.yml and a .md file.${colors.reset}`)
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
- const foundationPkgName = `${name}-foundation`
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, 'foundation'), {
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:../foundation',
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, 'foundation'), projectName)
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, '*/foundation')
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}/foundation/ (${foundationPkgName})${colors.reset}`)
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 placement for a foundation
562
+ * Resolve where a foundation or site should be placed, given the user's input.
591
563
  *
592
- * Rules:
593
- * - --path: use it directly
594
- * - --project: {project}/foundation (co-located)
595
- * - Existing co-located glob: follow pattern
596
- * - Existing segregated glob: follow pattern
597
- * - Named (e.g., "marketing"): segregated at foundations/{name}
598
- * - Unnamed: root-level 'foundation'
599
- * - Already have one: error in non-interactive, ask in interactive
600
- */
601
- async function resolveFoundationTarget(rootDir, name, opts) {
602
- if (opts.path) return opts.path
603
-
604
- if (opts.project) {
605
- return `${opts.project}/foundation`
606
- }
607
-
608
- // Check existing layout
609
- const { packages } = await readWorkspaceConfig(rootDir)
610
- const hasColocated = packages.some(p => p.includes('*/foundation'))
611
- const hasFoundationsGlob = packages.some(p => p.startsWith('foundations/'))
612
-
613
- // Respect existing co-located layout
614
- if (hasColocated && name) {
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.
652
569
  *
653
- * Same rules as resolveFoundationTarget, adapted for sites.
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).
587
+ *
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
- async function resolveSiteTarget(rootDir, name, opts) {
656
- if (opts.path) return opts.path
657
-
658
- if (opts.project) {
659
- return `${opts.project}/site`
660
- }
661
-
662
- const { packages } = await readWorkspaceConfig(rootDir)
663
- const hasColocated = packages.some(p => p.includes('*/site'))
664
- const hasSitesGlob = packages.some(p => p.startsWith('sites/'))
665
-
666
- // Respect existing co-located layout
667
- if (hasColocated && name) {
668
- return `${name}/site`
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
- // Respect existing segregated layout
672
- if (hasSitesGlob) {
673
- return `sites/${name || 'site'}`
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
- // Named site → segregated layout (sites/{name})
624
+ // 3. Bare name.
677
625
  if (name) {
678
- return `sites/${name}`
626
+ return {
627
+ relativePath: name,
628
+ packageName: name,
629
+ }
679
630
  }
680
631
 
681
- // Unnamed root-level 'site'
682
- const dirName = 'site'
683
-
684
- // Check if target already exists
685
- if (!existsSync(join(rootDir, dirName))) {
686
- return dirName
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
- // Already have one at the target path — error with guidance
690
- if (isNonInteractive(process.argv)) {
691
- error(`Directory '${dirName}' already exists.`)
692
- log(`\nTo add another site, specify a name:`)
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
@@ -815,12 +730,9 @@ async function applyFromTemplate(templateId, packageType, targetDir, projectName
815
730
  const metadata = await validateTemplate(resolved.path, {})
816
731
 
817
732
  // Look in contentDirs for matching package type
818
- let contentDir = null
819
733
  const match = metadata.contentDirs.find(d => d.type === packageType) ||
820
734
  metadata.contentDirs.find(d => d.name === packageType)
821
- if (match) {
822
- contentDir = match.dir
823
- }
735
+ const contentDir = match ? match.dir : null
824
736
 
825
737
  if (contentDir) {
826
738
  info(`Applying ${metadata.name} content...`)
@@ -829,6 +741,7 @@ async function applyFromTemplate(templateId, packageType, targetDir, projectName
829
741
  versions: getVersionsForTemplates(),
830
742
  }, {
831
743
  onProgress: (msg) => info(` ${msg}`),
744
+ renames: match.renames,
832
745
  })
833
746
 
834
747
  // Merge template dependencies
@@ -965,12 +878,16 @@ async function addSection(rootDir, opts) {
965
878
  foundation = response.foundation
966
879
  }
967
880
 
968
- // Resolve sections directory
969
- const sectionsDir = join(rootDir, foundation.path, 'src', 'sections')
881
+ // Resolve sections directory — source root comes from package.json::main
882
+ // (works for both nested `src/` layouts and flat layouts).
883
+ const foundationDir = join(rootDir, foundation.path)
884
+ const foundationSrc = resolveFoundationSrcPath(foundationDir)
885
+ const sectionsDir = join(foundationSrc, 'sections')
970
886
  const sectionDir = join(sectionsDir, name)
887
+ const relSectionPath = relative(foundationDir, sectionDir)
971
888
 
972
889
  if (existsSync(sectionDir)) {
973
- error(`Section '${name}' already exists at ${foundation.path}/src/sections/${name}/`)
890
+ error(`Section '${name}' already exists at ${foundation.path}/${relSectionPath}/`)
974
891
  process.exit(1)
975
892
  }
976
893
 
@@ -1015,7 +932,7 @@ export default function ${name}({ content, params }) {
1015
932
  await writeFile(join(sectionDir, 'index.jsx'), componentContent)
1016
933
  await writeFile(join(sectionDir, 'meta.js'), metaContent)
1017
934
 
1018
- success(`Created section '${name}' at ${foundation.path}/src/sections/${name}/`)
935
+ success(`Created section '${name}' at ${foundation.path}/${relSectionPath}/`)
1019
936
  log(` ${colors.dim}index.jsx${colors.reset} — component (customize the JSX)`)
1020
937
  log(` ${colors.dim}meta.js${colors.reset} — metadata (add content expectations, params, presets)`)
1021
938
  if (foundations.length === 1) {