diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 67bde95ff..fdc77c175 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -90,7 +90,7 @@ jobs: key: cache-playwright-binaries-${{ hashFiles('yarn.lock') }} - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() && failure() }} + if: ${{ failure() }} with: name: playwright path: | diff --git a/global.d.ts b/global.d.ts index b38f6a88b..c3ac3c956 100644 --- a/global.d.ts +++ b/global.d.ts @@ -2,13 +2,43 @@ import type { Env } from '@dd/core/types'; declare global { namespace NodeJS { - interface ProcessEnv { - [key: string]: string | undefined; + interface ProcessEnv extends NodeJS.ProcessEnv { + /** + * The environment in which the plugins will execute. + * + * For instance, we only submit logs to Datadog when the environment is `production`. + */ BUILD_PLUGINS_ENV?: Env; - NO_CLEANUP?: '1'; + /** + * Defined in github actions when running in CI. + */ + CI?: '1'; + /** + * Defined in github actions when running in CI. + * + * The commit SHA that triggered the workflow. + */ + GITHUB_SHA?: string; + /** + * Run jest in silent mode. + */ + JEST_SILENT?: '1'; + /** + * To also build the plugins before running the tests when using `yarn test:unit`. + */ NEED_BUILD?: '1'; + /** + * To skip the cleanup of the temporary working dirs where we build `runBundlers()`. + */ + NO_CLEANUP?: '1'; + /** + * The list of bundlers to use in our tests. + */ REQUESTED_BUNDLERS?: string; - JEST_SILENT?: '1'; + /** + * Defined by yarn and targets the root of the project. + */ + PROJECT_CWD?: string; } } } diff --git a/package.json b/package.json index af67c1ae1..4bad748ad 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "loop-published": "yarn workspaces foreach -A --include \"@datadog/*\" --exclude \"@datadog/build-plugins\"", "loop": "yarn loop-published -pti", "oss": "yarn cli oss -d packages -l mit", + "playwright": "yarn workspace @dd/tests playwright", "publish:all": "yarn loop --no-private npm publish", "typecheck:all": "yarn workspaces foreach -Apti run typecheck", "version:all": "yarn loop-published version ${0} --immediate", diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 66fe10c75..7b78d3cb3 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -13,10 +13,13 @@ import type { RequestInit } from 'undici-types'; import type { BuildReport, + BundlerFullName, Entry, File, + GetCustomPlugins, GlobalContext, Input, + IterableElement, Logger, Output, RequestOpts, @@ -36,9 +39,12 @@ export const formatDuration = (duration: number) => { const minutes = d.getUTCMinutes(); const seconds = d.getUTCSeconds(); const milliseconds = d.getUTCMilliseconds(); - return `${days ? `${days}d ` : ''}${hours ? `${hours}h ` : ''}${minutes ? `${minutes}m ` : ''}${ - seconds ? `${seconds}s ` : '' - }${milliseconds ? `${milliseconds}ms` : ''}`.trim(); + const timeString = + `${days ? `${days}d ` : ''}${hours ? `${hours}h ` : ''}${minutes ? `${minutes}m ` : ''}${ + seconds ? `${seconds}s` : '' + }`.trim(); + // Split here so we can show 0ms in case we have a duration of 0. + return `${timeString}${!timeString || milliseconds ? ` ${milliseconds}ms` : ''}`.trim(); }; // https://esbuild.github.io/api/#glob-style-entry-points @@ -212,11 +218,18 @@ export const truncateString = ( // Is the file coming from the injection plugin? export const isInjectionFile = (filename: string) => filename.includes(INJECTED_FILE); +// From a bundler's name, is it part of the "xpack" family? +export const isXpack = (bundlerName: BundlerFullName) => + ['rspack', 'webpack4', 'webpack5', 'webpack'].includes(bundlerName); + // Replacing fs-extra with local helpers. // Delete folders recursively. export const rm = async (dir: string) => { return fsp.rm(dir, { force: true, maxRetries: 3, recursive: true }); }; +export const rmSync = (dir: string) => { + return fs.rmSync(dir, { force: true, maxRetries: 3, recursive: true }); +}; // Mkdir recursively. export const mkdir = async (dir: string) => { @@ -411,3 +424,145 @@ export const unserializeBuildReport = (report: SerializedBuildReport): BuildRepo outputs, }; }; + +// Will only prepend the cwd if not already there. +export const getAbsolutePath = (cwd: string, filepath: string) => { + if (isInjectionFile(filepath)) { + return INJECTED_FILE; + } + + if (filepath.startsWith(cwd) || path.isAbsolute(filepath)) { + return filepath; + } + return path.resolve(cwd, filepath); +}; + +// Find the highest package.json from the current directory. +export const getHighestPackageJsonDir = (currentDir: string): string | undefined => { + let highestPackage; + let current = getAbsolutePath(process.cwd(), currentDir); + let currentDepth = current.split('/').length; + while (currentDepth > 0) { + const packagePath = path.resolve(current, `package.json`); + // Check if package.json exists in the current directory. + if (fs.existsSync(packagePath)) { + highestPackage = current; + } + // Remove the last part of the path. + current = current.split('/').slice(0, -1).join('/'); + currentDepth--; + } + return highestPackage; +}; + +// From a list of path, return the nearest common directory. +export const getNearestCommonDirectory = (dirs: string[], cwd?: string) => { + const dirsToCompare = [...dirs]; + + // We include the CWD because it's part of the paths we want to compare. + if (cwd) { + dirsToCompare.push(cwd); + } + + const splitPaths = dirsToCompare.map((dir) => { + const absolutePath = getAbsolutePath(cwd || process.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 + ? // Use "|| path.sep" to cover for the [''] case. + commonParts.join(path.sep) || path.sep + : path.sep; +}; + +// Returns a customPlugin to output some debug files. +type CustomPlugins = ReturnType; +export const debugFilesPlugins = (context: GlobalContext): CustomPlugins => { + const rollupPlugin: IterableElement['rollup'] = { + writeBundle(options, bundle) { + outputJsonSync( + path.resolve(context.bundler.outDir, `output.${context.bundler.fullName}.json`), + bundle, + ); + }, + }; + + const xpackPlugin: IterableElement['webpack'] & + IterableElement['rspack'] = (compiler) => { + type Stats = Parameters[1]>[0]; + + compiler.hooks.done.tap('bundler-outputs', (stats: Stats) => { + const statsJson = stats.toJson({ + all: false, + assets: true, + children: true, + chunks: true, + chunkGroupAuxiliary: true, + chunkGroupChildren: true, + chunkGroups: true, + chunkModules: true, + chunkRelations: true, + entrypoints: true, + errors: true, + ids: true, + modules: true, + nestedModules: true, + reasons: true, + relatedAssets: true, + warnings: true, + }); + outputJsonSync( + path.resolve(context.bundler.outDir, `output.${context.bundler.fullName}.json`), + statsJson, + ); + }); + }; + + return [ + { + name: 'build-report', + enforce: 'post', + writeBundle() { + outputJsonSync( + path.resolve(context.bundler.outDir, `report.${context.bundler.fullName}.json`), + serializeBuildReport(context.build), + ); + }, + }, + { + name: 'bundler-outputs', + enforce: 'post', + esbuild: { + setup(build) { + build.onEnd((result) => { + outputJsonSync( + path.resolve( + context.bundler.outDir, + `output.${context.bundler.fullName}.json`, + ), + result.metafile, + ); + }); + }, + }, + rspack: xpackPlugin, + rollup: rollupPlugin, + vite: rollupPlugin, + webpack: xpackPlugin, + }, + ]; +}; diff --git a/packages/factory/src/helpers.ts b/packages/factory/src/helpers.ts index 2acd8e11c..ec532e5a3 100644 --- a/packages/factory/src/helpers.ts +++ b/packages/factory/src/helpers.ts @@ -118,7 +118,9 @@ export const getContext = ({ }, }; - const passedEnv: Env = (process.env.BUILD_PLUGINS_ENV as Env) || 'development'; + // Use "production" if there is no env passed. + const passedEnv: Env = (process.env.BUILD_PLUGINS_ENV as Env) || 'production'; + // Fallback to "development" if the passed env is wrong. const env: Env = ALL_ENVS.includes(passedEnv) ? passedEnv : 'development'; const context: GlobalContext = { auth: options.auth, diff --git a/packages/plugins/build-report/src/esbuild.ts b/packages/plugins/build-report/src/esbuild.ts index 041038ad4..df40510c4 100644 --- a/packages/plugins/build-report/src/esbuild.ts +++ b/packages/plugins/build-report/src/esbuild.ts @@ -2,7 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { getEsbuildEntries, isInjectionFile } from '@dd/core/helpers'; +import { getAbsolutePath, getEsbuildEntries, isInjectionFile } from '@dd/core/helpers'; import type { Logger, Entry, @@ -12,8 +12,9 @@ import type { PluginOptions, ResolvedEntry, } from '@dd/core/types'; +import path from 'path'; -import { cleanName, getAbsolutePath, getType } from './helpers'; +import { cleanName, getType } from './helpers'; // Re-index metafile data for easier access. const reIndexMeta = (obj: Record, cwd: string) => @@ -138,7 +139,7 @@ export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOpt // It has no inputs, but still relates to its entryPoint. if (output.entryPoint && !inputFiles.length) { const inputFound = - reportInputsIndexed[getAbsolutePath(cwd, output.entryPoint!)]; + reportInputsIndexed[getAbsolutePath(cwd, output.entryPoint)]; if (!inputFound) { log.debug( `Input ${output.entryPoint} not found for output ${cleanedName}`, @@ -263,9 +264,53 @@ export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOpt } for (const imported of metaFile.imports) { - const importPath = getAbsolutePath(cwd, imported.path); + const isRelative = imported.path.match(/^\.\.?\//); + const root = isRelative ? path.dirname(filePath) : cwd; + const absoluteImportPath = getAbsolutePath(root, imported.path); + + // We need to register external imports, as this is the first time we see them. + if (imported.external) { + if (isFileSupported(imported.path)) { + // If it's an absolute external import, + // we can't trust our own getAbsolutePath(). + // We can't know what its "root" could be. + const filepath = isRelative ? absoluteImportPath : imported.path; + + // But we can still add it to the report. + const inputFile: Input = references.inputs.report[filepath] || { + filepath, + name: cleanName(context, imported.path), + size: 0, + type: 'external', + dependencies: new Set(), + dependents: new Set(), + }; + + if ('dependencies' in file) { + // file is an Input, so we add the external to its dependencies, + // and we add file to the external's dependents. + inputFile.dependents.add(file); + file.dependencies.add(inputFile); + } + + if ('inputs' in file && !file.inputs.includes(inputFile)) { + // file is an Output, so we add the external to its inputs. + file.inputs.push(inputFile); + } + + if (!inputs.includes(inputFile)) { + inputs.push(inputFile); + } + + references.inputs.report[filepath] = inputFile; + allImports[inputFile.filepath] = inputFile as T; + } + // We can't follow external imports. + continue; + } + // Look for the other inputs. - getAllImports(importPath, ref, allImports); + getAllImports(absoluteImportPath, ref, allImports); } return allImports; @@ -303,6 +348,12 @@ export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOpt // Loop through all inputs to aggregate dependencies and dependents. for (const input of inputs) { + // The metafile does not contain external dependencies. + // So we can only fill in their dependents. + if (input.type === 'external') { + continue; + } + const metaFile = references.inputs.meta[input.filepath]; if (!metaFile) { log.debug(`Could not find metafile's ${input.name}`); @@ -313,11 +364,26 @@ export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOpt if (!isFileSupported(dependency.path)) { continue; } - const dependencyPath = getAbsolutePath(cwd, dependency.path); - const dependencyFile = references.inputs.report[dependencyPath]; + + const isRelative = dependency.path.match(/^\.?\.\//); + const root = isRelative ? path.dirname(input.filepath) : cwd; + const absoluteDependencyPath = getAbsolutePath(root, dependency.path); + + let dependencyFile: Input | undefined; + if (dependency.external) { + // If it's an absolute external import, we can't trust our own getAbsolutePath(). + // We can't know what its "root" could be. + const filepath = isRelative ? absoluteDependencyPath : dependency.path; + // In case of externals, we use their path directly. + dependencyFile = references.inputs.report[filepath]; + } else { + dependencyFile = references.inputs.report[absoluteDependencyPath]; + } if (!dependencyFile) { - log.debug(`Could not find input file of ${dependency.path}`); + log.debug( + `Could not find input file of ${dependency.path} imported from ${input.name}`, + ); continue; } diff --git a/packages/plugins/build-report/src/helpers.ts b/packages/plugins/build-report/src/helpers.ts index aeaee5dcb..31d42fa8c 100644 --- a/packages/plugins/build-report/src/helpers.ts +++ b/packages/plugins/build-report/src/helpers.ts @@ -3,9 +3,8 @@ // Copyright 2019-Present Datadog, Inc. import { INJECTED_FILE } from '@dd/core/constants'; -import { isInjectionFile } from '@dd/core/helpers'; +import { getAbsolutePath, isInjectionFile } from '@dd/core/helpers'; import type { GlobalContext } from '@dd/core/types'; -import path from 'path'; // Will match any last part of a path after a dot or slash and is a word character. const EXTENSION_RX = /\.(?!.*(?:\.|\/|\\))(\w{1,})/g; @@ -82,18 +81,6 @@ export const cleanPath = (filepath: string) => { ); }; -// Will only prepend the cwd if not already there. -export const getAbsolutePath = (cwd: string, filepath: string) => { - if (isInjectionFile(filepath)) { - return INJECTED_FILE; - } - - if (filepath.startsWith(cwd)) { - return filepath; - } - return path.resolve(cwd, filepath); -}; - // Extract a name from a path based on the context (out dir and cwd). export const cleanName = (context: GlobalContext, filepath: string) => { if (isInjectionFile(filepath)) { @@ -114,7 +101,7 @@ export const cleanName = (context: GlobalContext, filepath: string) => { .split('!') .pop()! // Remove outDir's path. - .replace(context.bundler.outDir, '') + .replace(getAbsolutePath(context.cwd, context.bundler.outDir), '') // Remove the cwd's path. .replace(context.cwd, '') // Remove node_modules path. @@ -123,7 +110,7 @@ export const cleanName = (context: GlobalContext, filepath: string) => { // Remove query parameters. .split(QUERY_RX) .shift()! - // Remove leading slashes. - .replace(/^\/+/, '') + // Remove leading dots and slashes. + .replace(/^((\.\.?)?\/)+/, '') ); }; diff --git a/packages/plugins/build-report/src/rollup.ts b/packages/plugins/build-report/src/rollup.ts index c99be1b27..adce15942 100644 --- a/packages/plugins/build-report/src/rollup.ts +++ b/packages/plugins/build-report/src/rollup.ts @@ -2,9 +2,10 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +import { getAbsolutePath } from '@dd/core/helpers'; import type { Logger, Entry, GlobalContext, Input, Output, PluginOptions } from '@dd/core/types'; -import { cleanName, cleanPath, cleanReport, getAbsolutePath, getType } from './helpers'; +import { cleanName, cleanPath, cleanReport, getType } from './helpers'; export const getRollupPlugin = (context: GlobalContext, log: Logger): PluginOptions['rollup'] => { const importsReport: Record< @@ -59,6 +60,7 @@ export const getRollupPlugin = (context: GlobalContext, log: Logger): PluginOpti const outputs: Output[] = []; const tempEntryFiles: Entry[] = []; const tempSourcemaps: Output[] = []; + const tempOutputsImports: Record = {}; const entries: Entry[] = []; const reportInputsIndexed: Record = {}; @@ -123,7 +125,7 @@ export const getRollupPlugin = (context: GlobalContext, log: Logger): PluginOpti if ('modules' in asset) { for (const [modulepath, module] of Object.entries(asset.modules)) { - // We don't want to include commonjs wrappers that have a path like: + // We don't want to include commonjs wrappers and proxies that are like: // \u0000{{path}}?commonjs-proxy if (cleanPath(modulepath) !== modulepath) { continue; @@ -144,6 +146,44 @@ export const getRollupPlugin = (context: GlobalContext, log: Logger): PluginOpti } } + // Add imports as inputs. + // These are external imports since they are declared in the output file. + if ('imports' in asset) { + for (const importName of asset.imports) { + const cleanedImport = cleanPath(importName); + const importReport = importsReport[cleanedImport]; + if (!importReport) { + // We may not have this yet as it could be one of the chunks + // produced by the current build. + tempOutputsImports[ + getAbsolutePath(context.bundler.outDir, cleanedImport) + ] = file; + continue; + } + + if (reportInputsIndexed[cleanedImport]) { + log.debug( + `Input report already there for ${cleanedImport} from ${file.name}.`, + ); + continue; + } + + const importFile: Input = { + name: cleanName(context, importName), + dependencies: new Set(), + dependents: new Set(), + filepath: cleanedImport, + // Since it's external, we don't have the size. + size: 0, + type: 'external', + }; + file.inputs.push(importFile); + + reportInputsIndexed[importFile.filepath] = importFile; + inputs.push(importFile); + } + } + // Store entries for later filling. // As we may not have reported its outputs and inputs yet. if ('isEntry' in asset && asset.isEntry) { @@ -154,6 +194,18 @@ export const getRollupPlugin = (context: GlobalContext, log: Logger): PluginOpti outputs.push(file); } + for (const [filepath, output] of Object.entries(tempOutputsImports)) { + const outputReport = reportOutputsIndexed[filepath]; + if (!outputReport) { + log.debug(`Could not find the output report for ${filepath}.`); + continue; + } + + if (!output.inputs.includes(outputReport)) { + output.inputs.push(outputReport); + } + } + // Fill in inputs' dependencies and dependents. for (const input of inputs) { const importReport = importsReport[input.filepath]; @@ -211,12 +263,18 @@ export const getRollupPlugin = (context: GlobalContext, log: Logger): PluginOpti // Get its output. const foundOutput = reportOutputsIndexed[filepath]; if (!foundOutput) { - log.debug(`Could not find output for ${filename}`); + // If it's been reported in the indexes, it means it's an external here. + const isExternal = !!reportInputsIndexed[filename]; + // Do not log about externals, we don't expect to find them. + if (!isExternal) { + log.debug(`Could not find output for ${filename}`); + } return allOutputs; } allOutputs[filepath] = foundOutput; - const asset = bundle[filename]; + // Rollup indexes on the filepath relative to the outDir. + const asset = bundle[cleanName(context, filepath)]; if (!asset) { log.debug(`Could not find asset for ${filename}`); return allOutputs; diff --git a/packages/plugins/build-report/src/xpack.ts b/packages/plugins/build-report/src/xpack.ts index 404ba2716..a3481110e 100644 --- a/packages/plugins/build-report/src/xpack.ts +++ b/packages/plugins/build-report/src/xpack.ts @@ -2,7 +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 { getAbsolutePath, isInjectionFile } from '@dd/core/helpers'; import type { Logger, Entry, @@ -13,7 +13,7 @@ import type { PluginOptions, } from '@dd/core/types'; -import { cleanName, getAbsolutePath, getType } from './helpers'; +import { cleanName, getType } from './helpers'; export const getXpackPlugin = ( @@ -63,6 +63,12 @@ export const getXpackPlugin = * 2. Once the build is finished and emitted, we can compute the outputs and the entries. */ + const cleanExternalName = (name: string) => { + // Removes "external " prefix and surrounding quotes from external dependency names + // Example: 'external var "lodash"' -> 'lodash' + return name.replace(/(^external[^"]+"|"$)/g, ''); + }; + // Index the module by its identifier, resource, request, rawRequest, and userRequest. const getKeysToIndex = (mod: Module): Set => { const values: Record = { @@ -96,6 +102,11 @@ export const getXpackPlugin = } } else { keysToIndex.add(value); + // RSpack only use "external ..." for external dependencies. + // So we need to clean and add the actual name to the index too. + if (value.startsWith('external ')) { + keysToIndex.add(cleanExternalName(value)); + } } } @@ -134,6 +145,19 @@ export const getXpackPlugin = } }; + const isExternal = (mod: Module) => { + if ('externalType' in mod && mod.externalType) { + return true; + } + if ('external' in mod && mod.external) { + return true; + } + if (mod.identifier?.().startsWith('external ')) { + return true; + } + return false; + }; + // Intercept the compilation to then get the modules. compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { // Intercept the modules to build the dependency graph. @@ -151,13 +175,14 @@ export const getXpackPlugin = // Second loop to create the dependency graph. for (const module of finishedModules) { const moduleIdentifier = module.identifier(); + const moduleName = cleanName(context, moduleIdentifier); const dependencies: Set = new Set( getAllDependencies(module) .map((dep) => { const mod = getModuleFromDep(module, dep); // Ignore those we can't identify. - if (!mod || !mod.identifier()) { + if (!mod?.identifier()) { return false; } @@ -173,7 +198,9 @@ export const getXpackPlugin = return false; } - return identifier; + return isExternal(mod) + ? cleanExternalName(identifier) + : identifier; }) .filter(Boolean) as string[], ); @@ -205,16 +232,31 @@ export const getXpackPlugin = tempDeps.set(moduleIdentifier, moduleDeps); // Store the inputs. - const file: Input = { - size: module.size() || 0, - name: cleanName(context, moduleIdentifier), - dependencies: new Set(), - dependents: new Set(), - filepath: moduleIdentifier, - type: getType(moduleIdentifier), - }; + const file: Input = isExternal(module) + ? { + size: 0, + name: cleanExternalName(moduleName), + dependencies: new Set(), + dependents: new Set(), + filepath: moduleIdentifier, + type: 'external', + } + : { + size: module.size() || 0, + name: moduleName, + dependencies: new Set(), + dependents: new Set(), + filepath: moduleIdentifier, + type: getType(moduleIdentifier), + }; + inputs.push(file); reportInputsIndexed.set(moduleIdentifier, file); + + // If it's an external dependency, we also need to index it by its cleaned name. + if (isExternal(module)) { + reportInputsIndexed.set(cleanExternalName(moduleIdentifier), file); + } } // Assign dependencies and dependents. diff --git a/packages/plugins/bundler-report/src/index.ts b/packages/plugins/bundler-report/src/index.ts index 04f34f7d4..46fe5c842 100644 --- a/packages/plugins/bundler-report/src/index.ts +++ b/packages/plugins/bundler-report/src/index.ts @@ -2,39 +2,27 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +import { + getAbsolutePath, + getNearestCommonDirectory, + getHighestPackageJsonDir, +} from '@dd/core/helpers'; import type { GlobalContext, PluginOptions } from '@dd/core/types'; import path from 'path'; export const PLUGIN_NAME = 'datadog-bundler-report-plugin'; -// 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; - } +// Compute the CWD based on a list of directories and the outDir. +const getCwd = (dirs: Set, outDir: string) => { + const highestPackage = getHighestPackageJsonDir(outDir); + if (highestPackage) { + return highestPackage; } - return commonParts.length > 0 ? commonParts.join(path.sep) : path.sep; -}; - -const handleCwd = (dirs: string[], context: GlobalContext) => { - const nearestDir = getNearestCommonDirectory(dirs, context.cwd); + // Fall back to the nearest common directory. + const nearestDir = getNearestCommonDirectory(Array.from(dirs)); if (nearestDir !== path.sep) { - context.cwd = nearestDir; + return nearestDir; } }; @@ -64,15 +52,19 @@ export const getBundlerReportPlugins = (context: GlobalContext): PluginOptions[] directories.add(outputOptions.dir); } else if (outputOptions.file) { context.bundler.outDir = path.dirname(outputOptions.file); - directories.add(outputOptions.dir); + directories.add(context.bundler.outDir); } + // We need an absolute path for rollup because of the way we have to compute its CWD. + // It's relative to process.cwd(), because there is no cwd options for rollup. + context.bundler.outDir = getAbsolutePath(process.cwd(), context.bundler.outDir); + // Vite has the "root" option we're using. if (context.bundler.name === 'vite') { return; } - handleCwd(Array.from(directories), context); + context.cwd = getCwd(directories, context.bundler.outDir) || context.cwd; }; const rollupPlugin: () => PluginOptions['rollup'] & PluginOptions['vite'] = () => { @@ -96,12 +88,14 @@ export const getBundlerReportPlugins = (context: GlobalContext): PluginOptions[] } if ('output' in options) { - handleOutputOptions(options.output); + const outputOptions = Array.isArray(options.output) + ? options.output + : [options.output]; + for (const output of outputOptions) { + handleOutputOptions(output); + } } }, - outputOptions(options) { - handleOutputOptions(options); - }, }; }; @@ -140,7 +134,7 @@ export const getBundlerReportPlugins = (context: GlobalContext): PluginOptions[] if (config.root) { context.cwd = config.root; } else { - handleCwd(Array.from(directories), context); + context.cwd = getCwd(directories, context.bundler.outDir) || context.cwd; } }, }, diff --git a/packages/plugins/injection/src/esbuild.ts b/packages/plugins/injection/src/esbuild.ts index 2097cf388..77ae39b87 100644 --- a/packages/plugins/injection/src/esbuild.ts +++ b/packages/plugins/injection/src/esbuild.ts @@ -3,33 +3,37 @@ // Copyright 2019-Present Datadog, Inc. import { INJECTED_FILE } from '@dd/core/constants'; -import { getEsbuildEntries, getUniqueId, outputFile, rm } from '@dd/core/helpers'; +import { getAbsolutePath, getEsbuildEntries, getUniqueId, outputFile } 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 fs from 'fs'; +import os from 'os'; import path from 'path'; import { PLUGIN_NAME } from './constants'; import { getContentToInject } from './helpers'; import type { ContentsToInject } from './types'; +const fsp = fs.promises; + export const getEsbuildPlugin = ( log: Logger, context: GlobalContext, contentsToInject: ContentsToInject, ): PluginOptions['esbuild'] => ({ setup(build) { - const { onStart, onLoad, onEnd, esbuild, initialOptions } = build; + const { onStart, onResolve, 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 tmpDir = fs.realpathSync(os.tmpdir()); + const absoluteFilePath = path.resolve(tmpDir, 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 || []; + const initialInject = initialOptions.inject; + initialOptions.inject = initialInject ? [...initialInject] : []; initialOptions.inject.push(absoluteFilePath); onStart(async () => { @@ -37,8 +41,7 @@ export const getEsbuildPlugin = ( 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) || []; + build.initialOptions.inject = initialInject; try { // Create the MIDDLE file because esbuild will crash if it doesn't exist. @@ -49,6 +52,16 @@ export const getEsbuildPlugin = ( } }); + onResolve( + { + filter: injectionRx, + }, + async (args) => { + // Mark the file as being injected by us. + return { path: args.path, namespace: PLUGIN_NAME }; + }, + ); + onLoad( { filter: injectionRx, @@ -57,9 +70,6 @@ export const getEsbuildPlugin = ( 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 || ' ', diff --git a/packages/plugins/injection/src/helpers.ts b/packages/plugins/injection/src/helpers.ts index 9bf643dd6..c6dd2042c 100644 --- a/packages/plugins/injection/src/helpers.ts +++ b/packages/plugins/injection/src/helpers.ts @@ -2,10 +2,9 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { doRequest, truncateString } from '@dd/core/helpers'; +import { doRequest, getAbsolutePath, 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 { AFTER_INJECTION, BEFORE_INJECTION, DISTANT_FILE_RX } from './constants'; diff --git a/packages/plugins/injection/src/index.ts b/packages/plugins/injection/src/index.ts index 00e0f51e1..36ddd72a0 100644 --- a/packages/plugins/injection/src/index.ts +++ b/packages/plugins/injection/src/index.ts @@ -2,7 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { getUniqueId, isInjectionFile } from '@dd/core/helpers'; +import { getUniqueId, isInjectionFile, isXpack } from '@dd/core/helpers'; import { InjectPosition, type GlobalContext, @@ -35,56 +35,48 @@ export const getInjectionPlugins = (bundler: any, context: GlobalContext): Plugi injections.set(getUniqueId(), item); }; - const plugins: PluginOptions[] = [ - { - 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, injections, contentsToInject), - rspack: getXpackPlugin(bundler, log, context, injections, 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() { - // 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; - } + const plugin: PluginOptions = { + 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, injections, contentsToInject), + rspack: getXpackPlugin(bundler, log, context, injections, contentsToInject), + rollup: getRollupPlugin(contentsToInject), + vite: { ...getRollupPlugin(contentsToInject), enforce: 'pre' }, + }; - // Prepare the injections. - await addInjections(log, injections, contentsToInject, context.cwd); - }, - async resolveId(source) { - if (isInjectionFile(source)) { - return { id: source }; - } + // We need to handle the resolution in xpack, + // and it's easier to use unplugin's hooks for it. + if (isXpack(context.bundler.fullName)) { + plugin.loadInclude = (id) => { + if (isInjectionFile(id)) { + return true; + } - return null; - }, - loadInclude(id) { - if (isInjectionFile(id)) { - return true; - } + return null; + }; - return null; - }, - load(id) { - if (isInjectionFile(id)) { - return { - code: getContentToInject(contentsToInject[InjectPosition.MIDDLE]), - }; - } - return null; - }, - }, - ]; + plugin.load = (id) => { + if (isInjectionFile(id)) { + return { + code: getContentToInject(contentsToInject[InjectPosition.MIDDLE]), + }; + } + return null; + }; + } else { + // In xpack, we need to prepare the injections BEFORE the build starts. + // Otherwise, the bundler doesn't have the content when it needs it. + // So we do it in their specific plugin. + // Here for all the other non-xpack bundlers. + plugin.buildStart = async () => { + // Prepare the injections. + await addInjections(log, injections, contentsToInject, context.cwd); + }; + } - return plugins; + return [plugin]; }; diff --git a/packages/plugins/injection/src/xpack.ts b/packages/plugins/injection/src/xpack.ts index e4b1c6071..353b7f97f 100644 --- a/packages/plugins/injection/src/xpack.ts +++ b/packages/plugins/injection/src/xpack.ts @@ -3,7 +3,7 @@ // Copyright 2019-Present Datadog, Inc. import { INJECTED_FILE } from '@dd/core/constants'; -import { getUniqueId, outputFileSync, rm } from '@dd/core/helpers'; +import { getUniqueId, outputFileSync, rmSync } from '@dd/core/helpers'; import type { GlobalContext, Logger, PluginOptions, ToInjectItem } from '@dd/core/types'; import { InjectPosition } from '@dd/core/types'; import { createRequire } from 'module'; @@ -43,9 +43,21 @@ export const getXpackPlugin = ); // NOTE: 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') { - outputFileSync(filePath, ''); + // There must be some race condition around this, because it's not always failing. + outputFileSync(filePath, ''); + // WARNING: Can't use shutdown.tapPromise as rspack would randomly crash the process. + // Seems to be fixed in rspack@1.2.* + // We also do it for webpack, as it fixes some resolution edge cases. + const hookFn = () => { + // Delete the file we created. + rmSync(filePath); + }; + // Webpack4 doesn't have the "shutdown" hook. + if (compiler.hooks.shutdown) { + compiler.hooks.shutdown.tap(PLUGIN_NAME, hookFn); + } else { + compiler.hooks.done.tap(PLUGIN_NAME, hookFn); + compiler.hooks.failed.tap(PLUGIN_NAME, hookFn); } // Handle the InjectPosition.MIDDLE. @@ -108,13 +120,6 @@ export const getXpackPlugin = 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, diff --git a/packages/published/esbuild-plugin/package.json b/packages/published/esbuild-plugin/package.json index fd291184d..86fb83ea0 100644 --- a/packages/published/esbuild-plugin/package.json +++ b/packages/published/esbuild-plugin/package.json @@ -23,6 +23,7 @@ "types": "./dist/src/index.d.ts", "exports": { "./dist/src": "./dist/src/index.js", + "./dist/src/*": "./dist/src/*", ".": "./src/index.ts" }, "publishConfig": { @@ -41,11 +42,12 @@ "dist" ], "scripts": { - "build": "yarn clean && rollup --config rollup.config.mjs", + "buildCmd": "rollup --config rollup.config.mjs", + "build": "yarn clean && yarn buildCmd", "clean": "rm -rf dist", "prepack": "yarn build", "typecheck": "tsc --noEmit", - "watch": "yarn clean && rollup --config rollup.config.mjs --watch" + "watch": "yarn build --watch" }, "dependencies": { "async-retry": "1.3.3", diff --git a/packages/published/rollup-plugin/package.json b/packages/published/rollup-plugin/package.json index 9a88e5594..35b59bfb1 100644 --- a/packages/published/rollup-plugin/package.json +++ b/packages/published/rollup-plugin/package.json @@ -23,6 +23,7 @@ "types": "./dist/src/index.d.ts", "exports": { "./dist/src": "./dist/src/index.js", + "./dist/src/*": "./dist/src/*", ".": "./src/index.ts" }, "publishConfig": { @@ -41,11 +42,12 @@ "dist" ], "scripts": { - "build": "yarn clean && rollup --config rollup.config.mjs", + "buildCmd": "rollup --config rollup.config.mjs", + "build": "yarn clean && yarn buildCmd", "clean": "rm -rf dist", "prepack": "yarn build", "typecheck": "tsc --noEmit", - "watch": "yarn clean && rollup --config rollup.config.mjs --watch" + "watch": "yarn build --watch" }, "dependencies": { "async-retry": "1.3.3", diff --git a/packages/published/rspack-plugin/package.json b/packages/published/rspack-plugin/package.json index ad3fcf395..555c16c23 100644 --- a/packages/published/rspack-plugin/package.json +++ b/packages/published/rspack-plugin/package.json @@ -23,6 +23,7 @@ "types": "./dist/src/index.d.ts", "exports": { "./dist/src": "./dist/src/index.js", + "./dist/src/*": "./dist/src/*", ".": "./src/index.ts" }, "publishConfig": { @@ -41,11 +42,12 @@ "dist" ], "scripts": { - "build": "yarn clean && rollup --config rollup.config.mjs", + "buildCmd": "rollup --config rollup.config.mjs", + "build": "yarn clean && yarn buildCmd", "clean": "rm -rf dist", "prepack": "yarn build", "typecheck": "tsc --noEmit", - "watch": "yarn clean && rollup --config rollup.config.mjs --watch" + "watch": "yarn build --watch" }, "dependencies": { "async-retry": "1.3.3", diff --git a/packages/published/vite-plugin/package.json b/packages/published/vite-plugin/package.json index 621fbca5a..025222bdd 100644 --- a/packages/published/vite-plugin/package.json +++ b/packages/published/vite-plugin/package.json @@ -23,6 +23,7 @@ "types": "./dist/src/index.d.ts", "exports": { "./dist/src": "./dist/src/index.js", + "./dist/src/*": "./dist/src/*", ".": "./src/index.ts" }, "publishConfig": { @@ -41,11 +42,12 @@ "dist" ], "scripts": { - "build": "yarn clean && rollup --config rollup.config.mjs", + "buildCmd": "rollup --config rollup.config.mjs", + "build": "yarn clean && yarn buildCmd", "clean": "rm -rf dist", "prepack": "yarn build", "typecheck": "tsc --noEmit", - "watch": "yarn clean && rollup --config rollup.config.mjs --watch" + "watch": "yarn build --watch" }, "dependencies": { "async-retry": "1.3.3", diff --git a/packages/published/webpack-plugin/package.json b/packages/published/webpack-plugin/package.json index f4a189514..4c4e1b88a 100644 --- a/packages/published/webpack-plugin/package.json +++ b/packages/published/webpack-plugin/package.json @@ -22,6 +22,7 @@ "module": "./dist/src/index.mjs", "types": "./dist/src/index.d.ts", "exports": { + "./dist/src": "./dist/src/index.js", "./dist/src/*": "./dist/src/*", ".": "./src/index.ts" }, @@ -41,11 +42,12 @@ "dist" ], "scripts": { - "build": "yarn clean && rollup --config rollup.config.mjs", + "buildCmd": "rollup --config rollup.config.mjs", + "build": "yarn clean && yarn buildCmd", "clean": "rm -rf dist", "prepack": "yarn build", "typecheck": "tsc --noEmit", - "watch": "yarn clean && rollup --config rollup.config.mjs --watch" + "watch": "yarn build --watch" }, "dependencies": { "async-retry": "1.3.3", diff --git a/packages/tests/README.md b/packages/tests/README.md index faf1b0912..1b347b2ea 100644 --- a/packages/tests/README.md +++ b/packages/tests/README.md @@ -58,7 +58,6 @@ Here's a bootstrap to get you going: ```typescript import type { Options } from '@dd/core/types'; -import type { CleanupFn } from '@dd/tests/_jest/helpers/runBundlers'; import { runBundlers } from '@dd/tests/_jest/helpers/runBundlers'; describe('My very awesome plugin', () => { @@ -174,7 +173,6 @@ The best way would be to freeze the content you need to test, at the moment you ```typescript import type { GlobalContext, Options } from '@dd/core/types'; import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks'; -import type { CleanupFn } from '@dd/tests/_jest/helpers/runBundlers'; import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers'; describe('Global Context Plugin', () => { @@ -221,7 +219,6 @@ Giving the following, more involved example: import { serializeBuildReport, unserializeBuildReport } from '@dd/core/helpers'; import type { BuildReport, Options } from '@dd/core/types'; import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks'; -import type { CleanupFn } from '@dd/tests/_jest/helpers/runBundlers'; import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers'; describe('Build Reports', () => { diff --git a/packages/tests/jest.config.js b/packages/tests/jest.config.js index 796986f99..aa8e266e1 100644 --- a/packages/tests/jest.config.js +++ b/packages/tests/jest.config.js @@ -7,8 +7,6 @@ module.exports = { clearMocks: true, globalSetup: '/src/_jest/globalSetup.ts', preset: 'ts-jest/presets/js-with-ts', - // Without it, vite import is silently crashing the process with code SIGHUP 129 - resetModules: true, roots: ['./src/unit/'], setupFilesAfterEnv: ['/src/_jest/setupAfterEnv.ts'], testEnvironment: 'node', diff --git a/packages/tests/playwright.config.ts b/packages/tests/playwright.config.ts index 4251ae213..41dcb08fc 100644 --- a/packages/tests/playwright.config.ts +++ b/packages/tests/playwright.config.ts @@ -28,7 +28,8 @@ export default defineConfig({ bundlers: getRequestedBundlers(), trace: 'retain-on-failure', }, - timeout: 5_000, + globalTimeout: process.env.CI ? 20 * 60 * 1000 : undefined, + timeout: 60_000, /* Configure projects for each bundler */ // TODO Also build and test for ESM. projects: FULL_NAME_BUNDLERS.map((bundler) => [ diff --git a/packages/tests/src/_jest/fixtures/hard_project/package.json b/packages/tests/src/_jest/fixtures/hard_project/package.json index ac25238f5..5574de16a 100644 --- a/packages/tests/src/_jest/fixtures/hard_project/package.json +++ b/packages/tests/src/_jest/fixtures/hard_project/package.json @@ -6,10 +6,5 @@ "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/yarn.lock b/packages/tests/src/_jest/fixtures/yarn.lock index e8d79974f..ee71a905c 100644 --- a/packages/tests/src/_jest/fixtures/yarn.lock +++ b/packages/tests/src/_jest/fixtures/yarn.lock @@ -34,9 +34,6 @@ __metadata: 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 diff --git a/packages/tests/src/_jest/helpers/mocks.ts b/packages/tests/src/_jest/helpers/mocks.ts index 955d2d038..b1864f4cb 100644 --- a/packages/tests/src/_jest/helpers/mocks.ts +++ b/packages/tests/src/_jest/helpers/mocks.ts @@ -2,6 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +import { getAbsolutePath } from '@dd/core/helpers'; import type { BuildReport, File, @@ -11,7 +12,6 @@ import type { LogLevel, Options, } from '@dd/core/types'; -import { getAbsolutePath } from '@dd/internal-build-report-plugin/helpers'; import { getSourcemapsConfiguration } from '@dd/tests/unit/plugins/error-tracking/testHelpers'; import { getTelemetryConfiguration } from '@dd/tests/unit/plugins/telemetry/testHelpers'; import { configXpack } from '@dd/tools/bundlers'; diff --git a/packages/tests/src/_jest/helpers/runBundlers.ts b/packages/tests/src/_jest/helpers/runBundlers.ts index adf8ee003..2d7395874 100644 --- a/packages/tests/src/_jest/helpers/runBundlers.ts +++ b/packages/tests/src/_jest/helpers/runBundlers.ts @@ -223,7 +223,7 @@ export const runBundlers = async ( const errors: string[] = []; // Generate a seed to avoid collision of builds. - const seed: string = `${jest.getSeed()}.${getUniqueId()}`; + const seed: string = `${Math.abs(jest.getSeed())}.${getUniqueId()}`; const bundlersToRun = BUNDLERS.filter( (bundler) => !bundlers || bundlers.includes(bundler.name), @@ -231,20 +231,21 @@ export const runBundlers = async ( const workingDir = await prepareWorkingDir(seed); + if (NO_CLEANUP) { + console.log(`[NO_CLEANUP] Working directory: ${workingDir}`); + } + const bundlerOverridesResolved = typeof bundlerOverrides === 'function' ? bundlerOverrides(workingDir) : bundlerOverrides || {}; const runBundlerFunction = async (bundler: Bundler) => { - const bundlerOverride = bundlerOverridesResolved[bundler.name] || {}; - - let result: Awaited>; - // Isolate each runs to avoid conflicts between tests. - await jest.isolateModulesAsync(async () => { - result = await bundler.run(workingDir, pluginOverrides, bundlerOverride); - }); - return result!; + return bundler.run( + workingDir, + pluginOverrides, + bundlerOverridesResolved[bundler.name] || {}, + ); }; // Run the bundlers sequentially to ease the resources usage. diff --git a/packages/tests/src/unit/core/helpers.test.ts b/packages/tests/src/unit/core/helpers.test.ts index 645c46633..a71f30df5 100644 --- a/packages/tests/src/unit/core/helpers.test.ts +++ b/packages/tests/src/unit/core/helpers.test.ts @@ -2,6 +2,7 @@ // 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 } from '@dd/core/helpers'; import type { RequestOpts, ResolvedEntry } from '@dd/core/types'; import { @@ -25,10 +26,15 @@ jest.mock('fs', () => require('memfs').fs); describe('Core Helpers', () => { describe('formatDuration', () => { test.each([ + [0, '0ms'], [10, '10ms'], + [10000, '10s'], [10010, '10s 10ms'], + [1000000, '16m 40s'], [1000010, '16m 40s 10ms'], + [10000000, '2h 46m 40s'], [10000010, '2h 46m 40s 10ms'], + [1000000000, '11d 13h 46m 40s'], [1000000010, '11d 13h 46m 40s 10ms'], ])('Should format duration %s => %s', async (ms, expected) => { const { formatDuration } = await import('@dd/core/helpers'); @@ -363,4 +369,81 @@ describe('Core Helpers', () => { }, ); }); + + describe('getAbsolutePath', () => { + test.each([ + // With the injection file. + ['/path/to', `./to/1293.${INJECTED_FILE}.js`, INJECTED_FILE], + // With a path with no prefix. + ['/path/to', 'file.js', '/path/to/file.js'], + // With a path with a dot prefix. + ['/path/to', './file.js', '/path/to/file.js'], + ['/path/to', '../file.js', '/path/file.js'], + ['/path/to', '../../file.js', '/file.js'], + ['/path/to', '../../../file.js', '/file.js'], + // With an absolute path. + ['/path/to', '/file.js', '/file.js'], + ])('Should resolve "%s" with "%s" to "%s"', async (base, relative, expected) => { + const { getAbsolutePath } = await import('@dd/core/helpers'); + expect(getAbsolutePath(base, relative)).toBe(expected); + }); + }); + + describe('getNearestCommonDirectory', () => { + test.each([ + { + // With a single path. + directories: ['/path/to'], + expected: '/path/to', + }, + { + // Basic usage. + directories: ['/path/to', '/path/to/other'], + expected: '/path/to', + }, + { + // With a different root directory. + directories: ['/path/to', '/path2/to/other'], + expected: '/', + }, + { + // With an absolute file. + directories: ['/path/to', '/'], + expected: '/', + }, + { + // With a given cwd. + cwd: '/path', + directories: ['/path/to', './', '/path/to/other'], + expected: '/path', + }, + ])('Should find the nearest common directory', async ({ directories, cwd, expected }) => { + const { getNearestCommonDirectory } = await import('@dd/core/helpers'); + expect(getNearestCommonDirectory(directories, cwd)).toBe(expected); + }); + }); + + describe('getHighestPackageJsonDir', () => { + beforeEach(() => { + vol.fromJSON({ + '/path1/to/package.json': '', + '/path2/to/other/package.json': '', + '/path3/to/other/deeper/package.json': '', + }); + }); + + afterEach(() => { + vol.reset(); + }); + + test.each([ + ['/path1/to', '/path1/to'], + ['/path2/to/other/project/directory', '/path2/to/other'], + ['/path3/to/other/deeper/who/knows', '/path3/to/other/deeper'], + ['/', undefined], + ])('Should find the highest package.json', async (dirpath, expected) => { + const { getHighestPackageJsonDir } = await import('@dd/core/helpers'); + expect(getHighestPackageJsonDir(dirpath)).toBe(expected); + }); + }); }); diff --git a/packages/tests/src/unit/factory/helpers.test.ts b/packages/tests/src/unit/factory/helpers.test.ts index e3c8990d5..2d74c2552 100644 --- a/packages/tests/src/unit/factory/helpers.test.ts +++ b/packages/tests/src/unit/factory/helpers.test.ts @@ -28,6 +28,8 @@ const getOutput = (mock: jest.Mock, index: number) => stripAnsi(mock.mock.calls[ describe('Factory Helpers', () => { // Intercept contexts to verify it at the moment they're used. const initialContexts: Record = {}; + const cwds: Record = {}; + let workingDir: string; beforeAll(async () => { const pluginConfig: Options = { @@ -40,11 +42,19 @@ describe('Factory Helpers', () => { // These are functions, so they can't be serialized with parse/stringify. initialContexts[bundlerName].inject = context.inject; - return []; + return [ + { + name: 'custom-plugin', + buildStart() { + cwds[bundlerName] = context.cwd; + }, + }, + ]; }, }; - await runBundlers(pluginConfig); + const result = await runBundlers(pluginConfig); + workingDir = result.workingDir; }); describe('getContext', () => { @@ -62,6 +72,10 @@ describe('Factory Helpers', () => { expect(context.version).toBe(version); expect(context.inject).toEqual(expect.any(Function)); }); + + test('Should update to the right CWD.', () => { + expect(cwds[name]).toBe(workingDir); + }); }); }); diff --git a/packages/tests/src/unit/plugins/build-report/helpers.test.ts b/packages/tests/src/unit/plugins/build-report/helpers.test.ts index 31c1215b4..cd2a8427a 100644 --- a/packages/tests/src/unit/plugins/build-report/helpers.test.ts +++ b/packages/tests/src/unit/plugins/build-report/helpers.test.ts @@ -2,21 +2,163 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { getType } from '@dd/internal-build-report-plugin/helpers'; +import { INJECTED_FILE } from '@dd/core/constants'; +import { cleanPath, cleanName, getType } from '@dd/internal-build-report-plugin/helpers'; +import { getContextMock, getMockBuild } from '@dd/tests/_jest/helpers/mocks'; describe('Build report plugin helpers', () => { describe('getType', () => { const expectations = [ - ['unknown', 'unknown'], - ['webpack/runtime', 'runtime'], - ['path/to/file.js', 'js'], - [ - '/loaders/load.js??ref--4-0!/tests/_virtual_.%2Fsrc%2Ffixtures%2Fproject%2Fmain1.js%3Fadd-custom-injection', - 'js', - ], + { + name: 'unknown', + filepath: 'unknown', + expected: 'unknown', + }, + { + name: 'webpack runtime', + filepath: 'webpack/runtime', + expected: 'runtime', + }, + { + name: 'file with extension', + filepath: 'path/to/file.js', + expected: 'js', + }, + { + name: 'complex loader path', + filepath: + '/loaders/load.js??ref--4-0!/tests/_virtual_.%2Fsrc%2Ffixtures%2Fproject%2Fmain1.js%3Fadd-custom-injection', + expected: 'js', + }, ]; - test.each(expectations)('Should return the right type for "%s".', (filepath, type) => { - expect(getType(filepath)).toBe(type); - }); + test.each(expectations)( + 'Should return the right type for "$name".', + ({ filepath, expected }) => { + expect(getType(filepath)).toBe(expected); + }, + ); + }); + + describe('cleanName', () => { + const expectations = [ + { + name: 'injected file', + filepath: `./${INJECTED_FILE}`, + expected: INJECTED_FILE, + }, + { + name: 'unknown file', + filepath: 'unknown', + expected: 'unknown', + }, + { + name: 'webpack runtime', + filepath: 'webpack/runtime/make namespace object', + expected: 'make-namespace-object', + }, + { + name: 'loader path', + filepath: + 'webpack/loaders/load.js??ruleSet[1].rules[0].use[0]!/current/working/directory/path.js', + expected: 'path.js', + }, + { + name: 'cwd', + filepath: '/current/working/directory/src/path.js', + expected: 'src/path.js', + }, + { + name: 'outDir', + filepath: '/current/working/directory/dist/path.js', + expected: 'path.js', + }, + { + name: 'node_modules dependency', + filepath: '/current/working/directory/node_modules/module/path.js', + expected: 'module/path.js', + }, + { + name: 'query parameters', + filepath: '/current/working/directory/path.js?query=param', + expected: 'path.js', + }, + { + name: 'encoded query parameters', + filepath: '/current/working/directory/path.js%3Fquery=param', + expected: 'path.js', + }, + { + name: 'pipe query parameters', + filepath: '/current/working/directory/path.js|query=param', + expected: 'path.js', + }, + { + name: 'leading dots and slashes', + filepath: '../../path.js', + expected: 'path.js', + }, + { + name: 'some composition', + filepath: + 'webpack/loaders/load.js??ruleSet[1].rules[0].use[0]!/current/working/directory/node_modules/module/path.js?query=param', + expected: 'module/path.js', + }, + ]; + test.each(expectations)( + 'Should return a cleaned name for "$name".', + ({ filepath, expected }) => { + const context = getContextMock({ + cwd: '/current/working/directory', + bundler: { + ...getMockBuild().bundler, + outDir: '/current/working/directory/dist', + }, + }); + expect(cleanName(context, filepath)).toBe(expected); + }, + ); + }); + + describe('cleanPath', () => { + const expectations = [ + { + name: 'loader path', + filepath: + 'webpack/loaders/load.js??ruleSet[1].rules[0].use[0]!/current/working/directory/path.js', + expected: '/current/working/directory/path.js', + }, + { + name: 'query parameters', + filepath: '/current/working/directory/path.js?query=param', + expected: '/current/working/directory/path.js', + }, + { + name: 'encoded query parameters', + filepath: '/current/working/directory/path.js%3Fquery=param', + expected: '/current/working/directory/path.js', + }, + { + name: 'pipe query parameters', + filepath: '/current/working/directory/path.js|query=param', + expected: '/current/working/directory/path.js', + }, + { + name: 'leading invisible characters', + filepath: '\u0000/current/working/directory/path.js', + expected: '/current/working/directory/path.js', + }, + { + name: 'some composition', + filepath: + '\u0000/webpack/loaders/load.js??ruleSet[1].rules[0].use[0]!/current/working/directory/node_modules/module/path.js?query=param', + expected: '/current/working/directory/node_modules/module/path.js', + }, + ]; + test.each(expectations)( + 'Should return a cleaned path for "$name".', + ({ filepath, expected }) => { + expect(cleanPath(filepath)).toBe(expected); + }, + ); }); }); diff --git a/packages/tests/src/unit/plugins/build-report/index.test.ts b/packages/tests/src/unit/plugins/build-report/index.test.ts index 6f8d7ca8d..9e72368ae 100644 --- a/packages/tests/src/unit/plugins/build-report/index.test.ts +++ b/packages/tests/src/unit/plugins/build-report/index.test.ts @@ -2,7 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { serializeBuildReport, unserializeBuildReport } from '@dd/core/helpers'; +import { serializeBuildReport, unserializeBuildReport, debugFilesPlugins } from '@dd/core/helpers'; import type { Input, Entry, @@ -21,7 +21,6 @@ import { } from '@dd/tests/_jest/helpers/mocks'; import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers'; import type { BundlerOptionsOverrides } from '@dd/tests/_jest/helpers/types'; -import { debugFilesPlugins } from '@dd/tools/helpers'; import path from 'path'; const sortFiles = (a: File | Output | Entry, b: File | Output | Entry) => { @@ -55,6 +54,10 @@ const getPluginConfig: ( }; }; +const isFileThirdParty = (file: Input | Output) => { + return file.filepath.includes('node_modules') || file.type === 'external'; +}; + describe('Build Report Plugin', () => { describe('Basic build', () => { const bundlerOutdir: Record = {}; @@ -174,9 +177,27 @@ describe('Build Report Plugin', () => { let workingDir: string; beforeAll(async () => { + // Mark some dependencies as external to ensure it's correctly reported too. + const rollupExternals = { + external: ['supports-color'], + }; + const xpackExternals = { + externals: { + 'supports-color': 'supports-color', + }, + }; const result = await runBundlers( getPluginConfig(bundlerOutdir, buildReports), - getComplexBuildOverrides(), + getComplexBuildOverrides({ + rollup: rollupExternals, + vite: rollupExternals, + webpack4: xpackExternals, + webpack5: xpackExternals, + rspack: xpackExternals, + esbuild: { + external: ['supports-color'], + }, + }), ); workingDir = result.workingDir; }); @@ -222,7 +243,7 @@ describe('Build Report Plugin', () => { 'hard_project/src/srcFile1.js', 'hard_project/workspaces/app/workspaceFile0.js', 'hard_project/workspaces/app/workspaceFile1.js', - 'supports-color/browser.js', + 'supports-color', ]); }); @@ -233,9 +254,7 @@ describe('Build Report Plugin', () => { .sort(sortFiles); // Only list the common dependencies and remove any particularities from bundlers. - const thirdParties = inputs!.filter((input) => - input.filepath.includes('node_modules'), - ); + const thirdParties = inputs!.filter((input) => isFileThirdParty(input)); expect(thirdParties.map((d) => d.name).sort()).toEqual([ 'ansi-styles/index.js', @@ -246,7 +265,7 @@ describe('Build Report Plugin', () => { 'color-convert/route.js', 'color-name/index.js', 'escape-string-regexp/index.js', - 'supports-color/browser.js', + 'supports-color', ]); }); @@ -294,7 +313,7 @@ describe('Build Report Plugin', () => { 'ansi-styles/index.js', 'chalk/templates.js', 'escape-string-regexp/index.js', - 'supports-color/browser.js', + 'supports-color', ], // It should also have a single dependent which is main1. dependents: ['hard_project/main1.js'], @@ -451,10 +470,10 @@ describe('Build Report Plugin', () => { )!; const entryInputs = entry.inputs.filter(filterOutParticularities); const dependencies = entryInputs.filter((input) => - input.filepath.includes('node_modules'), + isFileThirdParty(input), ); const mainFiles = entryInputs.filter( - (input) => !input.filepath.includes('node_modules'), + (input) => !isFileThirdParty(input), ); expect(dependencies).toHaveLength(dependenciesLength); diff --git a/packages/tests/src/unit/plugins/injection/index.test.ts b/packages/tests/src/unit/plugins/injection/index.test.ts index 6d50d28dd..ab1e25782 100644 --- a/packages/tests/src/unit/plugins/injection/index.test.ts +++ b/packages/tests/src/unit/plugins/injection/index.test.ts @@ -2,7 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { outputFileSync } from '@dd/core/helpers'; +import { debugFilesPlugins, 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'; @@ -14,7 +14,7 @@ import { } from '@dd/tests/_jest/helpers/mocks'; import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers'; import { header, licenses } from '@dd/tools/commands/oss/templates'; -import { debugFilesPlugins, escapeStringForRegExp, execute } from '@dd/tools/helpers'; +import { escapeStringForRegExp, execute } from '@dd/tools/helpers'; import chalk from 'chalk'; import { readFileSync, writeFileSync } from 'fs'; import { glob } from 'glob'; diff --git a/packages/tests/src/unit/plugins/telemetry/index.test.ts b/packages/tests/src/unit/plugins/telemetry/index.test.ts index 65daf8033..4372fe5b9 100644 --- a/packages/tests/src/unit/plugins/telemetry/index.test.ts +++ b/packages/tests/src/unit/plugins/telemetry/index.test.ts @@ -2,6 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +import { debugFilesPlugins } from '@dd/core/helpers'; import type { GlobalContext, Options } from '@dd/core/types'; import { addMetrics } from '@dd/telemetry-plugin/common/aggregator'; import type { MetricToSend } from '@dd/telemetry-plugin/types'; @@ -12,7 +13,6 @@ import { } from '@dd/tests/_jest/helpers/mocks'; import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers'; import type { Bundler } from '@dd/tests/_jest/helpers/types'; -import { debugFilesPlugins } from '@dd/tools/helpers'; import nock from 'nock'; // Used to intercept metrics. @@ -212,17 +212,17 @@ describe('Telemetry Universal Plugin', () => { describe('Entry metrics', () => { test.each([ { metric: 'entries.size', tags: ['entryName:app1'] }, - { metric: 'entries.modules.count', tags: ['entryName:app1'], value: 13 }, + { metric: 'entries.modules.count', tags: ['entryName:app1'] }, { metric: 'entries.assets.count', tags: ['entryName:app1'] }, { metric: 'entries.size', tags: ['entryName:app2'] }, - { metric: 'entries.modules.count', tags: ['entryName:app2'], value: 5 }, + { metric: 'entries.modules.count', tags: ['entryName:app2'] }, { metric: 'entries.assets.count', tags: ['entryName:app2'] }, - ])('Should have $metric with $tags', ({ metric, tags, value }) => { + ])('Should have $metric with $tags', ({ metric, tags }) => { const entryMetrics = metrics[name].filter((m) => m.metric.startsWith('entries'), ); - const metricToTest = getMetric(metric, tags, value); + const metricToTest = getMetric(metric, tags); const foundMetrics = entryMetrics.filter( (m) => m.metric === metric && tags.every((t) => m.tags.includes(t)), ); diff --git a/packages/tests/src/unit/tools/src/commands/create-plugin/index.test.ts b/packages/tests/src/unit/tools/src/commands/create-plugin/index.test.ts index b050f2943..28d95554a 100644 --- a/packages/tests/src/unit/tools/src/commands/create-plugin/index.test.ts +++ b/packages/tests/src/unit/tools/src/commands/create-plugin/index.test.ts @@ -6,6 +6,7 @@ import { getMirroredFixtures } from '@dd/tests/_jest/helpers/mocks'; import commands from '@dd/tools/commands/create-plugin/index'; import { ROOT } from '@dd/tools/constants'; import { Cli } from 'clipanion'; +import { vol } from 'memfs'; jest.mock('fs', () => require('memfs').fs); @@ -19,12 +20,11 @@ describe('Command create-plugin', () => { beforeEach(() => { // Mock the files that are touched by yarn cli create-plugin. - // FIXME: Using require here because clipanion + memfs somehow breaks memfs' singleton. - require('memfs').vol.fromJSON(fixtures, ROOT); + vol.fromJSON(fixtures, ROOT); }); afterEach(() => { - require('memfs').vol.reset(); + vol.reset(); }); const cases = [ diff --git a/packages/tools/src/commands/prepare-link/index.ts b/packages/tools/src/commands/prepare-link/index.ts index 1d56bb2cb..4c7c65347 100644 --- a/packages/tools/src/commands/prepare-link/index.ts +++ b/packages/tools/src/commands/prepare-link/index.ts @@ -43,7 +43,11 @@ class PrepareLink extends Command { const pkgJsonPath = path.resolve(ROOT, pkg.location, 'package.json'); const pkgJson = require(pkgJsonPath); if (this.revert) { - pkgJson.exports = { '.': './src/index.ts' }; + pkgJson.exports = { + './dist/src': './dist/src/index.js', + './dist/src/*': './dist/src/*', + '.': './src/index.ts', + }; } else { pkgJson.exports = pkgJson.publishConfig.exports; } diff --git a/packages/tools/src/helpers.ts b/packages/tools/src/helpers.ts index 4a6170fee..df086c4b8 100644 --- a/packages/tools/src/helpers.ts +++ b/packages/tools/src/helpers.ts @@ -3,16 +3,8 @@ // Copyright 2019-Present Datadog, Inc. import { ALL_BUNDLERS, SUPPORTED_BUNDLERS } from '@dd/core/constants'; -import { outputJsonSync, readJsonSync, serializeBuildReport } from '@dd/core/helpers'; -import type { - BundlerFullName, - BundlerName, - GetCustomPlugins, - GetPlugins, - GlobalContext, - IterableElement, - OptionsWithDefaults, -} from '@dd/core/types'; +import { readJsonSync } from '@dd/core/helpers'; +import type { BundlerFullName, BundlerName, GetPlugins, OptionsWithDefaults } from '@dd/core/types'; import { getContext } from '@dd/factory/helpers'; import chalk from 'chalk'; import { execFile, execFileSync } from 'child_process'; @@ -260,79 +252,3 @@ export const getBundlerPicture = (bundler: string) => { export const isInternalPluginWorkspace = (workspace: Workspace) => workspace.name.startsWith('@dd/internal-'); - -// Returns a customPlugin to output some debug files. -type CustomPlugins = ReturnType; -export const debugFilesPlugins = (context: GlobalContext): CustomPlugins => { - const rollupPlugin: IterableElement['rollup'] = { - writeBundle(options, bundle) { - outputJsonSync( - path.resolve(context.bundler.outDir, `output.${context.bundler.fullName}.json`), - bundle, - ); - }, - }; - - const xpackPlugin: IterableElement['webpack'] & - IterableElement['rspack'] = (compiler) => { - type Stats = Parameters[1]>[0]; - - compiler.hooks.done.tap('bundler-outputs', (stats: Stats) => { - const statsJson = stats.toJson({ - all: false, - assets: true, - children: true, - chunks: true, - chunkGroupAuxiliary: true, - chunkGroupChildren: true, - chunkGroups: true, - chunkModules: true, - chunkRelations: true, - entrypoints: true, - errors: true, - ids: true, - modules: true, - nestedModules: true, - reasons: true, - relatedAssets: true, - warnings: true, - }); - outputJsonSync( - path.resolve(context.bundler.outDir, `output.${context.bundler.fullName}.json`), - statsJson, - ); - }); - }; - - return [ - { - name: 'build-report', - writeBundle() { - outputJsonSync( - path.resolve(context.bundler.outDir, `report.${context.bundler.fullName}.json`), - serializeBuildReport(context.build), - ); - }, - }, - { - name: 'bundler-outputs', - esbuild: { - setup(build) { - build.onEnd((result) => { - outputJsonSync( - path.resolve( - context.bundler.outDir, - `output.${context.bundler.fullName}.json`, - ), - result.metafile, - ); - }); - }, - }, - rspack: xpackPlugin, - rollup: rollupPlugin, - vite: rollupPlugin, - webpack: xpackPlugin, - }, - ]; -}; diff --git a/packages/tools/src/rollupConfig.mjs b/packages/tools/src/rollupConfig.mjs index 0f92613dd..edee58cda 100644 --- a/packages/tools/src/rollupConfig.mjs +++ b/packages/tools/src/rollupConfig.mjs @@ -2,6 +2,8 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +// @ts-check + import babel from '@rollup/plugin-babel'; import commonjs from '@rollup/plugin-commonjs'; import esmShim from '@rollup/plugin-esm-shim'; @@ -15,12 +17,32 @@ import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -const CWD = process.env.PROJECT_CWD; +const CWD = process.env.PROJECT_CWD || process.cwd(); + +/** + * @typedef {{ + * module: string; + * main: string; + * name: string; + * peerDependencies: Record; + * dependencies: Record + * }} PackageJson + * @typedef {import('rollup').InputPluginOption} InputPluginOption + * @typedef {import('rollup').Plugin} Plugin + * @typedef {import('@dd/core/types').Assign< + * import('rollup').RollupOptions, + * { + * external?: string[]; + * plugins?: InputPluginOption[]; + * } + * >} RollupOptions + * @typedef {import('rollup').OutputOptions} OutputOptions + */ /** - * @param {{module: string; main: string;}} packageJson - * @param {import('rollup').RollupOptions} config - * @returns {import('rollup').RollupOptions} + * @param {PackageJson} packageJson + * @param {RollupOptions} config + * @returns {RollupOptions} */ export const bundle = (packageJson, config) => ({ input: 'src/index.ts', @@ -31,7 +53,6 @@ export const bundle = (packageJson, config) => ({ // All dependencies are external dependencies. ...Object.keys(packageJson.dependencies), // These should be internal only and never be anywhere published. - '@dd/core', '@dd/tools', '@dd/tests', // We never want to include Node.js built-in modules in the bundle. @@ -53,14 +74,14 @@ export const bundle = (packageJson, config) => ({ json(), commonjs(), nodeResolve({ preferBuiltins: true }), - ...config.plugins, + ...(config.plugins || []), ], }); /** - * @param {{module: string; main: string;}} packageJson - * @param {Partial} overrides - * @returns {import('rollup').OutputOptions} + * @param {PackageJson} packageJson + * @param {Partial} overrides + * @returns {OutputOptions} */ const getOutput = (packageJson, overrides = {}) => { const filename = overrides.format === 'esm' ? packageJson.module : packageJson.main; @@ -88,13 +109,13 @@ const getOutput = (packageJson, overrides = {}) => { }; /** - * @param {{module: string; main: string;}} packageJson - * @returns {import('rollup').RollupOptions[]} + * @param {PackageJson} packageJson + * @returns {Promise} */ export const getDefaultBuildConfigs = async (packageJson) => { // Verify if we have anything else to build from plugins. const pkgs = glob.sync('packages/plugins/**/package.json', { cwd: CWD }); - const pluginBuilds = []; + const subBuilds = []; for (const pkg of pkgs) { const { default: content } = await import(path.resolve(CWD, pkg), { assert: { type: 'json' }, @@ -108,7 +129,7 @@ export const getDefaultBuildConfigs = async (packageJson) => { `Will also build ${chalk.green.bold(content.name)} additional files: ${chalk.green.bold(Object.keys(content.toBuild).join(', '))}`, ); - pluginBuilds.push( + subBuilds.push( ...Object.entries(content.toBuild).map(([name, config]) => { return bundle(packageJson, { plugins: [esbuild()], @@ -140,10 +161,9 @@ export const getDefaultBuildConfigs = async (packageJson) => { getOutput(packageJson, { format: 'cjs' }), ], }), - ...pluginBuilds, + ...subBuilds, // Bundle type definitions. // FIXME: This build is sloooow. - // Check https://github.com/timocov/dts-bundle-generator bundle(packageJson, { plugins: [dts()], output: {