uniweb 0.12.20 → 0.12.22
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 +30 -1
- package/package.json +7 -7
- package/partials/agents.md +41 -11
- package/partials/config-reference.hbs +1 -2
- package/src/commands/add.js +1 -87
- package/src/commands/build.js +2 -2
- package/src/commands/clone.js +337 -0
- package/src/commands/content.js +199 -0
- package/src/commands/deploy.js +27 -6
- package/src/commands/docs.js +2 -3
- package/src/commands/doctor.js +24 -5
- package/src/commands/org.js +66 -0
- package/src/commands/publish.js +4 -3
- package/src/commands/pull.js +238 -0
- package/src/commands/push.js +400 -0
- package/src/commands/register.js +274 -0
- package/src/commands/rename.js +10 -5
- package/src/commands/update.js +211 -245
- package/src/commands/validate.js +288 -0
- package/src/framework-index.json +11 -10
- package/src/index.js +155 -26
- package/src/templates/processor.js +41 -19
- package/src/utils/config.js +30 -2
- package/src/utils/dep-survey.js +99 -0
- package/src/utils/json-file.js +68 -0
- package/src/utils/placement.js +100 -0
- package/src/utils/pm.js +29 -0
- package/src/utils/registry-auth.js +380 -0
- package/src/utils/registry-orgs.js +179 -0
- package/src/utils/scaffold.js +18 -5
- package/src/utils/site-content-refs.js +21 -0
- package/src/utils/update-check.js +4 -2
- package/src/versions.js +11 -4
- package/templates/foundation/_gitignore +5 -0
- package/templates/site/_gitignore +5 -0
- package/templates/site/package.json.hbs +2 -2
- package/templates/workspace/_gitignore +33 -0
|
@@ -166,6 +166,35 @@ async function processFile(sourcePath, targetPath, data, options = {}) {
|
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Rename a leading single underscore to a dot: `_gitignore` → `.gitignore`,
|
|
171
|
+
* `_vscode` → `.vscode`.
|
|
172
|
+
*
|
|
173
|
+
* Dotfiles live in the template source under a `_`-prefixed name for two
|
|
174
|
+
* reasons: (1) so they aren't swept up by the template repo's own git ignore
|
|
175
|
+
* rules, and (2) — critically — so they survive `npm publish`. npm strips any
|
|
176
|
+
* file literally named `.gitignore` from the package tarball (it's treated as
|
|
177
|
+
* an ignore-source, not shippable content), so a template that needs to ship a
|
|
178
|
+
* `.gitignore` MUST store it as `_gitignore` and rename it at scaffold time.
|
|
179
|
+
* `.gitkeep` and other dotfiles ship fine; `.gitignore` is the trap.
|
|
180
|
+
*
|
|
181
|
+
* `__name` (double underscore) is left untouched, matching the prior
|
|
182
|
+
* directory-only behavior.
|
|
183
|
+
*/
|
|
184
|
+
function dotfileRename(name) {
|
|
185
|
+
return name.startsWith('_') && !name.startsWith('__') ? `.${name.slice(1)}` : name
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Compute a template file's scaffolded output name: strip the `.hbs`
|
|
190
|
+
* Handlebars suffix, then apply {@link dotfileRename}. So `_gitignore` →
|
|
191
|
+
* `.gitignore` and `_env.local.hbs` → `.env.local`.
|
|
192
|
+
*/
|
|
193
|
+
function templateOutputName(sourceName) {
|
|
194
|
+
const base = sourceName.endsWith('.hbs') ? sourceName.slice(0, -4) : sourceName
|
|
195
|
+
return dotfileRename(base)
|
|
196
|
+
}
|
|
197
|
+
|
|
169
198
|
/**
|
|
170
199
|
* Copy a directory structure recursively, processing templates
|
|
171
200
|
*
|
|
@@ -187,12 +216,8 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
|
|
|
187
216
|
|
|
188
217
|
if (entry.isDirectory()) {
|
|
189
218
|
const sourceFullPath = path.join(sourcePath, sourceName)
|
|
190
|
-
// Rename _prefix directories to .prefix (e.g., _vscode → .vscode)
|
|
191
|
-
|
|
192
|
-
const targetName = sourceName.startsWith('_') && !sourceName.startsWith('__')
|
|
193
|
-
? `.${sourceName.slice(1)}`
|
|
194
|
-
: sourceName
|
|
195
|
-
const targetFullPath = path.join(targetPath, targetName)
|
|
219
|
+
// Rename _prefix directories to .prefix (e.g., _vscode → .vscode).
|
|
220
|
+
const targetFullPath = path.join(targetPath, dotfileRename(sourceName))
|
|
196
221
|
|
|
197
222
|
await copyTemplateDirectory(sourceFullPath, targetFullPath, data, { onWarning, onProgress, skip })
|
|
198
223
|
} else {
|
|
@@ -201,17 +226,14 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
|
|
|
201
226
|
continue
|
|
202
227
|
}
|
|
203
228
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
229
|
+
// Output name: strip `.hbs`, then rename a leading `_` to `.`
|
|
230
|
+
// (e.g. `_gitignore` → `.gitignore`). Used for both the skip check and
|
|
231
|
+
// the written filename so they can't drift.
|
|
232
|
+
const targetName = templateOutputName(sourceName)
|
|
233
|
+
if (skip?.includes(targetName)) {
|
|
207
234
|
continue
|
|
208
235
|
}
|
|
209
236
|
|
|
210
|
-
// Remove .hbs extension for target filename
|
|
211
|
-
const targetName = sourceName.endsWith('.hbs')
|
|
212
|
-
? sourceName.slice(0, -4)
|
|
213
|
-
: sourceName
|
|
214
|
-
|
|
215
237
|
const sourceFullPath = path.join(sourcePath, sourceName)
|
|
216
238
|
const targetFullPath = path.join(targetPath, targetName)
|
|
217
239
|
|
|
@@ -228,7 +250,9 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
|
|
|
228
250
|
* Enumerate the output paths a template directory would write, without
|
|
229
251
|
* touching disk. Mirrors `copyTemplateDirectory`'s naming rules:
|
|
230
252
|
* - `.hbs` extension is stripped
|
|
231
|
-
* - `
|
|
253
|
+
* - a leading `_` is renamed to `.` for both files and directories
|
|
254
|
+
* (e.g. `_gitignore` → `.gitignore`, `_vscode` → `.vscode`); `__name`
|
|
255
|
+
* is preserved
|
|
232
256
|
* - `template.json` is excluded
|
|
233
257
|
* - `skip` filenames are excluded by their post-rename name
|
|
234
258
|
*
|
|
@@ -253,9 +277,7 @@ async function enumerateInto(sourcePath, relPath, outputs, skip) {
|
|
|
253
277
|
for (const entry of entries) {
|
|
254
278
|
const sourceName = entry.name
|
|
255
279
|
if (entry.isDirectory()) {
|
|
256
|
-
const targetName =
|
|
257
|
-
? `.${sourceName.slice(1)}`
|
|
258
|
-
: sourceName
|
|
280
|
+
const targetName = dotfileRename(sourceName)
|
|
259
281
|
await enumerateInto(
|
|
260
282
|
path.join(sourcePath, sourceName),
|
|
261
283
|
relPath ? path.join(relPath, targetName) : targetName,
|
|
@@ -264,7 +286,7 @@ async function enumerateInto(sourcePath, relPath, outputs, skip) {
|
|
|
264
286
|
)
|
|
265
287
|
} else {
|
|
266
288
|
if (sourceName === 'template.json') continue
|
|
267
|
-
const outputName =
|
|
289
|
+
const outputName = templateOutputName(sourceName)
|
|
268
290
|
if (skip.includes(outputName)) continue
|
|
269
291
|
outputs.push(relPath ? path.join(relPath, outputName) : outputName)
|
|
270
292
|
}
|
package/src/utils/config.js
CHANGED
|
@@ -20,6 +20,7 @@ import { join } from 'node:path'
|
|
|
20
20
|
import { homedir } from 'node:os'
|
|
21
21
|
import yaml from 'js-yaml'
|
|
22
22
|
import { filterCmd } from './pm.js'
|
|
23
|
+
import { writeJsonPreservingStyleAsync } from './json-file.js'
|
|
23
24
|
|
|
24
25
|
// ── Platform URLs ──────────────────────────────────────────────
|
|
25
26
|
|
|
@@ -82,6 +83,27 @@ export function getRegistryUrl() {
|
|
|
82
83
|
|| PRODUCTION_REGISTRY_URL
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Get the new registry backend's API base origin — DISTINCT from the
|
|
88
|
+
* legacy PHP getBackendUrl(). `register` POSTs to {origin}/dev/registry/register
|
|
89
|
+
* and the new-backend `login` to {origin}/dev/auth/login.
|
|
90
|
+
*
|
|
91
|
+
* Priority: UNIWEB_REGISTER_URL's origin (the env users already set for
|
|
92
|
+
* register) > ~/.uniweb/config.json registryApiUrl > local default.
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
export function getRegistryApiBaseUrl() {
|
|
96
|
+
const fromEnv = process.env.UNIWEB_REGISTER_URL
|
|
97
|
+
if (fromEnv) {
|
|
98
|
+
try { return new URL(fromEnv).origin } catch { /* fall through */ }
|
|
99
|
+
}
|
|
100
|
+
const fromCfg = readCliConfig().registryApiUrl
|
|
101
|
+
if (fromCfg) {
|
|
102
|
+
try { return new URL(fromCfg).origin } catch { return fromCfg }
|
|
103
|
+
}
|
|
104
|
+
return 'http://localhost:8080'
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
/**
|
|
86
108
|
* Read workspace package globs.
|
|
87
109
|
* Tries pnpm-workspace.yaml first, falls back to package.json workspaces.
|
|
@@ -154,13 +176,19 @@ export async function readRootPackageJson(rootDir) {
|
|
|
154
176
|
}
|
|
155
177
|
|
|
156
178
|
/**
|
|
157
|
-
* Write root package.json
|
|
179
|
+
* Write root package.json, preserving the file's existing indentation
|
|
180
|
+
* (newly scaffolded workspaces use 2-space; we don't reflow whatever's
|
|
181
|
+
* already there).
|
|
158
182
|
* @param {string} rootDir - Workspace root directory
|
|
159
183
|
* @param {Object} pkg - Package.json object
|
|
160
184
|
*/
|
|
161
185
|
export async function writeRootPackageJson(rootDir, pkg) {
|
|
162
186
|
const pkgPath = join(rootDir, 'package.json')
|
|
163
|
-
|
|
187
|
+
if (existsSync(pkgPath)) {
|
|
188
|
+
await writeJsonPreservingStyleAsync(pkgPath, pkg)
|
|
189
|
+
} else {
|
|
190
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
191
|
+
}
|
|
164
192
|
}
|
|
165
193
|
|
|
166
194
|
/**
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace `@uniweb/*` dependency survey.
|
|
3
|
+
*
|
|
4
|
+
* Compares the `@uniweb/*` + `uniweb` versions *declared* in every
|
|
5
|
+
* package.json across a workspace against the running CLI's bundled
|
|
6
|
+
* version matrix (`getResolvedVersions`). Shared by `uniweb update`
|
|
7
|
+
* (which fixes the drift) and `uniweb doctor` (which only reports it) so
|
|
8
|
+
* the two never disagree about what "out of date" means.
|
|
9
|
+
*
|
|
10
|
+
* Comparison is on declared specs, not installed (node_modules) versions
|
|
11
|
+
* — that's what's committed and what `git diff` will show after a fix.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
import { getResolvedVersions } from '../versions.js'
|
|
17
|
+
import { getWorkspacePackages } from './workspace.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Strip a leading semver range operator (^, ~, >=, <, …) so two specs can
|
|
21
|
+
* be compared by their underlying version. Range expressions like
|
|
22
|
+
* ">=0.5 <0.7" aren't fully parsed — the first version-shaped token wins.
|
|
23
|
+
* Sufficient for `@uniweb/*` deps, which use `^x.y.z` / `x.y.z`.
|
|
24
|
+
* @param {string} spec
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function stripVersionRange(spec) {
|
|
28
|
+
return (spec || '').replace(/^[\^~>=<\s]+/, '').trim().split(/\s+/)[0] || ''
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compare two version specs (range prefix tolerated). Returns 1 / -1 / 0.
|
|
33
|
+
* @param {string} a
|
|
34
|
+
* @param {string} b
|
|
35
|
+
* @returns {number}
|
|
36
|
+
*/
|
|
37
|
+
export function compareSemver(a, b) {
|
|
38
|
+
const pa = stripVersionRange(a).split('.').map(Number)
|
|
39
|
+
const pb = stripVersionRange(b).split('.').map(Number)
|
|
40
|
+
for (let i = 0; i < 3; i++) {
|
|
41
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1
|
|
42
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1
|
|
43
|
+
}
|
|
44
|
+
return 0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {object} DepRow
|
|
49
|
+
* @property {string} relDir Workspace-relative dir, or '(root)'.
|
|
50
|
+
* @property {string} section 'dependencies' | 'devDependencies' | 'peerDependencies'
|
|
51
|
+
* @property {string} name Package name (e.g. '@uniweb/core' or 'uniweb').
|
|
52
|
+
* @property {string} current The spec declared in package.json.
|
|
53
|
+
* @property {string} target The spec the running CLI's matrix wants.
|
|
54
|
+
* @property {'aligned'|'behind'|'ahead'} status current vs target.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Survey a workspace's declared `@uniweb/*` + `uniweb` deps against the
|
|
59
|
+
* running CLI's bundled matrix.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} workspaceDir Absolute path to the workspace root.
|
|
62
|
+
* @returns {Promise<{ targets: Record<string,string>, rows: DepRow[], anyDrift: boolean, anyAhead: boolean }>}
|
|
63
|
+
* `anyDrift` — at least one dep lags the matrix. `anyAhead` — at least
|
|
64
|
+
* one dep is newer than the matrix.
|
|
65
|
+
*/
|
|
66
|
+
export async function surveyWorkspaceDeps(workspaceDir) {
|
|
67
|
+
const targets = getResolvedVersions()
|
|
68
|
+
const packages = await getWorkspacePackages(workspaceDir)
|
|
69
|
+
const dirs = ['', ...packages]
|
|
70
|
+
const rows = []
|
|
71
|
+
let anyDrift = false
|
|
72
|
+
let anyAhead = false
|
|
73
|
+
|
|
74
|
+
for (const relDir of dirs) {
|
|
75
|
+
const pkgDir = relDir ? join(workspaceDir, relDir) : workspaceDir
|
|
76
|
+
const pkgPath = join(pkgDir, 'package.json')
|
|
77
|
+
if (!existsSync(pkgPath)) continue
|
|
78
|
+
let pkg
|
|
79
|
+
try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) } catch { continue }
|
|
80
|
+
|
|
81
|
+
for (const sectionName of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
82
|
+
const section = pkg[sectionName]
|
|
83
|
+
if (!section) continue
|
|
84
|
+
for (const [name, current] of Object.entries(section)) {
|
|
85
|
+
if (!(name.startsWith('@uniweb/') || name === 'uniweb')) continue
|
|
86
|
+
const target = targets[name]
|
|
87
|
+
if (!target) continue
|
|
88
|
+
const cmp = compareSemver(target, current)
|
|
89
|
+
let status
|
|
90
|
+
if (cmp > 0) { status = 'behind'; anyDrift = true }
|
|
91
|
+
else if (cmp < 0) { status = 'ahead'; anyAhead = true }
|
|
92
|
+
else { status = 'aligned' }
|
|
93
|
+
rows.push({ relDir: relDir || '(root)', section: sectionName, name, current, target, status })
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { targets, rows, anyDrift, anyAhead }
|
|
99
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style-preserving JSON file writes.
|
|
3
|
+
*
|
|
4
|
+
* The CLI rewrites `package.json` in several places (`update`, `doctor`,
|
|
5
|
+
* `rename`, `add`). A naive `JSON.stringify(obj, null, 2)` reflows the
|
|
6
|
+
* *entire* file whenever the project happened to use tabs or 4-space
|
|
7
|
+
* indentation — turning a one-key version bump into a hundred-line diff
|
|
8
|
+
* (and a needless merge-conflict surface). `framework/CLAUDE.md` calls
|
|
9
|
+
* this out as an anti-pattern for human commits; the tooling shouldn't do
|
|
10
|
+
* it either. These helpers detect the file's existing indentation and
|
|
11
|
+
* trailing-newline convention and preserve both.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
15
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detect the indentation unit a JSON source string uses.
|
|
19
|
+
* @param {string} src
|
|
20
|
+
* @returns {number|string} A space count, or '\t' for tab-indented files.
|
|
21
|
+
* Defaults to 2 when the file has no indented lines (e.g. `{}`).
|
|
22
|
+
*/
|
|
23
|
+
export function detectJsonIndent(src) {
|
|
24
|
+
const m = src.match(/\n([ \t]+)\S/)
|
|
25
|
+
if (!m) return 2
|
|
26
|
+
const lead = m[1]
|
|
27
|
+
if (lead.includes('\t')) return '\t'
|
|
28
|
+
return lead.length
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Serialize `obj` to JSON using the indentation and trailing-newline
|
|
33
|
+
* convention of `originalSrc` (or of the file currently at `filePath`).
|
|
34
|
+
* @param {object} obj
|
|
35
|
+
* @param {string} originalSrc - The file's current text.
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function stringifyJsonLike(obj, originalSrc) {
|
|
39
|
+
const indent = detectJsonIndent(originalSrc)
|
|
40
|
+
const body = JSON.stringify(obj, null, indent)
|
|
41
|
+
return originalSrc.endsWith('\n') ? body + '\n' : body
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Write `obj` to `filePath` as JSON, preserving the file's existing
|
|
46
|
+
* indentation and trailing-newline convention. Pass `originalSrc` when
|
|
47
|
+
* the caller already has the file contents in hand (avoids a re-read);
|
|
48
|
+
* otherwise the file is read to sniff its style.
|
|
49
|
+
* @param {string} filePath
|
|
50
|
+
* @param {object} obj
|
|
51
|
+
* @param {string|null} [originalSrc]
|
|
52
|
+
*/
|
|
53
|
+
export function writeJsonPreservingStyle(filePath, obj, originalSrc = null) {
|
|
54
|
+
const src = originalSrc ?? readFileSync(filePath, 'utf8')
|
|
55
|
+
writeFileSync(filePath, stringifyJsonLike(obj, src))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Async counterpart to {@link writeJsonPreservingStyle}, for the CLI's
|
|
60
|
+
* many `node:fs/promises`-based call sites.
|
|
61
|
+
* @param {string} filePath
|
|
62
|
+
* @param {object} obj
|
|
63
|
+
* @param {string|null} [originalSrc]
|
|
64
|
+
*/
|
|
65
|
+
export async function writeJsonPreservingStyleAsync(filePath, obj, originalSrc = null) {
|
|
66
|
+
const src = originalSrc ?? await readFile(filePath, 'utf8')
|
|
67
|
+
await writeFile(filePath, stringifyJsonLike(obj, src))
|
|
68
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package placement resolution — shared by `add` and `clone`.
|
|
3
|
+
*
|
|
4
|
+
* Kept dependency-free (no `@uniweb/build`, no fs) on purpose: `clone` imports
|
|
5
|
+
* it and must run from a global install before any project — anything that
|
|
6
|
+
* statically pulls in `@uniweb/build` would crash `npx uniweb@latest clone`
|
|
7
|
+
* (the same reason `utils/workspace.js` loads the classifier lazily).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Foundation placement defaults (folder `src/`, package `src`). */
|
|
11
|
+
export const FOUNDATION_KIND = { defaultDir: 'src', defaultPkg: 'src', projectSub: 'src' }
|
|
12
|
+
|
|
13
|
+
/** Site placement defaults (folder `site/`, package `site`). */
|
|
14
|
+
export const SITE_KIND = { defaultDir: 'site', defaultPkg: 'site', projectSub: 'site' }
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve where a foundation or site should be placed, given the user's input.
|
|
18
|
+
*
|
|
19
|
+
* The rule: **the user names a folder, and we create exactly that folder.**
|
|
20
|
+
* No silent nesting under `foundations/` / `sites/`, no inferring layout from
|
|
21
|
+
* pre-existing globs. The framework doesn't require any particular folder
|
|
22
|
+
* structure (the build classifies packages by their contents, not their
|
|
23
|
+
* location), so the CLI shouldn't impose one.
|
|
24
|
+
*
|
|
25
|
+
* Resolution priority (foundation example, same shape for site):
|
|
26
|
+
*
|
|
27
|
+
* 1. `--path <dir>` → explicit folder. Name is the path's
|
|
28
|
+
* last segment (used as the package
|
|
29
|
+
* name unless `name` was also given).
|
|
30
|
+
* 2. `name` contains `/` → treat as a path (e.g., `foundations/ui`).
|
|
31
|
+
* Folder = the path, package name =
|
|
32
|
+
* the last segment.
|
|
33
|
+
* 3. `name` (no slash) → folder = `<name>/`, package name = `<name>`.
|
|
34
|
+
* 4. `--project <project>` → folder = `<project>/<defaultSub>` and
|
|
35
|
+
* package name = `<project>-<defaultSub>`
|
|
36
|
+
* (the co-located convention; only this
|
|
37
|
+
* one uses the `-src` / `-site` suffix).
|
|
38
|
+
* 5. (no input) → folder = `<defaultDir>/`, package name
|
|
39
|
+
* = `<defaultPkg>` (`src/` + `src`
|
|
40
|
+
* for foundations; `site/` + `site` for
|
|
41
|
+
* sites).
|
|
42
|
+
*
|
|
43
|
+
* @param {string} rootDir
|
|
44
|
+
* @param {string|null} name - Either a bare name or a path-with-slash.
|
|
45
|
+
* @param {{ path?: string, project?: string }} opts
|
|
46
|
+
* @param {{ defaultDir: string, defaultPkg: string, projectSub: string }} kind
|
|
47
|
+
* @returns {{ relativePath: string, packageName: string }}
|
|
48
|
+
*/
|
|
49
|
+
export function resolvePlacement(rootDir, name, opts, kind) {
|
|
50
|
+
// 1. --path is a PARENT directory. The folder is `<path>/<name>` if a
|
|
51
|
+
// name was given, or `<path>` itself if not (the path's last segment
|
|
52
|
+
// is then taken as the package name).
|
|
53
|
+
if (opts.path) {
|
|
54
|
+
const parent = opts.path.replace(/\/+$/, '')
|
|
55
|
+
if (name) {
|
|
56
|
+
const last = name.split('/').filter(Boolean).pop()
|
|
57
|
+
return {
|
|
58
|
+
relativePath: `${parent}/${name}`.replace(/\/+/g, '/'),
|
|
59
|
+
packageName: last,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const lastSegment = parent.split('/').filter(Boolean).pop() || parent
|
|
63
|
+
return {
|
|
64
|
+
relativePath: parent,
|
|
65
|
+
packageName: lastSegment,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 2. name contains a slash → treat as a path.
|
|
70
|
+
if (name && name.includes('/')) {
|
|
71
|
+
const relativePath = name.replace(/\/+$/, '')
|
|
72
|
+
const lastSegment = relativePath.split('/').filter(Boolean).pop()
|
|
73
|
+
return {
|
|
74
|
+
relativePath,
|
|
75
|
+
packageName: lastSegment,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 3. Bare name.
|
|
80
|
+
if (name) {
|
|
81
|
+
return {
|
|
82
|
+
relativePath: name,
|
|
83
|
+
packageName: name,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 4. --project (co-located convention with -src / -site suffix).
|
|
88
|
+
if (opts.project) {
|
|
89
|
+
return {
|
|
90
|
+
relativePath: `${opts.project}/${kind.projectSub}`,
|
|
91
|
+
packageName: `${opts.project}-${kind.projectSub}`,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 5. Default placement.
|
|
96
|
+
return {
|
|
97
|
+
relativePath: kind.defaultDir,
|
|
98
|
+
packageName: kind.defaultPkg,
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/utils/pm.js
CHANGED
|
@@ -40,6 +40,35 @@ export function detectWorkspacePm(workspaceRoot) {
|
|
|
40
40
|
return null
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Detect which package manager owns a *globally installed* `uniweb` CLI
|
|
45
|
+
* binary, by inspecting its install path (`process.argv[1]`). pnpm and
|
|
46
|
+
* yarn keep global packages under recognizable directory segments;
|
|
47
|
+
* everything else is assumed to be npm. Only meaningful when the CLI is
|
|
48
|
+
* actually a global install (see index.js::isGlobalInstall) — a
|
|
49
|
+
* project-local or npx-launched copy is updated differently.
|
|
50
|
+
*
|
|
51
|
+
* @returns {'pnpm' | 'yarn' | 'npm'}
|
|
52
|
+
*/
|
|
53
|
+
export function detectGlobalCliPm() {
|
|
54
|
+
const path = (process.argv[1] || '').toLowerCase().replace(/\\/g, '/')
|
|
55
|
+
if (path.includes('/pnpm/')) return 'pnpm'
|
|
56
|
+
if (path.includes('/yarn/')) return 'yarn'
|
|
57
|
+
return 'npm'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The command to (re)install the latest `uniweb` CLI globally with a
|
|
62
|
+
* given package manager.
|
|
63
|
+
* @param {'pnpm' | 'yarn' | 'npm'} pm
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function globalCliUpdateCmd(pm) {
|
|
67
|
+
if (pm === 'pnpm') return 'pnpm add -g uniweb@latest'
|
|
68
|
+
if (pm === 'yarn') return 'yarn global add uniweb@latest'
|
|
69
|
+
return 'npm i -g uniweb@latest'
|
|
70
|
+
}
|
|
71
|
+
|
|
43
72
|
/**
|
|
44
73
|
* Generate a workspace-filtered command.
|
|
45
74
|
* pnpm: "pnpm --filter site dev"
|