diff --git a/.eslintrc.js b/.eslintrc.js index eff08d41f..04fe0f43c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -230,7 +230,8 @@ module.exports = { ], 'no-delete-var': 'error', 'no-label-var': 'error', - 'no-shadow': 'error', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': ['error'], 'no-shadow-restricted-names': 'error', 'no-undef': 'error', 'no-undef-init': 'error', diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 507327191..3232dfbb2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,18 +8,17 @@ jobs: fail-fast: false matrix: node: - - 18 - - 20 + - node-version-file: 'package.json' + - node-version: 20.x - name: Unit tests w/ Node.js ${{matrix.node}}.x + name: Unit tests w/ Node.js ${{matrix.node.node-version || matrix.node.node-version-file}} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Install Node ${{matrix.node}}.x + - name: Install Node ${{matrix.node.node-version || matrix.node.node-version-file}} uses: actions/setup-node@v4 - with: - node-version: ${{matrix.node}}.x + with: ${{matrix.node}} - run: yarn install - run: yarn build:all - run: yarn test @@ -33,7 +32,7 @@ jobs: - name: Install node uses: actions/setup-node@v4 with: - node-version: '18.19.0' + node-version-file: 'package.json' - run: yarn install - run: yarn build:all - run: yarn typecheck:all diff --git a/.node-version b/.node-version deleted file mode 100644 index a9d087399..000000000 --- a/.node-version +++ /dev/null @@ -1 +0,0 @@ -18.19.0 diff --git a/.vscode/settings.json b/.vscode/settings.json index be309b700..65d300429 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,5 +25,6 @@ "**/.yarn/": true, "**/.yarnrc.yml": true, "**/yarn.lock": true - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/package.json b/package.json index 366f83b69..fbb2c2344 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,10 @@ "workspaces": [ "packages/*", "packages/plugins/*", - "packages/published/*", - "packages/tests/src/_jest/fixtures/project" + "packages/published/*" ], "volta": { - "node": "18.19.0", + "node": "18.20.5", "yarn": "1.22.19" }, "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index f77ffaf15..abfafa166 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -24,12 +24,14 @@ }, "dependencies": { "async-retry": "1.3.3", - "chalk": "2.3.1" + "chalk": "2.3.1", + "glob": "11.0.0" }, "devDependencies": { "@types/async-retry": "1.4.8", "@types/chalk": "2.2.0", "@types/node": "^18", + "esbuild": "0.24.0", "typescript": "5.4.3", "unplugin": "1.16.0" } diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 4982a9c06..686abc9e9 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -7,10 +7,10 @@ export const INJECTED_FILE = '__datadog-helper-file'; export const ALL_BUNDLERS = ['webpack', 'vite', 'esbuild', 'rollup', 'rspack', 'rolldown', 'farm']; export const SUPPORTED_BUNDLERS = ['webpack', 'vite', 'esbuild', 'rollup', 'rspack'] as const; export const FULL_NAME_BUNDLERS = [ - 'webpack4', - 'webpack5', - 'vite', 'esbuild', 'rollup', 'rspack', + 'vite', + 'webpack4', + 'webpack5', ] as const; diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 582719656..3b0fcd1f2 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -4,12 +4,14 @@ import { INJECTED_FILE } from '@dd/core/constants'; import retry from 'async-retry'; +import type { PluginBuild } from 'esbuild'; import fsp from 'fs/promises'; import fs from 'fs'; +import { glob } from 'glob'; import path from 'path'; import type { RequestInit } from 'undici-types'; -import type { RequestOpts } from './types'; +import type { GlobalContext, Logger, RequestOpts, ResolvedEntry } from './types'; // Format a duration 0h 0m 0s 0ms export const formatDuration = (duration: number) => { @@ -25,12 +27,72 @@ export const formatDuration = (duration: number) => { }${milliseconds ? `${milliseconds}ms` : ''}`.trim(); }; -export const getResolvedPath = (filepath: string) => { - try { - return require.resolve(filepath); - } catch (e) { - return filepath; +// https://esbuild.github.io/api/#glob-style-entry-points +const getAllEntryFiles = (filepath: string): string[] => { + if (!filepath.includes('*')) { + return [filepath]; } + + const files = glob.sync(filepath); + return files; +}; + +// Parse, resolve and return all the entries of esbuild. +export const getEsbuildEntries = async ( + build: PluginBuild, + context: GlobalContext, + log: Logger, +): Promise => { + const entries: { name?: string; resolved: string; original: string }[] = []; + const entryPoints = build.initialOptions.entryPoints; + const entryPaths: { name?: string; path: string }[] = []; + const resolutionErrors: string[] = []; + + if (Array.isArray(entryPoints)) { + for (const entry of entryPoints) { + const fullPath = entry && typeof entry === 'object' ? entry.in : entry; + entryPaths.push({ path: fullPath }); + } + } else if (entryPoints && typeof entryPoints === 'object') { + entryPaths.push( + ...Object.entries(entryPoints).map(([name, filepath]) => ({ name, path: filepath })), + ); + } + + // Resolve all the paths. + const proms = entryPaths + .flatMap((entry) => + getAllEntryFiles(entry.path).map<[{ name?: string; path: string }, string]>((p) => [ + entry, + p, + ]), + ) + .map(async ([entry, p]) => { + const result = await build.resolve(p, { + kind: 'entry-point', + resolveDir: context.cwd, + }); + + if (result.errors.length) { + resolutionErrors.push(...result.errors.map((e) => e.text)); + } + + if (result.path) { + // Store them for later use. + entries.push({ + name: entry.name, + resolved: result.path, + original: entry.path, + }); + } + }); + + for (const resolutionError of resolutionErrors) { + log.error(resolutionError); + } + + await Promise.all(proms); + return entries; }; export const ERROR_CODES_NO_RETRY = [400, 403, 413]; @@ -169,3 +231,6 @@ export const readJsonSync = (filepath: string) => { const data = fs.readFileSync(filepath, { encoding: 'utf-8' }); return JSON.parse(data); }; + +let index = 0; +export const getUniqueId = () => `${Date.now()}.${performance.now()}.${++index}`; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index bdcf8a87f..55f553d4c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -74,7 +74,18 @@ export type BundlerReport = { version: string; }; -export type ToInjectItem = { type: 'file' | 'code'; value: string; fallback?: ToInjectItem }; +export type InjectedValue = string | (() => Promise); +export enum InjectPosition { + BEFORE, + MIDDLE, + AFTER, +} +export type ToInjectItem = { + type: 'file' | 'code'; + value: InjectedValue; + position?: InjectPosition; + fallback?: ToInjectItem; +}; export type GetLogger = (name: string) => Logger; export type Logger = { @@ -146,3 +157,5 @@ export type RequestOpts = { type?: 'json' | 'text'; onRetry?: (error: Error, attempt: number) => void; }; + +export type ResolvedEntry = { name?: string; resolved: string; original: string }; diff --git a/packages/factory/README.md b/packages/factory/README.md index 688963d77..2dbbef1f1 100644 --- a/packages/factory/README.md +++ b/packages/factory/README.md @@ -44,8 +44,12 @@ Most of the time they will interact via the global context. ### Injection -> This is used to prepend some code to the produced bundle.
-> Particularly useful if you want to share some global context, or to automatically inject some SDK. +> This is used to inject some code to the produced bundle.
+> Particularly useful : +> - to share some global context. +> - to automatically inject some SDK. +> - to initialise some global dependencies. +> - ... [📝 Full documentation ➡️](/packages/plugins/injection#readme) diff --git a/packages/factory/src/helpers.ts b/packages/factory/src/helpers.ts index 73c00828a..8d7bfdb38 100644 --- a/packages/factory/src/helpers.ts +++ b/packages/factory/src/helpers.ts @@ -2,6 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +import { getUniqueId } from '@dd/core/helpers'; import type { BuildReport, BundlerFullName, @@ -84,7 +85,7 @@ export const getContext = ({ options: OptionsWithDefaults; bundlerName: BundlerName; bundlerVersion: string; - injections: ToInjectItem[]; + injections: Map; version: FactoryMeta['version']; }): GlobalContext => { const cwd = process.cwd(); @@ -101,13 +102,15 @@ export const getContext = ({ name: bundlerName, fullName: `${bundlerName}${variant}` as BundlerFullName, variant, + // This will be updated in the bundler-report plugin once we have the configuration. outDir: cwd, version: bundlerVersion, }, build, + // This will be updated in the bundler-report plugin once we have the configuration. cwd, inject: (item: ToInjectItem) => { - injections.push(item); + injections.set(getUniqueId(), item); }, start: Date.now(), version, diff --git a/packages/factory/src/index.ts b/packages/factory/src/index.ts index 44dcd1668..bf633c12a 100644 --- a/packages/factory/src/index.ts +++ b/packages/factory/src/index.ts @@ -2,6 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +/* eslint-disable arca/import-ordering, arca/newline-after-import-section */ // This file is mostly generated. // Anything between // - #imports-injection-marker @@ -22,10 +23,10 @@ import type { } from '@dd/core/types'; import type { UnpluginContextMeta, UnpluginInstance, UnpluginOptions } from 'unplugin'; import { createUnplugin } from 'unplugin'; +import chalk from 'chalk'; import { getContext, getLoggerFactory, validateOptions } from './helpers'; -/* eslint-disable arca/import-ordering, arca/newline-after-import-section */ // #imports-injection-marker import type { OptionsWithErrorTracking } from '@dd/error-tracking-plugin/types'; import * as errorTracking from '@dd/error-tracking-plugin'; @@ -40,7 +41,6 @@ import { getInjectionPlugins } from '@dd/internal-injection-plugin'; export type { types as ErrorTrackingTypes } from '@dd/error-tracking-plugin'; export type { types as TelemetryTypes } from '@dd/telemetry-plugin'; // #types-export-injection-marker -/* eslint-enable arca/import-ordering, arca/newline-after-import-section */ export const helpers = { // Each product should have a unique entry. @@ -68,7 +68,7 @@ export const buildPluginFactory = ({ } // Create the global context. - const injections: ToInjectItem[] = []; + const injections: Map = new Map(); const context: GlobalContext = getContext({ options, bundlerVersion: bundler.version || bundler.VERSION, @@ -91,6 +91,7 @@ export const buildPluginFactory = ({ ...getGitPlugins(options, context), ...getInjectionPlugins( bundler, + options, context, injections, getLogger('datadog-injection-plugin'), @@ -136,6 +137,18 @@ export const buildPluginFactory = ({ // List all our plugins in the context. context.pluginNames.push(...plugins.map((plugin) => plugin.name)); + // Verify we don't have plugins with the same name, as they would override each other. + const duplicates = new Set( + context.pluginNames.filter( + (name) => context.pluginNames.filter((n) => n === name).length > 1, + ), + ); + if (duplicates.size > 0) { + throw new Error( + `Duplicate plugin names: ${chalk.bold.red(Array.from(duplicates).join(', '))}`, + ); + } + return plugins; }); }; diff --git a/packages/plugins/build-report/package.json b/packages/plugins/build-report/package.json index 77579e3a0..7aecd4e94 100644 --- a/packages/plugins/build-report/package.json +++ b/packages/plugins/build-report/package.json @@ -19,7 +19,6 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@dd/core": "workspace:*", - "glob": "11.0.0" + "@dd/core": "workspace:*" } } diff --git a/packages/plugins/build-report/src/esbuild.ts b/packages/plugins/build-report/src/esbuild.ts index 709b54665..624d06bd4 100644 --- a/packages/plugins/build-report/src/esbuild.ts +++ b/packages/plugins/build-report/src/esbuild.ts @@ -2,9 +2,16 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { getResolvedPath, isInjectionFile } from '@dd/core/helpers'; -import type { Logger, Entry, GlobalContext, Input, Output, PluginOptions } from '@dd/core/types'; -import { glob } from 'glob'; +import { getEsbuildEntries, isInjectionFile } from '@dd/core/helpers'; +import type { + Logger, + Entry, + GlobalContext, + Input, + Output, + PluginOptions, + ResolvedEntry, +} from '@dd/core/types'; import { cleanName, getAbsolutePath, getType } from './helpers'; @@ -17,56 +24,27 @@ const reIndexMeta = (obj: Record, cwd: string) => }), ); -// https://esbuild.github.io/api/#glob-style-entry-points -const getAllEntryFiles = (filepath: string, cwd: string): string[] => { - if (!filepath.includes('*')) { - return [filepath]; - } - - const files = glob.sync(filepath); - return files; -}; - -// Exported for testing purposes. -export const getEntryNames = ( - entrypoints: string[] | Record | { in: string; out: string }[] | undefined, - context: GlobalContext, -): Map => { - const entryNames = new Map(); - if (Array.isArray(entrypoints)) { - // We don't have an indexed object as entry, so we can't get an entry name from it. - for (const entry of entrypoints) { - const fullPath = entry && typeof entry === 'object' ? entry.in : entry; - const allFiles = getAllEntryFiles(fullPath, context.cwd); - for (const file of allFiles) { - // Using getResolvedPath because entries can be written with unresolved paths. - const cleanedName = cleanName(context, getResolvedPath(file)); - entryNames.set(cleanedName, cleanedName); - } - } - } else if (typeof entrypoints === 'object') { - const entryList = entrypoints ? Object.entries(entrypoints) : []; - for (const [entryName, entryPath] of entryList) { - const allFiles = getAllEntryFiles(entryPath, context.cwd); - for (const file of allFiles) { - const cleanedName = cleanName(context, getResolvedPath(file)); - entryNames.set(cleanedName, entryName); - } - } - } - return entryNames; -}; - export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOptions['esbuild'] => { return { setup(build) { - const cwd = context.cwd; - - // Store entry names based on the configuration. - const entrypoints = build.initialOptions.entryPoints; - const entryNames = getEntryNames(entrypoints, context); + const entryNames = new Map(); + const resolvedEntries: ResolvedEntry[] = []; + + build.onStart(async () => { + // Store entry names based on the configuration. + resolvedEntries.push(...(await getEsbuildEntries(build, context, log))); + for (const entry of resolvedEntries) { + const cleanedName = cleanName(context, entry.resolved); + if (entry.name) { + entryNames.set(cleanedName, entry.name); + } else { + entryNames.set(cleanedName, cleanedName); + } + } + }); build.onEnd((result) => { + const cwd = context.cwd; for (const error of result.errors) { context.build.errors.push(error.text); } diff --git a/packages/plugins/build-report/src/xpack.ts b/packages/plugins/build-report/src/xpack.ts index 62e6b8a63..e04d7ac30 100644 --- a/packages/plugins/build-report/src/xpack.ts +++ b/packages/plugins/build-report/src/xpack.ts @@ -2,6 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +import { isInjectionFile } from '@dd/core/helpers'; import type { Logger, Entry, @@ -45,13 +46,13 @@ export const getXpackPlugin = new Map(); const isModuleSupported = (moduleIdentifier?: string): boolean => { - // console.log('Module Identifier supported', moduleIdentifier); return ( // Ignore unidentified modules and runtimes. !!moduleIdentifier && !moduleIdentifier.startsWith('webpack/runtime') && !moduleIdentifier.includes('/webpack4/buildin/') && - !moduleIdentifier.startsWith('multi ') + !moduleIdentifier.startsWith('multi ') && + !isInjectionFile(moduleIdentifier) ); }; diff --git a/packages/plugins/bundler-report/src/index.ts b/packages/plugins/bundler-report/src/index.ts index 19c81fd08..04f34f7d4 100644 --- a/packages/plugins/bundler-report/src/index.ts +++ b/packages/plugins/bundler-report/src/index.ts @@ -7,20 +7,36 @@ import path from 'path'; export const PLUGIN_NAME = 'datadog-bundler-report-plugin'; -const rollupPlugin: (context: GlobalContext) => PluginOptions['rollup'] = (context) => ({ - options(options) { - context.bundler.rawConfig = options; - const outputOptions = (options as any).output; - if (outputOptions) { - context.bundler.outDir = outputOptions.dir; - } - }, - outputOptions(options) { - if (options.dir) { - context.bundler.outDir = options.dir; +// From a list of path, return the nearest common directory. +const getNearestCommonDirectory = (dirs: string[], cwd: string) => { + const splitPaths = dirs.map((dir) => { + const absolutePath = path.isAbsolute(dir) ? dir : path.resolve(cwd, dir); + return absolutePath.split(path.sep); + }); + + // Use the shortest length for faster results. + const minLength = Math.min(...splitPaths.map((parts) => parts.length)); + const commonParts = []; + + for (let i = 0; i < minLength; i++) { + // We use the first path as our basis. + const component = splitPaths[0][i]; + if (splitPaths.every((parts) => parts[i] === component)) { + commonParts.push(component); + } else { + break; } - }, -}); + } + + return commonParts.length > 0 ? commonParts.join(path.sep) : path.sep; +}; + +const handleCwd = (dirs: string[], context: GlobalContext) => { + const nearestDir = getNearestCommonDirectory(dirs, context.cwd); + if (nearestDir !== path.sep) { + context.cwd = nearestDir; + } +}; const xpackPlugin: (context: GlobalContext) => PluginOptions['webpack'] & PluginOptions['rspack'] = (context) => (compiler) => { @@ -29,34 +45,106 @@ const xpackPlugin: (context: GlobalContext) => PluginOptions['webpack'] & Plugin if (compiler.options.output?.path) { context.bundler.outDir = compiler.options.output.path; } + + if (compiler.options.context) { + context.cwd = compiler.options.context; + } }; // TODO: Add universal config report with list of plugins (names), loaders. -export const getBundlerReportPlugins = (globalContext: GlobalContext): PluginOptions[] => { +export const getBundlerReportPlugins = (context: GlobalContext): PluginOptions[] => { + const directories: Set = new Set(); + const handleOutputOptions = (outputOptions: any) => { + if (!outputOptions) { + return; + } + + if (outputOptions.dir) { + context.bundler.outDir = outputOptions.dir; + directories.add(outputOptions.dir); + } else if (outputOptions.file) { + context.bundler.outDir = path.dirname(outputOptions.file); + directories.add(outputOptions.dir); + } + + // Vite has the "root" option we're using. + if (context.bundler.name === 'vite') { + return; + } + + handleCwd(Array.from(directories), context); + }; + + const rollupPlugin: () => PluginOptions['rollup'] & PluginOptions['vite'] = () => { + return { + options(options) { + context.bundler.rawConfig = options; + if (options.input) { + if (Array.isArray(options.input)) { + for (const input of options.input) { + directories.add(path.dirname(input)); + } + } else if (typeof options.input === 'object') { + for (const input of Object.values(options.input)) { + directories.add(path.dirname(input)); + } + } else if (typeof options.input === 'string') { + directories.add(path.dirname(options.input)); + } else { + throw new Error('Invalid input type'); + } + } + + if ('output' in options) { + handleOutputOptions(options.output); + } + }, + outputOptions(options) { + handleOutputOptions(options); + }, + }; + }; + const bundlerReportPlugin: PluginOptions = { name: PLUGIN_NAME, enforce: 'pre', esbuild: { setup(build) { - globalContext.bundler.rawConfig = build.initialOptions; + context.bundler.rawConfig = build.initialOptions; if (build.initialOptions.outdir) { - globalContext.bundler.outDir = build.initialOptions.outdir; + context.bundler.outDir = build.initialOptions.outdir; } if (build.initialOptions.outfile) { - globalContext.bundler.outDir = path.dirname(build.initialOptions.outfile); + context.bundler.outDir = path.dirname(build.initialOptions.outfile); + } + + if (build.initialOptions.absWorkingDir) { + context.cwd = build.initialOptions.absWorkingDir; } // We force esbuild to produce its metafile. build.initialOptions.metafile = true; }, }, - webpack: xpackPlugin(globalContext), - rspack: xpackPlugin(globalContext), - // Vite and Rollup have the same API. - vite: rollupPlugin(globalContext), - rollup: rollupPlugin(globalContext), + webpack: xpackPlugin(context), + rspack: xpackPlugin(context), + // Vite and Rollup have (almost) the same API. + // They don't really support the CWD concept, + // so we have to compute it based on existing configurations. + // The basic idea is to compare input vs output and keep the common part of the paths. + vite: { + ...rollupPlugin(), + config(config) { + if (config.root) { + context.cwd = config.root; + } else { + handleCwd(Array.from(directories), context); + } + }, + }, + rollup: rollupPlugin(), }; return [bundlerReportPlugin]; diff --git a/packages/plugins/error-tracking/src/sourcemaps/payload.ts b/packages/plugins/error-tracking/src/sourcemaps/payload.ts index 7c83c5512..08b201389 100644 --- a/packages/plugins/error-tracking/src/sourcemaps/payload.ts +++ b/packages/plugins/error-tracking/src/sourcemaps/payload.ts @@ -66,6 +66,7 @@ export const prefixRepeat = (filePath: string, prefix: string): string => { let result = ''; for (let i = 0; i < prefixParts.length; i += 1) { + // TODO: Check compatibility with Windows paths. const partialPrefix = prefixParts.slice(-i).join('/'); if (normalizedPath.startsWith(partialPrefix)) { result = partialPrefix; diff --git a/packages/plugins/injection/README.md b/packages/plugins/injection/README.md index 555c4e653..67316b895 100644 --- a/packages/plugins/injection/README.md +++ b/packages/plugins/injection/README.md @@ -1,14 +1,26 @@ # Injection Plugin -This is used to prepend some code to the produced bundle.
-Particularly useful if you want to share some global context, or to automatically inject some SDK. +This is used to inject some code to the produced bundle.
+Particularly useful : +- to share some global context. +- to automatically inject some SDK. +- to initialise some global dependencies. +- ... It gives you access to the `context.inject()` function. All the injections will be resolved during the `buildStart` hook,
-so you'll have to have submitted your injection prior to that.
+so you'll have to "submit" your injection(s) prior to that.
Ideally, you'd submit it during your plugin's initialization. +There are three positions to inject content: + +- `InjectPosition.START`: Added at the very beginning of the bundle, outside any closure. +- `InjectPosition.MIDDLE`: Added at the begining of the entry file, within the context of the bundle. +- `InjectPosition.END`: Added at the very end of the bundle, outside any closure. + +There are three types of injection: + ## Distant file You can give it a distant file.
@@ -18,6 +30,7 @@ Be mindful that a 5s timeout is enforced. context.inject({ type: 'file', value: 'https://example.com/my_file.js', + position: InjectPosition.START, }); ``` @@ -31,6 +44,7 @@ Remember that the plugins are also bundled before distribution. context.inject({ type: 'file', value: path.resolve(__dirname, '../my_file.js'), + position: InjectPosition.END, }); ``` @@ -43,5 +57,6 @@ Be mindful that the code needs to be executable, or the plugins will crash. context.inject({ type: 'code', value: 'console.log("My un-invasive code");', + position: InjectPosition.MIDDLE, }); ``` diff --git a/packages/plugins/injection/src/constants.ts b/packages/plugins/injection/src/constants.ts index 0d4746d5f..72561fa79 100644 --- a/packages/plugins/injection/src/constants.ts +++ b/packages/plugins/injection/src/constants.ts @@ -2,6 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -export const PREPARATION_PLUGIN_NAME = 'datadog-injection-preparation-plugin'; export const PLUGIN_NAME = 'datadog-injection-plugin'; export const DISTANT_FILE_RX = /^https?:\/\//; +export const BEFORE_INJECTION = `// begin injection by Datadog build plugins`; +export const AFTER_INJECTION = `// end injection by Datadog build plugins`; diff --git a/packages/plugins/injection/src/esbuild.ts b/packages/plugins/injection/src/esbuild.ts new file mode 100644 index 000000000..2097cf388 --- /dev/null +++ b/packages/plugins/injection/src/esbuild.ts @@ -0,0 +1,122 @@ +// 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 { INJECTED_FILE } from '@dd/core/constants'; +import { getEsbuildEntries, getUniqueId, outputFile, rm } from '@dd/core/helpers'; +import type { Logger, PluginOptions, GlobalContext, ResolvedEntry } from '@dd/core/types'; +import { InjectPosition } from '@dd/core/types'; +import { getAbsolutePath } from '@dd/internal-build-report-plugin/helpers'; +import fsp from 'fs/promises'; +import path from 'path'; + +import { PLUGIN_NAME } from './constants'; +import { getContentToInject } from './helpers'; +import type { ContentsToInject } from './types'; + +export const getEsbuildPlugin = ( + log: Logger, + context: GlobalContext, + contentsToInject: ContentsToInject, +): PluginOptions['esbuild'] => ({ + setup(build) { + const { onStart, onLoad, onEnd, esbuild, initialOptions } = build; + const entries: ResolvedEntry[] = []; + const filePath = `${getUniqueId()}.${InjectPosition.MIDDLE}.${INJECTED_FILE}.js`; + const absoluteFilePath = path.resolve(context.bundler.outDir, filePath); + const injectionRx = new RegExp(`${filePath}$`); + + // InjectPosition.MIDDLE + // Inject the file in the build using the "inject" option. + // NOTE: This is made "safer" for sub-builds by actually creating the file. + initialOptions.inject = initialOptions.inject || []; + initialOptions.inject.push(absoluteFilePath); + + onStart(async () => { + // Get all the entry points for later reference. + entries.push(...(await getEsbuildEntries(build, context, log))); + + // Remove our injected file from the config, so we reduce our chances to leak our changes. + initialOptions.inject = + initialOptions.inject?.filter((file) => file !== absoluteFilePath) || []; + + try { + // Create the MIDDLE file because esbuild will crash if it doesn't exist. + // It seems to load entries outside of the onLoad hook once. + await outputFile(absoluteFilePath, ''); + } catch (e: any) { + log.error(`Could not create the files: ${e.message}`); + } + }); + + onLoad( + { + filter: injectionRx, + namespace: PLUGIN_NAME, + }, + async () => { + const content = getContentToInject(contentsToInject[InjectPosition.MIDDLE]); + + // Safe to delete the temp file now, the hook will take over. + await rm(absoluteFilePath); + + return { + // We can't use an empty string otherwise esbuild will crash. + contents: content || ' ', + // Resolve the imports from the project's root. + resolveDir: context.cwd, + loader: 'js', + }; + }, + ); + + // InjectPosition.START and InjectPosition.END + onEnd(async (result) => { + if (!result.metafile) { + log.warn('Missing metafile from build result.'); + return; + } + + const banner = getContentToInject(contentsToInject[InjectPosition.BEFORE]); + const footer = getContentToInject(contentsToInject[InjectPosition.AFTER]); + + if (!banner && !footer) { + // Nothing to inject. + return; + } + + // Rewrite outputs with the injected content. + // Only keep the entry files. + const outputs: string[] = Object.entries(result.metafile.outputs) + .map(([p, o]) => { + const entryPoint = o.entryPoint; + if (!entryPoint) { + return; + } + + const entry = entries.find((e) => e.resolved.endsWith(entryPoint)); + if (!entry) { + return; + } + + return getAbsolutePath(context.cwd, p); + }) + .filter(Boolean) as string[]; + + // Write the content. + const proms = outputs.map(async (output) => { + const source = await fsp.readFile(output, 'utf-8'); + const data = await esbuild.transform(source, { + loader: 'default', + banner, + footer, + }); + + // FIXME: Handle sourcemaps. + await fsp.writeFile(output, data.code); + }); + + await Promise.all(proms); + }); + }, +}); diff --git a/packages/plugins/injection/src/helpers.ts b/packages/plugins/injection/src/helpers.ts index d73871d1a..991e3a330 100644 --- a/packages/plugins/injection/src/helpers.ts +++ b/packages/plugins/injection/src/helpers.ts @@ -4,20 +4,30 @@ import { doRequest, truncateString } from '@dd/core/helpers'; import type { Logger, ToInjectItem } from '@dd/core/types'; +import { InjectPosition } from '@dd/core/types'; import { getAbsolutePath } from '@dd/internal-build-report-plugin/helpers'; import { readFile } from 'fs/promises'; -import { DISTANT_FILE_RX } from './constants'; +import { AFTER_INJECTION, BEFORE_INJECTION, DISTANT_FILE_RX } from './constants'; +import type { ContentsToInject } from './types'; const MAX_TIMEOUT_IN_MS = 5000; +export const getInjectedValue = async (item: ToInjectItem): Promise => { + if (typeof item.value === 'function') { + return item.value(); + } + + return item.value; +}; + export const processDistantFile = async ( - item: ToInjectItem, + url: string, timeout: number = MAX_TIMEOUT_IN_MS, ): Promise => { let timeoutId: ReturnType | undefined; return Promise.race([ - doRequest({ url: item.value }).finally(() => { + doRequest({ url }).finally(() => { if (timeout) { clearTimeout(timeoutId); } @@ -30,32 +40,36 @@ export const processDistantFile = async ( ]); }; -export const processLocalFile = async (item: ToInjectItem): Promise => { - const absolutePath = getAbsolutePath(process.cwd(), item.value); +export const processLocalFile = async ( + filepath: string, + cwd: string = process.cwd(), +): Promise => { + const absolutePath = getAbsolutePath(cwd, filepath); return readFile(absolutePath, { encoding: 'utf-8' }); }; -export const processRawCode = async (item: ToInjectItem): Promise => { - // TODO: Confirm the code actually executes without errors. - return item.value; -}; - -export const processItem = async (item: ToInjectItem, log: Logger): Promise => { +export const processItem = async ( + item: ToInjectItem, + log: Logger, + cwd: string = process.cwd(), +): Promise => { let result: string; + const value = await getInjectedValue(item); try { if (item.type === 'file') { - if (item.value.match(DISTANT_FILE_RX)) { - result = await processDistantFile(item); + if (value.match(DISTANT_FILE_RX)) { + result = await processDistantFile(value); } else { - result = await processLocalFile(item); + result = await processLocalFile(value, cwd); } } else if (item.type === 'code') { - result = await processRawCode(item); + // TODO: Confirm the code actually executes without errors. + result = value; } else { throw new Error(`Invalid item type "${item.type}", only accepts "code" or "file".`); } } catch (error: any) { - const itemId = `${item.type} - ${truncateString(item.value)}`; + const itemId = `${item.type} - ${truncateString(value)}`; if (item.fallback) { // In case of any error, we'll fallback to next item in queue. log.warn(`Fallback for "${itemId}": ${error.toString()}`); @@ -71,15 +85,43 @@ export const processItem = async (item: ToInjectItem, log: Logger): Promise, log: Logger, -): Promise => { - const proms: (Promise | string)[] = []; + cwd: string = process.cwd(), +): Promise> => { + const toReturn: Map = new Map(); - for (const item of toInject) { - proms.push(processItem(item, log)); + // Processing sequentially all the items. + for (const [id, item] of toInject.entries()) { + // eslint-disable-next-line no-await-in-loop + const value = await processItem(item, log, cwd); + if (value) { + toReturn.set(id, { value, position: item.position || InjectPosition.BEFORE }); + } } - const results = await Promise.all(proms); - return results.filter(Boolean); + return toReturn; +}; + +export const getContentToInject = (contentToInject: Map) => { + if (contentToInject.size === 0) { + return ''; + } + + const stringToInject = Array.from(contentToInject.values()).join('\n\n'); + return `${BEFORE_INJECTION}\n${stringToInject}\n${AFTER_INJECTION}`; +}; + +// Prepare and fetch the content to inject. +export const addInjections = async ( + log: Logger, + toInject: Map, + contentsToInject: ContentsToInject, + cwd: string = process.cwd(), +) => { + const results = await processInjections(toInject, log, cwd); + // Redistribute the content to inject in the right place. + for (const [id, value] of results.entries()) { + contentsToInject[value.position].set(id, value.value); + } }; diff --git a/packages/plugins/injection/src/index.ts b/packages/plugins/injection/src/index.ts index f936b318c..5215a0637 100644 --- a/packages/plugins/injection/src/index.ts +++ b/packages/plugins/injection/src/index.ts @@ -2,197 +2,87 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { INJECTED_FILE } from '@dd/core/constants'; -import { outputFile, rm } from '@dd/core/helpers'; -import type { GlobalContext, Logger, PluginOptions, ToInjectItem } from '@dd/core/types'; -import fs from 'fs'; -import path from 'path'; - -import { PLUGIN_NAME, PREPARATION_PLUGIN_NAME } from './constants'; -import { processInjections } from './helpers'; +import { isInjectionFile } from '@dd/core/helpers'; +import { + InjectPosition, + type GlobalContext, + type Logger, + type Options, + type PluginOptions, + type ToInjectItem, +} from '@dd/core/types'; + +import { PLUGIN_NAME } from './constants'; +import { getEsbuildPlugin } from './esbuild'; +import { addInjections, getContentToInject } from './helpers'; +import { getRollupPlugin } from './rollup'; +import type { ContentsToInject } from './types'; +import { getXpackPlugin } from './xpack'; export { PLUGIN_NAME } from './constants'; export const getInjectionPlugins = ( bundler: any, + options: Options, context: GlobalContext, - toInject: ToInjectItem[], + toInject: Map, log: Logger, ): PluginOptions[] => { - const contentToInject: string[] = []; - - const getContentToInject = () => { - // Needs a non empty string otherwise ESBuild will throw 'Do not know how to load path'. - // Most likely because it tries to generate an empty file. - const before = ` -/********************************************/ -/* BEGIN INJECTION BY DATADOG BUILD PLUGINS */`; - const after = ` -/* END INJECTION BY DATADOG BUILD PLUGINS */ -/********************************************/`; - - return `${before}\n${contentToInject.join('\n\n')}\n${after}`; + // Storage for all the positional contents we want to inject. + const contentsToInject: ContentsToInject = { + [InjectPosition.BEFORE]: new Map(), + [InjectPosition.MIDDLE]: new Map(), + [InjectPosition.AFTER]: new Map(), }; - // Rollup uses its own banner hook. - // We use its native functionality. - const rollupInjectionPlugin: PluginOptions['rollup'] = { - banner(chunk) { - if (chunk.isEntry) { - return getContentToInject(); - } - return ''; - }, - }; - - // Create a unique filename to avoid conflicts. - const INJECTED_FILE_PATH = `${Date.now()}.${performance.now()}.${INJECTED_FILE}.js`; - - // This plugin happens in 2 steps in order to cover all bundlers: - // 1. Prepare the content to inject, fetching distant/local files and anything necessary. - // a. [esbuild] We also create the actual file for esbuild to avoid any resolution errors - // and keep the inject override safe. - // b. [esbuild] With a custom resolver, every client side sub-builds would fail to resolve - // the file when re-using the same config as the parent build (with the inject). - // 2. Inject a virtual file into the bundling, this file will be home of all injected content. const plugins: PluginOptions[] = [ - // Prepare and fetch the content to inject for all bundlers. { - name: PREPARATION_PLUGIN_NAME, - enforce: 'pre', - // We use buildStart as it is the first async hook. + name: PLUGIN_NAME, + enforce: 'post', + // Bundler specific part of the plugin. + // We use it to: + // - Inject the content in the right places, each bundler offers this differently. + esbuild: getEsbuildPlugin(log, context, contentsToInject), + webpack: getXpackPlugin(bundler, log, context, toInject, contentsToInject), + rspack: getXpackPlugin(bundler, log, context, toInject, contentsToInject), + rollup: getRollupPlugin(contentsToInject), + vite: { ...getRollupPlugin(contentsToInject), enforce: 'pre' }, + // Universal part of the plugin. + // We use it to: + // - Prepare the injections. + // - Handle the resolution of the injection file. async buildStart() { - const results = await processInjections(toInject, log); - contentToInject.push(...results); - - // Only esbuild needs the following. - if (context.bundler.name !== 'esbuild') { + // In xpack, we need to prepare the injections before the build starts. + // So we do it in their specific plugin. + if (['webpack', 'rspack'].includes(context.bundler.name)) { return; } - // We put it in the outDir to avoid impacting any other part of the build. - // While still being under esbuild's cwd. - const absolutePathInjectFile = path.resolve( - context.bundler.outDir, - INJECTED_FILE_PATH, - ); - - // Actually create the file to avoid any resolution errors. - // It needs to be within cwd. - try { - // Verify that the file doesn't already exist. - if (fs.existsSync(absolutePathInjectFile)) { - log.warn(`Temporary file "${INJECTED_FILE_PATH}" already exists.`); - } - await outputFile(absolutePathInjectFile, getContentToInject()); - } catch (e: any) { - log.error(`Could not create the file: ${e.message}`); - } + // Prepare the injections. + await addInjections(log, toInject, contentsToInject, context.cwd); }, - - async buildEnd() { - // Only esbuild needs the following. - if (context.bundler.name !== 'esbuild') { - return; + async resolveId(source) { + if (isInjectionFile(source)) { + return { id: source }; } - const absolutePathInjectFile = path.resolve( - context.bundler.outDir, - INJECTED_FILE_PATH, - ); - - // Remove our assets. - log.debug(`Removing temporary file "${INJECTED_FILE_PATH}".`); - await rm(absolutePathInjectFile); - }, - }, - // Inject the file that will be home of all injected content. - // Each bundler has its own way to inject a file. - { - name: PLUGIN_NAME, - esbuild: { - setup(build) { - const { initialOptions } = build; - const absolutePathInjectFile = path.resolve( - context.bundler.outDir, - INJECTED_FILE_PATH, - ); - - // Inject the file in the build. - // This is made safe for sub-builds by actually creating the file. - initialOptions.inject = initialOptions.inject || []; - initialOptions.inject.push(absolutePathInjectFile); - }, + return null; }, - webpack: (compiler) => { - const BannerPlugin = - compiler?.webpack?.BannerPlugin || - bundler?.BannerPlugin || - bundler?.default?.BannerPlugin; - - const ChunkGraph = - compiler?.webpack?.ChunkGraph || - bundler?.ChunkGraph || - bundler?.default?.ChunkGraph; - - if (!BannerPlugin) { - log.error('Missing BannerPlugin'); + loadInclude(id) { + if (isInjectionFile(id)) { + return true; } - // Intercept the compilation's ChunkGraph - let chunkGraph: InstanceType; - compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { - compilation.hooks.afterChunks.tap(PLUGIN_NAME, () => { - chunkGraph = compilation.chunkGraph; - }); - }); - - compiler.options.plugins = compiler.options.plugins || []; - compiler.options.plugins.push( - new BannerPlugin({ - // Not wrapped in comments. - raw: true, - // Doesn't seem to work, but it's supposed to only add - // the banner to entry modules. - entryOnly: true, - banner(data) { - // In webpack5 we HAVE to use the chunkGraph. - if (context.bundler.variant === '5') { - if ( - !chunkGraph || - chunkGraph.getNumberOfEntryModules(data.chunk) === 0 - ) { - return ''; - } - - return getContentToInject(); - } else { - if (!data.chunk?.hasEntryModule()) { - return ''; - } - - return getContentToInject(); - } - }, - }), - ); + return null; }, - rspack: (compiler) => { - compiler.options.plugins = compiler.options.plugins || []; - compiler.options.plugins.push( - new compiler.rspack.BannerPlugin({ - // Not wrapped in comments. - raw: true, - // Only entry modules. - entryOnly: true, - banner() { - return getContentToInject(); - }, - }), - ); + load(id) { + if (isInjectionFile(id)) { + return { + code: getContentToInject(contentsToInject[InjectPosition.MIDDLE]), + }; + } + return null; }, - rollup: rollupInjectionPlugin, - vite: rollupInjectionPlugin, }, ]; diff --git a/packages/plugins/injection/src/rollup.ts b/packages/plugins/injection/src/rollup.ts new file mode 100644 index 000000000..ad1902f5d --- /dev/null +++ b/packages/plugins/injection/src/rollup.ts @@ -0,0 +1,86 @@ +// 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 { INJECTED_FILE } from '@dd/core/constants'; +import { isInjectionFile } from '@dd/core/helpers'; +import type { PluginOptions } from '@dd/core/types'; +import { InjectPosition } from '@dd/core/types'; + +import { getContentToInject } from './helpers'; +import type { ContentsToInject } from './types'; + +// Use "INJECTED_FILE" so it get flagged by isInjectionFile(). +const TO_INJECT_ID = INJECTED_FILE; +const TO_INJECT_SUFFIX = '?inject-proxy'; + +export const getRollupPlugin = (contentsToInject: ContentsToInject): PluginOptions['rollup'] => { + return { + banner(chunk) { + if (chunk.isEntry) { + // Can be empty. + return getContentToInject(contentsToInject[InjectPosition.BEFORE]); + } + return ''; + }, + async resolveId(source, importer, options) { + if (isInjectionFile(source)) { + // It is important that side effects are always respected for injections, otherwise using + // "treeshake.moduleSideEffects: false" may prevent the injection from being included. + return { id: source, moduleSideEffects: true }; + } + if (options.isEntry && getContentToInject(contentsToInject[InjectPosition.MIDDLE])) { + // Determine what the actual entry would have been. + const resolution = await this.resolve(source, importer, options); + // If it cannot be resolved or is external, just return it so that Rollup can display an error + if (!resolution || resolution.external) { + return resolution; + } + // In the load hook of the proxy, we need to know if the + // entry has a default export. There, however, we no longer + // have the full "resolution" object that may contain + // meta-data from other plugins that is only added on first + // load. Therefore we trigger loading here. + const moduleInfo = await this.load(resolution); + // We need to make sure side effects in the original entry + // point are respected even for + // treeshake.moduleSideEffects: false. "moduleSideEffects" + // is a writable property on ModuleInfo. + moduleInfo.moduleSideEffects = true; + // It is important that the new entry does not start with + // \0 and has the same directory as the original one to not + // mess up relative external import generation. Also + // keeping the name and just adding a "?query" to the end + // ensures that preserveModules will generate the original + // entry name for this entry. + return `${resolution.id}${TO_INJECT_SUFFIX}`; + } + return null; + }, + load(id) { + if (isInjectionFile(id)) { + // Replace with injection content. + return getContentToInject(contentsToInject[InjectPosition.MIDDLE]); + } + if (id.endsWith(TO_INJECT_SUFFIX)) { + const entryId = id.slice(0, -TO_INJECT_SUFFIX.length); + // We know ModuleInfo.hasDefaultExport is reliable because we awaited this.load in resolveId + const info = this.getModuleInfo(entryId); + let code = `import ${JSON.stringify(TO_INJECT_ID)};\nexport * from ${JSON.stringify(entryId)};`; + // Namespace reexports do not reexport default, so we need special handling here + if (info?.hasDefaultExport) { + code += `export { default } from ${JSON.stringify(entryId)};`; + } + return code; + } + return null; + }, + footer(chunk) { + if (chunk.isEntry) { + // Can be empty. + return getContentToInject(contentsToInject[InjectPosition.AFTER]); + } + return ''; + }, + }; +}; diff --git a/packages/plugins/injection/src/types.ts b/packages/plugins/injection/src/types.ts new file mode 100644 index 000000000..5b989b975 --- /dev/null +++ b/packages/plugins/injection/src/types.ts @@ -0,0 +1,14 @@ +// 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 type { InjectPosition } from '@dd/core/types'; + +export type ContentsToInject = Record>; + +export type FileToInject = { + absolutePath: string; + filename: string; + toInject: Map; +}; +export type FilesToInject = Record; diff --git a/packages/plugins/injection/src/xpack.ts b/packages/plugins/injection/src/xpack.ts new file mode 100644 index 000000000..254ae35df --- /dev/null +++ b/packages/plugins/injection/src/xpack.ts @@ -0,0 +1,169 @@ +// 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 { INJECTED_FILE } from '@dd/core/constants'; +import { getUniqueId, outputFile, rm } from '@dd/core/helpers'; +import type { GlobalContext, Logger, PluginOptions, ToInjectItem } from '@dd/core/types'; +import { InjectPosition } from '@dd/core/types'; +import { createRequire } from 'module'; +import path from 'path'; + +import { PLUGIN_NAME } from './constants'; +import { getContentToInject, addInjections } from './helpers'; +import type { ContentsToInject } from './types'; + +// A way to get the correct ConcatSource from either the bundler (rspack and webpack 5) +// or from 'webpack-sources' for webpack 4. +const getConcatSource = (bundler: any): typeof import('webpack-sources').ConcatSource => { + if (!bundler?.sources?.ConcatSource) { + // We need to require it as if we were "webpack", hence the createRequire from 'webpack'. + // This way, we don't have to declare them in our (peer)dependencies and always use the one + // that is compatible with the 'webpack' we're currently using. + const webpackRequire = createRequire(require.resolve('webpack')); + return webpackRequire('webpack-sources').ConcatSource; + } + return bundler.sources.ConcatSource; +}; + +export const getXpackPlugin = + ( + bundler: any, + log: Logger, + context: GlobalContext, + toInject: Map, + contentsToInject: ContentsToInject, + ): PluginOptions['rspack'] & PluginOptions['webpack'] => + (compiler) => { + const cache = new WeakMap(); + const ConcatSource = getConcatSource(bundler); + const filePath = path.resolve( + context.bundler.outDir, + `${getUniqueId()}.${InjectPosition.MIDDLE}.${INJECTED_FILE}.js`, + ); + + // Handle the InjectPosition.MIDDLE. + type Entry = typeof compiler.options.entry; + // TODO: Move this into @dd/core, add rspack/webpack types and tests. + const injectEntry = (initialEntry: Entry): Entry => { + const isWebpack4 = context.bundler.fullName === 'webpack4'; + + // Webpack 4 doesn't support the "import" property. + const injectedEntry = isWebpack4 + ? filePath + : { + import: [filePath], + }; + + const objectInjection = (entry: Entry) => { + for (const [entryKey, entryValue] of Object.entries(entry)) { + if (typeof entryValue === 'object') { + entryValue.import = entryValue.import || []; + entryValue.import.unshift(filePath); + } else if (typeof entryValue === 'string') { + // @ts-expect-error - Badly typed for strings. + entry[entryKey] = [filePath, entryValue]; + } else if (Array.isArray(entryValue)) { + entryValue.unshift(filePath); + } else { + log.error(`Invalid entry type: ${typeof entryValue}`); + } + } + }; + + if (!initialEntry) { + return { + // @ts-expect-error - Badly typed for strings. + ddHelper: injectedEntry, + }; + } else if (typeof initialEntry === 'function') { + // @ts-expect-error - This is webpack / rspack typing conflict. + return async () => { + const originEntry = await initialEntry(); + objectInjection(originEntry); + return originEntry; + }; + } else if (typeof initialEntry === 'object') { + objectInjection(initialEntry); + } else if (typeof initialEntry === 'string') { + // @ts-expect-error - Badly typed for strings. + return [injectedEntry, initialEntry]; + } else { + log.error(`Invalid entry type: ${typeof initialEntry}`); + return initialEntry; + } + return initialEntry; + }; + + const newEntry = injectEntry(compiler.options.entry); + // We inject the new entry. + compiler.options.entry = newEntry; + + // We need to prepare the injections before the build starts. + // Otherwise they'll be empty once resolved. + compiler.hooks.beforeRun.tapPromise(PLUGIN_NAME, async () => { + // RSpack MAY try to resolve the entry points before the loader is ready. + // There must be some race condition around this, because it's not always the case. + if (context.bundler.name === 'rspack') { + await outputFile(filePath, ''); + } + // Prepare the injections. + await addInjections(log, toInject, contentsToInject, context.cwd); + }); + + if (context.bundler.name === 'rspack') { + compiler.hooks.done.tapPromise(PLUGIN_NAME, async () => { + // Delete the fake file we created. + await rm(filePath); + }); + } + + // Handle the InjectPosition.START and InjectPosition.END. + // This is a re-implementation of the BannerPlugin, + // that is compatible with all versions of webpack and rspack, + // with both banner and footer. + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { + const hookCb = () => { + const banner = getContentToInject(contentsToInject[InjectPosition.BEFORE]); + const footer = getContentToInject(contentsToInject[InjectPosition.AFTER]); + + for (const chunk of compilation.chunks) { + if (!chunk.canBeInitial()) { + continue; + } + + for (const file of chunk.files) { + compilation.updateAsset(file, (old) => { + const cached = cache.get(old); + + // If anything changed, we need to re-create the source. + if (!cached || cached.banner !== banner || cached.footer !== footer) { + const source = new ConcatSource( + banner, + '\n', + // @ts-expect-error - This is webpack / rspack typing conflict. + old, + '\n', + footer, + ); + + // Cache the result. + cache.set(old, { source, banner, footer }); + return source; + } + + return cached.source; + }); + } + } + }; + + if (compilation.hooks.processAssets) { + const stage = bundler.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS; + compilation.hooks.processAssets.tap({ name: PLUGIN_NAME, stage }, hookCb); + } else { + // @ts-expect-error - "optimizeChunkAssets" is for webpack 4. + compilation.hooks.optimizeChunkAssets.tap({ name: PLUGIN_NAME }, hookCb); + } + }); + }; diff --git a/packages/tests/README.md b/packages/tests/README.md index 9008891e9..edde36c76 100644 --- a/packages/tests/README.md +++ b/packages/tests/README.md @@ -82,7 +82,7 @@ describe('My very awesome plugin', () => { We currently support `webpack4`, `webpack5`, `esbuild`, `rollup` and `vite`.
So we need to ensure that our plugin works everywhere. -When you use `runBundlers()` in your setup (usually `beforeAll()`), it will run the build of [a very basic default mock project](/packages/tests/src/_jest/fixtures/main.js).
+When you use `runBundlers()` in your setup (usually `beforeAll()`), it will run the build of [a very basic default mock project](/packages/tests/src/_jest/fixtures/easy_project/main.js).
Since it's building in a seeded directory, to avoid any collision, it will also return a cleanup function, that you'll need to use in your teardown (usually `afterAll()`). During development, you may want to target a specific bundler, to reduce noise from the others.
@@ -124,7 +124,6 @@ It will return the array of entries it created. Here's how you'd go with it: ```typescript -import { getWebpack4Entries } from '@dd/tests/_jest/helpers/xpackConfigs'; import { generateProject } from '@dd/tests/_jest/helpers/generateMassiveProject'; import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks'; import { runBundlers } from '@dd/tests/_jest/helpers/runBundlers'; @@ -145,11 +144,7 @@ describe('Some very massive project', () => { }, // Mode production makes the build waaaaayyyyy too slow. webpack5: { mode: 'none', entry: entries }, - webpack4: { - mode: 'none', - // Webpack4 needs some help for pnp resolutions. - entry: getWebpack4Entries(entries), - }, + webpack4: { mode: 'none', entry: entries }, }; cleanup = await runBundlers(defaultPluginOptions, bundlerOverrides); diff --git a/packages/tests/jest.config.js b/packages/tests/jest.config.js index 57fd27844..2ad83add8 100644 --- a/packages/tests/jest.config.js +++ b/packages/tests/jest.config.js @@ -14,4 +14,6 @@ module.exports = { setupFilesAfterEnv: ['/src/_jest/setupAfterEnv.ts'], testEnvironment: 'node', testMatch: ['**/*.test.*'], + // We're building a lot of projects in parallel, so we need to increase the timeout. + testTimeout: 20000, }; diff --git a/packages/tests/package.json b/packages/tests/package.json index 38bade13b..b7db57e06 100644 --- a/packages/tests/package.json +++ b/packages/tests/package.json @@ -26,6 +26,7 @@ "dependencies": { "@datadog/esbuild-plugin": "workspace:*", "@datadog/rollup-plugin": "workspace:*", + "@datadog/rspack-plugin": "workspace:*", "@datadog/vite-plugin": "workspace:*", "@datadog/webpack-plugin": "workspace:*", "@dd/core": "workspace:*", diff --git a/packages/tests/src/_jest/fixtures/.gitignore b/packages/tests/src/_jest/fixtures/.gitignore index 38ce858cb..5e2c6aa88 100644 --- a/packages/tests/src/_jest/fixtures/.gitignore +++ b/packages/tests/src/_jest/fixtures/.gitignore @@ -1 +1,11 @@ massiveProject +yarn-error.log +.yarn/* +!.yarn/cache +!.yarn/patches +!.yarn/releases +!.yarn/plugins +!.vscode + +node_modules/ +dist/ diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/@remix-run-router-npm-1.21.0-22ebfe59d7-cf0fb69d19.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/@remix-run-router-npm-1.21.0-22ebfe59d7-cf0fb69d19.zip new file mode 100644 index 000000000..d57ee4e82 Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/@remix-run-router-npm-1.21.0-22ebfe59d7-cf0fb69d19.zip differ diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/ansi-styles-npm-3.2.1-8cb8107983-d85ade01c1.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/ansi-styles-npm-3.2.1-8cb8107983-d85ade01c1.zip new file mode 100644 index 000000000..4ffdcc494 Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/ansi-styles-npm-3.2.1-8cb8107983-d85ade01c1.zip differ diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/chalk-npm-2.3.1-f10c7b2b06-53f7346b01.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/chalk-npm-2.3.1-f10c7b2b06-53f7346b01.zip new file mode 100644 index 000000000..34fc41f20 Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/chalk-npm-2.3.1-f10c7b2b06-53f7346b01.zip differ diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/color-convert-npm-1.9.3-1fe690075e-ffa3190250.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/color-convert-npm-1.9.3-1fe690075e-ffa3190250.zip new file mode 100644 index 000000000..c4d6feded Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/color-convert-npm-1.9.3-1fe690075e-ffa3190250.zip differ diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/color-name-npm-1.1.3-728b7b5d39-09c5d3e33d.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/color-name-npm-1.1.3-728b7b5d39-09c5d3e33d.zip new file mode 100644 index 000000000..f158de9e2 Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/color-name-npm-1.1.3-728b7b5d39-09c5d3e33d.zip differ diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/escape-string-regexp-npm-1.0.5-3284de402f-6092fda75c.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/escape-string-regexp-npm-1.0.5-3284de402f-6092fda75c.zip new file mode 100644 index 000000000..b7ea3be14 Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/escape-string-regexp-npm-1.0.5-3284de402f-6092fda75c.zip differ diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/has-flag-npm-3.0.0-16ac11fe05-4a15638b45.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/has-flag-npm-3.0.0-16ac11fe05-4a15638b45.zip new file mode 100644 index 000000000..60eafa65f Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/has-flag-npm-3.0.0-16ac11fe05-4a15638b45.zip differ diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/react-dom-npm-19.0.0-b7981c573e-aa64a2f199.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/react-dom-npm-19.0.0-b7981c573e-aa64a2f199.zip new file mode 100644 index 000000000..6ccb14bd6 Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/react-dom-npm-19.0.0-b7981c573e-aa64a2f199.zip differ diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/react-npm-19.0.0-e33c9aa1c0-2490969c50.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/react-npm-19.0.0-e33c9aa1c0-2490969c50.zip new file mode 100644 index 000000000..93d264328 Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/react-npm-19.0.0-e33c9aa1c0-2490969c50.zip differ diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/react-router-dom-npm-6.28.0-3bd3cd7fc0-e637825132.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/react-router-dom-npm-6.28.0-3bd3cd7fc0-e637825132.zip new file mode 100644 index 000000000..6fde01be5 Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/react-router-dom-npm-6.28.0-3bd3cd7fc0-e637825132.zip differ diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/react-router-npm-6.28.0-8611821701-f021a64451.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/react-router-npm-6.28.0-8611821701-f021a64451.zip new file mode 100644 index 000000000..bc89de389 Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/react-router-npm-6.28.0-8611821701-f021a64451.zip differ diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/scheduler-npm-0.25.0-f89e6cad04-e661e38503.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/scheduler-npm-0.25.0-f89e6cad04-e661e38503.zip new file mode 100644 index 000000000..05d831f8c Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/scheduler-npm-0.25.0-f89e6cad04-e661e38503.zip differ diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/supports-color-npm-5.5.0-183ac537bc-5f505c6fa3.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/supports-color-npm-5.5.0-183ac537bc-5f505c6fa3.zip new file mode 100644 index 000000000..55a34c67d Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/supports-color-npm-5.5.0-183ac537bc-5f505c6fa3.zip differ diff --git a/packages/tests/src/_jest/fixtures/.yarnrc.yml b/packages/tests/src/_jest/fixtures/.yarnrc.yml new file mode 100644 index 000000000..2aa2d15c7 --- /dev/null +++ b/packages/tests/src/_jest/fixtures/.yarnrc.yml @@ -0,0 +1,4 @@ +compressionLevel: mixed +defaultSemverRangePrefix: "" +enableGlobalCache: false +nodeLinker: node-modules diff --git a/packages/tests/src/_jest/fixtures/main.js b/packages/tests/src/_jest/fixtures/easy_project/main.js similarity index 100% rename from packages/tests/src/_jest/fixtures/main.js rename to packages/tests/src/_jest/fixtures/easy_project/main.js diff --git a/packages/tests/src/_jest/fixtures/easy_project/package.json b/packages/tests/src/_jest/fixtures/easy_project/package.json new file mode 100644 index 000000000..fe57ee635 --- /dev/null +++ b/packages/tests/src/_jest/fixtures/easy_project/package.json @@ -0,0 +1,13 @@ +{ + "name": "@tests/easy_project", + "private": true, + "license": "MIT", + "author": "Datadog", + "packageManager": "yarn@4.2.1", + "devDependencies": { + "chalk": "2.3.1", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-router-dom": "6.28.0" + } +} diff --git a/packages/tests/src/_jest/fixtures/project/empty.js b/packages/tests/src/_jest/fixtures/empty.js similarity index 100% rename from packages/tests/src/_jest/fixtures/project/empty.js rename to packages/tests/src/_jest/fixtures/empty.js diff --git a/packages/tests/src/_jest/fixtures/file-to-inject.js b/packages/tests/src/_jest/fixtures/fake-file-to-inject-after.js similarity index 80% rename from packages/tests/src/_jest/fixtures/file-to-inject.js rename to packages/tests/src/_jest/fixtures/fake-file-to-inject-after.js index 8c9d16b3e..2645ec5a0 100644 --- a/packages/tests/src/_jest/fixtures/file-to-inject.js +++ b/packages/tests/src/_jest/fixtures/fake-file-to-inject-after.js @@ -2,4 +2,4 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -console.log("Hello injection from local file."); +console.log("Hello injection from local file in after."); \ No newline at end of file diff --git a/packages/tests/src/_jest/fixtures/fake-file-to-inject-before.js b/packages/tests/src/_jest/fixtures/fake-file-to-inject-before.js new file mode 100644 index 000000000..6162e3d88 --- /dev/null +++ b/packages/tests/src/_jest/fixtures/fake-file-to-inject-before.js @@ -0,0 +1,5 @@ +// 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. + +console.log("Hello injection from local file in before."); \ No newline at end of file diff --git a/packages/tests/src/_jest/fixtures/fake-file-to-inject-middle.js b/packages/tests/src/_jest/fixtures/fake-file-to-inject-middle.js new file mode 100644 index 000000000..99815a1c5 --- /dev/null +++ b/packages/tests/src/_jest/fixtures/fake-file-to-inject-middle.js @@ -0,0 +1,5 @@ +// 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. + +console.log("Hello injection from local file in middle."); \ No newline at end of file diff --git a/packages/tests/src/_jest/fixtures/project/main1.js b/packages/tests/src/_jest/fixtures/hard_project/main1.js similarity index 91% rename from packages/tests/src/_jest/fixtures/project/main1.js rename to packages/tests/src/_jest/fixtures/hard_project/main1.js index 2c2c0ff17..05793a717 100644 --- a/packages/tests/src/_jest/fixtures/project/main1.js +++ b/packages/tests/src/_jest/fixtures/hard_project/main1.js @@ -10,7 +10,7 @@ import fn2 from './workspaces/app/workspaceFile1.js'; // Add a third party dependency. import * as chalk from 'chalk'; -console.log(chalk.cyan('Hello world!')); +console.log(chalk.cyan('Hello World!')); fn(); fn2(); diff --git a/packages/tests/src/_jest/fixtures/project/main2.js b/packages/tests/src/_jest/fixtures/hard_project/main2.js similarity index 100% rename from packages/tests/src/_jest/fixtures/project/main2.js rename to packages/tests/src/_jest/fixtures/hard_project/main2.js diff --git a/packages/tests/src/_jest/fixtures/hard_project/package.json b/packages/tests/src/_jest/fixtures/hard_project/package.json new file mode 100644 index 000000000..ac25238f5 --- /dev/null +++ b/packages/tests/src/_jest/fixtures/hard_project/package.json @@ -0,0 +1,15 @@ +{ + "name": "@tests/hard_project", + "private": true, + "license": "MIT", + "author": "Datadog", + "packageManager": "yarn@4.2.1", + "dependencies": { + "chalk": "2.3.1" + }, + "devDependencies": { + "react": "19.0.0", + "react-dom": "19.0.0", + "react-router-dom": "6.28.0" + } +} diff --git a/packages/tests/src/_jest/fixtures/project/src/srcFile0.js b/packages/tests/src/_jest/fixtures/hard_project/src/srcFile0.js similarity index 100% rename from packages/tests/src/_jest/fixtures/project/src/srcFile0.js rename to packages/tests/src/_jest/fixtures/hard_project/src/srcFile0.js diff --git a/packages/tests/src/_jest/fixtures/project/src/srcFile1.js b/packages/tests/src/_jest/fixtures/hard_project/src/srcFile1.js similarity index 100% rename from packages/tests/src/_jest/fixtures/project/src/srcFile1.js rename to packages/tests/src/_jest/fixtures/hard_project/src/srcFile1.js diff --git a/packages/tests/src/_jest/fixtures/project/workspaces/app/workspaceFile0.js b/packages/tests/src/_jest/fixtures/hard_project/workspaces/app/workspaceFile0.js similarity index 100% rename from packages/tests/src/_jest/fixtures/project/workspaces/app/workspaceFile0.js rename to packages/tests/src/_jest/fixtures/hard_project/workspaces/app/workspaceFile0.js diff --git a/packages/tests/src/_jest/fixtures/project/workspaces/app/workspaceFile1.js b/packages/tests/src/_jest/fixtures/hard_project/workspaces/app/workspaceFile1.js similarity index 100% rename from packages/tests/src/_jest/fixtures/project/workspaces/app/workspaceFile1.js rename to packages/tests/src/_jest/fixtures/hard_project/workspaces/app/workspaceFile1.js diff --git a/packages/tests/src/_jest/fixtures/project/package.json b/packages/tests/src/_jest/fixtures/package.json similarity index 50% rename from packages/tests/src/_jest/fixtures/project/package.json rename to packages/tests/src/_jest/fixtures/package.json index 77cc3b8b7..baafdfc02 100644 --- a/packages/tests/src/_jest/fixtures/project/package.json +++ b/packages/tests/src/_jest/fixtures/package.json @@ -1,10 +1,11 @@ { - "name": "project", + "name": "@tests/fixtures", "private": true, "license": "MIT", "author": "Datadog", "packageManager": "yarn@4.2.1", - "dependencies": { - "chalk": "2.3.1" - } + "workspaces": [ + "hard_project", + "easy_project" + ] } diff --git a/packages/tests/src/_jest/fixtures/yarn.lock b/packages/tests/src/_jest/fixtures/yarn.lock new file mode 100644 index 000000000..e8d79974f --- /dev/null +++ b/packages/tests/src/_jest/fixtures/yarn.lock @@ -0,0 +1,149 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10 + +"@remix-run/router@npm:1.21.0": + version: 1.21.0 + resolution: "@remix-run/router@npm:1.21.0" + checksum: 10/cf0fb69d19c1b79095ff67c59cea89086f3982a9a54c8a993818a60fc76e0ebab5a8db647c1a96a662729fad8e806ddd0a96622adf473f5a9f0b99998b2dbad4 + languageName: node + linkType: hard + +"@tests/easy_project@workspace:easy_project": + version: 0.0.0-use.local + resolution: "@tests/easy_project@workspace:easy_project" + dependencies: + chalk: "npm:2.3.1" + react: "npm:19.0.0" + react-dom: "npm:19.0.0" + react-router-dom: "npm:6.28.0" + languageName: unknown + linkType: soft + +"@tests/fixtures@workspace:.": + version: 0.0.0-use.local + resolution: "@tests/fixtures@workspace:." + languageName: unknown + linkType: soft + +"@tests/hard_project@workspace:hard_project": + version: 0.0.0-use.local + resolution: "@tests/hard_project@workspace:hard_project" + dependencies: + chalk: "npm:2.3.1" + react: "npm:19.0.0" + react-dom: "npm:19.0.0" + react-router-dom: "npm:6.28.0" + languageName: unknown + linkType: soft + +"ansi-styles@npm:^3.2.0": + version: 3.2.1 + resolution: "ansi-styles@npm:3.2.1" + dependencies: + color-convert: "npm:^1.9.0" + checksum: 10/d85ade01c10e5dd77b6c89f34ed7531da5830d2cb5882c645f330079975b716438cd7ebb81d0d6e6b4f9c577f19ae41ab55f07f19786b02f9dfd9e0377395665 + languageName: node + linkType: hard + +"chalk@npm:2.3.1": + version: 2.3.1 + resolution: "chalk@npm:2.3.1" + dependencies: + ansi-styles: "npm:^3.2.0" + escape-string-regexp: "npm:^1.0.5" + supports-color: "npm:^5.2.0" + checksum: 10/53f7346b01d5bd93cceb1645bf3858ef4a211b4c69be152e391cdbe386038308e227c14f5518c4f437cbca72054f0593c19f3ebc75b042892c79f46b0605f60b + languageName: node + linkType: hard + +"color-convert@npm:^1.9.0": + version: 1.9.3 + resolution: "color-convert@npm:1.9.3" + dependencies: + color-name: "npm:1.1.3" + checksum: 10/ffa319025045f2973919d155f25e7c00d08836b6b33ea2d205418c59bd63a665d713c52d9737a9e0fe467fb194b40fbef1d849bae80d674568ee220a31ef3d10 + languageName: node + linkType: hard + +"color-name@npm:1.1.3": + version: 1.1.3 + resolution: "color-name@npm:1.1.3" + checksum: 10/09c5d3e33d2105850153b14466501f2bfb30324a2f76568a408763a3b7433b0e50e5b4ab1947868e65cb101bb7cb75029553f2c333b6d4b8138a73fcc133d69d + languageName: node + linkType: hard + +"escape-string-regexp@npm:^1.0.5": + version: 1.0.5 + resolution: "escape-string-regexp@npm:1.0.5" + checksum: 10/6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 + languageName: node + linkType: hard + +"has-flag@npm:^3.0.0": + version: 3.0.0 + resolution: "has-flag@npm:3.0.0" + checksum: 10/4a15638b454bf086c8148979aae044dd6e39d63904cd452d970374fa6a87623423da485dfb814e7be882e05c096a7ccf1ebd48e7e7501d0208d8384ff4dea73b + languageName: node + linkType: hard + +"react-dom@npm:19.0.0": + version: 19.0.0 + resolution: "react-dom@npm:19.0.0" + dependencies: + scheduler: "npm:^0.25.0" + peerDependencies: + react: ^19.0.0 + checksum: 10/aa64a2f1991042f516260e8b0eca0ae777b6c8f1aa2b5ae096e80bbb6ac9b005aef2bca697969841d34f7e1819556263476bdfea36c35092e8d9aefde3de2d9a + languageName: node + linkType: hard + +"react-router-dom@npm:6.28.0": + version: 6.28.0 + resolution: "react-router-dom@npm:6.28.0" + dependencies: + "@remix-run/router": "npm:1.21.0" + react-router: "npm:6.28.0" + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: 10/e637825132ea96c3514ef7b8322f9bf0b752a942d6b4ffc4c20e389b5911726adf3dba8208ed4b97bf5b9c3bd465d9d1a1db1a58a610a8d528f18d890e0b143f + languageName: node + linkType: hard + +"react-router@npm:6.28.0": + version: 6.28.0 + resolution: "react-router@npm:6.28.0" + dependencies: + "@remix-run/router": "npm:1.21.0" + peerDependencies: + react: ">=16.8" + checksum: 10/f021a644513144884a567d9c2dcc432e8e3233f931378c219c5a3b5b842340f0faca86225a708bafca1e9010965afe1a7dada28aef5b7b6138c885c0552d9a7d + languageName: node + linkType: hard + +"react@npm:19.0.0": + version: 19.0.0 + resolution: "react@npm:19.0.0" + checksum: 10/2490969c503f644703c88990d20e4011fa6119ddeca451e9de48f6d7ab058d670d2852a5fcd3aa3cd90a923ab2815d532637bd4a814add402ae5c0d4f129ee71 + languageName: node + linkType: hard + +"scheduler@npm:^0.25.0": + version: 0.25.0 + resolution: "scheduler@npm:0.25.0" + checksum: 10/e661e38503ab29a153429a99203fefa764f28b35c079719eb5efdd2c1c1086522f6653d8ffce388209682c23891a6d1d32fa6badf53c35fb5b9cd0c55ace42de + languageName: node + linkType: hard + +"supports-color@npm:^5.2.0": + version: 5.5.0 + resolution: "supports-color@npm:5.5.0" + dependencies: + has-flag: "npm:^3.0.0" + checksum: 10/5f505c6fa3c6e05873b43af096ddeb22159831597649881aeb8572d6fe3b81e798cc10840d0c9735e0026b250368851b7f77b65e84f4e4daa820a4f69947f55b + languageName: node + linkType: hard diff --git a/packages/tests/src/_jest/globalSetup.ts b/packages/tests/src/_jest/globalSetup.ts index e87dd784a..81fd24ca6 100644 --- a/packages/tests/src/_jest/globalSetup.ts +++ b/packages/tests/src/_jest/globalSetup.ts @@ -2,11 +2,89 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { logTips } from './helpers/tips'; +import chalk from 'chalk'; +import { execFileSync } from 'child_process'; +import type { ExecFileSyncOptionsWithStringEncoding } from 'child_process'; +import path from 'path'; + +import { getEnv, logEnv, setupEnv } from './helpers/env'; + +const c = chalk.bold.dim; + +const setupGit = (execOptions: ExecFileSyncOptionsWithStringEncoding) => { + const setupSteps: { name: string; commands: string[]; fallbacks?: string[] }[] = [ + { + // Initialize a git repository. + name: 'Init', + commands: ['git init'], + }, + { + // Ensure we have a local user. + name: 'Git user', + commands: ['git config --local user.email'], + fallbacks: [ + 'git config --local user.email fake@example.com', + 'git config --local user.name fakeuser', + ], + }, + { + // Ensure origin exists + name: 'Origin', + commands: ['git ls-remote --get-url'], + fallbacks: ['git remote add origin fake_origin'], + }, + { + // Ensure HEAD exists + name: 'HEAD', + commands: ['git rev-parse --verify HEAD'], + // Fake HEAD. + fallbacks: ['git commit --allow-empty -n -m "abc"'], + }, + ]; + + const runCmds = (commands: string[]) => { + for (const command of commands) { + const args = command.split(' '); + execFileSync(args[0], args.slice(1), execOptions); + } + }; + for (const { name, commands, fallbacks } of setupSteps) { + try { + runCmds(commands); + } catch (e) { + if (!fallbacks || fallbacks.length === 0) { + throw e; + } + console.log(c.yellow(` - ${name} does not exist, creating it.`)); + runCmds(fallbacks); + } + } +}; const globalSetup = () => { + const timeId = `[${c.cyan('Test environment setup duration')}]`; + console.time(timeId); + const env = getEnv(process.argv); + // Setup the environment. + setupEnv(env); // Log some tips to the console. - logTips(); + logEnv(env); + + // Setup fixtures. + const execOptions: ExecFileSyncOptionsWithStringEncoding = { + cwd: path.resolve(__dirname, './fixtures'), + encoding: 'utf-8', + stdio: [], + }; + + try { + // Install dependencies. + execFileSync('yarn', ['install'], execOptions); + setupGit(execOptions); + } catch (e) { + console.error('Fixtures setup failed:', e); + } + console.timeEnd(timeId); }; export default globalSetup; diff --git a/packages/tests/src/_jest/helpers/configBundlers.ts b/packages/tests/src/_jest/helpers/configBundlers.ts index 3de7b2368..365d4723f 100644 --- a/packages/tests/src/_jest/helpers/configBundlers.ts +++ b/packages/tests/src/_jest/helpers/configBundlers.ts @@ -19,14 +19,15 @@ import webpack4 from 'webpack4'; import type { Configuration } from 'webpack5'; import webpack5 from 'webpack5'; -import { defaultDestination, defaultEntry, defaultPluginOptions } from './mocks'; -import type { BundlerOverrides } from './types'; -import { getBaseXpackConfig, getWebpack4Entries, getWebpackPlugin } from './xpackConfigs'; +import { getOutDir } from './env'; +import { defaultEntry, defaultPluginOptions } from './mocks'; +import type { BundlerOptionsOverrides } from './types'; +import { getBaseXpackConfig, getWebpackPlugin } from './xpackConfigs'; export const getRspackOptions = ( - seed: string, + workingDir: string, pluginOverrides: Partial = {}, - bundlerOverrides: BundlerOverrides['rspack'] = {}, + bundlerOverrides: BundlerOptionsOverrides['rspack'] = {}, ): RspackOptions => { const newPluginOptions = { ...defaultPluginOptions, @@ -34,16 +35,16 @@ export const getRspackOptions = ( }; return { - ...(getBaseXpackConfig(seed, 'rspack') as RspackOptions), + ...(getBaseXpackConfig(workingDir, 'rspack') as RspackOptions), plugins: [datadogRspackPlugin(newPluginOptions)], ...bundlerOverrides, }; }; export const getWebpack5Options = ( - seed: string, + workingDir: string, pluginOverrides: Partial = {}, - bundlerOverrides: BundlerOverrides['webpack5'] = {}, + bundlerOverrides: BundlerOptionsOverrides['webpack5'] = {}, ): Configuration => { const newPluginOptions = { ...defaultPluginOptions, @@ -53,16 +54,16 @@ export const getWebpack5Options = ( const plugin = getWebpackPlugin(newPluginOptions, webpack5); return { - ...getBaseXpackConfig(seed, 'webpack5'), + ...getBaseXpackConfig(workingDir, 'webpack5'), plugins: [plugin], ...bundlerOverrides, }; }; export const getWebpack4Options = ( - seed: string, + workingDir: string, pluginOverrides: Partial = {}, - bundlerOverrides: BundlerOverrides['webpack4'] = {}, + bundlerOverrides: BundlerOptionsOverrides['webpack4'] = {}, ): Configuration4 => { const newPluginOptions = { ...defaultPluginOptions, @@ -72,8 +73,7 @@ export const getWebpack4Options = ( const plugin = getWebpackPlugin(newPluginOptions, webpack4); return { - ...getBaseXpackConfig(seed, 'webpack4'), - entry: getWebpack4Entries(defaultEntry), + ...getBaseXpackConfig(workingDir, 'webpack4'), plugins: [plugin as unknown as Plugin], node: false, ...bundlerOverrides, @@ -81,9 +81,9 @@ export const getWebpack4Options = ( }; export const getEsbuildOptions = ( - seed: string, + workingDir: string, pluginOverrides: Partial = {}, - bundlerOverrides: BundlerOverrides['esbuild'] = {}, + bundlerOverrides: BundlerOptionsOverrides['esbuild'] = {}, ): BuildOptions => { const newPluginOptions = { ...defaultPluginOptions, @@ -91,12 +91,13 @@ export const getEsbuildOptions = ( }; return { + absWorkingDir: workingDir, bundle: true, chunkNames: 'chunk.[hash]', entryPoints: { main: defaultEntry }, entryNames: '[name]', format: 'esm', - outdir: path.join(defaultDestination, seed, 'esbuild'), + outdir: getOutDir(workingDir, 'esbuild'), plugins: [datadogEsbuildPlugin(newPluginOptions)], sourcemap: true, splitting: true, @@ -104,9 +105,10 @@ export const getEsbuildOptions = ( }; }; -export const getRollupBaseConfig = (seed: string, bundlerName: string): RollupOptions => { +export const getRollupBaseConfig = (workingDir: string, bundlerName: string): RollupOptions => { + const outDir = getOutDir(workingDir, bundlerName); return { - input: defaultEntry, + input: path.resolve(workingDir, defaultEntry), onwarn: (warning, handler) => { if ( !/Circular dependency:/.test(warning.message) && @@ -118,7 +120,7 @@ export const getRollupBaseConfig = (seed: string, bundlerName: string): RollupOp output: { chunkFileNames: 'chunk.[hash].js', compact: false, - dir: path.join(defaultDestination, seed, bundlerName), + dir: outDir, entryFileNames: '[name].js', sourcemap: true, }, @@ -126,16 +128,16 @@ export const getRollupBaseConfig = (seed: string, bundlerName: string): RollupOp }; export const getRollupOptions = ( - seed: string, + workingDir: string, pluginOverrides: Partial = {}, - bundlerOverrides: BundlerOverrides['rollup'] = {}, + bundlerOverrides: BundlerOptionsOverrides['rollup'] = {}, ): RollupOptions => { const newPluginOptions = { ...defaultPluginOptions, ...pluginOverrides, }; - const baseConfig = getRollupBaseConfig(seed, 'rollup'); + const baseConfig = getRollupBaseConfig(workingDir, 'rollup'); return { ...baseConfig, @@ -153,18 +155,19 @@ export const getRollupOptions = ( }; export const getViteOptions = ( - seed: string, + workingDir: string, pluginOverrides: Partial = {}, - bundlerOverrides: BundlerOverrides['vite'] = {}, + bundlerOverrides: BundlerOptionsOverrides['vite'] = {}, ): UserConfig => { const newPluginOptions = { ...defaultPluginOptions, ...pluginOverrides, }; - const baseConfig = getRollupBaseConfig(seed, 'vite'); + const baseConfig = getRollupBaseConfig(workingDir, 'vite'); return { + root: workingDir, build: { assetsDir: '', // Disable assets dir to simplify the test. minify: false, diff --git a/packages/tests/src/_jest/helpers/constants.ts b/packages/tests/src/_jest/helpers/constants.ts index 0e70787be..345b43b73 100644 --- a/packages/tests/src/_jest/helpers/constants.ts +++ b/packages/tests/src/_jest/helpers/constants.ts @@ -20,17 +20,3 @@ export const BUNDLER_VERSIONS: Record = { webpack4: require('webpack4').version, webpack5: require('webpack5').version, }; - -// Handle --cleanup flag. -export const NO_CLEANUP = process.argv.includes('--cleanup=0'); - -// Handle --build flag. -export const NEED_BUILD = process.argv.includes('--build=1'); - -// Handle --bundlers flag. -export const REQUESTED_BUNDLERS = process.argv.includes('--bundlers') - ? process.argv[process.argv.indexOf('--bundlers') + 1].split(',') - : process.argv - .find((arg) => arg.startsWith('--bundlers=')) - ?.split('=')[1] - .split(',') ?? []; diff --git a/packages/tests/src/_jest/helpers/env.ts b/packages/tests/src/_jest/helpers/env.ts new file mode 100644 index 000000000..6f87a3c9b --- /dev/null +++ b/packages/tests/src/_jest/helpers/env.ts @@ -0,0 +1,133 @@ +// 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 { FULL_NAME_BUNDLERS } from '@dd/core/constants'; +import { mkdir } from '@dd/core/helpers'; +import type { BundlerFullName } from '@dd/core/types'; +import { bgYellow, dim, green, red } from '@dd/tools/helpers'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +const fsp = fs.promises; + +type TestEnv = { + NO_CLEANUP: boolean; + NEED_BUILD: boolean; + REQUESTED_BUNDLERS: string[]; +}; + +export const getEnv = (argv: string[]): TestEnv => { + // Handle --cleanup flag. + const NO_CLEANUP = argv.includes('--cleanup=0'); + + // Handle --build flag. + const NEED_BUILD = argv.includes('--build=1'); + + // Handle --bundlers flag. + const REQUESTED_BUNDLERS = argv.includes('--bundlers') + ? argv[argv.indexOf('--bundlers') + 1].split(',') + : argv + .find((arg) => arg.startsWith('--bundlers=')) + ?.split('=')[1] + .split(',') ?? []; + + return { + NO_CLEANUP, + NEED_BUILD, + REQUESTED_BUNDLERS, + }; +}; + +export const setupEnv = (env: TestEnv): void => { + const { NO_CLEANUP, NEED_BUILD, REQUESTED_BUNDLERS } = env; + + if (NO_CLEANUP) { + process.env.NO_CLEANUP = '1'; + } + + if (NEED_BUILD) { + process.env.NEED_BUILD = '1'; + } + + if (REQUESTED_BUNDLERS.length) { + process.env.REQUESTED_BUNDLERS = REQUESTED_BUNDLERS.join(','); + } +}; + +export const logEnv = (env: TestEnv) => { + const { NO_CLEANUP, NEED_BUILD, REQUESTED_BUNDLERS } = env; + const envLogs = []; + if (NO_CLEANUP) { + envLogs.push(bgYellow(" Won't clean up ")); + } + + if (NEED_BUILD) { + envLogs.push(bgYellow(' Will also build used plugins ')); + } + + if (REQUESTED_BUNDLERS.length) { + if ( + !(REQUESTED_BUNDLERS as BundlerFullName[]).every((bundler) => + FULL_NAME_BUNDLERS.includes(bundler), + ) + ) { + throw new Error( + `Invalid "${red(`--bundlers ${REQUESTED_BUNDLERS.join(',')}`)}".\nValid bundlers are ${FULL_NAME_BUNDLERS.map( + (b) => green(b), + ).join(', ')}.`, + ); + } + const bundlersList = REQUESTED_BUNDLERS.map((bundler) => green(bundler)).join(', '); + envLogs.push(`Running ${bgYellow(' ONLY ')} for ${bundlersList}.`); + } + + if (!NO_CLEANUP || !NEED_BUILD || REQUESTED_BUNDLERS.length) { + const tips: string[] = []; + if (!NO_CLEANUP) { + tips.push(` ${green('--cleanup=0')} to keep the built artifacts.`); + } + if (!NEED_BUILD) { + tips.push(` ${green('--build=1')} to force the build of the used plugins.`); + } + if (!REQUESTED_BUNDLERS.length) { + tips.push(` ${green('--bundlers=webpack4,esbuild')} to only use specified bundlers.`); + } + envLogs.push(dim(`\nYou can also use : \n${tips.join('\n')}\n`)); + } + + if (envLogs.length) { + console.log(`\n${envLogs.join('\n')}\n`); + } +}; + +export const getOutDir = (workingDir: string, folderName: string): string => { + return path.resolve(workingDir, `./dist/${folderName}`); +}; + +const FIXTURE_DIR = path.resolve(__dirname, '../fixtures'); +export const prepareWorkingDir = async (seed: string) => { + const timeId = `[${dim.cyan('Preparing working directory duration')}]`; + console.time(timeId); + const tmpDir = os.tmpdir(); + const workingDir = path.resolve(tmpDir, seed); + + // Create the directory. + await mkdir(workingDir); + + // Need to use realpathSync to avoid issues with symlinks on macos (prefix with /private). + // cf: https://github.com/nodejs/node/issues/11422 + const realWorkingDir = await fsp.realpath(workingDir); + + // Copy mock projects into it. + await fsp.cp(`${FIXTURE_DIR}/`, `${realWorkingDir}/`, { + recursive: true, + errorOnExist: true, + force: true, + }); + + console.timeEnd(timeId); + + return realWorkingDir; +}; diff --git a/packages/tests/src/_jest/helpers/mocks.ts b/packages/tests/src/_jest/helpers/mocks.ts index afbe021c9..0318f267e 100644 --- a/packages/tests/src/_jest/helpers/mocks.ts +++ b/packages/tests/src/_jest/helpers/mocks.ts @@ -13,30 +13,24 @@ import type { LogLevel, Options, } from '@dd/core/types'; -import { serializeBuildReport } from '@dd/internal-build-report-plugin/helpers'; +import { getAbsolutePath, serializeBuildReport } from '@dd/internal-build-report-plugin/helpers'; import { getSourcemapsConfiguration } from '@dd/tests/plugins/error-tracking/testHelpers'; import { getTelemetryConfiguration } from '@dd/tests/plugins/telemetry/testHelpers'; +import type { PluginBuild } from 'esbuild'; import path from 'path'; -import type { Configuration as Configuration4 } from 'webpack4'; -import type { BundlerOverrides } from './types'; -import { getBaseXpackConfig, getWebpack4Entries } from './xpackConfigs'; - -if (!process.env.PROJECT_CWD) { - throw new Error('Please update the usage of `process.env.PROJECT_CWD`.'); -} -const ROOT = process.env.PROJECT_CWD!; +import type { BundlerOptionsOverrides, BundlerOverrides } from './types'; +import { getBaseXpackConfig } from './xpackConfigs'; export const FAKE_URL = 'https://example.com'; export const API_PATH = '/v2/srcmap'; export const INTAKE_URL = `${FAKE_URL}${API_PATH}`; -export const defaultEntry = '@dd/tests/_jest/fixtures/main.js'; +export const defaultEntry = './easy_project/main.js'; export const defaultEntries = { - app1: '@dd/tests/_jest/fixtures/project/main1.js', - app2: '@dd/tests/_jest/fixtures/project/main2.js', + app1: './hard_project/main1.js', + app2: './hard_project/main2.js', }; -export const defaultDestination = path.resolve(ROOT, 'packages/tests/src/_jest/fixtures/dist'); export const defaultPluginOptions: GetPluginsOptions = { auth: { @@ -64,7 +58,47 @@ const logFn: Logger = { }; export const mockLogger: Logger = logFn; -export const getContextMock = (options: Partial = {}): GlobalContext => { +export const getEsbuildMock = (overrides: Partial = {}): PluginBuild => { + return { + resolve: async (filepath) => { + return { + errors: [], + warnings: [], + external: false, + sideEffects: false, + namespace: '', + suffix: '', + pluginData: {}, + path: getAbsolutePath(process.cwd(), filepath), + }; + }, + onStart: jest.fn(), + onEnd: jest.fn(), + onResolve: jest.fn(), + onLoad: jest.fn(), + onDispose: jest.fn(), + ...overrides, + esbuild: { + context: jest.fn(), + build: jest.fn(), + buildSync: jest.fn(), + transform: jest.fn(), + transformSync: jest.fn(), + formatMessages: jest.fn(), + formatMessagesSync: jest.fn(), + analyzeMetafile: jest.fn(), + analyzeMetafileSync: jest.fn(), + initialize: jest.fn(), + version: '1.0.0', + ...(overrides.esbuild || {}), + }, + initialOptions: { + ...(overrides.initialOptions || {}), + }, + }; +}; + +export const getContextMock = (overrides: Partial = {}): GlobalContext => { return { auth: { apiKey: 'FAKE_API_KEY' }, bundler: { @@ -83,59 +117,73 @@ export const getContextMock = (options: Partial = {}): GlobalCont pluginNames: [], start: Date.now(), version: 'FAKE_VERSION', - ...options, + ...overrides, }; }; -export const getComplexBuildOverrides = ( - overrides: BundlerOverrides = {}, -): Required => { - const bundlerOverrides = { - rollup: { - input: defaultEntries, - ...overrides.rollup, - }, - vite: { - input: defaultEntries, - ...overrides.vite, - }, - esbuild: { - entryPoints: defaultEntries, - ...overrides.esbuild, - }, - rspack: { entry: defaultEntries, ...overrides.rspack }, - webpack5: { entry: defaultEntries, ...overrides.webpack5 }, - webpack4: { - entry: getWebpack4Entries(defaultEntries), - ...overrides.webpack4, - }, - }; +export const getComplexBuildOverrides = + (overrides?: BundlerOverrides) => + (workingDir: string): Required => { + const overridesResolved = + typeof overrides === 'function' ? overrides(workingDir) : overrides || {}; - return bundlerOverrides; -}; + // Using a function to avoid mutation of the same object later down the line. + const entries = () => + Object.fromEntries( + Object.entries(defaultEntries).map(([key, value]) => [ + key, + path.resolve(workingDir, value), + ]), + ); + + const bundlerOverrides = { + rollup: { + input: entries(), + ...overridesResolved.rollup, + }, + vite: { + input: entries(), + ...overridesResolved.vite, + }, + esbuild: { + entryPoints: entries(), + ...overridesResolved.esbuild, + }, + rspack: { entry: entries(), ...overridesResolved.rspack }, + webpack5: { entry: entries(), ...overridesResolved.webpack5 }, + webpack4: { entry: entries(), ...overridesResolved.webpack4 }, + }; + + return bundlerOverrides; + }; // To get a node safe build. export const getNodeSafeBuildOverrides = ( - overrides: BundlerOverrides = {}, -): Required => { + workingDir: string, + overrides?: BundlerOverrides, +): Required => { + const overridesResolved = + typeof overrides === 'function' ? overrides(workingDir) : overrides || {}; // We don't care about the seed and the bundler name // as we won't use the output config here. - const baseWebpack = getBaseXpackConfig('fake_seed', 'fake_bundler'); - const bundlerOverrides: Required = { + const baseWebpack = getBaseXpackConfig('fake_seed/dist', 'fake_bundler'); + const bundlerOverrides: Required = { rollup: { + ...overridesResolved.rollup, output: { + ...overridesResolved.rollup?.output, format: 'cjs', }, - ...overrides.rollup, }, vite: { + ...overridesResolved.vite, output: { + ...overridesResolved.vite?.output, format: 'cjs', }, - ...overrides.vite, }, esbuild: { - ...overrides.esbuild, + ...overridesResolved.esbuild, }, rspack: { target: 'node', @@ -143,7 +191,7 @@ export const getNodeSafeBuildOverrides = ( ...baseWebpack.optimization, splitChunks: false, }, - ...overrides.rspack, + ...overridesResolved.rspack, }, webpack5: { target: 'node', @@ -151,15 +199,15 @@ export const getNodeSafeBuildOverrides = ( ...baseWebpack.optimization, splitChunks: false, }, - ...overrides.webpack5, + ...overridesResolved.webpack5, }, webpack4: { target: 'node', optimization: { - ...(baseWebpack.optimization as Configuration4['optimization']), + ...baseWebpack.optimization, splitChunks: false, }, - ...overrides.webpack4, + ...overridesResolved.webpack4, }, }; @@ -203,10 +251,10 @@ export const debugFilesPlugins = (context: GlobalContext): CustomPlugins => { const xpackPlugin: IterableElement['webpack'] & IterableElement['rspack'] = (compiler) => { - type Compilation = Parameters[1]>[0]; + type Stats = Parameters[1]>[0]; - compiler.hooks.afterEmit.tap('bundler-outputs', (compilation: Compilation) => { - const stats = compilation.getStats().toJson({ + compiler.hooks.done.tap('bundler-outputs', (stats: Stats) => { + const statsJson = stats.toJson({ all: false, assets: true, children: true, @@ -227,7 +275,7 @@ export const debugFilesPlugins = (context: GlobalContext): CustomPlugins => { }); outputJsonSync( path.resolve(context.bundler.outDir, `output.${context.bundler.fullName}.json`), - stats, + statsJson, ); }); }; @@ -274,4 +322,4 @@ export const filterOutParticularities = (input: File) => // Exclude webpack buildin modules, which are webpack internal dependencies. !input.filepath.includes('webpack4/buildin') && // Exclude webpack's fake entry point. - !input.filepath.includes('fixtures/project/empty.js'); + !input.filepath.includes('fixtures/empty.js'); diff --git a/packages/tests/src/_jest/helpers/runBundlers.ts b/packages/tests/src/_jest/helpers/runBundlers.ts index ada0b3d96..59f8fb7b0 100644 --- a/packages/tests/src/_jest/helpers/runBundlers.ts +++ b/packages/tests/src/_jest/helpers/runBundlers.ts @@ -2,12 +2,11 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { rm } from '@dd/core/helpers'; +import { getUniqueId, rm } from '@dd/core/helpers'; import type { Options } from '@dd/core/types'; import { executeSync, green } from '@dd/tools/helpers'; import type { RspackOptions, Stats as RspackStats } from '@rspack/core'; import type { BuildOptions } from 'esbuild'; -import path from 'path'; import type { RollupOptions } from 'rollup'; import type { Configuration as Configuration4, Stats as Stats4 } from 'webpack4'; import type { Configuration, Stats } from 'webpack5'; @@ -20,9 +19,18 @@ import { getWebpack4Options, getWebpack5Options, } from './configBundlers'; -import { NEED_BUILD, NO_CLEANUP, PLUGIN_VERSIONS, REQUESTED_BUNDLERS } from './constants'; -import { defaultDestination } from './mocks'; -import type { Bundler, BundlerRunFunction, CleanupFn } from './types'; +import { PLUGIN_VERSIONS } from './constants'; +import { prepareWorkingDir } from './env'; +import type { + Bundler, + BundlerRunFunction, + CleanupFn, + BundlerOverrides, + CleanupEverythingFn, +} from './types'; + +// Get the environment variables. +const { NO_CLEANUP, NEED_BUILD, REQUESTED_BUNDLERS } = process.env; const xpackCallback = ( err: Error | null, @@ -37,7 +45,7 @@ const xpackCallback = ( } if (!stats) { - reject('No stats returned from webpack.'); + reject('No stats returned.'); return; } @@ -82,11 +90,11 @@ const getCleanupFunction = }; export const runRspack: BundlerRunFunction = async ( - seed: string, + workingDir: string, pluginOverrides: Options = {}, bundlerOverrides: Partial = {}, ) => { - const bundlerConfigs = getRspackOptions(seed, pluginOverrides, bundlerOverrides); + const bundlerConfigs = getRspackOptions(workingDir, pluginOverrides, bundlerOverrides); const { rspack } = await import('@rspack/core'); const errors = []; @@ -105,11 +113,11 @@ export const runRspack: BundlerRunFunction = async ( }; export const runWebpack5: BundlerRunFunction = async ( - seed: string, + workingDir: string, pluginOverrides: Options = {}, bundlerOverrides: Partial = {}, ) => { - const bundlerConfigs = getWebpack5Options(seed, pluginOverrides, bundlerOverrides); + const bundlerConfigs = getWebpack5Options(workingDir, pluginOverrides, bundlerOverrides); const { webpack } = await import('webpack5'); const errors = []; @@ -128,11 +136,11 @@ export const runWebpack5: BundlerRunFunction = async ( }; export const runWebpack4: BundlerRunFunction = async ( - seed: string, + workingDir: string, pluginOverrides: Options = {}, bundlerOverrides: Partial = {}, ) => { - const bundlerConfigs = getWebpack4Options(seed, pluginOverrides, bundlerOverrides); + const bundlerConfigs = getWebpack4Options(workingDir, pluginOverrides, bundlerOverrides); const webpack = (await import('webpack4')).default; const errors = []; @@ -151,11 +159,11 @@ export const runWebpack4: BundlerRunFunction = async ( }; export const runEsbuild: BundlerRunFunction = async ( - seed: string, + workingDir: string, pluginOverrides: Options = {}, bundlerOverrides: Partial = {}, ) => { - const bundlerConfigs = getEsbuildOptions(seed, pluginOverrides, bundlerOverrides); + const bundlerConfigs = getEsbuildOptions(workingDir, pluginOverrides, bundlerOverrides); const { build } = await import('esbuild'); const errors = []; @@ -170,11 +178,11 @@ export const runEsbuild: BundlerRunFunction = async ( }; export const runVite: BundlerRunFunction = async ( - seed: string, + workingDir: string, pluginOverrides: Options = {}, bundlerOverrides: Partial = {}, ) => { - const bundlerConfigs = getViteOptions(seed, pluginOverrides, bundlerOverrides); + const bundlerConfigs = getViteOptions(workingDir, pluginOverrides, bundlerOverrides); const vite = await import('vite'); const errors = []; try { @@ -195,11 +203,11 @@ export const runVite: BundlerRunFunction = async ( }; export const runRollup: BundlerRunFunction = async ( - seed: string, + workingDir: string, pluginOverrides: Options = {}, bundlerOverrides: Partial = {}, ) => { - const bundlerConfigs = getRollupOptions(seed, pluginOverrides, bundlerOverrides); + const bundlerConfigs = getRollupOptions(workingDir, pluginOverrides, bundlerOverrides); const { rollup } = await import('rollup'); const errors = []; @@ -272,8 +280,9 @@ const allBundlers: Bundler[] = [ }, ]; +const requestedBundlers = REQUESTED_BUNDLERS ? REQUESTED_BUNDLERS.split(',') : []; export const BUNDLERS: Bundler[] = allBundlers.filter( - (bundler) => REQUESTED_BUNDLERS.length === 0 || REQUESTED_BUNDLERS.includes(bundler.name), + (bundler) => requestedBundlers.length === 0 || requestedBundlers.includes(bundler.name), ); // Build only if needed. @@ -284,35 +293,39 @@ if (NEED_BUILD) { for (const bundler of bundlersToBuild) { console.log(`Building ${green(bundler)}...`); + // Can't do parallel builds because no await at root. executeSync('yarn', ['workspace', bundler, 'run', 'build']); } } export const runBundlers = async ( pluginOverrides: Partial = {}, - bundlerOverrides: Record = {}, + bundlerOverrides?: BundlerOverrides, bundlers?: string[], -): Promise => { - const cleanups: CleanupFn[] = []; +): Promise => { const errors: string[] = []; // Generate a seed to avoid collision of builds. - const seed: string = `${Date.now()}-${jest.getSeed()}`; + const seed: string = `${jest.getSeed()}.${getUniqueId()}`; const bundlersToRun = BUNDLERS.filter( (bundler) => !bundlers || bundlers.includes(bundler.name), ); + const workingDir = await prepareWorkingDir(seed); + + const bundlerOverridesResolved = + typeof bundlerOverrides === 'function' + ? bundlerOverrides(workingDir) + : bundlerOverrides || {}; + const runBundlerFunction = async (bundler: Bundler) => { - let bundlerOverride = {}; - if (bundlerOverrides[bundler.name]) { - bundlerOverride = bundlerOverrides[bundler.name]; - } + const bundlerOverride = bundlerOverridesResolved[bundler.name] || {}; let result: Awaited>; // Isolate each runs to avoid conflicts between tests. await jest.isolateModulesAsync(async () => { - result = await bundler.run(seed, pluginOverrides, bundlerOverride); + result = await bundler.run(workingDir, pluginOverrides, bundlerOverride); }); return result!; }; @@ -323,25 +336,19 @@ export const runBundlers = async ( // eslint-disable-next-line no-await-in-loop results.push(await runBundlerFunction(bundler)); } - cleanups.push(...results.map((result) => result.cleanup)); errors.push(...results.map((result) => result.errors).flat()); - // Add a cleanup for the root seeded directory. - cleanups.push(getCleanupFunction('Root', [path.resolve(defaultDestination, seed)])); - const cleanupEverything = async () => { try { - await Promise.all(cleanups.map((cleanup) => cleanup())); + // Cleanup working directory. + await getCleanupFunction('Root', [workingDir])(); } catch (e) { console.error('Error during cleanup', e); } }; - if (errors.length) { - // We'll throw, so clean everything first. - await cleanupEverything(); - throw new Error(errors.join('\n')); - } + cleanupEverything.errors = errors; + cleanupEverything.workingDir = workingDir; // Return a cleanUp function. return cleanupEverything; diff --git a/packages/tests/src/_jest/helpers/tips.ts b/packages/tests/src/_jest/helpers/tips.ts deleted file mode 100644 index 7af7394fb..000000000 --- a/packages/tests/src/_jest/helpers/tips.ts +++ /dev/null @@ -1,51 +0,0 @@ -// 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 { FULL_NAME_BUNDLERS } from '@dd/core/constants'; -import type { BundlerFullName } from '@dd/core/types'; -import { bgYellow, dim, green, red } from '@dd/tools/helpers'; - -import { NEED_BUILD, NO_CLEANUP, REQUESTED_BUNDLERS } from './constants'; - -export const logTips = () => { - if (NO_CLEANUP) { - console.log(bgYellow(" Won't clean up ")); - } - - if (NEED_BUILD) { - console.log(bgYellow(' Will also build used plugins ')); - } - - if (REQUESTED_BUNDLERS.length) { - if ( - !(REQUESTED_BUNDLERS as BundlerFullName[]).every((bundler) => - FULL_NAME_BUNDLERS.includes(bundler), - ) - ) { - throw new Error( - `Invalid "${red(`--bundlers ${REQUESTED_BUNDLERS.join(',')}`)}".\nValid bundlers are ${FULL_NAME_BUNDLERS.map( - (b) => green(b), - ) - .sort() - .join(', ')}.`, - ); - } - const bundlersList = REQUESTED_BUNDLERS.map((bundler) => green(bundler)).join(', '); - console.log(`Running ${bgYellow(' ONLY ')} for ${bundlersList}.`); - } - - if (!NO_CLEANUP || !NEED_BUILD || REQUESTED_BUNDLERS.length) { - const tips: string[] = []; - if (!NO_CLEANUP) { - tips.push(` ${green('--cleanup=0')} to keep the built artifacts.`); - } - if (!NEED_BUILD) { - tips.push(` ${green('--build=1')} to force the build of the used plugins.`); - } - if (!REQUESTED_BUNDLERS.length) { - tips.push(` ${green('--bundlers=webpack4,esbuild')} to only use specified bundlers.`); - } - console.log(dim(`\nYou can also use : \n${tips.join('\n')}\n`)); - } -}; diff --git a/packages/tests/src/_jest/helpers/types.ts b/packages/tests/src/_jest/helpers/types.ts index 62935f3a6..30c0ce84a 100644 --- a/packages/tests/src/_jest/helpers/types.ts +++ b/packages/tests/src/_jest/helpers/types.ts @@ -9,7 +9,7 @@ import type { RollupOptions } from 'rollup'; import type { Configuration as Configuration4 } from 'webpack4'; import type { Configuration } from 'webpack5'; -export type BundlerOverrides = { +export type BundlerOptionsOverrides = { rollup?: Partial; vite?: Partial; esbuild?: Partial; @@ -18,6 +18,10 @@ export type BundlerOverrides = { webpack4?: Partial; }; +export type BundlerOverrides = + | BundlerOptionsOverrides + | ((workingDir: string) => BundlerOptionsOverrides); + export type Bundler = { name: BundlerFullName; // TODO: Better type this without "any". @@ -27,6 +31,10 @@ export type Bundler = { }; export type CleanupFn = () => Promise; +export type CleanupEverythingFn = CleanupFn & { + errors: string[]; + workingDir: string; +}; export type BundlerRunFunction = ( seed: string, pluginOverrides: Options, diff --git a/packages/tests/src/_jest/helpers/xpackConfigs.ts b/packages/tests/src/_jest/helpers/xpackConfigs.ts index d6e16efee..877442eb8 100644 --- a/packages/tests/src/_jest/helpers/xpackConfigs.ts +++ b/packages/tests/src/_jest/helpers/xpackConfigs.ts @@ -2,7 +2,6 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { getResolvedPath } from '@dd/core/helpers'; import type { Options } from '@dd/core/types'; import { buildPluginFactory } from '@dd/factory'; import type { RspackOptions } from '@rspack/core'; @@ -13,37 +12,25 @@ import type { Configuration as Configuration5 } from 'webpack5'; import type webpack5 from 'webpack5'; import { PLUGIN_VERSIONS } from './constants'; -import { defaultDestination, defaultEntry } from './mocks'; +import { getOutDir } from './env'; +import { defaultEntry } from './mocks'; export const getBaseXpackConfig = ( - seed: string, + workingDir: string, bundlerName: string, ): Configuration5 & Configuration4 & RspackOptions => { + const outDir = getOutDir(workingDir, bundlerName); return { - entry: defaultEntry, + context: workingDir, + entry: path.resolve(workingDir, defaultEntry), mode: 'production', output: { - path: path.join(defaultDestination, seed, bundlerName), + path: outDir, filename: `[name].js`, }, devtool: 'source-map', optimization: { minimize: false, - splitChunks: { - chunks: 'initial', - minSize: 1, - minChunks: 1, - name: (...args: any[]) => { - // This is supposedly not available on rspack (based on types). - // But it is. - if (args[2]) { - return `chunk.${args[2]}`; - } - - // This is never reached. - return `chunk.shouldNeverHappen`; - }, - }, }, }; }; @@ -59,26 +46,3 @@ export const getWebpackPlugin = ( version: PLUGIN_VERSIONS.webpack, }).webpack(pluginOptions); }; - -// Webpack 4 doesn't support pnp resolution OOTB. -export const getWebpack4Entries = ( - entries: NonNullable, - cwd: string = process.cwd(), -): Configuration4['entry'] => { - const getTrueRelativePath = (filepath: string) => { - return `./${path.relative(cwd, getResolvedPath(filepath))}`; - }; - - if (typeof entries === 'string') { - return getTrueRelativePath(entries); - } - - return Object.fromEntries( - Object.entries(entries).map(([name, filepath]) => [ - name, - Array.isArray(filepath) - ? filepath.map(getTrueRelativePath) - : getTrueRelativePath(filepath), - ]), - ); -}; diff --git a/packages/tests/src/_jest/setupAfterEnv.ts b/packages/tests/src/_jest/setupAfterEnv.ts index 32e1b7dd7..c48729407 100644 --- a/packages/tests/src/_jest/setupAfterEnv.ts +++ b/packages/tests/src/_jest/setupAfterEnv.ts @@ -6,26 +6,22 @@ import console from 'console'; import nock from 'nock'; import { toBeWithinRange } from './toBeWithinRange.ts'; -import { toRepeatStringRange } from './toRepeatStringRange.ts'; import { toRepeatStringTimes } from './toRepeatStringTimes.ts'; // Extend Jest's expect with custom matchers. expect.extend({ toBeWithinRange, toRepeatStringTimes, - toRepeatStringRange, }); interface CustomMatchers { toBeWithinRange(floor: number, ceiling: number): R; - toRepeatStringTimes(st: string, occurences: number): R; - toRepeatStringRange(st: string, range: [number, number]): R; + toRepeatStringTimes(st: string | RegExp, occurences: number | [number, number]): R; } interface NonCustomMatchers { toBeWithinRange(floor: number, ceiling: number): number; - toRepeatStringTimes(st: string, occurences: number): string; - toRepeatStringRange(st: string, range: [number, number]): string; + toRepeatStringTimes(st: string | RegExp, occurences: number | [number, number]): string; } declare global { @@ -41,4 +37,5 @@ declare global { nock.disableNetConnect(); // Have a simpler, less verbose, console.log output. +// This bypasses Jest's --silent flag though. global.console = console; diff --git a/packages/tests/src/_jest/toRepeatStringRange.ts b/packages/tests/src/_jest/toRepeatStringRange.ts deleted file mode 100644 index f0b5cf863..000000000 --- a/packages/tests/src/_jest/toRepeatStringRange.ts +++ /dev/null @@ -1,35 +0,0 @@ -// 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 type { MatcherFunction } from 'expect'; - -export const toRepeatStringRange: MatcherFunction<[st: string, range: [number, number]]> = - // `st` and `occurences` get types from the line above - function toRepeatStringRange(actual, st, range) { - if (typeof actual !== 'string' || typeof st !== 'string') { - throw new TypeError('Only works with strings.'); - } - if (!Array.isArray(range) || range.length !== 2) { - throw new TypeError('Need an array of two numbers for "range".'); - } - - const { truncateString } = jest.requireActual('@dd/core/helpers'); - const result = actual.split(st).length - 1; - const pass = result <= range[1] && result >= range[0]; - - const time = (num: number) => (num > 1 ? 'times' : 'time'); - const failure = !pass - ? `\nBut got it ${this.utils.printReceived(result)} ${time(result)}.` - : '.'; - const expected = this.utils.printReceived(truncateString(actual).replace(/\n/g, ' ')); - - const message = `Expected: ${expected} -To repeat ${this.utils.printExpected(st)} -Between ${this.utils.printExpected(`${range[0]} and ${range[1]}`)} times${failure}`; - - return { - message: () => message, - pass, - }; - }; diff --git a/packages/tests/src/_jest/toRepeatStringTimes.ts b/packages/tests/src/_jest/toRepeatStringTimes.ts index 053fb3e65..d54cbbeea 100644 --- a/packages/tests/src/_jest/toRepeatStringTimes.ts +++ b/packages/tests/src/_jest/toRepeatStringTimes.ts @@ -4,29 +4,38 @@ import type { MatcherFunction } from 'expect'; -export const toRepeatStringTimes: MatcherFunction<[st: string, occurences: number]> = +export const toRepeatStringTimes: MatcherFunction< + [st: string | RegExp, occurences: number | [number, number]] +> = // `st` and `occurences` get types from the line above function toRepeatStringTimes(actual, st, occurences) { - if (typeof actual !== 'string' || typeof st !== 'string') { - throw new TypeError('Only works with strings.'); + if (typeof actual !== 'string' || (typeof st !== 'string' && !(st instanceof RegExp))) { + throw new TypeError('Only works with strings or RegExp.'); } - if (typeof occurences !== 'number') { - throw new TypeError('Need a number here.'); + if ( + typeof occurences !== 'number' && + (!Array.isArray(occurences) || occurences.length !== 2) + ) { + throw new TypeError('Need a number or an array of two numbers.'); } const { truncateString } = jest.requireActual('@dd/core/helpers'); const result = actual.split(st).length - 1; - const pass = result === occurences; + const isRange = Array.isArray(occurences); + const pass = isRange + ? result <= occurences[1] && result >= occurences[0] + : result === occurences; const time = (num: number) => (num > 1 ? 'times' : 'time'); const failure = !pass - ? `\nBut got it ${this.utils.printReceived(result)} ${time(result)}.` - : ''; + ? `\n\nBut got it ${this.utils.printReceived(result)} ${time(result)}.` + : '.'; const expected = this.utils.printReceived(truncateString(actual).replace(/\n/g, ' ')); + const expectedSt = isRange + ? `Between ${this.utils.printExpected(`${occurences[0]} and ${occurences[1]}`)} times${failure}` + : `Exactly ${this.utils.printExpected(occurences)} ${time(occurences)}${failure}`; - const message = `Expected: ${expected} -To repeat ${this.utils.printExpected(st)} -Exactly ${this.utils.printExpected(occurences)} ${time(occurences)}${failure}.`; + const message = `Expected: ${expected}\nTo repeat ${this.utils.printExpected(st)}\n${expectedSt}`; return { message: () => message, diff --git a/packages/tests/src/core/helpers.test.ts b/packages/tests/src/core/helpers.test.ts index 94f6d4313..aceac59eb 100644 --- a/packages/tests/src/core/helpers.test.ts +++ b/packages/tests/src/core/helpers.test.ts @@ -2,9 +2,20 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import type { RequestOpts } from '@dd/core/types'; -import { API_PATH, FAKE_URL, INTAKE_URL } from '@dd/tests/_jest/helpers/mocks'; +import { getEsbuildEntries } from '@dd/core/helpers'; +import type { RequestOpts, ResolvedEntry } from '@dd/core/types'; +import { + API_PATH, + FAKE_URL, + INTAKE_URL, + getContextMock, + getEsbuildMock, + mockLogger, +} from '@dd/tests/_jest/helpers/mocks'; +import type { BuildOptions } from 'esbuild'; +import { vol } from 'memfs'; import nock from 'nock'; +import path from 'path'; import { Readable } from 'stream'; import { createGzip } from 'zlib'; @@ -20,6 +31,9 @@ jest.mock('async-retry', () => { }); }); +// Use mock files. +jest.mock('fs', () => require('memfs').fs); + describe('Core Helpers', () => { describe('formatDuration', () => { test.each([ @@ -34,6 +48,165 @@ describe('Core Helpers', () => { }); }); + describe('getEsbuildEntries', () => { + beforeEach(() => { + // Emulate some fixtures. + vol.fromJSON({ + 'fixtures/main.js': '', + 'fixtures/in/main2.js': '', + 'fixtures/in/main3.js': '', + 'fixtures/main4.js': '', + }); + }); + + afterEach(() => { + vol.reset(); + }); + + const expectations: [string, BuildOptions['entryPoints'], ResolvedEntry[]][] = [ + [ + 'Array of strings', + [path.join(process.cwd(), 'fixtures/main.js')], + [ + { + original: path.join(process.cwd(), 'fixtures/main.js'), + resolved: path.join(process.cwd(), 'fixtures/main.js'), + }, + ], + ], + [ + 'Object with entry names', + { + app1: path.join(process.cwd(), 'fixtures/main.js'), + app2: path.join(process.cwd(), 'fixtures/main4.js'), + }, + [ + { + name: 'app1', + original: path.join(process.cwd(), 'fixtures/main.js'), + resolved: path.join(process.cwd(), 'fixtures/main.js'), + }, + { + name: 'app2', + original: path.join(process.cwd(), 'fixtures/main4.js'), + resolved: path.join(process.cwd(), 'fixtures/main4.js'), + }, + ], + ], + [ + 'Array of objects with in and out', + [ + { + in: 'fixtures/main.js', + out: 'outdir/main.js', + }, + ], + [ + { + original: 'fixtures/main.js', + resolved: path.join(process.cwd(), 'fixtures/main.js'), + }, + ], + ], + ['undefined', undefined, []], + [ + 'Array of strings with glob', + [path.join(process.cwd(), 'fixtures/*.js')], + [ + { + original: path.join(process.cwd(), 'fixtures/*.js'), + resolved: path.join(process.cwd(), 'fixtures/main4.js'), + }, + { + original: path.join(process.cwd(), 'fixtures/*.js'), + resolved: path.join(process.cwd(), 'fixtures/main.js'), + }, + ], + ], + [ + 'Object with entry names with glob', + { + app1: path.join(process.cwd(), 'fixtures/*.js'), + app2: path.join(process.cwd(), 'fixtures/**/*.js'), + }, + [ + { + name: 'app1', + original: path.join(process.cwd(), 'fixtures/*.js'), + resolved: path.join(process.cwd(), 'fixtures/main4.js'), + }, + { + name: 'app1', + original: path.join(process.cwd(), 'fixtures/*.js'), + resolved: path.join(process.cwd(), 'fixtures/main.js'), + }, + { + name: 'app2', + original: path.join(process.cwd(), 'fixtures/**/*.js'), + resolved: path.join(process.cwd(), 'fixtures/main4.js'), + }, + { + name: 'app2', + original: path.join(process.cwd(), 'fixtures/**/*.js'), + resolved: path.join(process.cwd(), 'fixtures/main.js'), + }, + { + name: 'app2', + original: path.join(process.cwd(), 'fixtures/**/*.js'), + resolved: path.join(process.cwd(), 'fixtures/in/main3.js'), + }, + { + name: 'app2', + original: path.join(process.cwd(), 'fixtures/**/*.js'), + resolved: path.join(process.cwd(), 'fixtures/in/main2.js'), + }, + ], + ], + [ + 'Array of objects with in and out with globs', + [ + { + in: 'fixtures/*.js', + out: 'outdir/main.js', + }, + { + in: 'fixtures/main4.js', + out: 'outdir/main4.js', + }, + ], + [ + { + original: 'fixtures/*.js', + resolved: path.join(process.cwd(), 'fixtures/main4.js'), + }, + { + original: 'fixtures/*.js', + resolved: path.join(process.cwd(), 'fixtures/main.js'), + }, + { + original: 'fixtures/main4.js', + resolved: path.join(process.cwd(), 'fixtures/main4.js'), + }, + ], + ], + ]; + test.each(expectations)( + 'Should return the right map of entrynames for "%s".', + async (name, entryPoints, entryNames) => { + const result = await getEsbuildEntries( + getEsbuildMock({ + initialOptions: { + entryPoints, + }, + }), + getContextMock(), + mockLogger, + ); + expect(result).toEqual(entryNames); + }, + ); + }); + describe('doRequest', () => { const getDataStream = () => { const gz = createGzip(); diff --git a/packages/tests/src/factory/helpers.test.ts b/packages/tests/src/factory/helpers.test.ts index 890b533f5..7cdd4e578 100644 --- a/packages/tests/src/factory/helpers.test.ts +++ b/packages/tests/src/factory/helpers.test.ts @@ -71,7 +71,7 @@ describe('Factory Helpers', () => { }); test('Should inject items for the injection plugin.', () => { - const injections: ToInjectItem[] = []; + const injections: Map = new Map(); const context = getContext({ options: defaultPluginOptions, bundlerName: 'webpack', @@ -81,7 +81,7 @@ describe('Factory Helpers', () => { }); const injectedItem: ToInjectItem = { type: 'code', value: 'injected' }; context.inject(injectedItem); - expect(injections).toEqual([injectedItem]); + expect(Array.from(injections.entries())).toEqual([[expect.any(String), injectedItem]]); }); }); diff --git a/packages/tests/src/plugins/build-report/esbuild.test.ts b/packages/tests/src/plugins/build-report/esbuild.test.ts deleted file mode 100644 index 56792b59f..000000000 --- a/packages/tests/src/plugins/build-report/esbuild.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -// 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 { getEntryNames } from '@dd/internal-build-report-plugin/esbuild'; -import { getContextMock } from '@dd/tests/_jest/helpers/mocks'; -import { vol } from 'memfs'; -import path from 'path'; - -jest.mock('fs', () => require('memfs').fs); - -describe('Build report plugin esbuild', () => { - describe('getEntrynames', () => { - beforeEach(() => { - // Emulate some fixtures. - vol.fromJSON({ - 'fixtures/main.js': '', - 'fixtures/in/main2.js': '', - 'fixtures/in/main3.js': '', - 'fixtures/main4.js': '', - }); - }); - - afterEach(() => { - vol.reset(); - }); - const expectations: [string, Parameters[0], Map][] = [ - [ - 'Array of strings', - [path.join(process.cwd(), 'fixtures/main.js')], - new Map([['fixtures/main.js', 'fixtures/main.js']]), - ], - [ - 'Object with entry names', - { - app1: path.join(process.cwd(), 'fixtures/main.js'), - app2: path.join(process.cwd(), 'fixtures/main4.js'), - }, - new Map([ - ['fixtures/main.js', 'app1'], - ['fixtures/main4.js', 'app2'], - ]), - ], - [ - 'Array of objects with in and out', - [ - { - in: 'fixtures/main.js', - out: 'outdir/main.js', - }, - ], - new Map([['fixtures/main.js', 'fixtures/main.js']]), - ], - ['undefined', undefined, new Map()], - [ - 'Array of strings with glob', - [path.join(process.cwd(), 'fixtures/*.js')], - new Map([ - ['fixtures/main.js', 'fixtures/main.js'], - ['fixtures/main4.js', 'fixtures/main4.js'], - ]), - ], - [ - 'Object with entry names with glob', - { - app1: path.join(process.cwd(), 'fixtures/*.js'), - app2: path.join(process.cwd(), 'fixtures/**/*.js'), - }, - new Map([ - ['fixtures/main.js', 'app1'], - ['fixtures/in/main2.js', 'app2'], - ['fixtures/in/main3.js', 'app2'], - ['fixtures/main.js', 'app2'], - // We expect the latest entry to take precendence. - ['fixtures/main4.js', 'app2'], - ]), - ], - [ - 'Array of objects with in and out with globs', - [ - { - in: 'fixtures/*.js', - out: 'outdir/main.js', - }, - { - in: 'fixtures/main4.js', - out: 'outdir/main4.js', - }, - ], - new Map([ - ['fixtures/main.js', 'fixtures/main.js'], - ['fixtures/main4.js', 'fixtures/main4.js'], - ]), - ], - ]; - test.each(expectations)( - 'Should return the right map of entrynames for "%s".', - (name, entryPoints, entryNames) => { - const result = getEntryNames( - entryPoints, - getContextMock({ - cwd: process.cwd(), - bundler: { - name: 'esbuild', - fullName: 'esbuild', - outDir: path.join(process.cwd(), 'outdir'), - version: '1.0.0', - }, - }), - ); - expect(result).toEqual(entryNames); - }, - ); - }); -}); diff --git a/packages/tests/src/plugins/build-report/index.test.ts b/packages/tests/src/plugins/build-report/index.test.ts index 9249b0f87..c251fd262 100644 --- a/packages/tests/src/plugins/build-report/index.test.ts +++ b/packages/tests/src/plugins/build-report/index.test.ts @@ -2,7 +2,6 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { getResolvedPath } from '@dd/core/helpers'; import type { Input, Entry, @@ -25,8 +24,11 @@ import { getComplexBuildOverrides, } from '@dd/tests/_jest/helpers/mocks'; import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers'; -import type { CleanupFn } from '@dd/tests/_jest/helpers/types'; -import { getWebpack4Entries } from '@dd/tests/_jest/helpers/xpackConfigs'; +import type { + BundlerOptionsOverrides, + CleanupEverythingFn, + CleanupFn, +} from '@dd/tests/_jest/helpers/types'; import path from 'path'; const sortFiles = (a: File | Output | Entry, b: File | Output | Entry) => { @@ -64,7 +66,7 @@ describe('Build Report Plugin', () => { describe('Basic build', () => { const bundlerOutdir: Record = {}; const buildReports: Record = {}; - let cleanup: CleanupFn; + let cleanup: CleanupEverythingFn; beforeAll(async () => { cleanup = await runBundlers(getPluginConfig(bundlerOutdir, buildReports)); @@ -76,8 +78,8 @@ describe('Build Report Plugin', () => { const expectedInput = () => expect.objectContaining({ - name: `src/_jest/fixtures/main.js`, - filepath: getResolvedPath(defaultEntry), + name: `easy_project/main.js`, + filepath: path.resolve(cleanup.workingDir, defaultEntry), dependencies: new Set(), dependents: new Set(), size: 302, @@ -90,8 +92,8 @@ describe('Build Report Plugin', () => { filepath: path.join(outDir, 'main.js'), inputs: [ expect.objectContaining({ - name: `src/_jest/fixtures/main.js`, - filepath: getResolvedPath(defaultEntry), + name: `easy_project/main.js`, + filepath: path.resolve(cleanup.workingDir, defaultEntry), dependencies: new Set(), dependents: new Set(), size: expect.any(Number), @@ -179,7 +181,7 @@ describe('Build Report Plugin', () => { // Intercept contexts to verify it at the moment they're used. const bundlerOutdir: Record = {}; const buildReports: Record = {}; - let cleanup: CleanupFn; + let cleanup: CleanupEverythingFn; beforeAll(async () => { cleanup = await runBundlers( @@ -194,8 +196,8 @@ describe('Build Report Plugin', () => { const expectedInput = (name: string) => expect.objectContaining({ - name: `src/_jest/fixtures/project/${name}.js`, - filepath: path.join(process.cwd(), `src/_jest/fixtures/project/${name}.js`), + name: `hard_project/${name}.js`, + filepath: path.join(cleanup.workingDir, `hard_project/${name}.js`), dependencies: expect.any(Array), dependents: [], size: expect.any(Number), @@ -227,12 +229,12 @@ describe('Build Report Plugin', () => { 'color-convert/route.js', 'color-name/index.js', 'escape-string-regexp/index.js', - 'src/_jest/fixtures/project/main1.js', - 'src/_jest/fixtures/project/main2.js', - 'src/_jest/fixtures/project/src/srcFile0.js', - 'src/_jest/fixtures/project/src/srcFile1.js', - 'src/_jest/fixtures/project/workspaces/app/workspaceFile0.js', - 'src/_jest/fixtures/project/workspaces/app/workspaceFile1.js', + 'hard_project/main1.js', + 'hard_project/main2.js', + 'hard_project/src/srcFile0.js', + 'hard_project/src/srcFile1.js', + 'hard_project/workspaces/app/workspaceFile0.js', + 'hard_project/workspaces/app/workspaceFile1.js', 'supports-color/browser.js', ]); }); @@ -269,7 +271,7 @@ describe('Build Report Plugin', () => { .sort(sortFiles); const entryFiles = inputs.filter((file) => - file.name.startsWith('src/_jest/fixtures/project/main'), + file.name.startsWith('hard_project/main'), ); expect(entryFiles).toEqual([expectedInput('main1'), expectedInput('main2')]); @@ -277,19 +279,19 @@ describe('Build Report Plugin', () => { test.each([ { - filename: 'src/_jest/fixtures/project/main1.js', + filename: 'hard_project/main1.js', dependencies: [ 'chalk/index.js', - 'src/_jest/fixtures/project/src/srcFile0.js', - 'src/_jest/fixtures/project/workspaces/app/workspaceFile1.js', + 'hard_project/src/srcFile0.js', + 'hard_project/workspaces/app/workspaceFile1.js', ], dependents: [], }, { - filename: 'src/_jest/fixtures/project/main2.js', + filename: 'hard_project/main2.js', dependencies: [ - 'src/_jest/fixtures/project/src/srcFile0.js', - 'src/_jest/fixtures/project/src/srcFile1.js', + 'hard_project/src/srcFile0.js', + 'hard_project/src/srcFile1.js', ], dependents: [], }, @@ -308,7 +310,7 @@ describe('Build Report Plugin', () => { 'supports-color/browser.js', ], // It should also have a single dependent which is main1. - dependents: ['src/_jest/fixtures/project/main1.js'], + dependents: ['hard_project/main1.js'], }, { filename: 'color-convert/route.js', @@ -553,7 +555,7 @@ describe('Build Report Plugin', () => { beforeAll(async () => { const entries = await generateProject(2, 500); - const bundlerOverrides = { + const bundlerOverrides: BundlerOptionsOverrides = { rollup: { input: entries, }, @@ -565,10 +567,7 @@ describe('Build Report Plugin', () => { }, // Mode production makes the build waaaaayyyyy too slow. webpack5: { mode: 'none', entry: entries }, - webpack4: { - mode: 'none', - entry: getWebpack4Entries(entries), - }, + webpack4: { mode: 'none', entry: entries }, }; cleanup = await runBundlers( getPluginConfig(bundlerOutdir, buildReports, { logLevel: 'error', telemetry: {} }), diff --git a/packages/tests/src/plugins/bundler-report/index.test.ts b/packages/tests/src/plugins/bundler-report/index.test.ts index bfedc1fc2..44b25101b 100644 --- a/packages/tests/src/plugins/bundler-report/index.test.ts +++ b/packages/tests/src/plugins/bundler-report/index.test.ts @@ -2,15 +2,16 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import type { BundlerReport, Options } from '@dd/core/types'; -import { defaultDestination, defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks'; +import type { BundlerReport, GlobalContext, Options } from '@dd/core/types'; +import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks'; import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers'; -import type { CleanupFn } from '@dd/tests/_jest/helpers/types'; +import type { CleanupEverythingFn } from '@dd/tests/_jest/helpers/types'; describe('Bundler Report', () => { // Intercept contexts to verify it at the moment they're used. const bundlerReports: Record = {}; - let cleanup: CleanupFn; + const contexts: Record> = {}; + let cleanup: CleanupEverythingFn; beforeAll(async () => { const pluginConfig: Options = { ...defaultPluginOptions, @@ -22,6 +23,9 @@ describe('Bundler Report', () => { name: 'custom-plugin', writeBundle() { const config = context.bundler.rawConfig; + contexts[bundlerName] = { + cwd: context.cwd, + }; bundlerReports[bundlerName] = JSON.parse( JSON.stringify({ ...context.bundler, @@ -48,7 +52,7 @@ describe('Bundler Report', () => { const report = bundlerReports[name]; const outDir = report.outDir; - const expectedOutDir = new RegExp(`^${defaultDestination}/[^/]+/${name}$`); + const expectedOutDir = new RegExp(`^${cleanup.workingDir}/[^/]+/${name}$`); expect(outDir).toMatch(expectedOutDir); }); @@ -59,5 +63,9 @@ describe('Bundler Report', () => { expect(rawConfig).toBeDefined(); expect(rawConfig).toEqual(expect.any(Object)); }); + + test('Should have the right cwd.', () => { + expect(contexts[name].cwd).toBe(cleanup.workingDir); + }); }); }); diff --git a/packages/tests/src/plugins/injection/helpers.test.ts b/packages/tests/src/plugins/injection/helpers.test.ts index 76ce46203..e7aec9b24 100644 --- a/packages/tests/src/plugins/injection/helpers.test.ts +++ b/packages/tests/src/plugins/injection/helpers.test.ts @@ -2,12 +2,13 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import type { ToInjectItem } from '@dd/core/types'; +import { InjectPosition, type ToInjectItem } from '@dd/core/types'; import { processInjections, processItem, processLocalFile, processDistantFile, + getInjectedValue, } from '@dd/internal-injection-plugin/helpers'; import { mockLogger } from '@dd/tests/_jest/helpers/mocks'; import { vol } from 'memfs'; @@ -39,13 +40,13 @@ const nonExistingDistantFile: ToInjectItem = { describe('Injection Plugin Helpers', () => { let nockScope: nock.Scope; - beforeEach(() => { + beforeEach(async () => { nockScope = nock('https://example.com') .get('/distant-file.js') .reply(200, distantFileContent); // Emulate some fixtures. vol.fromJSON({ - [existingFile.value]: localFileContent, + [await getInjectedValue(existingFile)]: localFileContent, }); }); @@ -55,18 +56,28 @@ describe('Injection Plugin Helpers', () => { describe('processInjections', () => { test('Should process injections without throwing.', async () => { - const items: ToInjectItem[] = [ - code, - existingFile, - nonExistingFile, - existingDistantFile, - nonExistingDistantFile, - ]; + const items: Map = new Map([ + ['code', code], + ['existingFile', existingFile], + ['nonExistingFile', nonExistingFile], + ['existingDistantFile', existingDistantFile], + ['nonExistingDistantFile', nonExistingDistantFile], + ]); - const expectResult = expect(processInjections(items, mockLogger)).resolves; + const prom = processInjections(items, mockLogger); + const expectResult = expect(prom).resolves; await expectResult.not.toThrow(); - await expectResult.toEqual([codeContent, localFileContent, distantFileContent]); + + const results = await prom; + expect(Array.from(results.entries())).toEqual([ + ['code', { position: InjectPosition.BEFORE, value: codeContent }], + ['existingFile', { position: InjectPosition.BEFORE, value: localFileContent }], + [ + 'existingDistantFile', + { position: InjectPosition.BEFORE, value: distantFileContent }, + ], + ]); expect(nockScope.isDone()).toBe(true); }); @@ -139,8 +150,7 @@ describe('Injection Plugin Helpers', () => { expectation: localFileContent, }, ])('Should process local file $description.', async ({ value, expectation }) => { - const item: ToInjectItem = { type: 'file', value }; - const expectResult = expect(processLocalFile(item)).resolves; + const expectResult = expect(processLocalFile(value)).resolves; await expectResult.not.toThrow(); await expectResult.toEqual(expectation); @@ -154,12 +164,9 @@ describe('Injection Plugin Helpers', () => { .delay(10) .reply(200, 'delayed distant file content'); - const item: ToInjectItem = { - type: 'file', - value: 'https://example.com/delayed-distant-file.js', - }; - - await expect(processDistantFile(item, 1)).rejects.toThrow('Timeout'); + await expect( + processDistantFile('https://example.com/delayed-distant-file.js', 1), + ).rejects.toThrow('Timeout'); }); }); }); diff --git a/packages/tests/src/plugins/injection/index.test.ts b/packages/tests/src/plugins/injection/index.test.ts index d15e006f5..c8d2724ba 100644 --- a/packages/tests/src/plugins/injection/index.test.ts +++ b/packages/tests/src/plugins/injection/index.test.ts @@ -2,7 +2,10 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import type { Options } from '@dd/core/types'; +import { outputFileSync } from '@dd/core/helpers'; +import type { Assign, BundlerFullName, Options, ToInjectItem } from '@dd/core/types'; +import { InjectPosition } from '@dd/core/types'; +import { AFTER_INJECTION, BEFORE_INJECTION } from '@dd/internal-injection-plugin/constants'; import { debugFilesPlugins, getComplexBuildOverrides, @@ -10,160 +13,397 @@ import { } from '@dd/tests/_jest/helpers/mocks'; import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers'; import type { CleanupFn } from '@dd/tests/_jest/helpers/types'; +import { header, licenses } from '@dd/tools/commands/oss/templates'; import { execute } from '@dd/tools/helpers'; import { readFileSync, writeFileSync } from 'fs'; import { glob } from 'glob'; import nock from 'nock'; import path from 'path'; +const FAKE_FILE_PREFIX = 'fake-file-to-inject-'; +// Files that we will execute part of the test. +const FILES = ['main.js', 'app1.js', 'app2.js'] as const; +const DOMAIN = 'https://example.com'; + +type ExpectedValues = [string | RegExp, number | [number, number]]; +type BaseExpectation = { + name: string; + logs?: Record; + content: ExpectedValues; +}; +type EasyExpectation = Assign; +type HardExpectation = Assign< + BaseExpectation, + { logs?: { 'app1.js': ExpectedValues; 'app2.js': ExpectedValues } } +>; +type BuildState = { + outdir?: string; + content?: string; + // Separate logs based on executed file. + logs?: Partial>; +}; +type File = (typeof FILES)[number]; +enum ContentType { + CODE = 'code', + LOCAL = 'local file', + DISTANT = 'distant file', +} +enum Position { + BEFORE = 'before', + MIDDLE = 'middle', + AFTER = 'after', +} + +const getLog = (type: ContentType, position: Position) => { + const positionString = `in ${position}`; + const contentString = `Hello injection from ${type}`; + return `${contentString} ${positionString}.`; +}; + +const getContent = (type: ContentType, position: Position) => { + return `console.log("${getLog(type, position)}");`; +}; + +const getFileUrl = (position: Position) => { + return `/${FAKE_FILE_PREFIX}${position}.js`; +}; + +const escapeStringForRegExp = (str: string) => + str + // Escape sensible chars in RegExps. + .replace(/([().[\]])/g, '\\$1') + // Replace quotes to allow for both single and double quotes. + .replace(/["']/g, `(?:"|')`); + describe('Injection Plugin', () => { - const distantFileLog = 'Hello injection from distant file.'; - const distantFileContent = `console.log("${distantFileLog}");`; - const localFileLog = 'Hello injection from local file.'; - const localFileContent = `console.log("${localFileLog}");`; - const codeLog = 'Hello injection from code.'; - const codeContent = `console.log("${codeLog}");`; - let outdirs: Record = {}; - - const expectations = [ - { type: 'some string', content: codeContent, log: codeLog }, - { type: 'a local file', content: localFileContent, log: localFileLog }, - { type: 'a distant file', content: distantFileContent, log: distantFileLog }, + // This is the string we log in our entry files + // easy_project/src/main.js and hard_project/src/main1.js. + const normalLog = 'Hello World!'; + + // Prepare a special injection where we use imports in MIDDLE. + const specialLog: string = 'Hello injection with colors from code in middle.'; + + // List of expectations for each type of tests. + const noMarkers: BaseExpectation[] = [ + { + name: 'No BEFORE_INJECTION markers in easy build', + content: [BEFORE_INJECTION, 0], + }, + { + name: 'No AFTER_INJECTION markers in easy build', + content: [AFTER_INJECTION, 0], + }, + ]; + const easyWithoutInjections: EasyExpectation[] = [ + { + name: 'Normal log in easy build', + logs: { + 'main.js': [normalLog, 1], + }, + content: [`console.log("${normalLog}");`, 1], + }, + ...noMarkers, + ]; + const hardWithoutInjections: HardExpectation[] = [ + { + name: 'Normal log in hard build', + logs: { + 'app1.js': [normalLog, 1], + 'app2.js': [normalLog, 0], + }, + // Using only normalLog here, as imports and function names (console.log, chalk) + // are probably re-written and mangled by the bundlers. + content: [normalLog, 1], + }, + ...noMarkers, + ]; + const easyWithInjections: EasyExpectation[] = [ + // We have the same expectation on the normalLog which is not due to injections. + easyWithoutInjections[0], + { + name: '[middle] code injection with imports in easy build', + logs: { + 'main.js': [specialLog, 1], + }, + // Using only 'specialLog' here, as imports and function names (console.log, chalk) + // are probably re-written and mangled by the bundlers. + content: [specialLog, 1], + }, + ]; + const hardWithInjections: HardExpectation[] = [ + // We have the same expectation on the normalLog which is not due to injections. + hardWithoutInjections[0], + { + name: '[middle] code injection with imports in hard build', + logs: { + 'app1.js': [specialLog, 1], + 'app2.js': [specialLog, 1], + }, + // Using only 'specialLog' here, as imports and function names (console.log, chalk) + // are probably re-written and mangled by the bundlers. + // Also, we don't know exactly how each bundler will concatenate the files. + // Since we have two entries here, we can expect the content + // to be repeated at least once and at most twice. + content: [specialLog, [1, 2]], + }, ]; - const customPlugins: Options['customPlugins'] = (opts, context) => { - context.inject({ - type: 'file', - value: 'https://example.com/distant-file.js', - }); - context.inject({ - type: 'file', - value: './src/_jest/fixtures/file-to-inject.js', - }); - context.inject({ + const toInjectItems: ToInjectItem[] = [ + // Add a special case of import to confirm this is working as expected in the middle of the code. + { type: 'code', - value: codeContent, - }); + value: `import chalk from 'chalk';\nconsole.log(chalk.bold.red('${specialLog}'));\n`, + position: InjectPosition.MIDDLE, + }, + ]; - return [ - { - name: 'get-outdirs', - writeBundle() { - // Store the seeded outdir to inspect the produced files. - outdirs[context.bundler.fullName] = context.bundler.outDir; - - // Add a package.json file to the esm builds. - if (['esbuild'].includes(context.bundler.fullName)) { - writeFileSync( - path.resolve(context.bundler.outDir, 'package.json'), - '{ "type": "module" }', - ); - } - }, - }, - ...debugFilesPlugins(context), - ]; - }; + // Build expectations and mock injections. + for (const type of Object.values(ContentType)) { + const injectType = type === ContentType.CODE ? 'code' : 'file'; + for (const position of Object.values(Position)) { + const positionType = + position === Position.BEFORE + ? InjectPosition.BEFORE + : position === Position.MIDDLE + ? InjectPosition.MIDDLE + : InjectPosition.AFTER; - describe('Basic build', () => { - let nockScope: nock.Scope; - let cleanup: CleanupFn; + const injectionLog = getLog(type, position); + const injectionContent = getContent(type, position); + const injection: ToInjectItem = { + type: injectType, + value: injectionContent, + position: positionType, + }; - beforeAll(async () => { - nockScope = nock('https://example.com') - .get('/distant-file.js') - .times(BUNDLERS.length) - .reply(200, distantFileContent); + // Fill in the expectations for each type of test. + hardWithInjections.push({ + name: `[${position}] ${type} injection in hard build`, + logs: { + 'app1.js': [injectionLog, 1], + 'app2.js': [injectionLog, 1], + }, + content: [injectionContent, [1, 2]], + }); - cleanup = await runBundlers( - { - customPlugins, + easyWithInjections.push({ + name: `[${position}] ${type} injection in easy build`, + logs: { + 'main.js': [injectionLog, 1], }, - getNodeSafeBuildOverrides(), - ); - }); + content: [injectionContent, 1], + }); - afterAll(async () => { - outdirs = {}; - nock.cleanAll(); - await cleanup(); - }); + if (type === ContentType.DISTANT) { + injection.value = `${DOMAIN}${getFileUrl(position)}`; + } else if (type === ContentType.LOCAL) { + injection.value = `.${getFileUrl(position)}`; + } - test('Should have requested the distant file for each bundler.', () => { - expect(nockScope.isDone()).toBe(true); - }); + toInjectItems.push(injection); + } + } - describe.each(BUNDLERS)('$name | $version', ({ name }) => { - let programOutput: string; - beforeAll(async () => { - // Test the actual bundled file too. - const result = await execute('node', [path.resolve(outdirs[name], 'main.js')]); - programOutput = result.stdout; - }); + // Create a custom plugin to inject the files/codes into the build, store some states and tweak some output. + const getPlugins = + ( + injections: ToInjectItem[] = [], + buildStates: Partial>, + ): Options['customPlugins'] => + (opts, context) => { + for (const injection of injections) { + context.inject(injection); + } - test.each(expectations)('Should inject $type once.', async ({ content, log }) => { - const files = glob.sync(path.resolve(outdirs[name], '*.{js,mjs}')); - const fullContent = files.map((file) => readFileSync(file, 'utf8')).join('\n'); + return [ + { + name: 'get-outdirs', + writeBundle() { + // Store the seeded outdir to inspect the produced files. + const buildState: BuildState = buildStates[context.bundler.fullName] || {}; + buildState.outdir = context.bundler.outDir; + buildStates[context.bundler.fullName] = buildState; - // We have a single entry, so the content should be repeated only once. - expect(fullContent).toRepeatStringTimes(content, 1); - // Verify the program output from the bundled project. - expect(programOutput).toRepeatStringTimes(log, 1); - }); - }); + // Add a package.json file to the esm builds. + if (['esbuild'].includes(context.bundler.fullName)) { + writeFileSync( + path.resolve(context.bundler.outDir, 'package.json'), + '{ "type": "module" }', + ); + } + }, + }, + ...debugFilesPlugins(context), + ]; + }; + + // Define our tests. + const tests: { + name: string; + overrides: Parameters[1]; + positions: Position[]; + injections: [ToInjectItem[], number]; + expectations: (EasyExpectation | HardExpectation)[]; + }[] = [ + { + name: 'Easy build without injections', + overrides: (workingDir: string) => getNodeSafeBuildOverrides(workingDir), + positions: [], + injections: [[], 0], + expectations: easyWithoutInjections, + }, + { + name: 'Hard build without injections', + overrides: (workingDir: string) => + getNodeSafeBuildOverrides(workingDir, getComplexBuildOverrides()), + positions: [], + injections: [[], 0], + expectations: hardWithoutInjections, + }, + { + name: 'Easy build with injections', + overrides: (workingDir: string) => getNodeSafeBuildOverrides(workingDir), + positions: Object.values(Position), + injections: [toInjectItems, 10], + expectations: easyWithInjections, + }, + { + name: 'Hard build with injections', + overrides: (workingDir: string) => + getNodeSafeBuildOverrides(workingDir, getComplexBuildOverrides()), + positions: Object.values(Position), + injections: [toInjectItems, 10], + expectations: hardWithInjections, + }, + ]; + + beforeAll(() => { + // Prepare mock files. + for (const position of Object.values(Position)) { + // NOTE: These files should already exist and have the correct content. + // It is just to confirm we keep the same content. + // We can't use memfs because bundlers, which read the files, runs within "jest.isolateModulesAsync" + // and don't have access to the same memfs' file system. + const fileContent = `${header(licenses.mit.name)}\n${getContent(ContentType.LOCAL, position)}`; + outputFileSync(`./src/_jest/fixtures${getFileUrl(position)}`, fileContent); + } }); - describe('Complex build', () => { + describe.each(tests)('$name', ({ overrides, positions, injections, expectations }) => { let nockScope: nock.Scope; let cleanup: CleanupFn; + let buildStates: Partial> = {}; beforeAll(async () => { - nockScope = nock('https://example.com') - .get('/distant-file.js') - .times(BUNDLERS.length) - .reply(200, distantFileContent); + nockScope = nock(DOMAIN); + + // Prepare mock routes. + for (const position of positions) { + // Add mock route to file. + nockScope + .get(getFileUrl(position)) + .times(BUNDLERS.length) + .reply(200, getContent(ContentType.DISTANT, position)); + } cleanup = await runBundlers( - { - customPlugins, - }, - getNodeSafeBuildOverrides(getComplexBuildOverrides()), + { customPlugins: getPlugins(injections[0], buildStates) }, + overrides, ); - }); + + // Execute the builds and store some state. + const proms: Promise[] = []; + for (const bundler of BUNDLERS) { + const buildState = buildStates[bundler.name]; + const outdir = buildState?.outdir; + + // This will be caught in the tests for each bundler. + if (!outdir || !buildState) { + continue; + } + + const builtFiles = glob.sync(path.resolve(outdir, '*.{js,mjs}')); + + // Only execute files we identified as entries. + const filesToRun: File[] = builtFiles + .map((file) => path.basename(file) as File) + .filter((basename) => FILES.includes(basename)); + + // Run the files through node to confirm they don't crash and assert their logs. + proms.push( + ...filesToRun.map(async (file) => { + const result = await execute('node', [path.resolve(outdir, file)]); + buildState.logs = buildState.logs || {}; + buildState.logs[file] = result.stdout; + }), + ); + + // Store the content of the built files to assert the injections. + buildState.content = builtFiles + .map((file) => readFileSync(file, 'utf8')) + .join('\n'); + } + + await Promise.all(proms); + // Webpack can be slow to build... + }, 100000); afterAll(async () => { - outdirs = {}; + buildStates = {}; nock.cleanAll(); await cleanup(); }); - test('Should have requested the distant file for each bundler.', () => { + test('Should have the correct test environment.', () => { + expect(injections[0]).toHaveLength(injections[1]); + + // We should have called everything we've mocked for. expect(nockScope.isDone()).toBe(true); }); describe.each(BUNDLERS)('$name | $version', ({ name }) => { - let programOutput1: string; - let programOutput2: string; - beforeAll(async () => { - // Test the actual bundled file too. - const result1 = await execute('node', [path.resolve(outdirs[name], 'app1.js')]); - programOutput1 = result1.stdout; - const result2 = await execute('node', [path.resolve(outdirs[name], 'app2.js')]); - programOutput2 = result2.stdout; - }); + let buildState: BuildState; - test.each(expectations)('Should inject $type.', ({ content, log }) => { - const files = glob.sync(path.resolve(outdirs[name], '*.{js,mjs}')); - const fullContent = files.map((file) => readFileSync(file, 'utf8')).join('\n'); - - // We don't know exactly how each bundler will concattenate the files. - // Since we have two entries here, we can expect the content - // to be repeated at least once and at most twice. - expect(fullContent).toRepeatStringRange(content, [1, 2]); - // Verify the program output from the bundled project. - expect(programOutput1).toRepeatStringTimes(log, 1); - expect(programOutput2).toRepeatStringTimes(log, 1); + test('Should have a buildState.', () => { + buildState = buildStates[name]!; + expect(buildState).toBeDefined(); + expect(buildState.outdir).toEqual(expect.any(String)); + expect(buildState.logs).toEqual(expect.any(Object)); + expect(buildState.content).toEqual(expect.any(String)); }); + + describe.each(expectations)( + '$name', + ({ content: [expectedContent, contentOccurencies], logs }) => { + test('Should have the expected content in the bundles.', () => { + const content = buildState.content; + const expectation = + expectedContent instanceof RegExp + ? expectedContent + : new RegExp(escapeStringForRegExp(expectedContent)); + + expect(content).toRepeatStringTimes(expectation, contentOccurencies); + }); + + if (!logs) { + return; + } + + test('Should have output the expected logs from execution.', () => { + const logExpectations = Object.entries(logs); + for (const [file, [expectedLog, logOccurencies]] of logExpectations) { + const stateLogs = buildState.logs?.[file as File]; + const expectation = + expectedLog instanceof RegExp + ? expectedLog + : new RegExp(escapeStringForRegExp(expectedLog)); + + expect(stateLogs).toBeDefined(); + expect(stateLogs).toRepeatStringTimes(expectation, logOccurencies); + } + }); + }, + ); }); }); }); diff --git a/packages/tests/src/plugins/telemetry/index.test.ts b/packages/tests/src/plugins/telemetry/index.test.ts index 51bd84b27..1051fb124 100644 --- a/packages/tests/src/plugins/telemetry/index.test.ts +++ b/packages/tests/src/plugins/telemetry/index.test.ts @@ -327,22 +327,10 @@ describe('Telemetry Universal Plugin', () => { // [name, entryNames, size, dependencies, dependents]; const modulesExpectations: [string, string[], number, number, number][] = [ - [ - 'src/_jest/fixtures/project/workspaces/app/workspaceFile0.js', - ['app1', 'app2'], - 30042, - 0, - 2, - ], - [ - 'src/_jest/fixtures/project/workspaces/app/workspaceFile1.js', - ['app1', 'app2'], - 4600, - 1, - 2, - ], - ['src/_jest/fixtures/project/src/srcFile1.js', ['app2'], 2237, 2, 1], - ['src/_jest/fixtures/project/src/srcFile0.js', ['app1', 'app2'], 13248, 1, 3], + ['hard_project/workspaces/app/workspaceFile0.js', ['app1', 'app2'], 30042, 0, 2], + ['hard_project/workspaces/app/workspaceFile1.js', ['app1', 'app2'], 4600, 1, 2], + ['hard_project/src/srcFile1.js', ['app2'], 2237, 2, 1], + ['hard_project/src/srcFile0.js', ['app1', 'app2'], 13248, 1, 3], ['escape-string-regexp/index.js', ['app1'], 226, 0, 1], ['color-name/index.js', ['app1'], 4617, 0, 1], ['color-convert/conversions.js', ['app1'], 16850, 1, 2], @@ -353,8 +341,8 @@ describe('Telemetry Universal Plugin', () => { ['chalk/templates.js', ['app1'], 3133, 0, 1], // Somehow rollup and vite are not reporting the same size. ['chalk/index.js', ['app1'], expect.toBeWithinRange(6437, 6439), 4, 1], - ['src/_jest/fixtures/project/main1.js', ['app1'], 462, 3, 0], - ['src/_jest/fixtures/project/main2.js', ['app2'], 337, 2, 0], + ['hard_project/main1.js', ['app1'], 462, 3, 0], + ['hard_project/main2.js', ['app2'], 337, 2, 0], ]; describe.each(modulesExpectations)( diff --git a/packages/tests/src/tools/src/rollupConfig.test.ts b/packages/tests/src/tools/src/rollupConfig.test.ts index f7fffa49a..cc83b9597 100644 --- a/packages/tests/src/tools/src/rollupConfig.test.ts +++ b/packages/tests/src/tools/src/rollupConfig.test.ts @@ -6,7 +6,7 @@ import { datadogEsbuildPlugin } from '@datadog/esbuild-plugin'; import { datadogRollupPlugin } from '@datadog/rollup-plugin'; import { datadogRspackPlugin } from '@datadog/rspack-plugin'; import { datadogVitePlugin } from '@datadog/vite-plugin'; -import { formatDuration, rm } from '@dd/core/helpers'; +import { formatDuration, getUniqueId, rm } from '@dd/core/helpers'; import type { BundlerFullName, Options } from '@dd/core/types'; import { getEsbuildOptions, @@ -14,11 +14,11 @@ import { getWebpack4Options, getWebpack5Options, } from '@dd/tests/_jest/helpers/configBundlers'; -import { BUNDLER_VERSIONS, NO_CLEANUP } from '@dd/tests/_jest/helpers/constants'; +import { BUNDLER_VERSIONS } from '@dd/tests/_jest/helpers/constants'; +import { getOutDir, prepareWorkingDir } from '@dd/tests/_jest/helpers/env'; import { API_PATH, FAKE_URL, - defaultDestination, defaultEntries, getComplexBuildOverrides, getFullPluginConfig, @@ -32,7 +32,7 @@ import { runWebpack5, } from '@dd/tests/_jest/helpers/runBundlers'; import type { CleanupFn } from '@dd/tests/_jest/helpers/types'; -import { getWebpack4Entries, getWebpackPlugin } from '@dd/tests/_jest/helpers/xpackConfigs'; +import { getWebpackPlugin } from '@dd/tests/_jest/helpers/xpackConfigs'; import { ROOT } from '@dd/tools/constants'; import { bgYellow, execute, green } from '@dd/tools/helpers'; import type { BuildOptions } from 'esbuild'; @@ -118,7 +118,6 @@ const getPackageDestination = (bundlerName: string) => { describe('Bundling', () => { let bundlerVersions: Partial> = {}; let processErrors: string[] = []; - const seededFolders: string[] = []; const pluginConfig = getFullPluginConfig({ logLevel: 'error', customPlugins: (opts, context) => [ @@ -143,12 +142,12 @@ describe('Bundling', () => { // Duplicate the webpack plugin to have one with webpack 4 and one with webpack 5. const webpack5Plugin = getPackageDestination('webpack'); const webpack4Plugin = path.resolve(webpack5Plugin, 'index4.js'); - // Create a new file that will use webpack4. + // Create a new file that will use webpack4 instead of webpack. fs.writeFileSync( webpack4Plugin, fs .readFileSync(path.resolve(webpack5Plugin, 'index.js'), { encoding: 'utf-8' }) - .replace("require('webpack')", "require('webpack4')"), + .replace(/require\(('|")webpack("|')\)/g, "require('webpack4')"), ); // Make the mocks target the built packages. @@ -182,12 +181,17 @@ describe('Bundling', () => { .reply(200, {}); // Intercept Node errors. (Especially DeprecationWarnings in the case of Webpack5). - const actualConsoleError = console.error; + const actualConsoleError = jest.requireActual('console').error; // Filter out the errors we expect. const ignoredErrors = [ + // Used for Jest runtime in "yarn test". 'ExperimentalWarning: VM Modules', + // Used in our sourcemaps sender, to build a stream of our zipped sourcemaps. 'ExperimentalWarning: buffer.File', + // Used in Unplugin's xpack loaders. + 'fs.rmdir(path, { recursive: true })', ]; + // NOTE: this will trigger only once per session, per error. jest.spyOn(console, 'error').mockImplementation((err) => { if (!ignoredErrors.some((e) => err.includes(e))) { @@ -205,97 +209,69 @@ describe('Bundling', () => { afterAll(async () => { nock.cleanAll(); - if (NO_CLEANUP) { - return; - } - console.log('[rollupConfig | Bundling] Cleaning up seeded folders.\n', seededFolders); - await Promise.all(seededFolders.map((folder) => rm(folder))); }); const nameSize = Math.max(...BUNDLERS.map((bundler) => bundler.name.length)) + 1; - const TIMESTAMP = Date.now(); describe.each(BUNDLERS)('Bundler: $name', (bundler) => { - test('Should not throw on a easy project.', async () => { - const projectName = 'easy'; - const timeId = `[ ${green(bundler.name.padEnd(nameSize))}] ${green(projectName)} run`; - console.time(timeId); - - const SEED = `${TIMESTAMP}-${projectName}-${jest.getSeed()}`; - const rootDir = path.resolve(defaultDestination, SEED); - const outdir = path.resolve(rootDir, bundler.name); - seededFolders.push(rootDir); - const bundlerConfig = bundler.config( - SEED, - pluginConfig, - getNodeSafeBuildOverrides()[bundler.name], - ); - - if (!bundlerConfig) { - throw new Error(`Missing bundlerConfig for ${bundler.name}.`); - } - - const bundlerConfigOverrides = - bundler.name === 'vite' ? bundlerConfig.build.rollupOptions : bundlerConfig; - const { errors } = await bundler.run(SEED, pluginConfig, bundlerConfigOverrides); - expect(errors).toHaveLength(0); - - // Test the actual bundled file too. - await expect(execute('node', [path.resolve(outdir, 'main.js')])).resolves.not.toThrow(); - - // It should use the correct version of the bundler. - // This is to ensure our test is running in the right conditions. - expect(bundlerVersions[bundler.name]).toBe(BUNDLER_VERSIONS[bundler.name]); - - // It should not have printed any error. - expect(processErrors).toHaveLength(0); - - console.timeEnd(timeId); - - // Adding some timeout because webpack is SLOW. - }, 10000); - - test('Should not throw on a hard project.', async () => { - const projectName = 'hard'; - const timeId = `[ ${green(bundler.name.padEnd(nameSize))}] ${green(projectName)} run`; - console.time(timeId); - - const SEED = `${TIMESTAMP}-${projectName}-${jest.getSeed()}`; - const rootDir = path.resolve(defaultDestination, SEED); - const outdir = path.resolve(rootDir, bundler.name); - seededFolders.push(rootDir); - const bundlerConfig = bundler.config( - SEED, - pluginConfig, - getNodeSafeBuildOverrides(getComplexBuildOverrides())[bundler.name], - ); + test.each<{ projectName: string; filesToRun: string[] }>([ + { projectName: 'easy', filesToRun: ['main.js'] }, + { projectName: 'hard', filesToRun: ['app1.js', 'app2.js'] }, + ])( + 'Should not throw on $projectName project.', + async ({ projectName, filesToRun }) => { + const timeId = `[ ${green(bundler.name.padEnd(nameSize))}] ${green(projectName)} run`; + console.time(timeId); + + const SEED = `${jest.getSeed()}.${projectName}.${getUniqueId()}`; + const rootDir = await prepareWorkingDir(SEED); + const overrides = getNodeSafeBuildOverrides( + rootDir, + projectName === 'hard' ? getComplexBuildOverrides() : {}, + ); + const outdir = getOutDir(rootDir, bundler.name); + const bundlerConfig = bundler.config( + rootDir, + pluginConfig, + overrides[bundler.name], + ); + + if (!bundlerConfig) { + throw new Error(`Missing bundlerConfig for ${bundler.name}.`); + } - if (!bundlerConfig) { - throw new Error(`Missing bundlerConfig for ${bundler.name}.`); - } + // Our vite run function has a slightly different signature due to how it sets up its bundling. + const bundlerConfigOverrides = + bundler.name === 'vite' ? bundlerConfig.build.rollupOptions : bundlerConfig; - // Vite only overrides its options.build.rollupOptions. - const bundlerConfigOverrides = - bundler.name === 'vite' ? bundlerConfig.build.rollupOptions : bundlerConfig; + const { errors } = await bundler.run(rootDir, pluginConfig, bundlerConfigOverrides); + expect(errors).toHaveLength(0); - const { errors } = await bundler.run(SEED, pluginConfig, bundlerConfigOverrides); - expect(errors).toHaveLength(0); + // Test the actual bundled files too. + await Promise.all( + filesToRun + .map((f) => path.resolve(outdir, f)) + .map((file) => expect(execute('node', [file])).resolves.not.toThrow()), + ); - // Test the actual bundled file too. - await expect(execute('node', [path.resolve(outdir, 'app1.js')])).resolves.not.toThrow(); - await expect(execute('node', [path.resolve(outdir, 'app2.js')])).resolves.not.toThrow(); + // It should use the correct version of the bundler. + // This is to ensure our test is running in the right conditions. + expect(bundlerVersions[bundler.name]).toBe(BUNDLER_VERSIONS[bundler.name]); - // It should use the correct version of the bundler. - // This is to ensure our test is running in the right conditions. - expect(bundlerVersions[bundler.name]).toBe(BUNDLER_VERSIONS[bundler.name]); + // It should not have printed any error. + expect(processErrors).toHaveLength(0); - // It should not have printed any error. - expect(processErrors).toHaveLength(0); + // Clean working dir. + if (!process.env.NO_CLEANUP) { + await rm(rootDir); + } - console.timeEnd(timeId); + console.timeEnd(timeId); - // Adding some timeout because webpack is SLOW. - }, 10000); + // Adding some timeout because webpack is SLOW. + }, + 10000, + ); }); test('Should not throw on a weird project.', async () => { @@ -303,21 +279,20 @@ describe('Bundling', () => { const timeId = `[ ${green('esbuild + webpack + rspack')}] ${green(projectName)} run`; console.time(timeId); - const SEED = `${TIMESTAMP}-${projectName}-${jest.getSeed()}`; - const rootDir = path.resolve(defaultDestination, SEED); - const outdir = path.resolve(rootDir, projectName); - seededFolders.push(rootDir); - const configs = getNodeSafeBuildOverrides(getComplexBuildOverrides()); + const SEED = `${jest.getSeed()}.${getUniqueId()}`; + const rootDir = await prepareWorkingDir(SEED); - // Build esbuild somewhere temporary first. - const esbuildOutdir = path.resolve(outdir, './temp'); + const overrides = getNodeSafeBuildOverrides(rootDir, getComplexBuildOverrides()); + const esbuildOverrides = overrides.esbuild; // Configure bundlers. - const baseEsbuildConfig = getEsbuildOptions(SEED, {}, configs.esbuild); + const baseEsbuildConfig = getEsbuildOptions(rootDir, {}, esbuildOverrides); + const esbuildOutdir = baseEsbuildConfig.outdir!; + const esbuildConfig1: BuildOptions = { ...baseEsbuildConfig, - outdir: esbuildOutdir, - entryPoints: { app1: defaultEntries.app1 }, + // Only one entry, we'll build the second one in a parallel build. + entryPoints: { app1: path.resolve(rootDir, defaultEntries.app1) }, plugins: [ ...(baseEsbuildConfig.plugins || []), // Add a custom loader that will build a new file using the parent configuration. @@ -325,9 +300,9 @@ describe('Bundling', () => { name: 'custom-build-loader', setup(build) { build.onLoad({ filter: /.*\/main1\.js/ }, async ({ path: filepath }) => { - const outfile = path.resolve(esbuildOutdir, 'app1.2.js'); + const outfile = path.resolve(build.initialOptions.outdir!, 'app1.2.js'); await runEsbuild( - SEED, + rootDir, {}, { ...build.initialOptions, @@ -349,36 +324,29 @@ describe('Bundling', () => { // Add a second parallel build. const esbuildConfig2: BuildOptions = { - ...baseEsbuildConfig, - outdir: esbuildOutdir, - entryPoints: { app2: defaultEntries.app2 }, + ...getEsbuildOptions(rootDir, {}, overrides.esbuild), + entryPoints: { app2: path.resolve(rootDir, defaultEntries.app2) }, }; // Webpack triggers some deprecations warnings only when we have multi-entry entries. const webpackEntries = { - app1: [ - path.resolve(esbuildOutdir, 'app1.js'), - '@dd/tests/_jest/fixtures/project/empty.js', - ], - app2: [ - path.resolve(esbuildOutdir, 'app2.js'), - '@dd/tests/_jest/fixtures/project/empty.js', - ], + app1: [path.resolve(esbuildOutdir, 'app1.js'), path.resolve(rootDir, './empty.js')], + app2: [path.resolve(esbuildOutdir, 'app2.js'), path.resolve(rootDir, './empty.js')], }; const rspackConfig = { - ...getRspackOptions(SEED, {}, configs.rspack), + ...getRspackOptions(rootDir, {}, overrides.rspack), entry: webpackEntries, }; const webpack5Config = { - ...getWebpack5Options(SEED, {}, configs.webpack5), + ...getWebpack5Options(rootDir, {}, overrides.webpack5), entry: webpackEntries, }; const webpack4Config = { - ...getWebpack4Options(SEED, {}, configs.webpack4), - entry: getWebpack4Entries(webpackEntries), + ...getWebpack4Options(rootDir, {}, overrides.webpack4), + entry: webpackEntries, }; // Build the sequence. @@ -386,14 +354,14 @@ describe('Bundling', () => { const sequence: (() => Promise)[] = [ () => Promise.all([ - runEsbuild(SEED, pluginConfig, esbuildConfig1), - runEsbuild(SEED, pluginConfig, esbuildConfig2), + runEsbuild(rootDir, pluginConfig, esbuildConfig1), + runEsbuild(rootDir, pluginConfig, esbuildConfig2), ]), () => Promise.all([ - runWebpack5(SEED, pluginConfig, webpack5Config), - runWebpack4(SEED, pluginConfig, webpack4Config), - runRspack(SEED, pluginConfig, rspackConfig), + runWebpack5(rootDir, pluginConfig, webpack5Config), + runWebpack4(rootDir, pluginConfig, webpack4Config), + runRspack(rootDir, pluginConfig, rspackConfig), ]), ]; @@ -415,6 +383,11 @@ describe('Bundling', () => { // It should not have printed any error. expect(processErrors).toHaveLength(0); + // Clean working dir. + if (!process.env.NO_CLEANUP) { + await rm(rootDir); + } + console.timeEnd(timeId); }); }); diff --git a/packages/tools/package.json b/packages/tools/package.json index b81c6c57f..8d746da09 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -13,6 +13,7 @@ "packageManager": "yarn@4.0.2", "exports": { "./rollupConfig.mjs": "./src/rollupConfig.mjs", + "./commands/oss/templates": "./src/commands/oss/templates.ts", "./*": "./src/*.ts" }, "scripts": { diff --git a/packages/tools/src/commands/integrity/readme.ts b/packages/tools/src/commands/integrity/readme.ts index 6024f41d5..36a7a9280 100644 --- a/packages/tools/src/commands/integrity/readme.ts +++ b/packages/tools/src/commands/integrity/readme.ts @@ -24,7 +24,7 @@ import { } from '@dd/tools/helpers'; import type { Workspace } from '@dd/tools/types'; import fs from 'fs'; -import glob from 'glob'; +import { glob } from 'glob'; import { outdent } from 'outdent'; import path from 'path'; diff --git a/packages/tools/src/commands/oss/apply.ts b/packages/tools/src/commands/oss/apply.ts index 93f1d98ff..b45fb159e 100644 --- a/packages/tools/src/commands/oss/apply.ts +++ b/packages/tools/src/commands/oss/apply.ts @@ -6,7 +6,7 @@ import checkbox from '@inquirer/checkbox'; import select from '@inquirer/select'; import chalk from 'chalk'; import fs from 'fs'; -import glob from 'glob'; +import { glob } from 'glob'; import path from 'path'; import { NAME, ROOT } from '../../constants'; diff --git a/yarn.lock b/yarn.lock index 38bb9c511..fc39359dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1504,7 +1504,7 @@ __metadata: languageName: unknown linkType: soft -"@datadog/rspack-plugin@workspace:packages/published/rspack-plugin": +"@datadog/rspack-plugin@workspace:*, @datadog/rspack-plugin@workspace:packages/published/rspack-plugin": version: 0.0.0-use.local resolution: "@datadog/rspack-plugin@workspace:packages/published/rspack-plugin" dependencies: @@ -1618,6 +1618,8 @@ __metadata: "@types/node": "npm:^18" async-retry: "npm:1.3.3" chalk: "npm:2.3.1" + esbuild: "npm:0.24.0" + glob: "npm:11.0.0" typescript: "npm:5.4.3" unplugin: "npm:1.16.0" languageName: unknown @@ -1655,7 +1657,6 @@ __metadata: resolution: "@dd/internal-build-report-plugin@workspace:packages/plugins/build-report" dependencies: "@dd/core": "workspace:*" - glob: "npm:11.0.0" languageName: unknown linkType: soft @@ -1712,6 +1713,7 @@ __metadata: dependencies: "@datadog/esbuild-plugin": "workspace:*" "@datadog/rollup-plugin": "workspace:*" + "@datadog/rspack-plugin": "workspace:*" "@datadog/vite-plugin": "workspace:*" "@datadog/webpack-plugin": "workspace:*" "@dd/core": "workspace:*" @@ -9802,14 +9804,6 @@ __metadata: languageName: node linkType: hard -"project@workspace:packages/tests/src/_jest/fixtures/project": - version: 0.0.0-use.local - resolution: "project@workspace:packages/tests/src/_jest/fixtures/project" - dependencies: - chalk: "npm:2.3.1" - languageName: unknown - linkType: soft - "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1"