diff --git a/packages/cli/package.json b/packages/cli/package.json index 54361c106..eb367c130 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -51,6 +51,7 @@ "@metamask/snaps-utils": "^11.7.1", "@metamask/utils": "^11.9.0", "@types/node": "^22.13.1", + "acorn": "^8.15.0", "chokidar": "^4.0.1", "glob": "^11.0.0", "libp2p": "2.10.0", diff --git a/packages/cli/src/vite/strip-comments-plugin.test.ts b/packages/cli/src/vite/strip-comments-plugin.test.ts new file mode 100644 index 000000000..951f8d8eb --- /dev/null +++ b/packages/cli/src/vite/strip-comments-plugin.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; + +import { stripCommentsPlugin } from './strip-comments-plugin.ts'; + +describe('stripCommentsPlugin', () => { + const plugin = stripCommentsPlugin(); + const renderChunk = plugin.renderChunk as (code: string) => string | null; + + it.each([ + [ + 'single-line comment', + 'const x = 1; // comment\nconst y = 2;', + 'const x = 1; \nconst y = 2;', + ], + [ + 'multi-line comment', + 'const x = 1; /* comment */ const y = 2;', + 'const x = 1; const y = 2;', + ], + [ + 'multiple comments', + '/* a */ const x = 1; // b\n/* c */', + ' const x = 1; \n', + ], + [ + 'comment containing import()', + 'const x = 1; // import("module")\nconst y = 2;', + 'const x = 1; \nconst y = 2;', + ], + [ + 'comment with string content preserved', + 'const x = "// in string"; // real comment', + 'const x = "// in string"; ', + ], + ['code that is only a comment', '// just a comment', ''], + ])('removes %s', (_name, code, expected) => { + expect(renderChunk(code)).toBe(expected); + }); + + it.each([ + ['string with // pattern', 'const x = "// not a comment";'], + ['string with /* */ pattern', 'const x = "/* not a comment */";'], + ['regex literal like //', 'const re = /\\/\\//;'], + ['template literal with // pattern', 'const x = `// not a comment`;'], + ['nested quotes in string', 'const x = "a \\"// not comment\\" b";'], + ['no comments', 'const x = 1;'], + ['empty code', ''], + ])('returns null for %s', (_name, code) => { + expect(renderChunk(code)).toBeNull(); + }); +}); diff --git a/packages/cli/src/vite/strip-comments-plugin.ts b/packages/cli/src/vite/strip-comments-plugin.ts index b5d1d11e2..c8bd03b35 100644 --- a/packages/cli/src/vite/strip-comments-plugin.ts +++ b/packages/cli/src/vite/strip-comments-plugin.ts @@ -1,11 +1,13 @@ +import type { Comment } from 'acorn'; +import { parse } from 'acorn'; import type { Plugin } from 'rollup'; /** - * Rollup plugin that strips comments from bundled code. + * Rollup plugin that strips comments from bundled code using AST parsing. * * SES rejects code containing `import(` patterns, even when they appear - * in comments. This plugin removes all comments to avoid triggering - * that detection. + * in comments. This plugin uses Acorn to definitively identify comment nodes + * and removes them to avoid triggering that detection. * * Uses the `renderChunk` hook to process the final output. * @@ -15,58 +17,29 @@ export function stripCommentsPlugin(): Plugin { return { name: 'strip-comments', renderChunk(code) { - // Remove single-line comments (// ...) - // Remove multi-line comments (/* ... */) - // Be careful not to remove comments inside strings + const comments: Comment[] = []; + + parse(code, { + ecmaVersion: 'latest', + sourceType: 'module', + onComment: comments, + }); + + if (comments.length === 0) { + return null; + } + + // Build result by copying non-comment ranges. + // Comments are sorted by position since acorn parses linearly. let result = ''; - let i = 0; - while (i < code.length) { - const char = code[i] as string; - const nextChar = code[i + 1]; + let position = 0; - // Check for string literals - if (char === '"' || char === "'" || char === '`') { - const quote = char; - result += quote; - i += 1; - // Copy string content including escape sequences - while (i < code.length) { - const strChar = code[i] as string; - if (strChar === '\\' && i + 1 < code.length) { - result += strChar + (code[i + 1] as string); - i += 2; - } else if (strChar === quote) { - result += quote; - i += 1; - break; - } else { - result += strChar; - i += 1; - } - } - } - // Check for single-line comment - else if (char === '/' && nextChar === '/') { - // Skip until end of line - while (i < code.length && code[i] !== '\n') { - i += 1; - } - } - // Check for multi-line comment - else if (char === '/' && nextChar === '*') { - i += 2; - // Skip until */ - while (i < code.length && !(code[i - 1] === '*' && code[i] === '/')) { - i += 1; - } - i += 1; // Skip the closing / - } - // Regular character - else { - result += char; - i += 1; - } + for (const comment of comments) { + result += code.slice(position, comment.start); + position = comment.end; } + + result += code.slice(position); return result; }, }; diff --git a/yarn.lock b/yarn.lock index f10c3d297..10dae702a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3369,6 +3369,7 @@ __metadata: "@typescript-eslint/parser": "npm:^8.29.0" "@typescript-eslint/utils": "npm:^8.29.0" "@vitest/eslint-plugin": "npm:^1.6.5" + acorn: "npm:^8.15.0" chokidar: "npm:^4.0.1" depcheck: "npm:^1.4.7" eslint: "npm:^9.23.0"