@gusnips/vite 0.8.32 → 0.8.34

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.
@@ -0,0 +1,61 @@
1
+ /** One SVG master at one pixel size. The renderer is the caller's; this only drives it. */
2
+ export type RenderIcon = (svg: string, size: number) => Uint8Array | Promise<Uint8Array>;
3
+ /** Several same-size PNGs into one multi-image `favicon.ico`. */
4
+ export type PackIco = (pngs: readonly Uint8Array[]) => Uint8Array | Promise<Uint8Array>;
5
+ export interface WriteIconSetOptions {
6
+ /** Where the files land — `public/` in every donor, so they are served as static files. */
7
+ outDir: string;
8
+ /** The one master artwork. A single path with no variants is what keeps every size in step. */
9
+ svg: string;
10
+ /** PNG sizes to write, e.g. `[180, 192, 512]`. Each is rendered once even when named twice. */
11
+ pngSizes: readonly number[];
12
+ /** The file name for one PNG size. Default `` `icon-${size}.png` ``. */
13
+ file?: (size: number) => string;
14
+ /**
15
+ * The size written as `apple-touch-icon.png`. Default 180 — what the reader's home screen
16
+ * asks for. Reuses the PNG render when it is already in `pngSizes`, so the common case costs
17
+ * no extra raster.
18
+ */
19
+ appleTouchSize?: number;
20
+ /** Sizes re-rendered and packed into `favicon.ico`, e.g. `[16, 32, 48]`. */
21
+ icoSizes: readonly number[];
22
+ /** Lay every icon out and report, without writing anything. */
23
+ check?: boolean;
24
+ render: RenderIcon;
25
+ packIco: PackIco;
26
+ }
27
+ /**
28
+ * Render one icon set and write it, plus the multi-size `favicon.ico`.
29
+ *
30
+ * Every size is rendered BEFORE anything is written, so a renderer that throws leaves no
31
+ * half-written set behind — the same order `writeOgCards` keeps for the same reason.
32
+ */
33
+ export declare function writeIconSet({ outDir, svg, pngSizes, file, appleTouchSize, icoSizes, check, render, packIco, }: WriteIconSetOptions): Promise<string[]>;
34
+ /** One icon as the manifest names it. `src` is the path `writeIconSet` wrote, from the root. */
35
+ export interface ManifestIcon {
36
+ src: string;
37
+ /** `"192x192"`, `"48x48"`, or several space-separated when one file serves several. */
38
+ sizes: string;
39
+ type: string;
40
+ purpose?: string;
41
+ }
42
+ export interface WebManifestOptions {
43
+ name: string;
44
+ shortName?: string;
45
+ description?: string;
46
+ /** Default `"."`: the manifest travels with the app, so a relative start stays correct. */
47
+ startUrl?: string;
48
+ scope?: string;
49
+ /** Default `"standalone"`: a PWA that opens in a tab is a bookmark, not an app. */
50
+ display?: string;
51
+ backgroundColor?: string;
52
+ themeColor?: string;
53
+ /** At least one — a manifest with no icons fails installability with no error anywhere. */
54
+ icons: readonly ManifestIcon[];
55
+ }
56
+ /**
57
+ * `site.webmanifest`, as text with a trailing newline. Keys in install order, so two runs over
58
+ * the same options write the same bytes.
59
+ */
60
+ export declare function webManifest({ name, shortName, description, startUrl, scope, display, backgroundColor, themeColor, icons, }: WebManifestOptions): string;
61
+ //# sourceMappingURL=icons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../src/icons.ts"],"names":[],"mappings":"AAaA,2FAA2F;AAC3F,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEzF,iEAAiE;AACjE,MAAM,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAExF,MAAM,WAAW,mBAAmB;IAClC,2FAA2F;IAC3F,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,GAAG,EAAE,MAAM,CAAC;IACZ,+FAA+F;IAC/F,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,wEAAwE;IACxE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAChC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,4EAA4E;IAC5E,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,EACjC,MAAM,EACN,GAAG,EACH,QAAQ,EACR,IAAmC,EACnC,cAAoB,EACpB,QAAQ,EACR,KAAa,EACb,MAAM,EACN,OAAO,GACR,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAsBzC;AAED,gGAAgG;AAChG,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,uFAAuF;IACvF,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2FAA2F;IAC3F,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,SAAS,EACT,WAAW,EACX,QAAc,EACd,KAAK,EACL,OAAsB,EACtB,eAAe,EACf,UAAU,EACV,KAAK,GACN,EAAE,kBAAkB,GAAG,MAAM,CAmB7B"}
package/dist/icons.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * PWA icons and the manifest that names them, driven the way `writeOgCards` is.
3
+ *
4
+ * The artwork is the product's — one SVG master — and so are the two operations this package
5
+ * cannot do without dragging image tooling into every adopter: rasterizing (resvg, sharp,
6
+ * satori, …) and ICO packing. Both arrive as callbacks, so this file stays dependency-free and
7
+ * a repo that only wants a sitemap installs neither. What ships is the part every copy got
8
+ * subtly wrong: which sizes exist, what they are named, and the manifest agreeing with the
9
+ * files on disk.
10
+ */
11
+ import { mkdir, writeFile } from "node:fs/promises";
12
+ import { join } from "node:path";
13
+ /**
14
+ * Render one icon set and write it, plus the multi-size `favicon.ico`.
15
+ *
16
+ * Every size is rendered BEFORE anything is written, so a renderer that throws leaves no
17
+ * half-written set behind — the same order `writeOgCards` keeps for the same reason.
18
+ */
19
+ export async function writeIconSet({ outDir, svg, pngSizes, file = (size) => `icon-${size}.png`, appleTouchSize = 180, icoSizes, check = false, render, packIco, }) {
20
+ const sizes = [...new Set([...pngSizes, appleTouchSize, ...icoSizes])].sort((a, b) => a - b);
21
+ const rendered = new Map();
22
+ for (const size of sizes)
23
+ rendered.set(size, await render(svg, size));
24
+ const written = [];
25
+ for (const size of pngSizes)
26
+ written.push({ file: file(size), png: rendered.get(size) ?? new Uint8Array() });
27
+ written.push({
28
+ file: "apple-touch-icon.png",
29
+ png: rendered.get(appleTouchSize) ?? new Uint8Array(),
30
+ });
31
+ written.push({
32
+ file: "favicon.ico",
33
+ png: await packIco(icoSizes.map((size) => rendered.get(size) ?? new Uint8Array())),
34
+ });
35
+ if (check)
36
+ return written.map((entry) => entry.file);
37
+ await mkdir(outDir, { recursive: true });
38
+ for (const entry of written)
39
+ await writeFile(join(outDir, entry.file), entry.png);
40
+ return written.map((entry) => entry.file);
41
+ }
42
+ /**
43
+ * `site.webmanifest`, as text with a trailing newline. Keys in install order, so two runs over
44
+ * the same options write the same bytes.
45
+ */
46
+ export function webManifest({ name, shortName, description, startUrl = ".", scope, display = "standalone", backgroundColor, themeColor, icons, }) {
47
+ if (name.trim() === "")
48
+ throw new Error("manifest: `name` is empty");
49
+ if (icons.length === 0)
50
+ throw new Error("manifest: `icons` is empty — name what writeIconSet wrote");
51
+ const manifest = { name };
52
+ if (shortName !== undefined)
53
+ manifest["short_name"] = shortName;
54
+ if (description !== undefined)
55
+ manifest["description"] = description;
56
+ manifest["start_url"] = startUrl;
57
+ if (scope !== undefined)
58
+ manifest["scope"] = scope;
59
+ manifest["display"] = display;
60
+ if (backgroundColor !== undefined)
61
+ manifest["background_color"] = backgroundColor;
62
+ if (themeColor !== undefined)
63
+ manifest["theme_color"] = themeColor;
64
+ manifest["icons"] = icons.map((icon) => ({
65
+ src: icon.src,
66
+ sizes: icon.sizes,
67
+ type: icon.type,
68
+ ...(icon.purpose !== undefined && { purpose: icon.purpose }),
69
+ }));
70
+ return `${JSON.stringify(manifest, null, 2)}\n`;
71
+ }
72
+ //# sourceMappingURL=icons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.js","sourceRoot":"","sources":["../src/icons.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -9,19 +9,21 @@
9
9
  * Four repos wrote it independently and each learned something the others had not. What is
10
10
  * here is the merge; every non-obvious rule carries the reason it exists.
11
11
  *
12
- * Three things live behind their own subpath, because each drags a dependency this barrel would
12
+ * Five things live behind their own subpath, because each drags a dependency this barrel would
13
13
  * otherwise force on everyone: the vite config preset at `@gusnips/vite/preset` (the React and
14
- * Tailwind plugins), `renderTree` at `@gusnips/vite/render`, and the theme pre-paint plugin at
15
- * `@gusnips/vite/theme` (both React itself). Nothing here
16
- * imports React or vite, so a prerender script, an OG generator and a repo that only wants a
17
- * sitemap all install exactly what they use.
14
+ * Tailwind plugins), `renderTree` at `@gusnips/vite/render`, the theme pre-paint plugin at
15
+ * `@gusnips/vite/theme` (both React itself), the catalog check at `@gusnips/vite/i18n`, and the
16
+ * contrast gate at `@gusnips/vite/contrast` (tailwindcss) — the last two run in CI and never in
17
+ * a build. Nothing here imports React, vite or tailwindcss, so a prerender script, an OG
18
+ * generator and a repo that only wants a sitemap all install exactly what they use.
18
19
  */
19
20
  export { resolveKey } from "./catalog.ts";
20
21
  export { escapeAttr } from "./escape.ts";
21
- export { assertRendered, bakeHead, EMPTY_ROOT, ogLocale, type Alternate, type HeadTags, type RenderedChecks, } from "./head.ts";
22
+ export { assertRendered, bakeHead, EMPTY_ROOT, faqJsonLd, ogLocale, type Alternate, type FaqEntry, type HeadTags, type RenderedChecks, } from "./head.ts";
22
23
  export { assertOgImages, loadRenderer, loadTemplate, writeDist, writeOgCards, type OgCard, type WriteOgCardsOptions, } from "./node.ts";
24
+ export { webManifest, writeIconSet, type ManifestIcon, type PackIco, type RenderIcon, type WebManifestOptions, type WriteIconSetOptions, } from "./icons.ts";
23
25
  export { describeOverflow, fitText, OG_CANVAS, type FitOptions, type FitResult, type OgOverflow, } from "./og.ts";
24
26
  export { prePaintScript, type PrePaintScriptOptions } from "./pre-paint.ts";
25
27
  export type { PageRenderer } from "./render.ts";
26
- export { ogImagePath, pageFile, pageSlug, robotsTxt, siteOrigin, sitemapFor, sitemapXml, type PublicPage, type RobotsOptions, type SitemapEntry, } from "./sitemap.ts";
28
+ export { ogImagePath, pageFile, pageSlug, robotsTxt, siteOrigin, sitemapFor, sitemapXml, templateOrigin, type PublicPage, type RobotsOptions, type SitemapEntry, } from "./sitemap.ts";
27
29
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK1C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,cAAc,EACd,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,cAAc,GACpB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,KAAK,MAAM,EACX,KAAK,mBAAmB,GACzB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,gBAAgB,EAChB,OAAO,EACP,SAAS,EACT,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAK5E,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,EACV,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK1C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,cAAc,EACd,QAAQ,EACR,UAAU,EACV,SAAS,EACT,QAAQ,EACR,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,cAAc,GACpB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,KAAK,MAAM,EACX,KAAK,mBAAmB,GACzB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,WAAW,EACX,YAAY,EACZ,KAAK,YAAY,EACjB,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,gBAAgB,EAChB,OAAO,EACP,SAAS,EACT,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAK5E,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,EACV,cAAc,EACd,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -9,12 +9,13 @@
9
9
  * Four repos wrote it independently and each learned something the others had not. What is
10
10
  * here is the merge; every non-obvious rule carries the reason it exists.
11
11
  *
12
- * Three things live behind their own subpath, because each drags a dependency this barrel would
12
+ * Five things live behind their own subpath, because each drags a dependency this barrel would
13
13
  * otherwise force on everyone: the vite config preset at `@gusnips/vite/preset` (the React and
14
- * Tailwind plugins), `renderTree` at `@gusnips/vite/render`, and the theme pre-paint plugin at
15
- * `@gusnips/vite/theme` (both React itself). Nothing here
16
- * imports React or vite, so a prerender script, an OG generator and a repo that only wants a
17
- * sitemap all install exactly what they use.
14
+ * Tailwind plugins), `renderTree` at `@gusnips/vite/render`, the theme pre-paint plugin at
15
+ * `@gusnips/vite/theme` (both React itself), the catalog check at `@gusnips/vite/i18n`, and the
16
+ * contrast gate at `@gusnips/vite/contrast` (tailwindcss) — the last two run in CI and never in
17
+ * a build. Nothing here imports React, vite or tailwindcss, so a prerender script, an OG
18
+ * generator and a repo that only wants a sitemap all install exactly what they use.
18
19
  */
19
20
  export { resolveKey } from "./catalog.js";
20
21
  // `headExtra` takes raw markup, so the moment an adopter builds a tag out of its own copy it
@@ -22,10 +23,11 @@ export { resolveKey } from "./catalog.js";
22
23
  // re-invented per app: an adopter's hand-rolled copy is one that can drift from ours, and the
23
24
  // first one to need it was building a `<link title="…">` out of a translated page title.
24
25
  export { escapeAttr } from "./escape.js";
25
- export { assertRendered, bakeHead, EMPTY_ROOT, ogLocale, } from "./head.js";
26
+ export { assertRendered, bakeHead, EMPTY_ROOT, faqJsonLd, ogLocale, } from "./head.js";
26
27
  export { assertOgImages, loadRenderer, loadTemplate, writeDist, writeOgCards, } from "./node.js";
28
+ export { webManifest, writeIconSet, } from "./icons.js";
27
29
  export { describeOverflow, fitText, OG_CANVAS, } from "./og.js";
28
30
  // The locale gate's delivery. Type-only use of vite, so the barrel still imports no peer.
29
31
  export { prePaintScript } from "./pre-paint.js";
30
- export { ogImagePath, pageFile, pageSlug, robotsTxt, siteOrigin, sitemapFor, sitemapXml, } from "./sitemap.js";
32
+ export { ogImagePath, pageFile, pageSlug, robotsTxt, siteOrigin, sitemapFor, sitemapXml, templateOrigin, } from "./sitemap.js";
31
33
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,6FAA6F;AAC7F,2FAA2F;AAC3F,8FAA8F;AAC9F,yFAAyF;AACzF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,cAAc,EACd,QAAQ,EACR,UAAU,EACV,QAAQ,GAIT,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,YAAY,GAGb,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,gBAAgB,EAChB,OAAO,EACP,SAAS,GAIV,MAAM,SAAS,CAAC;AACjB,0FAA0F;AAC1F,OAAO,EAAE,cAAc,EAA8B,MAAM,gBAAgB,CAAC;AAM5E,OAAO,EACL,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,GAIX,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,6FAA6F;AAC7F,2FAA2F;AAC3F,8FAA8F;AAC9F,yFAAyF;AACzF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,cAAc,EACd,QAAQ,EACR,UAAU,EACV,SAAS,EACT,QAAQ,GAKT,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,YAAY,GAGb,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,WAAW,EACX,YAAY,GAMb,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,gBAAgB,EAChB,OAAO,EACP,SAAS,GAIV,MAAM,SAAS,CAAC;AACjB,0FAA0F;AAC1F,OAAO,EAAE,cAAc,EAA8B,MAAM,gBAAgB,CAAC;AAM5E,OAAO,EACL,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,EACV,cAAc,GAIf,MAAM,cAAc,CAAC"}
package/dist/sitemap.d.ts CHANGED
@@ -33,6 +33,15 @@ export declare function pageFile(routePath: string): string;
33
33
  * URL. That is a bad build, not a bad page, so it fails here.
34
34
  */
35
35
  export declare function siteOrigin(url: string): string;
36
+ /**
37
+ * The origin the template already declares, read out of its canonical.
38
+ *
39
+ * One file is the answer for the head Vite ships and for every file written after it, so there
40
+ * is no second place for a domain to drift to — no env var the script has to resolve beside
41
+ * it. Demanded rather than defaulted, for the reason `bakeHead` demands the tag: a template
42
+ * without one would hand every page somebody else's origin, quietly.
43
+ */
44
+ export declare function templateOrigin(template: string): string;
36
45
  /**
37
46
  * The three things about a public page that do not translate: where it is, how often it
38
47
  * changes, and how it ranks against its siblings.
@@ -1 +1 @@
1
- {"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../src/sitemap.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGlD;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9C;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;CAC5F;AAED;+CAC+C;AAC/C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7C;AAED,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,CAAC;IACjB;iEAC6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;iFAC6E;IAC7E,UAAU,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;CACnC;AAED,+DAA+D;AAC/D,wBAAgB,UAAU,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAyBnE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAYjG;AAED,MAAM,WAAW,aAAa;IAC5B;;uFAEmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf;;;;iDAI6C;IAC7C,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B;0FACsF;IACtF,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAa,EAAE,EAAE,aAAa,GAAG,MAAM,CAQpF"}
1
+ {"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../src/sitemap.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGlD;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM9C;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CASvD;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;CAC5F;AAED;+CAC+C;AAC/C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7C;AAED,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,CAAC;IACjB;iEAC6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;iFAC6E;IAC7E,UAAU,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;CACnC;AAED,+DAA+D;AAC/D,wBAAgB,UAAU,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAyBnE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAYjG;AAED,MAAM,WAAW,aAAa;IAC5B;;uFAEmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf;;;;iDAI6C;IAC7C,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B;0FACsF;IACtF,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAa,EAAE,EAAE,aAAa,GAAG,MAAM,CAQpF"}
package/dist/sitemap.js CHANGED
@@ -46,6 +46,23 @@ export function siteOrigin(url) {
46
46
  throw new Error(`prerender: "${url}" is not an absolute origin — it needs a scheme, as in https://example.com`);
47
47
  return url.replace(/\/+$/, "");
48
48
  }
49
+ /**
50
+ * The origin the template already declares, read out of its canonical.
51
+ *
52
+ * One file is the answer for the head Vite ships and for every file written after it, so there
53
+ * is no second place for a domain to drift to — no env var the script has to resolve beside
54
+ * it. Demanded rather than defaulted, for the reason `bakeHead` demands the tag: a template
55
+ * without one would hand every page somebody else's origin, quietly.
56
+ */
57
+ export function templateOrigin(template) {
58
+ const href = /<link[^>]*rel="canonical"[^>]*href="([^"]*)"/.exec(template)?.[1] ??
59
+ /<link[^>]*href="([^"]*)"[^>]*rel="canonical"/.exec(template)?.[1];
60
+ if (href === undefined)
61
+ throw new Error('prerender: <link rel="canonical"> not found in index.html');
62
+ // A canonical names a PAGE, not the origin — drop the path. `siteOrigin` first, for its
63
+ // scheme demand: `VITE_SITE_URL=acme.com` looks right in a `.env` and is a relative URL.
64
+ return new URL(siteOrigin(href)).origin;
65
+ }
49
66
  /** URL slug for a page path (`/pricing` → `pricing`, `/` → `home`), used to name its share
50
67
  * card and to key a locale catalog's copy. */
51
68
  export function pageSlug(path) {
@@ -1 +1 @@
1
- {"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../src/sitemap.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../src/sitemap.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gusnips/vite",
3
- "version": "0.8.32",
3
+ "version": "0.8.34",
4
4
  "description": "The build-time half of a prerendered Vite SPA: head baking, sitemap, robots, the prerender driver, a shared vite preset, and the translation catalog check.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,6 +18,10 @@
18
18
  "types": "./dist/preset.d.ts",
19
19
  "default": "./dist/preset.js"
20
20
  },
21
+ "./contrast": {
22
+ "types": "./dist/contrast.d.ts",
23
+ "default": "./dist/contrast.js"
24
+ },
21
25
  "./i18n": {
22
26
  "types": "./dist/i18n.d.ts",
23
27
  "default": "./dist/i18n.js"
@@ -69,16 +73,21 @@
69
73
  "release:major": "bun pm version major && bun publish --access public"
70
74
  },
71
75
  "dependencies": {
72
- "@gusnips/react": "0.9.22"
76
+ "@gusnips/react": "0.9.23"
73
77
  },
74
78
  "peerDependencies": {
79
+ "@tailwindcss/cli": "^4",
75
80
  "@tailwindcss/vite": "^4",
76
81
  "@vitejs/plugin-react": "^6",
77
82
  "react": "^19",
78
83
  "react-dom": "^19",
84
+ "tailwindcss": "^4",
79
85
  "vite": "^8"
80
86
  },
81
87
  "peerDependenciesMeta": {
88
+ "@tailwindcss/cli": {
89
+ "optional": true
90
+ },
82
91
  "@tailwindcss/vite": {
83
92
  "optional": true
84
93
  },
@@ -91,11 +100,15 @@
91
100
  "react-dom": {
92
101
  "optional": true
93
102
  },
103
+ "tailwindcss": {
104
+ "optional": true
105
+ },
94
106
  "vite": {
95
107
  "optional": true
96
108
  }
97
109
  },
98
110
  "devDependencies": {
111
+ "@tailwindcss/cli": "^4.3.3",
99
112
  "@tailwindcss/vite": "^4.2.2",
100
113
  "@types/node": "^24.12.0",
101
114
  "@types/react": "^19.2.14",
@@ -103,6 +116,7 @@
103
116
  "@vitejs/plugin-react": "^6.0.1",
104
117
  "react": "^19.2.4",
105
118
  "react-dom": "^19.2.4",
119
+ "tailwindcss": "^4.3.3",
106
120
  "typescript": "^5.9.3",
107
121
  "vite": "^8.0.1",
108
122
  "vitest": "^4.1.2"
@@ -0,0 +1,94 @@
1
+ import { rm } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { checkContrast, formatContrastReport, type ContrastReport } from "./contrast.ts";
6
+
7
+ /**
8
+ * These run the real Tailwind CLI over a committed fixture theme, the way an adopter's CI
9
+ * would — a unit test with a mocked compiler would prove the assertions and nothing about the
10
+ * compile they read. Two fixtures: one that keeps the whole contract, one that breaks every
11
+ * rule once. Each rule below is watched firing, and the clean fixture is the positive control
12
+ * that the gate can pass.
13
+ */
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+ const ok = join(here, "fixtures/contrast/ok");
16
+ const bad = join(here, "fixtures/contrast/bad");
17
+
18
+ afterEach(async () => {
19
+ // The compile workdir is removed on success and kept on failure for reading; in a test run
20
+ // the assertion IS the reading, so both fixtures clean up after themselves.
21
+ await rm(join(ok, ".contrast-check"), { recursive: true, force: true });
22
+ await rm(join(bad, ".contrast-check"), { recursive: true, force: true });
23
+ });
24
+
25
+ function rules(report: ContrastReport): string[] {
26
+ return report.problems.map((p) => `${p.level[0]}:${p.rule}`).sort();
27
+ }
28
+
29
+ describe("checkContrast", () => {
30
+ it("passes a theme and classes that keep the contract", async () => {
31
+ const report = await checkContrast([{ name: "ok", entry: "theme.css", sources: ["*.html"] }], {
32
+ root: ok,
33
+ });
34
+ expect(report.problems).toEqual([]);
35
+ expect(formatContrastReport(report)).toBe("✔ contrast: no errors");
36
+ }, 120_000);
37
+
38
+ it("fails every rule once on the bad fixture, and points at the source", async () => {
39
+ const report = await checkContrast([{ name: "bad", entry: "theme.css", sources: ["*.html"] }], {
40
+ root: bad,
41
+ });
42
+ expect(rules(report)).toEqual([
43
+ "e:floor",
44
+ "e:floor",
45
+ "e:rebind",
46
+ "e:retune",
47
+ "e:untokened",
48
+ "e:untokened",
49
+ "e:untokened",
50
+ "w:dead",
51
+ "w:hover",
52
+ ]);
53
+ const untokened = report.problems.filter((p) => p.rule === "untokened");
54
+ expect(untokened.map((p) => p.class).sort()).toEqual([
55
+ "bg-black/40",
56
+ "border-gray-300",
57
+ "text-white",
58
+ ]);
59
+ for (const p of untokened) expect(p.at).toMatch(/^page\.html:\d+$/);
60
+ expect(report.problems.find((p) => p.rule === "hover")?.class).toBe("hover:bg-primary");
61
+ expect(report.problems.find((p) => p.rule === "dead")?.class).toBe("not-a-token-xyz");
62
+ expect(report.problems.find((p) => p.rule === "retune")?.message).toContain("#7c3aed");
63
+ expect(formatContrastReport(report)).toContain("✖ contrast: 7 error(s), 2 warning(s)");
64
+ }, 120_000);
65
+
66
+ it("an allow list silences the classes a repo means, and nothing else", async () => {
67
+ const report = await checkContrast(
68
+ [
69
+ {
70
+ name: "bad",
71
+ entry: "theme.css",
72
+ sources: ["*.html"],
73
+ allow: [/^text-white$/, /^bg-black/],
74
+ },
75
+ ],
76
+ { root: bad },
77
+ );
78
+ expect(report.problems.filter((p) => p.rule === "untokened").map((p) => p.class)).toEqual([
79
+ "border-gray-300",
80
+ ]);
81
+ // The floors do not take an allow list: a measured ratio is not a judgement call.
82
+ expect(report.problems.filter((p) => p.rule === "floor")).toHaveLength(2);
83
+ }, 120_000);
84
+
85
+ it("a theme that does not load fails the gate instead of passing it", async () => {
86
+ const report = await checkContrast(
87
+ [{ name: "missing", entry: "no-such-file.css", sources: ["*.html"] }],
88
+ { root: ok },
89
+ );
90
+ expect(report.problems).toHaveLength(1);
91
+ expect(report.problems[0]?.rule).toBe("load");
92
+ expect(report.problems[0]?.level).toBe("error");
93
+ });
94
+ });