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.
- package/README.md +1 -1
- package/package.json +7 -7
- package/partials/agents.md +51 -0
- package/src/backend/client.js +173 -40
- package/src/backend/data-bundle.js +15 -3
- package/src/backend/foundation-bring-along.js +76 -21
- package/src/backend/payment-handoff.js +28 -9
- package/src/backend/site-media.js +147 -19
- package/src/backend/site-sync.js +362 -40
- package/src/commands/add.js +575 -273
- package/src/commands/build.js +160 -57
- package/src/commands/clone.js +79 -26
- package/src/commands/content.js +11 -7
- package/src/commands/deploy.js +80 -32
- package/src/commands/dev.js +39 -17
- package/src/commands/docs.js +44 -16
- package/src/commands/doctor.js +338 -82
- package/src/commands/export.js +15 -7
- package/src/commands/handoff.js +6 -2
- package/src/commands/i18n.js +248 -99
- package/src/commands/inspect.js +48 -19
- package/src/commands/invite.js +9 -3
- package/src/commands/org.js +19 -5
- package/src/commands/publish.js +229 -60
- package/src/commands/pull.js +157 -48
- package/src/commands/push.js +144 -42
- package/src/commands/refresh.js +37 -10
- package/src/commands/register.js +235 -64
- package/src/commands/rename.js +126 -46
- package/src/commands/runtime.js +74 -24
- package/src/commands/status.js +48 -15
- package/src/commands/sync.js +7 -2
- package/src/commands/template.js +12 -4
- package/src/commands/update.js +263 -87
- package/src/commands/validate.js +115 -32
- package/src/framework-index.json +15 -13
- package/src/index.js +298 -145
- package/src/templates/fetchers/github.js +7 -7
- package/src/templates/fetchers/npm.js +3 -3
- package/src/templates/fetchers/release.js +21 -18
- package/src/templates/index.js +34 -12
- package/src/templates/processor.js +44 -12
- package/src/templates/resolver.js +42 -15
- package/src/templates/validator.js +14 -7
- package/src/utils/asset-upload.js +90 -22
- package/src/utils/code-upload.js +62 -37
- package/src/utils/config.js +31 -10
- package/src/utils/dep-survey.js +33 -7
- package/src/utils/destination-prompt.js +27 -17
- package/src/utils/git.js +66 -22
- package/src/utils/host-prompt.js +4 -3
- package/src/utils/install-integrity.js +8 -4
- package/src/utils/interactive.js +3 -3
- package/src/utils/json-file.js +6 -2
- package/src/utils/names.js +15 -6
- package/src/utils/placement.js +16 -8
- package/src/utils/registry-auth.js +185 -57
- package/src/utils/registry-orgs.js +142 -53
- package/src/utils/runtime-upload.js +52 -14
- package/src/utils/scaffold.js +55 -14
- package/src/utils/site-content-refs.js +3 -1
- package/src/utils/update-check.js +43 -17
- package/src/utils/workspace.js +13 -7
- package/src/versions.js +18 -7
- package/templates/site/_gitignore +5 -0
|
@@ -34,7 +34,9 @@ const HANDLE_RE = /^[a-z0-9][a-z0-9-]{1,37}[a-z0-9]$/
|
|
|
34
34
|
|
|
35
35
|
/** Strip a leading `@` and any `/suffix`, returning the bare handle segment. */
|
|
36
36
|
export function bareHandle(scope) {
|
|
37
|
-
return String(scope || '')
|
|
37
|
+
return String(scope || '')
|
|
38
|
+
.replace(/^@/, '')
|
|
39
|
+
.replace(/\/.*$/, '')
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
|
@@ -57,14 +59,17 @@ export function validateHandle(handle) {
|
|
|
57
59
|
*/
|
|
58
60
|
export async function fetchOrgs({ apiBase, token }) {
|
|
59
61
|
const res = await fetch(`${apiBase.replace(/\/$/, '')}${ORGS_PATH}`, {
|
|
60
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
62
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
61
63
|
})
|
|
62
|
-
if (!res.ok)
|
|
64
|
+
if (!res.ok)
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Could not list your orgs: HTTP ${res.status} ${res.statusText}`
|
|
67
|
+
)
|
|
63
68
|
const data = await res.json().catch(() => null)
|
|
64
69
|
return {
|
|
65
70
|
account_handle: data?.account_handle ?? null,
|
|
66
71
|
personal_org_exists: data?.personal_org_exists === true,
|
|
67
|
-
orgs: Array.isArray(data?.orgs) ? data.orgs : []
|
|
72
|
+
orgs: Array.isArray(data?.orgs) ? data.orgs : []
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
|
|
@@ -82,8 +87,11 @@ export async function createOrg({ apiBase, token, handle }) {
|
|
|
82
87
|
const h = bareHandle(handle)
|
|
83
88
|
const res = await fetch(`${apiBase.replace(/\/$/, '')}${ORGS_PATH}`, {
|
|
84
89
|
method: 'POST',
|
|
85
|
-
headers: {
|
|
86
|
-
|
|
90
|
+
headers: {
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
Authorization: `Bearer ${token}`
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify({ handle: h })
|
|
87
95
|
})
|
|
88
96
|
if (res.ok) return res.json()
|
|
89
97
|
|
|
@@ -93,11 +101,15 @@ export async function createOrg({ apiBase, token, handle }) {
|
|
|
93
101
|
try {
|
|
94
102
|
const body = await res.json()
|
|
95
103
|
detail = body?.detail || ''
|
|
96
|
-
} catch {
|
|
104
|
+
} catch {
|
|
105
|
+
/* non-JSON body — keep the generic line */
|
|
106
|
+
}
|
|
97
107
|
const fallback =
|
|
98
|
-
res.status === 409
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
res.status === 409
|
|
109
|
+
? `@${h} is not available.`
|
|
110
|
+
: res.status === 422
|
|
111
|
+
? `@${h} is not a valid handle.`
|
|
112
|
+
: `Could not create @${h}: HTTP ${res.status}`
|
|
101
113
|
const e = new Error(detail || fallback)
|
|
102
114
|
e.status = res.status
|
|
103
115
|
throw e
|
|
@@ -118,10 +130,17 @@ export async function createOrg({ apiBase, token, handle }) {
|
|
|
118
130
|
* @param {string[]} [p.args] - argv slice; checked for --non-interactive
|
|
119
131
|
* @returns {Promise<string|null>}
|
|
120
132
|
*/
|
|
121
|
-
export async function deriveScope({
|
|
133
|
+
export async function deriveScope({
|
|
134
|
+
apiBase,
|
|
135
|
+
token,
|
|
136
|
+
accountHandle = null,
|
|
137
|
+
args = []
|
|
138
|
+
}) {
|
|
122
139
|
const envelope = await fetchOrgs({ apiBase, token })
|
|
123
140
|
const { orgs } = envelope
|
|
124
|
-
const personal =
|
|
141
|
+
const personal =
|
|
142
|
+
envelope.account_handle ||
|
|
143
|
+
(accountHandle ? bareHandle(accountHandle) : null)
|
|
125
144
|
const personalOrgExists = envelope.personal_org_exists
|
|
126
145
|
const { isNonInteractive } = await import('./interactive.js')
|
|
127
146
|
const nonInteractive = isNonInteractive(args)
|
|
@@ -131,15 +150,30 @@ export async function deriveScope({ apiBase, token, accountHandle = null, args =
|
|
|
131
150
|
const h = orgs[0].handle
|
|
132
151
|
const label = isPersonal(h) ? `your personal org @${h}` : `your org @${h}`
|
|
133
152
|
if (nonInteractive) {
|
|
134
|
-
console.log(
|
|
153
|
+
console.log(
|
|
154
|
+
`Publishing under ${label.replace(`@${h}`, `\x1b[1m@${h}\x1b[0m`)}.`
|
|
155
|
+
)
|
|
135
156
|
return h
|
|
136
157
|
}
|
|
137
158
|
const prompts = (await import('prompts')).default
|
|
138
|
-
const { ok } = await prompts(
|
|
139
|
-
|
|
140
|
-
|
|
159
|
+
const { ok } = await prompts(
|
|
160
|
+
{
|
|
161
|
+
type: 'confirm',
|
|
162
|
+
name: 'ok',
|
|
163
|
+
message: `Publish under ${label}?`,
|
|
164
|
+
initial: true
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
onCancel: () => {
|
|
168
|
+
console.log('\nCancelled.')
|
|
169
|
+
process.exit(0)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
)
|
|
141
173
|
if (!ok) {
|
|
142
|
-
console.log(
|
|
174
|
+
console.log(
|
|
175
|
+
'Pass --scope @org, or create another with `uniweb org create <handle>`.'
|
|
176
|
+
)
|
|
143
177
|
return null
|
|
144
178
|
}
|
|
145
179
|
return h
|
|
@@ -147,32 +181,54 @@ export async function deriveScope({ apiBase, token, accountHandle = null, args =
|
|
|
147
181
|
|
|
148
182
|
if (orgs.length > 1) {
|
|
149
183
|
// Personal org first; the rest in server order (primary-first).
|
|
150
|
-
const ordered = [...orgs].sort(
|
|
184
|
+
const ordered = [...orgs].sort(
|
|
185
|
+
(a, b) => (isPersonal(b.handle) ? 1 : 0) - (isPersonal(a.handle) ? 1 : 0)
|
|
186
|
+
)
|
|
151
187
|
if (nonInteractive) {
|
|
152
|
-
const pick =
|
|
153
|
-
|
|
188
|
+
const pick =
|
|
189
|
+
ordered.find((u) => isPersonal(u.handle)) ||
|
|
190
|
+
orgs.find((u) => u.is_primary) ||
|
|
191
|
+
orgs[0]
|
|
192
|
+
console.log(
|
|
193
|
+
`Multiple orgs; using \x1b[1m@${pick.handle}\x1b[0m (non-interactive).`
|
|
194
|
+
)
|
|
154
195
|
return pick.handle
|
|
155
196
|
}
|
|
156
197
|
const prompts = (await import('prompts')).default
|
|
157
|
-
const { choice } = await prompts(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
198
|
+
const { choice } = await prompts(
|
|
199
|
+
{
|
|
200
|
+
type: 'select',
|
|
201
|
+
name: 'choice',
|
|
202
|
+
message: 'Publish under which org?',
|
|
203
|
+
choices: ordered.map((u) => ({
|
|
204
|
+
title: `@${u.handle}${isPersonal(u.handle) ? ' — your personal org' : u.is_primary ? ' (primary)' : ''}`,
|
|
205
|
+
value: u.handle
|
|
206
|
+
})),
|
|
207
|
+
initial: 0
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
onCancel: () => {
|
|
211
|
+
console.log('\nCancelled.')
|
|
212
|
+
process.exit(0)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
)
|
|
167
216
|
return choice || null
|
|
168
217
|
}
|
|
169
218
|
|
|
170
219
|
// 0 orgs → the first-publish picker.
|
|
171
220
|
if (nonInteractive) {
|
|
172
|
-
console.error(
|
|
221
|
+
console.error(
|
|
222
|
+
'\x1b[31m✗\x1b[0m You have no org to publish under. Create one with `uniweb org create <handle>`, or pass --scope @org.'
|
|
223
|
+
)
|
|
173
224
|
process.exit(1)
|
|
174
225
|
}
|
|
175
|
-
return offerCreateOrg({
|
|
226
|
+
return offerCreateOrg({
|
|
227
|
+
apiBase,
|
|
228
|
+
token,
|
|
229
|
+
accountHandle: personal,
|
|
230
|
+
personalOrgExists
|
|
231
|
+
})
|
|
176
232
|
}
|
|
177
233
|
|
|
178
234
|
/**
|
|
@@ -192,14 +248,22 @@ export async function deriveScope({ apiBase, token, accountHandle = null, args =
|
|
|
192
248
|
* (created-then-left) → the lazy claim would 409; don't offer it.
|
|
193
249
|
* Returns the chosen bare handle, or null if cancelled/failed.
|
|
194
250
|
*/
|
|
195
|
-
export async function offerCreateOrg({
|
|
251
|
+
export async function offerCreateOrg({
|
|
252
|
+
apiBase,
|
|
253
|
+
token,
|
|
254
|
+
accountHandle = null,
|
|
255
|
+
personalOrgExists = false
|
|
256
|
+
}) {
|
|
196
257
|
const prompts = (await import('prompts')).default
|
|
197
|
-
const personal =
|
|
258
|
+
const personal =
|
|
259
|
+
accountHandle && !validateHandle(accountHandle)
|
|
260
|
+
? bareHandle(accountHandle)
|
|
261
|
+
: null
|
|
198
262
|
|
|
199
263
|
if (!personal) {
|
|
200
264
|
console.error(
|
|
201
|
-
|
|
202
|
-
|
|
265
|
+
"\x1b[31m✗\x1b[0m This account has no handle (service accounts don't get one), so there is no ready-to-go org.\n" +
|
|
266
|
+
' Log in with a personal account or set a handle in the app — or pass --scope @org / `uniweb org create <handle>`.'
|
|
203
267
|
)
|
|
204
268
|
return null
|
|
205
269
|
}
|
|
@@ -207,38 +271,63 @@ export async function offerCreateOrg({ apiBase, token, accountHandle = null, per
|
|
|
207
271
|
const canClaimPersonal = !personalOrgExists
|
|
208
272
|
const choices = [
|
|
209
273
|
...(canClaimPersonal
|
|
210
|
-
? [
|
|
274
|
+
? [
|
|
275
|
+
{
|
|
276
|
+
title: `@${personal} — your personal org (created on first publish)`,
|
|
277
|
+
value: personal
|
|
278
|
+
}
|
|
279
|
+
]
|
|
211
280
|
: []),
|
|
212
|
-
{ title: 'A new organization…', value: ':new' }
|
|
281
|
+
{ title: 'A new organization…', value: ':new' }
|
|
213
282
|
]
|
|
214
283
|
if (!canClaimPersonal) {
|
|
215
|
-
console.log(
|
|
284
|
+
console.log(
|
|
285
|
+
`\x1b[2m@${personal} exists but you're not a member of it — ask its admin, or create another org.\x1b[0m`
|
|
286
|
+
)
|
|
216
287
|
}
|
|
217
288
|
|
|
218
|
-
const { choice } = await prompts(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
289
|
+
const { choice } = await prompts(
|
|
290
|
+
{
|
|
291
|
+
type: 'select',
|
|
292
|
+
name: 'choice',
|
|
293
|
+
message: 'Publish under which org?',
|
|
294
|
+
choices,
|
|
295
|
+
initial: 0
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
onCancel: () => {
|
|
299
|
+
console.log('\nCancelled.')
|
|
300
|
+
process.exit(0)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
)
|
|
225
304
|
if (!choice) return null
|
|
226
305
|
|
|
227
306
|
let handle = choice
|
|
228
307
|
if (choice === ':new') {
|
|
229
|
-
const answer = await prompts(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
308
|
+
const answer = await prompts(
|
|
309
|
+
{
|
|
310
|
+
type: 'text',
|
|
311
|
+
name: 'handle',
|
|
312
|
+
message: 'Org handle (e.g. acme):',
|
|
313
|
+
validate: (v) => validateHandle(v) || true
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
onCancel: () => {
|
|
317
|
+
console.log('\nCancelled.')
|
|
318
|
+
process.exit(0)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
)
|
|
235
322
|
if (!answer.handle) return null
|
|
236
323
|
handle = answer.handle
|
|
237
324
|
}
|
|
238
325
|
|
|
239
326
|
try {
|
|
240
327
|
const org = await createOrg({ apiBase, token, handle })
|
|
241
|
-
console.log(
|
|
328
|
+
console.log(
|
|
329
|
+
`\x1b[32m✓\x1b[0m Created \x1b[1m@${org.handle}\x1b[0m — you're a member${org.is_primary ? ' (primary)' : ''}.`
|
|
330
|
+
)
|
|
242
331
|
return org.handle
|
|
243
332
|
} catch (err) {
|
|
244
333
|
console.error(`\x1b[31m✗\x1b[0m ${err.message}`)
|
|
@@ -51,7 +51,7 @@ function fileEntry(diskPath, path) {
|
|
|
51
51
|
content_type: contentTypeFor(path),
|
|
52
52
|
size: bytes.length,
|
|
53
53
|
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
54
|
-
diskPath
|
|
54
|
+
diskPath
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -75,7 +75,8 @@ export function collectRuntimeFiles(distDir) {
|
|
|
75
75
|
const rel = prefix ? `${prefix}/${name}` : name
|
|
76
76
|
const st = statSync(full)
|
|
77
77
|
if (st.isDirectory()) walk(full, rel)
|
|
78
|
-
else if (st.isFile() && !rel.endsWith('.map'))
|
|
78
|
+
else if (st.isFile() && !rel.endsWith('.map'))
|
|
79
|
+
files.push(fileEntry(full, rel))
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
walk(appDir, '')
|
|
@@ -111,22 +112,40 @@ export function hasShims(files) {
|
|
|
111
112
|
* @param {object} opts - { apiBase, token, version, distDir, files?, onProgress? }
|
|
112
113
|
* @returns {Promise<{ mode, uploaded: string[], failed: Array, serveBase: string|null }>}
|
|
113
114
|
*/
|
|
114
|
-
export async function uploadRuntime({
|
|
115
|
+
export async function uploadRuntime({
|
|
116
|
+
apiBase,
|
|
117
|
+
token,
|
|
118
|
+
version,
|
|
119
|
+
distDir,
|
|
120
|
+
files,
|
|
121
|
+
onProgress = () => {}
|
|
122
|
+
}) {
|
|
115
123
|
const list = files || collectRuntimeFiles(distDir)
|
|
116
|
-
if (!list.length)
|
|
124
|
+
if (!list.length)
|
|
125
|
+
return { mode: 'none', uploaded: [], failed: [], serveBase: null }
|
|
117
126
|
|
|
118
127
|
const origin = apiBase.replace(/\/$/, '')
|
|
119
128
|
const planRes = await fetch(`${origin}/dev/runtime`, {
|
|
120
129
|
method: 'POST',
|
|
121
|
-
headers: {
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'application/json',
|
|
132
|
+
Authorization: `Bearer ${token}`
|
|
133
|
+
},
|
|
122
134
|
body: JSON.stringify({
|
|
123
135
|
version,
|
|
124
|
-
files: list.map(({ path, content_type, size, sha256 }) => ({
|
|
125
|
-
|
|
136
|
+
files: list.map(({ path, content_type, size, sha256 }) => ({
|
|
137
|
+
path,
|
|
138
|
+
content_type,
|
|
139
|
+
size,
|
|
140
|
+
sha256
|
|
141
|
+
}))
|
|
142
|
+
})
|
|
126
143
|
})
|
|
127
144
|
if (!planRes.ok) {
|
|
128
145
|
const detail = await planRes.text().catch(() => '')
|
|
129
|
-
const err = new Error(
|
|
146
|
+
const err = new Error(
|
|
147
|
+
`runtime plan rejected: HTTP ${planRes.status}${detail ? ` — ${detail.slice(0, 300)}` : ''}`
|
|
148
|
+
)
|
|
130
149
|
err.status = planRes.status
|
|
131
150
|
throw err
|
|
132
151
|
}
|
|
@@ -134,14 +153,19 @@ export async function uploadRuntime({ apiBase, token, version, distDir, files, o
|
|
|
134
153
|
const targets = new Map((plan.uploads || []).map((u) => [u.path, u]))
|
|
135
154
|
// The one mode-aware bit: direct PUTs are bearer-authed backend routes;
|
|
136
155
|
// presigned URLs are self-authorizing and must NOT carry a foreign bearer.
|
|
137
|
-
const authHeaders =
|
|
156
|
+
const authHeaders =
|
|
157
|
+
plan.mode === 'presigned' ? {} : { Authorization: `Bearer ${token}` }
|
|
138
158
|
|
|
139
159
|
const uploaded = []
|
|
140
160
|
const failed = []
|
|
141
161
|
for (const f of list) {
|
|
142
162
|
const target = targets.get(f.path)
|
|
143
163
|
if (!target) {
|
|
144
|
-
failed.push({
|
|
164
|
+
failed.push({
|
|
165
|
+
path: f.path,
|
|
166
|
+
status: 0,
|
|
167
|
+
detail: 'no upload target in plan'
|
|
168
|
+
})
|
|
145
169
|
continue
|
|
146
170
|
}
|
|
147
171
|
onProgress(`↑ ${f.path}`)
|
|
@@ -149,15 +173,29 @@ export async function uploadRuntime({ apiBase, token, version, distDir, files, o
|
|
|
149
173
|
try {
|
|
150
174
|
res = await fetch(new URL(target.url, origin), {
|
|
151
175
|
method: target.method || 'PUT',
|
|
152
|
-
headers: {
|
|
153
|
-
|
|
176
|
+
headers: {
|
|
177
|
+
...(target.headers || {}),
|
|
178
|
+
...authHeaders,
|
|
179
|
+
'x-uniweb-sha256': f.sha256
|
|
180
|
+
},
|
|
181
|
+
body: readFileSync(f.diskPath)
|
|
154
182
|
})
|
|
155
183
|
} catch (err) {
|
|
156
184
|
failed.push({ path: f.path, status: 0, detail: err.message })
|
|
157
185
|
continue
|
|
158
186
|
}
|
|
159
187
|
if (res.ok) uploaded.push(f.path)
|
|
160
|
-
else
|
|
188
|
+
else
|
|
189
|
+
failed.push({
|
|
190
|
+
path: f.path,
|
|
191
|
+
status: res.status,
|
|
192
|
+
detail: (await res.text().catch(() => '')).slice(0, 200)
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
mode: plan.mode || 'direct',
|
|
197
|
+
uploaded,
|
|
198
|
+
failed,
|
|
199
|
+
serveBase: plan.serve_base || null
|
|
161
200
|
}
|
|
162
|
-
return { mode: plan.mode || 'direct', uploaded, failed, serveBase: plan.serve_base || null }
|
|
163
201
|
}
|
package/src/utils/scaffold.js
CHANGED
|
@@ -11,7 +11,11 @@ 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 {
|
|
14
|
+
import {
|
|
15
|
+
copyTemplateDirectory,
|
|
16
|
+
enumerateTemplateOutputs,
|
|
17
|
+
registerVersions
|
|
18
|
+
} from '../templates/processor.js'
|
|
15
19
|
import { getVersionsForTemplates, getCliVersion } from '../versions.js'
|
|
16
20
|
import { writeJsonPreservingStyleAsync } from './json-file.js'
|
|
17
21
|
|
|
@@ -45,7 +49,7 @@ export async function scaffoldWorkspace(targetDir, context, options = {}) {
|
|
|
45
49
|
await copyTemplateDirectory(templatePath, targetDir, context, {
|
|
46
50
|
onProgress: options.onProgress,
|
|
47
51
|
onWarning: options.onWarning,
|
|
48
|
-
skip
|
|
52
|
+
skip
|
|
49
53
|
})
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -87,7 +91,7 @@ export async function scaffoldFoundation(targetDir, context, options = {}) {
|
|
|
87
91
|
const templatePath = join(TEMPLATES_DIR, 'foundation')
|
|
88
92
|
await copyTemplateDirectory(templatePath, targetDir, context, {
|
|
89
93
|
onProgress: options.onProgress,
|
|
90
|
-
onWarning: options.onWarning
|
|
94
|
+
onWarning: options.onWarning
|
|
91
95
|
})
|
|
92
96
|
}
|
|
93
97
|
|
|
@@ -120,7 +124,7 @@ export async function scaffoldSite(targetDir, context, options = {}) {
|
|
|
120
124
|
const templatePath = join(TEMPLATES_DIR, 'site')
|
|
121
125
|
await copyTemplateDirectory(templatePath, targetDir, context, {
|
|
122
126
|
onProgress: options.onProgress,
|
|
123
|
-
onWarning: options.onWarning
|
|
127
|
+
onWarning: options.onWarning
|
|
124
128
|
})
|
|
125
129
|
}
|
|
126
130
|
|
|
@@ -146,7 +150,12 @@ export async function scaffoldSite(targetDir, context, options = {}) {
|
|
|
146
150
|
* directories. Used to migrate legacy `foundation/foundation.js`
|
|
147
151
|
* templates onto the new flat `src/main.js` layout.
|
|
148
152
|
*/
|
|
149
|
-
export async function applyContent(
|
|
153
|
+
export async function applyContent(
|
|
154
|
+
contentDir,
|
|
155
|
+
targetDir,
|
|
156
|
+
context,
|
|
157
|
+
options = {}
|
|
158
|
+
) {
|
|
150
159
|
if (!existsSync(contentDir)) return
|
|
151
160
|
|
|
152
161
|
registerVersions(getVersionsForTemplates())
|
|
@@ -157,16 +166,24 @@ export async function applyContent(contentDir, targetDir, context, options = {})
|
|
|
157
166
|
'vite.config.js',
|
|
158
167
|
'entry.js',
|
|
159
168
|
'index.html',
|
|
160
|
-
'.gitignore'
|
|
169
|
+
'.gitignore'
|
|
161
170
|
])
|
|
162
171
|
|
|
163
172
|
// Config files that should be merged, not overwritten.
|
|
164
173
|
// Keys listed here are preserved from the scaffolded version.
|
|
165
174
|
const MERGE_FILES = {
|
|
166
|
-
'site.yml': ['name', 'foundation']
|
|
175
|
+
'site.yml': ['name', 'foundation']
|
|
167
176
|
}
|
|
168
177
|
|
|
169
|
-
await copyContentRecursive(
|
|
178
|
+
await copyContentRecursive(
|
|
179
|
+
contentDir,
|
|
180
|
+
targetDir,
|
|
181
|
+
context,
|
|
182
|
+
STRUCTURAL_FILES,
|
|
183
|
+
MERGE_FILES,
|
|
184
|
+
options,
|
|
185
|
+
0
|
|
186
|
+
)
|
|
170
187
|
}
|
|
171
188
|
|
|
172
189
|
/**
|
|
@@ -178,7 +195,15 @@ export async function applyContent(contentDir, targetDir, context, options = {})
|
|
|
178
195
|
* nested `sections/foo/foundation.js` if one existed, which is not the
|
|
179
196
|
* intent.
|
|
180
197
|
*/
|
|
181
|
-
async function copyContentRecursive(
|
|
198
|
+
async function copyContentRecursive(
|
|
199
|
+
sourceDir,
|
|
200
|
+
targetDir,
|
|
201
|
+
context,
|
|
202
|
+
structuralFiles,
|
|
203
|
+
mergeFiles,
|
|
204
|
+
options,
|
|
205
|
+
depth = 0
|
|
206
|
+
) {
|
|
182
207
|
await fs.mkdir(targetDir, { recursive: true })
|
|
183
208
|
|
|
184
209
|
const entries = readdirSync(sourceDir, { withFileTypes: true })
|
|
@@ -189,7 +214,15 @@ async function copyContentRecursive(sourceDir, targetDir, context, structuralFil
|
|
|
189
214
|
|
|
190
215
|
if (entry.isDirectory()) {
|
|
191
216
|
const targetSubDir = join(targetDir, entry.name)
|
|
192
|
-
await copyContentRecursive(
|
|
217
|
+
await copyContentRecursive(
|
|
218
|
+
sourcePath,
|
|
219
|
+
targetSubDir,
|
|
220
|
+
context,
|
|
221
|
+
structuralFiles,
|
|
222
|
+
mergeFiles,
|
|
223
|
+
options,
|
|
224
|
+
depth + 1
|
|
225
|
+
)
|
|
193
226
|
} else {
|
|
194
227
|
// Determine the output filename (strip .hbs extension)
|
|
195
228
|
let outputName = entry.name.endsWith('.hbs')
|
|
@@ -197,7 +230,10 @@ async function copyContentRecursive(sourceDir, targetDir, context, structuralFil
|
|
|
197
230
|
: entry.name
|
|
198
231
|
|
|
199
232
|
// Apply top-level rename (e.g. legacy `foundation.js` → `main.js`)
|
|
200
|
-
if (
|
|
233
|
+
if (
|
|
234
|
+
renames &&
|
|
235
|
+
Object.prototype.hasOwnProperty.call(renames, outputName)
|
|
236
|
+
) {
|
|
201
237
|
outputName = renames[outputName]
|
|
202
238
|
}
|
|
203
239
|
|
|
@@ -236,7 +272,7 @@ async function copyContentRecursive(sourceDir, targetDir, context, structuralFil
|
|
|
236
272
|
if (preserveKeys && existsSync(targetPath)) {
|
|
237
273
|
const existingContent = await fs.readFile(targetPath, 'utf-8')
|
|
238
274
|
const existing = yaml.load(existingContent) || {}
|
|
239
|
-
let merged = newContent ?? await fs.readFile(sourcePath, 'utf-8')
|
|
275
|
+
let merged = newContent ?? (await fs.readFile(sourcePath, 'utf-8'))
|
|
240
276
|
|
|
241
277
|
for (const key of preserveKeys) {
|
|
242
278
|
if (existing[key] === undefined) continue
|
|
@@ -292,7 +328,12 @@ export async function applyStarter(projectDir, context, options = {}) {
|
|
|
292
328
|
// Apply foundation starter content
|
|
293
329
|
const foundationContentDir = join(STARTER_DIR, 'foundation')
|
|
294
330
|
const foundationTargetDir = join(projectDir, foundationDir)
|
|
295
|
-
await applyContent(
|
|
331
|
+
await applyContent(
|
|
332
|
+
foundationContentDir,
|
|
333
|
+
foundationTargetDir,
|
|
334
|
+
context,
|
|
335
|
+
options
|
|
336
|
+
)
|
|
296
337
|
|
|
297
338
|
// Apply site starter content
|
|
298
339
|
const siteContentDir = join(STARTER_DIR, 'site')
|
|
@@ -328,7 +369,7 @@ function matchTopLevelLine(content, key) {
|
|
|
328
369
|
function replaceTopLevelLine(content, key, replacement) {
|
|
329
370
|
return content.replace(
|
|
330
371
|
new RegExp(`^${escapeRegex(key)}:.*$`, 'm'),
|
|
331
|
-
() => replacement
|
|
372
|
+
() => replacement
|
|
332
373
|
)
|
|
333
374
|
}
|
|
334
375
|
|
|
@@ -17,5 +17,7 @@
|
|
|
17
17
|
* site.yml; the runtime loads it as a federated module.
|
|
18
18
|
*/
|
|
19
19
|
export function extractFoundationRef(info = {}, document = {}) {
|
|
20
|
-
return
|
|
20
|
+
return (
|
|
21
|
+
info?.foundation ?? info?.foundation_name ?? document?.foundation ?? null
|
|
22
|
+
)
|
|
21
23
|
}
|