@monocle.sh/vite-plugin 0.2.1 → 0.3.1

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/README.md CHANGED
@@ -70,14 +70,28 @@ unless it is one of the placeholders above.
70
70
 
71
71
  ## How it works
72
72
 
73
- The plugin prepends `globalThis.__MONOCLE_BUILD__ = {…}` to each of your modules
74
- that imports `@monocle.sh/browser-sdk`. The SDK reads it in `initMonocle()`.
75
-
76
- This targets application code rather than relying on `define`, which Vite does
77
- not apply to pre-bundled dependencies. It needs no inline `<script>`, so it
78
- works with SSR frameworks that don't use `index.html` and with strict CSPs.
79
- During SSR, the global is also set on the server, where the Browser SDK stays
80
- inactive.
73
+ When every entry of `vite build` is an HTML page (the default `index.html`
74
+ setup), the plugin writes the release into the page as
75
+ `<meta name="monocle-build" content="{…}">`, which the SDK reads in
76
+ `initMonocle()`. Your JavaScript chunks never contain the release, so two builds
77
+ of the same code produce identical chunks, file names and debug IDs: a deploy
78
+ only uploads the source maps of the chunks that changed. A `<meta>` tag is not
79
+ affected by Content Security Policies. This needs `@monocle.sh/browser-sdk`
80
+ 0.13.0 or later; with an older SDK the plugin warns and falls back to the
81
+ bundle.
82
+
83
+ Otherwise (dev server, SSR, library builds, frameworks rendering the HTML
84
+ themselves), the plugin prepends `globalThis.__MONOCLE_BUILD__ = {…}` to each of
85
+ your modules that imports `@monocle.sh/browser-sdk`. This targets application
86
+ code rather than relying on `define`, which Vite does not apply to pre-bundled
87
+ dependencies. During SSR, the global is also set on the server, where the
88
+ Browser SDK stays inactive.
89
+
90
+ Vite and Rolldown also put the hashed file name of every imported chunk into
91
+ the importing chunk, so changing one chunk renames the chunks that import it,
92
+ up to the entry. To keep those unchanged too, enable Vite's
93
+ [`build.chunkImportMap`](https://vite.dev/config/build-options) (Vite 8,
94
+ experimental).
81
95
 
82
96
  ## Source maps
83
97
 
package/dist/vite.d.mts CHANGED
@@ -60,6 +60,7 @@ interface MonocleSourcemapsOptions {
60
60
  * Translate Vite hooks to the bundler-agnostic Monocle build.
61
61
  */
62
62
  declare class MonocleVitePlugin {
63
+ #private;
63
64
  static create(options?: MonoclePluginOptions): Plugin;
64
65
  }
65
66
  /**
package/dist/vite.mjs CHANGED
@@ -1,8 +1,51 @@
1
- import { dirname, join, resolve } from "node:path";
2
- import { readFileSync, statSync } from "node:fs";
1
+ import { createRequire } from "node:module";
2
+ import { dirname, join, resolve, sep } from "node:path";
3
+ import { readFileSync, realpathSync, statSync } from "node:fs";
3
4
  import MagicString from "magic-string";
4
5
  import { rm } from "node:fs/promises";
5
6
  import { injectDebugIds, uploadSourcemaps } from "@monocle.sh/sourcemaps";
7
+ //#region src/core/browser_sdk.ts
8
+ /**
9
+ * What the Browser SDK the application resolves can read.
10
+ */
11
+ var BrowserSdk = class {
12
+ /**
13
+ * First `@monocle.sh/browser-sdk` reading `<meta name="monocle-build">`.
14
+ */
15
+ static pageReleaseSince = "0.13.0";
16
+ static #atLeast(version, minimum) {
17
+ const parse = (value) => value.split(/[-+]/)[0].split(".").map(Number);
18
+ const [actual, expected] = [parse(version), parse(minimum)];
19
+ for (let index = 0; index < 3; index++) {
20
+ const difference = (actual[index] ?? 0) - (expected[index] ?? 0);
21
+ if (Number.isNaN(difference)) return false;
22
+ if (difference !== 0) return difference > 0;
23
+ }
24
+ return true;
25
+ }
26
+ /**
27
+ * Where the installed SDK reads the release: `page` from the HTML, `bundle` only from the
28
+ * global, `missing` when the application does not resolve it, so nothing reads a page tag. A
29
+ * linked local copy (workspace) is assumed current; one that cannot be inspected (Yarn PnP
30
+ * archives…) reads the bundle.
31
+ */
32
+ static releaseSource(root) {
33
+ let path;
34
+ try {
35
+ path = createRequire(join(root, "package.json")).resolve("@monocle.sh/browser-sdk/package.json");
36
+ } catch {
37
+ return "missing";
38
+ }
39
+ try {
40
+ if (!realpathSync(path).includes(`${sep}node_modules${sep}`)) return "page";
41
+ const { version } = JSON.parse(readFileSync(path, "utf8"));
42
+ return typeof version === "string" && this.#atLeast(version, this.pageReleaseSince) ? "page" : "bundle";
43
+ } catch {
44
+ return "bundle";
45
+ }
46
+ }
47
+ };
48
+ //#endregion
6
49
  //#region ../otel-utils/dist/service-version.mjs
7
50
  var ServiceVersionDetector = class {
8
51
  /**
@@ -327,7 +370,10 @@ var BuildMetadataResolver = class {
327
370
  //#endregion
328
371
  //#region src/core/release_injection.ts
329
372
  /**
330
- * Prepend a global assignment to every module importing the Browser SDK.
373
+ * Expose the release to the Browser SDK. Builds with an HTML page write it into a
374
+ * `<meta name="monocle-build">` tag: JavaScript chunks then stay byte-identical between builds of
375
+ * the same code, so their hashes and debug IDs do too. Other builds (SSR, library, dev server)
376
+ * prepend a global assignment to every module importing the Browser SDK.
331
377
  *
332
378
  * Import declarations are hoisted, so the SDK module is evaluated first, but it only reads the
333
379
  * global when `initMonocle()` runs, which always happens after the importing module starts
@@ -339,6 +385,10 @@ var ReleaseInjection = class {
339
385
  * Read by `@monocle.sh/browser-sdk`. Keep both sides in sync.
340
386
  */
341
387
  static globalName = "__MONOCLE_BUILD__";
388
+ /**
389
+ * Name of the `<meta>` tag the Browser SDK reads when the global is not set.
390
+ */
391
+ static metaName = "monocle-build";
342
392
  static sdkImport = /["']@monocle\.sh\/browser-sdk(?:\/[^"'\s]*)?["']/;
343
393
  /**
344
394
  * Stylesheets, markup and data can mention the package without being JavaScript. Markup is
@@ -353,6 +403,15 @@ var ReleaseInjection = class {
353
403
  static snippet(metadata) {
354
404
  return `globalThis.${this.globalName}=${JSON.stringify(metadata)};`;
355
405
  }
406
+ /**
407
+ * URI-encoded JSON: without quotes, it survives Vite 5, which does not escape attributes.
408
+ */
409
+ static meta(metadata) {
410
+ return {
411
+ name: this.metaName,
412
+ content: encodeURIComponent(JSON.stringify(metadata))
413
+ };
414
+ }
356
415
  static shouldInject(options) {
357
416
  return !this.nonScriptIds.test(options.id) && this.sdkImport.test(options.code);
358
417
  }
@@ -409,6 +468,13 @@ var MonocleBuild = class {
409
468
  this.#log(value);
410
469
  return value;
411
470
  }
471
+ /**
472
+ * The release for the `<meta>` tag of an HTML page, when injection is on.
473
+ */
474
+ pageMetadata() {
475
+ if (!this.enabled || this.#options.options.inject === false) return;
476
+ return this.resolve();
477
+ }
412
478
  transform(options) {
413
479
  if (!this.enabled || this.#options.options.inject === false) return;
414
480
  if (!ReleaseInjection.shouldInject(options)) return;
@@ -529,12 +595,47 @@ var SourcemapsStep = class {
529
595
  /**
530
596
  * Translate Vite hooks to the bundler-agnostic Monocle build.
531
597
  */
532
- var MonocleVitePlugin = class {
598
+ var MonocleVitePlugin = class MonocleVitePlugin {
599
+ /**
600
+ * Client builds whose every entry is an HTML page, the only ones where the release can live
601
+ * in the page instead of the chunks. Vite builds `index.html` without an explicit input.
602
+ * Frameworks rendering the HTML themselves set another input, per environment since Vite 6.
603
+ */
604
+ static #htmlBuild(config) {
605
+ const { build } = config;
606
+ if (config.consumer === "server" || build.ssr || build.lib) return false;
607
+ const input = (build.rolldownOptions ?? build.rollupOptions)?.input;
608
+ if (input === void 0) return true;
609
+ const entries = typeof input === "string" ? [input] : Object.values(input);
610
+ return entries.length > 0 && entries.every((entry) => /\.html?$/.test(entry));
611
+ }
533
612
  static create(options = {}) {
534
613
  const plugin = { name: "@monocle.sh/vite-plugin" };
535
614
  if (options.enabled === false) return plugin;
536
615
  let build;
537
616
  let resolved;
617
+ let sdkReadsPage;
618
+ /**
619
+ * Whether the release can go into the HTML pages of this build: a build with injection on,
620
+ * for an installed Browser SDK reading the page. Detected once, with a warning for older
621
+ * SDKs. Without the SDK, nothing reads a page tag: none is written.
622
+ */
623
+ function pageSupported() {
624
+ if (!resolved || resolved.command !== "build" || options.inject === false) return false;
625
+ if (sdkReadsPage === void 0) {
626
+ const source = BrowserSdk.releaseSource(resolved.root);
627
+ sdkReadsPage = source === "page";
628
+ if (source === "bundle") resolved.logger.warn(`[monocle] @monocle.sh/browser-sdk < ${BrowserSdk.pageReleaseSince} reads the release from the bundle only, so every build changes all chunk hashes. Upgrade it to keep unchanged chunks identical.`);
629
+ }
630
+ return sdkReadsPage;
631
+ }
632
+ /**
633
+ * Whether modules of this environment leave the release to its HTML page. Vite 5 has no
634
+ * environments: the top-level configuration applies.
635
+ */
636
+ function releaseInPage(environment) {
637
+ return !!resolved && MonocleVitePlugin.#htmlBuild(environment?.config ?? resolved) && pageSupported();
638
+ }
538
639
  const sourcemaps = new SourcemapsStep({
539
640
  options,
540
641
  env: process.env
@@ -576,6 +677,7 @@ var MonocleVitePlugin = class {
576
677
  code: ReleaseInjection.sdkImport
577
678
  },
578
679
  handler(code, id) {
680
+ if (releaseInPage(this.environment)) return;
579
681
  return build?.transform({
580
682
  code,
581
683
  id
@@ -583,6 +685,20 @@ var MonocleVitePlugin = class {
583
685
  }
584
686
  },
585
687
  /**
688
+ * Builds only call it for HTML entries. When some modules still got the global (mixed
689
+ * HTML and script entries), the SDK prefers the global, so the tag is harmless.
690
+ */
691
+ transformIndexHtml() {
692
+ if (!pageSupported()) return;
693
+ const metadata = build?.pageMetadata();
694
+ if (!metadata) return;
695
+ return [{
696
+ tag: "meta",
697
+ attrs: ReleaseInjection.meta(metadata),
698
+ injectTo: "head-prepend"
699
+ }];
700
+ },
701
+ /**
586
702
  * Sequential and last, so compression or copy plugins see the injected files.
587
703
  */
588
704
  writeBundle: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monocle.sh/vite-plugin",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Vite plugin that injects your release into the Monocle Browser SDK and uploads source maps at build time",
5
5
  "keywords": [
6
6
  "monocle",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "magic-string": "^0.30.21",
42
- "@monocle.sh/sourcemaps": "^0.1.0"
42
+ "@monocle.sh/sourcemaps": "^0.3.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@playwright/test": "^1.63.0",
@@ -49,8 +49,8 @@
49
49
  "tsdown": "^0.21.10",
50
50
  "vite": "^8.3.4",
51
51
  "vue": "^3.5.44",
52
- "@monocle.sh/browser-sdk": "0.12.0",
53
- "@monocle.sh/otel-utils": "1.9.0"
52
+ "@monocle.sh/browser-sdk": "0.13.1",
53
+ "@monocle.sh/otel-utils": "1.10.0"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "vite": ">=5.0.0"