uniweb 0.13.17 → 0.14.0

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.
Files changed (65) hide show
  1. package/README.md +1 -1
  2. package/package.json +7 -7
  3. package/partials/agents.md +51 -0
  4. package/src/backend/client.js +173 -40
  5. package/src/backend/data-bundle.js +15 -3
  6. package/src/backend/foundation-bring-along.js +76 -21
  7. package/src/backend/payment-handoff.js +28 -9
  8. package/src/backend/site-media.js +147 -19
  9. package/src/backend/site-sync.js +362 -40
  10. package/src/commands/add.js +575 -273
  11. package/src/commands/build.js +160 -57
  12. package/src/commands/clone.js +79 -26
  13. package/src/commands/content.js +11 -7
  14. package/src/commands/deploy.js +80 -32
  15. package/src/commands/dev.js +39 -17
  16. package/src/commands/docs.js +44 -16
  17. package/src/commands/doctor.js +338 -82
  18. package/src/commands/export.js +15 -7
  19. package/src/commands/handoff.js +6 -2
  20. package/src/commands/i18n.js +248 -99
  21. package/src/commands/inspect.js +48 -19
  22. package/src/commands/invite.js +9 -3
  23. package/src/commands/org.js +19 -5
  24. package/src/commands/publish.js +229 -60
  25. package/src/commands/pull.js +157 -48
  26. package/src/commands/push.js +144 -42
  27. package/src/commands/refresh.js +37 -10
  28. package/src/commands/register.js +235 -64
  29. package/src/commands/rename.js +126 -46
  30. package/src/commands/runtime.js +74 -24
  31. package/src/commands/status.js +48 -15
  32. package/src/commands/sync.js +7 -2
  33. package/src/commands/template.js +12 -4
  34. package/src/commands/update.js +263 -87
  35. package/src/commands/validate.js +115 -32
  36. package/src/framework-index.json +15 -13
  37. package/src/index.js +298 -145
  38. package/src/templates/fetchers/github.js +7 -7
  39. package/src/templates/fetchers/npm.js +3 -3
  40. package/src/templates/fetchers/release.js +21 -18
  41. package/src/templates/index.js +34 -12
  42. package/src/templates/processor.js +44 -12
  43. package/src/templates/resolver.js +42 -15
  44. package/src/templates/validator.js +14 -7
  45. package/src/utils/asset-upload.js +90 -22
  46. package/src/utils/code-upload.js +62 -37
  47. package/src/utils/config.js +31 -10
  48. package/src/utils/dep-survey.js +33 -7
  49. package/src/utils/destination-prompt.js +27 -17
  50. package/src/utils/git.js +66 -22
  51. package/src/utils/host-prompt.js +4 -3
  52. package/src/utils/install-integrity.js +8 -4
  53. package/src/utils/interactive.js +3 -3
  54. package/src/utils/json-file.js +6 -2
  55. package/src/utils/names.js +15 -6
  56. package/src/utils/placement.js +16 -8
  57. package/src/utils/registry-auth.js +185 -57
  58. package/src/utils/registry-orgs.js +142 -53
  59. package/src/utils/runtime-upload.js +52 -14
  60. package/src/utils/scaffold.js +55 -14
  61. package/src/utils/site-content-refs.js +3 -1
  62. package/src/utils/update-check.js +43 -17
  63. package/src/utils/workspace.js +13 -7
  64. package/src/versions.js +18 -7
  65. package/templates/site/_gitignore +5 -0
package/src/utils/git.js CHANGED
@@ -32,11 +32,20 @@ import yaml from 'js-yaml'
32
32
  * rather than assuming the defaults.
33
33
  */
34
34
  export function siteContentRoots(siteDir) {
35
- const roots = new Set(['site.yml', 'theme.yml', 'head.html', 'collections.yml', 'locales'])
35
+ const roots = new Set([
36
+ 'site.yml',
37
+ 'theme.yml',
38
+ 'head.html',
39
+ 'collections.yml',
40
+ 'locales'
41
+ ])
36
42
  let paths = {}
37
43
  try {
38
- paths = yaml.load(readFileSync(join(siteDir, 'site.yml'), 'utf8'))?.paths || {}
39
- } catch { /* no or unreadable site.yml the defaults are right */ }
44
+ paths =
45
+ yaml.load(readFileSync(join(siteDir, 'site.yml'), 'utf8'))?.paths || {}
46
+ } catch {
47
+ /* no or unreadable site.yml — the defaults are right */
48
+ }
40
49
  roots.add(paths.pages || 'pages')
41
50
  roots.add(paths.layout || 'layout')
42
51
  roots.add(paths.collections || 'collections')
@@ -50,7 +59,11 @@ export function hasUncommittedContent(siteDir) {
50
59
  }
51
60
 
52
61
  function git(args, cwd) {
53
- return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
62
+ return execFileSync('git', args, {
63
+ cwd,
64
+ encoding: 'utf8',
65
+ stdio: ['ignore', 'pipe', 'ignore']
66
+ })
54
67
  }
55
68
 
56
69
  /**
@@ -78,14 +91,19 @@ export function isGitRepo(dir) {
78
91
  export function uncommittedUnder(dir, relPaths) {
79
92
  if (!isGitRepo(dir)) return null
80
93
  try {
81
- const out = git(['status', '--porcelain', '--untracked-files=all', '--', ...relPaths], dir)
82
- return out
83
- .split('\n')
84
- .filter(Boolean)
85
- // porcelain v1: XY<space>path, and a rename is "orig -> new".
86
- .map((line) => line.slice(3).trim())
87
- .map((p) => (p.includes(' -> ') ? p.split(' -> ')[1] : p))
88
- .filter(Boolean)
94
+ const out = git(
95
+ ['status', '--porcelain', '--untracked-files=all', '--', ...relPaths],
96
+ dir
97
+ )
98
+ return (
99
+ out
100
+ .split('\n')
101
+ .filter(Boolean)
102
+ // porcelain v1: XY<space>path, and a rename is "orig -> new".
103
+ .map((line) => line.slice(3).trim())
104
+ .map((p) => (p.includes(' -> ') ? p.split(' -> ')[1] : p))
105
+ .filter(Boolean)
106
+ )
89
107
  } catch {
90
108
  // A path git doesn't know (e.g. none of the roots exist yet) is not an error
91
109
  // worth failing a pull over.
@@ -111,7 +129,7 @@ export function showAtHead(dir, relPath) {
111
129
  return execFileSync('git', ['show', `HEAD:${relPath}`], {
112
130
  cwd: dir,
113
131
  stdio: ['ignore', 'pipe', 'ignore'],
114
- maxBuffer: 64 * 1024 * 1024,
132
+ maxBuffer: 64 * 1024 * 1024
115
133
  })
116
134
  } catch {
117
135
  return null
@@ -135,10 +153,15 @@ export function mergeFile(dir, minePath, basePath, theirsPath, labels = {}) {
135
153
  'git',
136
154
  [
137
155
  'merge-file',
138
- '-L', labels.mine || 'yours (local)',
139
- '-L', labels.base || 'common ancestor',
140
- '-L', labels.theirs || 'theirs (backend)',
141
- minePath, basePath, theirsPath,
156
+ '-L',
157
+ labels.mine || 'yours (local)',
158
+ '-L',
159
+ labels.base || 'common ancestor',
160
+ '-L',
161
+ labels.theirs || 'theirs (backend)',
162
+ minePath,
163
+ basePath,
164
+ theirsPath
142
165
  ],
143
166
  { cwd: dir, stdio: ['ignore', 'ignore', 'ignore'] }
144
167
  )
@@ -174,15 +197,36 @@ export function hasRemote(dir) {
174
197
  * @returns {{ ok: boolean, changed: boolean, message: string }}
175
198
  */
176
199
  export function pullRemote(dir) {
177
- const before = (() => { try { return git(['rev-parse', 'HEAD'], dir).trim() } catch { return null } })()
200
+ const before = (() => {
201
+ try {
202
+ return git(['rev-parse', 'HEAD'], dir).trim()
203
+ } catch {
204
+ return null
205
+ }
206
+ })()
178
207
  try {
179
208
  const out = execFileSync('git', ['pull', '--ff-only'], {
180
- cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
209
+ cwd: dir,
210
+ encoding: 'utf8',
211
+ stdio: ['ignore', 'pipe', 'pipe']
181
212
  })
182
- const after = (() => { try { return git(['rev-parse', 'HEAD'], dir).trim() } catch { return null } })()
183
- return { ok: true, changed: Boolean(before && after && before !== after), message: out.trim() }
213
+ const after = (() => {
214
+ try {
215
+ return git(['rev-parse', 'HEAD'], dir).trim()
216
+ } catch {
217
+ return null
218
+ }
219
+ })()
220
+ return {
221
+ ok: true,
222
+ changed: Boolean(before && after && before !== after),
223
+ message: out.trim()
224
+ }
184
225
  } catch (err) {
185
- const msg = [err?.stderr, err?.stdout].map((b) => (b ? String(b) : '')).join('\n').trim()
226
+ const msg = [err?.stderr, err?.stdout]
227
+ .map((b) => (b ? String(b) : ''))
228
+ .join('\n')
229
+ .trim()
186
230
  return { ok: false, changed: false, message: msg || 'git pull failed' }
187
231
  }
188
232
  }
@@ -38,9 +38,10 @@ export async function promptForHost({ args, preselect = null } = {}) {
38
38
 
39
39
  // promptSelect doesn't expose initial-index, so move the preselect to
40
40
  // the top of the list — the menu still highlights index 0 by default.
41
- const ordered = preselect && adapters.includes(preselect)
42
- ? [preselect, ...adapters.filter(a => a !== preselect)]
43
- : adapters
41
+ const ordered =
42
+ preselect && adapters.includes(preselect)
43
+ ? [preselect, ...adapters.filter((a) => a !== preselect)]
44
+ : adapters
44
45
 
45
46
  const choice = await promptSelect('Pick a host adapter:', ordered)
46
47
  if (!choice) {
@@ -117,7 +117,8 @@ export function checkSiteInstall(site, foundation, workspaceDir) {
117
117
  ` ${rel(foundation.path)} taken when it was installed. Edits you make will not\n` +
118
118
  ` reach the dev server. \`uniweb build\` resolves the foundation by path and reads\n` +
119
119
  ` your source, so builds stay correct — which is why this is easy to miss.`,
120
- remedy: 'Reinstall so it links to the workspace source (e.g. pnpm install)',
120
+ remedy:
121
+ 'Reinstall so it links to the workspace source (e.g. pnpm install)'
121
122
  })
122
123
  } else {
123
124
  // Linked, but possibly at the wrong source.
@@ -130,7 +131,7 @@ export function checkSiteInstall(site, foundation, workspaceDir) {
130
131
  site: site.name,
131
132
  message: `In dev, this site resolves "${foundation.name}" somewhere other than your workspace source.`,
132
133
  detail: ` resolves to: ${rel(target)}\n workspace source: ${rel(expected)}`,
133
- remedy: 'Reinstall, and check the site\'s package.json file: reference',
134
+ remedy: "Reinstall, and check the site's package.json file: reference"
134
135
  })
135
136
  }
136
137
  }
@@ -138,7 +139,10 @@ export function checkSiteInstall(site, foundation, workspaceDir) {
138
139
  // What the site actually reaches, versus what the foundation asks for. These
139
140
  // agree through a link and drift through a copy — the drift is what runs.
140
141
  const foundationPkg = readJson(join(foundation.path, 'package.json'))
141
- const declared = { ...foundationPkg?.dependencies, ...foundationPkg?.peerDependencies }
142
+ const declared = {
143
+ ...foundationPkg?.dependencies,
144
+ ...foundationPkg?.peerDependencies
145
+ }
142
146
 
143
147
  for (const [name, spec] of Object.entries(declared)) {
144
148
  if (!name.startsWith('@uniweb/')) continue
@@ -160,7 +164,7 @@ export function checkSiteInstall(site, foundation, workspaceDir) {
160
164
  ` resolved from ${rel(linkPath)}: ${reached}\n` +
161
165
  ` ${name} is bundled into the foundation, so the dev server runs the version it\n` +
162
166
  ` reaches here. A build reads the workspace tree and may compile a different one.`,
163
- remedy: 'Reinstall so the resolved tree matches what is declared',
167
+ remedy: 'Reinstall so the resolved tree matches what is declared'
164
168
  })
165
169
  }
166
170
  }
@@ -37,7 +37,7 @@ export function getCliPrefix() {
37
37
  * @returns {string[]}
38
38
  */
39
39
  export function stripNonInteractiveFlag(args) {
40
- return args.filter(a => a !== '--non-interactive')
40
+ return args.filter((a) => a !== '--non-interactive')
41
41
  }
42
42
 
43
43
  /**
@@ -46,8 +46,8 @@ export function stripNonInteractiveFlag(args) {
46
46
  * @returns {string}
47
47
  */
48
48
  export function formatOptions(options) {
49
- const maxLen = Math.max(...options.map(o => o.label.length))
49
+ const maxLen = Math.max(...options.map((o) => o.label.length))
50
50
  return options
51
- .map(o => ` ${o.label.padEnd(maxLen + 3)}${o.description}`)
51
+ .map((o) => ` ${o.label.padEnd(maxLen + 3)}${o.description}`)
52
52
  .join('\n')
53
53
  }
@@ -62,7 +62,11 @@ export function writeJsonPreservingStyle(filePath, obj, originalSrc = null) {
62
62
  * @param {object} obj
63
63
  * @param {string|null} [originalSrc]
64
64
  */
65
- export async function writeJsonPreservingStyleAsync(filePath, obj, originalSrc = null) {
66
- const src = originalSrc ?? await readFile(filePath, 'utf8')
65
+ export async function writeJsonPreservingStyleAsync(
66
+ filePath,
67
+ obj,
68
+ originalSrc = null
69
+ ) {
70
+ const src = originalSrc ?? (await readFile(filePath, 'utf8'))
67
71
  await writeFile(filePath, stringifyJsonLike(obj, src))
68
72
  }
@@ -25,9 +25,15 @@ import { join } from 'node:path'
25
25
  * that's a real constraint, not aesthetic.
26
26
  */
27
27
  const RESERVED_NAMES = new Set([
28
- 'default', 'undefined', 'null', 'true', 'false',
29
- 'node_modules', 'package',
30
- 'dist', 'build',
28
+ 'default',
29
+ 'undefined',
30
+ 'null',
31
+ 'true',
32
+ 'false',
33
+ 'node_modules',
34
+ 'package',
35
+ 'dist',
36
+ 'build'
31
37
  ])
32
38
 
33
39
  /**
@@ -38,9 +44,12 @@ const RESERVED_NAMES = new Set([
38
44
  */
39
45
  export function validatePackageName(name, existingNames) {
40
46
  if (!name) return 'Name is required'
41
- if (!/^[a-z0-9-]+$/.test(name)) return 'Use lowercase letters, numbers, and hyphens'
42
- if (RESERVED_NAMES.has(name)) return `"${name}" is a reserved name — choose a different one`
43
- if (existingNames?.has(name)) return `"${name}" already exists in this workspace`
47
+ if (!/^[a-z0-9-]+$/.test(name))
48
+ return 'Use lowercase letters, numbers, and hyphens'
49
+ if (RESERVED_NAMES.has(name))
50
+ return `"${name}" is a reserved name — choose a different one`
51
+ if (existingNames?.has(name))
52
+ return `"${name}" already exists in this workspace`
44
53
  return true
45
54
  }
46
55
 
@@ -8,10 +8,18 @@
8
8
  */
9
9
 
10
10
  /** Foundation placement defaults (folder `src/`, package `src`). */
11
- export const FOUNDATION_KIND = { defaultDir: 'src', defaultPkg: 'src', projectSub: 'src' }
11
+ export const FOUNDATION_KIND = {
12
+ defaultDir: 'src',
13
+ defaultPkg: 'src',
14
+ projectSub: 'src'
15
+ }
12
16
 
13
17
  /** Site placement defaults (folder `site/`, package `site`). */
14
- export const SITE_KIND = { defaultDir: 'site', defaultPkg: 'site', projectSub: 'site' }
18
+ export const SITE_KIND = {
19
+ defaultDir: 'site',
20
+ defaultPkg: 'site',
21
+ projectSub: 'site'
22
+ }
15
23
 
16
24
  /**
17
25
  * Resolve where a foundation or site should be placed, given the user's input.
@@ -56,13 +64,13 @@ export function resolvePlacement(rootDir, name, opts, kind) {
56
64
  const last = name.split('/').filter(Boolean).pop()
57
65
  return {
58
66
  relativePath: `${parent}/${name}`.replace(/\/+/g, '/'),
59
- packageName: last,
67
+ packageName: last
60
68
  }
61
69
  }
62
70
  const lastSegment = parent.split('/').filter(Boolean).pop() || parent
63
71
  return {
64
72
  relativePath: parent,
65
- packageName: lastSegment,
73
+ packageName: lastSegment
66
74
  }
67
75
  }
68
76
 
@@ -72,7 +80,7 @@ export function resolvePlacement(rootDir, name, opts, kind) {
72
80
  const lastSegment = relativePath.split('/').filter(Boolean).pop()
73
81
  return {
74
82
  relativePath,
75
- packageName: lastSegment,
83
+ packageName: lastSegment
76
84
  }
77
85
  }
78
86
 
@@ -80,7 +88,7 @@ export function resolvePlacement(rootDir, name, opts, kind) {
80
88
  if (name) {
81
89
  return {
82
90
  relativePath: name,
83
- packageName: name,
91
+ packageName: name
84
92
  }
85
93
  }
86
94
 
@@ -88,13 +96,13 @@ export function resolvePlacement(rootDir, name, opts, kind) {
88
96
  if (opts.project) {
89
97
  return {
90
98
  relativePath: `${opts.project}/${kind.projectSub}`,
91
- packageName: `${opts.project}-${kind.projectSub}`,
99
+ packageName: `${opts.project}-${kind.projectSub}`
92
100
  }
93
101
  }
94
102
 
95
103
  // 5. Default placement.
96
104
  return {
97
105
  relativePath: kind.defaultDir,
98
- packageName: kind.defaultPkg,
106
+ packageName: kind.defaultPkg
99
107
  }
100
108
  }