uniweb 0.16.4 → 0.17.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/package.json +7 -7
- package/src/backend/site-sync.js +288 -4
- package/src/commands/clone.js +48 -4
- package/src/commands/publish.js +39 -3
- package/src/commands/pull.js +17 -73
- package/src/commands/push.js +39 -3
- package/src/commands/register.js +8 -0
- package/src/commands/status.js +9 -0
- package/src/framework-index.json +8 -8
- package/src/index.js +10 -0
- package/src/utils/args.js +98 -0
- package/src/utils/flag-guard.js +89 -0
- package/src/utils/git.js +43 -4
- package/src/utils/pull-written.js +127 -0
- package/src/utils/uwx-read.js +157 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal `.uwx` reader — for the commands that run BEFORE a project exists.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists rather than importing the real one
|
|
5
|
+
*
|
|
6
|
+
* Every other `.uwx` reader in the CLI (`pull.js`, `site-sync.js`, `content.js`)
|
|
7
|
+
* imports `readZip` from `@uniweb/build/uwx`. That is correct for them: they run
|
|
8
|
+
* *inside* a project, where `@uniweb/build` resolves from the project's own
|
|
9
|
+
* `node_modules`.
|
|
10
|
+
*
|
|
11
|
+
* **`@uniweb/build` is not a dependency of this package** (see `package.json`),
|
|
12
|
+
* so a command that runs where no project exists cannot reach it — statically or
|
|
13
|
+
* dynamically. `uniweb clone` is exactly that command: its whole job is to turn a
|
|
14
|
+
* backend site into a local project that does not exist yet.
|
|
15
|
+
*
|
|
16
|
+
* So this is a deliberate second implementation, kept minimal and dependency-free
|
|
17
|
+
* (`node:zlib` is a builtin), the same way `clone.js` keeps its own
|
|
18
|
+
* `extractDocument` for the same reason. **Do not import this from a command that
|
|
19
|
+
* runs inside a project** — use `@uniweb/build/uwx`'s `readZip` there, so the
|
|
20
|
+
* producer and the consumer of a `.uwx` stay one implementation wherever they can.
|
|
21
|
+
*
|
|
22
|
+
* ## Two things measured on a real payload, both of which would break a naive reader
|
|
23
|
+
*
|
|
24
|
+
* 1. ⛔ **The archive is NOT "stored".** `pull.js` describes the format as *"our
|
|
25
|
+
* Stored ZIP"*, and that is true of `manifest.json` and false of the entity
|
|
26
|
+
* files — measured on a live pull: `manifest.json` method 0 (STORED), the
|
|
27
|
+
* entity JSON method 8 (DEFLATED). A stored-only reader silently yields the
|
|
28
|
+
* manifest and drops the document, i.e. it returns success and no entities.
|
|
29
|
+
* **Both methods are handled below and the tests cover a mixed archive.**
|
|
30
|
+
* 2. The entity payload is the *only* thing wanted; `manifest.json` is skipped, as
|
|
31
|
+
* every other reader in the tree does.
|
|
32
|
+
*
|
|
33
|
+
* We read the **central directory** rather than walking local file headers,
|
|
34
|
+
* because a local header may carry zeroed sizes when the general-purpose bit 3
|
|
35
|
+
* flag defers them to a trailing data descriptor. The central directory always
|
|
36
|
+
* carries the real sizes.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { inflateRawSync } from 'node:zlib'
|
|
40
|
+
|
|
41
|
+
const SIG_EOCD = 0x06054b50
|
|
42
|
+
const SIG_CENTRAL = 0x02014b50
|
|
43
|
+
const SIG_LOCAL = 0x04034b50
|
|
44
|
+
const METHOD_STORED = 0
|
|
45
|
+
const METHOD_DEFLATED = 8
|
|
46
|
+
|
|
47
|
+
/** ZIP local-file-header magic, "PK\x03\x04" — the first two bytes are enough. */
|
|
48
|
+
export function looksLikeZip(buf) {
|
|
49
|
+
return Boolean(buf) && buf.length >= 2 && buf[0] === 0x50 && buf[1] === 0x4b
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Locate the end-of-central-directory record.
|
|
54
|
+
*
|
|
55
|
+
* Scanned backwards because the record is last and variable-length (it carries an
|
|
56
|
+
* optional trailing comment).
|
|
57
|
+
*
|
|
58
|
+
* @param {Buffer} buf
|
|
59
|
+
* @returns {number} offset of the EOCD, or -1
|
|
60
|
+
*/
|
|
61
|
+
function findEocd(buf) {
|
|
62
|
+
const min = Math.max(0, buf.length - 22 - 0xffff)
|
|
63
|
+
for (let i = buf.length - 22; i >= min; i--) {
|
|
64
|
+
if (buf.readUInt32LE(i) === SIG_EOCD) return i
|
|
65
|
+
}
|
|
66
|
+
return -1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read every entry out of a ZIP as `[name, Buffer]` pairs.
|
|
71
|
+
*
|
|
72
|
+
* @param {Buffer} buf
|
|
73
|
+
* @returns {Array<[string, Buffer]>} empty when the buffer is not a readable ZIP
|
|
74
|
+
*/
|
|
75
|
+
export function readUwxZip(buf) {
|
|
76
|
+
if (!looksLikeZip(buf)) return []
|
|
77
|
+
const eocd = findEocd(buf)
|
|
78
|
+
if (eocd < 0) return []
|
|
79
|
+
|
|
80
|
+
const count = buf.readUInt16LE(eocd + 10)
|
|
81
|
+
let ptr = buf.readUInt32LE(eocd + 16)
|
|
82
|
+
const out = []
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < count; i++) {
|
|
85
|
+
if (ptr + 46 > buf.length || buf.readUInt32LE(ptr) !== SIG_CENTRAL) break
|
|
86
|
+
|
|
87
|
+
const method = buf.readUInt16LE(ptr + 10)
|
|
88
|
+
const csize = buf.readUInt32LE(ptr + 20)
|
|
89
|
+
const nameLen = buf.readUInt16LE(ptr + 28)
|
|
90
|
+
const extraLen = buf.readUInt16LE(ptr + 30)
|
|
91
|
+
const commentLen = buf.readUInt16LE(ptr + 32)
|
|
92
|
+
const localOff = buf.readUInt32LE(ptr + 42)
|
|
93
|
+
const name = buf.subarray(ptr + 46, ptr + 46 + nameLen).toString('utf8')
|
|
94
|
+
|
|
95
|
+
// The local header's name/extra lengths are independent of the central
|
|
96
|
+
// directory's — read them where the data actually starts, not from above.
|
|
97
|
+
if (localOff + 30 <= buf.length && buf.readUInt32LE(localOff) === SIG_LOCAL) {
|
|
98
|
+
const lNameLen = buf.readUInt16LE(localOff + 26)
|
|
99
|
+
const lExtraLen = buf.readUInt16LE(localOff + 28)
|
|
100
|
+
const start = localOff + 30 + lNameLen + lExtraLen
|
|
101
|
+
const raw = buf.subarray(start, start + csize)
|
|
102
|
+
try {
|
|
103
|
+
if (method === METHOD_STORED) out.push([name, Buffer.from(raw)])
|
|
104
|
+
else if (method === METHOD_DEFLATED) out.push([name, inflateRawSync(raw)])
|
|
105
|
+
// any other method: skip rather than guess
|
|
106
|
+
} catch {
|
|
107
|
+
/* a corrupt entry must not lose the readable ones */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
ptr += 46 + nameLen + extraLen + commentLen
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return out
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The entity `$`-documents inside a `.uwx`, or a JSON body's documents.
|
|
119
|
+
*
|
|
120
|
+
* Mirrors `pull.js`'s `readPullDocuments` in behaviour — including its JSON
|
|
121
|
+
* fallback, so a future envelope change does not break this lane either — but
|
|
122
|
+
* without the `@uniweb/build` import. See the header for why that matters.
|
|
123
|
+
*
|
|
124
|
+
* @param {Buffer} buf
|
|
125
|
+
* @returns {object[]} parsed documents, possibly empty
|
|
126
|
+
*/
|
|
127
|
+
export function readUwxDocuments(buf) {
|
|
128
|
+
if (!buf || buf.length === 0) return []
|
|
129
|
+
|
|
130
|
+
if (looksLikeZip(buf)) {
|
|
131
|
+
const docs = []
|
|
132
|
+
for (const [name, data] of readUwxZip(buf)) {
|
|
133
|
+
if (name === 'manifest.json' || !name.endsWith('.json')) continue
|
|
134
|
+
try {
|
|
135
|
+
docs.push(JSON.parse(data.toString('utf8')))
|
|
136
|
+
} catch {
|
|
137
|
+
/* skip a non-document entry */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return docs
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let payload
|
|
144
|
+
try {
|
|
145
|
+
payload = JSON.parse(buf.toString('utf8'))
|
|
146
|
+
} catch {
|
|
147
|
+
return []
|
|
148
|
+
}
|
|
149
|
+
if (Array.isArray(payload)) return payload.filter(Boolean)
|
|
150
|
+
const list = Array.isArray(payload?.entities)
|
|
151
|
+
? payload.entities
|
|
152
|
+
: Array.isArray(payload?.documents)
|
|
153
|
+
? payload.documents
|
|
154
|
+
: null
|
|
155
|
+
if (list) return list.filter(Boolean)
|
|
156
|
+
return payload ? [payload] : []
|
|
157
|
+
}
|