uniweb 0.48.5 → 0.49.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,15 +41,15 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/
|
|
44
|
+
"@uniweb/core": "^0.25.0",
|
|
45
|
+
"@uniweb/kit": "^0.18.3",
|
|
46
|
+
"@uniweb/semantic-parser": "^1.4.0",
|
|
47
|
+
"@uniweb/runtime": "^0.20.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/
|
|
50
|
+
"@uniweb/build": "^0.45.0",
|
|
51
51
|
"@uniweb/content-reader": "^1.2.4",
|
|
52
|
-
"@uniweb/
|
|
52
|
+
"@uniweb/semantic-parser": "^1.4.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -857,7 +857,7 @@ A bare string is a path under `entities/`, naming one file or matching many.
|
|
|
857
857
|
- article/2025-*.md
|
|
858
858
|
```
|
|
859
859
|
|
|
860
|
-
A query
|
|
860
|
+
A query reads one branch with `scope: archive` — the folder and everything inside it (the older `where: { path: { under: … } }` is refused by the build). An entity belongs to one folder; if you want a computed subset, that is a query, not a second placement.
|
|
861
861
|
|
|
862
862
|
**`queries.yml` — how content is reached.** A bare map of name → query. A query names a schema and the published records of that schema are its rows:
|
|
863
863
|
|
|
@@ -897,9 +897,11 @@ pages/blog/
|
|
|
897
897
|
|
|
898
898
|
`entities/article/design-tips.md` becomes `/blog/design-tips`. The section inside `[slug]/` needs no special markdown — the matched record is delivered to it. Generated pages are excluded from navigation menus.
|
|
899
899
|
|
|
900
|
+
**Which query the URL narrows — the page's route query:** the `[slug]` page's own `data:`, else its parent page's (the usual shape, above), else `site.yml`'s; if none declares one, the query its sections all declare. The first query of that level wins. Every section the route query reaches gets the one record; a section declaring a *different* query of its own gets that query as declared. The folder name says what the URL segment matches: `[slug]` the record's handle (`$name`, which compiled records carry — equal to their `slug`), `[uuid]` its `$uuid`, any other `[name]` the record's own field of that name. A folder inside `[slug]/` (`[slug]/cv/` → `/blog/:slug/cv`) is a parametric page too, reading the record when `[slug]/page.yml` declares the query. `[dir]` and `[path]` are refused as folder names, and so is any folder inside `[...path]/`.
|
|
901
|
+
|
|
900
902
|
> **The record arrives as a single-element array under the query key** — `content.data.recent[0]`, not `content.data.article`. The runtime never coerces it to an object and never synthesizes a singular key. See *Data* in Part 4.
|
|
901
903
|
|
|
902
|
-
**Records with URLs of their own shape — `[...path]/`.** A folder named exactly `[...path]` (one fixed spelling) captures the rest of the URL: `/blog/my-post` and `/blog/rust/2025/my-post` both reach it. The capture yields three standard variables — `:path` (the whole capture), `:dir` (everything before the last segment), `:slug` (the last segment, the record's handle) — and the record is still delivered by
|
|
904
|
+
**Records with URLs of their own shape — `[...path]/`.** A folder named exactly `[...path]` (one fixed spelling) captures the rest of the URL: `/blog/my-post` and `/blog/rust/2025/my-post` both reach it. The capture yields three standard variables — `:path` (the whole capture), `:dir` (everything before the last segment), `:slug` (the last segment, the record's handle) — and the record is still delivered by its handle, so the section reads `content.data.recent[0]` as before. The same three variables exist under every parametric page: under `[slug]`, `:slug` and `:path` are the segment and `:dir` is empty. A record's URL is its folder placement plus its slug (`- folder: rust/2025` in `records.yml` → `/blog/rust/2025/my-post`). A query may bind a part — `scope: :dir` exposes the folder branch, `where: { tag: :dir }` keeps it private — and an unbound or empty variable drops its clause, so one saved query serves the list page and the parametric page, on a static site and a hosted one alike. Without `scope: :dir` the directory is decoration: the record is found by its handle wherever it sits. Reference: `reference/dynamic-routes.md`.
|
|
903
905
|
|
|
904
906
|
**Two options for bigger sets:**
|
|
905
907
|
|
|
@@ -1773,7 +1775,7 @@ Content-less containers appear as group nodes (`hasContent: false`) — use `nav
|
|
|
1773
1775
|
|
|
1774
1776
|
A component on a page with a `data:` or `fetch:` declaration automatically receives that data in `content.data.{key}` — no opt-in in `meta.js`.
|
|
1775
1777
|
|
|
1776
|
-
**Bound collections always arrive as arrays.** On a list page, `content.data.articles` is the full collection. On a
|
|
1778
|
+
**Bound collections always arrive as arrays.** On a list page, `content.data.articles` is the full collection. On a parametric page (`[slug]/`), the matched record is delivered under the *same* key as a single-element array — the detail section reads `content.data.articles[0]`. When nothing matches, the key is `[]`. The runtime never coerces to a single object and never synthesizes a singular key.
|
|
1777
1779
|
|
|
1778
1780
|
```jsx
|
|
1779
1781
|
function Article({ content, block }) {
|
|
@@ -2447,6 +2449,7 @@ uniweb add ci --target foundation # Publish a foundation for free at permanent v
|
|
|
2447
2449
|
# (GitHub Pages → foundations/<name>/<version>/entry.js)
|
|
2448
2450
|
|
|
2449
2451
|
uniweb push / pull / clone / status # Git-style content sync with the Uniweb backend
|
|
2452
|
+
uniweb refresh / sync # Catch up (git + backend, never pushes) / catch up, then push
|
|
2450
2453
|
uniweb push --org @acme # First push/publish of a site: who owns it (see below)
|
|
2451
2454
|
uniweb register [--scope @org] # Register a foundation + its data schemas to the registry
|
|
2452
2455
|
uniweb login / logout # Start or clear the backend session the verbs above reuse
|
|
@@ -2516,7 +2519,7 @@ Foundations have their own free path too: `uniweb add ci --target foundation` pu
|
|
|
2516
2519
|
- **Git is the reviewed, durable record** — you `pull` content back, read it with `git diff`, and commit.
|
|
2517
2520
|
- **Authors never push or pull.** For them, content simply updates, whether the change came from another author or from a developer's CLI.
|
|
2518
2521
|
|
|
2519
|
-
**
|
|
2522
|
+
**Nobody's work is overwritten without asking.** `uniweb push` is refused if the site changed on the backend since your last pull — usually an author editing in the app — and reports what changed; nothing is written. Edits to different sections never collide. `uniweb pull` overwrites rather than merges, so it refuses while you have uncommitted changes. The recovery for both is `uniweb pull --merge` (or `uniweb refresh`, which runs `git pull` first): changes to different parts of a file combine silently, and a genuine overlap leaves conflict markers and a non-zero exit — so `uniweb pull --merge && uniweb push` never ships markers. `--force` is the deliberate overwrite (on `push` it replaces the backend's changes, on `pull` it discards yours); don't use it to get past a refusal. `uniweb sync` is `refresh` then `push`; neither publishes.
|
|
2520
2523
|
|
|
2521
2524
|
**The Cloud also provides a real backend for structured data:** a database for every registered data schema, and a CMS that edits both static page content and dynamic data entities typed by those schemas. That's the piece that makes it viable for teams and client work — the client manages records, not markdown files.
|
|
2522
2525
|
|
|
@@ -34,7 +34,11 @@
|
|
|
34
34
|
import { mkdirSync, existsSync, writeFileSync } from 'node:fs'
|
|
35
35
|
import { dirname, join } from 'node:path'
|
|
36
36
|
import { ASSET_SLOTS } from '@uniweb/semantic-parser'
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
readAssetMap,
|
|
39
|
+
updateAssetMap,
|
|
40
|
+
servedFingerprint
|
|
41
|
+
} from '@uniweb/build/uwx'
|
|
38
42
|
|
|
39
43
|
/**
|
|
40
44
|
* Collect every `{ id, ext, url }` an entity document references.
|
|
@@ -43,7 +47,7 @@ import { readAssetMap, updateAssetMap } from '@uniweb/build/uwx'
|
|
|
43
47
|
* carrying the reference — so a ProseMirror image node's attrs and a section
|
|
44
48
|
* background's media object are both found by one walk.
|
|
45
49
|
*/
|
|
46
|
-
export function collectAssetRefs(document) {
|
|
50
|
+
export function collectAssetRefs(document, map = {}) {
|
|
47
51
|
const found = new Map() // id → { id, ext, url }
|
|
48
52
|
const visit = (node) => {
|
|
49
53
|
if (Array.isArray(node)) return node.forEach(visit)
|
|
@@ -59,6 +63,28 @@ export function collectAssetRefs(document) {
|
|
|
59
63
|
for (const v of Object.values(node)) visit(v)
|
|
60
64
|
}
|
|
61
65
|
visit(document)
|
|
66
|
+
// A BARE-STRING reference (`info.preview`, `seo.image`, a section param) has no
|
|
67
|
+
// object to carry identity beside it. The map recognizes it by the fingerprint the
|
|
68
|
+
// push recorded for its serve URL — see `servedFingerprint` in @uniweb/build/uwx.
|
|
69
|
+
const byServed = new Map()
|
|
70
|
+
for (const v of Object.values(map || {})) {
|
|
71
|
+
if (v?.served && v.id && !byServed.has(v.served)) byServed.set(v.served, v)
|
|
72
|
+
}
|
|
73
|
+
if (byServed.size) {
|
|
74
|
+
const visitStrings = (node) => {
|
|
75
|
+
if (typeof node === 'string') {
|
|
76
|
+
if (!node.startsWith('/') && !/^https?:\/\//i.test(node)) return
|
|
77
|
+
const entry = byServed.get(servedFingerprint(node))
|
|
78
|
+
if (entry && !found.has(entry.id))
|
|
79
|
+
found.set(entry.id, { id: entry.id, ext: entry.ext || '', url: node })
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(node)) return node.forEach(visitStrings)
|
|
83
|
+
if (node && typeof node === 'object')
|
|
84
|
+
for (const v of Object.values(node)) visitStrings(v)
|
|
85
|
+
}
|
|
86
|
+
visitStrings(document)
|
|
87
|
+
}
|
|
62
88
|
return [...found.values()]
|
|
63
89
|
}
|
|
64
90
|
|
|
@@ -89,11 +115,12 @@ export async function downloadMissingAssets({
|
|
|
89
115
|
warn = () => {}
|
|
90
116
|
}) {
|
|
91
117
|
const doFetch = fetchImpl || ((u) => globalThis.fetch(u))
|
|
92
|
-
|
|
118
|
+
// Read first: a bare-string reference is only recognizable through the map.
|
|
119
|
+
const map = readAssetMap(siteDir)
|
|
120
|
+
const refs = collectAssetRefs(document, map)
|
|
93
121
|
const out = { downloaded: [], present: [], failed: [], skipped: [] }
|
|
94
122
|
if (!refs.length) return out
|
|
95
123
|
|
|
96
|
-
const map = readAssetMap(siteDir)
|
|
97
124
|
const byId = new Map()
|
|
98
125
|
for (const [ref, v] of Object.entries(map)) if (v?.id) byId.set(v.id, ref)
|
|
99
126
|
|
|
@@ -16,6 +16,7 @@ import { createHash } from 'node:crypto'
|
|
|
16
16
|
import { existsSync, readFileSync } from 'node:fs'
|
|
17
17
|
import { basename } from 'node:path'
|
|
18
18
|
import { resolveAssetPath } from '@uniweb/build/site'
|
|
19
|
+
import { servedFingerprint } from '@uniweb/build/uwx'
|
|
19
20
|
import { contentTypeFor } from '../utils/code-upload.js'
|
|
20
21
|
import { humanBytes } from '../utils/bytes.js'
|
|
21
22
|
|
|
@@ -79,7 +80,17 @@ export async function uploadSiteMedia(
|
|
|
79
80
|
for (const ref of refs) {
|
|
80
81
|
const entry = result.assetsByLocalUrl[ref]
|
|
81
82
|
if (!entry) continue
|
|
82
|
-
|
|
83
|
+
// `served` — a fingerprint of the address, never the address. It is how a pull
|
|
84
|
+
// recognizes this asset where the reference is a bare string with no room for an
|
|
85
|
+
// id beside it (`info.preview`, `seo.image`, a section param); `assets.json`'s
|
|
86
|
+
// header says why a hash and not the URL.
|
|
87
|
+
if (entry.id) {
|
|
88
|
+
ids[ref] = {
|
|
89
|
+
id: entry.id,
|
|
90
|
+
ext: entry.ext || '',
|
|
91
|
+
...(entry.serveUrl ? { served: servedFingerprint(entry.serveUrl) } : {})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
83
94
|
// The backend's canonical serve URL, READ — never composed. An entry without
|
|
84
95
|
// one is an asset we cannot address, and inventing a location for it is the
|
|
85
96
|
// exact failure this lane exists to avoid: a guessed host is SILENTLY wrong,
|
package/src/backend/site-sync.js
CHANGED
|
@@ -29,7 +29,8 @@ import {
|
|
|
29
29
|
collectUnitUuids,
|
|
30
30
|
collectFolderItemUuids,
|
|
31
31
|
collectQueryUuids,
|
|
32
|
-
readAssetMap
|
|
32
|
+
readAssetMap,
|
|
33
|
+
removeYamlScalar
|
|
33
34
|
} from '@uniweb/build/uwx'
|
|
34
35
|
|
|
35
36
|
// First entity `$`-document out of a `.uwx` we produced or the backend served.
|
|
@@ -323,6 +324,51 @@ export function clearRemoteSyncStateIfUnbound(siteDir) {
|
|
|
323
324
|
return stale
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Drop the `site.yml` values that describe ONE PARTICULAR backend site, when this
|
|
329
|
+
* project is bound to none (no `$uuid`) — a brand-new site, or the state our own
|
|
330
|
+
* "clear `$uuid` to re-publish as a new site" recovery puts you in:
|
|
331
|
+
*
|
|
332
|
+
* · `$url` — where the PREVIOUS site was live. The new one is not live anywhere
|
|
333
|
+
* yet; its first publish records its own.
|
|
334
|
+
* · `preview` in the APP's form — a timestamp naming the previous site's generated
|
|
335
|
+
* card image, which is keyed by that site's uuid and would dangle on this one.
|
|
336
|
+
*
|
|
337
|
+
* ⛔ An AUTHOR's preview is theirs and stays: a URL, or a path to an image in the
|
|
338
|
+
* project, is as good for the new site as for the old.
|
|
339
|
+
*
|
|
340
|
+
* The site.yml sibling of `clearRemoteSyncStateIfUnbound`, called beside it for the
|
|
341
|
+
* same reason — before the create mints a uuid and makes the project look bound.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} siteDir
|
|
344
|
+
* @returns {string[]} the keys removed
|
|
345
|
+
*/
|
|
346
|
+
export function dropSiteBoundValues(siteDir) {
|
|
347
|
+
const file = join(siteDir, 'site.yml')
|
|
348
|
+
let y
|
|
349
|
+
try {
|
|
350
|
+
y = yaml.load(readFileSync(file, 'utf8'))
|
|
351
|
+
} catch {
|
|
352
|
+
return []
|
|
353
|
+
}
|
|
354
|
+
if (!y || typeof y !== 'object' || typeof y.$uuid === 'string') return []
|
|
355
|
+
const dropped = []
|
|
356
|
+
if (y.$url !== undefined && removeYamlScalar(file, '$url')) dropped.push('$url')
|
|
357
|
+
if (
|
|
358
|
+
y.preview !== undefined &&
|
|
359
|
+
!isAuthoredPreview(y.preview) &&
|
|
360
|
+
removeYamlScalar(file, 'preview')
|
|
361
|
+
) {
|
|
362
|
+
dropped.push('preview')
|
|
363
|
+
}
|
|
364
|
+
return dropped
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// An author's preview is an address — a URL, or a site-root / relative path. Anything
|
|
368
|
+
// else is the app's generated-image token.
|
|
369
|
+
const isAuthoredPreview = (v) =>
|
|
370
|
+
typeof v === 'string' && (/^https?:\/\//i.test(v) || /^\.{0,2}\//.test(v))
|
|
371
|
+
|
|
326
372
|
export function readSyncCache(siteDir) {
|
|
327
373
|
return readMap(siteDir, 'hashes')
|
|
328
374
|
}
|
package/src/commands/publish.js
CHANGED
|
@@ -87,6 +87,7 @@ import {
|
|
|
87
87
|
readFolderItemUuids,
|
|
88
88
|
ensureSiteExists,
|
|
89
89
|
clearRemoteSyncStateIfUnbound,
|
|
90
|
+
dropSiteBoundValues,
|
|
90
91
|
pushSyncPackages,
|
|
91
92
|
resolveSiteOrgForCreate
|
|
92
93
|
} from '../backend/site-sync.js'
|
|
@@ -578,6 +579,12 @@ export async function publish(args = []) {
|
|
|
578
579
|
`Cleared stale sync state from a previous site (${droppedState.join(', ')}).`
|
|
579
580
|
)
|
|
580
581
|
}
|
|
582
|
+
const droppedValues = dropSiteBoundValues(siteDir)
|
|
583
|
+
if (droppedValues.length) {
|
|
584
|
+
say.dim(
|
|
585
|
+
`Dropped the previous site's ${droppedValues.join(' and ')} from site.yml.`
|
|
586
|
+
)
|
|
587
|
+
}
|
|
581
588
|
|
|
582
589
|
// 3b. Make sure the SITE EXISTS before a single byte is uploaded.
|
|
583
590
|
//
|
|
@@ -1106,6 +1113,11 @@ export async function publish(args = []) {
|
|
|
1106
1113
|
say.dim(` site.yml lists ${unserved.join(', ')}, and ${unserved.length === 1 ? 'it was' : 'they were'} not published.`)
|
|
1107
1114
|
}
|
|
1108
1115
|
if (serveUrl) console.log(` ${c.cyan}${serveUrl}${c.reset}`)
|
|
1116
|
+
// ⛔ No site.yml write for where it went live. The backend records the reply's
|
|
1117
|
+
// `url` on `info.url` at every publish, and pull brings it into `site.yml::$url`
|
|
1118
|
+
// like any other key. A copy written here was a second writer that had to match
|
|
1119
|
+
// the backend's byte for byte (the address printed above is made absolute, so it
|
|
1120
|
+
// did not), and it left site.yml modified — which a plain `pull` refuses over.
|
|
1109
1121
|
if (result.deploy_uuid) say.dim(`deploy: ${result.deploy_uuid}`)
|
|
1110
1122
|
return { exitCode: 0 }
|
|
1111
1123
|
}
|
package/src/commands/push.js
CHANGED
|
@@ -89,6 +89,7 @@ import {
|
|
|
89
89
|
ensureItemUuids,
|
|
90
90
|
ensureSiteExists,
|
|
91
91
|
clearRemoteSyncStateIfUnbound,
|
|
92
|
+
dropSiteBoundValues,
|
|
92
93
|
pushSyncPackages,
|
|
93
94
|
resolveSiteOrgForCreate
|
|
94
95
|
} from '../backend/site-sync.js'
|
|
@@ -303,6 +304,10 @@ export async function push(args = [], deps = {}) {
|
|
|
303
304
|
`Cleared stale sync state from a previous site (${dropped.join(', ')}).`
|
|
304
305
|
)
|
|
305
306
|
}
|
|
307
|
+
const stale = dropSiteBoundValues(siteDir)
|
|
308
|
+
if (stale.length) {
|
|
309
|
+
note(`Dropped the previous site's ${stale.join(' and ')} from site.yml.`)
|
|
310
|
+
}
|
|
306
311
|
}
|
|
307
312
|
const priorHashes = readSyncCache(siteDir)
|
|
308
313
|
// Per-item identity, without which the backend reads every record as new and
|
package/src/framework-index.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-11T18:41:11.718Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.2",
|
|
7
7
|
"path": "framework/api",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/core"
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"@uniweb/build": {
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.45.0",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"deps": []
|
|
35
35
|
},
|
|
36
36
|
"@uniweb/core": {
|
|
37
|
-
"version": "0.
|
|
37
|
+
"version": "0.25.0",
|
|
38
38
|
"path": "framework/core",
|
|
39
39
|
"deps": [
|
|
40
40
|
"@uniweb/semantic-parser",
|
|
@@ -47,14 +47,14 @@
|
|
|
47
47
|
"deps": []
|
|
48
48
|
},
|
|
49
49
|
"@uniweb/icons": {
|
|
50
|
-
"version": "0.4.
|
|
50
|
+
"version": "0.4.16",
|
|
51
51
|
"path": "framework/icons",
|
|
52
52
|
"deps": [
|
|
53
53
|
"@uniweb/core"
|
|
54
54
|
]
|
|
55
55
|
},
|
|
56
56
|
"@uniweb/kit": {
|
|
57
|
-
"version": "0.18.
|
|
57
|
+
"version": "0.18.3",
|
|
58
58
|
"path": "framework/kit",
|
|
59
59
|
"deps": [
|
|
60
60
|
"@uniweb/core",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"deps": []
|
|
75
75
|
},
|
|
76
76
|
"@uniweb/projections": {
|
|
77
|
-
"version": "0.6.
|
|
77
|
+
"version": "0.6.1",
|
|
78
78
|
"path": "framework/projections",
|
|
79
79
|
"deps": [
|
|
80
80
|
"@uniweb/content-writer",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
]
|
|
83
83
|
},
|
|
84
84
|
"@uniweb/runtime": {
|
|
85
|
-
"version": "0.
|
|
85
|
+
"version": "0.20.0",
|
|
86
86
|
"path": "framework/runtime",
|
|
87
87
|
"deps": [
|
|
88
88
|
"@uniweb/core",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"deps": []
|
|
111
111
|
},
|
|
112
112
|
"@uniweb/templates": {
|
|
113
|
-
"version": "0.12.
|
|
113
|
+
"version": "0.12.5",
|
|
114
114
|
"path": "framework/templates",
|
|
115
115
|
"deps": []
|
|
116
116
|
},
|