From 7866025bd867824c186598840bd0480edafcecfa Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:32:01 -0700 Subject: [PATCH 1/2] fix(#2736): canonicalize PURE annotations in dist-es5 (module) output Modern bundlers (Rolldown / Vite 8) prefer the package "module" entry, which resolves to the per-module dist-es5 tsc output. TypeScript emits parenthesized PURE annotations with whitespace after the opening paren (e.g. `( /*#__PURE__*/"http.")`), which Rolldown rejects with [INVALID_ANNOTATION]. The existing rollup fixPureAnnotations() pass only covers the bundled dist/es5 ("main") output, so the ESM consumer path stayed broken (#2737 only fixed the bundle). Changes: - Add shared tools/pureAnnotations.js (single source of truth for the canonicalization regex), reused by both rollup.base.config.js and the new grunt task. - Add "fix-pure" grunt multitask that canonicalizes dist-es5/**/*.js after the tsc compile; wired into the shared tsBuildActions pipeline for all packages. - Extract shared getDistPackageRoots() in gruntfile.js (reused by validate-es5 and fix-pure). - Keep the wrapping parens (required for older Rollup/Webpack/Terser to tree-shake the constants), per maintainer guidance. - Extend AISKU/AppInsightsCore size tests to assert canonical PURE form in the dist-es5 module output. - Document the fix in RELEASES.md (3.4.2). --- AISKU/Tests/Unit/src/AISKUSize.Tests.ts | 22 ++++-- RELEASES.md | 3 + gruntfile.js | 42 ++++++++++- rollup.base.config.js | 5 +- .../Unit/src/ai/AppInsightsCoreSize.Tests.ts | 22 ++++-- tools/grunt-tasks/fixPureAnnotations.js | 74 +++++++++++++++++++ tools/pureAnnotations.js | 38 ++++++++++ 7 files changed, 185 insertions(+), 21 deletions(-) create mode 100644 tools/grunt-tasks/fixPureAnnotations.js create mode 100644 tools/pureAnnotations.js diff --git a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts index 2af11a802..8200915d8 100644 --- a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts +++ b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts @@ -108,26 +108,34 @@ export class AISKUSizeCheck extends AITestClass { } private addRolldownPureAnnotationCheck(): void { + // The rollup-bundled dist (the package "main" entry) + this._checkPureAnnotations(this.rawFilePath, "AISKU dist"); + // The per-module dist-es5 tsc output (the package "module" entry that + // modern bundlers such as Rolldown / Vite 8 import). See issue #2736. + this._checkPureAnnotations("../dist-es5/internal/trace/spanUtils.js", "AISKU dist-es5 (module)"); + } + + private _checkPureAnnotations(filePath: string, label: string): void { this.testCase({ - name: "Test AISKU dist canonicalizes PURE annotation spacing", + name: `Test ${label} canonicalizes PURE annotation spacing`, test: () => { - let request = new Request(this.rawFilePath, { method: "GET" }); + let request = new Request(filePath, { method: "GET" }); return fetch(request).then((response) => { if (!response.ok) { - Assert.ok(false, `fetch AISKU dist for PURE annotation check error: ${response.statusText}`); + Assert.ok(false, `fetch ${label} for PURE annotation check error: ${response.statusText}`); return; } return response.text().then((text) => { - // Validate the final bundle no longer contains spaced PURE comment forms. + // Validate the output no longer contains spaced PURE comment forms. let nonCanonicalPurePattern = /\(\s+\/\*\s*[#@]__PURE__\s*\*\/|\(\s*\/\*\s*[#@]__PURE__\s*\*\/\s+/g; let matches = text.match(nonCanonicalPurePattern) || []; - Assert.equal(0, matches.length, `Found ${matches.length} non-canonical PURE annotations in AISKU dist`); + Assert.equal(0, matches.length, `Found ${matches.length} non-canonical PURE annotations in ${label}`); }, (error: Error) => { - Assert.ok(false, `AISKU dist PURE annotation check response error: ${error}`); + Assert.ok(false, `${label} PURE annotation check response error: ${error}`); }); }).catch((error: Error) => { - Assert.ok(false, `AISKU dist PURE annotation check error: ${error}`); + Assert.ok(false, `${label} PURE annotation check error: ${error}`); }); } }); diff --git a/RELEASES.md b/RELEASES.md index 42a28f416..65f931d4e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -20,6 +20,8 @@ This is a maintenance release for the 3.4.x version line containing security har - **Offline Channel Reliability**: Fixed a missing `return` after `reject()` in the offline channel that could lead to a null provider dereference. +- **Fixed `[INVALID_ANNOTATION]` warnings in Rolldown / Vite 8 consumers** ([#2736](https://github.com/microsoft/ApplicationInsights-JS/issues/2736)): The per-module `dist-es5` output (the package `module` entry that modern bundlers import) emitted parenthesized PURE tree-shaking annotations with whitespace after the opening parenthesis (e.g. `( /*#__PURE__*/"http.")`), which stricter bundlers such as Rolldown (Vite 8) rejected. The build now canonicalizes these annotations to the flush form (`(/*#__PURE__*/"http.")`) in the `dist-es5` output, accepted by all bundlers while preserving the wrapping parentheses required for older Rollup / Webpack / Terser to tree-shake the constants. This complements #2737, which only normalized the rollup-bundled `dist/es5` (`main`) output. + ### CI / Tooling - **Dropped Node.js 16 from CI matrix**: Node.js 16 is End-of-Life and several dependencies (e.g. `puppeteer`, `@pnpm/error`) now require Node.js 18 or later. The CI pipeline no longer runs against Node.js 16. @@ -31,6 +33,7 @@ This is a maintenance release for the 3.4.x version line containing security har - #2733 fix: Migrate from npm to pnpm and resolve all dependency vulnerabilities - #2742 fix(ci): repair Node.js CI (Chrome install, bundle-size limits, ts-async offline-channel hang) - #2737 fix: remove invalid PURE literal annotations and add bundle validation tests +- #2736 fix: canonicalize PURE annotations in dist-es5 (module) output to fix Rolldown/Vite 8 [INVALID_ANNOTATION] warnings - #2735 fix: prevent prototype pollution in extend() and objExtend() via unsafe key filtering - #2734 fix(offline-channel): Add missing return after reject() to prevent null provider dereference - #2732 fix(OsPlugin): use correct CS 4.0 field names ext.os.name and ext.os.ver diff --git a/gruntfile.js b/gruntfile.js index 3a0189176..31cd94d96 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -723,6 +723,12 @@ module.exports = function (grunt) { actions.push("eslint-ts:" + name + "-lint"); } + // Canonicalize PURE annotations in the dist-es5 (module) output + var fixPureConfig = grunt.config.get("fix-pure") || {}; + if (fixPureConfig[name]) { + actions.push("fix-pure:" + name); + } + // Validate ES5 compatibility of browser bundles var es5Config = grunt.config.get("validate-es5") || {}; if (es5Config[name]) { @@ -857,12 +863,15 @@ module.exports = function (grunt) { ] } }, - "validate-es5": validateEs5Config() + "validate-es5": validateEs5Config(), + "fix-pure": fixPureConfig() })); - function validateEs5Config() { - var cfg = {}; - var packages = { + // Shared package root map used by the dist validation / post-processing + // tasks (validate-es5, fix-pure). Keyed by the same module name passed + // to tsBuildActions(). + function getDistPackageRoots() { + return { "core": "./shared/AppInsightsCore", "common": "./shared/AppInsightsCommon", "1dsCore": "./shared/1ds-core-js", @@ -881,6 +890,11 @@ module.exports = function (grunt) { "perfmarkmeasure": "./extensions/applicationinsights-perfmarkmeasure-js", "shims": "./tools/shims" }; + } + + function validateEs5Config() { + var cfg = {}; + var packages = getDistPackageRoots(); Object.keys(packages).forEach(function(name) { cfg[name] = { @@ -895,6 +909,26 @@ module.exports = function (grunt) { return cfg; } + // Builds the "fix-pure" task config. Canonicalizes PURE tree-shaking + // annotations in the per-module dist-es5 tsc output (the package + // "module" entry that npm consumers import). The rollup pass already + // covers the bundled dist/es5 (the "main" entry); this closes the gap + // for the un-bundled ESM output. See issue #2736. + function fixPureConfig() { + var cfg = {}; + var packages = getDistPackageRoots(); + + Object.keys(packages).forEach(function(name) { + cfg[name] = { + src: [ + packages[name] + "/dist-es5/**/*.js" + ] + }; + }); + + return cfg; + } + // Additional setup for lint-fix task function getLintFixTasks() { var nodeMajorVer = parseInt(process.version.substring(1)); diff --git a/rollup.base.config.js b/rollup.base.config.js index dd6eec9e4..87808043f 100644 --- a/rollup.base.config.js +++ b/rollup.base.config.js @@ -8,6 +8,7 @@ import dynamicRemove from "@microsoft/dynamicproto-js/tools/rollup/dist/node/rem import { es5Poly, es5Check, importCheck } from "@microsoft/applicationinsights-rollup-es5"; import { resolve } from 'path'; import { readFileSync } from "fs"; +const { canonicalizePureAnnotations } = require("./tools/pureAnnotations"); const rootVersion = require("./package.json").version; @@ -31,12 +32,10 @@ function doCleanup() { } function fixPureAnnotations() { - const PURE_COMMENT_CANONICALIZE = /\(\s*\/\*\s*([#@])__PURE__\s*\*\/\s*/g; - return { name: "fix-pure-annotations", renderChunk(code) { - let normalized = code.replace(PURE_COMMENT_CANONICALIZE, "(/*$1__PURE__*/"); + let normalized = canonicalizePureAnnotations(code); if (normalized === code) { return null; diff --git a/shared/AppInsightsCore/Tests/Unit/src/ai/AppInsightsCoreSize.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/ai/AppInsightsCoreSize.Tests.ts index 367df5462..862102b1b 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/ai/AppInsightsCoreSize.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/ai/AppInsightsCoreSize.Tests.ts @@ -83,26 +83,34 @@ export class AppInsightsCoreSizeCheck extends AITestClass { } private addRolldownPureAnnotationCheck(): void { + // The rollup-bundled dist (the package "main" entry) + this._checkPureAnnotations(this.rawFilePath, "AppInsightsCore dist"); + // The per-module dist-es5 tsc output (the package "module" entry that + // modern bundlers such as Rolldown / Vite 8 import). See issue #2736. + this._checkPureAnnotations("../dist-es5/ext/extUtils.js", "AppInsightsCore dist-es5 (module)"); + } + + private _checkPureAnnotations(filePath: string, label: string): void { this.testCase({ - name: "Test applicationinsights-core dist canonicalizes PURE annotation spacing", + name: `Test ${label} canonicalizes PURE annotation spacing`, test: () => { - let request = new Request(this.rawFilePath, { method: "GET" }); + let request = new Request(filePath, { method: "GET" }); return fetch(request).then((response) => { if (!response.ok) { - Assert.ok(false, `fetch applicationinsights-core dist for PURE annotation check error: ${response.statusText}`); + Assert.ok(false, `fetch ${label} for PURE annotation check error: ${response.statusText}`); return; } return response.text().then((text) => { - // Validate the final bundle no longer contains spaced PURE comment forms. + // Validate the output no longer contains spaced PURE comment forms. let nonCanonicalPurePattern = /\(\s+\/\*\s*[#@]__PURE__\s*\*\/|\(\s*\/\*\s*[#@]__PURE__\s*\*\/\s+/g; let matches = text.match(nonCanonicalPurePattern) || []; - Assert.equal(0, matches.length, `Found ${matches.length} non-canonical PURE annotations in AppInsightsCore dist`); + Assert.equal(0, matches.length, `Found ${matches.length} non-canonical PURE annotations in ${label}`); }, (error) => { - Assert.ok(false, `applicationinsights-core dist PURE annotation check response error: ${error}`); + Assert.ok(false, `${label} PURE annotation check response error: ${error}`); }); }).catch((error: Error) => { - Assert.ok(false, `applicationinsights-core dist PURE annotation check error: ${error}`); + Assert.ok(false, `${label} PURE annotation check error: ${error}`); }); } }); diff --git a/tools/grunt-tasks/fixPureAnnotations.js b/tools/grunt-tasks/fixPureAnnotations.js new file mode 100644 index 000000000..eec8f3167 --- /dev/null +++ b/tools/grunt-tasks/fixPureAnnotations.js @@ -0,0 +1,74 @@ +/** + * fixPureAnnotations.js - Grunt task to canonicalize `/*#__PURE__*\/` (and + * `/*#@__PURE__*\/`) tree-shaking annotations in the per-module `dist-es5` + * output that is emitted by the TypeScript compiler (the package `module` + * entry point that npm consumers import). + * + * Why this is needed: + * TypeScript emits parenthesized PURE annotations with whitespace after the + * opening parenthesis, e.g. `( /*#__PURE__*\/"http.")`. The wrapping + * parentheses are required so that older versions of Rollup / Webpack / + * Terser still tree-shake the constants, so they must NOT be removed. + * However, newer bundlers such as Rolldown (Vite 8) are stricter and reject + * the spaced form, emitting `[INVALID_ANNOTATION]` warnings. This task + * rewrites the spaced form back to the canonical, flush-against-the-paren + * form `(/*#__PURE__*\/...)` which is accepted by every bundler while still + * preserving the tree-shaking behaviour. + * + * The `rollup.base.config.js` `fixPureAnnotations()` plugin already performs + * this canonicalization for the rollup-bundled `dist/es5` / `browser` CDN + * outputs (the package `main` entry). This task closes the gap for the + * un-bundled `dist-es5` tsc output (the package `module` entry) which never + * passes through rollup. See issue #2736. + * + * Usage in gruntfile: + * grunt.loadTasks("./tools/grunt-tasks"); + * grunt.registerTask("mytask", ["fix-pure:mypackage"]); + * + * Config example: + * "fix-pure": { + * "mypackage": { + * src: ["dist-es5/**\/*.js"] + * } + * } + */ +module.exports = function (grunt) { + "use strict"; + + var canonicalizePureAnnotations = require("../pureAnnotations").canonicalizePureAnnotations; + + grunt.registerMultiTask("fix-pure", "Canonicalize PURE tree-shaking annotations in dist-es5 output", function () { + var files = this.filesSrc; + var filesChecked = 0; + var filesChanged = 0; + + if (!files || files.length === 0) { + grunt.log.warn("No files matched for PURE annotation canonicalization."); + return; + } + + files.forEach(function (filepath) { + if (!grunt.file.exists(filepath)) { + return; + } + + // Skip source map files - only the emitted JavaScript is rewritten. + if (filepath.indexOf(".map") !== -1) { + return; + } + + filesChecked++; + + var content = grunt.file.read(filepath); + var normalized = canonicalizePureAnnotations(content); + + if (normalized !== content) { + grunt.file.write(filepath, normalized); + filesChanged++; + } + }); + + grunt.log.ok("Canonicalized PURE annotations: checked " + filesChecked + " file(s), updated " + filesChanged + " file(s)."); + return true; + }); +}; diff --git a/tools/pureAnnotations.js b/tools/pureAnnotations.js new file mode 100644 index 000000000..5e181286a --- /dev/null +++ b/tools/pureAnnotations.js @@ -0,0 +1,38 @@ +/** + * pureAnnotations.js - Shared helpers for canonicalizing `/*#__PURE__*\/` + * (and `/*#@__PURE__*\/`) tree-shaking annotations. + * + * TypeScript emits parenthesized PURE annotations with whitespace after the + * opening parenthesis, e.g. `( /*#__PURE__*\/"http.")`. The wrapping + * parentheses are required so that older versions of Rollup / Webpack / Terser + * still tree-shake the constants, so they must NOT be removed. However, newer + * bundlers such as Rolldown (Vite 8) are stricter and reject the spaced form, + * emitting `[INVALID_ANNOTATION]` warnings. Canonicalizing to the flush form + * `(/*#__PURE__*\/...)` is accepted by every bundler while preserving the + * tree-shaking behaviour. + * + * This single source of truth is shared by: + * - rollup.base.config.js `fixPureAnnotations()` (rollup-bundled dist/es5) + * - tools/grunt-tasks/fixPureAnnotations.js `fix-pure` (tsc dist-es5) + */ +"use strict"; + +// Matches an opening parenthesis followed by a (possibly whitespace padded) +// PURE / @__PURE__ annotation, capturing the leading marker char (# or @). +var PURE_COMMENT_CANONICALIZE = /\(\s*\/\*\s*([#@])__PURE__\s*\*\/\s*/g; + +/** + * Rewrites any spaced PURE annotation forms in the supplied code to the + * canonical flush-against-the-paren form. Returns the (possibly unchanged) + * code string. + * @param {string} code + * @returns {string} + */ +function canonicalizePureAnnotations(code) { + return code.replace(PURE_COMMENT_CANONICALIZE, "(/*$1__PURE__*/"); +} + +module.exports = { + PURE_COMMENT_CANONICALIZE: PURE_COMMENT_CANONICALIZE, + canonicalizePureAnnotations: canonicalizePureAnnotations +}; From 48016034e0c02400aac2e4196321e323f1b9a2cb Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:44:40 -0700 Subject: [PATCH 2/2] fix: load shared PURE helper as ESM (.mjs) so rollup config bundling resolves it rollup's --bundleConfigAsCjs writes the bundled config into the subpackage dir and does not run the commonjs plugin, so a relative require()/CJS import of the shared helper failed to resolve. Convert the shared helper to an ES module and import it directly in rollup.base.config.js (rollup inlines it); the CommonJS grunt task now loads it via async dynamic import(). --- rollup.base.config.js | 2 +- tools/grunt-tasks/fixPureAnnotations.js | 52 +++++++++++-------- ...pureAnnotations.js => pureAnnotations.mjs} | 21 ++++---- 3 files changed, 41 insertions(+), 34 deletions(-) rename tools/{pureAnnotations.js => pureAnnotations.mjs} (70%) diff --git a/rollup.base.config.js b/rollup.base.config.js index 87808043f..d849b870d 100644 --- a/rollup.base.config.js +++ b/rollup.base.config.js @@ -8,7 +8,7 @@ import dynamicRemove from "@microsoft/dynamicproto-js/tools/rollup/dist/node/rem import { es5Poly, es5Check, importCheck } from "@microsoft/applicationinsights-rollup-es5"; import { resolve } from 'path'; import { readFileSync } from "fs"; -const { canonicalizePureAnnotations } = require("./tools/pureAnnotations"); +import { canonicalizePureAnnotations } from "./tools/pureAnnotations.mjs"; const rootVersion = require("./package.json").version; diff --git a/tools/grunt-tasks/fixPureAnnotations.js b/tools/grunt-tasks/fixPureAnnotations.js index eec8f3167..fb035953d 100644 --- a/tools/grunt-tasks/fixPureAnnotations.js +++ b/tools/grunt-tasks/fixPureAnnotations.js @@ -35,40 +35,48 @@ module.exports = function (grunt) { "use strict"; - var canonicalizePureAnnotations = require("../pureAnnotations").canonicalizePureAnnotations; - grunt.registerMultiTask("fix-pure", "Canonicalize PURE tree-shaking annotations in dist-es5 output", function () { var files = this.filesSrc; - var filesChecked = 0; - var filesChanged = 0; + var done = this.async(); if (!files || files.length === 0) { grunt.log.warn("No files matched for PURE annotation canonicalization."); + done(); return; } - files.forEach(function (filepath) { - if (!grunt.file.exists(filepath)) { - return; - } + // The shared helper is an ES module, so load it via dynamic import(). + import("../pureAnnotations.mjs").then(function (mod) { + var canonicalizePureAnnotations = mod.canonicalizePureAnnotations; + var filesChecked = 0; + var filesChanged = 0; - // Skip source map files - only the emitted JavaScript is rewritten. - if (filepath.indexOf(".map") !== -1) { - return; - } + files.forEach(function (filepath) { + if (!grunt.file.exists(filepath)) { + return; + } - filesChecked++; + // Skip source map files - only the emitted JavaScript is rewritten. + if (filepath.indexOf(".map") !== -1) { + return; + } - var content = grunt.file.read(filepath); - var normalized = canonicalizePureAnnotations(content); + filesChecked++; - if (normalized !== content) { - grunt.file.write(filepath, normalized); - filesChanged++; - } - }); + var content = grunt.file.read(filepath); + var normalized = canonicalizePureAnnotations(content); - grunt.log.ok("Canonicalized PURE annotations: checked " + filesChecked + " file(s), updated " + filesChanged + " file(s)."); - return true; + if (normalized !== content) { + grunt.file.write(filepath, normalized); + filesChanged++; + } + }); + + grunt.log.ok("Canonicalized PURE annotations: checked " + filesChecked + " file(s), updated " + filesChanged + " file(s)."); + done(); + }, function (err) { + grunt.log.error("Failed to load PURE annotation helper: " + err); + done(false); + }); }); }; diff --git a/tools/pureAnnotations.js b/tools/pureAnnotations.mjs similarity index 70% rename from tools/pureAnnotations.js rename to tools/pureAnnotations.mjs index 5e181286a..ed2562aab 100644 --- a/tools/pureAnnotations.js +++ b/tools/pureAnnotations.mjs @@ -1,5 +1,5 @@ /** - * pureAnnotations.js - Shared helpers for canonicalizing `/*#__PURE__*\/` + * pureAnnotations.mjs - Shared helpers for canonicalizing `/*#__PURE__*\/` * (and `/*#@__PURE__*\/`) tree-shaking annotations. * * TypeScript emits parenthesized PURE annotations with whitespace after the @@ -12,14 +12,18 @@ * tree-shaking behaviour. * * This single source of truth is shared by: - * - rollup.base.config.js `fixPureAnnotations()` (rollup-bundled dist/es5) - * - tools/grunt-tasks/fixPureAnnotations.js `fix-pure` (tsc dist-es5) + * - rollup.base.config.js `fixPureAnnotations()` (rollup-bundled dist/es5), + * which imports it directly (rollup inlines it when bundling the config). + * - tools/grunt-tasks/fixPureAnnotations.js `fix-pure` (tsc dist-es5), which + * loads it via dynamic import() from the CommonJS grunt task. + * + * It is authored as an ES module (.mjs) so the rollup config bundler (which + * does not run the commonjs plugin) can consume the named exports. */ -"use strict"; // Matches an opening parenthesis followed by a (possibly whitespace padded) // PURE / @__PURE__ annotation, capturing the leading marker char (# or @). -var PURE_COMMENT_CANONICALIZE = /\(\s*\/\*\s*([#@])__PURE__\s*\*\/\s*/g; +export var PURE_COMMENT_CANONICALIZE = /\(\s*\/\*\s*([#@])__PURE__\s*\*\/\s*/g; /** * Rewrites any spaced PURE annotation forms in the supplied code to the @@ -28,11 +32,6 @@ var PURE_COMMENT_CANONICALIZE = /\(\s*\/\*\s*([#@])__PURE__\s*\*\/\s*/g; * @param {string} code * @returns {string} */ -function canonicalizePureAnnotations(code) { +export function canonicalizePureAnnotations(code) { return code.replace(PURE_COMMENT_CANONICALIZE, "(/*$1__PURE__*/"); } - -module.exports = { - PURE_COMMENT_CANONICALIZE: PURE_COMMENT_CANONICALIZE, - canonicalizePureAnnotations: canonicalizePureAnnotations -};