uniweb 0.12.56 → 0.12.58
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 +5 -5
- package/partials/agents.md +66 -4
- package/src/commands/build.js +17 -2
- package/src/commands/i18n.js +2 -1
- package/src/commands/publish.js +4 -1
- package/src/commands/register.js +11 -0
- package/src/framework-index.json +7 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.58",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/core": "0.7.
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
44
|
+
"@uniweb/core": "0.7.24",
|
|
45
|
+
"@uniweb/runtime": "0.8.30",
|
|
46
|
+
"@uniweb/kit": "0.9.31"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@uniweb/build": "0.14.
|
|
49
|
+
"@uniweb/build": "0.14.38",
|
|
50
50
|
"@uniweb/content-reader": "1.1.14",
|
|
51
51
|
"@uniweb/semantic-parser": "1.1.18"
|
|
52
52
|
},
|
package/partials/agents.md
CHANGED
|
@@ -217,7 +217,7 @@ The `uniweb` block in `package.json` carries platform-specific configuration tha
|
|
|
217
217
|
| `namespace` | `uniweb register` | (none — see scope resolution) | Legacy explicit org-namespace override. Equivalent to using a scoped `package.json::name` (`"@myorg/foundation"`). Rarely needed in modern foundations. |
|
|
218
218
|
| `runtimePolicy` | `dist/runtime-pin.json` (foundation build) | `"auto-minor"` | Controls how sites using this foundation receive runtime updates. Three values: `"exact"`, `"auto-patch"`, `"auto-minor"`. See "Foundation runtime policy" below. |
|
|
219
219
|
|
|
220
|
-
**How a foundation reaches the catalog.** Foundations on Uniweb hosting always live in the catalog as `@org/name@version`. Two ways to get one there, both from the same `dist/
|
|
220
|
+
**How a foundation reaches the catalog.** Foundations on Uniweb hosting always live in the catalog as `@org/name@version`. Two ways to get one there, both from the same `dist/entry.js` artifact:
|
|
221
221
|
|
|
222
222
|
- **Brought along by `uniweb publish`.** When a foundation powers a single site, don't run `uniweb register` yourself. Run `uniweb publish` from the site directory — it releases the site's local foundation to the catalog under your `@org` (when its code changed) and then goes live, in one step. No separate register ceremony.
|
|
223
223
|
- **Registered deliberately with `uniweb register --scope @org`.** When the foundation is a product meant for multiple sites — listed in the catalog, consumable by other developers' sites — register it on its own schedule. Consuming sites then pin a versioned ref (`foundation: '@org/name@1.2.3'`).
|
|
@@ -845,6 +845,8 @@ seo:
|
|
|
845
845
|
|
|
846
846
|
**Localized URLs:** On a multilingual site (`languages:` in site.yml), a page's `slug: { <lang>: <segment> }` gives it a native URL segment per language (`/about` → `/fr/a-propos`); the folder name stays the canonical route. Nested folders compose automatically, and localized URLs flow through navigation, the language switcher, and the sitemap.
|
|
847
847
|
|
|
848
|
+
**Draft languages:** `publishLanguages: [en, fr]` in site.yml lists which declared languages a published build ships — unlisted ones stay dev-previewable drafts. Absent field = publish all declared; the default language must be listed.
|
|
849
|
+
|
|
848
850
|
**Content-less containers:** Folders with `page.yml` but no markdown are structural groups. They appear in `getPageHierarchy()` with `hasContent: false` and their own title/label. When visited directly, the runtime auto-redirects to the first descendant with content. This supports hierarchical navigation (courses → modules → lessons) at any depth.
|
|
849
851
|
|
|
850
852
|
### Lists as Navigation Menus
|
|
@@ -976,6 +978,10 @@ contexts:
|
|
|
976
978
|
light:
|
|
977
979
|
section: '#fafaf9' # Override individual tokens per context
|
|
978
980
|
|
|
981
|
+
appearance:
|
|
982
|
+
default: light # 'light' | 'dark' | 'system'; see Light/dark appearance below
|
|
983
|
+
allowToggle: true # offer a visitor light/dark switch
|
|
984
|
+
|
|
979
985
|
fonts:
|
|
980
986
|
import:
|
|
981
987
|
- url: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'
|
|
@@ -1015,6 +1021,60 @@ contexts:
|
|
|
1015
1021
|
|
|
1016
1022
|
> **Contrast warning:** Bright brand colors (orange, yellow, light green) at shade 500 may not meet WCAG contrast (4.5:1) with white foreground text. Test buttons for readability — if contrast is insufficient, keep the default shade 600 mapping.
|
|
1017
1023
|
|
|
1024
|
+
### Light/dark appearance (scheme vs. context)
|
|
1025
|
+
|
|
1026
|
+
Uniweb splits what other frameworks fuse into one "dark mode" into two independent ideas:
|
|
1027
|
+
|
|
1028
|
+
- **Section context** — the `theme:` field on a section (`light` / `medium` / `dark`). It sets the color tokens for *that one section*, so a single page can move through light, dark, and colored sections. Per-section, author-controlled, covered above.
|
|
1029
|
+
- **Site scheme** — the global light/dark preference, configured under `appearance:` in `theme.yml`. This is the site-wide toggle a visitor flips (or that follows their OS).
|
|
1030
|
+
|
|
1031
|
+
They compose rather than fight: a section with **no `theme:`** inherits the site scheme, so it follows the toggle; a section that **pins** `theme: dark` stays dark in either scheme. A dark-scheme site can still carry one bright white CTA; a light-scheme site can carry one dramatic dark hero. You never write `isDark ? … : …` — components built on semantic tokens (`text-heading`, `bg-card`, `border`, …) adapt to whichever scheme and context resolve around them.
|
|
1032
|
+
|
|
1033
|
+
Configure the scheme in `theme.yml`:
|
|
1034
|
+
|
|
1035
|
+
```yaml
|
|
1036
|
+
appearance:
|
|
1037
|
+
default: light # 'light' | 'dark' | 'system' (start by following the OS)
|
|
1038
|
+
allowToggle: true # offer a visitor switch (also generates the dark tokens)
|
|
1039
|
+
respectSystemPreference: true # first visit follows the OS; set false to pin `default`
|
|
1040
|
+
schemes: [light, dark] # optional — declares the available set
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
Shorthand strings work too: `appearance: light` / `appearance: dark` (fixed, no toggle), `appearance: system` (follow the OS).
|
|
1044
|
+
|
|
1045
|
+
A site **has dark mode** whenever it offers a toggle, defaults to `dark`/`system`, or lists `dark` in `schemes:`. Any such site follows the visitor's OS on first visit (unless `respectSystemPreference: false`) and remembers a manual choice. Precedence: a stored choice wins over the OS, which wins over `default:`.
|
|
1046
|
+
|
|
1047
|
+
Common intents:
|
|
1048
|
+
|
|
1049
|
+
| Goal | `theme.yml` |
|
|
1050
|
+
|---|---|
|
|
1051
|
+
| Always light, no switch | `appearance: light` |
|
|
1052
|
+
| Always dark, no switch | `appearance: dark` |
|
|
1053
|
+
| Toggle, follow the OS on first visit | `appearance: { default: system, allowToggle: true }` |
|
|
1054
|
+
| Toggle, but always start light | `appearance: { default: light, allowToggle: true, respectSystemPreference: false }` |
|
|
1055
|
+
|
|
1056
|
+
Note that `default: light` is a *fallback*, not a guarantee: a dark OS still wins on first visit unless you set `respectSystemPreference: false` (the last row). `default: system` is just the honest way to say "there is no fixed default — use the OS."
|
|
1057
|
+
|
|
1058
|
+
**Rendering a toggle (foundation side).** The runtime resolves the scheme and applies it to `<html>` (`scheme-dark` / `scheme-light`) *before the page paints* — you never touch `localStorage` or `document` yourself, and there's no flash of the wrong scheme. A section type only renders the button:
|
|
1059
|
+
|
|
1060
|
+
```jsx
|
|
1061
|
+
import { useAppearance } from '@uniweb/kit'
|
|
1062
|
+
|
|
1063
|
+
function SchemeToggle() {
|
|
1064
|
+
const { scheme, toggle, canToggle } = useAppearance()
|
|
1065
|
+
if (!canToggle) return null // hidden unless the site enables toggling
|
|
1066
|
+
return <button onClick={toggle}>{scheme === 'dark' ? 'Light' : 'Dark'}</button>
|
|
1067
|
+
}
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
**Tailwind `dark:` variant.** If your foundation uses Tailwind's `dark:` utilities, bind them to the site scheme (not the OS media query) so they track the toggle. In `styles.css`:
|
|
1071
|
+
|
|
1072
|
+
```css
|
|
1073
|
+
@custom-variant dark (&:where(.scheme-dark, .scheme-dark *));
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
Without this, `dark:` defaults to `@media (prefers-color-scheme: dark)` and ignores the site's own light/dark setting. Prefer semantic tokens over `dark:` where you can — they adapt to per-section context, which a global `dark:` cannot.
|
|
1077
|
+
|
|
1018
1078
|
### Fonts
|
|
1019
1079
|
|
|
1020
1080
|
Font families are a **site** setting, configured in `theme.yml` under `fonts:`. A foundation never installs a font package (no `@fontsource/*`) and never hardcodes a family in its code — if you reach for either, you're off the paved path. The framework provides three roles and wires each onto the standard elements for you:
|
|
@@ -1201,7 +1261,7 @@ export default {
|
|
|
1201
1261
|
export default { '@acme': '../shared/acme-schemas', '@brand': process.env.BRAND_SCHEMAS }
|
|
1202
1262
|
```
|
|
1203
1263
|
|
|
1204
|
-
Plain JS, so paths can be relative, absolute, or read from an env var. A routed scope wins over the package convention; `@/` and `@uniweb` are never routable; an empty value (e.g. an unset env var) falls back to the package convention.
|
|
1264
|
+
Plain JS, so paths can be relative, absolute, or read from an env var. A routed scope wins over the package convention; `@/` and `@uniweb` are never routable; an empty value (e.g. an unset env var) falls back to the package convention. A routed scope has no package fallback for a *missing* schema (it errors rather than silently load a different definition). To override a single schema — keeping the rest of the scope routed to the shared folder — add a per-schema key pointing at a file: `'@acme/person': './schemas/acme-person.yml'` (most-specific wins: file › directory › package).
|
|
1205
1265
|
|
|
1206
1266
|
**Validate your data.** `uniweb validate` checks your file-based data against these declared schemas — missing required fields, type/enum/format mismatches, nested fields — before you ship. Warns by default, `--strict` for a non-zero CI exit. It's distinct from `uniweb doctor` (which checks project structure): `validate` checks your *data* against the schemas you *declared*. Remote (`url:`), `ref`/`options`, and rich `sections`-form inputs are reported deferred — validate those against live data.
|
|
1207
1267
|
|
|
@@ -1340,6 +1400,8 @@ import { Visual } from '@uniweb/kit'
|
|
|
1340
1400
|
|
|
1341
1401
|
**Utilities:** `cn()` (Tailwind class merge — `cn('px-4', condition && 'bg-primary')` resolves conflicts), `Link`, `Image`, `Asset`, `SafeHtml`, `SocialIcon`, `filterSocialLinks(links)`, `getSocialPlatform(url)`
|
|
1342
1402
|
|
|
1403
|
+
> **`cn()` gotcha — a later `text-<size>` silently drops an earlier `leading-*`.** Tailwind's size classes set line height too, so `cn()` treats the size as replacing the leading: `cn('leading-[1.1] text-4xl')` → `text-4xl`. Put the size first, or fold the leading into it (`text-[clamp(2rem,5vw,4rem)]/[1.1]`). Most likely to bite when the size comes from a lookup and the leading sits in a shared base string.
|
|
1404
|
+
|
|
1343
1405
|
**Other styled:** `SidebarLayout`, `Prose`, `Article`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
|
|
1344
1406
|
|
|
1345
1407
|
### Hook Signatures
|
|
@@ -1354,7 +1416,7 @@ useThemeData() → Theme // programmatic color
|
|
|
1354
1416
|
useColorContext(block) → 'light' | 'medium' | 'dark' // current section context
|
|
1355
1417
|
```
|
|
1356
1418
|
|
|
1357
|
-
`isActive` and `isActiveOrAncestor` accept a Page object or a route string. `useAppearance`
|
|
1419
|
+
`isActive` and `isActiveOrAncestor` accept a Page object or a route string. `useAppearance` reflects the site scheme from `appearance:` in `theme.yml` — `scheme` is `'light'`|`'dark'`, `canToggle` reflects `allowToggle`. The runtime applies the scheme to `<html>` before paint; the hook reads it, `toggle`/`setScheme` flip it and persist the choice to localStorage. First visit follows the OS when the site has dark mode. Full model: *Light/dark appearance* above.
|
|
1358
1420
|
|
|
1359
1421
|
### Icon Component
|
|
1360
1422
|
|
|
@@ -1622,7 +1684,7 @@ import LessonHeader from '../../components/LessonHeader'
|
|
|
1622
1684
|
|
|
1623
1685
|
Within the same directory (e.g., one component importing a sibling), use normal relative imports (`./AIFeedbackCard`).
|
|
1624
1686
|
|
|
1625
|
-
**Foundation entry shape (`main.js`).** A single `export default { … }` whose top-level keys are the capabilities the foundation provides — e.g. `name`, `description`, `defaultLayout`, `defaultSection`, `viewTransitions`, `props`, `defaultInsets`, `xref`, `outputs`, `handlers`. Optionally a named `vars` export for theme-variable metadata (see *Foundation variables*). Everything else (section types, layouts) is auto-discovered from `sections/` and `layouts/` and merged in by `@uniweb/build`. The build wraps your default export under `default.capabilities` in the produced `dist/
|
|
1687
|
+
**Foundation entry shape (`main.js`).** A single `export default { … }` whose top-level keys are the capabilities the foundation provides — e.g. `name`, `description`, `defaultLayout`, `defaultSection`, `viewTransitions`, `props`, `defaultInsets`, `xref`, `outputs`, `handlers`. Optionally a named `vars` export for theme-variable metadata (see *Foundation variables*). Everything else (section types, layouts) is auto-discovered from `sections/` and `layouts/` and merged in by `@uniweb/build`. The build wraps your default export under `default.capabilities` in the produced `dist/entry.js`; you don't write that wrapper yourself, and most foundation code never sees it. The one place it matters: when you import your **own** `main.js` from a foundation component (e.g., a download button calling `compileDocument(website, { foundation })`), you get the bare default object — pass it through directly, Press handles both shapes.
|
|
1626
1688
|
|
|
1627
1689
|
### Website and Page APIs
|
|
1628
1690
|
|
package/src/commands/build.js
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
classifyPackage,
|
|
61
61
|
isExtensionPackage,
|
|
62
62
|
} from '@uniweb/build'
|
|
63
|
+
import { resolveDefaultLocale, normalizeLanguageList } from '@uniweb/core/locale-config'
|
|
63
64
|
import { readSiteConfig } from '@uniweb/build/site'
|
|
64
65
|
import { readWorkspaceConfig, resolveGlob } from '../utils/config.js'
|
|
65
66
|
|
|
@@ -373,12 +374,26 @@ async function loadI18nConfig(projectDir, siteConfig = null) {
|
|
|
373
374
|
|
|
374
375
|
// Resolve locales (undefined/'*' → all available, array → specific)
|
|
375
376
|
const { resolveLocales } = await import('@uniweb/build/i18n')
|
|
376
|
-
|
|
377
|
+
let locales = await resolveLocales(config.languages, localesPath)
|
|
378
|
+
|
|
379
|
+
// Publish filter: build outputs are visitor artifacts. With an explicit
|
|
380
|
+
// `publishLanguages:`, only listed locales get dist/{locale}/ content,
|
|
381
|
+
// per-locale HTML, hreflang, and translated collections. Draft locales
|
|
382
|
+
// stay previewable in dev, which doesn't run this path. Intersecting the
|
|
383
|
+
// DISK-resolved list here also covers wildcard (`languages: '*'`) sites.
|
|
384
|
+
if (config.publishLanguages != null) {
|
|
385
|
+
const publishSet = new Set(normalizeLanguageList(config.publishLanguages))
|
|
386
|
+
const dropped = locales.filter((l) => !publishSet.has(l))
|
|
387
|
+
locales = locales.filter((l) => publishSet.has(l))
|
|
388
|
+
if (dropped.length > 0) {
|
|
389
|
+
info(`Skipping non-publishable locale(s): ${dropped.join(', ')} (not in publishLanguages)`)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
377
392
|
|
|
378
393
|
if (locales.length === 0) return null
|
|
379
394
|
|
|
380
395
|
return {
|
|
381
|
-
defaultLocale: config
|
|
396
|
+
defaultLocale: resolveDefaultLocale(config),
|
|
382
397
|
locales,
|
|
383
398
|
localesDir,
|
|
384
399
|
}
|
package/src/commands/i18n.js
CHANGED
|
@@ -19,6 +19,7 @@ import { resolve, join, dirname, basename, relative } from 'path'
|
|
|
19
19
|
import { existsSync } from 'fs'
|
|
20
20
|
import { readFile, writeFile, mkdir, readdir, unlink, rename } from 'fs/promises'
|
|
21
21
|
import yaml from 'js-yaml'
|
|
22
|
+
import { resolveDefaultLocale } from '@uniweb/core/locale-config'
|
|
22
23
|
import {
|
|
23
24
|
isWorkspaceRoot,
|
|
24
25
|
findSites,
|
|
@@ -216,7 +217,7 @@ async function loadSiteConfig(siteRoot) {
|
|
|
216
217
|
const locales = await resolveLocales(config.languages, localesPath)
|
|
217
218
|
|
|
218
219
|
return {
|
|
219
|
-
defaultLocale: config
|
|
220
|
+
defaultLocale: resolveDefaultLocale(config),
|
|
220
221
|
locales,
|
|
221
222
|
localesDir,
|
|
222
223
|
...config.i18n,
|
package/src/commands/publish.js
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
rewriteBallAssets,
|
|
49
49
|
} from '@uniweb/build/site'
|
|
50
50
|
import { emitSyncPackages } from '@uniweb/build/uwx'
|
|
51
|
+
import { resolveDefaultLocale } from '@uniweb/core/locale-config'
|
|
51
52
|
|
|
52
53
|
import { BackendClient } from '../backend/client.js'
|
|
53
54
|
import { resolveSiteDir, resolveSiteBackend } from './deploy.js'
|
|
@@ -117,7 +118,9 @@ function languagesFromContent(siteContent) {
|
|
|
117
118
|
|
|
118
119
|
// Languages from site.yml — used only for the dry-run summary (no build yet).
|
|
119
120
|
function languagesFromSiteYml(siteYml) {
|
|
120
|
-
|
|
121
|
+
// Legacy `lang:` still honored between defaultLanguage and the shared
|
|
122
|
+
// `defaultLanguage || languages[0] || 'en'` rule.
|
|
123
|
+
const def = siteYml.defaultLanguage || siteYml.lang || resolveDefaultLocale(siteYml)
|
|
121
124
|
const locales = siteYml.i18n?.locales || siteYml.languages
|
|
122
125
|
if (!Array.isArray(locales) || locales.length === 0) return null
|
|
123
126
|
const norm = locales.map((l) => (typeof l === 'string' ? l : l?.value || l?.code)).filter(Boolean)
|
package/src/commands/register.js
CHANGED
|
@@ -454,6 +454,17 @@ async function runRegister(args = []) {
|
|
|
454
454
|
return { exitCode: 1 }
|
|
455
455
|
}
|
|
456
456
|
}
|
|
457
|
+
// Registering ≠ delivering the file. A registered schema is a content type
|
|
458
|
+
// authors use in the app; a FOUNDATION build still resolves '@scope/name' from
|
|
459
|
+
// disk (a package or a routed folder). This is the moment that belief forms —
|
|
460
|
+
// you just registered it — so correct it here, not in a doc nobody's reading.
|
|
461
|
+
if (!jsonMode && defined.length > 0 && scope) {
|
|
462
|
+
log('')
|
|
463
|
+
info('Registered schemas are content types authors can use in the Uniweb app.')
|
|
464
|
+
log(` ${colors.dim}Delivery is separate: a foundation that binds ${scope}/<name> resolves it from${colors.reset}`)
|
|
465
|
+
log(` ${colors.dim}disk — an ${scope}/schemas package, or a folder routed in schemas.config.js.${colors.reset}`)
|
|
466
|
+
}
|
|
467
|
+
|
|
457
468
|
if (jsonMode) {
|
|
458
469
|
// Join my authoritative submitted names with the backend's minted ids. Each
|
|
459
470
|
// response entry is `{ registered: { name, version, payload_model_uuid, … },
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-07-
|
|
3
|
+
"generatedAt": "2026-07-24T20:04:13.283Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.14.
|
|
6
|
+
"version": "0.14.38",
|
|
7
7
|
"path": "framework/build",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/content-reader",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"deps": []
|
|
26
26
|
},
|
|
27
27
|
"@uniweb/core": {
|
|
28
|
-
"version": "0.7.
|
|
28
|
+
"version": "0.7.24",
|
|
29
29
|
"path": "framework/core",
|
|
30
30
|
"deps": [
|
|
31
31
|
"@uniweb/semantic-parser",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"deps": []
|
|
44
44
|
},
|
|
45
45
|
"@uniweb/kit": {
|
|
46
|
-
"version": "0.9.
|
|
46
|
+
"version": "0.9.31",
|
|
47
47
|
"path": "framework/kit",
|
|
48
48
|
"deps": [
|
|
49
49
|
"@uniweb/core",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"deps": []
|
|
62
62
|
},
|
|
63
63
|
"@uniweb/runtime": {
|
|
64
|
-
"version": "0.8.
|
|
64
|
+
"version": "0.8.30",
|
|
65
65
|
"path": "framework/runtime",
|
|
66
66
|
"deps": [
|
|
67
67
|
"@uniweb/core",
|
|
@@ -94,12 +94,12 @@
|
|
|
94
94
|
"deps": []
|
|
95
95
|
},
|
|
96
96
|
"@uniweb/theming": {
|
|
97
|
-
"version": "0.1.
|
|
97
|
+
"version": "0.1.10",
|
|
98
98
|
"path": "framework/theming",
|
|
99
99
|
"deps": []
|
|
100
100
|
},
|
|
101
101
|
"@uniweb/unipress": {
|
|
102
|
-
"version": "0.4.
|
|
102
|
+
"version": "0.4.45",
|
|
103
103
|
"path": "framework/unipress",
|
|
104
104
|
"deps": [
|
|
105
105
|
"@uniweb/build",
|