@getvitops/vite 0.9.0 → 1.0.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,91 @@
1
1
  # @getvitops/vite
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bb92a14: Generate legal documents from your site config
8
+
9
+ `vitops legal` renders a privacy policy, terms of service and cookie notice from a site
10
+ config, in markdown, HTML or EmDash Portable Text:
11
+
12
+ ```sh
13
+ vitops legal --out ./content # every enabled document, as markdown
14
+ vitops legal --doc privacy --format html # one document, as an HTML fragment
15
+ ```
16
+
17
+ The documents are **derived from your config**, not filled into a form. The analytics
18
+ provider they name is the one whose ID you set; the personal information they list is what
19
+ your configured forms actually collect; the countries they name come from the providers you
20
+ use. So a provider swap updates the policy on the next build, and the fix for a wrong policy
21
+ is a corrected config — hand-editing the output is overwritten.
22
+
23
+ Enable documents under `legal`, which gains the facts the prose asserts:
24
+
25
+ ```jsonc
26
+ {
27
+ "legal": {
28
+ "jurisdiction": "ca", // only 'ca' (PIPEDA) ships today
29
+ "privacyPolicy": {
30
+ "enabled": true,
31
+ "lastUpdated": "2026-08-01",
32
+ "retention": "24 months after our last contact with you",
33
+ // Third parties the config cannot imply. Analytics, Turnstile and your
34
+ // deploy platform are detected automatically — list only the rest.
35
+ "processors": [
36
+ {
37
+ "name": "Stripe",
38
+ "purpose": "payment processing",
39
+ "country": "the United States",
40
+ },
41
+ ],
42
+ },
43
+ "termsOfService": { "enabled": true },
44
+ "cookieConsent": {
45
+ "enabled": true,
46
+ "type": "opt-in",
47
+ "categories": ["Essential", "Analytics"],
48
+ },
49
+ },
50
+ }
51
+ ```
52
+
53
+ Delivery, by stack:
54
+ - **Any stack** — `vitops legal`. No integration code; prints to stdout without `--out`.
55
+ - **WordPress/Bricks** — `vitops generate --site <path>` also writes `dist/legal/*.html`, and
56
+ the theme loader now registers `[vitops_legal doc="privacy"]` to render one in a page. The
57
+ document updates on the next deploy with no action in WordPress.
58
+ - **Astro** — `getvitops({ legal: { input: 'site.json', out: 'src/content/legal' } })` writes
59
+ markdown into a content collection and re-renders when the site config changes. It needs a
60
+ `css` config (that is what registers the Vite plugin); without one, use the CLI.
61
+ - **EmDash** — `--format portable-text`, pasted into the admin.
62
+
63
+ Also new on the public API: `generateLegal()`, `renderMarkdown()`, `renderNodes()`,
64
+ `derivePolicyVars()`, `parseMarkdown()` / `toHtmlFragment()` / `toPortableText()`, and
65
+ `resolvePrivacyContact()`.
66
+
67
+ Two things to know before you publish anything this produces:
68
+ - **It is not legal advice.** Every document opens with a review banner saying so. The
69
+ bundled terms-of-service prose in particular is generic website boilerplate and
70
+ deliberately does not cover sales, refunds, subscriptions, accounts or user-generated
71
+ content — a site doing any of those needs clauses drafted for it.
72
+ - **It is only as true as your config.** A policy asserting things your site does not do is
73
+ worse than no policy. Check that the config describes reality before you ship the output.
74
+
75
+ `validateSite` now rejects a config that enables a privacy policy without a contact for
76
+ privacy requests or a `domains.canonical`, since both are interpolated into sentences that
77
+ would otherwise render blank.
78
+
79
+ ### Patch Changes
80
+
81
+ - Updated dependencies [bb92a14]
82
+ - Updated dependencies [bb92a14]
83
+ - Updated dependencies [bb92a14]
84
+ - Updated dependencies [bb92a14]
85
+ - Updated dependencies [eeb059f]
86
+ - @getvitops/generator@1.0.0
87
+ - @getvitops/utils@1.0.0
88
+
3
89
  ## 0.9.0
4
90
 
5
91
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { Format } from "@getvitops/generator";
1
+ import { Format, LegalOutput } from "@getvitops/generator";
2
2
  import { Plugin } from "vite";
3
3
 
4
4
  //#region src/index.d.ts
@@ -32,8 +32,26 @@ interface VitopsPluginOptions {
32
32
  * it can't race the first generation and can't go stale on regeneration.
33
33
  */
34
34
  editorManifestDir?: string;
35
+ /**
36
+ * Also render the site's legal documents on build, and re-render when the site
37
+ * config changes.
38
+ *
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`.
41
+ */
42
+ legal?: VitopsLegalOptions;
43
+ }
44
+ interface VitopsLegalOptions {
45
+ /** Path to the site config (JSON). */
46
+ input: string;
47
+ /** Directory to write documents into. Default: 'src/content/legal'. */
48
+ out?: string;
49
+ /** Output format. Default: 'md', which suits an Astro content collection. */
50
+ format?: LegalOutput;
51
+ /** Environment whose A/B variant applies. Default: 'production'. */
52
+ siteEnv?: string;
35
53
  }
36
54
  declare function vitops(options?: VitopsPluginOptions): Plugin;
37
55
  //#endregion
38
- export { VitopsFaviconOptions, VitopsPluginOptions, vitops as default };
56
+ export { VitopsFaviconOptions, VitopsLegalOptions, VitopsPluginOptions, vitops as default };
39
57
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;UAqEiB,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;EAM8B;EAJ9B,MAAA,GAAS,MAAA;EAAT;EAEA,GAAA;EAAA;EAEA,OAAA,GAAU,oBAAoB;EAApB;;;AAOO;AAClB;;EADC,iBAAA;AAAA;AAAA,iBAGsB,MAAA,CAAO,OAAA,GAAS,mBAAA,GAA2B,MAAM"}
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"}
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
- import { generate, validate } from "@getvitops/generator";
3
+ import { generate, generateLegal, resolveSiteConfig, validate } from "@getvitops/generator";
4
4
  import { generateFavicons, writeFaviconManifest } from "@getvitops/utils";
5
5
 
6
6
  //#region src/index.ts
@@ -64,6 +64,7 @@ function vitops(options = {}) {
64
64
  const format = options.format ?? "tailwind";
65
65
  const outDir = options.out ?? "src/styles";
66
66
  let input = resolve(options.input ?? "design-system.json");
67
+ let legalInput = options.legal ? resolve(options.legal.input) : "";
67
68
  let root = "";
68
69
  const run = async () => {
69
70
  await generate({
@@ -71,6 +72,12 @@ function vitops(options = {}) {
71
72
  format,
72
73
  outDir
73
74
  });
75
+ if (options.legal) {
76
+ 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" });
78
+ mkdirSync(legalOut, { recursive: true });
79
+ for (const [name, content] of Object.entries(files)) writeFileSync(join(legalOut, name), content);
80
+ }
74
81
  if (options.editorManifestDir) {
75
82
  const src = resolve(root, outDir, "design-manifest.json");
76
83
  if (existsSync(src)) {
@@ -93,23 +100,25 @@ function vitops(options = {}) {
93
100
  });
94
101
  }
95
102
  };
103
+ const watched = () => legalInput ? [input, legalInput] : [input];
96
104
  return {
97
105
  name: "@getvitops/vite",
98
106
  configResolved(config) {
99
107
  root = config.root;
100
108
  input = resolve(config.root, options.input ?? "design-system.json");
109
+ if (options.legal) legalInput = resolve(config.root, options.legal.input);
101
110
  },
102
111
  async buildStart() {
103
- this.addWatchFile(input);
112
+ for (const f of watched()) this.addWatchFile(f);
104
113
  await run();
105
114
  },
106
115
  async watchChange(id) {
107
- if (resolve(id) === input) await run();
116
+ if (watched().includes(resolve(id))) await run();
108
117
  },
109
118
  configureServer(server) {
110
- server.watcher.add(input);
119
+ for (const f of watched()) server.watcher.add(f);
111
120
  server.watcher.on("change", async (file) => {
112
- if (resolve(file) !== input) return;
121
+ if (!watched().includes(resolve(file))) return;
113
122
  await run();
114
123
  server.ws.send({ type: "full-reload" });
115
124
  });
@@ -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 { generate, validate, type Format } 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\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 root = '';\n\n const run = async () => {\n await generate({ input, format, outDir });\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 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 },\n async buildStart() {\n this.addWatchFile(input);\n await run();\n },\n async watchChange(id) {\n if (resolve(id) === input) await run();\n },\n configureServer(server) {\n server.watcher.add(input);\n server.watcher.on('change', async (file) => {\n if (resolve(file) !== input) 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` 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getvitops/vite",
3
- "version": "0.9.0",
3
+ "version": "1.0.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,8 +18,8 @@
18
18
  "access": "public"
19
19
  },
20
20
  "dependencies": {
21
- "@getvitops/generator": "0.9.0",
22
- "@getvitops/utils": "0.9.0"
21
+ "@getvitops/generator": "1.0.0",
22
+ "@getvitops/utils": "1.0.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "vite": "^6.0.0"