diff --git a/src/syntax.ts b/src/syntax.ts index cc3769f..fc664c7 100644 --- a/src/syntax.ts +++ b/src/syntax.ts @@ -1,4 +1,5 @@ import { promises as fsp } from "node:fs"; +import { parse } from "acorn"; import { extname } from "pathe"; import { readPackageJSON } from "pkg-types"; import { ResolveOptions, resolvePath } from "./resolve"; @@ -10,8 +11,6 @@ const ESM_RE = const CJS_RE = /([\s;]|^)(module.exports\b|exports\.\w|require\s*\(|global\.\w)/m; -const COMMENT_RE = /\/\*.+?\*\/|\/\/.*(?=[nr])/g; - const BUILTIN_EXTENSIONS = new Set([".mjs", ".cjs", ".node", ".wasm"]); /** @@ -25,6 +24,38 @@ export type DetectSyntaxOptions = { stripComments?: boolean; }; +interface TokenLocation { + start: number; + end: number; +} + +function _getCommentLocations(code: string) { + const locations: TokenLocation[] = []; + parse(code, { + ecmaVersion: "latest", + allowHashBang: true, + allowAwaitOutsideFunction: true, + allowImportExportEverywhere: true, + onComment(_isBlock, _text, start, end) { + locations.push({ start, end }); + }, + }); + return locations; +} + +/** + * Strip comments from a string of source code + * + * @param code - The source code to remove comments from. + */ +export function stripComments(code: string) { + const locations = _getCommentLocations(code); + for (const location of locations.reverse()) { + code = code.slice(0, location.start) + code.slice(location.end); + } + return code; +} + /** * Determines if a given code string contains ECMAScript module syntax. * @@ -37,7 +68,7 @@ export function hasESMSyntax( opts: DetectSyntaxOptions = {}, ): boolean { if (opts.stripComments) { - code = code.replace(COMMENT_RE, ""); + code = stripComments(code); } return ESM_RE.test(code); } @@ -54,7 +85,7 @@ export function hasCJSSyntax( opts: DetectSyntaxOptions = {}, ): boolean { if (opts.stripComments) { - code = code.replace(COMMENT_RE, ""); + code = stripComments(code); } return CJS_RE.test(code); } @@ -68,7 +99,7 @@ export function hasCJSSyntax( */ export function detectSyntax(code: string, opts: DetectSyntaxOptions = {}) { if (opts.stripComments) { - code = code.replace(COMMENT_RE, ""); + code = stripComments(code); } // We strip comments once hence not passing opts down to hasESMSyntax and hasCJSSyntax const hasESM = hasESMSyntax(code, {}); diff --git a/test/syntax.test.ts b/test/syntax.test.ts index 785cb07..2a5130d 100644 --- a/test/syntax.test.ts +++ b/test/syntax.test.ts @@ -1,6 +1,6 @@ import { join } from "pathe"; import { describe, it, expect } from "vitest"; -import { detectSyntax, isValidNodeImport } from "../src"; +import { detectSyntax, isValidNodeImport, stripComments } from "../src"; const staticTests = { // ESM @@ -92,6 +92,21 @@ const staticTests = { const staticTestsWithComments = { '// They\'re exposed using "export import" so that types are passed along as expected\nmodule.exports={};': { hasESM: false, hasCJS: true, isMixed: false }, + "/* export * */": { hasESM: false, hasCJS: false, isMixed: false }, + "/* \n export * \n */": { hasESM: false, hasCJS: false, isMixed: false }, + "/* \n export * \n */ export * from 'foo' /* \n export * \n */": { + hasESM: true, + hasCJS: false, + isMixed: false, + }, +}; + +const commentStrippingTests = { + '// They\'re exposed using "export import" so that types are passed along as expected\nmodule.exports={};': + "\nmodule.exports={};", + "/* export * */": "", + "/* \n export * \n */": "", + "/* \n export * \n */ export * from 'foo' /* \n export * \n */": ` export * from 'foo' `, }; describe("detectSyntax", () => { @@ -112,6 +127,14 @@ describe("detectSyntax (with comment)", () => { } }); +describe("stripComments", () => { + for (const [input, result] of Object.entries(commentStrippingTests)) { + it(input, () => { + expect(stripComments(input)).to.deep.equal(result); + }); + } +}); + const nodeImportTests = { "node:fs": true, fs: true,