@getvitops/astro 2.1.0 → 4.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/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, relative, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { isSiteConfig, resolveSiteConfig } from "@getvitops/generator";
4
+ import { isConfig, resolveConfig } from "@getvitops/generator";
5
5
  import { collectIconRefs, faviconLinks, generateFavicons, generateIconInclude, resolveIcon, scanFiles, writeFaviconManifest } from "@getvitops/utils";
6
- import vitops from "@getvitops/vite";
6
+ import vitopsPlugin from "@getvitops/vite";
7
7
  import { fontProviders } from "astro/config";
8
8
  import { execFile } from "node:child_process";
9
9
  import { promisify } from "node:util";
@@ -204,10 +204,10 @@ function resolveFonts(decls) {
204
204
  const head = [];
205
205
  const seen = /* @__PURE__ */ new Set();
206
206
  for (const decl of decls) {
207
- if (!PROVIDERS.has(decl.provider)) throw new Error(`[getvitops] fonts: unknown provider "${decl.provider}" for "${decl.name}". Astro provides: ${[...PROVIDERS].sort().join(", ")}.`);
208
- if (NEEDS_OPTIONS.has(decl.provider)) throw new Error(`[getvitops] fonts: provider "${decl.provider}" needs an API id from your environment ("${decl.name}"), and a JSON declaration has nowhere to hold one. Declare this family in astro.config instead — fonts: [{ provider: fontProviders.${decl.provider}({ id: process.env.ADOBE_ID }), name: '${decl.name}', cssVariable: '${decl.cssVariable}' }] — and render its <Font /> yourself; the design-system token can still point at it.`);
209
- if (!decl.cssVariable.startsWith("--")) throw new Error(`[getvitops] fonts: cssVariable "${decl.cssVariable}" (${decl.name}) must start with "--".`);
210
- if (seen.has(decl.cssVariable)) throw new Error(`[getvitops] fonts: two families both declare ${decl.cssVariable} (the second is "${decl.name}"). Astro resolves one family per variable — give them separate variables, or drop one.`);
207
+ if (!PROVIDERS.has(decl.provider)) throw new Error(`[vitops] fonts: unknown provider "${decl.provider}" for "${decl.name}". Astro provides: ${[...PROVIDERS].sort().join(", ")}.`);
208
+ if (NEEDS_OPTIONS.has(decl.provider)) throw new Error(`[vitops] fonts: provider "${decl.provider}" needs an API id from your environment ("${decl.name}"), and a JSON declaration has nowhere to hold one. Declare this family in astro.config instead — fonts: [{ provider: fontProviders.${decl.provider}({ id: process.env.ADOBE_ID }), name: '${decl.name}', cssVariable: '${decl.cssVariable}' }] — and render its <Font /> yourself; the design-system token can still point at it.`);
209
+ if (!decl.cssVariable.startsWith("--")) throw new Error(`[vitops] fonts: cssVariable "${decl.cssVariable}" (${decl.name}) must start with "--".`);
210
+ if (seen.has(decl.cssVariable)) throw new Error(`[vitops] fonts: two families both declare ${decl.cssVariable} (the second is "${decl.name}"). Astro resolves one family per variable — give them separate variables, or drop one.`);
211
211
  seen.add(decl.cssVariable);
212
212
  const { provider, preload, ...rest } = decl;
213
213
  fonts.push({
@@ -225,6 +225,39 @@ function resolveFonts(decls) {
225
225
  };
226
226
  }
227
227
 
228
+ //#endregion
229
+ //#region src/tracking.ts
230
+ /** The cookie the capture script writes. Mirrors `TRACKING_COOKIE` in utils. */
231
+ const TRACKING_COOKIE = "_ac";
232
+ /**
233
+ * Resolve the tracking config into what the component emits.
234
+ *
235
+ * Two warnings, both about the same failure — a persistent identifier written
236
+ * with no way for the visitor to decline, or a permission asked for with no way
237
+ * to answer. Returned as data rather than logged here so the check is a test
238
+ * rather than a line someone might scroll past, exactly as `resolveAnalytics`
239
+ * does.
240
+ */
241
+ function resolveTracking(opts, ctx = {}) {
242
+ const warnings = [];
243
+ const category = opts?.category ?? "marketing";
244
+ const enabled = opts?.enabled === true;
245
+ if (!enabled) return {
246
+ enabled: false,
247
+ category,
248
+ cookies: [],
249
+ warnings
250
+ };
251
+ if (!ctx.consent) warnings.push(`tracking is on, so this site writes the \`${"_ac"}\` cookie — a 90-day identifier tying a visitor to the ad that brought them — and \`consent\` is off, so it is written for every visitor with no way to decline. Set \`consent: true\`.`);
252
+ else if (ctx.consentCategories && !ctx.consentCategories.includes(category)) warnings.push(`tracking demands the \`${category}\` consent category, but the banner offers ${ctx.consentCategories.join(", ") || "nothing"}. Add \`${category}\` to \`consent.categories\`, or the visitor is asked about something the banner never names.`);
253
+ return {
254
+ enabled,
255
+ category,
256
+ cookies: ["_ac"],
257
+ warnings
258
+ };
259
+ }
260
+
228
261
  //#endregion
229
262
  //#region src/integration.ts
230
263
  /**
@@ -237,11 +270,11 @@ function resolveFonts(decls) {
237
270
  * config from the `virtual:getvitops/head` module this integration provides.
238
271
  */
239
272
  /**
240
- * Which providers `getvitops({ analytics })` configures that the site config's
273
+ * Which providers `vitops({ analytics })` configures that the site config's
241
274
  * own `analytics` block does not.
242
275
  *
243
276
  * The two are separate surfaces on purpose — the integration must not import
244
- * `SiteConfig` — but they describe the same site, and a disagreement between them
277
+ * `Config` — but they describe the same site, and a disagreement between them
245
278
  * is a compliance defect rather than a style issue: `vitops legal` derives the
246
279
  * cookie notice from the site config, so a provider missing there is a tag the
247
280
  * site runs and its own notice never mentions. That is precisely what the
@@ -259,7 +292,7 @@ const SITE_CONFIG_KEYS = {
259
292
  function undisclosedProviders(configPath, analytics) {
260
293
  let declared;
261
294
  try {
262
- declared = JSON.parse(readFileSync(configPath, "utf8")).analytics ?? {};
295
+ declared = JSON.parse(readFileSync(configPath, "utf8")).site?.analytics ?? {};
263
296
  } catch {
264
297
  return [];
265
298
  }
@@ -328,14 +361,14 @@ async function probeIconPackage(name) {
328
361
  * the build here would turn "your JSON has a trailing comma" into an error about
329
362
  * an option the consumer never set.
330
363
  */
331
- function readsAsSiteConfig(path) {
364
+ function readsAsConfig(path) {
332
365
  try {
333
- return isSiteConfig(JSON.parse(readFileSync(path, "utf8")));
366
+ return isConfig(JSON.parse(readFileSync(path, "utf8")));
334
367
  } catch {
335
368
  return false;
336
369
  }
337
370
  }
338
- function getvitops(opts = {}) {
371
+ function vitops(opts = {}) {
339
372
  return {
340
373
  name: "@getvitops/astro",
341
374
  hooks: { "astro:config:setup": async ({ config, updateConfig, injectScript, logger }) => {
@@ -347,10 +380,10 @@ function getvitops(opts = {}) {
347
380
  if (opts.legal && !opts.css) logger.warn("legal: needs a `css` config — the generation runs in the Vite plugin that `css` registers. Run `npx vitops legal` instead if you do not use `css`.");
348
381
  if (opts.media && !opts.css) logger.warn("media: needs a `css` config — the encoding runs in the Vite plugin that `css` registers. Run `npx vitops media` instead if you do not use `css`.");
349
382
  const cssInput = opts.css ? opts.css.input ?? "design-system.json" : void 0;
350
- const siteInput = opts.site?.input ?? (cssInput != null && readsAsSiteConfig(resolve(root, cssInput)) ? cssInput : void 0);
383
+ const siteInput = opts.site?.input ?? (cssInput != null && readsAsConfig(resolve(root, cssInput)) ? cssInput : void 0);
351
384
  if (opts.site && !opts.css) logger.warn("site: needs a `css` config — it is read during generation, which runs in the Vite plugin that `css` registers. Without it the site config changes nothing here.");
352
385
  const legalInput = opts.legal?.input ?? siteInput;
353
- if (opts.legal && !legalInput) throw new Error("[getvitops] legal: needs `legal.input` (a site config path), the top-level `site` option, or a `css.input` that is itself a site config.");
386
+ if (opts.legal && !legalInput) throw new Error("[vitops] legal: needs `legal.input` (a site config path), the top-level `site` option, or a `css.input` that is itself a site config.");
354
387
  if (opts.seo) {
355
388
  if (!config.site) logger.warn("seo: needs the `site` astro.config option (your deployed URL) — without it <Seo /> omits canonical, og:url and any relative og:image rather than guessing an origin.");
356
389
  if (config.integrations.some((i) => i.name === "emdash")) logger.warn("seo: emdash() is registered, and its <EmDashHead> emits the same title/description/canonical/og tags from the CMS. Use one or the other — rendering both duplicates every tag.");
@@ -358,9 +391,18 @@ function getvitops(opts = {}) {
358
391
  const consentEnabled = !!opts.consent;
359
392
  const consentOpts = typeof opts.consent === "object" ? opts.consent : {};
360
393
  const analytics = resolveAnalytics(opts.analytics, { consent: consentEnabled });
394
+ const trackingOpts = opts.tracking === true ? { enabled: true } : opts.tracking || void 0;
361
395
  const detectedCategories = consentCategories(analytics.tags);
362
- const offeredCategories = consentOpts.categories ?? (detectedCategories.length ? detectedCategories : ["analytics"]);
396
+ const defaultCategories = ["analytics", "preferences"];
397
+ const trackingCategory = trackingOpts?.enabled ? trackingOpts.category ?? "marketing" : null;
398
+ if (detectedCategories.includes("marketing") || trackingCategory === "marketing") defaultCategories.push("marketing");
399
+ const offeredCategories = consentOpts.categories ?? defaultCategories;
363
400
  for (const warning of analytics.warnings) logger.warn(warning);
401
+ const tracking = resolveTracking(trackingOpts, {
402
+ consent: consentEnabled,
403
+ consentCategories: offeredCategories
404
+ });
405
+ for (const warning of tracking.warnings) logger.warn(warning);
364
406
  if (opts.analytics && legalInput) {
365
407
  const undisclosed = undisclosedProviders(resolve(root, legalInput), opts.analytics);
366
408
  if (undisclosed.length) logger.warn(`analytics: ${undisclosed.join(", ")} ${undisclosed.length === 1 ? "is" : "are"} configured here but absent from your site config's \`analytics\` block, so the cookie notice \`legal\` generates will not disclose ${undisclosed.length === 1 ? "it" : "them"}. Add ${undisclosed.map((k) => SITE_CONFIG_KEYS[k]).join(", ")} to ${legalInput}.`);
@@ -395,8 +437,8 @@ function getvitops(opts = {}) {
395
437
  };
396
438
  const declared = [];
397
439
  if (o.input) {
398
- const site = resolveSiteConfig(JSON.parse(readFileSync(resolve(root, o.input), "utf8")), o.siteEnv ?? "production");
399
- declared.push(...site.fonts ?? []);
440
+ const cfg = resolveConfig(JSON.parse(readFileSync(resolve(root, o.input), "utf8")), o.siteEnv ?? "production");
441
+ declared.push(...cfg.site.fonts ?? []);
400
442
  }
401
443
  declared.push(...o.families ?? []);
402
444
  if (declared.length === 0) logger.warn("fonts: nothing declared — no webfonts will load. Add families, or drop the option.");
@@ -441,7 +483,7 @@ function getvitops(opts = {}) {
441
483
  try {
442
484
  ({default: sitemap} = await import("@astrojs/sitemap"));
443
485
  } catch {
444
- throw new Error("[getvitops] sitemap requires `@astrojs/sitemap` in your devDependencies — install it, or drop the `sitemap` option.");
486
+ throw new Error("[vitops] sitemap requires `@astrojs/sitemap` in your devDependencies — install it, or drop the `sitemap` option.");
445
487
  }
446
488
  if (config.output === "server") logger.warn("sitemap: output: 'server' — @astrojs/sitemap lists prerendered routes only, so on-demand pages are left out. Add `export const prerender = true` to the pages you want indexed, or list them in `sitemap.customPages`.");
447
489
  const sitemapOpts = opts.sitemap === true ? void 0 : opts.sitemap;
@@ -451,7 +493,7 @@ function getvitops(opts = {}) {
451
493
  }
452
494
  }
453
495
  if (siteInput) try {
454
- const key = resolveSiteConfig(JSON.parse(readFileSync(resolve(root, siteInput), "utf8")), opts.site?.siteEnv ?? "production").seo?.indexing?.indexNow?.key;
496
+ const key = resolveConfig(JSON.parse(readFileSync(resolve(root, siteInput), "utf8")), opts.site?.siteEnv ?? "production").site.seo?.indexing?.indexNow?.key;
455
497
  if (key) {
456
498
  mkdirSync(publicDir, { recursive: true });
457
499
  writeFileSync(join(publicDir, `${key}.txt`), `${key}\n`);
@@ -558,13 +600,18 @@ function getvitops(opts = {}) {
558
600
  consent: consentEnabled,
559
601
  consentCategories: offeredCategories,
560
602
  consentPolicyUrl: consentOpts.policyUrl ?? null,
561
- consentRuntime
603
+ consentRuntime,
604
+ tracking: tracking.enabled ? {
605
+ enabled: true,
606
+ category: tracking.category,
607
+ cookies: tracking.cookies
608
+ } : null
562
609
  })] } });
563
610
  if (opts.css) {
564
611
  const format = opts.css.format ?? "tailwind";
565
- if (format === "design") throw new Error("[getvitops] css.format: 'design' emits only DESIGN.md, not a stylesheet. Run `npx vitops generate --format design --out .` for that file, and set css.format to 'tailwind' | 'css' | 'bricks' here.");
612
+ if (format === "design") throw new Error("[vitops] css.format: 'design' emits only DESIGN.md, not a stylesheet. Run `npx vitops generate --format design --out .` for that file, and set css.format to 'tailwind' | 'css' | 'bricks' here.");
566
613
  const out = opts.css.out ?? "src/styles";
567
- const plugins = [vitops({
614
+ const plugins = [vitopsPlugin({
568
615
  input: opts.css.input ?? "design-system.json",
569
616
  ...opts.css.theme != null ? { theme: opts.css.theme } : {},
570
617
  format,
@@ -587,7 +634,7 @@ function getvitops(opts = {}) {
587
634
  TAILWIND_VITE
588
635
  ));
589
636
  } catch {
590
- throw new Error("[getvitops] css.format: 'tailwind' requires `tailwindcss` and `@tailwindcss/vite` in your devDependencies — install them, or switch to css.format: 'css' for the standalone bundle.");
637
+ throw new Error("[vitops] css.format: 'tailwind' requires `tailwindcss` and `@tailwindcss/vite` in your devDependencies — install them, or switch to css.format: 'css' for the standalone bundle.");
591
638
  }
592
639
  plugins.push(...tailwindcss());
593
640
  }
@@ -609,7 +656,7 @@ function getvitops(opts = {}) {
609
656
  * Real per-page `<lastmod>` for the sitemap, derived from git.
610
657
  *
611
658
  * This is the one lever that reliably moves Google. It is also load-bearing for
612
- * `vitops indexing`, which decides what to submit by diffing lastmod against the
659
+ * `vitops search notify`, which decides what to submit by diffing lastmod against the
613
660
  * previous build — with no lastmod, that diff can see pages appear and disappear
614
661
  * but never see one *change*, so an edited page is never resubmitted.
615
662
  *
@@ -714,7 +761,7 @@ async function commitDates(dirs, cwd) {
714
761
  */
715
762
  async function gitLastmod(options = {}) {
716
763
  const cwd = options.cwd ?? process.cwd();
717
- const warn = options.onWarn ?? ((m) => console.warn(`[getvitops] gitLastmod: ${m}`));
764
+ const warn = options.onWarn ?? ((m) => console.warn(`[vitops] gitLastmod: ${m}`));
718
765
  const dirs = (options.dirs ?? DEFAULT_DIRS).filter((d) => existsSync(`${cwd}/${d}`));
719
766
  const identity = (item) => item;
720
767
  if (dirs.length === 0) {
@@ -886,5 +933,5 @@ function resolveSeo(defaults, props, ctx) {
886
933
  }
887
934
 
888
935
  //#endregion
889
- export { consentCategories, getvitops as default, gitLastmod, resolveAnalytics, resolveSeo, routeFromPage, slugFromContent };
936
+ export { TRACKING_COOKIE, consentCategories, vitops as default, gitLastmod, resolveAnalytics, resolveSeo, resolveTracking, routeFromPage, slugFromContent };
890
937
  //# sourceMappingURL=index.mjs.map