Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions AISKU/Tests/Unit/src/AISKUSize.Tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
});
}
});
Expand Down
3 changes: 3 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
42 changes: 38 additions & 4 deletions gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down Expand Up @@ -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",
Expand All @@ -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] = {
Expand All @@ -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));
Expand Down
5 changes: 2 additions & 3 deletions rollup.base.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
import { canonicalizePureAnnotations } from "./tools/pureAnnotations.mjs";

const rootVersion = require("./package.json").version;

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
});
}
});
Expand Down
82 changes: 82 additions & 0 deletions tools/grunt-tasks/fixPureAnnotations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* 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";

grunt.registerMultiTask("fix-pure", "Canonicalize PURE tree-shaking annotations in dist-es5 output", function () {
var files = this.filesSrc;
var done = this.async();

if (!files || files.length === 0) {
grunt.log.warn("No files matched for PURE annotation canonicalization.");
done();
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;

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).");
done();
}, function (err) {
grunt.log.error("Failed to load PURE annotation helper: " + err);
done(false);
});
});
};
37 changes: 37 additions & 0 deletions tools/pureAnnotations.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* pureAnnotations.mjs - 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),
* 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.
*/

// Matches an opening parenthesis followed by a (possibly whitespace padded)
// PURE / @__PURE__ annotation, capturing the leading marker char (# or @).
export 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}
*/
export function canonicalizePureAnnotations(code) {
return code.replace(PURE_COMMENT_CANONICALIZE, "(/*$1__PURE__*/");
}
Loading