uniweb 0.87.2 → 0.87.4
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 +8 -8
- package/src/backend/site-sync.js +11 -4
- package/src/framework-index.json +2 -2
- package/src/index.js +34 -36
- package/src/utils/exit.js +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.87.
|
|
3
|
+
"version": "0.87.4",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,17 +41,17 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/core": "^0.38.0",
|
|
45
|
-
"@uniweb/schemas": "^0.13.0",
|
|
46
|
-
"@uniweb/kit": "^0.20.0",
|
|
47
|
-
"@uniweb/semantic-parser": "^1.5.1",
|
|
48
44
|
"@uniweb/content-writer": "^0.3.4",
|
|
49
|
-
"@uniweb/
|
|
45
|
+
"@uniweb/kit": "^0.20.0",
|
|
46
|
+
"@uniweb/schemas": "^0.13.0",
|
|
47
|
+
"@uniweb/runtime": "^0.29.4",
|
|
48
|
+
"@uniweb/core": "^0.38.0",
|
|
49
|
+
"@uniweb/semantic-parser": "^1.5.1"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@uniweb/content-reader": "^1.2.8",
|
|
53
|
-
"@uniweb/
|
|
54
|
-
"@uniweb/
|
|
53
|
+
"@uniweb/semantic-parser": "^1.5.1",
|
|
54
|
+
"@uniweb/build": "^0.80.3"
|
|
55
55
|
},
|
|
56
56
|
"peerDependenciesMeta": {
|
|
57
57
|
"@uniweb/build": {
|
package/src/backend/site-sync.js
CHANGED
|
@@ -55,7 +55,8 @@ import {
|
|
|
55
55
|
backfillLinkUuids,
|
|
56
56
|
LINK_MODEL,
|
|
57
57
|
bankedFileUuids,
|
|
58
|
-
existingSectionFile
|
|
58
|
+
existingSectionFile,
|
|
59
|
+
layoutUnitFile
|
|
59
60
|
} from '@uniweb/build/uwx'
|
|
60
61
|
|
|
61
62
|
// First entity `$`-document out of a `.uwx` we produced or the backend served.
|
|
@@ -312,9 +313,15 @@ export function unitFilesOf(siteDir, unitPath) {
|
|
|
312
313
|
} catch {
|
|
313
314
|
/* no or unreadable site.yml — the defaults are right */
|
|
314
315
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
// A layout unit names its layout and area (`footer.md`, `default/header/1-alert.md`); the author may
|
|
317
|
+
// keep it as `default/footer.md` or `default/header/0-alert.md`, where a pull writes it back
|
|
318
|
+
// (`layoutUnitFile`). ⛔ Until 2026-10-08 it was looked up beside the unit's own path only.
|
|
319
|
+
if (unitPath.startsWith('layout/')) {
|
|
320
|
+
const base = join(siteDir, paths.layout || 'layout')
|
|
321
|
+
return [relative(siteDir, layoutUnitFile(base, unitPath.slice('layout/'.length)))]
|
|
322
|
+
}
|
|
323
|
+
if (unitPath.startsWith('pages/')) {
|
|
324
|
+
const rel = `${paths.pages || 'pages'}${unitPath.slice('pages'.length)}`
|
|
318
325
|
if (!rel.endsWith('.md') || existsSync(join(siteDir, rel))) return [rel]
|
|
319
326
|
const id = basename(rel, '.md')
|
|
320
327
|
const found = existingSectionFile(join(siteDir, dirname(rel)), id, id, { child: true })
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-08T17:12:48.725Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "0.7.0",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"@uniweb/build": {
|
|
13
|
-
"version": "0.80.
|
|
13
|
+
"version": "0.80.3",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
package/src/index.js
CHANGED
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
formatOptions
|
|
73
73
|
} from './utils/interactive.js'
|
|
74
74
|
import { findWorkspaceRoot } from './utils/workspace.js'
|
|
75
|
+
import { exitWhenDrained } from './utils/exit.js'
|
|
75
76
|
|
|
76
77
|
// Colors for terminal output
|
|
77
78
|
const colors = {
|
|
@@ -223,7 +224,7 @@ function delegateToLocal(localCliPath) {
|
|
|
223
224
|
[localCliPath, ...process.argv.slice(2)],
|
|
224
225
|
{ stdio: 'inherit' }
|
|
225
226
|
)
|
|
226
|
-
child.on('close', (code) =>
|
|
227
|
+
child.on('close', (code) => exitWhenDrained(code ?? 0))
|
|
227
228
|
child.on('error', reject)
|
|
228
229
|
})
|
|
229
230
|
}
|
|
@@ -320,7 +321,7 @@ async function importProjectCommand(modulePath) {
|
|
|
320
321
|
log(` ${colors.cyan}npm install uniweb ${missing}${colors.reset}`)
|
|
321
322
|
}
|
|
322
323
|
|
|
323
|
-
|
|
324
|
+
await exitWhenDrained(1)
|
|
324
325
|
}
|
|
325
326
|
}
|
|
326
327
|
|
|
@@ -651,7 +652,7 @@ async function main() {
|
|
|
651
652
|
// beside the new one it is ignored, so a script can carry both while CLIs of both ages run it.
|
|
652
653
|
if (process.env.UNIWEB_REGISTER_URL && !process.env.UNIWEB_SERVER) {
|
|
653
654
|
error('UNIWEB_REGISTER_URL is now UNIWEB_SERVER — rename it, and the command goes to the same backend.')
|
|
654
|
-
|
|
655
|
+
await exitWhenDrained(2)
|
|
655
656
|
}
|
|
656
657
|
|
|
657
658
|
// Handle --version / -v
|
|
@@ -794,14 +795,14 @@ async function main() {
|
|
|
794
795
|
if (command === 'doctor') {
|
|
795
796
|
const { doctor } = await importProjectCommand('./commands/doctor.js')
|
|
796
797
|
const result = await doctor(args.slice(1))
|
|
797
|
-
|
|
798
|
+
await exitWhenDrained(result?.errors > 0 ? 1 : 0)
|
|
798
799
|
}
|
|
799
800
|
|
|
800
801
|
// Handle validate command (dynamic import — depends on @uniweb/build)
|
|
801
802
|
if (command === 'validate') {
|
|
802
803
|
const { validate } = await importProjectCommand('./commands/validate.js')
|
|
803
804
|
const result = await validate(args.slice(1))
|
|
804
|
-
|
|
805
|
+
await exitWhenDrained(result?.exitCode ?? 0)
|
|
805
806
|
}
|
|
806
807
|
|
|
807
808
|
// Handle register command (dynamic import — depends on @uniweb/build).
|
|
@@ -810,35 +811,35 @@ async function main() {
|
|
|
810
811
|
if (command === 'register' || command === 'release') {
|
|
811
812
|
const { register } = await importProjectCommand('./commands/register.js')
|
|
812
813
|
const result = await register(args.slice(1))
|
|
813
|
-
|
|
814
|
+
await exitWhenDrained(result?.exitCode ?? 0)
|
|
814
815
|
}
|
|
815
816
|
|
|
816
817
|
// Handle push command (dynamic import — depends on @uniweb/build)
|
|
817
818
|
if (command === 'push') {
|
|
818
819
|
const { push } = await importProjectCommand('./commands/push.js')
|
|
819
820
|
const result = await push(args.slice(1))
|
|
820
|
-
|
|
821
|
+
await exitWhenDrained(result?.exitCode ?? 0)
|
|
821
822
|
}
|
|
822
823
|
|
|
823
824
|
// Handle pull command (dynamic import — depends on @uniweb/build)
|
|
824
825
|
if (command === 'pull') {
|
|
825
826
|
const { pull } = await importProjectCommand('./commands/pull.js')
|
|
826
827
|
const result = await pull(args.slice(1))
|
|
827
|
-
|
|
828
|
+
await exitWhenDrained(result?.exitCode ?? 0)
|
|
828
829
|
}
|
|
829
830
|
|
|
830
831
|
// Handle sync command (refresh + push; both halves imported, never reimplemented)
|
|
831
832
|
if (command === 'sync') {
|
|
832
833
|
const { sync } = await importProjectCommand('./commands/sync.js')
|
|
833
834
|
const result = await sync(args.slice(1))
|
|
834
|
-
|
|
835
|
+
await exitWhenDrained(result?.exitCode ?? 0)
|
|
835
836
|
}
|
|
836
837
|
|
|
837
838
|
// Handle refresh command (dynamic import — depends on @uniweb/build via pull)
|
|
838
839
|
if (command === 'refresh') {
|
|
839
840
|
const { refresh } = await importProjectCommand('./commands/refresh.js')
|
|
840
841
|
const result = await refresh(args.slice(1))
|
|
841
|
-
|
|
842
|
+
await exitWhenDrained(result?.exitCode ?? 0)
|
|
842
843
|
}
|
|
843
844
|
|
|
844
845
|
// Handle forget — remove one backend's local records (sync.json, cache, deploy
|
|
@@ -847,14 +848,14 @@ async function main() {
|
|
|
847
848
|
if (command === 'forget') {
|
|
848
849
|
const { forget } = await importProjectCommand('./commands/forget.js')
|
|
849
850
|
const result = await forget(args.slice(1))
|
|
850
|
-
|
|
851
|
+
await exitWhenDrained(result?.exitCode ?? 0)
|
|
851
852
|
}
|
|
852
853
|
|
|
853
854
|
// Handle status command (dynamic import — offline emit via @uniweb/build)
|
|
854
855
|
if (command === 'status') {
|
|
855
856
|
const { status } = await importProjectCommand('./commands/status.js')
|
|
856
857
|
const result = await status(args.slice(1))
|
|
857
|
-
|
|
858
|
+
await exitWhenDrained(result?.exitCode ?? 0)
|
|
858
859
|
}
|
|
859
860
|
|
|
860
861
|
// Handle clone command (global — bootstraps a new project from a backend site;
|
|
@@ -864,7 +865,7 @@ async function main() {
|
|
|
864
865
|
// `uniweb pull` after install.)
|
|
865
866
|
if (command === 'clone') {
|
|
866
867
|
const result = await clone(args.slice(1))
|
|
867
|
-
|
|
868
|
+
await exitWhenDrained(result?.exitCode ?? 0)
|
|
868
869
|
}
|
|
869
870
|
|
|
870
871
|
// Handle update command
|
|
@@ -899,7 +900,7 @@ async function main() {
|
|
|
899
900
|
if (command === 'publish') {
|
|
900
901
|
const { publish } = await importProjectCommand('./commands/publish.js')
|
|
901
902
|
const result = await publish(args.slice(1))
|
|
902
|
-
|
|
903
|
+
await exitWhenDrained(result?.exitCode ?? 0)
|
|
903
904
|
}
|
|
904
905
|
|
|
905
906
|
// Handle deploy command — third-party hosts only (dynamic import — @uniweb/build)
|
|
@@ -946,7 +947,7 @@ async function main() {
|
|
|
946
947
|
// dropped, the login would go to the default backend.
|
|
947
948
|
if (loginArgs.some((a) => a === '--backend' || a.startsWith('--backend='))) {
|
|
948
949
|
console.error('\x1b[31m✗\x1b[0m `--backend` is now `--server`: uniweb login --server <url>')
|
|
949
|
-
|
|
950
|
+
await exitWhenDrained(2)
|
|
950
951
|
}
|
|
951
952
|
let apiBase
|
|
952
953
|
try {
|
|
@@ -955,7 +956,7 @@ async function main() {
|
|
|
955
956
|
console.error(
|
|
956
957
|
`\x1b[31m✗\x1b[0m ${err.message} — e.g. uniweb login --server http://localhost:8080`
|
|
957
958
|
)
|
|
958
|
-
|
|
959
|
+
await exitWhenDrained(2)
|
|
959
960
|
}
|
|
960
961
|
await runRegistryLogin({ apiBase, args: loginArgs })
|
|
961
962
|
return
|
|
@@ -971,7 +972,7 @@ async function main() {
|
|
|
971
972
|
console.error(
|
|
972
973
|
`\x1b[31m✗\x1b[0m \`uniweb logout\` takes no options (got ${stray}) — it logs you out of the backend you are logged in to.`
|
|
973
974
|
)
|
|
974
|
-
|
|
975
|
+
await exitWhenDrained(2)
|
|
975
976
|
}
|
|
976
977
|
const { clearRegistryAuth } = await import('./utils/registry-auth.js')
|
|
977
978
|
const { loggedInOrigin } = await import('./utils/config.js')
|
|
@@ -994,12 +995,9 @@ async function main() {
|
|
|
994
995
|
if (command === 'site') {
|
|
995
996
|
const { site } = await import('./commands/site.js')
|
|
996
997
|
const result = await site(args.slice(1))
|
|
997
|
-
//
|
|
998
|
-
//
|
|
999
|
-
|
|
1000
|
-
// into a file (synchronous) was whole. Pinned by test/site-list-pipe.test.js.
|
|
1001
|
-
await new Promise((resolve) => process.stdout.write('', resolve))
|
|
1002
|
-
process.exit(result?.exitCode ?? 0)
|
|
998
|
+
// Its long list is where a bare exit was first caught cutting a pipe short — pinned by
|
|
999
|
+
// test/site-list-pipe.test.js; why every exit waits: utils/exit.js.
|
|
1000
|
+
await exitWhenDrained(result?.exitCode ?? 0)
|
|
1003
1001
|
}
|
|
1004
1002
|
|
|
1005
1003
|
// Handle org command (new-backend orgs/units — publish-scope management)
|
|
@@ -1038,7 +1036,7 @@ async function main() {
|
|
|
1038
1036
|
if (command !== 'create') {
|
|
1039
1037
|
error(`Unknown command: ${command}`)
|
|
1040
1038
|
showHelp()
|
|
1041
|
-
|
|
1039
|
+
await exitWhenDrained(1)
|
|
1042
1040
|
}
|
|
1043
1041
|
|
|
1044
1042
|
title('Uniweb Project Generator')
|
|
@@ -1053,7 +1051,7 @@ async function main() {
|
|
|
1053
1051
|
log(
|
|
1054
1052
|
` ${colors.cyan}uniweb add foundation --from <template>${colors.reset}\n`
|
|
1055
1053
|
)
|
|
1056
|
-
|
|
1054
|
+
await exitWhenDrained(1)
|
|
1057
1055
|
}
|
|
1058
1056
|
|
|
1059
1057
|
// Parse arguments
|
|
@@ -1081,7 +1079,7 @@ async function main() {
|
|
|
1081
1079
|
parseTemplateId(templateType)
|
|
1082
1080
|
} catch (err) {
|
|
1083
1081
|
error(`Invalid template: ${err.message}`)
|
|
1084
|
-
|
|
1082
|
+
await exitWhenDrained(1)
|
|
1085
1083
|
}
|
|
1086
1084
|
}
|
|
1087
1085
|
|
|
@@ -1127,7 +1125,7 @@ async function main() {
|
|
|
1127
1125
|
`Could not derive a valid project name from the current directory ("${dirName}").`
|
|
1128
1126
|
)
|
|
1129
1127
|
log(`Re-run with ${colors.cyan}--name=<your-name>${colors.reset}.`)
|
|
1130
|
-
|
|
1128
|
+
await exitWhenDrained(1)
|
|
1131
1129
|
}
|
|
1132
1130
|
projectName = slug
|
|
1133
1131
|
if (slug !== dirName) {
|
|
@@ -1144,7 +1142,7 @@ async function main() {
|
|
|
1144
1142
|
if (nonInteractive && !projectName) {
|
|
1145
1143
|
error(`Missing project name.\n`)
|
|
1146
1144
|
log(`Usage: ${prefix} create <project-name> [--template <name>] [--blank]`)
|
|
1147
|
-
|
|
1145
|
+
await exitWhenDrained(1)
|
|
1148
1146
|
}
|
|
1149
1147
|
|
|
1150
1148
|
// Non-interactive: default to starter when no template specified
|
|
@@ -1172,7 +1170,7 @@ async function main() {
|
|
|
1172
1170
|
{
|
|
1173
1171
|
onCancel: () => {
|
|
1174
1172
|
log('\nScaffolding cancelled.')
|
|
1175
|
-
process.exit(0)
|
|
1173
|
+
process.exit(0) // a prompt: a terminal, so nothing to drain — and it must stop here, now
|
|
1176
1174
|
}
|
|
1177
1175
|
}
|
|
1178
1176
|
)
|
|
@@ -1181,7 +1179,7 @@ async function main() {
|
|
|
1181
1179
|
|
|
1182
1180
|
if (!projectName) {
|
|
1183
1181
|
error('Missing project name')
|
|
1184
|
-
|
|
1182
|
+
await exitWhenDrained(1)
|
|
1185
1183
|
}
|
|
1186
1184
|
|
|
1187
1185
|
// Prompt for template if not specified via --template or --blank
|
|
@@ -1197,7 +1195,7 @@ async function main() {
|
|
|
1197
1195
|
{
|
|
1198
1196
|
onCancel: () => {
|
|
1199
1197
|
log('\nScaffolding cancelled.')
|
|
1200
|
-
process.exit(0)
|
|
1198
|
+
process.exit(0) // a prompt: a terminal, so nothing to drain — and it must stop here, now
|
|
1201
1199
|
}
|
|
1202
1200
|
}
|
|
1203
1201
|
)
|
|
@@ -1219,7 +1217,7 @@ async function main() {
|
|
|
1219
1217
|
|
|
1220
1218
|
if (!inPlace && existsSync(projectDir)) {
|
|
1221
1219
|
error(`Directory already exists: ${projectName}`)
|
|
1222
|
-
|
|
1220
|
+
await exitWhenDrained(1)
|
|
1223
1221
|
}
|
|
1224
1222
|
|
|
1225
1223
|
if (inPlace) {
|
|
@@ -1245,7 +1243,7 @@ async function main() {
|
|
|
1245
1243
|
log(
|
|
1246
1244
|
`Move or remove them, then re-run ${colors.cyan}uniweb create .${colors.reset}.`
|
|
1247
1245
|
)
|
|
1248
|
-
|
|
1246
|
+
await exitWhenDrained(1)
|
|
1249
1247
|
}
|
|
1250
1248
|
}
|
|
1251
1249
|
|
|
@@ -1320,7 +1318,7 @@ async function main() {
|
|
|
1320
1318
|
log(
|
|
1321
1319
|
` • Try the starter template instead: ${colors.cyan}uniweb create ${projectName} --template starter${colors.reset}`
|
|
1322
1320
|
)
|
|
1323
|
-
|
|
1321
|
+
await exitWhenDrained(1)
|
|
1324
1322
|
}
|
|
1325
1323
|
}
|
|
1326
1324
|
|
|
@@ -2231,7 +2229,7 @@ ${colors.bright}Install:${colors.reset}
|
|
|
2231
2229
|
}
|
|
2232
2230
|
|
|
2233
2231
|
// Run CLI
|
|
2234
|
-
main().catch((err) => {
|
|
2232
|
+
main().catch(async (err) => {
|
|
2235
2233
|
error(err.message)
|
|
2236
|
-
|
|
2234
|
+
await exitWhenDrained(1)
|
|
2237
2235
|
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit once everything this process wrote to stdout and stderr has left it.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ `process.exit()` does not wait for output a pipe has not taken yet. On macOS Node writes
|
|
5
|
+
* to a pipe asynchronously, and an exit drops whatever is still queued: `uniweb site list
|
|
6
|
+
* --json` reached a reading process as half a JSON document, cut at 8192 bytes, and a single
|
|
7
|
+
* 100 000-byte write followed by an exit delivered 65 536 — on stdout and on stderr alike
|
|
8
|
+
* (measured 2026-10-07). Into a terminal or a file, or on Linux, the writes are synchronous
|
|
9
|
+
* and nothing is lost, which is how it went unseen: the reader it bites is a script, `| jq`,
|
|
10
|
+
* or an agent capturing the output.
|
|
11
|
+
*
|
|
12
|
+
* ⭐ An empty write's callback runs once every write before it on that stream has been handed
|
|
13
|
+
* over, so waiting for one per stream is enough.
|
|
14
|
+
*
|
|
15
|
+
* Every exit in `index.js` goes through here — `test/exit-drain.test.js` holds it to that.
|
|
16
|
+
*
|
|
17
|
+
* @param {number} [code]
|
|
18
|
+
* @returns {Promise<never>}
|
|
19
|
+
*/
|
|
20
|
+
export async function exitWhenDrained(code = 0) {
|
|
21
|
+
await Promise.all([process.stdout, process.stderr].map(drained))
|
|
22
|
+
process.exit(code)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function drained(stream) {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
if (!stream || stream.destroyed || stream.writableEnded) return resolve()
|
|
28
|
+
stream.write('', () => resolve())
|
|
29
|
+
})
|
|
30
|
+
}
|