@getvitops/vite 1.0.0 → 2.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,271 @@
1
1
  # @getvitops/vite
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9bf975a: Add `vitops media` — encode raw video into web-ready outputs, instead of hand-rolling an ffmpeg
8
+ script per project.
9
+
10
+ Keep unprocessed video in a `raw/` directory and run:
11
+
12
+ ```
13
+ vitops media --raw raw --out src/assets/processed
14
+ ```
15
+
16
+ Each source becomes three files: **VP9/WebM**, an **H.264/MP4** fallback, and a **JPG poster**.
17
+ Import them like any other asset, so your bundler content-hashes them:
18
+
19
+ ```astro
20
+ ---
21
+ import hero from '../assets/processed/hero.webm';
22
+ import poster from '../assets/processed/hero.jpg';
23
+ ---
24
+ <video poster={poster.src} autoplay muted loop playsinline>
25
+ <source src={hero} type="video/webm" />
26
+ </video>
27
+ ```
28
+
29
+ In Astro it's an integration option — `vitops({ css, media: { raw: 'raw', out: 'src/assets/processed' } })`
30
+ — and it runs in the same pass that generates your CSS. `@getvitops/vite` gains a matching `media`
31
+ option, and `@getvitops/utils/media` exports `processMedia()` for anything else.
32
+
33
+ Defaults: capped at 1920px wide, CRF 32, audio dropped (the common case is a muted autoplay loop),
34
+ poster from frame 0. All of them are flags. `--dry` prints exactly what a run would do.
35
+
36
+ **Runs are cached**, on source content plus encode settings, in `.vitops/media-manifest.json` — a
37
+ 24 MB clip that took 88 seconds the first time takes 0.14 seconds the second. A missing output
38
+ re-encodes; a corrupt manifest re-encodes everything. Neither ever reads as "already done", because
39
+ that failure is silent and no rebuild would fix it.
40
+
41
+ **Commit the outputs and the manifest.** A fresh CI clone has neither and would re-encode from
42
+ scratch; committing both means CI never needs ffmpeg. It also keeps your history clean — ffmpeg
43
+ output isn't byte-reproducible across versions, so a CI re-encode would rewrite every video on any
44
+ toolchain bump. Use `--force` when you mean to re-encode.
45
+
46
+ **`ffmpeg` is an external tool, not an npm dependency** — install it yourself (`brew install
47
+ ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails without it rather
48
+ than skipping: a page referencing a video that was never encoded is broken, not degraded.
49
+
50
+ Two defaults worth knowing about. The MP4 exists because older iOS and the in-app webviews inside
51
+ social apps still don't decode VP9, and it's written with `+faststart` — without which a browser
52
+ downloads the whole file before showing frame one. And a poster taken from frame 0 is often black
53
+ on a clip that fades in; that's `--poster-time`, not a bug.
54
+
55
+ ### Patch Changes
56
+
57
+ - Updated dependencies [20e518e]
58
+ - Updated dependencies [9bf975a]
59
+ - @getvitops/generator@2.1.0
60
+ - @getvitops/utils@2.1.0
61
+
62
+ ## 2.0.0
63
+
64
+ ### Minor Changes
65
+
66
+ - bf453b0: Anywhere the toolchain takes a config, that file may now be a `design-system.json` **or** the larger
67
+ site config that embeds one.
68
+
69
+ If you keep your tokens inside a `company.json` / `site.json`, you had to maintain a second file for
70
+ the tooling's sake — or duplicate your whole token set, because a site config must carry a complete
71
+ `designSystem`. Now you point the same option at the file you already have:
72
+
73
+ ```js
74
+ // astro.config.mjs
75
+ getvitops({ css: { input: 'company.json', format: 'css' } });
76
+ ```
77
+
78
+ ```
79
+ vitops generate --input company.json --format css --out dist
80
+ vitops lint --input company.json --src src
81
+ ```
82
+
83
+ The design system is taken from `designSystem.themes[theme]` with its `extends` chain resolved —
84
+ `defaultTheme`, else `default`, else whatever `--theme` / the `theme` option names. Nothing changes
85
+ for a plain `design-system.json`; the two are told apart by shape, not by filename, so you can call
86
+ the file anything.
87
+
88
+ **A site config also supplies the site-level facts generation reads**, so the path is declared once
89
+ rather than per option:
90
+ - `designSystem.defaultColorScheme: "system"` — emits the `prefers-color-scheme` block, which is
91
+ what makes `<color-scheme-toggle>`'s System position do anything.
92
+ - `legal.*.enabled` — renders the documents. `legal: {}` is now the whole declaration; `legal.input`
93
+ is optional.
94
+ - `icons.sprite` — builds `icons.svg`.
95
+ - `fonts` — `fonts: true` reads the families from it.
96
+
97
+ Each of those is already gated on a field in that config, so nothing new appears unless the config
98
+ asks for it. An explicit `site` option still wins when the two are genuinely different files.
99
+
100
+ Also:
101
+ - **`vitops validate` routes on the file's shape.** Pointed at a site config it used to report a
102
+ single `unrecognized_keys` for `designSystem` and nothing about the file's actual contents — a
103
+ wrong answer that reads like a right one. It now validates a site config as one, including the
104
+ cross-field integrity JSON Schema can't express, and checks every theme resolves to a complete
105
+ design system (`validateSite` only checked that the `extends` chain resolved, not what it resolved
106
+ to).
107
+ - **`generate()` gained `theme` and `siteEnv`.** The A/B variant for `siteEnv` is applied before the
108
+ theme is selected, so a variant can override tokens.
109
+ - **The theme editor's Save to source follows the design system into a site config.** Its patch is
110
+ design-system-relative, so the dev server merges into that subtree and writes only the surrounding
111
+ file whole. It locates the subtree in the raw on-disk object rather than the normalised one, so an
112
+ author who wrote either `designSystem` shorthand still gets their own keys edited.
113
+
114
+ - bf453b0: **Breaking (site config):** `designSystem` is now an object, and the toggle's "System" position works.
115
+
116
+ `<color-scheme-toggle>` has always shipped three segments, and "System" did nothing. It removes the
117
+ theme attribute, and with no `prefers-color-scheme` block in the generated CSS the page fell straight
118
+ through to light on every machine — so a third of the control was inert, and a no-JS page could never
119
+ follow the OS at all.
120
+
121
+ The fix is a second, opt-in copy of the dark delta under
122
+ `@media (prefers-color-scheme: dark)`, scoped to "no explicit choice has been made". An explicit
123
+ light choice still wins over a dark OS. It costs **+303 B gzipped** on the colour layer — the block
124
+ repeats only the tokens whose dark value differs, and two identical runs of text compress to almost
125
+ nothing. It is opt-in because switching it on visibly flips an existing site dark for dark-OS
126
+ visitors.
127
+
128
+ Turning it on is a site-level fact, which needed somewhere to live:
129
+
130
+ ```jsonc
131
+ "designSystem": {
132
+ "themes": { "default": { … }, "elegant": { "extends": "default", … } },
133
+ "defaultTheme": "default",
134
+ "defaultColorScheme": "system" // 'light' | 'dark' | 'system'
135
+ }
136
+ ```
137
+
138
+ **What changed.** `designSystem` was a bare map of themeName → design system, so _every_ key was a
139
+ theme name and there was nowhere to put a system-wide field without it colliding with a theme of that
140
+ name. The map moves under `themes`, and `defaultTheme` + `defaultColorScheme` move inside the block.
141
+ `respectSystemPreference` is gone — `defaultColorScheme: "system"` says the same thing, and the two
142
+ were always read together, so the incoherent combination is no longer expressible.
143
+
144
+ **Migration is automatic at runtime.** `resolveSiteConfig` accepts all three spellings — the
145
+ canonical shape, a bare theme map (the old schema), and a bare design system written inline. But
146
+ `site.schema.json` is published to a stable URL, so an editor pinned to `$schema` will flag the old
147
+ shape; update it when convenient.
148
+
149
+ One behavioural change worth knowing: shorthand normalisation now runs **before** the A/B variant
150
+ merge, not after. Previously an `abTesting` override's key path depended on which shorthand the base
151
+ config happened to use, so the same patch landed in different places in two otherwise-equivalent
152
+ configs. Overrides now always address `designSystem.themes.<name>`.
153
+
154
+ Also: `getvitops({ site: { input } })` gives the Astro integration one place to name the site config
155
+ — `legal`, `fonts` and the colour scheme all read from it — and `css.systemColorScheme` sets the
156
+ appearance directly for consumers who have no site config.
157
+
158
+ Light/dark remains **derived**, not a theme: `functionalRole()` builds both appearances from one ramp,
159
+ which is what lets the contrast contract check both at build time and what gives every consumer a
160
+ working dark mode without authoring one. `themes` is for authored variants.
161
+
162
+ - 04a51d8: Icons: one semantic vocabulary across icon sets, with the bundle derived from your source.
163
+
164
+ Name icons by meaning — `<Icon name="menu" />` resolves to `fa7-solid:bars`, `ph:list` or
165
+ `lucide:menu` depending on the set you configure, so swapping sets is a config edit rather than a
166
+ find-and-replace. A name containing `:` still passes through untouched, which is the escape hatch
167
+ for a set-specific glyph.
168
+
169
+ **New `icons` option on `getvitops()`.** Configures the sets once per site and, under
170
+ `output: 'server'`, derives astro-icon's `include` map by scanning your source — the list most
171
+ projects end up maintaining by hand. On a static build no `include` is passed at all, because
172
+ astro-icon is already zero-config there and a list could only drop a glyph the scan couldn't see.
173
+ Names you declare that don't resolve fail the build; names only the scan found just warn; names
174
+ computed at runtime are reported with file and line so you can declare them.
175
+
176
+ **New `<Icon />` component**, and a real fix with it: `Popover`, `Details` and `Drawer` imported
177
+ `astro-icon/components` at module scope, so the _optional_ peer was resolved whether or not an icon
178
+ ever rendered — hard-failing anyone who hadn't installed it. Engines now load dynamically. The
179
+ per-component `iconResolver` prop still works but is deprecated in favour of the integration option.
180
+
181
+ **New SVG sprite** (`icons.sprite: true` in your site config) for consumers that can't run an icon
182
+ integration — Bricks/WordPress, EmDash renderers, plain HTML. `<use href="…/icons.svg#ph--list">`,
183
+ no JavaScript and no icon-API request. Every semantic name also gets a set-independent `icon-<name>`
184
+ alias, so sprite markup survives an icon-set change. WordPress gets `vitops_icon()`, a
185
+ `[vitops_icon]` shortcode and a **Vitops → Icon** Bricks element.
186
+
187
+ **New `vitops icons`** command: reports which icons your source uses, which names couldn't be
188
+ resolved, and which are computed at runtime; `--sprite` builds the sprite, `--json` for CI.
189
+
190
+ **Renamed, and completed to all four directions.** Chevrons and arrows are the
191
+ one family whose _meaning is_ its direction, and that direction is physical, not
192
+ logical — a chevron in a details marker points down in every writing mode. So they
193
+ are named for where they point: `expand-more`/`expand-less` →
194
+ `chevron-down`/`chevron-up`, and `arrow-forward`/`arrow-back` →
195
+ `arrow-right`/`arrow-left`. `chevron-left`/`chevron-right` keep their names.
196
+ `arrow-up`/`arrow-down` are new, so both families now cover all four directions.
197
+ `lightning` is new.
198
+
199
+ If you passed any of the old names to `<Icon />`, `resolveIcon` or an `icons`
200
+ config, update them. They fail loudly rather than silently: an unresolvable
201
+ declared name throws, and `vitops icons` reports scanned ones.
202
+
203
+ **Fixed three Font Awesome mappings that named no real glyph** and so rendered an
204
+ empty box: `login`/`logout` were mapped to `login`/`logout` (Font Awesome calls
205
+ them `right-to-bracket`/`right-from-bracket`) and `backup` to `backup`
206
+ (`cloud-arrow-up`). Every value in all four UI sets is now checked against the
207
+ installed collection.
208
+
209
+ **Phosphor (`ph`) joins the semantic map**, with all 83 names verified against the real icon set.
210
+ Phosphor keeps every weight in one collection and varies the name (`list`, `list-bold`), unlike Font
211
+ Awesome's per-weight collections, so `resolveIcon` and `generateIconInclude` gained a `weight`
212
+ option for sets shaped that way.
213
+
214
+ Fixes: `site.icons` was a closed object, so any icon collection not in its hand-written key list was
215
+ silently dropped during validation — your config passed and the icons never bundled. It accepts any
216
+ collection name now.
217
+
218
+ - bf453b0: Maskable favicons are now opaque, so they stop rendering as a logo in a black box.
219
+
220
+ `icon-mask.png` is declared `purpose: "maskable"` and `apple-touch-icon.png` is linked on every
221
+ page. Both sit a deliberately-inset logo on a larger canvas — that inset **is** the maskable safe
222
+ zone and was always correct — but the canvas was filled with `alpha: 0`. From any transparent
223
+ source that left 36% of `icon-mask.png` and 40% of `apple-touch-icon.png` transparent, on two files
224
+ whose entire contract is full bleed: the OS crops them to its own shape and composites the rest onto
225
+ whatever it likes, usually black. iOS discards alpha on the apple-touch-icon outright.
226
+
227
+ `backgroundColor` was already the obvious input for this and already plumbed — as far as the web
228
+ manifest, and no further. So the raster and the manifest disagreed about the very colour meant to
229
+ sit behind the icon.
230
+
231
+ Now:
232
+ - those two outputs are composited onto `backgroundColor`, defaulting to `#ffffff` — the same
233
+ default the manifest's `background_color` already used;
234
+ - `favicon.svg`, `icon-{16,32,192,512}.png` and `favicon.ico` keep the source's transparency, since
235
+ none of them is maskable;
236
+ - a transparent source with no `backgroundColor` set now warns, rather than silently inheriting
237
+ white — a dark logo would lose against it;
238
+ - `icon-192`/`icon-512` declare `purpose: "any"` explicitly. Omitting it was legal (unset means
239
+ "any"), but with a maskable present and nothing claiming "any", some launchers picked the maskable
240
+ — the one with the safe-zone inset — for slots that wanted a plain icon.
241
+
242
+ `vitops favicon` gained `--background <hex>`; it previously had no way to set this at all.
243
+ `getvitops({ favicon })` and the Vite plugin now forward the option they were already accepting.
244
+
245
+ ### Patch Changes
246
+
247
+ - Updated dependencies [4756788]
248
+ - Updated dependencies [bf453b0]
249
+ - Updated dependencies [bf453b0]
250
+ - Updated dependencies [bf453b0]
251
+ - Updated dependencies [bf453b0]
252
+ - Updated dependencies [bf453b0]
253
+ - Updated dependencies [bf453b0]
254
+ - Updated dependencies [bf453b0]
255
+ - Updated dependencies [bf453b0]
256
+ - Updated dependencies [04a51d8]
257
+ - Updated dependencies [bf453b0]
258
+ - Updated dependencies [bf453b0]
259
+ - Updated dependencies [bf453b0]
260
+ - Updated dependencies [bf453b0]
261
+ - Updated dependencies [bf453b0]
262
+ - Updated dependencies [bf453b0]
263
+ - Updated dependencies [bf453b0]
264
+ - Updated dependencies [bf453b0]
265
+ - Updated dependencies [bf453b0]
266
+ - @getvitops/generator@2.0.0
267
+ - @getvitops/utils@2.0.0
268
+
3
269
  ## 1.0.0
4
270
 
5
271
  ### Minor Changes
package/README.md CHANGED
@@ -10,7 +10,7 @@ import vitops from '@getvitops/vite';
10
10
  export default {
11
11
  plugins: [
12
12
  vitops({
13
- input: 'design-system.json', // default
13
+ input: 'design-system.json', // default — or a site config (company.json) that embeds one
14
14
  format: 'tailwind', // 'bricks' | 'css' | 'tailwind' (default: 'tailwind')
15
15
  out: 'src/styles', // default
16
16
  }),
@@ -18,6 +18,11 @@ export default {
18
18
  };
19
19
  ```
20
20
 
21
+ `input` accepts either config kind. Pointed at a site config it builds
22
+ `designSystem.themes[theme]` (`theme` option; default the config's `defaultTheme`, else `default`)
23
+ and reads the site-level facts from the same file — so `legal: {}` needs no `input` of its own, and
24
+ the theme editor's **Save to source** writes back into that theme rather than the file root.
25
+
21
26
  Then import the generated stylesheet (Tailwind v4):
22
27
 
23
28
  ```css
package/dist/index.d.mts CHANGED
@@ -1,7 +1,47 @@
1
1
  import { Format, LegalOutput } from "@getvitops/generator";
2
+ import { OutputKind } from "@getvitops/utils/media";
2
3
  import { Plugin } from "vite";
3
4
 
4
5
  //#region src/index.d.ts
6
+ /**
7
+ * Where the design system lives inside the config file on disk.
8
+ *
9
+ * `[]` for a `design-system.json` — the file *is* the design system. For a site
10
+ * config it is under `designSystem`, and this reads the **raw** object rather
11
+ * than the resolved one because the editor's write-back has to land where the
12
+ * author actually put it. `resolveSiteConfig` normalises the two shorthands in
13
+ * memory; a writer that assumed the canonical shape would grow a second
14
+ * `themes` key beside the author's bare map and silently stop editing anything.
15
+ */
16
+ declare function designSystemPath(raw: unknown, theme?: string): string[];
17
+ declare const getAt: (obj: unknown, path: string[]) => unknown;
18
+ /** Return a copy of `obj` with `path` replaced by `value`. Creates missing objects. */
19
+ declare function setAt(obj: unknown, path: string[], value: unknown): unknown;
20
+ interface VitopsMediaOptions {
21
+ /** Directory of unprocessed video, walked recursively. Default: 'raw'. */
22
+ raw?: string;
23
+ /**
24
+ * Where encoded outputs go. Default: `'src/assets/processed'`.
25
+ *
26
+ * Under `src/` so Vite content-hashes each file and it can be served immutable;
27
+ * the raw sources stay outside anything Vite scans.
28
+ */
29
+ out?: string;
30
+ /** Cap the output width, keeping aspect ratio. Default 1920; 0 disables. */
31
+ maxWidth?: number;
32
+ /** Quality on VP9's CRF scale (0–63, lower is better). Default 32. */
33
+ crf?: number;
34
+ /** Optional bitrate ceiling, e.g. `'2M'`. Absent means constant quality. */
35
+ maxBitrate?: string;
36
+ /** Keep the audio track. Default false. */
37
+ audio?: boolean;
38
+ /** Timestamp (seconds) the poster frame comes from. Default 0. */
39
+ posterTime?: number;
40
+ /** Which outputs to produce. Default: webm, mp4 and a poster. */
41
+ outputs?: OutputKind[];
42
+ /** Cache file. Default `.vitops/media-manifest.json`. */
43
+ manifest?: string;
44
+ }
5
45
  interface VitopsFaviconOptions {
6
46
  /** Source SVG or PNG. */
7
47
  source: string;
@@ -17,14 +57,45 @@ interface VitopsFaviconOptions {
17
57
  backgroundColor?: string;
18
58
  }
19
59
  interface VitopsPluginOptions {
20
- /** Path to the design-system.json. Default: 'design-system.json'. */
60
+ /**
61
+ * Path to the config. Default: `'design-system.json'`.
62
+ *
63
+ * May be a `design-system.json` **or** the larger site config that embeds one
64
+ * (`company.json` / `site.json`) — told apart by shape. Pointing it at a site
65
+ * config also supplies `site` below, so a consumer who keeps their tokens
66
+ * there declares the path once instead of three times.
67
+ */
21
68
  input?: string;
69
+ /**
70
+ * Which `designSystem.themes` entry to build, when `input` is a site config.
71
+ * Default: the config's `defaultTheme`, else `default`.
72
+ *
73
+ * Also the theme `<wc-theme-editor>`'s **Save to source** writes back into.
74
+ */
75
+ theme?: string;
22
76
  /** Output target. Default: 'tailwind' (the EmDash/Astro case). */
23
77
  format?: Format;
24
78
  /** Directory to write generated output into. Default: 'src/styles'. */
25
79
  out?: string;
26
80
  /** Also generate a favicon set on build from this source image. */
27
81
  favicon?: VitopsFaviconOptions;
82
+ /**
83
+ * Also encode raw video on build, into WebM + MP4 + a poster frame.
84
+ *
85
+ * Runs inside this pass rather than in the Astro integration's `config:setup`
86
+ * hook, for the same reason the manifest and sprite copies do: it produces bytes,
87
+ * so it belongs where it cannot race the first generation.
88
+ *
89
+ * Cached on source content and encode settings, so re-running costs a hash per
90
+ * file. Needs `ffmpeg` on PATH and fails loudly without it — an unencoded hero
91
+ * video is not a degraded build, it is a broken page.
92
+ *
93
+ * The raw directory is deliberately **not** watched: `watched()` matches config
94
+ * files by exact path, and a multi-minute encode triggered mid-dev-server is a
95
+ * poor trade for it. A video added during dev arrives on restart, or via
96
+ * `vitops media`.
97
+ */
98
+ media?: VitopsMediaOptions;
28
99
  /**
29
100
  * Directory to mirror `design-manifest.json` into as `vitops/design-manifest.json`
30
101
  * — the URL `<wc-theme-editor>` fetches by default. Set by the Astro integration's
@@ -32,18 +103,52 @@ interface VitopsPluginOptions {
32
103
  * it can't race the first generation and can't go stale on regeneration.
33
104
  */
34
105
  editorManifestDir?: string;
106
+ /**
107
+ * Directory to mirror the generated `icons.svg` into as `vitops/icons.svg` —
108
+ * the URL `<Icon />` targets under the `sprite` engine. Same reasoning as
109
+ * `editorManifestDir`: copied inside the generate pass, so it can't race a
110
+ * cold start or go stale after a config edit. The *href* is a plain string
111
+ * decided at config:setup; only the bytes arrive here.
112
+ */
113
+ spriteDir?: string;
35
114
  /**
36
115
  * Also render the site's legal documents on build, and re-render when the site
37
116
  * config changes.
38
117
  *
39
- * Its own option rather than part of the design-system pass because it reads a
40
- * different config kind — a `SiteConfig`, not a `design-system.json`.
118
+ * Its own option because it is an *output* you opt into — but its `input` is
119
+ * optional, defaulting to `site.input`, then to `input` when that is itself a
120
+ * site config. `legal: {}` is therefore the whole declaration in the common case.
41
121
  */
42
122
  legal?: VitopsLegalOptions;
123
+ /**
124
+ * The site config, for the parts of generation that depend on site-level facts
125
+ * rather than on the design system — currently `designSystem.defaultColorScheme`, which
126
+ * decides whether the colour layer carries a `prefers-color-scheme` block.
127
+ *
128
+ * Only needed when the site config is a *different* file from `input`; when
129
+ * `input` is itself a site config, that one is used. Watched, so an edit
130
+ * regenerates.
131
+ */
132
+ site?: VitopsSiteOptions;
133
+ /**
134
+ * Emit the `prefers-color-scheme: dark` block. Normally read from the site
135
+ * config's `designSystem.defaultColorScheme: "system"`; set here it wins, for consumers with no
136
+ * site config.
137
+ */
138
+ systemColorScheme?: boolean;
43
139
  }
44
- interface VitopsLegalOptions {
140
+ interface VitopsSiteOptions {
45
141
  /** Path to the site config (JSON). */
46
142
  input: string;
143
+ /** Environment whose A/B variant applies. Default: 'production'. */
144
+ siteEnv?: string;
145
+ }
146
+ interface VitopsLegalOptions {
147
+ /**
148
+ * Path to the site config (JSON). Optional: defaults to `site.input`, then to
149
+ * the plugin's own `input` when that is itself a site config.
150
+ */
151
+ input?: string;
47
152
  /** Directory to write documents into. Default: 'src/content/legal'. */
48
153
  out?: string;
49
154
  /** Output format. Default: 'md', which suits an Astro content collection. */
@@ -53,5 +158,5 @@ interface VitopsLegalOptions {
53
158
  }
54
159
  declare function vitops(options?: VitopsPluginOptions): Plugin;
55
160
  //#endregion
56
- export { VitopsFaviconOptions, VitopsLegalOptions, VitopsPluginOptions, vitops as default };
161
+ export { VitopsFaviconOptions, VitopsLegalOptions, VitopsMediaOptions, VitopsPluginOptions, VitopsSiteOptions, vitops as default, designSystemPath, getAt, setAt };
57
162
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;UA4EiB,oBAAA;;EAEf,MAAA;EAFmC;EAInC,YAAA;EAJmC;EAMnC,GAAA;EAFA;EAIA,IAAA;EAAA;EAEA,UAAA;EAEA;EAAA,eAAA;AAAA;AAAA,UAGe,mBAAA;EAAmB;EAElC,KAAA;EAES;EAAT,MAAA,GAAS,MAAA;EAmBD;EAjBR,GAAA;EAiB0B;EAf1B,OAAA,GAAU,oBAAA;EAJV;;;;;;EAWA,iBAAA;EAQQ;;AAAkB;AAG5B;;;;EAHE,KAAA,GAAQ,kBAAA;AAAA;AAAA,UAGO,kBAAA;EAMN;EAJT,KAAA;EAMO;EAJP,GAAA;EAOsB;EALtB,MAAA,GAAS,WAAW;;EAEpB,OAAA;AAAA;AAAA,iBAGsB,MAAA,CAAO,OAAA,GAAS,mBAAA,GAA2B,MAAM"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;AA6CA;;;;AAAgE;AAWhE;;;iBAXgB,gBAAA,CAAiB,GAAA,WAAc,KAAiB;AAAA,cAWnD,KAAA,GAAS,GAAA,WAAc,IAAc;AAIlD;AAAA,iBAAgB,KAAA,CAAM,GAAA,WAAc,IAAA,YAAgB,KAAA;AAAA,UAyDnC,kBAAA;EAzDI;EA2DnB,GAAA;EA3DkC;;;AAA8B;AAyDlE;;EASE,GAAA;EAYoB;EAVpB,QAAA;EAFA;EAIA,GAAA;EAAA;EAEA,UAAA;EAEA;EAAA,KAAA;EAIA;EAFA,UAAA;EAIA;EAFA,OAAA,GAAU,UAAU;EAEZ;EAAR,QAAA;AAAA;AAAA,UAGe,oBAAA;EAAoB;EAEnC,MAAA;EAEA;EAAA,YAAA;EAIA;EAFA,GAAA;EAMA;EAJA,IAAA;EAIe;EAFf,UAAA;EAKkC;EAHlC,eAAA;AAAA;AAAA,UAGe,mBAAA;EAuCP;;;;;;;;EA9BR,KAAA;EAWA;;;;;;EAJA,KAAA;EA+CA;EA7CA,MAAA,GAAS,MAAA;EAuDT;EArDA,GAAA;EA2DA;EAzDA,OAAA,GAAU,oBAAA;EAyDO;AAGnB;;;;AAIS;AAGT;;;;;;;;;;EAlDE,KAAA,GAAQ,kBAAA;EAgEc;;;;;;EAzDtB,iBAAA;EAyDuE;AAAA;;;;;;EAjDvE,SAAA;;;;;;;;;EASA,KAAA,GAAQ,kBAAA;;;;;;;;;;EAUR,IAAA,GAAO,iBAAA;;;;;;EAMP,iBAAA;AAAA;AAAA,UAGe,iBAAA;;EAEf,KAAA;;EAEA,OAAO;AAAA;AAAA,UAGQ,kBAAA;;;;;EAKf,KAAA;;EAEA,GAAA;;EAEA,MAAA,GAAS,WAAW;;EAEpB,OAAA;AAAA;AAAA,iBAGsB,MAAA,CAAO,OAAA,GAAS,mBAAA,GAA2B,MAAM"}
package/dist/index.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
- import { generate, generateLegal, resolveSiteConfig, validate } from "@getvitops/generator";
3
+ import { generate, generateLegal, isSiteConfig, resolveSiteConfig, validate } from "@getvitops/generator";
4
4
  import { generateFavicons, writeFaviconManifest } from "@getvitops/utils";
5
+ import { MEDIA_MANIFEST_PATH, processMedia } from "@getvitops/utils/media";
5
6
 
6
7
  //#region src/index.ts
7
8
  /**
8
9
  * @getvitops/vite — a Vite plugin that generates Vitops design-system output from a
9
- * `design-system.json` during a Vite/Astro (EmDash) build, and hot-regenerates when
10
- * the config changes in dev.
10
+ * `design-system.json` (or from the larger site config that embeds one) during a
11
+ * Vite/Astro (EmDash) build, and hot-regenerates when the config changes in dev.
11
12
  *
12
13
  * import vitops from '@getvitops/vite';
13
14
  * export default { plugins: [vitops({ input: 'design-system.json', format: 'tailwind', out: 'src/styles' })] };
@@ -17,6 +18,40 @@ import { generateFavicons, writeFaviconManifest } from "@getvitops/utils";
17
18
  /** Endpoint `<wc-theme-editor>` probes for, and POSTs its patch to. */
18
19
  const EDITOR_ENDPOINT = "/__vitops/design-system";
19
20
  /**
21
+ * Where the design system lives inside the config file on disk.
22
+ *
23
+ * `[]` for a `design-system.json` — the file *is* the design system. For a site
24
+ * config it is under `designSystem`, and this reads the **raw** object rather
25
+ * than the resolved one because the editor's write-back has to land where the
26
+ * author actually put it. `resolveSiteConfig` normalises the two shorthands in
27
+ * memory; a writer that assumed the canonical shape would grow a second
28
+ * `themes` key beside the author's bare map and silently stop editing anything.
29
+ */
30
+ function designSystemPath(raw, theme = "default") {
31
+ if (!isSiteConfig(raw)) return [];
32
+ const ds = raw.designSystem;
33
+ if (ds != null && typeof ds === "object") {
34
+ if ("themes" in ds) return [
35
+ "designSystem",
36
+ "themes",
37
+ theme
38
+ ];
39
+ if ("colors" in ds) return ["designSystem"];
40
+ }
41
+ return ["designSystem", theme];
42
+ }
43
+ const getAt = (obj, path) => path.reduce((cur, k) => cur?.[k], obj);
44
+ /** Return a copy of `obj` with `path` replaced by `value`. Creates missing objects. */
45
+ function setAt(obj, path, value) {
46
+ if (!path.length) return value;
47
+ const [head, ...rest] = path;
48
+ const base = typeof obj === "object" && obj !== null ? obj : {};
49
+ return {
50
+ ...base,
51
+ [head]: setAt(base[head], rest, value)
52
+ };
53
+ }
54
+ /**
20
55
  * Recursively merge a patch into a config. Plain objects merge key-wise;
21
56
  * everything else (scalars, and arrays — a token list is replaced, not appended)
22
57
  * is overwritten by the patch.
@@ -64,17 +99,27 @@ function vitops(options = {}) {
64
99
  const format = options.format ?? "tailwind";
65
100
  const outDir = options.out ?? "src/styles";
66
101
  let input = resolve(options.input ?? "design-system.json");
67
- let legalInput = options.legal ? resolve(options.legal.input) : "";
102
+ let legalInput = options.legal?.input ? resolve(options.legal.input) : "";
103
+ let siteInput = options.site ? resolve(options.site.input) : "";
68
104
  let root = "";
105
+ const readJson = (p) => JSON.parse(readFileSync(p, "utf8"));
69
106
  const run = async () => {
107
+ const raw = readJson(input);
108
+ const inputIsSite = isSiteConfig(raw);
70
109
  await generate({
71
- input,
110
+ input: raw,
72
111
  format,
73
- outDir
112
+ outDir,
113
+ ...options.theme != null ? { theme: options.theme } : {},
114
+ siteEnv: options.site?.siteEnv ?? "production",
115
+ ...options.systemColorScheme != null ? { systemColorScheme: options.systemColorScheme } : {},
116
+ ...siteInput ? { site: readJson(siteInput) } : {}
74
117
  });
75
118
  if (options.legal) {
119
+ const legalPath = legalInput || siteInput || (inputIsSite ? input : "");
120
+ if (!legalPath) throw new Error("[vitops] legal: needs a site config — set `legal.input`, or the `site` option, or point `input` at a site config.");
76
121
  const legalOut = resolve(root, options.legal.out ?? "src/content/legal");
77
- const files = generateLegal(resolveSiteConfig(JSON.parse(readFileSync(legalInput, "utf8")), options.legal.siteEnv ?? "production"), { output: options.legal.format ?? "md" });
122
+ const files = generateLegal(resolveSiteConfig(readJson(legalPath), options.legal.siteEnv ?? "production"), { output: options.legal.format ?? "md" });
78
123
  mkdirSync(legalOut, { recursive: true });
79
124
  for (const [name, content] of Object.entries(files)) writeFileSync(join(legalOut, name), content);
80
125
  }
@@ -86,12 +131,21 @@ function vitops(options = {}) {
86
131
  copyFileSync(src, join(dest, "design-manifest.json"));
87
132
  }
88
133
  }
134
+ if (options.spriteDir) {
135
+ const src = resolve(root, outDir, "icons.svg");
136
+ if (existsSync(src)) {
137
+ const dest = resolve(root, options.spriteDir, "vitops");
138
+ mkdirSync(dest, { recursive: true });
139
+ copyFileSync(src, join(dest, "icons.svg"));
140
+ }
141
+ }
89
142
  if (options.favicon) {
90
143
  const faviconOut = resolve(root, options.favicon.out ?? "public");
91
144
  await generateFavicons({
92
145
  source: resolve(root, options.favicon.source),
93
146
  outputDir: faviconOut,
94
- ...options.favicon.lowResSource ? { lowResSource: resolve(root, options.favicon.lowResSource) } : {}
147
+ ...options.favicon.lowResSource ? { lowResSource: resolve(root, options.favicon.lowResSource) } : {},
148
+ ...options.favicon.backgroundColor ? { backgroundColor: options.favicon.backgroundColor } : {}
95
149
  });
96
150
  if (options.favicon.name && options.favicon.themeColor) await writeFaviconManifest(faviconOut, {
97
151
  name: options.favicon.name,
@@ -99,14 +153,38 @@ function vitops(options = {}) {
99
153
  ...options.favicon.backgroundColor ? { backgroundColor: options.favicon.backgroundColor } : {}
100
154
  });
101
155
  }
156
+ if (options.media) {
157
+ const media = options.media;
158
+ const config = {
159
+ ...media.maxWidth != null ? { maxWidth: media.maxWidth } : {},
160
+ ...media.crf != null ? { crf: media.crf } : {},
161
+ ...media.maxBitrate ? { maxBitrate: media.maxBitrate } : {},
162
+ ...media.audio != null ? { audio: media.audio } : {},
163
+ ...media.posterTime != null ? { posterTime: media.posterTime } : {},
164
+ ...media.outputs ? { outputs: media.outputs } : {}
165
+ };
166
+ const result = await processMedia({
167
+ raw: resolve(root, media.raw ?? "raw"),
168
+ out: resolve(root, media.out ?? "src/assets/processed"),
169
+ config,
170
+ manifest: resolve(root, media.manifest ?? MEDIA_MANIFEST_PATH),
171
+ onProgress: (message) => console.log(`[vitops] media: ${message}`)
172
+ });
173
+ if (result.written.length > 0) console.log(`[vitops] media: ${result.written.length} written, ${result.skipped} unchanged`);
174
+ }
102
175
  };
103
- const watched = () => legalInput ? [input, legalInput] : [input];
176
+ const watched = () => [...new Set([
177
+ input,
178
+ siteInput,
179
+ legalInput
180
+ ].filter(Boolean))];
104
181
  return {
105
182
  name: "@getvitops/vite",
106
183
  configResolved(config) {
107
184
  root = config.root;
108
185
  input = resolve(config.root, options.input ?? "design-system.json");
109
- if (options.legal) legalInput = resolve(config.root, options.legal.input);
186
+ if (options.legal?.input) legalInput = resolve(config.root, options.legal.input);
187
+ if (options.site) siteInput = resolve(config.root, options.site.input);
110
188
  },
111
189
  async buildStart() {
112
190
  for (const f of watched()) this.addWatchFile(f);
@@ -129,10 +207,13 @@ function vitops(options = {}) {
129
207
  res.end(JSON.stringify(body));
130
208
  };
131
209
  if (req.method === "GET") try {
210
+ const current = readJson(input);
211
+ const at = designSystemPath(current, options.theme);
132
212
  return json(200, {
133
213
  writable: true,
134
214
  path: input,
135
- config: JSON.parse(readFileSync(input, "utf8"))
215
+ at,
216
+ config: getAt(current, at)
136
217
  });
137
218
  } catch (err) {
138
219
  return json(500, { error: err.message });
@@ -144,10 +225,12 @@ function vitops(options = {}) {
144
225
  try {
145
226
  const { patch } = JSON.parse(Buffer.concat(chunks).toString("utf8"));
146
227
  if (patch == null || typeof patch !== "object") return json(400, { errors: ["body must be { patch: object }"] });
147
- const merged = deepMerge(JSON.parse(readFileSync(input, "utf8")), patch);
228
+ const current = readJson(input);
229
+ const at = designSystemPath(current, options.theme);
230
+ const merged = deepMerge(getAt(current, at), patch);
148
231
  const result = validate(merged);
149
232
  if (!result.ok) return json(400, { errors: result.errors.map((e) => `${e.path.join(".") || "(root)"}: ${e.message}`) });
150
- writeFileSync(input, `${formatConfig(merged)}\n`);
233
+ writeFileSync(input, `${formatConfig(setAt(current, at, merged))}\n`);
151
234
  return json(200, {
152
235
  ok: true,
153
236
  warnings: result.warnings
@@ -162,5 +245,5 @@ function vitops(options = {}) {
162
245
  }
163
246
 
164
247
  //#endregion
165
- export { vitops as default };
248
+ export { vitops as default, designSystemPath, getAt, setAt };
166
249
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @getvitops/vite — a Vite plugin that generates Vitops design-system output from a\n * `design-system.json` during a Vite/Astro (EmDash) build, and hot-regenerates when\n * the config changes in dev.\n *\n * import vitops from '@getvitops/vite';\n * export default { plugins: [vitops({ input: 'design-system.json', format: 'tailwind', out: 'src/styles' })] };\n *\n * Thin wrapper over @getvitops/generator — the same generator the CLI uses.\n */\nimport { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport type { Plugin } from 'vite';\nimport {\n generate,\n generateLegal,\n resolveSiteConfig,\n validate,\n type Format,\n type LegalOutput,\n} from '@getvitops/generator';\nimport { generateFavicons, writeFaviconManifest } from '@getvitops/utils';\n\n/** Endpoint `<wc-theme-editor>` probes for, and POSTs its patch to. */\nconst EDITOR_ENDPOINT = '/__vitops/design-system';\n\n/**\n * Recursively merge a patch into a config. Plain objects merge key-wise;\n * everything else (scalars, and arrays — a token list is replaced, not appended)\n * is overwritten by the patch.\n */\nfunction deepMerge(base: unknown, patch: unknown): unknown {\n const isPlain = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v);\n if (!isPlain(base) || !isPlain(patch)) return patch;\n const out: Record<string, unknown> = { ...base };\n for (const [k, v] of Object.entries(patch)) out[k] = deepMerge(base[k], v);\n return out;\n}\n\n/** Printed width the config is formatted to — matches the repo formatter's default. */\nconst PRINT_WIDTH = 100;\n\n/**\n * Serialize a config the way a formatter would: 2-space indent, but arrays of\n * scalars kept on one line when they fit.\n *\n * `JSON.stringify(v, null, 2)` explodes every array onto its own lines, so writing\n * back a two-key patch reflowed ~300 lines of `design-system.json` — a diff nobody\n * can review, for a change of two values. Matching the prevailing style keeps the\n * write surgical, which is the whole point of saving to source rather than copying\n * a patch by hand.\n */\nfunction formatConfig(value: unknown, col = 0, indent = 0): string {\n const pad = ' '.repeat(indent);\n if (Array.isArray(value)) {\n if (!value.length) return '[]';\n if (value.every((v) => v === null || typeof v !== 'object')) {\n const inline = `[${value.map((v) => JSON.stringify(v)).join(', ')}]`;\n if (col + inline.length <= PRINT_WIDTH) return inline;\n }\n const items = value.map((v) => `${pad} ${formatConfig(v, indent + 2, indent + 2)}`);\n return `[\\n${items.join(',\\n')}\\n${pad}]`;\n }\n if (value !== null && typeof value === 'object') {\n const entries = Object.entries(value as Record<string, unknown>);\n if (!entries.length) return '{}';\n const items = entries.map(([k, v]) => {\n const head = `${pad} ${JSON.stringify(k)}: `;\n return head + formatConfig(v, head.length, indent + 2);\n });\n return `{\\n${items.join(',\\n')}\\n${pad}}`;\n }\n return JSON.stringify(value) ?? 'null';\n}\n\nexport interface VitopsFaviconOptions {\n /** Source SVG or PNG. */\n source: string;\n /** Optional simplified source for the 16px icon. */\n lowResSource?: string;\n /** Directory to write the favicon set into. Default: 'public'. */\n out?: string;\n /** App name — when set (with `themeColor`), also emit `site.webmanifest`. */\n name?: string;\n /** PWA theme color (also drives `<meta name=\"theme-color\">`). */\n themeColor?: string;\n /** PWA background color. */\n backgroundColor?: string;\n}\n\nexport interface VitopsPluginOptions {\n /** Path to the design-system.json. Default: 'design-system.json'. */\n input?: string;\n /** Output target. Default: 'tailwind' (the EmDash/Astro case). */\n format?: Format;\n /** Directory to write generated output into. Default: 'src/styles'. */\n out?: string;\n /** Also generate a favicon set on build from this source image. */\n favicon?: VitopsFaviconOptions;\n /**\n * Directory to mirror `design-manifest.json` into as `vitops/design-manifest.json`\n * — the URL `<wc-theme-editor>` fetches by default. Set by the Astro integration's\n * `editor` option. Copied inside the generate pass rather than by the caller, so\n * it can't race the first generation and can't go stale on regeneration.\n */\n editorManifestDir?: string;\n /**\n * Also render the site's legal documents on build, and re-render when the site\n * config changes.\n *\n * Its own option rather than part of the design-system pass because it reads a\n * different config kind — a `SiteConfig`, not a `design-system.json`.\n */\n legal?: VitopsLegalOptions;\n}\n\nexport interface VitopsLegalOptions {\n /** Path to the site config (JSON). */\n input: string;\n /** Directory to write documents into. Default: 'src/content/legal'. */\n out?: string;\n /** Output format. Default: 'md', which suits an Astro content collection. */\n format?: LegalOutput;\n /** Environment whose A/B variant applies. Default: 'production'. */\n siteEnv?: string;\n}\n\nexport default function vitops(options: VitopsPluginOptions = {}): Plugin {\n const format: Format = options.format ?? 'tailwind';\n const outDir = options.out ?? 'src/styles';\n let input = resolve(options.input ?? 'design-system.json');\n let legalInput = options.legal ? resolve(options.legal.input) : '';\n let root = '';\n\n const run = async () => {\n await generate({ input, format, outDir });\n if (options.legal) {\n const legalOut = resolve(root, options.legal.out ?? 'src/content/legal');\n const site = resolveSiteConfig(\n JSON.parse(readFileSync(legalInput, 'utf8')),\n options.legal.siteEnv ?? 'production',\n );\n const files = generateLegal(site, { output: options.legal.format ?? 'md' });\n mkdirSync(legalOut, { recursive: true });\n for (const [name, content] of Object.entries(files))\n writeFileSync(join(legalOut, name), content);\n }\n if (options.editorManifestDir) {\n const src = resolve(root, outDir, 'design-manifest.json');\n if (existsSync(src)) {\n const dest = resolve(root, options.editorManifestDir, 'vitops');\n mkdirSync(dest, { recursive: true });\n copyFileSync(src, join(dest, 'design-manifest.json'));\n }\n }\n if (options.favicon) {\n const faviconOut = resolve(root, options.favicon.out ?? 'public');\n await generateFavicons({\n source: resolve(root, options.favicon.source),\n outputDir: faviconOut,\n ...(options.favicon.lowResSource\n ? { lowResSource: resolve(root, options.favicon.lowResSource) }\n : {}),\n });\n if (options.favicon.name && options.favicon.themeColor) {\n await writeFaviconManifest(faviconOut, {\n name: options.favicon.name,\n themeColor: options.favicon.themeColor,\n ...(options.favicon.backgroundColor\n ? { backgroundColor: options.favicon.backgroundColor }\n : {}),\n });\n }\n }\n };\n\n // Both configs are inputs to `run()`, so both must trigger a re-run — a legal\n // document that only refreshes when the design system changes is worse than\n // one that never refreshes, because it looks current.\n const watched = () => (legalInput ? [input, legalInput] : [input]);\n\n return {\n name: '@getvitops/vite',\n // Resolve paths against Vite's project root once it's known.\n configResolved(config) {\n root = config.root;\n input = resolve(config.root, options.input ?? 'design-system.json');\n if (options.legal) legalInput = resolve(config.root, options.legal.input);\n },\n async buildStart() {\n for (const f of watched()) this.addWatchFile(f);\n await run();\n },\n async watchChange(id) {\n if (watched().includes(resolve(id))) await run();\n },\n configureServer(server) {\n for (const f of watched()) server.watcher.add(f);\n server.watcher.on('change', async (file) => {\n if (!watched().includes(resolve(file))) return;\n await run();\n server.ws.send({ type: 'full-reload' });\n });\n\n // Dev-only design-system write-back, for `<wc-theme-editor>`'s \"Save to\n // source\". Registered in configureServer only, so it exists in `vite dev`\n // and never in a build — on a static deploy the editor's probe 404s and the\n // button simply isn't rendered. The only writable path is `input`, already\n // resolved from config: a request supplies a patch, never a destination.\n server.middlewares.use(EDITOR_ENDPOINT, (req, res, next) => {\n const json = (status: number, body: unknown): void => {\n res.statusCode = status;\n res.setHeader('content-type', 'application/json');\n res.end(JSON.stringify(body));\n };\n\n if (req.method === 'GET') {\n try {\n return json(200, {\n writable: true,\n path: input,\n config: JSON.parse(readFileSync(input, 'utf8')),\n });\n } catch (err) {\n return json(500, { error: (err as Error).message });\n }\n }\n\n if (req.method !== 'POST') return next();\n\n const chunks: Buffer[] = [];\n req.on('data', (c: Buffer) => chunks.push(c));\n req.on('end', () => {\n try {\n const { patch } = JSON.parse(Buffer.concat(chunks).toString('utf8')) as {\n patch?: unknown;\n };\n if (patch == null || typeof patch !== 'object')\n return json(400, { errors: ['body must be { patch: object }'] });\n\n const current = JSON.parse(readFileSync(input, 'utf8')) as unknown;\n const merged = deepMerge(current, patch);\n // Validate the *merged* result, not the patch: a patch is a partial\n // by design, so it can't be validated on its own, and writing an\n // invalid config would break the very dev server doing the writing.\n const result = validate(merged);\n if (!result.ok)\n return json(400, {\n errors: result.errors.map((e) => `${e.path.join('.') || '(root)'}: ${e.message}`),\n });\n\n writeFileSync(input, `${formatConfig(merged)}\\n`);\n // The watcher above picks the write up and regenerates + reloads.\n return json(200, { ok: true, warnings: result.warnings });\n } catch (err) {\n return json(400, { errors: [(err as Error).message] });\n }\n });\n });\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @getvitops/vite — a Vite plugin that generates Vitops design-system output from a\n * `design-system.json` (or from the larger site config that embeds one) during a\n * Vite/Astro (EmDash) build, and hot-regenerates when the config changes in dev.\n *\n * import vitops from '@getvitops/vite';\n * export default { plugins: [vitops({ input: 'design-system.json', format: 'tailwind', out: 'src/styles' })] };\n *\n * Thin wrapper over @getvitops/generator — the same generator the CLI uses.\n */\nimport { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport type { Plugin } from 'vite';\nimport {\n generate,\n generateLegal,\n isSiteConfig,\n resolveSiteConfig,\n validate,\n type Format,\n type LegalOutput,\n} from '@getvitops/generator';\nimport { generateFavicons, writeFaviconManifest } from '@getvitops/utils';\n// Own subpath: it shells out to ffmpeg, and a project without a `media` option\n// should never load it.\nimport {\n MEDIA_MANIFEST_PATH,\n processMedia,\n type MediaConfig,\n type OutputKind,\n} from '@getvitops/utils/media';\n\n/** Endpoint `<wc-theme-editor>` probes for, and POSTs its patch to. */\nconst EDITOR_ENDPOINT = '/__vitops/design-system';\n\n/**\n * Where the design system lives inside the config file on disk.\n *\n * `[]` for a `design-system.json` — the file *is* the design system. For a site\n * config it is under `designSystem`, and this reads the **raw** object rather\n * than the resolved one because the editor's write-back has to land where the\n * author actually put it. `resolveSiteConfig` normalises the two shorthands in\n * memory; a writer that assumed the canonical shape would grow a second\n * `themes` key beside the author's bare map and silently stop editing anything.\n */\nexport function designSystemPath(raw: unknown, theme = 'default'): string[] {\n if (!isSiteConfig(raw)) return [];\n const ds = (raw as Record<string, unknown>).designSystem as Record<string, unknown>;\n if (ds != null && typeof ds === 'object') {\n if ('themes' in ds) return ['designSystem', 'themes', theme];\n // A bare `DesignSystem` written inline — one theme, and it is `default`.\n if ('colors' in ds) return ['designSystem'];\n }\n return ['designSystem', theme]; // the legacy bare theme map\n}\n\nexport const getAt = (obj: unknown, path: string[]): unknown =>\n path.reduce<unknown>((cur, k) => (cur as Record<string, unknown>)?.[k], obj);\n\n/** Return a copy of `obj` with `path` replaced by `value`. Creates missing objects. */\nexport function setAt(obj: unknown, path: string[], value: unknown): unknown {\n if (!path.length) return value;\n const [head, ...rest] = path as [string, ...string[]];\n const base = (typeof obj === 'object' && obj !== null ? obj : {}) as Record<string, unknown>;\n return { ...base, [head]: setAt(base[head], rest, value) };\n}\n\n/**\n * Recursively merge a patch into a config. Plain objects merge key-wise;\n * everything else (scalars, and arrays — a token list is replaced, not appended)\n * is overwritten by the patch.\n */\nfunction deepMerge(base: unknown, patch: unknown): unknown {\n const isPlain = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v);\n if (!isPlain(base) || !isPlain(patch)) return patch;\n const out: Record<string, unknown> = { ...base };\n for (const [k, v] of Object.entries(patch)) out[k] = deepMerge(base[k], v);\n return out;\n}\n\n/** Printed width the config is formatted to — matches the repo formatter's default. */\nconst PRINT_WIDTH = 100;\n\n/**\n * Serialize a config the way a formatter would: 2-space indent, but arrays of\n * scalars kept on one line when they fit.\n *\n * `JSON.stringify(v, null, 2)` explodes every array onto its own lines, so writing\n * back a two-key patch reflowed ~300 lines of `design-system.json` — a diff nobody\n * can review, for a change of two values. Matching the prevailing style keeps the\n * write surgical, which is the whole point of saving to source rather than copying\n * a patch by hand.\n */\nfunction formatConfig(value: unknown, col = 0, indent = 0): string {\n const pad = ' '.repeat(indent);\n if (Array.isArray(value)) {\n if (!value.length) return '[]';\n if (value.every((v) => v === null || typeof v !== 'object')) {\n const inline = `[${value.map((v) => JSON.stringify(v)).join(', ')}]`;\n if (col + inline.length <= PRINT_WIDTH) return inline;\n }\n const items = value.map((v) => `${pad} ${formatConfig(v, indent + 2, indent + 2)}`);\n return `[\\n${items.join(',\\n')}\\n${pad}]`;\n }\n if (value !== null && typeof value === 'object') {\n const entries = Object.entries(value as Record<string, unknown>);\n if (!entries.length) return '{}';\n const items = entries.map(([k, v]) => {\n const head = `${pad} ${JSON.stringify(k)}: `;\n return head + formatConfig(v, head.length, indent + 2);\n });\n return `{\\n${items.join(',\\n')}\\n${pad}}`;\n }\n return JSON.stringify(value) ?? 'null';\n}\n\nexport interface VitopsMediaOptions {\n /** Directory of unprocessed video, walked recursively. Default: 'raw'. */\n raw?: string;\n /**\n * Where encoded outputs go. Default: `'src/assets/processed'`.\n *\n * Under `src/` so Vite content-hashes each file and it can be served immutable;\n * the raw sources stay outside anything Vite scans.\n */\n out?: string;\n /** Cap the output width, keeping aspect ratio. Default 1920; 0 disables. */\n maxWidth?: number;\n /** Quality on VP9's CRF scale (0–63, lower is better). Default 32. */\n crf?: number;\n /** Optional bitrate ceiling, e.g. `'2M'`. Absent means constant quality. */\n maxBitrate?: string;\n /** Keep the audio track. Default false. */\n audio?: boolean;\n /** Timestamp (seconds) the poster frame comes from. Default 0. */\n posterTime?: number;\n /** Which outputs to produce. Default: webm, mp4 and a poster. */\n outputs?: OutputKind[];\n /** Cache file. Default `.vitops/media-manifest.json`. */\n manifest?: string;\n}\n\nexport interface VitopsFaviconOptions {\n /** Source SVG or PNG. */\n source: string;\n /** Optional simplified source for the 16px icon. */\n lowResSource?: string;\n /** Directory to write the favicon set into. Default: 'public'. */\n out?: string;\n /** App name — when set (with `themeColor`), also emit `site.webmanifest`. */\n name?: string;\n /** PWA theme color (also drives `<meta name=\"theme-color\">`). */\n themeColor?: string;\n /** PWA background color. */\n backgroundColor?: string;\n}\n\nexport interface VitopsPluginOptions {\n /**\n * Path to the config. Default: `'design-system.json'`.\n *\n * May be a `design-system.json` **or** the larger site config that embeds one\n * (`company.json` / `site.json`) — told apart by shape. Pointing it at a site\n * config also supplies `site` below, so a consumer who keeps their tokens\n * there declares the path once instead of three times.\n */\n input?: string;\n /**\n * Which `designSystem.themes` entry to build, when `input` is a site config.\n * Default: the config's `defaultTheme`, else `default`.\n *\n * Also the theme `<wc-theme-editor>`'s **Save to source** writes back into.\n */\n theme?: string;\n /** Output target. Default: 'tailwind' (the EmDash/Astro case). */\n format?: Format;\n /** Directory to write generated output into. Default: 'src/styles'. */\n out?: string;\n /** Also generate a favicon set on build from this source image. */\n favicon?: VitopsFaviconOptions;\n /**\n * Also encode raw video on build, into WebM + MP4 + a poster frame.\n *\n * Runs inside this pass rather than in the Astro integration's `config:setup`\n * hook, for the same reason the manifest and sprite copies do: it produces bytes,\n * so it belongs where it cannot race the first generation.\n *\n * Cached on source content and encode settings, so re-running costs a hash per\n * file. Needs `ffmpeg` on PATH and fails loudly without it — an unencoded hero\n * video is not a degraded build, it is a broken page.\n *\n * The raw directory is deliberately **not** watched: `watched()` matches config\n * files by exact path, and a multi-minute encode triggered mid-dev-server is a\n * poor trade for it. A video added during dev arrives on restart, or via\n * `vitops media`.\n */\n media?: VitopsMediaOptions;\n /**\n * Directory to mirror `design-manifest.json` into as `vitops/design-manifest.json`\n * — the URL `<wc-theme-editor>` fetches by default. Set by the Astro integration's\n * `editor` option. Copied inside the generate pass rather than by the caller, so\n * it can't race the first generation and can't go stale on regeneration.\n */\n editorManifestDir?: string;\n /**\n * Directory to mirror the generated `icons.svg` into as `vitops/icons.svg` —\n * the URL `<Icon />` targets under the `sprite` engine. Same reasoning as\n * `editorManifestDir`: copied inside the generate pass, so it can't race a\n * cold start or go stale after a config edit. The *href* is a plain string\n * decided at config:setup; only the bytes arrive here.\n */\n spriteDir?: string;\n /**\n * Also render the site's legal documents on build, and re-render when the site\n * config changes.\n *\n * Its own option because it is an *output* you opt into — but its `input` is\n * optional, defaulting to `site.input`, then to `input` when that is itself a\n * site config. `legal: {}` is therefore the whole declaration in the common case.\n */\n legal?: VitopsLegalOptions;\n /**\n * The site config, for the parts of generation that depend on site-level facts\n * rather than on the design system — currently `designSystem.defaultColorScheme`, which\n * decides whether the colour layer carries a `prefers-color-scheme` block.\n *\n * Only needed when the site config is a *different* file from `input`; when\n * `input` is itself a site config, that one is used. Watched, so an edit\n * regenerates.\n */\n site?: VitopsSiteOptions;\n /**\n * Emit the `prefers-color-scheme: dark` block. Normally read from the site\n * config's `designSystem.defaultColorScheme: \"system\"`; set here it wins, for consumers with no\n * site config.\n */\n systemColorScheme?: boolean;\n}\n\nexport interface VitopsSiteOptions {\n /** Path to the site config (JSON). */\n input: string;\n /** Environment whose A/B variant applies. Default: 'production'. */\n siteEnv?: string;\n}\n\nexport interface VitopsLegalOptions {\n /**\n * Path to the site config (JSON). Optional: defaults to `site.input`, then to\n * the plugin's own `input` when that is itself a site config.\n */\n input?: string;\n /** Directory to write documents into. Default: 'src/content/legal'. */\n out?: string;\n /** Output format. Default: 'md', which suits an Astro content collection. */\n format?: LegalOutput;\n /** Environment whose A/B variant applies. Default: 'production'. */\n siteEnv?: string;\n}\n\nexport default function vitops(options: VitopsPluginOptions = {}): Plugin {\n const format: Format = options.format ?? 'tailwind';\n const outDir = options.out ?? 'src/styles';\n let input = resolve(options.input ?? 'design-system.json');\n let legalInput = options.legal?.input ? resolve(options.legal.input) : '';\n let siteInput = options.site ? resolve(options.site.input) : '';\n let root = '';\n\n const readJson = (p: string) => JSON.parse(readFileSync(p, 'utf8')) as unknown;\n\n const run = async () => {\n // Read once and hand `generate` the parsed object: it has to be inspected\n // here anyway to know whether `input` is a site config, and reading the same\n // file twice per regeneration is how a half-written save gets picked up.\n const raw = readJson(input);\n const inputIsSite = isSiteConfig(raw);\n // `generate` reads the site config only for what the stylesheet depends on;\n // legal documents stay the separate opt-in below.\n await generate({\n input: raw as Parameters<typeof generate>[0]['input'],\n format,\n outDir,\n ...(options.theme != null ? { theme: options.theme } : {}),\n siteEnv: options.site?.siteEnv ?? 'production',\n ...(options.systemColorScheme != null\n ? { systemColorScheme: options.systemColorScheme }\n : {}),\n // An explicit `site` wins; otherwise `input` supplies it when it is one.\n ...(siteInput ? { site: readJson(siteInput) as never } : {}),\n });\n if (options.legal) {\n // The three paths are the same file in almost every project, so the\n // declaration cascades rather than being repeated.\n const legalPath = legalInput || siteInput || (inputIsSite ? input : '');\n if (!legalPath)\n throw new Error(\n '[vitops] legal: needs a site config — set `legal.input`, or the `site` option, or ' +\n 'point `input` at a site config.',\n );\n const legalOut = resolve(root, options.legal.out ?? 'src/content/legal');\n const site = resolveSiteConfig(readJson(legalPath), options.legal.siteEnv ?? 'production');\n const files = generateLegal(site, { output: options.legal.format ?? 'md' });\n mkdirSync(legalOut, { recursive: true });\n for (const [name, content] of Object.entries(files))\n writeFileSync(join(legalOut, name), content);\n }\n if (options.editorManifestDir) {\n const src = resolve(root, outDir, 'design-manifest.json');\n if (existsSync(src)) {\n const dest = resolve(root, options.editorManifestDir, 'vitops');\n mkdirSync(dest, { recursive: true });\n copyFileSync(src, join(dest, 'design-manifest.json'));\n }\n }\n if (options.spriteDir) {\n const src = resolve(root, outDir, 'icons.svg');\n if (existsSync(src)) {\n const dest = resolve(root, options.spriteDir, 'vitops');\n mkdirSync(dest, { recursive: true });\n copyFileSync(src, join(dest, 'icons.svg'));\n }\n }\n if (options.favicon) {\n const faviconOut = resolve(root, options.favicon.out ?? 'public');\n await generateFavicons({\n source: resolve(root, options.favicon.source),\n outputDir: faviconOut,\n ...(options.favicon.lowResSource\n ? { lowResSource: resolve(root, options.favicon.lowResSource) }\n : {}),\n // Composited under the maskable outputs, so they match the manifest's\n // `background_color` rather than being transparent where it must not be.\n ...(options.favicon.backgroundColor\n ? { backgroundColor: options.favicon.backgroundColor }\n : {}),\n });\n if (options.favicon.name && options.favicon.themeColor) {\n await writeFaviconManifest(faviconOut, {\n name: options.favicon.name,\n themeColor: options.favicon.themeColor,\n ...(options.favicon.backgroundColor\n ? { backgroundColor: options.favicon.backgroundColor }\n : {}),\n });\n }\n }\n if (options.media) {\n const media = options.media;\n const config: MediaConfig = {\n ...(media.maxWidth != null ? { maxWidth: media.maxWidth } : {}),\n ...(media.crf != null ? { crf: media.crf } : {}),\n ...(media.maxBitrate ? { maxBitrate: media.maxBitrate } : {}),\n ...(media.audio != null ? { audio: media.audio } : {}),\n ...(media.posterTime != null ? { posterTime: media.posterTime } : {}),\n ...(media.outputs ? { outputs: media.outputs } : {}),\n };\n const result = await processMedia({\n raw: resolve(root, media.raw ?? 'raw'),\n out: resolve(root, media.out ?? 'src/assets/processed'),\n config,\n // Resolved against the project root even when defaulted. Left to\n // `processMedia` it would resolve against cwd, and in a monorepo run from\n // the workspace root that puts the cache somewhere the next build won't\n // look — so every build re-encodes, silently.\n manifest: resolve(root, media.manifest ?? MEDIA_MANIFEST_PATH),\n // An encode is minutes of silence otherwise, which in a build reads as a hang.\n onProgress: (message) => console.log(`[vitops] media: ${message}`),\n });\n if (result.written.length > 0)\n console.log(\n `[vitops] media: ${result.written.length} written, ${result.skipped} unchanged`,\n );\n }\n };\n\n // Every config `run()` reads is an input, so every one must trigger a re-run —\n // a legal document (or a colour layer) that only refreshes when the design\n // system changes is worse than one that never refreshes, because it looks\n // current. De-duplicated because `site.input` and `legal.input` are usually the\n // same file, and watching it twice would regenerate twice per edit.\n const watched = () => [...new Set([input, siteInput, legalInput].filter(Boolean))];\n\n return {\n name: '@getvitops/vite',\n // Resolve paths against Vite's project root once it's known.\n configResolved(config) {\n root = config.root;\n input = resolve(config.root, options.input ?? 'design-system.json');\n if (options.legal?.input) legalInput = resolve(config.root, options.legal.input);\n if (options.site) siteInput = resolve(config.root, options.site.input);\n },\n async buildStart() {\n for (const f of watched()) this.addWatchFile(f);\n await run();\n },\n async watchChange(id) {\n if (watched().includes(resolve(id))) await run();\n },\n configureServer(server) {\n for (const f of watched()) server.watcher.add(f);\n server.watcher.on('change', async (file) => {\n if (!watched().includes(resolve(file))) return;\n await run();\n server.ws.send({ type: 'full-reload' });\n });\n\n // Dev-only design-system write-back, for `<wc-theme-editor>`'s \"Save to\n // source\". Registered in configureServer only, so it exists in `vite dev`\n // and never in a build — on a static deploy the editor's probe 404s and the\n // button simply isn't rendered. The only writable path is `input`, already\n // resolved from config: a request supplies a patch, never a destination.\n //\n // The editor's patch is always design-system-relative (`colors.palette.…`),\n // because that is the only config it knows about. When `input` is a site\n // config the patch is therefore merged into — and validated against — the\n // design system SUBTREE, and only the surrounding file is written whole.\n // Merging it at the root instead would write a `colors` key beside\n // `organization` that nothing reads, and `validate` would reject the site\n // config wholesale, so every save would fail with errors about the wrong\n // document.\n server.middlewares.use(EDITOR_ENDPOINT, (req, res, next) => {\n const json = (status: number, body: unknown): void => {\n res.statusCode = status;\n res.setHeader('content-type', 'application/json');\n res.end(JSON.stringify(body));\n };\n\n if (req.method === 'GET') {\n try {\n const current = readJson(input);\n const at = designSystemPath(current, options.theme);\n return json(200, {\n writable: true,\n path: input,\n // The subtree, not the file: the editor's own model of a config is\n // a design system, and `at` says where in the file it lives.\n at,\n config: getAt(current, at),\n });\n } catch (err) {\n return json(500, { error: (err as Error).message });\n }\n }\n\n if (req.method !== 'POST') return next();\n\n const chunks: Buffer[] = [];\n req.on('data', (c: Buffer) => chunks.push(c));\n req.on('end', () => {\n try {\n const { patch } = JSON.parse(Buffer.concat(chunks).toString('utf8')) as {\n patch?: unknown;\n };\n if (patch == null || typeof patch !== 'object')\n return json(400, { errors: ['body must be { patch: object }'] });\n\n const current = readJson(input);\n const at = designSystemPath(current, options.theme);\n const merged = deepMerge(getAt(current, at), patch);\n // Validate the *merged* result, not the patch: a patch is a partial\n // by design, so it can't be validated on its own, and writing an\n // invalid config would break the very dev server doing the writing.\n const result = validate(merged);\n if (!result.ok)\n return json(400, {\n errors: result.errors.map((e) => `${e.path.join('.') || '(root)'}: ${e.message}`),\n });\n\n writeFileSync(input, `${formatConfig(setAt(current, at, merged))}\\n`);\n // The watcher above picks the write up and regenerates + reloads.\n return json(200, { ok: true, warnings: result.warnings });\n } catch (err) {\n return json(400, { errors: [(err as Error).message] });\n }\n });\n });\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getvitops/vite",
3
- "version": "1.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Vite plugin that generates Vitops design-system outputs from a design-system.json during a Vite/Astro build.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -18,11 +18,12 @@
18
18
  "access": "public"
19
19
  },
20
20
  "dependencies": {
21
- "@getvitops/generator": "1.0.0",
22
- "@getvitops/utils": "1.0.0"
21
+ "@getvitops/generator": "2.1.0",
22
+ "@getvitops/utils": "2.1.0"
23
23
  },
24
24
  "devDependencies": {
25
- "vite": "^6.0.0"
25
+ "vite": "^6.0.0",
26
+ "vitest": "^4.1.9"
26
27
  },
27
28
  "peerDependencies": {
28
29
  "vite": ">=5"