uniweb 0.67.0 → 0.68.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 +4 -4
- package/partials/agents.md +21 -2
- package/src/backend/site-sync.js +80 -1
- package/src/commands/publish.js +30 -16
- package/src/commands/push.js +21 -8
- package/src/framework-index.json +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.68.0",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
44
|
"@uniweb/content-writer": "^0.3.4",
|
|
45
45
|
"@uniweb/core": "^0.29.8",
|
|
46
|
-
"@uniweb/kit": "^0.19.4",
|
|
47
|
-
"@uniweb/schemas": "^0.6.0",
|
|
48
46
|
"@uniweb/runtime": "^0.27.4",
|
|
47
|
+
"@uniweb/kit": "^0.19.4",
|
|
48
|
+
"@uniweb/schemas": "^0.7.0",
|
|
49
49
|
"@uniweb/semantic-parser": "^1.4.1"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@uniweb/build": "^0.
|
|
52
|
+
"@uniweb/build": "^0.62.0",
|
|
53
53
|
"@uniweb/content-reader": "^1.2.5",
|
|
54
54
|
"@uniweb/semantic-parser": "^1.4.1"
|
|
55
55
|
},
|
package/partials/agents.md
CHANGED
|
@@ -879,6 +879,25 @@ Keep those folders flat: `records/article/design-tips.md` works; `records/articl
|
|
|
879
879
|
|
|
880
880
|
Item frontmatter conventionally uses `title`, `date`, `tags`, `image`, `description`, `author` — plus any fields your content needs (`price`, `role`, `order`). Images can sit beside the item file and be referenced with `./`. **`draft: true` keeps a record off the live site without deleting it**: it is still a record, in `records/`, and `pnpm dev` shows it so you can preview it, but a build for hosting leaves it out, and a backend you `uniweb push` it to keeps it in the site's folder without delivering it. (`published: false`, an older spelling, is refused.)
|
|
881
881
|
|
|
882
|
+
**A record's file follows its data schema's sections.** A schema with one section (the `fields:` form) is written flat, as above. A schema with more than one is written **by section** — each field under its section's name — and a field left at the top stops the build and a push, naming where it goes. `@std/article` has two, its card `article` and `article_body`:
|
|
883
|
+
|
|
884
|
+
```markdown
|
|
885
|
+
<!-- records/std/article/hello.md -->
|
|
886
|
+
---
|
|
887
|
+
article:
|
|
888
|
+
title: Hello
|
|
889
|
+
date: 2026-05-01
|
|
890
|
+
article_body:
|
|
891
|
+
author: Ada
|
|
892
|
+
---
|
|
893
|
+
|
|
894
|
+
The body — the value of the schema's content field, `article_body.content`.
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
**A component receives it differently from how the file writes it**, the same way on a static site and a hosted one: the brief's fields at the top (`article.title`), every other section under its name (`article.article_body.content`, `article.article_body.author`), and `$name`, the handle. Lists carry the brief alone unless the query says otherwise (`deferred:`, below). Full rules: `reference/entity-content.md`.
|
|
898
|
+
|
|
899
|
+
**A reference names the record it points at.** A `{ ref: '@/speaker' }` field is written as that record's name — `speaker: ada` for `records/speaker/ada.yml` (its file's name, or its `slug:`) — and a `many` one as a list of names. A component receives `{ entity, brief }`: the record reduced to its brief, so `talk.speaker.brief.name`. `uniweb validate` and a push report a name that matches no record; a push sends each reference to the record it names (a second pass when that record is new too), and a pull writes the name back.
|
|
900
|
+
|
|
882
901
|
**`records/folder.yml` — optional, and only for folders.** It is the one file in `records/` that is not a record, it moves with the directory (`paths.records`), and like a pages folder's `folder.yml` it describes the folder it sits in. Every record sits at the top of the site's records folder unless `folder.yml` places it in a sub-folder. Add one only when a query needs to ask for a *slice* of the records rather than all of them — most sites never do:
|
|
883
902
|
|
|
884
903
|
```yaml
|
|
@@ -946,7 +965,7 @@ pages/blog/
|
|
|
946
965
|
|
|
947
966
|
**Two options for bigger sets:**
|
|
948
967
|
|
|
949
|
-
`deferred: [body]` strips heavy fields from the list payload — cards stay light, while a `[slug]` page still receives the full record automatically and other components fetch the whole record on demand with `useWholeRecord`. File-based records emit per-record files at `/data/<name>/<slug>.json` and need no configuration; an external query names its one-record request with `record:` instead (*Fetching from other sources* in Part 4).
|
|
968
|
+
`deferred: [body]` strips heavy fields from the list payload — cards stay light, while a `[slug]` page still receives the full record automatically and other components fetch the whole record on demand with `useWholeRecord`. It names keys of the record as a component receives it — a field, or a section's name — and a schema with more than one section implies it: every section but the brief (`[article_body]` for `@std/article`), so it is rarely written. File-based records emit per-record files at `/data/<name>/<slug>.json` and need no configuration; an external query names its one-record request with `record:` instead (*Fetching from other sources* in Part 4).
|
|
950
969
|
|
|
951
970
|
`queryable:` declares which fields a reader may filter on, with enough metadata for the foundation to render controls:
|
|
952
971
|
|
|
@@ -1866,7 +1885,7 @@ fetch:
|
|
|
1866
1885
|
limit: 3
|
|
1867
1886
|
```
|
|
1868
1887
|
|
|
1869
|
-
**Lean lists with `deferred:`.** A query over records with heavy fields (article bodies, large nested arrays) can declare `deferred: [body]
|
|
1888
|
+
**Lean lists with `deferred:`.** A query over records with heavy fields (article bodies, large nested arrays) can declare `deferred: [body]` — keys of the record as a component receives it, a section's name included; a schema with more than one section implies every section but its brief. The list payload omits those fields; per-record full files are emitted at `/data/<name>/<slug>.json`. (An external query declares no `deferred:` — its whole record comes from `record:`, below.) On a parametric page the record's full data is delivered automatically; elsewhere components fetch the whole record on demand with `useWholeRecord(record, { query })`. The hook is safe to call on any query: when the query has no separate source for the whole record it returns the record you passed in, because nothing was stripped from it.
|
|
1870
1889
|
|
|
1871
1890
|
**Component-side fetching.** When a component genuinely needs to fetch on its own (a search box, "load more", a lazy popover), use the kit hooks — `useFetched`, `useCacheEntry`, `useWholeRecord`. They share the framework's cache and dispatcher with declarative fetches; same-key requests dedupe automatically.
|
|
1872
1891
|
|
package/src/backend/site-sync.js
CHANGED
|
@@ -1186,8 +1186,12 @@ export async function probeUnpushed(siteDir, { backend = null, sendAll = false }
|
|
|
1186
1186
|
// ⚠️ Until 2026-09-23 this said the site's recorded ORG resolves it and must be
|
|
1187
1187
|
// passed in — true before 2026-09-22, and read as a reason to pass it.
|
|
1188
1188
|
const pkg = await comparisonEmit(siteDir, { backend, priorHashes, sendAll })
|
|
1189
|
+
// A record held back for a reference (`pushInPasses`) is not in either lane, and is
|
|
1190
|
+
// still unpushed.
|
|
1189
1191
|
const changed =
|
|
1190
|
-
(pkg.siteContent?.entityCount || 0) +
|
|
1192
|
+
(pkg.siteContent?.entityCount || 0) +
|
|
1193
|
+
(pkg.records?.entityCount || 0) +
|
|
1194
|
+
(pkg.waiting || []).filter((w) => w.held).length
|
|
1191
1195
|
return { changed, unchanged: pkg.skipped || 0, warnings: pkg.warnings || [] }
|
|
1192
1196
|
}
|
|
1193
1197
|
|
|
@@ -1846,3 +1850,78 @@ export async function pushSyncPackages({
|
|
|
1846
1850
|
}
|
|
1847
1851
|
return { exitCode: 0, boundSiteUuid, finalizedTotal, wrote }
|
|
1848
1852
|
}
|
|
1853
|
+
|
|
1854
|
+
/**
|
|
1855
|
+
* Push, then push again while records WAIT for a reference's record — the verbs' way to
|
|
1856
|
+
* send a package (`push`, `publish`).
|
|
1857
|
+
*
|
|
1858
|
+
* ⭐ A reference is sent as the uuid the backend minted for the record it names, so a
|
|
1859
|
+
* record that points at one created by the same push cannot be completed in that push
|
|
1860
|
+
* (`@uniweb/build`'s `emitSyncPackages` → `waiting`). The first pass creates what it can —
|
|
1861
|
+
* records whose waiting references are not required go without them — and each further
|
|
1862
|
+
* pass, emitted afresh from the state the last one banked, sends what has become
|
|
1863
|
+
* nameable. A chain of required references takes one pass per link; two records that
|
|
1864
|
+
* each require the other can never be completed, and the push stops, naming them.
|
|
1865
|
+
*
|
|
1866
|
+
* @param {object} params
|
|
1867
|
+
* @param {object} params.pkg - the first pass's package (`emitSyncPackages`)
|
|
1868
|
+
* @param {() => Promise<object>} params.reemit - the next pass's package, emitted afresh
|
|
1869
|
+
* @param {Function} [params.push] - sends one package; `pushSyncPackages` (a test's seam)
|
|
1870
|
+
* @returns {Promise<{ exitCode: number, boundSiteUuid?: string, finalizedTotal: number, wrote: string[], passes: number }>}
|
|
1871
|
+
*/
|
|
1872
|
+
export async function pushInPasses({ client, siteDir, pkg, report, reemit, maxPasses = 12, push = pushSyncPackages }) {
|
|
1873
|
+
const { note, error } = report
|
|
1874
|
+
let current = pkg
|
|
1875
|
+
let passes = 1
|
|
1876
|
+
let result = await push({ client, siteDir, pkg: current, report })
|
|
1877
|
+
let finalizedTotal = result.finalizedTotal || 0
|
|
1878
|
+
const wrote = [...(result.wrote || [])]
|
|
1879
|
+
while (result.exitCode === 0 && current.waiting?.length) {
|
|
1880
|
+
const n = current.waiting.length
|
|
1881
|
+
note(
|
|
1882
|
+
`${n} record${n === 1 ? '' : 's'} name${n === 1 ? 's' : ''} a record this push created — ` +
|
|
1883
|
+
`pushing again to complete ${n === 1 ? 'its references' : 'their references'}.`
|
|
1884
|
+
)
|
|
1885
|
+
let next
|
|
1886
|
+
try {
|
|
1887
|
+
next = await reemit()
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
error(`Could not build the next pass: ${err.message}`)
|
|
1890
|
+
return { ...result, exitCode: 1, finalizedTotal, wrote, passes }
|
|
1891
|
+
}
|
|
1892
|
+
if (refuseUnsendableRecords(next.refusals, report)) {
|
|
1893
|
+
return { ...result, exitCode: 1, finalizedTotal, wrote, passes }
|
|
1894
|
+
}
|
|
1895
|
+
// Nothing became nameable: every record still waiting waits on one that waits too.
|
|
1896
|
+
if (waitingKey(next.waiting) === waitingKey(current.waiting) || passes >= maxPasses) {
|
|
1897
|
+
error(
|
|
1898
|
+
'These records could not be completed — each requires a reference to a record that ' +
|
|
1899
|
+
'is itself waiting to be created:'
|
|
1900
|
+
)
|
|
1901
|
+
for (const w of next.waiting || []) {
|
|
1902
|
+
const refs = w.pending.map((p) => `${p.path} → "${p.name}"`).join(', ')
|
|
1903
|
+
note(` ${w.id} (${w.model}): ${refs}`)
|
|
1904
|
+
}
|
|
1905
|
+
note(
|
|
1906
|
+
'Push one of them without that reference first — or make it optional in the schema — ' +
|
|
1907
|
+
'then add it back and push again.'
|
|
1908
|
+
)
|
|
1909
|
+
return { ...result, exitCode: 1, finalizedTotal, wrote, passes }
|
|
1910
|
+
}
|
|
1911
|
+
current = next
|
|
1912
|
+
passes++
|
|
1913
|
+
result = await push({ client, siteDir, pkg: current, report })
|
|
1914
|
+
finalizedTotal += result.finalizedTotal || 0
|
|
1915
|
+
for (const w of result.wrote || []) if (!wrote.includes(w)) wrote.push(w)
|
|
1916
|
+
}
|
|
1917
|
+
return { ...result, finalizedTotal, wrote, passes }
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// Which records wait, and on what — two passes that leave it the same made no progress.
|
|
1921
|
+
function waitingKey(waiting) {
|
|
1922
|
+
return JSON.stringify(
|
|
1923
|
+
(waiting || [])
|
|
1924
|
+
.map((w) => [w.model, w.id, w.pending.map((p) => `${p.path}:${p.name}`).sort()])
|
|
1925
|
+
.sort()
|
|
1926
|
+
)
|
|
1927
|
+
}
|
package/src/commands/publish.js
CHANGED
|
@@ -90,7 +90,8 @@ import {
|
|
|
90
90
|
refuseUnsendableRecords,
|
|
91
91
|
clearRemoteSyncStateIfUnbound,
|
|
92
92
|
dropSiteBoundValues,
|
|
93
|
-
|
|
93
|
+
readItemUuids,
|
|
94
|
+
pushInPasses
|
|
94
95
|
} from '../backend/site-sync.js'
|
|
95
96
|
import { resolveWorkspace, describeWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
|
|
96
97
|
import { uploadSiteMedia, describeAssetRefusal } from '../backend/site-media.js'
|
|
@@ -767,12 +768,6 @@ export async function publish(args = []) {
|
|
|
767
768
|
// (It stamped `info.data_bundle` until 2026-08-18; the ball is gone and
|
|
768
769
|
// collection data now lands at its serving tail, so nothing records it.)
|
|
769
770
|
const priorHashes = readSyncCache(siteDir, client.origin)
|
|
770
|
-
// publish rides the same gated push as `uniweb push`: if an app author has
|
|
771
|
-
// edited since this clone last synced, the push is refused rather than
|
|
772
|
-
// overwriting them, and nothing goes live. `--force` drops the precondition.
|
|
773
|
-
const baseVersions = args.includes('--force')
|
|
774
|
-
? null
|
|
775
|
-
: readBaseVersions(siteDir, client.origin)
|
|
776
771
|
// Per-item identity, recovered from the backend when this clone has never seen it.
|
|
777
772
|
// Without it the backend re-mints every page and section row (see readItemUuids).
|
|
778
773
|
const itemUuids = await ensureItemUuids({
|
|
@@ -948,9 +943,14 @@ export async function publish(args = []) {
|
|
|
948
943
|
say.dim('Services unchanged.')
|
|
949
944
|
}
|
|
950
945
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
946
|
+
// The emit's options, read afresh for each pass (`pushInPasses`): a pass banks
|
|
947
|
+
// hashes, identity and base versions that the next one must build on.
|
|
948
|
+
//
|
|
949
|
+
// publish rides the same gated push as `uniweb push`: if an app author has
|
|
950
|
+
// edited since this clone last synced, the push is refused rather than
|
|
951
|
+
// overwriting them, and nothing goes live. `--force` drops the precondition.
|
|
952
|
+
const forced = args.includes('--force')
|
|
953
|
+
const emitOptions = ({ priorHashes, itemUuids }) => ({
|
|
954
954
|
backend: client.origin,
|
|
955
955
|
...(declaration.declare ? {} : { declareServices: false }),
|
|
956
956
|
// Placement identity for the folder — see writeFolderItemUuids.
|
|
@@ -959,14 +959,20 @@ export async function publish(args = []) {
|
|
|
959
959
|
resolveModel,
|
|
960
960
|
priorHashes,
|
|
961
961
|
itemUuids,
|
|
962
|
-
...(
|
|
963
|
-
? {
|
|
964
|
-
: {
|
|
962
|
+
...(forced
|
|
963
|
+
? {}
|
|
964
|
+
: {
|
|
965
|
+
baseVersions: readBaseVersions(siteDir, client.origin),
|
|
966
|
+
itemBaseVersions: readItemBaseVersions(siteDir, client.origin)
|
|
967
|
+
}),
|
|
965
968
|
...(Object.keys(injectInfo).length ? { injectInfo } : {}),
|
|
966
969
|
...(Object.keys(ext.pins).length ? { injectExtensions: ext.pins } : {}),
|
|
967
970
|
...(assetRewrite ? { assetRewrite } : {}),
|
|
968
971
|
...(assetIds ? { assetIds } : {})
|
|
969
|
-
|
|
972
|
+
})
|
|
973
|
+
let pkg
|
|
974
|
+
try {
|
|
975
|
+
pkg = await emitSyncPackages(siteDir, emitOptions({ priorHashes, itemUuids }))
|
|
970
976
|
} catch (err) {
|
|
971
977
|
say.err(`Could not build the sync package: ${err.message}`)
|
|
972
978
|
return { exitCode: 1 }
|
|
@@ -981,11 +987,19 @@ export async function publish(args = []) {
|
|
|
981
987
|
error: (m) => say.err(m),
|
|
982
988
|
dim: (s) => `${c.dim}${s}${c.reset}`
|
|
983
989
|
}
|
|
984
|
-
const pushResult = await
|
|
990
|
+
const pushResult = await pushInPasses({
|
|
985
991
|
client,
|
|
986
992
|
siteDir,
|
|
987
993
|
pkg,
|
|
988
|
-
report
|
|
994
|
+
report,
|
|
995
|
+
reemit: () =>
|
|
996
|
+
emitSyncPackages(
|
|
997
|
+
siteDir,
|
|
998
|
+
emitOptions({
|
|
999
|
+
priorHashes: readSyncCache(siteDir, client.origin),
|
|
1000
|
+
itemUuids: readItemUuids(siteDir, client.origin)
|
|
1001
|
+
})
|
|
1002
|
+
)
|
|
989
1003
|
})
|
|
990
1004
|
if (pushResult.exitCode !== 0) return { exitCode: pushResult.exitCode }
|
|
991
1005
|
const siteUuid = pushResult.boundSiteUuid
|
package/src/commands/push.js
CHANGED
|
@@ -87,7 +87,7 @@ import {
|
|
|
87
87
|
ensureSiteExists,
|
|
88
88
|
clearRemoteSyncStateIfUnbound,
|
|
89
89
|
dropSiteBoundValues,
|
|
90
|
-
|
|
90
|
+
pushInPasses
|
|
91
91
|
} from '../backend/site-sync.js'
|
|
92
92
|
import { resolveWorkspace, describeWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
|
|
93
93
|
|
|
@@ -496,9 +496,9 @@ export async function push(args = [], deps = {}) {
|
|
|
496
496
|
output || dryRun
|
|
497
497
|
? readItemUuids(siteDir, client.origin)
|
|
498
498
|
: await ensureItemUuids({ client, siteDir, note })
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
499
|
+
// The emit's options, read afresh for each pass (`pushInPasses`): a pass banks
|
|
500
|
+
// hashes, identity and base versions that the next one must build on.
|
|
501
|
+
const emitOptions = ({ priorHashes, sendAll, itemUuids }) => ({
|
|
502
502
|
backend: client.origin,
|
|
503
503
|
// Placement identity for the folder — see writeFolderItemUuids.
|
|
504
504
|
folderItemUuids: readFolderItemUuids(siteDir, client.origin),
|
|
@@ -529,7 +529,10 @@ export async function push(args = [], deps = {}) {
|
|
|
529
529
|
}),
|
|
530
530
|
...(assetRewrite ? { assetRewrite } : {}),
|
|
531
531
|
...(assetIds ? { assetIds } : {})
|
|
532
|
-
|
|
532
|
+
})
|
|
533
|
+
let pkg
|
|
534
|
+
try {
|
|
535
|
+
pkg = await emitSyncPackages(siteDir, emitOptions({ priorHashes, sendAll, itemUuids }))
|
|
533
536
|
} catch (err) {
|
|
534
537
|
error(`Could not build the sync package: ${err.message}`)
|
|
535
538
|
return { exitCode: 2 }
|
|
@@ -595,8 +598,9 @@ export async function push(args = [], deps = {}) {
|
|
|
595
598
|
}
|
|
596
599
|
|
|
597
600
|
// Submit both lanes, back-fill the minted uuids, and persist the send-only-changed
|
|
598
|
-
// cache
|
|
599
|
-
|
|
601
|
+
// cache — again for any record whose reference names a record this push created.
|
|
602
|
+
// Shared with `uniweb publish` via ../backend/site-sync.js.
|
|
603
|
+
const result = await pushInPasses({
|
|
600
604
|
client,
|
|
601
605
|
siteDir,
|
|
602
606
|
pkg,
|
|
@@ -605,7 +609,16 @@ export async function push(args = [], deps = {}) {
|
|
|
605
609
|
note,
|
|
606
610
|
error,
|
|
607
611
|
dim: (s) => `${colors.dim}${s}${colors.reset}`
|
|
608
|
-
}
|
|
612
|
+
},
|
|
613
|
+
reemit: () =>
|
|
614
|
+
emitSyncPackages(
|
|
615
|
+
siteDir,
|
|
616
|
+
emitOptions({
|
|
617
|
+
priorHashes: readSyncCache(siteDir, client.origin),
|
|
618
|
+
sendAll: false,
|
|
619
|
+
itemUuids: readItemUuids(siteDir, client.origin)
|
|
620
|
+
})
|
|
621
|
+
)
|
|
609
622
|
})
|
|
610
623
|
if (result.exitCode !== 0) return { exitCode: result.exitCode }
|
|
611
624
|
success(
|
package/src/framework-index.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-24T21:52:18.260Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
|
-
"version": "0.6.
|
|
6
|
+
"version": "0.6.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.62.0",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"deps": []
|
|
97
97
|
},
|
|
98
98
|
"@uniweb/schemas": {
|
|
99
|
-
"version": "0.
|
|
99
|
+
"version": "0.7.0",
|
|
100
100
|
"path": "framework/schemas",
|
|
101
101
|
"deps": []
|
|
102
102
|
},
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"deps": []
|
|
117
117
|
},
|
|
118
118
|
"@uniweb/templates": {
|
|
119
|
-
"version": "0.
|
|
119
|
+
"version": "0.17.0",
|
|
120
120
|
"path": "framework/templates",
|
|
121
121
|
"deps": []
|
|
122
122
|
},
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"deps": []
|
|
127
127
|
},
|
|
128
128
|
"@uniweb/unipress": {
|
|
129
|
-
"version": "0.10.
|
|
129
|
+
"version": "0.10.5",
|
|
130
130
|
"path": "framework/unipress",
|
|
131
131
|
"deps": [
|
|
132
132
|
"@uniweb/build",
|