From 26dedb712f368faeef8210a95e5d1df46db366ee Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Sat, 5 Sep 2026 11:09:14 +0200 Subject: [PATCH] Add safe Live Debugger file extension filtering Co-authored-by: Cursor --- ...omatch-npm-4.0.2-bea121c197-7b74f860c4.zip | Bin 0 -> 7968 bytes LICENSES-3rdparty.csv | 1 + packages/plugins/live-debugger/README.md | 25 ++++- packages/plugins/live-debugger/package.json | 4 +- .../plugins/live-debugger/src/constants.ts | 11 ++ packages/plugins/live-debugger/src/filter.ts | 94 ++++++++++++++++++ .../plugins/live-debugger/src/index.test.ts | 82 ++++++++++++--- packages/plugins/live-debugger/src/index.ts | 25 ++--- packages/plugins/live-debugger/src/types.ts | 3 + .../live-debugger/src/validate.test.ts | 65 +++++++++++- .../plugins/live-debugger/src/validate.ts | 40 +++++++- .../published/esbuild-plugin/package.json | 1 + packages/published/rollup-plugin/package.json | 1 + packages/published/rspack-plugin/package.json | 1 + packages/published/vite-plugin/package.json | 1 + .../published/webpack-plugin/package.json | 1 + yarn.lock | 28 ++++-- 17 files changed, 336 insertions(+), 47 deletions(-) create mode 100644 .yarn/cache/@types-picomatch-npm-4.0.2-bea121c197-7b74f860c4.zip create mode 100644 packages/plugins/live-debugger/src/filter.ts diff --git a/.yarn/cache/@types-picomatch-npm-4.0.2-bea121c197-7b74f860c4.zip b/.yarn/cache/@types-picomatch-npm-4.0.2-bea121c197-7b74f860c4.zip new file mode 100644 index 0000000000000000000000000000000000000000..765c9064b36b96cf4808814cc492ab37333ef873 GIT binary patch literal 7968 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 vG5-tl_nPqU;rnO$&zax{Js default: `[/\.[jt]sx?$/]` +> default: `[]` (all paths) -Array of file patterns (strings or RegExp) to include for instrumentation. By default, all JavaScript and TypeScript files (`.js`, `.jsx`, `.ts`, `.tsx`) are included. +Array of file patterns to include for instrumentation. Strings are glob patterns resolved relative to the current working directory; regular expressions are matched directly against the file ID. Multiple patterns are alternatives, so a file is included when any pattern matches. + +The [`fileExtensions`](#livedebuggerfileextensions) filter is applied independently. For example, `include: ['src/**']` limits instrumentation to the `src` directory without accidentally including CSS or other non-JavaScript assets. ### liveDebugger.exclude @@ -146,6 +150,23 @@ Array of file patterns (strings or RegExp) to exclude from instrumentation. By d - Datadog browser SDK packages (`@datadog/browser-*`, when npm linked) - Datadog browser SDK source files (`browser-sdk/packages/`) +### liveDebugger.fileExtensions + +> default: `['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts']` + +Non-empty array of file extensions eligible for instrumentation. Extension matching is case-insensitive and is applied in addition to `include` and `exclude`. + +Use custom extensions for file IDs containing JavaScript produced by another transform: + +```ts +liveDebugger: { + include: ['src/**'], + fileExtensions: ['.js', '.ts', '.vue'], +} +``` + +Set `fileExtensions: 'all'` to disable the extension guard. This is intended for advanced configurations where `include` fully identifies instrumentable files. + ### liveDebugger.honorSkipComments > default: `true` diff --git a/packages/plugins/live-debugger/package.json b/packages/plugins/live-debugger/package.json index f145f16d5..486a1f10d 100644 --- a/packages/plugins/live-debugger/package.json +++ b/packages/plugins/live-debugger/package.json @@ -25,13 +25,15 @@ "dependencies": { "@dd/core": "workspace:*", "@jridgewell/remapping": "2.3.5", - "chalk": "2.3.1" + "chalk": "2.3.1", + "picomatch": "4.0.3" }, "devDependencies": { "@babel/parser": "7.24.5", "@babel/traverse": "7.24.5", "@babel/types": "7.24.5", "@jridgewell/trace-mapping": "0.3.31", + "@types/picomatch": "4.0.2", "magic-string": "0.30.21", "typescript": "5.4.3" }, diff --git a/packages/plugins/live-debugger/src/constants.ts b/packages/plugins/live-debugger/src/constants.ts index 137408d06..0c12cbbb3 100644 --- a/packages/plugins/live-debugger/src/constants.ts +++ b/packages/plugins/live-debugger/src/constants.ts @@ -9,3 +9,14 @@ export const PLUGIN_NAME: PluginName = 'datadog-live-debugger-plugin' as const; // Skip instrumentation comment export const SKIP_INSTRUMENTATION_COMMENT = '@dd-no-instrumentation'; + +export const DEFAULT_FILE_EXTENSIONS = [ + '.js', + '.jsx', + '.ts', + '.tsx', + '.mjs', + '.cjs', + '.mts', + '.cts', +] as const; diff --git a/packages/plugins/live-debugger/src/filter.ts b/packages/plugins/live-debugger/src/filter.ts new file mode 100644 index 000000000..4c2ac0c71 --- /dev/null +++ b/packages/plugins/live-debugger/src/filter.ts @@ -0,0 +1,94 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. + +import path from 'path'; +import picomatch from 'picomatch'; + +import type { FileExtensions, LiveDebuggerOptionsWithDefaults } from './types'; + +type Pattern = string | RegExp; +type Matcher = (id: string) => boolean; + +const BACKSLASH_PATTERN = /\\/g; +const WINDOWS_ABSOLUTE_PATH_PATTERN = /^(?:[A-Z]:[\\/]|\\\\)/i; +const ID_SUFFIX_PATTERN = /[?#]/; + +const normalizePath = (filePath: string): string => filePath.replace(BACKSLASH_PATTERN, '/'); + +const resolveGlob = (glob: string): string => { + if ( + glob.startsWith('**') || + path.isAbsolute(glob) || + WINDOWS_ABSOLUTE_PATH_PATTERN.test(glob) + ) { + return normalizePath(glob); + } + + const absoluteGlob = path.resolve(glob); + return normalizePath(absoluteGlob); +}; + +const createPatternMatcher = (pattern: Pattern): Matcher => { + if (pattern instanceof RegExp) { + return (id) => { + const normalizedId = normalizePath(id); + const matches = pattern.test(normalizedId); + pattern.lastIndex = 0; + return matches; + }; + } + + const glob = resolveGlob(pattern); + const matchesGlob = picomatch(glob, { dot: true }); + return (id) => { + const normalizedId = normalizePath(id); + return matchesGlob(normalizedId); + }; +}; + +const createPatternFilter = (include: Pattern[], exclude: Pattern[]): Matcher => { + const includeMatchers = include.map(createPatternMatcher); + const excludeMatchers = exclude.map(createPatternMatcher); + + return (id) => { + if (excludeMatchers.some((matches) => matches(id))) { + return false; + } + + return includeMatchers.length === 0 || includeMatchers.some((matches) => matches(id)); + }; +}; + +const createFileExtensionFilter = (fileExtensions: FileExtensions): Matcher => { + if (fileExtensions === 'all') { + return () => true; + } + + const normalizedExtensions = fileExtensions.map((extension) => extension.toLowerCase()); + return (id) => { + const [filePath] = id.split(ID_SUFFIX_PATTERN); + const normalizedFilePath = filePath.toLowerCase(); + return normalizedExtensions.some((extension) => normalizedFilePath.endsWith(extension)); + }; +}; + +export const createFileFilter = ( + options: Pick, +): Matcher => { + const matchesPatterns = createPatternFilter(options.include, options.exclude); + const matchesFileExtension = createFileExtensionFilter(options.fileExtensions); + + return (id) => matchesFileExtension(id) && matchesPatterns(id); +}; + +const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +export const createFileExtensionPattern = (fileExtensions: FileExtensions): RegExp | undefined => { + if (fileExtensions === 'all') { + return undefined; + } + + const escapedExtensions = fileExtensions.map(escapeRegExp); + return new RegExp(`(?:${escapedExtensions.join('|')})(?:[?#].*)?$`, 'i'); +}; diff --git a/packages/plugins/live-debugger/src/index.test.ts b/packages/plugins/live-debugger/src/index.test.ts index 2bbd7f5fa..f9d7c4d14 100644 --- a/packages/plugins/live-debugger/src/index.test.ts +++ b/packages/plugins/live-debugger/src/index.test.ts @@ -6,7 +6,7 @@ import { InjectPosition } from '@dd/core/types'; import { getContextMock, getGetPluginsArg } from '@dd/tests/_jest/helpers/mocks'; import type { UnpluginBuildContext, UnpluginContext } from 'unplugin'; -import { PLUGIN_NAME } from './constants'; +import { DEFAULT_FILE_EXTENSIONS, PLUGIN_NAME } from './constants'; import { getLiveDebuggerPlugin, getPlugins } from './index'; import { getRuntimeBootstrap } from './runtime-bootstrap'; import type { LiveDebuggerOptionsWithDefaults } from './types'; @@ -15,8 +15,9 @@ const makeOptions = ( overrides: Partial = {}, ): LiveDebuggerOptionsWithDefaults => ({ version: '1.0.0', - include: [/\.[jt]sx?$/], + include: [], exclude: [/\/node_modules\//], + fileExtensions: [...DEFAULT_FILE_EXTENSIONS], honorSkipComments: false, functionTypes: undefined, namedOnly: false, @@ -159,7 +160,7 @@ describe('getLiveDebuggerPlugin', () => { const handler = getHandler(makeOptions({ include: [/\.tsx?$/] })); const code = 'function f() { return 1; }'; - expect(handler(code, '/src/style.css')).toEqual({ code }); + expect(handler(code, '/src/utils.js')).toEqual({ code }); }); it('should process files matching an include pattern', () => { @@ -169,12 +170,63 @@ describe('getLiveDebuggerPlugin', () => { expect(handler(code, '/src/utils.ts').code).toContain('$dd_probes'); }); - it('should skip include filtering when include array is empty', () => { + it.each(DEFAULT_FILE_EXTENSIONS)( + 'should process the default %s file extension', + (extension) => { + const handler = getHandler(makeOptions({ include: [], exclude: [] })); + const code = 'function f() { return 1; }'; + + expect(handler(code, `/src/utils${extension}`).code).toContain('$dd_probes'); + }, + ); + + it('should apply the extension filter independently of a broad include glob', () => { + const handler = getHandler( + makeOptions({ + include: ['**/src/**'], + exclude: [], + }), + ); + const code = 'function f() { return 1; }'; + + expect(handler(code, '/project/src/styles.css')).toEqual({ code }); + expect(handler(code, '/project/src/utils.ts').code).toContain('$dd_probes'); + }); + + it('should process a custom file extension', () => { + const handler = getHandler( + makeOptions({ + include: ['**/src/**'], + exclude: [], + fileExtensions: ['.vue'], + }), + ); + const code = 'function f() { return 1; }'; + + expect(handler(code, '/project/src/component.vue').code).toContain('$dd_probes'); + expect(handler(code, '/project/src/utils.ts')).toEqual({ code }); + }); + + it('should process any file extension when configured with "all"', () => { + const handler = getHandler( + makeOptions({ + include: ['**/src/**'], + exclude: [], + fileExtensions: 'all', + }), + ); + const code = 'function f() { return 1; }'; + + expect(handler(code, '/project/src/anything.xyz').code).toContain('$dd_probes'); + }); + + it('should match file extensions case-insensitively before an ID query', () => { const handler = getHandler(makeOptions({ include: [], exclude: [] })); const code = 'function f() { return 1; }'; - // With no include patterns, all file types pass through - expect(handler(code, '/src/anything.xyz').code).toContain('$dd_probes'); + expect(handler(code, '/src/component.TSX?loader=transformed').code).toContain( + '$dd_probes', + ); }); it('should exclude files matching an exclude pattern even if included', () => { @@ -189,25 +241,27 @@ describe('getLiveDebuggerPlugin', () => { expect(handler(code, '/node_modules/dep/index.ts')).toEqual({ code }); }); - it('should support string include patterns', () => { - const handler = getHandler(makeOptions({ include: ['src/'], exclude: [] })); + it('should support glob include patterns', () => { + const handler = getHandler(makeOptions({ include: ['src/**'], exclude: [] })); const code = 'function f() { return 1; }'; + const projectRoot = process.cwd(); - expect(handler(code, '/project/src/utils.ts').code).toContain('$dd_probes'); - expect(handler(code, '/project/vendor/lib.ts')).toEqual({ code }); + expect(handler(code, `${projectRoot}/src/utils.ts`).code).toContain('$dd_probes'); + expect(handler(code, `${projectRoot}/vendor/lib.ts`)).toEqual({ code }); }); - it('should support string exclude patterns', () => { + it('should support glob exclude patterns', () => { const handler = getHandler( makeOptions({ include: [], - exclude: ['vendor/'], + exclude: ['vendor/**'], }), ); const code = 'function f() { return 1; }'; + const projectRoot = process.cwd(); - expect(handler(code, '/project/src/utils.ts').code).toContain('$dd_probes'); - expect(handler(code, '/project/vendor/lib.ts')).toEqual({ code }); + expect(handler(code, `${projectRoot}/src/utils.ts`).code).toContain('$dd_probes'); + expect(handler(code, `${projectRoot}/vendor/lib.ts`)).toEqual({ code }); }); }); diff --git a/packages/plugins/live-debugger/src/index.ts b/packages/plugins/live-debugger/src/index.ts index 88f90a3e4..cbdbe3f77 100644 --- a/packages/plugins/live-debugger/src/index.ts +++ b/packages/plugins/live-debugger/src/index.ts @@ -10,6 +10,7 @@ import type { SourceMap } from 'magic-string'; import type { SourceMapCompact, UnpluginBuildContext } from 'unplugin'; import { CONFIG_KEY, PLUGIN_NAME } from './constants'; +import { createFileExtensionPattern, createFileFilter } from './filter'; import { getRuntimeBootstrap } from './runtime-bootstrap'; import { transformCode } from './transform'; import type { LiveDebuggerOptions, LiveDebuggerOptionsWithDefaults } from './types'; @@ -29,6 +30,9 @@ export const getLiveDebuggerPlugin = ( context: GlobalContext, ): PluginOptions => { const log = context.getLogger(PLUGIN_NAME); + const shouldInstrumentFile = createFileFilter(pluginOptions); + const fileExtensionPattern = createFileExtensionPattern(pluginOptions.fileExtensions); + const nativeInclude = fileExtensionPattern ? [fileExtensionPattern] : pluginOptions.include; let instrumentedCount = 0; let failedCount = 0; @@ -48,29 +52,16 @@ export const getLiveDebuggerPlugin = ( transform: { filter: { id: { - include: pluginOptions.include, + include: nativeInclude, exclude: pluginOptions.exclude, }, }, handler(code, id) { - // Enforce include/exclude patterns at runtime because unplugin's + // Enforce all file filters at runtime because unplugin's // native filter is not applied in bundler child compilations // (e.g., web worker bundles in rspack/webpack). - if (pluginOptions.include.length > 0) { - const included = pluginOptions.include.some((pattern) => - typeof pattern === 'string' ? id.includes(pattern) : pattern.test(id), - ); - if (!included) { - return { code }; - } - } - - for (const pattern of pluginOptions.exclude) { - const excluded = - typeof pattern === 'string' ? id.includes(pattern) : pattern.test(id); - if (excluded) { - return { code }; - } + if (!shouldInstrumentFile(id)) { + return { code }; } if (totalFilesWithFunctions >= DD_LD_LIMIT) { diff --git a/packages/plugins/live-debugger/src/types.ts b/packages/plugins/live-debugger/src/types.ts index f6d69074a..f29315dfb 100644 --- a/packages/plugins/live-debugger/src/types.ts +++ b/packages/plugins/live-debugger/src/types.ts @@ -12,11 +12,13 @@ export const VALID_FUNCTION_KINDS = [ ] as const; export type FunctionKind = (typeof VALID_FUNCTION_KINDS)[number]; +export type FileExtensions = string[] | 'all'; export type LiveDebuggerOptions = { enable?: boolean; include?: (string | RegExp)[]; exclude?: (string | RegExp)[]; + fileExtensions?: FileExtensions; honorSkipComments?: boolean; functionTypes?: FunctionKind[]; namedOnly?: boolean; @@ -26,6 +28,7 @@ export type LiveDebuggerOptionsWithDefaults = { version: string | undefined; include: (string | RegExp)[]; exclude: (string | RegExp)[]; + fileExtensions: FileExtensions; honorSkipComments: boolean; functionTypes: FunctionKind[] | undefined; namedOnly: boolean; diff --git a/packages/plugins/live-debugger/src/validate.test.ts b/packages/plugins/live-debugger/src/validate.test.ts index 09a282a58..01ef6d4ac 100644 --- a/packages/plugins/live-debugger/src/validate.test.ts +++ b/packages/plugins/live-debugger/src/validate.test.ts @@ -5,7 +5,7 @@ import type { BuildMetadata, Options } from '@dd/core/types'; import { getMockLogger } from '@dd/tests/_jest/helpers/mocks'; -import { PLUGIN_NAME } from './constants'; +import { DEFAULT_FILE_EXTENSIONS, PLUGIN_NAME } from './constants'; import type { LiveDebuggerOptions, LiveDebuggerOptionsWithDefaults } from './types'; import { validateOptions } from './validate'; @@ -34,8 +34,9 @@ describe('validateOptions', () => { input: makeConfig(undefined), expected: { version: undefined, - include: [/\.[jt]sx?$/], + include: [], exclude: expect.arrayContaining([/\/node_modules\//]), + fileExtensions: [...DEFAULT_FILE_EXTENSIONS], honorSkipComments: true, functionTypes: undefined, namedOnly: false, @@ -46,8 +47,9 @@ describe('validateOptions', () => { input: makeConfig({}), expected: { version: undefined, - include: [/\.[jt]sx?$/], + include: [], exclude: expect.arrayContaining([/\/node_modules\//]), + fileExtensions: [...DEFAULT_FILE_EXTENSIONS], honorSkipComments: true, functionTypes: undefined, namedOnly: false, @@ -58,8 +60,9 @@ describe('validateOptions', () => { input: makeConfig({}, { version: '1.0.0' }), expected: { version: '1.0.0', - include: [/\.[jt]sx?$/], + include: [], exclude: expect.arrayContaining([/\/node_modules\//]), + fileExtensions: [...DEFAULT_FILE_EXTENSIONS], honorSkipComments: true, functionTypes: undefined, namedOnly: false, @@ -119,6 +122,21 @@ describe('validateOptions', () => { input: makeConfig({ exclude: [/node_modules/] }), expected: expect.objectContaining({ exclude: [/node_modules/] }), }, + { + description: 'accept custom file extensions', + input: makeConfig({ fileExtensions: ['.js', '.vue'] }), + expected: expect.objectContaining({ fileExtensions: ['.js', '.vue'] }), + }, + { + description: 'normalize custom file extensions to lowercase and remove duplicates', + input: makeConfig({ fileExtensions: ['.JS', '.Vue', '.js', '.VUE'] }), + expected: expect.objectContaining({ fileExtensions: ['.js', '.vue'] }), + }, + { + description: 'accept all file extensions', + input: makeConfig({ fileExtensions: 'all' }), + expected: expect.objectContaining({ fileExtensions: 'all' }), + }, { description: 'accept honorSkipComments as true', input: makeConfig({ honorSkipComments: true }), @@ -240,6 +258,43 @@ describe('validateOptions', () => { }); }); + describe('invalid fileExtensions', () => { + const cases = [ + { + description: 'reject fileExtensions when not an array or "all"', + input: makeInvalidConfig({ fileExtensions: '.ts' }), + errorPattern: /fileExtensions.*must be an array of strings or "all"/, + }, + { + description: 'reject an empty file extension array', + input: makeInvalidConfig({ fileExtensions: [] }), + errorPattern: /fileExtensions.*must contain at least one extension/, + }, + { + description: 'reject a non-string file extension', + input: makeInvalidConfig({ fileExtensions: ['.ts', 42] }), + errorPattern: /fileExtensions.*values must begin with/, + }, + { + description: 'reject a file extension without a leading dot', + input: makeInvalidConfig({ fileExtensions: ['ts'] }), + errorPattern: /fileExtensions.*values must begin with/, + }, + { + description: 'reject a file extension containing a query separator', + input: makeInvalidConfig({ fileExtensions: ['.vue?script'] }), + errorPattern: /fileExtensions.*values must begin with/, + }, + ]; + + test.each(cases)('should $description', ({ input, errorPattern }) => { + expect(() => validateOptions(input, mockLogger)).toThrow( + `Invalid configuration for ${PLUGIN_NAME}.`, + ); + expect(mockError).toHaveBeenCalledWith(expect.stringMatching(errorPattern)); + }); + }); + describe('invalid honorSkipComments', () => { const cases = [ { @@ -311,6 +366,7 @@ describe('validateOptions', () => { const input = makeInvalidConfig({ include: 'bad', exclude: 'bad', + fileExtensions: 'bad', honorSkipComments: 42, functionTypes: 'bad', namedOnly: 42, @@ -323,6 +379,7 @@ describe('validateOptions', () => { const errorMessage = mockError.mock.calls[0][0]; expect(errorMessage).toMatch(/include/); expect(errorMessage).toMatch(/exclude/); + expect(errorMessage).toMatch(/fileExtensions/); expect(errorMessage).toMatch(/honorSkipComments/); expect(errorMessage).toMatch(/functionTypes/); expect(errorMessage).toMatch(/namedOnly/); diff --git a/packages/plugins/live-debugger/src/validate.ts b/packages/plugins/live-debugger/src/validate.ts index eb8020792..a0024fe50 100644 --- a/packages/plugins/live-debugger/src/validate.ts +++ b/packages/plugins/live-debugger/src/validate.ts @@ -5,11 +5,17 @@ import type { Logger, Options } from '@dd/core/types'; import chalk from 'chalk'; -import { CONFIG_KEY, PLUGIN_NAME } from './constants'; +import { CONFIG_KEY, DEFAULT_FILE_EXTENSIONS, PLUGIN_NAME } from './constants'; import type { LiveDebuggerOptions, LiveDebuggerOptionsWithDefaults } from './types'; import { VALID_FUNCTION_KINDS } from './types'; const red = chalk.bold.red; +const INVALID_FILE_EXTENSION_PATTERN = /[\\/?#]/; + +const normalizeFileExtensions = (fileExtensions: readonly string[]): string[] => { + const lowercaseExtensions = fileExtensions.map((extension) => extension.toLowerCase()); + return [...new Set(lowercaseExtensions)]; +}; export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptionsWithDefaults => { const pluginConfig: LiveDebuggerOptions = config[CONFIG_KEY] || {}; @@ -44,6 +50,29 @@ export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptio } } + // Validate fileExtensions option + if (pluginConfig.fileExtensions !== undefined && pluginConfig.fileExtensions !== 'all') { + if (!Array.isArray(pluginConfig.fileExtensions)) { + errors.push(`${red('fileExtensions')} must be an array of strings or "all"`); + } else if (pluginConfig.fileExtensions.length === 0) { + errors.push(`${red('fileExtensions')} must contain at least one extension`); + } else { + for (const extension of pluginConfig.fileExtensions) { + if ( + typeof extension !== 'string' || + extension.length < 2 || + !extension.startsWith('.') || + INVALID_FILE_EXTENSION_PATTERN.test(extension) + ) { + errors.push( + `${red('fileExtensions')} values must begin with "." and contain no path or query separators`, + ); + break; + } + } + } + } + // Validate honorSkipComments option if ( pluginConfig.honorSkipComments !== undefined && @@ -79,10 +108,16 @@ export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptio throw new Error(`Invalid configuration for ${PLUGIN_NAME}.`); } + const configuredFileExtensions = pluginConfig.fileExtensions ?? DEFAULT_FILE_EXTENSIONS; + const fileExtensions = + configuredFileExtensions === 'all' + ? configuredFileExtensions + : normalizeFileExtensions(configuredFileExtensions); + // Build the final configuration with defaults return { version: metadataVersion, - include: pluginConfig.include || [/\.[jt]sx?$/], // .js, .jsx, .ts, .tsx + include: pluginConfig.include ?? [], exclude: pluginConfig.exclude || [ /\/node_modules\//, /\.min\.js$/, @@ -94,6 +129,7 @@ export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptio /@datadog\/browser-/, // Datadog browser SDK packages (when npm linked) /browser-sdk\/packages\//, // Datadog browser SDK source files ], + fileExtensions, honorSkipComments: pluginConfig.honorSkipComments ?? true, functionTypes: pluginConfig.functionTypes, namedOnly: pluginConfig.namedOnly ?? false, diff --git a/packages/published/esbuild-plugin/package.json b/packages/published/esbuild-plugin/package.json index 19d645369..309f40de1 100644 --- a/packages/published/esbuild-plugin/package.json +++ b/packages/published/esbuild-plugin/package.json @@ -62,6 +62,7 @@ "magic-string": "0.30.21", "outdent": "0.8.0", "p-queue": "6.6.2", + "picomatch": "4.0.3", "pretty-bytes": "5.6.0", "rollup": "4.45.1", "simple-git": "3.36.0", diff --git a/packages/published/rollup-plugin/package.json b/packages/published/rollup-plugin/package.json index db71afe0e..35c6a597c 100644 --- a/packages/published/rollup-plugin/package.json +++ b/packages/published/rollup-plugin/package.json @@ -65,6 +65,7 @@ "magic-string": "0.30.21", "outdent": "0.8.0", "p-queue": "6.6.2", + "picomatch": "4.0.3", "pretty-bytes": "5.6.0", "rollup": "4.45.1", "simple-git": "3.36.0", diff --git a/packages/published/rspack-plugin/package.json b/packages/published/rspack-plugin/package.json index d4e92ecba..b4e286404 100644 --- a/packages/published/rspack-plugin/package.json +++ b/packages/published/rspack-plugin/package.json @@ -62,6 +62,7 @@ "magic-string": "0.30.21", "outdent": "0.8.0", "p-queue": "6.6.2", + "picomatch": "4.0.3", "pretty-bytes": "5.6.0", "rollup": "4.45.1", "simple-git": "3.36.0", diff --git a/packages/published/vite-plugin/package.json b/packages/published/vite-plugin/package.json index 1149aae11..29c049967 100644 --- a/packages/published/vite-plugin/package.json +++ b/packages/published/vite-plugin/package.json @@ -62,6 +62,7 @@ "magic-string": "0.30.21", "outdent": "0.8.0", "p-queue": "6.6.2", + "picomatch": "4.0.3", "pretty-bytes": "5.6.0", "rollup": "4.45.1", "simple-git": "3.36.0", diff --git a/packages/published/webpack-plugin/package.json b/packages/published/webpack-plugin/package.json index 8faf9cb9a..cfe94ef31 100644 --- a/packages/published/webpack-plugin/package.json +++ b/packages/published/webpack-plugin/package.json @@ -62,6 +62,7 @@ "magic-string": "0.30.21", "outdent": "0.8.0", "p-queue": "6.6.2", + "picomatch": "4.0.3", "pretty-bytes": "5.6.0", "rollup": "4.45.1", "simple-git": "3.36.0", diff --git a/yarn.lock b/yarn.lock index 03495766f..7299a768d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1708,6 +1708,7 @@ __metadata: magic-string: "npm:0.30.21" outdent: "npm:0.8.0" p-queue: "npm:6.6.2" + picomatch: "npm:4.0.3" pretty-bytes: "npm:5.6.0" rollup: "npm:4.45.1" rollup-plugin-esbuild: "npm:6.1.1" @@ -1766,6 +1767,7 @@ __metadata: magic-string: "npm:0.30.21" outdent: "npm:0.8.0" p-queue: "npm:6.6.2" + picomatch: "npm:4.0.3" pretty-bytes: "npm:5.6.0" rollup: "npm:4.45.1" rollup-plugin-esbuild: "npm:6.1.1" @@ -1817,6 +1819,7 @@ __metadata: magic-string: "npm:0.30.21" outdent: "npm:0.8.0" p-queue: "npm:6.6.2" + picomatch: "npm:4.0.3" pretty-bytes: "npm:5.6.0" rollup: "npm:4.45.1" rollup-plugin-esbuild: "npm:6.1.1" @@ -1871,6 +1874,7 @@ __metadata: magic-string: "npm:0.30.21" outdent: "npm:0.8.0" p-queue: "npm:6.6.2" + picomatch: "npm:4.0.3" pretty-bytes: "npm:5.6.0" rollup: "npm:4.45.1" rollup-plugin-esbuild: "npm:6.1.1" @@ -1922,6 +1926,7 @@ __metadata: magic-string: "npm:0.30.21" outdent: "npm:0.8.0" p-queue: "npm:6.6.2" + picomatch: "npm:4.0.3" pretty-bytes: "npm:5.6.0" rollup: "npm:4.45.1" rollup-plugin-esbuild: "npm:6.1.1" @@ -2107,8 +2112,10 @@ __metadata: "@dd/core": "workspace:*" "@jridgewell/remapping": "npm:2.3.5" "@jridgewell/trace-mapping": "npm:0.3.31" + "@types/picomatch": "npm:4.0.2" chalk: "npm:2.3.1" magic-string: "npm:0.30.21" + picomatch: "npm:4.0.3" typescript: "npm:5.4.3" peerDependencies: "@babel/parser": ^7.24.5 @@ -4186,6 +4193,13 @@ __metadata: languageName: node linkType: hard +"@types/picomatch@npm:4.0.2": + version: 4.0.2 + resolution: "@types/picomatch@npm:4.0.2" + checksum: 10/7b74f860c4c2bf30f7952254717df2dd78441b0add79858dffe7f6a30c9dd6da6c3d7769d2a9f0c499bc5ffe820c629bffc7c5128948febfc2b935800169b1c3 + languageName: node + linkType: hard + "@types/resolve@npm:1.20.2": version: 1.20.2 resolution: "@types/resolve@npm:1.20.2" @@ -9502,6 +9516,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:4.0.3, picomatch@npm:^4.0.3": + version: 4.0.3 + resolution: "picomatch@npm:4.0.3" + checksum: 10/57b99055f40b16798f2802916d9c17e9744e620a0db136554af01d19598b96e45e2f00014c91d1b8b13874b80caa8c295b3d589a3f72373ec4aaf54baa5962d5 + languageName: node + linkType: hard + "picomatch@npm:^2.0.4, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" @@ -9516,13 +9537,6 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^4.0.3": - version: 4.0.3 - resolution: "picomatch@npm:4.0.3" - checksum: 10/57b99055f40b16798f2802916d9c17e9744e620a0db136554af01d19598b96e45e2f00014c91d1b8b13874b80caa8c295b3d589a3f72373ec4aaf54baa5962d5 - languageName: node - linkType: hard - "pirates@npm:^4.0.7": version: 4.0.7 resolution: "pirates@npm:4.0.7"