uniweb 0.11.0 → 0.12.1
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 +20 -18
- package/package.json +6 -6
- package/partials/agents.md +163 -39
- package/src/commands/add.js +154 -237
- package/src/commands/build.js +14 -42
- package/src/commands/deploy.js +262 -3
- package/src/commands/docs.js +5 -6
- package/src/commands/doctor.js +21 -22
- package/src/commands/publish.js +255 -34
- package/src/framework-index.json +7 -7
- package/src/index.js +32 -15
- package/src/templates/validator.js +52 -4
- package/src/utils/auth.js +82 -6
- package/src/utils/names.js +9 -2
- package/src/utils/registry.js +88 -16
- package/src/utils/scaffold.js +67 -10
- package/src/utils/workspace.js +8 -46
- package/starter/site/pages/home/1-welcome.md.hbs +1 -1
- package/templates/foundation/{src/foundation.js.hbs → main.js.hbs} +1 -1
- package/templates/foundation/package.json.hbs +6 -6
- package/templates/foundation/{src/styles.css → styles.css} +1 -1
- package/templates/site/index.html.hbs +1 -1
- package/templates/site/theme.yml +1 -1
- package/templates/workspace/README.md.hbs +9 -7
- /package/starter/foundation/{src/foundation.js → main.js} +0 -0
- /package/starter/foundation/{src/sections → sections}/Section/index.jsx +0 -0
- /package/starter/foundation/{src/sections → sections}/Section/meta.js +0 -0
- /package/templates/foundation/{src/components → components}/.gitkeep +0 -0
- /package/templates/foundation/{src/sections → sections}/.gitkeep +0 -0
- /package/templates/site/{main.js → entry.js} +0 -0
package/src/commands/publish.js
CHANGED
|
@@ -12,14 +12,15 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync } from 'node:fs'
|
|
15
|
-
import { readFile } from 'node:fs/promises'
|
|
15
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
16
16
|
import { resolve, join } from 'node:path'
|
|
17
17
|
import { execSync } from 'node:child_process'
|
|
18
18
|
|
|
19
|
+
import { resolveFoundationSrcPath, classifyPackage } from '@uniweb/build'
|
|
19
20
|
import { createLocalRegistry, RemoteRegistry } from '../utils/registry.js'
|
|
20
21
|
import { ensureAuth, readAuth } from '../utils/auth.js'
|
|
21
22
|
import { getRegistryUrl } from '../utils/config.js'
|
|
22
|
-
import { findWorkspaceRoot, findFoundations, findSites,
|
|
23
|
+
import { findWorkspaceRoot, findFoundations, findSites, promptSelect } from '../utils/workspace.js'
|
|
23
24
|
import { isNonInteractive, getCliPrefix } from '../utils/interactive.js'
|
|
24
25
|
|
|
25
26
|
// Colors for terminal output
|
|
@@ -62,7 +63,7 @@ async function resolveFoundationDir(args) {
|
|
|
62
63
|
const prefix = getCliPrefix()
|
|
63
64
|
|
|
64
65
|
// Check if current directory is a foundation
|
|
65
|
-
const type =
|
|
66
|
+
const type = classifyPackage(cwd)
|
|
66
67
|
if (type === 'foundation') {
|
|
67
68
|
return cwd
|
|
68
69
|
}
|
|
@@ -128,6 +129,21 @@ function parseNamespace(args) {
|
|
|
128
129
|
return args[idx + 1]
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Parse --name <id> from args.
|
|
134
|
+
* The publish-time "id" — the bare-name segment in the registry name.
|
|
135
|
+
* Distinct from `package.json::name` (a workspace concern). Persisted
|
|
136
|
+
* to `package.json::uniweb.id` after the first successful publish so
|
|
137
|
+
* it doesn't need to be supplied again.
|
|
138
|
+
* @param {string[]} args
|
|
139
|
+
* @returns {string|null}
|
|
140
|
+
*/
|
|
141
|
+
function parseName(args) {
|
|
142
|
+
const idx = args.indexOf('--name')
|
|
143
|
+
if (idx === -1 || !args[idx + 1]) return null
|
|
144
|
+
return args[idx + 1]
|
|
145
|
+
}
|
|
146
|
+
|
|
131
147
|
/**
|
|
132
148
|
* Parse --edit-access <policy> from args.
|
|
133
149
|
* @param {string[]} args
|
|
@@ -150,16 +166,24 @@ function parseEditAccess(args) {
|
|
|
150
166
|
export async function publish(args = []) {
|
|
151
167
|
const isLocal = args.includes('--local')
|
|
152
168
|
const isDryRun = args.includes('--dry-run')
|
|
169
|
+
// --propagate opts the new version into the registry's version-update
|
|
170
|
+
// walk: trusting sites whose policy permits the jump pick it up
|
|
171
|
+
// automatically via gated rollout. Without --propagate (default
|
|
172
|
+
// 'silent'), the artifact is stored but no site moves until republish
|
|
173
|
+
// or manual refresh.
|
|
174
|
+
const isPropagate = args.includes('--propagate')
|
|
153
175
|
const registryUrl = parseRegistryUrl(args)
|
|
154
176
|
const editAccess = parseEditAccess(args)
|
|
155
177
|
const namespaceFlag = parseNamespace(args)
|
|
178
|
+
const nameFlag = parseName(args)
|
|
156
179
|
|
|
157
180
|
// 1. Resolve foundation directory
|
|
158
181
|
const foundationDir = await resolveFoundationDir(args)
|
|
159
182
|
|
|
160
|
-
// Verify it's actually a foundation (
|
|
161
|
-
|
|
162
|
-
|
|
183
|
+
// Verify it's actually a foundation (canonical classifier checks
|
|
184
|
+
// package.json::main, then main.js, then legacy foundation.js).
|
|
185
|
+
if (classifyPackage(foundationDir) !== 'foundation') {
|
|
186
|
+
error(`Not a foundation directory: ${foundationDir}`)
|
|
163
187
|
process.exit(1)
|
|
164
188
|
}
|
|
165
189
|
|
|
@@ -201,45 +225,206 @@ export async function publish(args = []) {
|
|
|
201
225
|
process.exit(1)
|
|
202
226
|
}
|
|
203
227
|
|
|
204
|
-
// 3b. Resolve
|
|
205
|
-
|
|
228
|
+
// 3b. Resolve scope and foundation id.
|
|
229
|
+
//
|
|
230
|
+
// The publish-time identity is two pieces: a SCOPE (org `@`, personal `~`,
|
|
231
|
+
// or empty → server-resolved personal) and an ID (the bare name segment).
|
|
232
|
+
// They live in different places and get different defaults.
|
|
233
|
+
//
|
|
234
|
+
// Scope priority:
|
|
235
|
+
// 1. --namespace <handle> CLI flag → forces `@<handle>` org scope
|
|
236
|
+
// 2. Sigil in `package.json::name`:
|
|
237
|
+
// - `@org/x` → `@org`
|
|
238
|
+
// - `~user/x` → `~user` (personal alias)
|
|
239
|
+
// 3. `package.json::uniweb.namespace` → legacy explicit org field
|
|
240
|
+
// 4. (none) → empty scope; server attaches
|
|
241
|
+
// the publisher's personal
|
|
242
|
+
// scope at upload time
|
|
243
|
+
//
|
|
244
|
+
// ID priority (the bare name segment):
|
|
245
|
+
// 1. --name <id> CLI flag → override
|
|
246
|
+
// 2. Sigil-stripped `package.json::name` → @org/<id> or ~user/<id>
|
|
247
|
+
// 3. `package.json::uniweb.id` → persisted publish-id
|
|
248
|
+
// 4. Interactive prompt → and write back to
|
|
249
|
+
// `package.json::uniweb.id`
|
|
250
|
+
// so future publishes don't
|
|
251
|
+
// re-prompt.
|
|
252
|
+
// 5. Non-interactive without a usable id → fail with guidance.
|
|
253
|
+
//
|
|
254
|
+
// Note: a bare `package.json::name` (e.g. the scaffold default `src`)
|
|
255
|
+
// is intentionally NOT used as a fallback id. The workspace name is for
|
|
256
|
+
// pnpm linking and the file: dependency in site/package.json — using it
|
|
257
|
+
// as the publish id would couple the registry identity to the workspace,
|
|
258
|
+
// exactly what `uniweb.id` exists to prevent. Users who want their
|
|
259
|
+
// workspace name to be the publish id pass `--name <pkg-name>` once;
|
|
260
|
+
// it persists.
|
|
261
|
+
//
|
|
262
|
+
// Why two storage locations for an ID? `package.json::name` is a
|
|
263
|
+
// workspace concern — pnpm uses it to link packages, sites reference
|
|
264
|
+
// it via `file:` deps and `site.yml::foundation`. Renaming it cascades
|
|
265
|
+
// through several files. `uniweb.id` is publish-only — changing it
|
|
266
|
+
// affects only the registry identity, never the workspace. Most users
|
|
267
|
+
// benefit from leaving `package.json::name` as the scaffold default
|
|
268
|
+
// (`src`) and putting the published-as id in `uniweb.id`.
|
|
269
|
+
const pkgPath = join(foundationDir, 'package.json')
|
|
270
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
206
271
|
const uniwebNamespace = pkg.uniweb?.namespace
|
|
207
|
-
const
|
|
208
|
-
const
|
|
272
|
+
const uniwebId = pkg.uniweb?.id
|
|
273
|
+
const orgScopeMatch = (pkg.name || '').match(/^@([a-z0-9_-]+)\/([a-z0-9_-]+)$/)
|
|
274
|
+
const personalScopeMatch = (pkg.name || '').match(/^~([a-z0-9_-]+)\/([a-z0-9_-]+)$/)
|
|
275
|
+
|
|
276
|
+
// Resolve the SCOPE.
|
|
277
|
+
let scopeSigil = null
|
|
278
|
+
let scopeName = null
|
|
279
|
+
if (namespaceFlag) {
|
|
280
|
+
scopeSigil = '@'
|
|
281
|
+
scopeName = namespaceFlag
|
|
282
|
+
} else if (orgScopeMatch) {
|
|
283
|
+
scopeSigil = '@'
|
|
284
|
+
scopeName = orgScopeMatch[1]
|
|
285
|
+
} else if (personalScopeMatch) {
|
|
286
|
+
scopeSigil = '~'
|
|
287
|
+
scopeName = personalScopeMatch[1]
|
|
288
|
+
} else if (uniwebNamespace) {
|
|
289
|
+
scopeSigil = '@'
|
|
290
|
+
scopeName = uniwebNamespace
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Resolve the ID.
|
|
294
|
+
const ID_RE = /^[a-z0-9_-]+$/
|
|
295
|
+
let foundationName = null
|
|
296
|
+
let writeBackId = false
|
|
297
|
+
if (nameFlag) {
|
|
298
|
+
foundationName = nameFlag
|
|
299
|
+
// Persist the flag's value when it differs from what's already in
|
|
300
|
+
// `uniweb.id`. This makes rename a one-shot:
|
|
301
|
+
// $ uniweb publish --name new-name
|
|
302
|
+
// From here on, `uniweb publish` (no flag) keeps using `new-name`.
|
|
303
|
+
// No-op when --name matches the existing id.
|
|
304
|
+
if (nameFlag !== uniwebId) writeBackId = true
|
|
305
|
+
} else if (orgScopeMatch) {
|
|
306
|
+
foundationName = orgScopeMatch[2]
|
|
307
|
+
} else if (personalScopeMatch) {
|
|
308
|
+
foundationName = personalScopeMatch[2]
|
|
309
|
+
} else if (uniwebId) {
|
|
310
|
+
foundationName = uniwebId
|
|
311
|
+
}
|
|
312
|
+
if (!foundationName) {
|
|
313
|
+
// No id resolvable from any field. Prompt — or fail in non-interactive.
|
|
314
|
+
if (isNonInteractive(process.argv)) {
|
|
315
|
+
error('Foundation id is required for publishing.')
|
|
316
|
+
console.log('')
|
|
317
|
+
console.log(` ${colors.dim}Use one of:${colors.reset}`)
|
|
318
|
+
console.log(` ${colors.cyan}uniweb publish --name <id>${colors.reset}`)
|
|
319
|
+
console.log(` ${colors.dim}Add ${colors.reset}"uniweb": { "id": "<your-id>" }${colors.dim} to package.json${colors.reset}`)
|
|
320
|
+
console.log(` ${colors.dim}Or use a scoped name in package.json: ${colors.reset}"name": "@org/<id>"${colors.reset}`)
|
|
321
|
+
process.exit(1)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const prompts = (await import('prompts')).default
|
|
325
|
+
// Default suggestion: derive from the workspace folder or pkg.name.
|
|
326
|
+
// Strip the suffix `-src` so a foundation in `marketing-src/` defaults
|
|
327
|
+
// to `marketing` as its publish id.
|
|
328
|
+
const workspaceRoot = findWorkspaceRoot(foundationDir) || foundationDir
|
|
329
|
+
const folderName = workspaceRoot === foundationDir
|
|
330
|
+
? null
|
|
331
|
+
: foundationDir.replace(workspaceRoot + '/', '').split('/')[0]
|
|
332
|
+
const suggestion =
|
|
333
|
+
(typeof pkg.name === 'string' && ID_RE.test(pkg.name) ? pkg.name : null) ||
|
|
334
|
+
(folderName ? folderName.replace(/-src$/, '') : null) ||
|
|
335
|
+
'foundation'
|
|
209
336
|
|
|
210
|
-
if (!namespace) {
|
|
211
|
-
error('Namespace is required for publishing.')
|
|
212
337
|
console.log('')
|
|
213
|
-
console.log(
|
|
214
|
-
console.log(
|
|
215
|
-
|
|
216
|
-
|
|
338
|
+
console.log(`${colors.dim}This is the first publish of this foundation. Pick a name${colors.reset}`)
|
|
339
|
+
console.log(`${colors.dim}for the registry — what your foundation will be known as.${colors.reset}`)
|
|
340
|
+
const response = await prompts({
|
|
341
|
+
type: 'text',
|
|
342
|
+
name: 'id',
|
|
343
|
+
message: 'Foundation name',
|
|
344
|
+
initial: suggestion,
|
|
345
|
+
validate: (v) => {
|
|
346
|
+
if (!v) return 'Required'
|
|
347
|
+
if (!ID_RE.test(v)) return 'Lowercase letters, digits, hyphens, underscores only'
|
|
348
|
+
return true
|
|
349
|
+
},
|
|
350
|
+
}, {
|
|
351
|
+
onCancel: () => {
|
|
352
|
+
console.log('')
|
|
353
|
+
console.log('Publish cancelled.')
|
|
354
|
+
process.exit(0)
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
if (!response.id) process.exit(0)
|
|
358
|
+
foundationName = response.id
|
|
359
|
+
writeBackId = true
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Validate the resolved id (may have come from any source).
|
|
363
|
+
if (!ID_RE.test(foundationName)) {
|
|
364
|
+
error(`Invalid foundation name: "${foundationName}"`)
|
|
365
|
+
console.log(` ${colors.dim}Names must be lowercase letters, digits, hyphens, or underscores.${colors.reset}`)
|
|
217
366
|
process.exit(1)
|
|
218
367
|
}
|
|
219
368
|
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
369
|
+
// Persist the id so future publishes don't re-prompt.
|
|
370
|
+
if (writeBackId) {
|
|
371
|
+
pkg.uniweb = pkg.uniweb || {}
|
|
372
|
+
pkg.uniweb.id = foundationName
|
|
373
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
374
|
+
info(`Wrote ${colors.cyan}uniweb.id: "${foundationName}"${colors.reset} to ${colors.dim}package.json${colors.reset}`)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// The registry name. Three cases:
|
|
378
|
+
//
|
|
379
|
+
// 1. Explicit scope (`@org/x` or `~user/x`) → `<sigil><name>/<base>`.
|
|
380
|
+
// 2. Empty-scope, --local → synthesize a
|
|
381
|
+
// personal-scope form `~<loginName-or-sub-or-'me'>/<base>` so the
|
|
382
|
+
// local index mirrors what production will write. This is the
|
|
383
|
+
// local mock's stand-in for the server-side memberId resolution.
|
|
384
|
+
// 3. Empty-scope, remote → send the bare
|
|
385
|
+
// name. The Worker attaches the personal scope server-side
|
|
386
|
+
// (anchoring to the `sub` claim), and the publish response
|
|
387
|
+
// carries the canonical URL back to the CLI for the receipt.
|
|
388
|
+
let name
|
|
389
|
+
if (scopeSigil) {
|
|
390
|
+
name = `${scopeSigil}${scopeName}/${foundationName}`
|
|
391
|
+
} else if (isLocal) {
|
|
392
|
+
const localAuth = await readAuth()
|
|
393
|
+
const personalSeed = localAuth?.loginName || localAuth?.sub || 'me'
|
|
394
|
+
name = `~${personalSeed}/${foundationName}`
|
|
395
|
+
} else {
|
|
396
|
+
name = foundationName
|
|
397
|
+
}
|
|
223
398
|
|
|
224
|
-
// 3c. Advisory
|
|
399
|
+
// 3c. Advisory scope authorization (Worker enforces — this is for early UX feedback)
|
|
225
400
|
if (!isLocal) {
|
|
226
401
|
const auth = await readAuth()
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
402
|
+
if (scopeSigil === '@') {
|
|
403
|
+
// Org scope: must be in the user's namespaces[] claim.
|
|
404
|
+
const namespaces = auth?.namespaces
|
|
405
|
+
if (Array.isArray(namespaces) && !namespaces.includes(scopeName)) {
|
|
406
|
+
error(`You don't have publish access to namespace "${colors.bright}@${scopeName}${colors.reset}"`)
|
|
407
|
+
if (namespaces.length > 0) {
|
|
408
|
+
console.log(` ${colors.dim}Your organizations: ${namespaces.map(n => '@' + n).join(', ')}${colors.reset}`)
|
|
409
|
+
console.log(` ${colors.dim}Or remove the scope from package.json::name to publish under your personal scope.${colors.reset}`)
|
|
410
|
+
} else {
|
|
411
|
+
console.log(` ${colors.dim}You don't belong to any organizations.${colors.reset}`)
|
|
412
|
+
console.log(` ${colors.dim}Use a bare name in package.json (e.g. "src") to publish under your personal scope.${colors.reset}`)
|
|
238
413
|
}
|
|
239
|
-
|
|
240
|
-
|
|
414
|
+
process.exit(1)
|
|
415
|
+
}
|
|
416
|
+
} else if (scopeSigil === '~') {
|
|
417
|
+
// Personal alias scope: must match the user's loginName claim
|
|
418
|
+
// (until handle-aliasing ships, this is loginName-only).
|
|
419
|
+
if (auth?.loginName && auth.loginName !== scopeName) {
|
|
420
|
+
error(`Personal scope "${colors.bright}~${scopeName}${colors.reset}" doesn't match your account`)
|
|
421
|
+
console.log(` ${colors.dim}Your personal scope: ~${auth.loginName}${colors.reset}`)
|
|
422
|
+
console.log(` ${colors.dim}Or remove the scope from package.json::name to publish under your personal scope.${colors.reset}`)
|
|
423
|
+
process.exit(1)
|
|
241
424
|
}
|
|
242
425
|
}
|
|
426
|
+
// Empty-scope: no client-side check. The server resolves to the
|
|
427
|
+
// memberId from the JWT (sub claim) and writes ownership accordingly.
|
|
243
428
|
}
|
|
244
429
|
|
|
245
430
|
// 4. Create registry (local or remote)
|
|
@@ -288,13 +473,15 @@ export async function publish(args = []) {
|
|
|
288
473
|
const auth = isLocal ? null : await readAuth()
|
|
289
474
|
const publishMetadata = {
|
|
290
475
|
publishedBy: auth?.email || (isLocal ? 'local' : 'cli'),
|
|
476
|
+
classification: isPropagate ? 'propagate' : 'silent',
|
|
291
477
|
}
|
|
292
478
|
if (editAccess) {
|
|
293
479
|
publishMetadata.editAccess = editAccess
|
|
294
480
|
}
|
|
295
481
|
|
|
482
|
+
let publishResult
|
|
296
483
|
try {
|
|
297
|
-
await registry.publish(name, version, distDir, publishMetadata)
|
|
484
|
+
publishResult = await registry.publish(name, version, distDir, publishMetadata)
|
|
298
485
|
} catch (err) {
|
|
299
486
|
if (err.code === 'CONFLICT') {
|
|
300
487
|
error(`${colors.bright}${name}@${version}${colors.reset} already exists on the registry.`)
|
|
@@ -309,6 +496,24 @@ export async function publish(args = []) {
|
|
|
309
496
|
throw err
|
|
310
497
|
}
|
|
311
498
|
|
|
499
|
+
// Local event memory — read by `uniweb deploy` to decide whether a
|
|
500
|
+
// workspace-local foundation needs republishing. Lives under dist/ which
|
|
501
|
+
// is gitignored; not part of the upload.
|
|
502
|
+
const receiptUrl = publishResult?.url
|
|
503
|
+
|| (isLocal
|
|
504
|
+
? `file://${registry.getPackagePath(name, version)}/`
|
|
505
|
+
: `${registry.apiUrl}/${name}/${version}/`)
|
|
506
|
+
const { gitSha, gitDirty } = readGitState(foundationDir)
|
|
507
|
+
const receipt = {
|
|
508
|
+
schemaVersion: 1,
|
|
509
|
+
publishedFromGitSha: gitSha,
|
|
510
|
+
publishedFromGitDirty: gitDirty,
|
|
511
|
+
url: receiptUrl,
|
|
512
|
+
publishedAt: new Date().toISOString(),
|
|
513
|
+
classification: isPropagate ? 'propagate' : 'silent',
|
|
514
|
+
}
|
|
515
|
+
await writeFile(join(distDir, 'publish.json'), JSON.stringify(receipt, null, 2) + '\n')
|
|
516
|
+
|
|
312
517
|
const prefix = getCliPrefix()
|
|
313
518
|
const isExtension = schema._self?.role === 'extension'
|
|
314
519
|
console.log('')
|
|
@@ -351,4 +556,20 @@ function bumpPatch(version) {
|
|
|
351
556
|
return parts.join('.')
|
|
352
557
|
}
|
|
353
558
|
|
|
559
|
+
function readGitState(dir) {
|
|
560
|
+
try {
|
|
561
|
+
const sha = execSync('git rev-parse HEAD', {
|
|
562
|
+
cwd: dir,
|
|
563
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
564
|
+
}).toString().trim()
|
|
565
|
+
const status = execSync('git status --porcelain', {
|
|
566
|
+
cwd: dir,
|
|
567
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
568
|
+
}).toString()
|
|
569
|
+
return { gitSha: sha || null, gitDirty: status.length > 0 }
|
|
570
|
+
} catch {
|
|
571
|
+
return { gitSha: null, gitDirty: false }
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
354
575
|
export default publish
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-04-
|
|
3
|
+
"generatedAt": "2026-04-29T13:42:04.684Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.13.1",
|
|
7
7
|
"path": "framework/build",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/content-reader",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"deps": []
|
|
25
25
|
},
|
|
26
26
|
"@uniweb/core": {
|
|
27
|
-
"version": "0.7.
|
|
27
|
+
"version": "0.7.9",
|
|
28
28
|
"path": "framework/core",
|
|
29
29
|
"deps": [
|
|
30
30
|
"@uniweb/semantic-parser",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"deps": []
|
|
43
43
|
},
|
|
44
44
|
"@uniweb/kit": {
|
|
45
|
-
"version": "0.9.
|
|
45
|
+
"version": "0.9.9",
|
|
46
46
|
"path": "framework/kit",
|
|
47
47
|
"deps": [
|
|
48
48
|
"@uniweb/core"
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"deps": []
|
|
60
60
|
},
|
|
61
61
|
"@uniweb/runtime": {
|
|
62
|
-
"version": "0.8.
|
|
62
|
+
"version": "0.8.10",
|
|
63
63
|
"path": "framework/runtime",
|
|
64
64
|
"deps": [
|
|
65
65
|
"@uniweb/core",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"deps": []
|
|
78
78
|
},
|
|
79
79
|
"@uniweb/semantic-parser": {
|
|
80
|
-
"version": "1.1.
|
|
80
|
+
"version": "1.1.16",
|
|
81
81
|
"path": "framework/semantic-parser",
|
|
82
82
|
"deps": []
|
|
83
83
|
},
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"deps": []
|
|
93
93
|
},
|
|
94
94
|
"@uniweb/unipress": {
|
|
95
|
-
"version": "0.
|
|
95
|
+
"version": "0.4.0",
|
|
96
96
|
"path": "framework/unipress",
|
|
97
97
|
"deps": [
|
|
98
98
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -201,7 +201,7 @@ async function createFromPackageTemplates(projectDir, projectName, options = {})
|
|
|
201
201
|
// 1. Scaffold workspace
|
|
202
202
|
await scaffoldWorkspace(projectDir, {
|
|
203
203
|
projectName,
|
|
204
|
-
workspaceGlobs: ['
|
|
204
|
+
workspaceGlobs: ['site', 'src'],
|
|
205
205
|
scripts: {
|
|
206
206
|
dev: filterCmd(pm, 'site', 'dev'),
|
|
207
207
|
build: 'uniweb build',
|
|
@@ -209,10 +209,13 @@ async function createFromPackageTemplates(projectDir, projectName, options = {})
|
|
|
209
209
|
},
|
|
210
210
|
}, { onProgress, onWarning })
|
|
211
211
|
|
|
212
|
-
// 2. Scaffold foundation
|
|
212
|
+
// 2. Scaffold foundation (folder: src/, package name: src)
|
|
213
|
+
// The folder name 'src' carries the meaning — a foundation is the site's
|
|
214
|
+
// source code. The package name 'src' keeps it unique within the
|
|
215
|
+
// workspace, since 'site' is taken by the site package.
|
|
213
216
|
onProgress?.('Creating foundation...')
|
|
214
|
-
await scaffoldFoundation(join(projectDir, '
|
|
215
|
-
name: '
|
|
217
|
+
await scaffoldFoundation(join(projectDir, 'src'), {
|
|
218
|
+
name: 'src',
|
|
216
219
|
projectName,
|
|
217
220
|
isExtension: false,
|
|
218
221
|
}, { onProgress, onWarning })
|
|
@@ -222,8 +225,9 @@ async function createFromPackageTemplates(projectDir, projectName, options = {})
|
|
|
222
225
|
await scaffoldSite(join(projectDir, 'site'), {
|
|
223
226
|
name: 'site',
|
|
224
227
|
projectName,
|
|
225
|
-
foundationName: '
|
|
226
|
-
foundationPath: 'file:../
|
|
228
|
+
foundationName: 'src',
|
|
229
|
+
foundationPath: 'file:../src',
|
|
230
|
+
foundationRef: 'src',
|
|
227
231
|
}, { onProgress, onWarning })
|
|
228
232
|
|
|
229
233
|
// 4. Apply starter content (unless creating a "none" project)
|
|
@@ -264,9 +268,12 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
|
|
|
264
268
|
const { onProgress, onWarning, pm = 'pnpm' } = options
|
|
265
269
|
|
|
266
270
|
// Determine packages to create
|
|
271
|
+
// Default single-foundation single-site project uses package names
|
|
272
|
+
// 'src' (folder: src/) and 'site' (folder: site/). The folder
|
|
273
|
+
// convention is set in computePlacement() below.
|
|
267
274
|
const packages = metadata.packages || [
|
|
268
|
-
{ type: 'foundation', name: '
|
|
269
|
-
{ type: 'site', name: 'site', foundation: '
|
|
275
|
+
{ type: 'foundation', name: 'src' },
|
|
276
|
+
{ type: 'site', name: 'site', foundation: 'src' },
|
|
270
277
|
]
|
|
271
278
|
|
|
272
279
|
// Compute placement for each package
|
|
@@ -314,21 +321,25 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
|
|
|
314
321
|
}, { onProgress, onWarning })
|
|
315
322
|
} else if (pkg.type === 'site') {
|
|
316
323
|
// Find the foundation this site wires to
|
|
317
|
-
const foundationName = pkg.foundation || '
|
|
324
|
+
const foundationName = pkg.foundation || 'src'
|
|
318
325
|
const foundationPkg = placed.find(p =>
|
|
319
326
|
(p.type === 'foundation') && (p.name === foundationName)
|
|
320
327
|
)
|
|
321
328
|
const foundationPath = foundationPkg
|
|
322
329
|
? computeFoundationFilePath(pkg.relativePath, foundationPkg.relativePath)
|
|
323
|
-
: 'file:../
|
|
330
|
+
: 'file:../src'
|
|
324
331
|
|
|
325
332
|
onProgress?.(`Creating site: ${pkg.name}...`)
|
|
333
|
+
// Always write `foundation: <name>` to site.yml — the value is
|
|
334
|
+
// never the implicit default in the new layout (the build's
|
|
335
|
+
// `detectFoundationType` defaults to 'foundation' when absent,
|
|
336
|
+
// which doesn't match 'src').
|
|
326
337
|
await scaffoldSite(fullPath, {
|
|
327
338
|
name: pkg.name,
|
|
328
339
|
projectName,
|
|
329
340
|
foundationName,
|
|
330
341
|
foundationPath,
|
|
331
|
-
foundationRef: foundationName
|
|
342
|
+
foundationRef: foundationName,
|
|
332
343
|
}, { onProgress, onWarning })
|
|
333
344
|
}
|
|
334
345
|
|
|
@@ -336,7 +347,11 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
|
|
|
336
347
|
const contentDir = findContentDirFor(metadata.contentDirs, pkg)
|
|
337
348
|
if (contentDir) {
|
|
338
349
|
onProgress?.(`Applying ${metadata.name} content to ${pkg.name}...`)
|
|
339
|
-
await applyContent(contentDir.dir, fullPath, { projectName }, {
|
|
350
|
+
await applyContent(contentDir.dir, fullPath, { projectName }, {
|
|
351
|
+
onProgress,
|
|
352
|
+
onWarning,
|
|
353
|
+
renames: contentDir.renames,
|
|
354
|
+
})
|
|
340
355
|
}
|
|
341
356
|
|
|
342
357
|
// Merge template dependencies into package.json
|
|
@@ -355,7 +370,9 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
|
|
|
355
370
|
* Compute placement (relative paths) for packages
|
|
356
371
|
*
|
|
357
372
|
* Rules:
|
|
358
|
-
* - 1 foundation
|
|
373
|
+
* - 1 foundation → src/ (folder name is 'src' regardless of package name;
|
|
374
|
+
* the package name is typically 'src' or
|
|
375
|
+
* whatever the template declared)
|
|
359
376
|
* - Multiple foundations → foundations/{name}/
|
|
360
377
|
* - Extensions → extensions/{name}/
|
|
361
378
|
* - 1 site named "site" → site/
|
|
@@ -369,8 +386,8 @@ function computePlacement(packages) {
|
|
|
369
386
|
const placed = []
|
|
370
387
|
|
|
371
388
|
for (const f of foundations) {
|
|
372
|
-
if (foundations.length === 1
|
|
373
|
-
placed.push({ ...f, relativePath: '
|
|
389
|
+
if (foundations.length === 1) {
|
|
390
|
+
placed.push({ ...f, relativePath: 'src' })
|
|
374
391
|
} else {
|
|
375
392
|
placed.push({ ...f, relativePath: `foundations/${f.name}` })
|
|
376
393
|
}
|
|
@@ -175,7 +175,7 @@ export async function validateTemplate(templateRoot, options = {}) {
|
|
|
175
175
|
*
|
|
176
176
|
* @param {string} templateRoot - Root of the template (contains template.json)
|
|
177
177
|
* @param {Object} metadata - Parsed template.json
|
|
178
|
-
* @returns {Array<Object>} Content directories: [{ type, name, dir, foundation? }]
|
|
178
|
+
* @returns {Array<Object>} Content directories: [{ type, name, dir, foundation?, renames? }]
|
|
179
179
|
*/
|
|
180
180
|
export function resolveContentDirs(templateRoot, metadata) {
|
|
181
181
|
const dirs = []
|
|
@@ -185,19 +185,25 @@ export function resolveContentDirs(templateRoot, metadata) {
|
|
|
185
185
|
for (const pkg of metadata.packages) {
|
|
186
186
|
const dir = path.join(templateRoot, pkg.name)
|
|
187
187
|
if (existsSync(dir)) {
|
|
188
|
-
|
|
188
|
+
const entry = {
|
|
189
189
|
type: pkg.type,
|
|
190
190
|
name: pkg.name,
|
|
191
191
|
dir,
|
|
192
192
|
...(pkg.foundation ? { foundation: pkg.foundation } : {}),
|
|
193
|
-
}
|
|
193
|
+
}
|
|
194
|
+
if (entry.type === 'foundation' || entry.type === 'extension') {
|
|
195
|
+
applyLegacyFoundationLayout(entry)
|
|
196
|
+
}
|
|
197
|
+
dirs.push(entry)
|
|
194
198
|
}
|
|
195
199
|
}
|
|
196
200
|
} else {
|
|
197
201
|
// Standard template: look for foundation/ and site/
|
|
198
202
|
const foundationDir = path.join(templateRoot, 'foundation')
|
|
199
203
|
if (existsSync(foundationDir)) {
|
|
200
|
-
|
|
204
|
+
const entry = { type: 'foundation', name: 'foundation', dir: foundationDir }
|
|
205
|
+
applyLegacyFoundationLayout(entry)
|
|
206
|
+
dirs.push(entry)
|
|
201
207
|
}
|
|
202
208
|
|
|
203
209
|
const siteDir = path.join(templateRoot, 'site')
|
|
@@ -209,6 +215,48 @@ export function resolveContentDirs(templateRoot, metadata) {
|
|
|
209
215
|
return dirs
|
|
210
216
|
}
|
|
211
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Detect and unwrap the legacy foundation layout.
|
|
220
|
+
*
|
|
221
|
+
* Old templates (published in the `uniweb/templates` releases up to
|
|
222
|
+
* v0.7.x) shipped foundation content nested one level deeper, with the
|
|
223
|
+
* package source under `foundation/src/` and the user-authored
|
|
224
|
+
* declarations file named `foundation.js`:
|
|
225
|
+
*
|
|
226
|
+
* <template>/foundation/src/foundation.js
|
|
227
|
+
* <template>/foundation/src/sections/...
|
|
228
|
+
* <template>/foundation/src/components/...
|
|
229
|
+
*
|
|
230
|
+
* The current layout is flat — the foundation package root contains
|
|
231
|
+
* the source directly, and the declarations file is named `main.js`:
|
|
232
|
+
*
|
|
233
|
+
* <template>/foundation/main.js
|
|
234
|
+
* <template>/foundation/sections/...
|
|
235
|
+
*
|
|
236
|
+
* The CLI scaffolds the new flat shape into the project's `src/`
|
|
237
|
+
* directory, so an unmodified copy of an old-format template would
|
|
238
|
+
* land at `src/src/foundation.js` (extra `src/` layer + old name).
|
|
239
|
+
*
|
|
240
|
+
* This helper detects the legacy marker (`<dir>/src/foundation.js`),
|
|
241
|
+
* mutates the contentDir entry to point at the inner `src/` directory,
|
|
242
|
+
* and records a top-level rename so `foundation.js` is written as
|
|
243
|
+
* `main.js`. Once `uniweb/templates` is republished with the flat
|
|
244
|
+
* layout, this branch becomes a no-op.
|
|
245
|
+
*/
|
|
246
|
+
function applyLegacyFoundationLayout(entry) {
|
|
247
|
+
const innerSrc = path.join(entry.dir, 'src')
|
|
248
|
+
if (!existsSync(innerSrc)) return
|
|
249
|
+
|
|
250
|
+
const legacyMain = path.join(innerSrc, 'foundation.js')
|
|
251
|
+
const newMain = path.join(innerSrc, 'main.js')
|
|
252
|
+
if (!existsSync(legacyMain) && !existsSync(newMain)) return
|
|
253
|
+
|
|
254
|
+
entry.dir = innerSrc
|
|
255
|
+
if (existsSync(legacyMain)) {
|
|
256
|
+
entry.renames = { ...(entry.renames || {}), 'foundation.js': 'main.js' }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
212
260
|
/**
|
|
213
261
|
* Get list of available templates in a templates directory
|
|
214
262
|
*
|