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 +10 -5
- package/package.json +5 -2
- package/src/commands/add.js +1 -2
- package/src/commands/dev.js +2 -1
- package/src/commands/doctor.js +1 -1
- package/src/commands/rename.js +1 -2
- package/src/framework-index.json +2 -2
- package/src/index.js +109 -10
- package/src/templates/processor.js +47 -0
- package/src/utils/config.js +11 -60
- package/src/utils/discover.js +75 -0
- package/src/utils/names.js +2 -1
- package/src/utils/scaffold.js +23 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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 | [
|
|
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.
|
|
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.
|
|
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
|
}
|
package/src/commands/add.js
CHANGED
|
@@ -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'
|
package/src/commands/dev.js
CHANGED
|
@@ -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 {
|
|
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
|
|
package/src/commands/doctor.js
CHANGED
|
@@ -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/
|
|
13
|
+
import { discoverFoundations, discoverSites } from '../utils/discover.js'
|
|
14
14
|
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
15
15
|
|
|
16
16
|
/**
|
package/src/commands/rename.js
CHANGED
|
@@ -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'
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-05-
|
|
3
|
+
"generatedAt": "2026-05-06T22:01:04.763Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.14.
|
|
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
|
|
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
|
-
//
|
|
777
|
-
|
|
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
|
|
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
|
*/
|
package/src/utils/config.js
CHANGED
|
@@ -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`
|
|
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
|
+
}
|
package/src/utils/names.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* and detects collisions with existing workspace packages.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
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'
|
package/src/utils/scaffold.js
CHANGED
|
@@ -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
|
*
|