uniweb 0.12.16 → 0.12.18

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 CHANGED
@@ -40,12 +40,18 @@ Edit files in `site/pages/` and `src/sections/` to see changes instantly.
40
40
 
41
41
  Use `--blank` for an empty workspace (no packages) — grow with `uniweb add`.
42
42
 
43
- Or skip the interactive prompt:
43
+ **Two starting points.** Either let the CLI create a new directory:
44
44
 
45
45
  ```bash
46
46
  pnpm create uniweb my-site --template docs
47
47
  ```
48
48
 
49
+ …or scaffold inside an existing directory (e.g., a freshly-cloned GitHub repo):
50
+
51
+ ```bash
52
+ pnpm create uniweb . --template docs
53
+ ```
54
+
49
55
  ### Local Scripts
50
56
 
51
57
  Run these from the **project root**:
@@ -243,7 +249,7 @@ The parser extracts semantic elements from markdown—`title` from the first hea
243
249
 
244
250
  ## Foundations Are Portable
245
251
 
246
- The `src/` folder (your project's foundation) ships with your project as a convenience, but a foundation is a dynamically linked module (DML) with no dependency on any specific site. Sites reference foundations by configuration, not by folder proximity.
252
+ The `src/` folder (your project's foundation) ships with your project as a convenience, but a foundation is dynamically loaded sites reference it by configuration, not by folder proximity.
247
253
 
248
254
  **Two ways to use a foundation:**
249
255
 
@@ -308,8 +314,7 @@ You (or your dev team) write the markdown. Deploy site + foundation together.
308
314
  The shortest path to a live site is free, on GitHub Pages, with a custom domain:
309
315
 
310
316
  ```bash
311
- npm create uniweb my-site
312
- cd my-site
317
+ uniweb create . # from within a freshly-cloned GitHub repo
313
318
  uniweb add ci --host=github-pages
314
319
  # Commit, push to GitHub, enable Pages in repo settings → live site
315
320
  ```
@@ -374,7 +379,7 @@ Full documentation is available at **[github.com/uniweb/docs](https://github.com
374
379
  | Content Structure | [How markdown becomes component props](https://github.com/uniweb/docs/blob/main/reference/content-structure.md) |
375
380
  | Component Metadata | [The meta.js schema](https://github.com/uniweb/docs/blob/main/reference/component-metadata.md) |
376
381
  | Site Configuration | [site.yml reference](https://github.com/uniweb/docs/blob/main/reference/site-configuration.md) |
377
- | CLI Commands | [create, add, build, docs, doctor, i18n](https://github.com/uniweb/docs/blob/main/reference/cli-commands.md) |
382
+ | CLI Commands | [All CLI commands and flags](https://github.com/uniweb/docs/blob/main/reference/cli-commands.md) |
378
383
  | Templates | [Built-in, official, and external templates](https://github.com/uniweb/docs/blob/main/getting-started/templates.md) |
379
384
  | Deployment | [Two artifacts, two verbs — bundled, linked, and per-host recipes](https://github.com/uniweb/docs/blob/main/development/deploying.md) |
380
385
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.12.16",
3
+ "version": "0.12.18",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,7 +46,7 @@
46
46
  "@uniweb/core": "0.7.11"
47
47
  },
48
48
  "peerDependencies": {
49
- "@uniweb/build": "0.14.3",
49
+ "@uniweb/build": "0.14.4",
50
50
  "@uniweb/content-reader": "1.1.10",
51
51
  "@uniweb/semantic-parser": "1.1.17"
52
52
  },
@@ -60,5 +60,8 @@
60
60
  "@uniweb/semantic-parser": {
61
61
  "optional": true
62
62
  }
63
+ },
64
+ "scripts": {
65
+ "test": "node --test 'test/**/*.test.js'"
63
66
  }
64
67
  }
@@ -21,10 +21,9 @@ import { scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemp
21
21
  import {
22
22
  readWorkspaceConfig,
23
23
  addWorkspaceGlob,
24
- discoverFoundations,
25
- discoverSites,
26
24
  updateRootScripts,
27
25
  } from '../utils/config.js'
26
+ import { discoverFoundations, discoverSites } from '../utils/discover.js'
28
27
  import { validatePackageName, getExistingPackageNames, resolveUniqueName } from '../utils/names.js'
29
28
  import { findWorkspaceRoot } from '../utils/workspace.js'
30
29
  import { detectPackageManager, filterCmd, installCmd } from '../utils/pm.js'
@@ -36,7 +36,8 @@ import { spawn } from 'node:child_process'
36
36
  import { join } from 'node:path'
37
37
 
38
38
  import { detectPackageManager, filterCmd } from '../utils/pm.js'
39
- import { discoverSites, readWorkspaceConfig } from '../utils/config.js'
39
+ import { readWorkspaceConfig } from '../utils/config.js'
40
+ import { discoverSites } from '../utils/discover.js'
40
41
  import { findWorkspaceRoot } from '../utils/workspace.js'
41
42
  import { readFlagValue } from '../utils/args.js'
42
43
 
@@ -10,7 +10,7 @@ import { loadDeployYml } from '@uniweb/build/site'
10
10
  import { listAdapters } from '@uniweb/build/hosts'
11
11
  import { getCliVersion } from '../versions.js'
12
12
  import { readAgentsVersion } from '../utils/agents-stamp.js'
13
- import { discoverFoundations, discoverSites } from '../utils/config.js'
13
+ import { discoverFoundations, discoverSites } from '../utils/discover.js'
14
14
  import { findWorkspaceRoot } from '../utils/workspace.js'
15
15
 
16
16
  /**
@@ -47,14 +47,13 @@ import yaml from 'js-yaml'
47
47
  import { isExtensionPackage } from '@uniweb/build'
48
48
  import { findWorkspaceRoot } from '../utils/workspace.js'
49
49
  import {
50
- discoverFoundations,
51
- discoverSites,
52
50
  readWorkspaceConfig,
53
51
  writeWorkspaceConfig,
54
52
  readRootPackageJson,
55
53
  writeRootPackageJson,
56
54
  updateRootScripts,
57
55
  } from '../utils/config.js'
56
+ import { discoverFoundations, discoverSites } from '../utils/discover.js'
58
57
  import { getExistingPackageNames, validatePackageName } from '../utils/names.js'
59
58
  import { detectPackageManager, installCmd } from '../utils/pm.js'
60
59
  import { getCliPrefix } from '../utils/interactive.js'
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-05-06T17:01:18.376Z",
3
+ "generatedAt": "2026-05-06T22:01:04.763Z",
4
4
  "packages": {
5
5
  "@uniweb/build": {
6
- "version": "0.14.3",
6
+ "version": "0.14.4",
7
7
  "path": "framework/build",
8
8
  "deps": [
9
9
  "@uniweb/content-reader",
package/src/index.js CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  import { existsSync, readFileSync } from 'node:fs'
25
25
  import { execSync, spawn as spawnChild } from 'node:child_process'
26
- import { resolve, join, relative, dirname } from 'node:path'
26
+ import { resolve, join, relative, dirname, basename } from 'node:path'
27
27
  import { fileURLToPath } from 'node:url'
28
28
  import prompts from 'prompts'
29
29
  // `doctor`, `add`, `publish`, and `deploy` are loaded lazily via
@@ -43,7 +43,7 @@ import {
43
43
  parseTemplateId,
44
44
  } from './templates/index.js'
45
45
  import { validateTemplate } from './templates/validator.js'
46
- import { scaffoldWorkspace, scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies } from './utils/scaffold.js'
46
+ import { scaffoldWorkspace, scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies, getWorkspaceTemplateOutputs } from './utils/scaffold.js'
47
47
  import { detectPackageManager, filterCmd, installCmd, runCmd } from './utils/pm.js'
48
48
  import { isNonInteractive, getCliPrefix, stripNonInteractiveFlag, formatOptions } from './utils/interactive.js'
49
49
  import { findWorkspaceRoot } from './utils/workspace.js'
@@ -73,6 +73,30 @@ const TEMPLATE_CHOICES = [
73
73
  { title: 'Blank workspace', value: 'blank', description: 'Empty workspace — grow with uniweb add' },
74
74
  ]
75
75
 
76
+ // Files that may pre-exist in the target dir during `uniweb create .` and
77
+ // will be silently overwritten by the scaffold. Anything else colliding
78
+ // causes the verb to abort. README and .gitignore are the only two files
79
+ // the workspace template writes that overlap with what `gh repo create`
80
+ // puts in a fresh repo, and the scaffold's versions are more useful in
81
+ // this context (Vite/Node-aware .gitignore, project-shaped README).
82
+ const IN_PLACE_OVERWRITE_ALLOWED = new Set(['README.md', '.gitignore'])
83
+
84
+ /**
85
+ * Slugify a directory name into a valid project slug — lowercase,
86
+ * `[a-z0-9-]+`, no leading/trailing/duplicated hyphens. Matches the
87
+ * validation regex used for the interactive name prompt.
88
+ *
89
+ * @param {string} name
90
+ * @returns {string} Slugified name; empty if no valid characters remain.
91
+ */
92
+ function slugifyName(name) {
93
+ return String(name)
94
+ .toLowerCase()
95
+ .replace(/[^a-z0-9-]+/g, '-')
96
+ .replace(/^-+|-+$/g, '')
97
+ .replace(/-{2,}/g, '-')
98
+ }
99
+
76
100
  function log(message) {
77
101
  console.log(message)
78
102
  }
@@ -669,6 +693,18 @@ async function main() {
669
693
  let projectName = args[1]
670
694
  let templateType = null // null = use new package template flow
671
695
 
696
+ // In-place mode: `uniweb create .` scaffolds into the current working
697
+ // directory instead of creating a new one. Pairs with the GitHub-first
698
+ // workflow where the user already cloned an empty repo (README.md and
699
+ // optionally .gitignore present) and wants to scaffold inside it.
700
+ const inPlace = projectName === '.'
701
+ if (inPlace) {
702
+ // Clear the positional so downstream logic (template prompt, name
703
+ // prompt, etc.) doesn't see `.` as a literal name. The actual project
704
+ // name is derived below from the cwd basename or `--name`.
705
+ projectName = null
706
+ }
707
+
672
708
  // Check for --template flag
673
709
  const templateIndex = args.indexOf('--template')
674
710
  if (templateIndex !== -1 && args[templateIndex + 1]) {
@@ -682,11 +718,14 @@ async function main() {
682
718
  }
683
719
  }
684
720
 
685
- // Check for --name flag (used for project display name)
721
+ // Check for --name flag. Accepts both `--name foo` and `--name=foo`.
686
722
  let displayName = null
687
723
  const nameIndex = args.indexOf('--name')
688
724
  if (nameIndex !== -1 && args[nameIndex + 1]) {
689
725
  displayName = args[nameIndex + 1]
726
+ } else {
727
+ const nameEq = args.find(a => a.startsWith('--name='))
728
+ if (nameEq) displayName = nameEq.slice('--name='.length)
690
729
  }
691
730
 
692
731
  // Check for --blank flag
@@ -708,6 +747,28 @@ async function main() {
708
747
 
709
748
  const prefix = getCliPrefix()
710
749
 
750
+ // In-place: derive the project name from the cwd basename (slugified),
751
+ // or use --name when provided. Skip the interactive name prompt below.
752
+ if (inPlace) {
753
+ if (displayName) {
754
+ projectName = displayName
755
+ } else {
756
+ const dirName = basename(process.cwd())
757
+ const slug = slugifyName(dirName)
758
+ if (!slug) {
759
+ error(`Could not derive a valid project name from the current directory ("${dirName}").`)
760
+ log(`Re-run with ${colors.cyan}--name=<your-name>${colors.reset}.`)
761
+ process.exit(1)
762
+ }
763
+ projectName = slug
764
+ if (slug !== dirName) {
765
+ log(`${colors.dim}Project name:${colors.reset} ${slug} ${colors.dim}(slugified from "${dirName}")${colors.reset}`)
766
+ } else {
767
+ log(`${colors.dim}Project name:${colors.reset} ${slug}`)
768
+ }
769
+ }
770
+ }
771
+
711
772
  // Non-interactive: fail with actionable message instead of prompting
712
773
  if (nonInteractive && !projectName) {
713
774
  error(`Missing project name.\n`)
@@ -720,7 +781,7 @@ async function main() {
720
781
  templateType = 'starter'
721
782
  }
722
783
 
723
- // Interactive prompts
784
+ // Interactive prompts (skipped in in-place mode — name was derived above)
724
785
  const response = await prompts([
725
786
  {
726
787
  type: projectName ? null : 'text',
@@ -773,14 +834,40 @@ async function main() {
773
834
 
774
835
  const effectiveName = displayName || projectName
775
836
 
776
- // Create project directory
777
- const projectDir = resolve(process.cwd(), projectName)
837
+ // Resolve target directory. In-place mode scaffolds into the cwd;
838
+ // otherwise create `./<projectName>`.
839
+ const projectDir = inPlace ? process.cwd() : resolve(process.cwd(), projectName)
778
840
 
779
- if (existsSync(projectDir)) {
841
+ if (!inPlace && existsSync(projectDir)) {
780
842
  error(`Directory already exists: ${projectName}`)
781
843
  process.exit(1)
782
844
  }
783
845
 
846
+ if (inPlace) {
847
+ // Conflict check: enumerate the workspace template's would-write paths
848
+ // (the only stage that touches the project root) and bail if any
849
+ // collide with existing files outside the small allowlist.
850
+ //
851
+ // Allowlist: README.md and .gitignore overwrite cleanly. README is
852
+ // typically GitHub-generated boilerplate; .gitignore should be ours
853
+ // since the scaffold ships sensible Vite/Node ignores.
854
+ const outputs = await getWorkspaceTemplateOutputs({ blank: isBlank })
855
+ const conflicts = []
856
+ for (const rel of outputs) {
857
+ const full = resolve(projectDir, rel)
858
+ if (existsSync(full) && !IN_PLACE_OVERWRITE_ALLOWED.has(rel)) {
859
+ conflicts.push(rel)
860
+ }
861
+ }
862
+ if (conflicts.length > 0) {
863
+ error(`Cannot scaffold in place — these files would be overwritten:`)
864
+ for (const c of conflicts) log(` ${colors.yellow}${c}${colors.reset}`)
865
+ log('')
866
+ log(`Move or remove them, then re-run ${colors.cyan}uniweb create .${colors.reset}.`)
867
+ process.exit(1)
868
+ }
869
+ }
870
+
784
871
  // Template routing logic
785
872
  const progressCb = (msg) => log(` ${colors.dim}${msg}${colors.reset}`)
786
873
  const warningCb = (msg) => log(` ${colors.yellow}Warning: ${msg}${colors.reset}`)
@@ -879,13 +966,13 @@ async function main() {
879
966
 
880
967
  if (isBlank) {
881
968
  log(`Next steps:\n`)
882
- log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
969
+ if (!inPlace) log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
883
970
  log(` ${colors.cyan}${prefix} add project${colors.reset}`)
884
971
  log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
885
972
  log(` ${colors.cyan}${prefix} dev${colors.reset} ${colors.dim}# Start dev server${colors.reset}`)
886
973
  } else {
887
974
  log(`Next steps:\n`)
888
- log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
975
+ if (!inPlace) log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
889
976
  log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
890
977
  log(` ${colors.cyan}${prefix} dev${colors.reset} ${colors.dim}# Start dev server${colors.reset}`)
891
978
  }
@@ -979,6 +1066,7 @@ ${colors.cyan}${colors.bright}uniweb create${colors.reset} ${colors.dim}— Crea
979
1066
 
980
1067
  ${colors.bright}Usage:${colors.reset}
981
1068
  uniweb create [name] [options]
1069
+ uniweb create . Scaffold into the current directory
982
1070
 
983
1071
  ${colors.bright}Options:${colors.reset}
984
1072
  --template <type> Project template (default: starter)
@@ -987,13 +1075,24 @@ ${colors.bright}Options:${colors.reset}
987
1075
  npm: @scope/template-name
988
1076
  GitHub: github:user/repo or https://github.com/user/repo
989
1077
  --blank Create an empty workspace (grow with \`uniweb add\`)
990
- --name <name> Project display name
1078
+ --name <name> Project name (overrides slugified basename when used with \`.\`)
991
1079
  --no-git Skip git repository initialization
992
1080
 
1081
+ ${colors.bright}In-place mode (\`uniweb create .\`):${colors.reset}
1082
+ Pairs with the GitHub-first workflow — clone an empty repo locally
1083
+ (README, optional .gitignore), then scaffold inside it. Project name
1084
+ is the cwd basename, slugified to a valid npm name. Pass \`--name\` to
1085
+ override. Pre-existing \`README.md\` and \`.gitignore\` are overwritten;
1086
+ any other collision aborts with the list of conflicting files. Skips
1087
+ \`git init\` when a \`.git/\` directory already exists.
1088
+
993
1089
  ${colors.bright}Examples:${colors.reset}
994
1090
  uniweb create my-project # Foundation + site + starter content
995
1091
  uniweb create my-project --template marketing # Official template
996
1092
  uniweb create my-project --blank # Empty workspace
1093
+ uniweb create . # Scaffold into current dir
1094
+ uniweb create . --template docs # In place + a content template
1095
+ uniweb create . --name=my-app # In place, explicit slug
997
1096
  `,
998
1097
  dev: `
999
1098
  ${colors.cyan}${colors.bright}uniweb dev${colors.reset} ${colors.dim}— Start a dev server for a site${colors.reset}
@@ -224,6 +224,53 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
224
224
  }
225
225
  }
226
226
 
227
+ /**
228
+ * Enumerate the output paths a template directory would write, without
229
+ * touching disk. Mirrors `copyTemplateDirectory`'s naming rules:
230
+ * - `.hbs` extension is stripped
231
+ * - `_dir` is renamed to `.dir` (but `__dir` is preserved as `_dir` would be)
232
+ * - `template.json` is excluded
233
+ * - `skip` filenames are excluded by their post-rename name
234
+ *
235
+ * Returns relative paths (POSIX-style separators on POSIX, native on Windows
236
+ * — `path.join` semantics). Used by the in-place create flow to detect
237
+ * conflicts before any I/O begins.
238
+ *
239
+ * @param {string} sourcePath - Source template directory
240
+ * @param {Object} [options]
241
+ * @param {string[]} [options.skip] - Output filenames to exclude
242
+ * @returns {Promise<string[]>}
243
+ */
244
+ export async function enumerateTemplateOutputs(sourcePath, options = {}) {
245
+ const { skip = [] } = options
246
+ const outputs = []
247
+ await enumerateInto(sourcePath, '', outputs, skip)
248
+ return outputs
249
+ }
250
+
251
+ async function enumerateInto(sourcePath, relPath, outputs, skip) {
252
+ const entries = await fs.readdir(sourcePath, { withFileTypes: true })
253
+ for (const entry of entries) {
254
+ const sourceName = entry.name
255
+ if (entry.isDirectory()) {
256
+ const targetName = sourceName.startsWith('_') && !sourceName.startsWith('__')
257
+ ? `.${sourceName.slice(1)}`
258
+ : sourceName
259
+ await enumerateInto(
260
+ path.join(sourcePath, sourceName),
261
+ relPath ? path.join(relPath, targetName) : targetName,
262
+ outputs,
263
+ skip,
264
+ )
265
+ } else {
266
+ if (sourceName === 'template.json') continue
267
+ const outputName = sourceName.endsWith('.hbs') ? sourceName.slice(0, -4) : sourceName
268
+ if (skip.includes(outputName)) continue
269
+ outputs.push(relPath ? path.join(relPath, outputName) : outputName)
270
+ }
271
+ }
272
+ }
273
+
227
274
  /**
228
275
  * Clear the template cache
229
276
  */
@@ -1,8 +1,17 @@
1
1
  /**
2
2
  * Workspace Config Management
3
3
  *
4
- * Read/write pnpm-workspace.yaml and root package.json.
5
- * Used by both `create` and `add` commands.
4
+ * Read/write pnpm-workspace.yaml and root package.json, plus URL config
5
+ * for the platform backend / registry. Used by both `create` and `add`
6
+ * commands.
7
+ *
8
+ * This module is on the CLI's startup path (statically imported by
9
+ * `commands/login.js`, which is loaded by `src/index.js`). It MUST NOT
10
+ * import any optional peer dependency — most importantly `@uniweb/build`,
11
+ * which is absent in `npx uniweb create` scratch dirs and global
12
+ * installs. Workspace package discovery (which does need `@uniweb/build`)
13
+ * lives in `./discover.js` and is loaded only by commands that already
14
+ * require a project context.
6
15
  */
7
16
 
8
17
  import { existsSync, readFileSync } from 'node:fs'
@@ -10,7 +19,6 @@ import { readFile, writeFile } from 'node:fs/promises'
10
19
  import { join } from 'node:path'
11
20
  import { homedir } from 'node:os'
12
21
  import yaml from 'js-yaml'
13
- import { classifyPackage } from '@uniweb/build'
14
22
  import { filterCmd } from './pm.js'
15
23
 
16
24
  // ── Platform URLs ──────────────────────────────────────────────
@@ -223,63 +231,6 @@ export async function updateRootScripts(rootDir, sites, pm = 'pnpm') {
223
231
  await writeRootPackageJson(rootDir, pkg)
224
232
  }
225
233
 
226
- /**
227
- * Discover foundations in the workspace
228
- * @param {string} rootDir - Workspace root directory
229
- * @returns {Promise<Array<{name: string, path: string}>>}
230
- */
231
- export async function discoverFoundations(rootDir) {
232
- return discoverByKind(rootDir, 'foundation')
233
- }
234
-
235
- /**
236
- * Discover sites in the workspace
237
- * @param {string} rootDir - Workspace root directory
238
- * @returns {Promise<Array<{name: string, path: string}>>}
239
- */
240
- export async function discoverSites(rootDir) {
241
- return discoverByKind(rootDir, 'site')
242
- }
243
-
244
- /**
245
- * Walk the workspace globs and return packages of the requested kind.
246
- * Uses `classifyPackage` from @uniweb/build — the canonical classifier
247
- * shared with the build pipeline, which keys on real signals (site.yml
248
- * for sites, generated entry for foundations) rather than which
249
- * `@uniweb/*` packages happen to be in dependencies. Templates whose
250
- * sites pull runtime transitively through the foundation (e.g.,
251
- * marketing) used to be invisible to the older dependency-based check.
252
- */
253
- async function discoverByKind(rootDir, kind) {
254
- const { packages } = await readWorkspaceConfig(rootDir)
255
- const out = []
256
-
257
- for (const pattern of packages) {
258
- const dirs = await resolveGlob(rootDir, pattern)
259
- for (const dir of dirs) {
260
- const fullPath = join(rootDir, dir)
261
- if (classifyPackage(fullPath) !== kind) continue
262
-
263
- // Read package.json for the package name. Synthesize one from
264
- // the directory if it's missing or malformed — we still want
265
- // the package to surface in pickers.
266
- const pkgPath = join(fullPath, 'package.json')
267
- let name = dir.split('/').pop()
268
- if (existsSync(pkgPath)) {
269
- try {
270
- const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'))
271
- if (pkg.name) name = pkg.name
272
- } catch {
273
- // keep directory-derived name
274
- }
275
- }
276
- out.push({ name, path: dir })
277
- }
278
- }
279
-
280
- return out
281
- }
282
-
283
234
  // Resolve a workspace glob pattern to actual directories
284
235
  export async function resolveGlob(rootDir, pattern) {
285
236
  const clean = pattern.replace(/^["']|["']$/g, '')
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Workspace package discovery
3
+ *
4
+ * Walks the workspace globs and classifies each package as a site or
5
+ * foundation using `classifyPackage` from `@uniweb/build` — the canonical
6
+ * classifier shared with the build pipeline. It keys on real signals
7
+ * (site.yml for sites, generated entry for foundations) rather than which
8
+ * `@uniweb/*` packages happen to be in dependencies, so templates whose
9
+ * sites pull runtime transitively through the foundation (e.g., marketing)
10
+ * are classified correctly.
11
+ *
12
+ * Why this lives in its own file: `@uniweb/build` is an OPTIONAL peer
13
+ * dependency of the CLI. The CLI's startup path (`src/index.js` and
14
+ * everything it statically imports) MUST run in environments where
15
+ * `@uniweb/build` is not installed — `npx uniweb create` in a scratch
16
+ * dir, `npm i -g uniweb` before any project exists, etc. Anything that
17
+ * imports `@uniweb/build` therefore must NOT be reachable from the
18
+ * startup graph; it must be loaded dynamically by commands that already
19
+ * require a project context. Keeping discovery in this dedicated module
20
+ * makes that boundary structural rather than conventional.
21
+ */
22
+
23
+ import { existsSync } from 'node:fs'
24
+ import { readFile } from 'node:fs/promises'
25
+ import { join } from 'node:path'
26
+ import { classifyPackage } from '@uniweb/build'
27
+ import { readWorkspaceConfig, resolveGlob } from './config.js'
28
+
29
+ /**
30
+ * Discover foundations in the workspace.
31
+ * @param {string} rootDir - Workspace root directory
32
+ * @returns {Promise<Array<{name: string, path: string}>>}
33
+ */
34
+ export async function discoverFoundations(rootDir) {
35
+ return discoverByKind(rootDir, 'foundation')
36
+ }
37
+
38
+ /**
39
+ * Discover sites in the workspace.
40
+ * @param {string} rootDir - Workspace root directory
41
+ * @returns {Promise<Array<{name: string, path: string}>>}
42
+ */
43
+ export async function discoverSites(rootDir) {
44
+ return discoverByKind(rootDir, 'site')
45
+ }
46
+
47
+ async function discoverByKind(rootDir, kind) {
48
+ const { packages } = await readWorkspaceConfig(rootDir)
49
+ const out = []
50
+
51
+ for (const pattern of packages) {
52
+ const dirs = await resolveGlob(rootDir, pattern)
53
+ for (const dir of dirs) {
54
+ const fullPath = join(rootDir, dir)
55
+ if (classifyPackage(fullPath) !== kind) continue
56
+
57
+ // Read package.json for the package name. Synthesize one from
58
+ // the directory if it's missing or malformed — we still want
59
+ // the package to surface in pickers.
60
+ const pkgPath = join(fullPath, 'package.json')
61
+ let name = dir.split('/').pop()
62
+ if (existsSync(pkgPath)) {
63
+ try {
64
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'))
65
+ if (pkg.name) name = pkg.name
66
+ } catch {
67
+ // keep directory-derived name
68
+ }
69
+ }
70
+ out.push({ name, path: dir })
71
+ }
72
+ }
73
+
74
+ return out
75
+ }
@@ -5,7 +5,8 @@
5
5
  * and detects collisions with existing workspace packages.
6
6
  */
7
7
 
8
- import { discoverFoundations, discoverSites, readWorkspaceConfig, resolveGlob } from './config.js'
8
+ import { readWorkspaceConfig, resolveGlob } from './config.js'
9
+ import { discoverFoundations, discoverSites } from './discover.js'
9
10
  import { existsSync } from 'node:fs'
10
11
  import { readFile } from 'node:fs/promises'
11
12
  import { join } from 'node:path'
@@ -11,7 +11,7 @@ import { join, dirname } from 'node:path'
11
11
  import { fileURLToPath } from 'node:url'
12
12
  import yaml from 'js-yaml'
13
13
  import Handlebars from 'handlebars'
14
- import { copyTemplateDirectory, registerVersions } from '../templates/processor.js'
14
+ import { copyTemplateDirectory, enumerateTemplateOutputs, registerVersions } from '../templates/processor.js'
15
15
  import { getVersionsForTemplates, getCliVersion } from '../versions.js'
16
16
 
17
17
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -48,6 +48,28 @@ export async function scaffoldWorkspace(targetDir, context, options = {}) {
48
48
  })
49
49
  }
50
50
 
51
+ /**
52
+ * Return the relative paths the workspace template would write into the
53
+ * project root, given the same skip rules `scaffoldWorkspace` applies.
54
+ * Used by the in-place create flow (`uniweb create .`) to detect conflicts
55
+ * before any I/O begins.
56
+ *
57
+ * Only the workspace template's outputs are enumerated. The foundation,
58
+ * site, and starter stages write into newly-created subdirectories
59
+ * (`src/`, `site/`) that don't pre-exist in a target like a fresh GitHub
60
+ * clone, so they can't conflict.
61
+ *
62
+ * @param {Object} [options]
63
+ * @param {boolean} [options.blank] - True when scaffolding a blank workspace
64
+ * (no packages yet) — skips `pnpm-workspace.yaml`, mirroring scaffoldWorkspace.
65
+ * @returns {Promise<string[]>}
66
+ */
67
+ export async function getWorkspaceTemplateOutputs({ blank = false } = {}) {
68
+ const skip = blank ? ['pnpm-workspace.yaml'] : []
69
+ const templatePath = join(TEMPLATES_DIR, 'workspace')
70
+ return enumerateTemplateOutputs(templatePath, { skip })
71
+ }
72
+
51
73
  /**
52
74
  * Scaffold a foundation from the foundation package template
53
75
  *