uniweb 1.0.0 → 1.0.2
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/LICENSE +201 -0
- package/README.md +532 -3
- package/package.json +60 -11
- package/partials/adding-pages.hbs +77 -0
- package/partials/agents.md +3000 -0
- package/partials/ai-assistance.hbs +28 -0
- package/partials/components-docs.hbs +13 -0
- package/partials/config-reference.hbs +66 -0
- package/partials/deployment.hbs +13 -0
- package/partials/learn-more.hbs +6 -0
- package/partials/quick-start.hbs +8 -0
- package/partials/search-docs.hbs +110 -0
- package/src/backend/asset-download.js +222 -0
- package/src/backend/client.js +986 -0
- package/src/backend/foundation-bring-along.js +692 -0
- package/src/backend/payment-handoff.js +362 -0
- package/src/backend/service-request.js +210 -0
- package/src/backend/site-media.js +255 -0
- package/src/backend/site-preview.js +77 -0
- package/src/backend/site-sync.js +2515 -0
- package/src/backend/workspace.js +234 -0
- package/src/commands/add.js +1900 -0
- package/src/commands/build.js +1214 -0
- package/src/commands/clone.js +466 -0
- package/src/commands/content.js +202 -0
- package/src/commands/deploy.js +537 -0
- package/src/commands/dev.js +202 -0
- package/src/commands/docs.js +530 -0
- package/src/commands/doctor.js +1982 -0
- package/src/commands/export.js +116 -0
- package/src/commands/families.js +82 -0
- package/src/commands/forget.js +197 -0
- package/src/commands/handoff.js +21 -0
- package/src/commands/i18n.js +1707 -0
- package/src/commands/inspect.js +375 -0
- package/src/commands/invite.js +24 -0
- package/src/commands/org.js +96 -0
- package/src/commands/publish.js +1087 -0
- package/src/commands/pull.js +1257 -0
- package/src/commands/push.js +663 -0
- package/src/commands/refresh.js +212 -0
- package/src/commands/register.js +1092 -0
- package/src/commands/rename.js +647 -0
- package/src/commands/site.js +358 -0
- package/src/commands/snapshot.js +461 -0
- package/src/commands/starter.js +206 -0
- package/src/commands/status.js +302 -0
- package/src/commands/sync.js +93 -0
- package/src/commands/template.js +107 -0
- package/src/commands/update.js +887 -0
- package/src/commands/validate.js +376 -0
- package/src/framework-index.json +248 -0
- package/src/index.js +2241 -0
- package/src/templates/fetchers/github.js +113 -0
- package/src/templates/fetchers/npm.js +85 -0
- package/src/templates/fetchers/release.js +92 -0
- package/src/templates/index.js +190 -0
- package/src/templates/processor.js +333 -0
- package/src/templates/resolver.js +232 -0
- package/src/templates/validator.js +155 -0
- package/src/utils/agents-stamp.js +110 -0
- package/src/utils/args.js +126 -0
- package/src/utils/asset-upload.js +270 -0
- package/src/utils/bytes.js +30 -0
- package/src/utils/code-upload.js +394 -0
- package/src/utils/config.js +417 -0
- package/src/utils/conformance.js +274 -0
- package/src/utils/dep-survey.js +176 -0
- package/src/utils/destination-prompt.js +146 -0
- package/src/utils/discover.js +75 -0
- package/src/utils/env.js +22 -0
- package/src/utils/exit.js +30 -0
- package/src/utils/fetch-retry.js +109 -0
- package/src/utils/flag-guard.js +246 -0
- package/src/utils/foundation-name.js +179 -0
- package/src/utils/git.js +342 -0
- package/src/utils/host-prompt.js +51 -0
- package/src/utils/install-integrity.js +210 -0
- package/src/utils/interactive.js +118 -0
- package/src/utils/json-file.js +72 -0
- package/src/utils/names.js +91 -0
- package/src/utils/placement.js +151 -0
- package/src/utils/pm.js +165 -0
- package/src/utils/preview.js +20 -0
- package/src/utils/pull-written.js +132 -0
- package/src/utils/records-guard.js +88 -0
- package/src/utils/registry-auth.js +994 -0
- package/src/utils/registry-orgs.js +290 -0
- package/src/utils/scaffold.js +483 -0
- package/src/utils/schemaless-report.js +30 -0
- package/src/utils/semver-precedence.js +80 -0
- package/src/utils/session-file.js +95 -0
- package/src/utils/site-content-refs.js +23 -0
- package/src/utils/site-copies.js +123 -0
- package/src/utils/site-data-upload.js +302 -0
- package/src/utils/site-identity.js +137 -0
- package/src/utils/snapshot-settings.js +112 -0
- package/src/utils/update-check.js +302 -0
- package/src/utils/uwx-read.js +157 -0
- package/src/utils/workspace.js +223 -0
- package/src/versions.js +467 -0
- package/starter/foundation/main.js +50 -0
- package/starter/foundation/sections/Section/index.jsx +95 -0
- package/starter/foundation/sections/Section/meta.js +44 -0
- package/starter/site/pages/about/1-about.md.hbs +13 -0
- package/starter/site/pages/about/page.yml +3 -0
- package/starter/site/pages/home/1-welcome.md.hbs +14 -0
- package/starter/site/pages/home/page.yml +3 -0
- package/templates/foundation/_gitignore +5 -0
- package/templates/foundation/components/.gitkeep +0 -0
- package/templates/foundation/main.js.hbs +33 -0
- package/templates/foundation/package.json.hbs +51 -0
- package/templates/foundation/sections/.gitkeep +0 -0
- package/templates/foundation/styles.css +6 -0
- package/templates/foundation/vite.config.js +3 -0
- package/templates/site/_gitignore +12 -0
- package/templates/site/entry.js +7 -0
- package/templates/site/index.html.hbs +13 -0
- package/templates/site/package.json.hbs +27 -0
- package/templates/site/public/favicon.svg +7 -0
- package/templates/site/site.yml.hbs +91 -0
- package/templates/site/theme.yml +99 -0
- package/templates/site/vite.config.js +7 -0
- package/templates/workspace/AGENTS.md.hbs +2 -0
- package/templates/workspace/README.md.hbs +87 -0
- package/templates/workspace/_gitignore +29 -0
- package/templates/workspace/_vscode/settings.json +6 -0
- package/templates/workspace/package.json.hbs +20 -0
- package/templates/workspace/pnpm-workspace.yaml.hbs +26 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rename Command
|
|
3
|
+
*
|
|
4
|
+
* Renames packages across the workspace transactionally. Supports
|
|
5
|
+
* `rename foundation`, `rename site`, and `rename extension`. Each
|
|
6
|
+
* subcommand updates a different set of touch points:
|
|
7
|
+
*
|
|
8
|
+
* foundation: package.json::name + folder + every dependent site's
|
|
9
|
+
* package.json (dep key + file: path) + every site.yml::foundation
|
|
10
|
+
* reference + workspace manifests + root scripts.
|
|
11
|
+
*
|
|
12
|
+
* site: package.json::name + folder + workspace manifests + root
|
|
13
|
+
* scripts (sites are referenced by name in `pnpm --filter`-style
|
|
14
|
+
* scripts, hence the regen).
|
|
15
|
+
*
|
|
16
|
+
* extension: package.json::name + folder + every site.yml::extensions
|
|
17
|
+
* entry whose URL prefix matches the old folder path + workspace
|
|
18
|
+
* manifests. Sites don't carry a `file:` dep on extensions (extensions
|
|
19
|
+
* load by URL at runtime), so no per-site package.json updates.
|
|
20
|
+
*
|
|
21
|
+
* Extensions are technically a flavor of foundation (same build, same
|
|
22
|
+
* package.json shape, distinguished only by `extension: true` in
|
|
23
|
+
* main.js or `role: 'extension'` in the built schema). The
|
|
24
|
+
* rename verb still keeps them on a separate subcommand because the
|
|
25
|
+
* touch-point sets differ — `rename foundation` against an extension
|
|
26
|
+
* would update the wrong things. Each subcommand guards its target
|
|
27
|
+
* type and points at the right verb when wrong.
|
|
28
|
+
*
|
|
29
|
+
* Pre-flight checks run before any mutation. If anything would conflict
|
|
30
|
+
* (target name already taken, target not found, folder collision,
|
|
31
|
+
* type mismatch) we bail with a clear message and no partial state.
|
|
32
|
+
*
|
|
33
|
+
* Out of scope: registry side. What a foundation registers as is `name` in its
|
|
34
|
+
* main.js, which is independent of the workspace name and stays untouched —
|
|
35
|
+
* except for a foundation with no main.js `name`, whose package name IS its name
|
|
36
|
+
* (`readFoundationName`), so renaming the package renames what it would register
|
|
37
|
+
* as. There is no registry-rename flag — a registered version is immutable; to
|
|
38
|
+
* change a foundation's registered name, change main.js's `name` and register
|
|
39
|
+
* again (consuming sites repoint their `foundation:` ref).
|
|
40
|
+
*
|
|
41
|
+
* Usage:
|
|
42
|
+
* uniweb rename foundation <old> <new>
|
|
43
|
+
* uniweb rename site <old> <new>
|
|
44
|
+
* uniweb rename extension <old> <new>
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { existsSync } from 'node:fs'
|
|
48
|
+
import { readFile, writeFile, rename as fsRename } from 'node:fs/promises'
|
|
49
|
+
import { join, relative, dirname, basename } from 'node:path'
|
|
50
|
+
import yaml from 'js-yaml'
|
|
51
|
+
import { isExtensionPackage } from '@uniweb/build'
|
|
52
|
+
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
53
|
+
import {
|
|
54
|
+
readWorkspaceConfig,
|
|
55
|
+
writeWorkspaceConfig,
|
|
56
|
+
readRootPackageJson,
|
|
57
|
+
writeRootPackageJson,
|
|
58
|
+
updateRootScripts
|
|
59
|
+
} from '../utils/config.js'
|
|
60
|
+
import { discoverFoundations, discoverSites } from '../utils/discover.js'
|
|
61
|
+
import { writeJsonPreservingStyleAsync } from '../utils/json-file.js'
|
|
62
|
+
import { getExistingPackageNames, validatePackageName } from '../utils/names.js'
|
|
63
|
+
import { detectPackageManager, installCmd } from '../utils/pm.js'
|
|
64
|
+
import { getCliPrefix } from '../utils/interactive.js'
|
|
65
|
+
import { replaceInYamlList, setYamlKey } from '@uniweb/build/uwx'
|
|
66
|
+
|
|
67
|
+
const colors = {
|
|
68
|
+
reset: '\x1b[0m',
|
|
69
|
+
bright: '\x1b[1m',
|
|
70
|
+
dim: '\x1b[2m',
|
|
71
|
+
cyan: '\x1b[36m',
|
|
72
|
+
green: '\x1b[32m',
|
|
73
|
+
yellow: '\x1b[33m',
|
|
74
|
+
red: '\x1b[31m'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const success = (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`)
|
|
78
|
+
const error = (msg) => console.error(`${colors.red}✗${colors.reset} ${msg}`)
|
|
79
|
+
const info = (msg) => console.log(`${colors.dim}${msg}${colors.reset}`)
|
|
80
|
+
const log = console.log
|
|
81
|
+
|
|
82
|
+
const SUPPORTED_SUBCOMMANDS = new Set(['foundation', 'site', 'extension'])
|
|
83
|
+
|
|
84
|
+
export async function rename(args = []) {
|
|
85
|
+
const [subcommand, oldName, newName] = args
|
|
86
|
+
const prefix = getCliPrefix()
|
|
87
|
+
|
|
88
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
89
|
+
showHelp(prefix)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!SUPPORTED_SUBCOMMANDS.has(subcommand)) {
|
|
94
|
+
error(`Unknown subcommand: ${subcommand}`)
|
|
95
|
+
log(`Supported: rename foundation|site|extension <old> <new>`)
|
|
96
|
+
process.exit(1)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!oldName || !newName) {
|
|
100
|
+
error('Missing arguments.')
|
|
101
|
+
log(`Usage: ${prefix} rename ${subcommand} <old> <new>`)
|
|
102
|
+
process.exit(1)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (oldName === newName) {
|
|
106
|
+
error('Old and new names are identical — nothing to do.')
|
|
107
|
+
process.exit(1)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rootDir = findWorkspaceRoot()
|
|
111
|
+
if (!rootDir) {
|
|
112
|
+
error('Not in a Uniweb workspace.')
|
|
113
|
+
log(
|
|
114
|
+
`Run this command from your project root or a site/foundation directory.`
|
|
115
|
+
)
|
|
116
|
+
process.exit(1)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (subcommand === 'foundation') {
|
|
120
|
+
await renameFoundation(rootDir, oldName, newName, prefix)
|
|
121
|
+
} else if (subcommand === 'site') {
|
|
122
|
+
await renameSite(rootDir, oldName, newName, prefix)
|
|
123
|
+
} else if (subcommand === 'extension') {
|
|
124
|
+
await renameExtension(rootDir, oldName, newName, prefix)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Common helpers ──────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compute the new folder path for a rename. Returns { folderWillRename,
|
|
132
|
+
* newPath, newDir }. The leaf is renamed to match the new package name
|
|
133
|
+
* only when the leaf already matched the old package name — preserves
|
|
134
|
+
* any folder convention the user adopted that diverges from the package
|
|
135
|
+
* name.
|
|
136
|
+
*/
|
|
137
|
+
function computeNewFolderPath(rootDir, oldPath, oldName, newName) {
|
|
138
|
+
const leaf = basename(oldPath)
|
|
139
|
+
const folderWillRename = leaf === oldName
|
|
140
|
+
if (!folderWillRename) {
|
|
141
|
+
return {
|
|
142
|
+
folderWillRename,
|
|
143
|
+
newPath: oldPath,
|
|
144
|
+
newDir: join(rootDir, oldPath)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const parent = dirname(oldPath)
|
|
148
|
+
const newPath = parent === '.' ? newName : join(parent, newName)
|
|
149
|
+
return { folderWillRename, newPath, newDir: join(rootDir, newPath) }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Update the workspace manifests when a folder path moves. Both
|
|
154
|
+
* pnpm-workspace.yaml and package.json::workspaces are kept in sync
|
|
155
|
+
* (the multi-PM compatibility invariant). Wildcard entries
|
|
156
|
+
* (`extensions/*`) are left alone — only specific paths get rewritten.
|
|
157
|
+
*/
|
|
158
|
+
async function updateWorkspaceManifestsForFolderMove(
|
|
159
|
+
rootDir,
|
|
160
|
+
oldPath,
|
|
161
|
+
newPath
|
|
162
|
+
) {
|
|
163
|
+
const wsConfig = await readWorkspaceConfig(rootDir)
|
|
164
|
+
const rootPkg = await readRootPackageJson(rootDir)
|
|
165
|
+
if (wsConfig.packages.includes(oldPath)) {
|
|
166
|
+
const idx = wsConfig.packages.indexOf(oldPath)
|
|
167
|
+
wsConfig.packages[idx] = newPath
|
|
168
|
+
await writeWorkspaceConfig(rootDir, wsConfig)
|
|
169
|
+
}
|
|
170
|
+
if (
|
|
171
|
+
Array.isArray(rootPkg.workspaces) &&
|
|
172
|
+
rootPkg.workspaces.includes(oldPath)
|
|
173
|
+
) {
|
|
174
|
+
const idx = rootPkg.workspaces.indexOf(oldPath)
|
|
175
|
+
rootPkg.workspaces[idx] = newPath
|
|
176
|
+
await writeRootPackageJson(rootDir, rootPkg)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Validate that a name is a legal package name and not already taken in
|
|
182
|
+
* the workspace. Bails the process with an error if either check fails.
|
|
183
|
+
* `src` and `site` are grandfathered as valid names because they're the
|
|
184
|
+
* default folder leaf for the canonical foundation and site, and `add
|
|
185
|
+
* foundation` / `add site` already grandfather them at scaffold time.
|
|
186
|
+
*/
|
|
187
|
+
async function validateRenameName(rootDir, newName) {
|
|
188
|
+
if (newName !== 'src' && newName !== 'site') {
|
|
189
|
+
const valid = validatePackageName(newName)
|
|
190
|
+
if (valid !== true) {
|
|
191
|
+
error(valid)
|
|
192
|
+
process.exit(1)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const existingNames = await getExistingPackageNames(rootDir)
|
|
196
|
+
if (existingNames.has(newName)) {
|
|
197
|
+
error(
|
|
198
|
+
`Cannot rename: a package named ${colors.bright}${newName}${colors.reset} already exists in this workspace.`
|
|
199
|
+
)
|
|
200
|
+
process.exit(1)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Rewrite the `name` field in a package.json file.
|
|
206
|
+
*/
|
|
207
|
+
async function rewritePackageJsonName(pkgPath, newName) {
|
|
208
|
+
const src = await readFile(pkgPath, 'utf-8')
|
|
209
|
+
const pkg = JSON.parse(src)
|
|
210
|
+
pkg.name = newName
|
|
211
|
+
await writeJsonPreservingStyleAsync(pkgPath, pkg, src)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Stop, before any file moves, when a site's `site.yml` names the package in a
|
|
216
|
+
* form the in-place edit cannot reach. The alternative is rewriting the whole
|
|
217
|
+
* file, which loses the author's comments and formatting — so we refuse and say
|
|
218
|
+
* how to write it instead.
|
|
219
|
+
*/
|
|
220
|
+
function refuseUneditableSiteYml(sitePaths, key, hint) {
|
|
221
|
+
if (sitePaths.length === 0) return
|
|
222
|
+
for (const path of sitePaths) {
|
|
223
|
+
error(
|
|
224
|
+
`Cannot rename: ${colors.bright}${path}/site.yml${colors.reset} writes \`${key}:\` in a form this command cannot edit without rewriting the file.`
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
log(`Nothing was changed. ${hint}`)
|
|
228
|
+
log('Then run the rename again.')
|
|
229
|
+
process.exit(1)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Foundation rename ───────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
async function renameFoundation(rootDir, oldName, newName, prefix) {
|
|
235
|
+
await validateRenameName(rootDir, newName)
|
|
236
|
+
|
|
237
|
+
const foundations = await discoverFoundations(rootDir)
|
|
238
|
+
const target = foundations.find((f) => f.name === oldName)
|
|
239
|
+
if (!target) {
|
|
240
|
+
error(
|
|
241
|
+
`No foundation named ${colors.bright}${oldName}${colors.reset} in this workspace.`
|
|
242
|
+
)
|
|
243
|
+
if (foundations.length > 0) {
|
|
244
|
+
log(`Available: ${foundations.map((f) => f.name).join(', ')}`)
|
|
245
|
+
}
|
|
246
|
+
process.exit(1)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Type guard — point users at the right subcommand if they're trying
|
|
250
|
+
// to rename an extension via the foundation verb. They share a build
|
|
251
|
+
// shape but their touch-point sets differ (foundation rename touches
|
|
252
|
+
// sites' deps + site.yml::foundation; extension rename touches
|
|
253
|
+
// site.yml::extensions URLs).
|
|
254
|
+
if (isExtensionPackage(join(rootDir, target.path))) {
|
|
255
|
+
error(
|
|
256
|
+
`${colors.bright}${oldName}${colors.reset} is an extension, not a foundation.`
|
|
257
|
+
)
|
|
258
|
+
log(`Use \`${prefix} rename extension ${oldName} ${newName}\` instead.`)
|
|
259
|
+
process.exit(1)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const oldFoundationPath = target.path
|
|
263
|
+
const oldFoundationDir = join(rootDir, oldFoundationPath)
|
|
264
|
+
const {
|
|
265
|
+
folderWillRename,
|
|
266
|
+
newPath: newFoundationPath,
|
|
267
|
+
newDir: newFoundationDir
|
|
268
|
+
} = computeNewFolderPath(rootDir, oldFoundationPath, oldName, newName)
|
|
269
|
+
if (folderWillRename && existsSync(newFoundationDir)) {
|
|
270
|
+
error(
|
|
271
|
+
`Cannot rename: target folder ${colors.bright}${newFoundationPath}/${colors.reset} already exists.`
|
|
272
|
+
)
|
|
273
|
+
process.exit(1)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Find every site that depends on the foundation. Two signals must
|
|
277
|
+
// agree: site.yml::foundation === oldName AND package.json has the
|
|
278
|
+
// dep key. If they disagree (which doctor would have flagged) we
|
|
279
|
+
// still rename whichever signals point to the old name — the goal
|
|
280
|
+
// is to leave the workspace consistent under the new name.
|
|
281
|
+
const sites = await discoverSites(rootDir)
|
|
282
|
+
const affectedSites = []
|
|
283
|
+
for (const site of sites) {
|
|
284
|
+
const sitePkgPath = join(rootDir, site.path, 'package.json')
|
|
285
|
+
const siteYmlPath = join(rootDir, site.path, 'site.yml')
|
|
286
|
+
let pkg, pkgSrc, ymlText, ymlData
|
|
287
|
+
try {
|
|
288
|
+
pkgSrc = await readFile(sitePkgPath, 'utf-8')
|
|
289
|
+
pkg = JSON.parse(pkgSrc)
|
|
290
|
+
} catch {
|
|
291
|
+
pkg = null
|
|
292
|
+
pkgSrc = null
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
ymlText = await readFile(siteYmlPath, 'utf-8')
|
|
296
|
+
ymlData = yaml.load(ymlText) || {}
|
|
297
|
+
} catch {
|
|
298
|
+
ymlData = null
|
|
299
|
+
}
|
|
300
|
+
const hasDep = pkg?.dependencies && oldName in pkg.dependencies
|
|
301
|
+
const ymlMatches = ymlData?.foundation === oldName
|
|
302
|
+
if (hasDep || ymlMatches) {
|
|
303
|
+
affectedSites.push({
|
|
304
|
+
path: site.path,
|
|
305
|
+
name: site.name,
|
|
306
|
+
pkg,
|
|
307
|
+
pkgSrc,
|
|
308
|
+
sitePkgPath,
|
|
309
|
+
siteYmlPath,
|
|
310
|
+
// Edited in place — `setYamlKey`, @uniweb/build — and computed HERE, before
|
|
311
|
+
// anything moves, so a file it cannot edit stops the rename cleanly.
|
|
312
|
+
newYmlText: ymlMatches ? setYamlKey(ymlText, 'foundation', newName) : null,
|
|
313
|
+
hasDep,
|
|
314
|
+
ymlMatches
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
refuseUneditableSiteYml(
|
|
319
|
+
affectedSites.filter((s) => s.ymlMatches && s.newYmlText === null).map((s) => s.path),
|
|
320
|
+
'foundation',
|
|
321
|
+
`Write \`foundation: ${oldName}\` as a plain value — not through a YAML alias (\`*name\`) or merge key (\`<<:\`).`
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
// ─── Print plan, then execute ────────────────────────────────
|
|
325
|
+
|
|
326
|
+
log('')
|
|
327
|
+
log(
|
|
328
|
+
`${colors.bright}Rename foundation${colors.reset}: ${colors.yellow}${oldName}${colors.reset} → ${colors.green}${newName}${colors.reset}`
|
|
329
|
+
)
|
|
330
|
+
log('')
|
|
331
|
+
if (folderWillRename) {
|
|
332
|
+
info(` Folder: ${oldFoundationPath}/ → ${newFoundationPath}/`)
|
|
333
|
+
} else {
|
|
334
|
+
info(
|
|
335
|
+
` Folder: ${oldFoundationPath}/ (unchanged — leaf doesn't match package name)`
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
info(` package.json::name: "${oldName}" → "${newName}"`)
|
|
339
|
+
if (affectedSites.length === 0) {
|
|
340
|
+
info(` Sites depending on this foundation: none`)
|
|
341
|
+
} else {
|
|
342
|
+
info(` Sites depending on this foundation:`)
|
|
343
|
+
for (const s of affectedSites) {
|
|
344
|
+
info(` • ${s.name} at ${s.path}/`)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
log('')
|
|
348
|
+
|
|
349
|
+
if (folderWillRename) {
|
|
350
|
+
await fsRename(oldFoundationDir, newFoundationDir)
|
|
351
|
+
}
|
|
352
|
+
await rewritePackageJsonName(join(newFoundationDir, 'package.json'), newName)
|
|
353
|
+
|
|
354
|
+
for (const s of affectedSites) {
|
|
355
|
+
if (s.hasDep) {
|
|
356
|
+
const newRel = relative(join(rootDir, s.path), newFoundationDir) || '.'
|
|
357
|
+
const oldValue = s.pkg.dependencies[oldName]
|
|
358
|
+
delete s.pkg.dependencies[oldName]
|
|
359
|
+
s.pkg.dependencies[newName] = oldValue.startsWith('file:')
|
|
360
|
+
? `file:${newRel}`
|
|
361
|
+
: oldValue // npm-pinned, leave it; rename-then-republish is out of scope.
|
|
362
|
+
await writeJsonPreservingStyleAsync(s.sitePkgPath, s.pkg, s.pkgSrc)
|
|
363
|
+
}
|
|
364
|
+
if (s.ymlMatches) {
|
|
365
|
+
await writeFile(s.siteYmlPath, s.newYmlText)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (folderWillRename) {
|
|
370
|
+
await updateWorkspaceManifestsForFolderMove(
|
|
371
|
+
rootDir,
|
|
372
|
+
oldFoundationPath,
|
|
373
|
+
newFoundationPath
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Root scripts can reference the foundation by name (e.g. `pnpm --filter
|
|
378
|
+
// <name> build`). updateRootScripts regenerates them from the current
|
|
379
|
+
// discoverSites output, which has fresh names after the writes above.
|
|
380
|
+
const pm = detectPackageManager()
|
|
381
|
+
const freshSites = await discoverSites(rootDir)
|
|
382
|
+
await updateRootScripts(rootDir, freshSites, pm)
|
|
383
|
+
|
|
384
|
+
log('')
|
|
385
|
+
success(
|
|
386
|
+
`Renamed foundation ${colors.bright}${oldName}${colors.reset} → ${colors.bright}${newName}${colors.reset}`
|
|
387
|
+
)
|
|
388
|
+
printNextSteps(prefix, pm)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ─── Site rename ─────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
async function renameSite(rootDir, oldName, newName, prefix) {
|
|
394
|
+
await validateRenameName(rootDir, newName)
|
|
395
|
+
|
|
396
|
+
const sites = await discoverSites(rootDir)
|
|
397
|
+
const target = sites.find((s) => s.name === oldName)
|
|
398
|
+
if (!target) {
|
|
399
|
+
error(
|
|
400
|
+
`No site named ${colors.bright}${oldName}${colors.reset} in this workspace.`
|
|
401
|
+
)
|
|
402
|
+
if (sites.length > 0) {
|
|
403
|
+
log(`Available: ${sites.map((s) => s.name).join(', ')}`)
|
|
404
|
+
}
|
|
405
|
+
process.exit(1)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const oldSitePath = target.path
|
|
409
|
+
const oldSiteDir = join(rootDir, oldSitePath)
|
|
410
|
+
const {
|
|
411
|
+
folderWillRename,
|
|
412
|
+
newPath: newSitePath,
|
|
413
|
+
newDir: newSiteDir
|
|
414
|
+
} = computeNewFolderPath(rootDir, oldSitePath, oldName, newName)
|
|
415
|
+
if (folderWillRename && existsSync(newSiteDir)) {
|
|
416
|
+
error(
|
|
417
|
+
`Cannot rename: target folder ${colors.bright}${newSitePath}/${colors.reset} already exists.`
|
|
418
|
+
)
|
|
419
|
+
process.exit(1)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
log('')
|
|
423
|
+
log(
|
|
424
|
+
`${colors.bright}Rename site${colors.reset}: ${colors.yellow}${oldName}${colors.reset} → ${colors.green}${newName}${colors.reset}`
|
|
425
|
+
)
|
|
426
|
+
log('')
|
|
427
|
+
if (folderWillRename) {
|
|
428
|
+
info(` Folder: ${oldSitePath}/ → ${newSitePath}/`)
|
|
429
|
+
} else {
|
|
430
|
+
info(
|
|
431
|
+
` Folder: ${oldSitePath}/ (unchanged — leaf doesn't match package name)`
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
info(` package.json::name: "${oldName}" → "${newName}"`)
|
|
435
|
+
log('')
|
|
436
|
+
|
|
437
|
+
if (folderWillRename) {
|
|
438
|
+
await fsRename(oldSiteDir, newSiteDir)
|
|
439
|
+
}
|
|
440
|
+
await rewritePackageJsonName(join(newSiteDir, 'package.json'), newName)
|
|
441
|
+
if (folderWillRename) {
|
|
442
|
+
await updateWorkspaceManifestsForFolderMove(
|
|
443
|
+
rootDir,
|
|
444
|
+
oldSitePath,
|
|
445
|
+
newSitePath
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Root scripts include `pnpm --filter <site-name>` style invocations
|
|
450
|
+
// for `dev` / `preview` and per-site aliases. Regenerate from fresh
|
|
451
|
+
// site discovery to pick up the new name.
|
|
452
|
+
const pm = detectPackageManager()
|
|
453
|
+
const freshSites = await discoverSites(rootDir)
|
|
454
|
+
await updateRootScripts(rootDir, freshSites, pm)
|
|
455
|
+
|
|
456
|
+
log('')
|
|
457
|
+
success(
|
|
458
|
+
`Renamed site ${colors.bright}${oldName}${colors.reset} → ${colors.bright}${newName}${colors.reset}`
|
|
459
|
+
)
|
|
460
|
+
printNextSteps(prefix, pm)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ─── Extension rename ────────────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
async function renameExtension(rootDir, oldName, newName, prefix) {
|
|
466
|
+
await validateRenameName(rootDir, newName)
|
|
467
|
+
|
|
468
|
+
// Extensions are a subset of foundations (same build, distinguished
|
|
469
|
+
// by `extension: true` declaration). Find via discoverFoundations,
|
|
470
|
+
// then verify the extension marker.
|
|
471
|
+
const foundations = await discoverFoundations(rootDir)
|
|
472
|
+
const target = foundations.find((f) => f.name === oldName)
|
|
473
|
+
if (!target) {
|
|
474
|
+
error(
|
|
475
|
+
`No package named ${colors.bright}${oldName}${colors.reset} in this workspace.`
|
|
476
|
+
)
|
|
477
|
+
process.exit(1)
|
|
478
|
+
}
|
|
479
|
+
if (!isExtensionPackage(join(rootDir, target.path))) {
|
|
480
|
+
error(
|
|
481
|
+
`${colors.bright}${oldName}${colors.reset} is a foundation, not an extension.`
|
|
482
|
+
)
|
|
483
|
+
log(`Use \`${prefix} rename foundation ${oldName} ${newName}\` instead.`)
|
|
484
|
+
process.exit(1)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const oldExtPath = target.path
|
|
488
|
+
const oldExtDir = join(rootDir, oldExtPath)
|
|
489
|
+
const {
|
|
490
|
+
folderWillRename,
|
|
491
|
+
newPath: newExtPath,
|
|
492
|
+
newDir: newExtDir
|
|
493
|
+
} = computeNewFolderPath(rootDir, oldExtPath, oldName, newName)
|
|
494
|
+
if (folderWillRename && existsSync(newExtDir)) {
|
|
495
|
+
error(
|
|
496
|
+
`Cannot rename: target folder ${colors.bright}${newExtPath}/${colors.reset} already exists.`
|
|
497
|
+
)
|
|
498
|
+
process.exit(1)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Find every site referencing this extension via its site.yml's
|
|
502
|
+
// `extensions:` array. Sites declare extensions by URL, with the
|
|
503
|
+
// shape `/<workspace-relative-path>/dist/<file>` (where <file> is
|
|
504
|
+
// `entry.js` post-Phase-4, `foundation.js` for older builds — match
|
|
505
|
+
// both). Foreign URLs (https://, anything not starting with
|
|
506
|
+
// `/<oldExtPath>/`) are left untouched.
|
|
507
|
+
const oldUrlPrefix = `/${oldExtPath}/`
|
|
508
|
+
const newUrlPrefix = `/${newExtPath}/`
|
|
509
|
+
const sites = await discoverSites(rootDir)
|
|
510
|
+
const affectedSites = []
|
|
511
|
+
for (const site of sites) {
|
|
512
|
+
const siteYmlPath = join(rootDir, site.path, 'site.yml')
|
|
513
|
+
let ymlText, ymlData
|
|
514
|
+
try {
|
|
515
|
+
ymlText = await readFile(siteYmlPath, 'utf-8')
|
|
516
|
+
ymlData = yaml.load(ymlText) || {}
|
|
517
|
+
} catch {
|
|
518
|
+
continue
|
|
519
|
+
}
|
|
520
|
+
const exts = Array.isArray(ymlData.extensions) ? ymlData.extensions : []
|
|
521
|
+
const hits = exts.filter(
|
|
522
|
+
(e) => typeof e === 'string' && e.startsWith(oldUrlPrefix)
|
|
523
|
+
)
|
|
524
|
+
if (hits.length > 0) {
|
|
525
|
+
const replacements = new Map(
|
|
526
|
+
hits.map((e) => [e, newUrlPrefix + e.slice(oldUrlPrefix.length)])
|
|
527
|
+
)
|
|
528
|
+
affectedSites.push({
|
|
529
|
+
site,
|
|
530
|
+
siteYmlPath,
|
|
531
|
+
hits,
|
|
532
|
+
// Edited in place, and computed before anything moves — as for foundations.
|
|
533
|
+
newYmlText: replaceInYamlList(ymlText, 'extensions', replacements)
|
|
534
|
+
})
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
refuseUneditableSiteYml(
|
|
538
|
+
affectedSites.filter((a) => a.newYmlText === null).map((a) => a.site.path),
|
|
539
|
+
'extensions',
|
|
540
|
+
'Write `extensions:` as a plain list — not through a YAML alias (`*name`) or merge key (`<<:`).'
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
log('')
|
|
544
|
+
log(
|
|
545
|
+
`${colors.bright}Rename extension${colors.reset}: ${colors.yellow}${oldName}${colors.reset} → ${colors.green}${newName}${colors.reset}`
|
|
546
|
+
)
|
|
547
|
+
log('')
|
|
548
|
+
if (folderWillRename) {
|
|
549
|
+
info(` Folder: ${oldExtPath}/ → ${newExtPath}/`)
|
|
550
|
+
} else {
|
|
551
|
+
info(
|
|
552
|
+
` Folder: ${oldExtPath}/ (unchanged — leaf doesn't match package name)`
|
|
553
|
+
)
|
|
554
|
+
}
|
|
555
|
+
info(` package.json::name: "${oldName}" → "${newName}"`)
|
|
556
|
+
if (affectedSites.length === 0) {
|
|
557
|
+
info(` Sites referencing this extension: none`)
|
|
558
|
+
} else {
|
|
559
|
+
info(` Sites referencing this extension:`)
|
|
560
|
+
for (const { site, hits } of affectedSites) {
|
|
561
|
+
info(
|
|
562
|
+
` • ${site.name} at ${site.path}/ (${hits.length} entr${hits.length === 1 ? 'y' : 'ies'})`
|
|
563
|
+
)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
log('')
|
|
567
|
+
|
|
568
|
+
if (folderWillRename) {
|
|
569
|
+
await fsRename(oldExtDir, newExtDir)
|
|
570
|
+
}
|
|
571
|
+
await rewritePackageJsonName(join(newExtDir, 'package.json'), newName)
|
|
572
|
+
|
|
573
|
+
for (const a of affectedSites) {
|
|
574
|
+
await writeFile(a.siteYmlPath, a.newYmlText)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (folderWillRename) {
|
|
578
|
+
await updateWorkspaceManifestsForFolderMove(rootDir, oldExtPath, newExtPath)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Extensions don't appear in root scripts (no dev/preview filter by
|
|
582
|
+
// extension name — the foundation's own scripts handle building).
|
|
583
|
+
// No updateRootScripts needed.
|
|
584
|
+
|
|
585
|
+
const pm = detectPackageManager()
|
|
586
|
+
log('')
|
|
587
|
+
success(
|
|
588
|
+
`Renamed extension ${colors.bright}${oldName}${colors.reset} → ${colors.bright}${newName}${colors.reset}`
|
|
589
|
+
)
|
|
590
|
+
printNextSteps(prefix, pm)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ─── Shared output ───────────────────────────────────────────────
|
|
594
|
+
|
|
595
|
+
function printNextSteps(prefix, pm) {
|
|
596
|
+
log('')
|
|
597
|
+
log(
|
|
598
|
+
`Next: ${colors.cyan}${installCmd(pm)}${colors.reset} ${colors.dim}(refresh symlinks under the new name)${colors.reset}`
|
|
599
|
+
)
|
|
600
|
+
log(
|
|
601
|
+
` ${colors.cyan}${prefix} doctor${colors.reset} ${colors.dim}(verify wiring)${colors.reset}`
|
|
602
|
+
)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function showHelp(prefix) {
|
|
606
|
+
log(`
|
|
607
|
+
${colors.cyan}${colors.bright}Uniweb Rename${colors.reset}
|
|
608
|
+
|
|
609
|
+
Rename a package across the workspace, keeping all wiring in sync.
|
|
610
|
+
|
|
611
|
+
${colors.bright}Usage:${colors.reset}
|
|
612
|
+
${prefix} rename foundation <old-name> <new-name>
|
|
613
|
+
${prefix} rename site <old-name> <new-name>
|
|
614
|
+
${prefix} rename extension <old-name> <new-name>
|
|
615
|
+
|
|
616
|
+
${colors.bright}What rename foundation does:${colors.reset}
|
|
617
|
+
• Updates the foundation's package.json::name.
|
|
618
|
+
• Renames the folder if its leaf matched the old package name.
|
|
619
|
+
• Updates every site's package.json dependency key + file: path.
|
|
620
|
+
• Updates every site's site.yml::foundation reference.
|
|
621
|
+
• Updates pnpm-workspace.yaml + package.json::workspaces (kept in sync).
|
|
622
|
+
• Regenerates root scripts.
|
|
623
|
+
|
|
624
|
+
${colors.bright}What rename site does:${colors.reset}
|
|
625
|
+
• Updates the site's package.json::name.
|
|
626
|
+
• Renames the folder if its leaf matched the old package name.
|
|
627
|
+
• Updates pnpm-workspace.yaml + package.json::workspaces.
|
|
628
|
+
• Regenerates root scripts (\`dev\` / \`preview\` filter by site name).
|
|
629
|
+
|
|
630
|
+
${colors.bright}What rename extension does:${colors.reset}
|
|
631
|
+
• Updates the extension's package.json::name.
|
|
632
|
+
• Renames the folder if its leaf matched the old package name.
|
|
633
|
+
• Updates every site.yml::extensions URL whose path matched the old folder.
|
|
634
|
+
• Updates pnpm-workspace.yaml + package.json::workspaces.
|
|
635
|
+
|
|
636
|
+
${colors.bright}What rename does NOT do (any subcommand):${colors.reset}
|
|
637
|
+
• Push to the registry, or change what a foundation registers as — that is
|
|
638
|
+
\`name\` in its main.js, independent of the package name. To register under
|
|
639
|
+
a new name, change it there and run \`${prefix} register\` (alias
|
|
640
|
+
\`${prefix} release\`) — there is no rename flag.
|
|
641
|
+
|
|
642
|
+
${colors.bright}Examples:${colors.reset}
|
|
643
|
+
${prefix} rename foundation src marketing-src
|
|
644
|
+
${prefix} rename site site marketing-com
|
|
645
|
+
${prefix} rename extension effects animations
|
|
646
|
+
`)
|
|
647
|
+
}
|