uniweb 0.12.4 → 0.12.5
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/package.json +3 -3
- package/src/commands/deploy.js +25 -2
- package/src/commands/publish.js +217 -12
- package/src/framework-index.json +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.5",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
+
"@uniweb/runtime": "0.8.10",
|
|
44
45
|
"@uniweb/core": "0.7.9",
|
|
45
|
-
"@uniweb/kit": "0.9.9"
|
|
46
|
-
"@uniweb/runtime": "0.8.10"
|
|
46
|
+
"@uniweb/kit": "0.9.9"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@uniweb/build": "0.13.3",
|
package/src/commands/deploy.js
CHANGED
|
@@ -166,13 +166,36 @@ const say = {
|
|
|
166
166
|
dim: (m) => console.log(` ${c.dim}${m}${c.reset}`),
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Read the git state for `dir`, scoped to that directory's history and
|
|
171
|
+
* working tree — NOT the whole repo's HEAD.
|
|
172
|
+
*
|
|
173
|
+
* `gitSha` : last commit that touched `dir` (`git log -1 -- .`).
|
|
174
|
+
* `gitDirty`: uncommitted changes inside `dir` only (`git status -- .`).
|
|
175
|
+
*
|
|
176
|
+
* Why scope it. In a multi-package monorepo, `git rev-parse HEAD` is
|
|
177
|
+
* the same value for every directory — the repo's current HEAD. That
|
|
178
|
+
* meant editing a SITE then deploying triggered the foundation's
|
|
179
|
+
* staleness check (its receipt's recorded sha didn't match the new
|
|
180
|
+
* repo HEAD), even though the foundation source was unchanged. The
|
|
181
|
+
* receipt's `publishedFromGitSha` field is per-foundation by design;
|
|
182
|
+
* the comparison side has to be too.
|
|
183
|
+
*
|
|
184
|
+
* If the path is outside a git repo, or has no commits touching it
|
|
185
|
+
* yet, the function returns `{ gitSha: null, gitDirty: false }` —
|
|
186
|
+
* same fallback shape as before.
|
|
187
|
+
*/
|
|
169
188
|
function readGitState(dir) {
|
|
170
189
|
try {
|
|
171
|
-
|
|
190
|
+
// `git log -1 --format=%H -- .` returns the SHA of the last
|
|
191
|
+
// commit that touched the cwd path. If no such commit exists
|
|
192
|
+
// yet (path was never committed), output is empty — caller
|
|
193
|
+
// treats null as "no published-from-sha to compare against."
|
|
194
|
+
const sha = execSync('git log -1 --format=%H -- .', {
|
|
172
195
|
cwd: dir,
|
|
173
196
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
174
197
|
}).toString().trim()
|
|
175
|
-
const status = execSync('git status --porcelain', {
|
|
198
|
+
const status = execSync('git status --porcelain -- .', {
|
|
176
199
|
cwd: dir,
|
|
177
200
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
178
201
|
}).toString()
|
package/src/commands/publish.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync } from 'node:fs'
|
|
15
|
-
import { readFile, writeFile } from 'node:fs/promises'
|
|
15
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
16
16
|
import { resolve, join } from 'node:path'
|
|
17
17
|
import { execSync } from 'node:child_process'
|
|
18
18
|
|
|
@@ -188,13 +188,129 @@ export async function publish(args = []) {
|
|
|
188
188
|
process.exit(1)
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
// 2. Auto-build if dist/ is missing
|
|
191
|
+
// 2. Auto-build if dist/ is missing OR stale.
|
|
192
|
+
//
|
|
193
|
+
// "Stale" means the schema fingerprint baked into
|
|
194
|
+
// `dist/meta/schema.json::_self.version` doesn't match the user's
|
|
195
|
+
// current `package.json::version`. That happens when the user bumps
|
|
196
|
+
// the version and runs `uniweb publish` without rebuilding — the
|
|
197
|
+
// artifact in dist/ encodes the OLD version, but the publish
|
|
198
|
+
// intends the NEW one. Without rebuilding we'd ship inconsistent
|
|
199
|
+
// bytes (schema says one version, registry record says another).
|
|
192
200
|
const distDir = join(foundationDir, 'dist')
|
|
193
201
|
const foundationJs = join(distDir, 'foundation.js')
|
|
194
202
|
const schemaJson = join(distDir, 'meta', 'schema.json')
|
|
195
203
|
|
|
196
|
-
|
|
197
|
-
|
|
204
|
+
// Pre-read package.json so we can compare its version against the
|
|
205
|
+
// schema before deciding whether to rebuild.
|
|
206
|
+
const pkgPath = join(foundationDir, 'package.json')
|
|
207
|
+
let earlyPkg
|
|
208
|
+
try {
|
|
209
|
+
earlyPkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
210
|
+
} catch (err) {
|
|
211
|
+
error(`Failed to read package.json: ${err.message}`)
|
|
212
|
+
process.exit(1)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let needsBuild = !existsSync(foundationJs) || !existsSync(schemaJson)
|
|
216
|
+
let buildReason = needsBuild ? 'no dist/ found' : null
|
|
217
|
+
|
|
218
|
+
if (!needsBuild) {
|
|
219
|
+
try {
|
|
220
|
+
const peekSchema = JSON.parse(await readFile(schemaJson, 'utf8'))
|
|
221
|
+
if (peekSchema?._self?.version && earlyPkg.version && peekSchema._self.version !== earlyPkg.version) {
|
|
222
|
+
needsBuild = true
|
|
223
|
+
buildReason = `package.json::version (${earlyPkg.version}) differs from dist/meta/schema.json::_self.version (${peekSchema._self.version})`
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// Malformed schema → treat as stale.
|
|
227
|
+
needsBuild = true
|
|
228
|
+
buildReason = 'dist/meta/schema.json could not be parsed'
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 2b. Pre-flight registry check — runs BEFORE the build so we don't
|
|
233
|
+
// burn vite cycles on a foundation we already know we can't (or
|
|
234
|
+
// don't need to) publish.
|
|
235
|
+
//
|
|
236
|
+
// Two outcomes short-circuit the build:
|
|
237
|
+
//
|
|
238
|
+
// a. The registry already has `<canonicalName>@<version>`
|
|
239
|
+
// published from the CURRENT git sha (per-foundation last
|
|
240
|
+
// commit). The artifact upstream is correct; refresh the
|
|
241
|
+
// local receipt and exit. (Same outcome as the post-build
|
|
242
|
+
// duplicate check, just earlier — saves a build.)
|
|
243
|
+
//
|
|
244
|
+
// b. The registry has the version published from a DIFFERENT
|
|
245
|
+
// sha. The user has unpublished changes against an already-
|
|
246
|
+
// published version → "bump the version" error before any
|
|
247
|
+
// build work. Was the eval skill's pp-03 row.
|
|
248
|
+
//
|
|
249
|
+
// If the pre-flight can't determine the canonical name from
|
|
250
|
+
// pkg.json + flags + auth alone (e.g., needs a TTY prompt for
|
|
251
|
+
// the foundation id), it falls through silently to the existing
|
|
252
|
+
// post-build path. No-build-saved is still the existing behavior.
|
|
253
|
+
if (!isLocal) {
|
|
254
|
+
const preflightName = quickResolveCanonicalName(earlyPkg, { namespaceFlag, nameFlag })
|
|
255
|
+
const preflightVersion = earlyPkg.version
|
|
256
|
+
if (preflightName && preflightVersion) {
|
|
257
|
+
try {
|
|
258
|
+
const auth = await readAuth()
|
|
259
|
+
if (auth?.token) {
|
|
260
|
+
const claims = decodeJwtPayload(auth.token)
|
|
261
|
+
const memberUuid = claims?.memberUuid
|
|
262
|
+
// Empty-scope publishes are server-rewritten to ~<memberUuid>/<id>.
|
|
263
|
+
// Mirror that here so getVersionEntry queries the canonical key.
|
|
264
|
+
const lookupName = preflightName.startsWith('@') || preflightName.startsWith('~')
|
|
265
|
+
? preflightName
|
|
266
|
+
: memberUuid ? `~${memberUuid}/${preflightName}` : null
|
|
267
|
+
if (lookupName) {
|
|
268
|
+
const registryUrlPre = registryUrl || getRegistryUrl()
|
|
269
|
+
const registryPre = new RemoteRegistry(registryUrlPre, auth.token)
|
|
270
|
+
const existing = await registryPre.getVersionEntry(lookupName, preflightVersion)
|
|
271
|
+
if (existing) {
|
|
272
|
+
const { gitSha } = readGitState(foundationDir)
|
|
273
|
+
if (gitSha && existing.publishedFromGitSha === gitSha) {
|
|
274
|
+
// Match → refresh receipt, exit clean. NO BUILD.
|
|
275
|
+
const refreshed = receiptFromRegistryEntry({
|
|
276
|
+
existingEntry: existing,
|
|
277
|
+
registry: registryPre,
|
|
278
|
+
name: lookupName,
|
|
279
|
+
version: preflightVersion,
|
|
280
|
+
isLocal: false,
|
|
281
|
+
isPropagateDefault: isPropagate,
|
|
282
|
+
})
|
|
283
|
+
if (refreshed) {
|
|
284
|
+
await mkdir(distDir, { recursive: true })
|
|
285
|
+
await writeFile(join(distDir, 'publish.json'), JSON.stringify(refreshed, null, 2) + '\n')
|
|
286
|
+
console.log('')
|
|
287
|
+
success(`${colors.bright}${lookupName}@${preflightVersion}${colors.reset} already published from ${gitSha.slice(0, 7)} — receipt refreshed.`)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Sha mismatch (or no provenance recorded for the existing
|
|
292
|
+
// entry, which shouldn't happen for new publishes after
|
|
293
|
+
// the receipt-as-cache work shipped). Clean error before
|
|
294
|
+
// any build work.
|
|
295
|
+
console.log('')
|
|
296
|
+
error(`${colors.bright}${lookupName}@${preflightVersion}${colors.reset} is already published.`)
|
|
297
|
+
console.log('')
|
|
298
|
+
console.log(` Bump the version in package.json to publish an update:`)
|
|
299
|
+
console.log(` ${colors.dim}"version": "${bumpPatch(preflightVersion)}"${colors.reset}`)
|
|
300
|
+
process.exit(1)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
// Network down, malformed auth, etc. — fall through to the
|
|
306
|
+
// existing post-build flow. No-build-saved is still the same
|
|
307
|
+
// behavior the user got before this pre-flight existed.
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (needsBuild) {
|
|
313
|
+
console.log(`${colors.yellow}⚠${colors.reset} ${buildReason}. Building foundation...`)
|
|
198
314
|
console.log('')
|
|
199
315
|
execSync('npx uniweb build --target foundation', {
|
|
200
316
|
cwd: foundationDir,
|
|
@@ -208,7 +324,13 @@ export async function publish(args = []) {
|
|
|
208
324
|
}
|
|
209
325
|
}
|
|
210
326
|
|
|
211
|
-
// 3. Read name
|
|
327
|
+
// 3. Read name + version from the (now-fresh) schema + package.json.
|
|
328
|
+
//
|
|
329
|
+
// `_self.name` is the build-RESOLVED form — applies `uniweb.id`,
|
|
330
|
+
// scope resolution, etc., that are easier to read off the build
|
|
331
|
+
// output than to redo here. `version` is sourced from package.json
|
|
332
|
+
// directly; the version-skew check above already ensured the
|
|
333
|
+
// schema and package.json agree.
|
|
212
334
|
let schema
|
|
213
335
|
try {
|
|
214
336
|
schema = JSON.parse(await readFile(schemaJson, 'utf8'))
|
|
@@ -218,11 +340,12 @@ export async function publish(args = []) {
|
|
|
218
340
|
}
|
|
219
341
|
|
|
220
342
|
const rawName = schema._self?.name
|
|
221
|
-
const version =
|
|
343
|
+
const version = earlyPkg.version
|
|
222
344
|
|
|
223
345
|
if (!rawName || !version) {
|
|
224
|
-
error('
|
|
225
|
-
console.log(`${colors.dim} Ensure your package.json has "name" and "version" fields
|
|
346
|
+
error('Foundation missing name or version')
|
|
347
|
+
console.log(`${colors.dim} Ensure your package.json has "name" and "version" fields,${colors.reset}`)
|
|
348
|
+
console.log(`${colors.dim} and that the build has produced dist/meta/schema.json with _self.name.${colors.reset}`)
|
|
226
349
|
process.exit(1)
|
|
227
350
|
}
|
|
228
351
|
|
|
@@ -267,8 +390,9 @@ export async function publish(args = []) {
|
|
|
267
390
|
// affects only the registry identity, never the workspace. Most users
|
|
268
391
|
// benefit from leaving `package.json::name` as the scaffold default
|
|
269
392
|
// (`src`) and putting the published-as id in `uniweb.id`.
|
|
270
|
-
|
|
271
|
-
|
|
393
|
+
// pkgPath was declared earlier (during the rebuild-stale-dist check).
|
|
394
|
+
// Reuse the already-loaded `earlyPkg` rather than re-reading from disk.
|
|
395
|
+
const pkg = earlyPkg
|
|
272
396
|
const uniwebNamespace = pkg.uniweb?.namespace
|
|
273
397
|
const uniwebId = pkg.uniweb?.id
|
|
274
398
|
const orgScopeMatch = (pkg.name || '').match(/^@([a-z0-9_-]+)\/([a-z0-9_-]+)$/)
|
|
@@ -541,6 +665,20 @@ export async function publish(args = []) {
|
|
|
541
665
|
isPropagateDefault: isPropagate,
|
|
542
666
|
})
|
|
543
667
|
if (refreshedReceipt) {
|
|
668
|
+
// Persist uniweb.id BEFORE the early return when an auto-derive
|
|
669
|
+
// or prompt-resolved id was set in this run. Without this, the
|
|
670
|
+
// next run wouldn't know the id and would have to re-derive
|
|
671
|
+
// from scratch — which means the pre-flight registry check at
|
|
672
|
+
// the top of publish() can't fire either (it relies on a
|
|
673
|
+
// resolvable id from pkg.json alone). Persisting here closes
|
|
674
|
+
// that loop so future deploys hit the pre-flight bail and skip
|
|
675
|
+
// the build entirely.
|
|
676
|
+
if (writeBackId) {
|
|
677
|
+
pkg.uniweb = pkg.uniweb || {}
|
|
678
|
+
pkg.uniweb.id = foundationName
|
|
679
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
680
|
+
info(`Wrote ${colors.cyan}uniweb.id: "${foundationName}"${colors.reset} to ${colors.dim}package.json${colors.reset}`)
|
|
681
|
+
}
|
|
544
682
|
await writeFile(join(distDir, 'publish.json'), JSON.stringify(refreshedReceipt, null, 2) + '\n')
|
|
545
683
|
console.log('')
|
|
546
684
|
success(`${colors.bright}${lookupName}@${version}${colors.reset} already published from ${gitSha.slice(0, 7)} — receipt refreshed.`)
|
|
@@ -650,6 +788,65 @@ export async function publish(args = []) {
|
|
|
650
788
|
* @param {string} version - e.g. "1.0.0"
|
|
651
789
|
* @returns {string} - e.g. "1.0.1"
|
|
652
790
|
*/
|
|
791
|
+
/**
|
|
792
|
+
* Quickly compute the canonical foundation name from `package.json` +
|
|
793
|
+
* CLI flags alone, without prompting and without reading the build's
|
|
794
|
+
* `dist/meta/schema.json`. Used by the pre-flight registry check so we
|
|
795
|
+
* can short-circuit the build when the registry already has this
|
|
796
|
+
* version published.
|
|
797
|
+
*
|
|
798
|
+
* Returns null when resolution would need a prompt or auto-derive
|
|
799
|
+
* (caller falls through to the existing post-build resolution path,
|
|
800
|
+
* which handles those cases). The returned string is one of:
|
|
801
|
+
* - `@<scope>/<id>` (org scope, full canonical form)
|
|
802
|
+
* - `~<handle>/<id>` (personal alias scope)
|
|
803
|
+
* - `<id>` (bare; caller may prepend `~<memberUuid>/`
|
|
804
|
+
* from the JWT for the actual lookup)
|
|
805
|
+
*
|
|
806
|
+
* The full resolution at line 313+ is the canonical implementation;
|
|
807
|
+
* this helper is a strict subset that mirrors the high-confidence
|
|
808
|
+
* paths only. If they diverge, the helper is the one that should
|
|
809
|
+
* stay conservative (return null on uncertainty).
|
|
810
|
+
*/
|
|
811
|
+
function quickResolveCanonicalName(pkg, { namespaceFlag, nameFlag } = {}) {
|
|
812
|
+
if (!pkg) return null
|
|
813
|
+
const orgScopeMatch = (pkg.name || '').match(/^@([a-z0-9_-]+)\/([a-z0-9_-]+)$/)
|
|
814
|
+
const personalScopeMatch = (pkg.name || '').match(/^~([a-z0-9_-]+)\/([a-z0-9_-]+)$/)
|
|
815
|
+
const uniwebNamespace = pkg.uniweb?.namespace
|
|
816
|
+
const uniwebId = pkg.uniweb?.id
|
|
817
|
+
|
|
818
|
+
// Scope precedence mirrors the full resolution.
|
|
819
|
+
let scopeSigil = null
|
|
820
|
+
let scopeName = null
|
|
821
|
+
if (namespaceFlag) {
|
|
822
|
+
scopeSigil = '@'
|
|
823
|
+
scopeName = namespaceFlag
|
|
824
|
+
} else if (orgScopeMatch) {
|
|
825
|
+
scopeSigil = '@'
|
|
826
|
+
scopeName = orgScopeMatch[1]
|
|
827
|
+
} else if (personalScopeMatch) {
|
|
828
|
+
scopeSigil = '~'
|
|
829
|
+
scopeName = personalScopeMatch[1]
|
|
830
|
+
} else if (uniwebNamespace) {
|
|
831
|
+
scopeSigil = '@'
|
|
832
|
+
scopeName = uniwebNamespace
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Id precedence mirrors the full resolution but stops at "no-prompt"
|
|
836
|
+
// sources. Auto-derive and TTY prompts both happen post-build so the
|
|
837
|
+
// user sees suggestions in context; the pre-flight only fires when
|
|
838
|
+
// the id is already determined.
|
|
839
|
+
let id = null
|
|
840
|
+
if (nameFlag) id = nameFlag
|
|
841
|
+
else if (orgScopeMatch) id = orgScopeMatch[2]
|
|
842
|
+
else if (personalScopeMatch) id = personalScopeMatch[2]
|
|
843
|
+
else if (uniwebId) id = uniwebId
|
|
844
|
+
else return null
|
|
845
|
+
|
|
846
|
+
if (scopeSigil) return `${scopeSigil}${scopeName}/${id}`
|
|
847
|
+
return id
|
|
848
|
+
}
|
|
849
|
+
|
|
653
850
|
function bumpPatch(version) {
|
|
654
851
|
const parts = version.split('.')
|
|
655
852
|
if (parts.length !== 3) return version
|
|
@@ -799,13 +996,21 @@ async function buildIdSuggestions({ foundationDir, workspaceRoot, pkg }) {
|
|
|
799
996
|
return out
|
|
800
997
|
}
|
|
801
998
|
|
|
999
|
+
/**
|
|
1000
|
+
* Per-directory git state. Mirrors `deploy.js::readGitState` exactly —
|
|
1001
|
+
* scopes the sha + dirty check to `dir` rather than reading the whole
|
|
1002
|
+
* repo's HEAD. Receipts compare against this; if publish records the
|
|
1003
|
+
* repo HEAD but deploy compares against the foundation's last commit,
|
|
1004
|
+
* the receipt-as-cache no-op-refresh path drifts. Both sides must read
|
|
1005
|
+
* the same shape.
|
|
1006
|
+
*/
|
|
802
1007
|
function readGitState(dir) {
|
|
803
1008
|
try {
|
|
804
|
-
const sha = execSync('git
|
|
1009
|
+
const sha = execSync('git log -1 --format=%H -- .', {
|
|
805
1010
|
cwd: dir,
|
|
806
1011
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
807
1012
|
}).toString().trim()
|
|
808
|
-
const status = execSync('git status --porcelain', {
|
|
1013
|
+
const status = execSync('git status --porcelain -- .', {
|
|
809
1014
|
cwd: dir,
|
|
810
1015
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
811
1016
|
}).toString()
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-04-
|
|
3
|
+
"generatedAt": "2026-04-30T02:16:23.561Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
6
|
"version": "0.13.3",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"deps": []
|
|
93
93
|
},
|
|
94
94
|
"@uniweb/unipress": {
|
|
95
|
-
"version": "0.4.
|
|
95
|
+
"version": "0.4.3",
|
|
96
96
|
"path": "framework/unipress",
|
|
97
97
|
"deps": [
|
|
98
98
|
"@uniweb/build",
|