diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3232dfbb2..a8cb30c31 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,25 +16,126 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install Node ${{matrix.node.node-version || matrix.node.node-version-file}} uses: actions/setup-node@v4 with: ${{matrix.node}} + + - name: Cache build:all + id: cache-build + uses: actions/cache@v4 + with: + path: packages/published/**/dist + key: ${{ matrix.node }}-cache-build-${{ hashFiles('packages/published/**', 'yarn.lock') }} + - run: yarn install - - run: yarn build:all - - run: yarn test + + - name: Build all plugins + if: steps.cache-build.outputs.cache-hit != 'true' + run: yarn build:all + + - run: yarn test:unit + + e2e: + strategy: + fail-fast: false + matrix: + node: + - 18 + + timeout-minutes: 10 + + name: End to End + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Node ${{matrix.node}}.x + uses: actions/setup-node@v4 + with: + node-version: ${{matrix.node}}.x + + - name: Cache build:all + id: cache-build + uses: actions/cache@v4 + with: + path: packages/published/**/dist + key: ${{ matrix.node }}-cache-build-${{ hashFiles('packages/published/**', 'yarn.lock') }} + + - name: Cache playwright binaries + id: cache-playwright-binaries + uses: actions/cache@v4 + with: + path: | + ~/.cache/ms-playwright + ~/Library/Caches/ms-playwright + %USERPROFILE%\AppData\Local\ms-playwright + key: cache-playwright-binaries-${{ hashFiles('yarn.lock') }} + + - run: yarn install + + - name: Install playwright + run: yarn workspace @dd/tests playwright install --with-deps + + - name: Build all plugins + if: steps.cache-build.outputs.cache-hit != 'true' + run: yarn build:all + + - run: yarn test:e2e + + - name: Save playwright cache + if: always() && steps.cache-playwright-binaries.outputs.cache-hit != 'true' + id: save-playwright-cache + uses: actions/cache/save@v4 + with: + path: | + ~/.cache/ms-playwright + ~/Library/Caches/ms-playwright + %USERPROFILE%\AppData\Local\ms-playwright + key: cache-playwright-binaries-${{ hashFiles('yarn.lock') }} + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() && failure() }} + with: + name: playwright + path: | + packages/tests/playwright-report + packages/tests/test-results + retention-days: 3 lint: + strategy: + matrix: + node: + - 18 + name: Linting runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Install node + + - name: Install Node ${{matrix.node}}.x uses: actions/setup-node@v4 with: node-version-file: 'package.json' + + - name: Cache build:all + id: cache-build + uses: actions/cache@v4 + with: + path: packages/published/**/dist + key: ${{ matrix.node }}-cache-build-${{ hashFiles('packages/published/**', 'yarn.lock') }} + - run: yarn install - - run: yarn build:all + + - name: Build all plugins + if: steps.cache-build.outputs.cache-hit != 'true' + run: yarn build:all + - run: yarn typecheck:all + - run: yarn cli integrity + - run: git diff --exit-code && git diff --cached --exit-code || (echo "Please run 'yarn cli integrity' and commit the result." && exit 1) diff --git a/.yarn/cache/@playwright-test-npm-1.49.1-6026636721-bb0d5eda58.zip b/.yarn/cache/@playwright-test-npm-1.49.1-6026636721-bb0d5eda58.zip new file mode 100644 index 000000000..611afa47a Binary files /dev/null and b/.yarn/cache/@playwright-test-npm-1.49.1-6026636721-bb0d5eda58.zip differ diff --git a/.yarn/cache/@rollup-plugin-esm-shim-npm-0.1.7-6570f9e71a-c3cc762ce7.zip b/.yarn/cache/@rollup-plugin-esm-shim-npm-0.1.7-6570f9e71a-c3cc762ce7.zip new file mode 100644 index 000000000..26985dd79 Binary files /dev/null and b/.yarn/cache/@rollup-plugin-esm-shim-npm-0.1.7-6570f9e71a-c3cc762ce7.zip differ diff --git a/.yarn/cache/@types-lodash-npm-4.17.14-6b38705727-6ee40725f3.zip b/.yarn/cache/@types-lodash-npm-4.17.14-6b38705727-6ee40725f3.zip new file mode 100644 index 000000000..e1c1ed3f5 Binary files /dev/null and b/.yarn/cache/@types-lodash-npm-4.17.14-6b38705727-6ee40725f3.zip differ diff --git a/.yarn/cache/@types-lodash.template-npm-4.5.3-a68fcd8d4e-7c9d32b8d8.zip b/.yarn/cache/@types-lodash.template-npm-4.5.3-a68fcd8d4e-7c9d32b8d8.zip new file mode 100644 index 000000000..5c51bf9cc Binary files /dev/null and b/.yarn/cache/@types-lodash.template-npm-4.5.3-a68fcd8d4e-7c9d32b8d8.zip differ diff --git a/.yarn/cache/fsevents-npm-2.3.2-a881d6ac9f-6b5b6f5692.zip b/.yarn/cache/fsevents-npm-2.3.2-a881d6ac9f-6b5b6f5692.zip new file mode 100644 index 000000000..816292417 Binary files /dev/null and b/.yarn/cache/fsevents-npm-2.3.2-a881d6ac9f-6b5b6f5692.zip differ diff --git a/.yarn/cache/fsevents-patch-19706e7e35-10.zip b/.yarn/cache/fsevents-patch-19706e7e35-10.zip new file mode 100644 index 000000000..aff1ab12c Binary files /dev/null and b/.yarn/cache/fsevents-patch-19706e7e35-10.zip differ diff --git a/.yarn/cache/lodash._reinterpolate-npm-3.0.0-3c62ca439e-06d2d5f331.zip b/.yarn/cache/lodash._reinterpolate-npm-3.0.0-3c62ca439e-06d2d5f331.zip new file mode 100644 index 000000000..cf016b22b Binary files /dev/null and b/.yarn/cache/lodash._reinterpolate-npm-3.0.0-3c62ca439e-06d2d5f331.zip differ diff --git a/.yarn/cache/lodash.template-npm-4.5.0-5272df3039-56d18ba410.zip b/.yarn/cache/lodash.template-npm-4.5.0-5272df3039-56d18ba410.zip new file mode 100644 index 000000000..325273d92 Binary files /dev/null and b/.yarn/cache/lodash.template-npm-4.5.0-5272df3039-56d18ba410.zip differ diff --git a/.yarn/cache/lodash.templatesettings-npm-4.2.0-15fbdebcf4-ef470fa8b6.zip b/.yarn/cache/lodash.templatesettings-npm-4.2.0-15fbdebcf4-ef470fa8b6.zip new file mode 100644 index 000000000..d1d09d5dd Binary files /dev/null and b/.yarn/cache/lodash.templatesettings-npm-4.2.0-15fbdebcf4-ef470fa8b6.zip differ diff --git a/.yarn/cache/playwright-core-npm-1.49.1-a372dbc965-baa39a5302.zip b/.yarn/cache/playwright-core-npm-1.49.1-a372dbc965-baa39a5302.zip new file mode 100644 index 000000000..c1722107a Binary files /dev/null and b/.yarn/cache/playwright-core-npm-1.49.1-a372dbc965-baa39a5302.zip differ diff --git a/.yarn/cache/playwright-npm-1.49.1-0a8fed5892-49fb063f4a.zip b/.yarn/cache/playwright-npm-1.49.1-0a8fed5892-49fb063f4a.zip new file mode 100644 index 000000000..d52f8a070 Binary files /dev/null and b/.yarn/cache/playwright-npm-1.49.1-0a8fed5892-49fb063f4a.zip differ diff --git a/LICENSES-3rdparty.csv b/LICENSES-3rdparty.csv index f8873962b..fb065bf4f 100644 --- a/LICENSES-3rdparty.csv +++ b/LICENSES-3rdparty.csv @@ -167,8 +167,10 @@ Component,Origin,Licence,Copyright @nodelib/fs.walk,npm,MIT,(https://www.npmjs.com/package/@nodelib/fs.walk) @pkgjs/parseargs,npm,MIT,(https://github.com/pkgjs/parseargs#readme) @pkgr/core,npm,MIT,JounQin (https://github.com/un-ts/pkgr/blob/master/packages/core) +@playwright/test,npm,Apache-2.0,Microsoft Corporation (https://playwright.dev) @rollup/plugin-babel,virtual,MIT,Rich Harris (https://github.com/rollup/plugins/tree/master/packages/babel#readme) @rollup/plugin-commonjs,virtual,MIT,Rich Harris (https://github.com/rollup/plugins/tree/master/packages/commonjs/#readme) +@rollup/plugin-esm-shim,virtual,MIT,Peter Placzek (https://github.com/rollup/plugins/tree/master/packages/esm-shim#readme) @rollup/plugin-json,virtual,MIT,rollup (https://github.com/rollup/plugins/tree/master/packages/json#readme) @rollup/plugin-node-resolve,virtual,MIT,Rich Harris (https://github.com/rollup/plugins/tree/master/packages/node-resolve/#readme) @rollup/plugin-terser,virtual,MIT,Peter Placzek (https://github.com/rollup/plugins/tree/master/packages/terser#readme) @@ -206,6 +208,8 @@ Component,Origin,Licence,Copyright @types/jest,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jest) @types/json-schema,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/json-schema) @types/json5,npm,MIT,Jason Swearingen (https://www.npmjs.com/package/@types/json5) +@types/lodash,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/lodash) +@types/lodash.template,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/lodash.template) @types/minimatch,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/minimatch) @types/mute-stream,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mute-stream) @types/node,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) @@ -641,9 +645,12 @@ listr2,npm,MIT,Cenk Kilic (https://srcs.kilic.dev) loader-runner,npm,MIT,Tobias Koppers @sokra (https://github.com/webpack/loader-runner#readme) loader-utils,npm,MIT,Tobias Koppers @sokra (https://www.npmjs.com/package/loader-utils) locate-path,npm,MIT,Sindre Sorhus (sindresorhus.com) +lodash._reinterpolate,npm,MIT,John-David Dalton (https://lodash.com/) lodash.debounce,npm,MIT,John-David Dalton (https://lodash.com/) lodash.memoize,npm,MIT,John-David Dalton (https://lodash.com/) lodash.merge,npm,MIT,John-David Dalton (https://lodash.com/) +lodash.template,npm,MIT,John-David Dalton (https://lodash.com/) +lodash.templatesettings,npm,MIT,John-David Dalton (https://lodash.com/) log-symbols,npm,MIT,Sindre Sorhus (sindresorhus.com) log-update,npm,MIT,Sindre Sorhus (sindresorhus.com) lru-cache,npm,ISC,Isaac Z. Schlueter (https://www.npmjs.com/package/lru-cache) @@ -740,6 +747,8 @@ picomatch,npm,MIT,Jon Schlinkert (https://github.com/micromatch/picomatch) pify,npm,MIT,Sindre Sorhus (sindresorhus.com) pirates,npm,MIT,Ari Porad (https://github.com/danez/pirates#readme) pkg-dir,npm,MIT,Sindre Sorhus (sindresorhus.com) +playwright,npm,Apache-2.0,Microsoft Corporation (https://playwright.dev) +playwright-core,npm,Apache-2.0,Microsoft Corporation (https://playwright.dev) please-upgrade-node,npm,MIT,typicode (https://github.com/typicode/please-upgrade-node#readme) posix-character-classes,npm,MIT,Jon Schlinkert (https://github.com/jonschlinkert/posix-character-classes) possible-typed-array-names,npm,MIT,Jordan Harband (https://github.com/ljharb/possible-typed-array-names#readme) diff --git a/package.json b/package.json index fbb2c2344..fa5594976 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "loop": "yarn workspaces foreach -Apti --include \"@datadog/*\" --exclude \"@datadog/build-plugins\"", "oss": "yarn cli oss -d packages -l mit", "publish:all": "yarn loop --no-private npm publish", - "test": "yarn workspace @dd/tests test", "typecheck:all": "yarn workspaces foreach -Apti run typecheck", "version:all": "yarn loop version --deferred ${0} && yarn version apply --all", "watch:all": "yarn loop run watch" diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 686abc9e9..13096e63a 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -14,3 +14,4 @@ export const FULL_NAME_BUNDLERS = [ 'webpack4', 'webpack5', ] as const; +export const ENV_VAR_REQUESTED_BUNDLERS = 'PLAYWRIGHT_REQUESTED_BUNDLERS'; diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 582d3d4d7..822c0c5be 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -11,7 +11,21 @@ import { glob } from 'glob'; import path from 'path'; import type { RequestInit } from 'undici-types'; -import type { GlobalContext, Logger, RequestOpts, ResolvedEntry } from './types'; +import type { + BuildReport, + Entry, + File, + GlobalContext, + Input, + Logger, + Output, + RequestOpts, + ResolvedEntry, + SerializedBuildReport, + SerializedEntry, + SerializedInput, + SerializedOutput, +} from './types'; // Format a duration 0h 0m 0s 0ms export const formatDuration = (duration: number) => { @@ -244,3 +258,155 @@ export const readJsonSync = (filepath: string) => { let index = 0; export const getUniqueId = () => `${Date.now()}.${performance.now()}.${++index}`; + +// Returns an object that is safe to serialize to JSON. +// Mostly useful for debugging and testing. +export const serializeBuildReport = (report: BuildReport): SerializedBuildReport => { + // Report is an object that self reference some of its values. + // To make it JSON serializable, we need to remove the self references + // and replace them with strings, we'll use "filepath" to still have them uniquely identifiable. + const jsonReport: SerializedBuildReport = { + bundler: report.bundler, + errors: report.errors, + warnings: report.warnings, + logs: report.logs, + start: report.start, + end: report.end, + duration: report.duration, + writeDuration: report.writeDuration, + entries: [], + inputs: [], + outputs: [], + }; + + for (const entry of report.entries || []) { + const newEntry: SerializedEntry = { ...entry, inputs: [], outputs: [] }; + if (entry.inputs) { + newEntry.inputs = entry.inputs.map((file: File) => file.filepath); + } + if (entry.outputs) { + newEntry.outputs = entry.outputs.map((file: File) => file.filepath); + } + jsonReport.entries.push(newEntry); + } + + for (const input of report.inputs || []) { + const newInput: SerializedInput = { ...input, dependencies: [], dependents: [] }; + if (input.dependencies) { + for (const dependency of input.dependencies) { + newInput.dependencies.push(dependency.filepath); + } + } + if (input.dependents) { + for (const dependent of input.dependents) { + newInput.dependents.push(dependent.filepath); + } + } + jsonReport.inputs.push(newInput); + } + + for (const output of report.outputs || []) { + const newOutput: SerializedOutput = { ...output, inputs: [] }; + if (output.inputs) { + newOutput.inputs = output.inputs.map((file: File) => file.filepath); + } + jsonReport.outputs.push(newOutput); + } + + return jsonReport; +}; + +// Returns an object that is unserialized from serializeBuildReport(). +// Mostly useful for debugging and testing. +export const unserializeBuildReport = (report: SerializedBuildReport): BuildReport => { + const buildReport: BuildReport = { + bundler: report.bundler, + errors: report.errors, + warnings: report.warnings, + logs: report.logs, + start: report.start, + end: report.end, + duration: report.duration, + writeDuration: report.writeDuration, + }; + + const reportInputs = report.inputs || []; + const reportOutputs = report.outputs || []; + + const entries: Entry[] = []; + + // Prefill inputs and outputs as they are sometimes self-referencing themselves. + const indexedInputs: Map = new Map(); + const inputs: Input[] = reportInputs.map((input) => { + const newInput: Input = { + ...input, + // Keep them empty for now, we'll fill them later. + dependencies: new Set(), + dependents: new Set(), + }; + indexedInputs.set(input.filepath, newInput); + return newInput; + }); + + const indexedOutputs: Map = new Map(); + const outputs: Output[] = reportOutputs.map((output) => { + const newOutput: Output = { ...output, inputs: [] }; + indexedOutputs.set(output.filepath, newOutput); + return newOutput; + }); + + // Fill in the inputs' dependencies and dependents. + for (const input of reportInputs) { + const newInput: Input = indexedInputs.get(input.filepath)!; + + // Re-assign the dependencies and dependents to the actual objects. + if (input.dependencies) { + for (const dependency of input.dependencies) { + const newDependency = indexedInputs.get(dependency)!; + newInput.dependencies.add(newDependency); + } + } + if (input.dependents) { + for (const dependent of input.dependents) { + const newDependent = indexedInputs.get(dependent)!; + newInput.dependents.add(newDependent); + } + } + } + + // Fill in the outputs' inputs. + for (const output of reportOutputs) { + const newOutput: Output = indexedOutputs.get(output.filepath)!; + if (output.inputs) { + // Re-assign the inputs to the actual objects. + newOutput.inputs = output.inputs + .map< + // Can be either an input or an output (for sourcemaps). + Input | Output | undefined + >((filepath: string) => indexedInputs.get(filepath) || indexedOutputs.get(filepath)) + .filter(Boolean) as (Input | Output)[]; + } + } + + for (const entry of report.entries || []) { + const newEntry: Entry = { ...entry, inputs: [], outputs: [] }; + if (entry.inputs) { + newEntry.inputs = entry.inputs + .map((filepath: string) => indexedInputs.get(filepath)) + .filter(Boolean) as (Output | Input)[]; + } + if (entry.outputs) { + newEntry.outputs = entry.outputs + .map((filepath: string) => indexedOutputs.get(filepath)) + .filter(Boolean) as Output[]; + } + entries.push(newEntry); + } + + return { + ...buildReport, + entries, + inputs, + outputs, + }; +}; diff --git a/packages/plugins/build-report/src/helpers.ts b/packages/plugins/build-report/src/helpers.ts index 449fce03c..aeaee5dcb 100644 --- a/packages/plugins/build-report/src/helpers.ts +++ b/packages/plugins/build-report/src/helpers.ts @@ -4,18 +4,7 @@ import { INJECTED_FILE } from '@dd/core/constants'; import { isInjectionFile } from '@dd/core/helpers'; -import type { - BuildReport, - SerializedEntry, - File, - GlobalContext, - SerializedInput, - SerializedBuildReport, - SerializedOutput, - Entry, - Input, - Output, -} from '@dd/core/types'; +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. @@ -43,158 +32,6 @@ export const getType = (name: string): string => { return getExtension(cleanPath(name)) || 'unknown'; }; -// Returns an object that is safe to serialize to JSON. -// Mostly useful for debugging and testing. -export const serializeBuildReport = (report: BuildReport): SerializedBuildReport => { - // Report is an object that self reference some of its values. - // To make it JSON serializable, we need to remove the self references - // and replace them with strings, we'll use "filepath" to still have them uniquely identifiable. - const jsonReport: SerializedBuildReport = { - bundler: report.bundler, - errors: report.errors, - warnings: report.warnings, - logs: report.logs, - start: report.start, - end: report.end, - duration: report.duration, - writeDuration: report.writeDuration, - entries: [], - inputs: [], - outputs: [], - }; - - for (const entry of report.entries || []) { - const newEntry: SerializedEntry = { ...entry, inputs: [], outputs: [] }; - if (entry.inputs) { - newEntry.inputs = entry.inputs.map((file: File) => file.filepath); - } - if (entry.outputs) { - newEntry.outputs = entry.outputs.map((file: File) => file.filepath); - } - jsonReport.entries.push(newEntry); - } - - for (const input of report.inputs || []) { - const newInput: SerializedInput = { ...input, dependencies: [], dependents: [] }; - if (input.dependencies) { - for (const dependency of input.dependencies) { - newInput.dependencies.push(dependency.filepath); - } - } - if (input.dependents) { - for (const dependent of input.dependents) { - newInput.dependents.push(dependent.filepath); - } - } - jsonReport.inputs.push(newInput); - } - - for (const output of report.outputs || []) { - const newOutput: SerializedOutput = { ...output, inputs: [] }; - if (output.inputs) { - newOutput.inputs = output.inputs.map((file: File) => file.filepath); - } - jsonReport.outputs.push(newOutput); - } - - return jsonReport; -}; - -// Returns an object that is unserialized from serializeBuildReport(). -// Mostly useful for debugging and testing. -export const unserializeBuildReport = (report: SerializedBuildReport): BuildReport => { - const buildReport: BuildReport = { - bundler: report.bundler, - errors: report.errors, - warnings: report.warnings, - logs: report.logs, - start: report.start, - end: report.end, - duration: report.duration, - writeDuration: report.writeDuration, - }; - - const reportInputs = report.inputs || []; - const reportOutputs = report.outputs || []; - - const entries: Entry[] = []; - - // Prefill inputs and outputs as they are sometimes self-referencing themselves. - const indexedInputs: Map = new Map(); - const inputs: Input[] = reportInputs.map((input) => { - const newInput: Input = { - ...input, - // Keep them empty for now, we'll fill them later. - dependencies: new Set(), - dependents: new Set(), - }; - indexedInputs.set(input.filepath, newInput); - return newInput; - }); - - const indexedOutputs: Map = new Map(); - const outputs: Output[] = reportOutputs.map((output) => { - const newOutput: Output = { ...output, inputs: [] }; - indexedOutputs.set(output.filepath, newOutput); - return newOutput; - }); - - // Fill in the inputs' dependencies and dependents. - for (const input of reportInputs) { - const newInput: Input = indexedInputs.get(input.filepath)!; - - // Re-assign the dependencies and dependents to the actual objects. - if (input.dependencies) { - for (const dependency of input.dependencies) { - const newDependency = indexedInputs.get(dependency)!; - newInput.dependencies.add(newDependency); - } - } - if (input.dependents) { - for (const dependent of input.dependents) { - const newDependent = indexedInputs.get(dependent)!; - newInput.dependents.add(newDependent); - } - } - } - - // Fill in the outputs' inputs. - for (const output of reportOutputs) { - const newOutput: Output = indexedOutputs.get(output.filepath)!; - if (output.inputs) { - // Re-assign the inputs to the actual objects. - newOutput.inputs = output.inputs - .map< - // Can be either an input or an output (for sourcemaps). - Input | Output | undefined - >((filepath: string) => indexedInputs.get(filepath) || indexedOutputs.get(filepath)) - .filter(Boolean) as (Input | Output)[]; - } - } - - for (const entry of report.entries || []) { - const newEntry: Entry = { ...entry, inputs: [], outputs: [] }; - if (entry.inputs) { - newEntry.inputs = entry.inputs - .map((filepath: string) => indexedInputs.get(filepath)) - .filter(Boolean) as (Output | Input)[]; - } - if (entry.outputs) { - newEntry.outputs = entry.outputs - .map((filepath: string) => indexedOutputs.get(filepath)) - .filter(Boolean) as Output[]; - } - entries.push(newEntry); - } - - return { - ...buildReport, - entries, - inputs, - outputs, - }; -}; - const BUNDLER_SPECIFICS = ['unknown', 'commonjsHelpers.js', 'vite/preload-helper.js']; // Make list of paths unique, remove the current file and particularities. export const cleanReport = ( diff --git a/packages/plugins/injection/src/xpack.ts b/packages/plugins/injection/src/xpack.ts index 254ae35df..e4b1c6071 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, outputFile, rm } from '@dd/core/helpers'; +import { getUniqueId, outputFileSync, 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'; @@ -42,6 +42,12 @@ export const getXpackPlugin = `${getUniqueId()}.${InjectPosition.MIDDLE}.${INJECTED_FILE}.js`, ); + // 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, ''); + } + // Handle the InjectPosition.MIDDLE. type Entry = typeof compiler.options.entry; // TODO: Move this into @dd/core, add rspack/webpack types and tests. @@ -95,18 +101,9 @@ export const getXpackPlugin = 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); }); @@ -166,4 +163,8 @@ export const getXpackPlugin = compilation.hooks.optimizeChunkAssets.tap({ name: PLUGIN_NAME }, hookCb); } }); + + // We inject the new entry. + const newEntry = injectEntry(compiler.options.entry); + compiler.options.entry = newEntry; }; diff --git a/packages/plugins/telemetry/src/common/output/text.ts b/packages/plugins/telemetry/src/common/output/text.ts index 1be2299ec..fac1b8e78 100644 --- a/packages/plugins/telemetry/src/common/output/text.ts +++ b/packages/plugins/telemetry/src/common/output/text.ts @@ -2,9 +2,8 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { formatDuration, truncateString } from '@dd/core/helpers'; +import { formatDuration, serializeBuildReport, truncateString } from '@dd/core/helpers'; import type { Logger, Entry, GlobalContext, Output } from '@dd/core/types'; -import { serializeBuildReport } from '@dd/internal-build-report-plugin/helpers'; import chalk from 'chalk'; import prettyBytes from 'pretty-bytes'; diff --git a/packages/published/esbuild-plugin/package.json b/packages/published/esbuild-plugin/package.json index 4cc424b7b..fd291184d 100644 --- a/packages/published/esbuild-plugin/package.json +++ b/packages/published/esbuild-plugin/package.json @@ -20,7 +20,9 @@ }, "main": "./dist/src/index.js", "module": "./dist/src/index.mjs", + "types": "./dist/src/index.d.ts", "exports": { + "./dist/src": "./dist/src/index.js", ".": "./src/index.ts" }, "publishConfig": { @@ -63,6 +65,7 @@ "@dd/tools": "workspace:*", "@rollup/plugin-babel": "6.0.4", "@rollup/plugin-commonjs": "28.0.1", + "@rollup/plugin-esm-shim": "0.1.7", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "15.3.0", "@rollup/plugin-terser": "0.4.4", diff --git a/packages/published/rollup-plugin/package.json b/packages/published/rollup-plugin/package.json index a810d24ab..9a88e5594 100644 --- a/packages/published/rollup-plugin/package.json +++ b/packages/published/rollup-plugin/package.json @@ -20,7 +20,9 @@ }, "main": "./dist/src/index.js", "module": "./dist/src/index.mjs", + "types": "./dist/src/index.d.ts", "exports": { + "./dist/src": "./dist/src/index.js", ".": "./src/index.ts" }, "publishConfig": { @@ -63,6 +65,7 @@ "@dd/tools": "workspace:*", "@rollup/plugin-babel": "6.0.4", "@rollup/plugin-commonjs": "28.0.1", + "@rollup/plugin-esm-shim": "0.1.7", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "15.3.0", "@rollup/plugin-terser": "0.4.4", diff --git a/packages/published/rspack-plugin/package.json b/packages/published/rspack-plugin/package.json index e2a9a7fbf..ad3fcf395 100644 --- a/packages/published/rspack-plugin/package.json +++ b/packages/published/rspack-plugin/package.json @@ -20,7 +20,9 @@ }, "main": "./dist/src/index.js", "module": "./dist/src/index.mjs", + "types": "./dist/src/index.d.ts", "exports": { + "./dist/src": "./dist/src/index.js", ".": "./src/index.ts" }, "publishConfig": { @@ -63,6 +65,7 @@ "@dd/tools": "workspace:*", "@rollup/plugin-babel": "6.0.4", "@rollup/plugin-commonjs": "28.0.1", + "@rollup/plugin-esm-shim": "0.1.7", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "15.3.0", "@rollup/plugin-terser": "0.4.4", diff --git a/packages/published/vite-plugin/package.json b/packages/published/vite-plugin/package.json index 6ab6230c5..621fbca5a 100644 --- a/packages/published/vite-plugin/package.json +++ b/packages/published/vite-plugin/package.json @@ -20,7 +20,9 @@ }, "main": "./dist/src/index.js", "module": "./dist/src/index.mjs", + "types": "./dist/src/index.d.ts", "exports": { + "./dist/src": "./dist/src/index.js", ".": "./src/index.ts" }, "publishConfig": { @@ -63,6 +65,7 @@ "@dd/tools": "workspace:*", "@rollup/plugin-babel": "6.0.4", "@rollup/plugin-commonjs": "28.0.1", + "@rollup/plugin-esm-shim": "0.1.7", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "15.3.0", "@rollup/plugin-terser": "0.4.4", diff --git a/packages/published/webpack-plugin/package.json b/packages/published/webpack-plugin/package.json index 506760381..f4a189514 100644 --- a/packages/published/webpack-plugin/package.json +++ b/packages/published/webpack-plugin/package.json @@ -20,7 +20,9 @@ }, "main": "./dist/src/index.js", "module": "./dist/src/index.mjs", + "types": "./dist/src/index.d.ts", "exports": { + "./dist/src/*": "./dist/src/*", ".": "./src/index.ts" }, "publishConfig": { @@ -63,6 +65,7 @@ "@dd/tools": "workspace:*", "@rollup/plugin-babel": "6.0.4", "@rollup/plugin-commonjs": "28.0.1", + "@rollup/plugin-esm-shim": "0.1.7", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "15.3.0", "@rollup/plugin-terser": "0.4.4", diff --git a/packages/tests/.gitignore b/packages/tests/.gitignore new file mode 100644 index 000000000..584439da2 --- /dev/null +++ b/packages/tests/.gitignore @@ -0,0 +1,5 @@ +# Playwright stuffs +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/packages/tests/README.md b/packages/tests/README.md index edde36c76..59d81e2c2 100644 --- a/packages/tests/README.md +++ b/packages/tests/README.md @@ -10,32 +10,39 @@ Especially useful for having mock projects, built with specific bundlers and run -- [Run all the tests](#run-all-the-tests) -- [Debug a test](#debug-a-test) -- [Test a plugin](#test-a-plugin) - - [Bootstrapping your test](#bootstrapping-your-test) - - [Bundlers](#bundlers) - - [More complex projects](#more-complex-projects) - - [Work with the global context](#work-with-the-global-context) +- [Unit tests](#unit-tests) + - [Run](#run) + - [Debug](#debug) + - [Test a plugin](#test-a-plugin) +- [End to End tests](#end-to-end-tests) + - [Run](#run) + - [Debug](#debug) -## Run all the tests +## Unit tests + +Place your tests in `packages/tests/src/unit/plugins//**/*.test.ts`.
+ +### Run ```bash -yarn test +yarn test:unit +yarn workspace @dd/tests test ``` -## Debug a test +You can use [jest flags](https://jestjs.io/docs/cli) directly after the command. + +### Debug You can target a single file the same as if you were using Jest's CLI. Within your test you can then use `.only` or `.skip` to target a single test in particular. ```bash -yarn test packages/tests/... +yarn test:unit packages/tests/... ``` -## Test a plugin +### Test a plugin Once you have your plugin ready, you can test it in two ways, both are not exclusive. @@ -45,7 +52,7 @@ This doesn't need much explanation as it is pretty straight-forward. Or the **integration** way, which will test the plugin within the whole ecosystem, but is a bit more involved to setup correctly.
Let's talk about this a bit more. -### Bootstrapping your test +#### Bootstrapping your test Here's a bootstrap to get you going: @@ -77,9 +84,9 @@ describe('My very awesome plugin', () => { }); ``` -### Bundlers +#### Bundlers -We currently support `webpack4`, `webpack5`, `esbuild`, `rollup` and `vite`.
+We currently support `webpack4`, `webpack5`, `rspack`, `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/easy_project/main.js).
@@ -89,22 +96,23 @@ During development, you may want to target a specific bundler, to reduce noise f For this, you can use the `--bundlers=,` flag when running your tests: ```bash -yarn test packages/tests/... --bundlers=webpack4,esbuild +yarn test:unit packages/tests/... --bundlers=webpack4,esbuild ``` If you want to keep the built files for debugging purpose, you can use the `--cleanup=0` parameter: ```bash -yarn test packages/tests/... --cleanup=0 +yarn test:unit packages/tests/... --cleanup=0 ``` If you want to also build the bundlers you're targeting, you can use the `--build=1` parameter: ```bash -yarn test packages/tests/... --build=1 +# Will also build both webpack and esbuild plugins before running the tests. +yarn test:unit packages/tests/... --build=1 --bundlers=webpack4,esbuild ``` -### More complex projects +#### More complex projects We also have [a more complex project](/packages/tests/src/_jest/fixtures/project), with third parties dependencies for instance, that you can use with the `getComplexBuildOverrides()` function.
To be used as follow: @@ -160,12 +168,12 @@ describe('Some very massive project', () => { > `generateProject()` is not persistent. > So for now it's only to be used to debug your plugin when necessary. -### Work with the global context +#### Work with the global context The global context is pretty nifty to share data between plugins.
But, it is a mutable object, so you'll have to keep that in mind when testing around it. -The best way would be to freeze the content you need to test, at the moment you want to test it: +The best way would be to freeze the content you need to test, at the moment you want to test it, for instance, to capture the initial context using `JSON.parse(JSON.stringify(context.bundler))` to freeze it: ```typescript import type { GlobalContext, Options } from '@dd/core/types'; @@ -184,7 +192,7 @@ describe('Global Context Plugin', () => { customPlugins: (opts, context) => { const bundlerName = context.bundler.fullName; // Freeze the context here, to verify what's available during initialization. - initialContexts[bundlerName] = JSON.parse(JSON.stringify(context)); + initialContexts[bundlerName] = JSON.parse(JSON.stringify(context.bundler)); return []; }, }; @@ -219,10 +227,7 @@ buildReports[bundlerName] = unserializeBuildReport(serializeBuildReport(context. Giving the following, more involved example: ```typescript -import { - serializeBuildReport, - unserializeBuildReport, -} from '@dd/core/plugins/build-report/helpers'; +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'; @@ -262,3 +267,66 @@ describe('Build Reports', () => { }); }); ``` + +## End to End tests + +We use [Playwright](https://playwright.dev/) for our end to end tests. + +Place your tests in `packages/tests/src/e2e/**/*.spec.ts`. + +The test run takes care of building the `@datadog/*-plugin` packages locally.
+You can bypass this build step prefixing your command with `CI=1 yarn [...]` reducing the duration of the run. + +### Run + +```bash +yarn test:e2e +``` + +You can use [Playwright flags](https://playwright.dev/docs/running-tests#command-line) directly after the command. + +### Debug + +#### From the CI + +If your CI job fails, you can download the `playwright` artifact of the run, at the bottom of the summary page. + +Once downloaded, extract it by double clicking on it and run the following command: + +```bash +yarn workspace @dd/tests playwright show-report ~/Downloads/playwright/playwright-report +``` + +#### Locally + +Run the test with the UI enabled: + +```bash +yarn test:e2e --ui +``` + +Then, you can use the Playwright UI to debug your test. + +More information on the [Playwright documentation](https://playwright.dev/docs/running-tests#command-line). + + +#### Run a specific bundler or browser + +There is one project for each bundler / browser combination.
+The naming follows the pattern ` | ` eg. `chrome | webpack4`. + +You can use the `--project` flag to target a specific project (or multiple projects): + +```bash +yarn test:e2e --project "chrome | webpack4" --project "firefox | esbuild" +``` + +It also supports glob patterns: + +```bash +# Run all the bundlers for the chrome browser. +yarn test:e2e --project "chrome | *" + +# Run all browsers for the webpack4 bundler. +yarn test:e2e --project "* | webpack4" +``` diff --git a/packages/tests/jest.config.js b/packages/tests/jest.config.js index 2b2598680..796986f99 100644 --- a/packages/tests/jest.config.js +++ b/packages/tests/jest.config.js @@ -9,7 +9,7 @@ module.exports = { preset: 'ts-jest/presets/js-with-ts', // Without it, vite import is silently crashing the process with code SIGHUP 129 resetModules: true, - roots: ['./src/'], + roots: ['./src/unit/'], setupFilesAfterEnv: ['/src/_jest/setupAfterEnv.ts'], testEnvironment: 'node', testMatch: ['**/*.test.*'], diff --git a/packages/tests/package.json b/packages/tests/package.json index b7db57e06..c757b4208 100644 --- a/packages/tests/package.json +++ b/packages/tests/package.json @@ -14,13 +14,13 @@ "exports": { "./*": "./src/*.ts", "./_jest/fixtures/*": "./src/_jest/fixtures/*", - "./_jest/helpers/*": "./src/_jest/helpers/*.ts", - "./plugins/telemetry/*": "./src/plugins/telemetry/*.ts" + "./_jest/helpers/*": "./src/_jest/helpers/*.ts" }, "scripts": { "build": "yarn clean && tsc", "clean": "rm -rf dist", - "test": "JEST_CONFIG_TRANSPILE_ONLY=true VITE_CJS_IGNORE_WARNING=true NODE_OPTIONS=\"--openssl-legacy-provider --experimental-vm-modules ${NODE_OPTIONS:-}\" jest", + "test:e2e": "FORCE_COLOR=true playwright test", + "test:unit": "FORCE_COLOR=true JEST_CONFIG_TRANSPILE_ONLY=true VITE_CJS_IGNORE_WARNING=true NODE_OPTIONS=\"--openssl-legacy-provider --experimental-vm-modules ${NODE_OPTIONS:-}\" jest", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -36,6 +36,7 @@ "@dd/internal-git-plugin": "workspace:*", "@dd/internal-injection-plugin": "workspace:*", "@dd/telemetry-plugin": "workspace:*", + "@dd/tools": "workspace:*", "@rollup/plugin-commonjs": "28.0.1", "clipanion": "4.0.0-rc.3", "glob": "11.0.0", @@ -43,6 +44,7 @@ "ts-jest": "29.1.2" }, "devDependencies": { + "@playwright/test": "1.49.1", "@rspack/core": "1.1.2", "@types/faker": "5.5.9", "@types/jest": "29.5.12", diff --git a/packages/tests/playwright.config.ts b/packages/tests/playwright.config.ts new file mode 100644 index 000000000..4251ae213 --- /dev/null +++ b/packages/tests/playwright.config.ts @@ -0,0 +1,72 @@ +// 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 { DEV_SERVER_PORT, DEV_SERVER_URL, PUBLIC_DIR } from '@dd/tests/_playwright/constants'; +import { getRequestedBundlers } from '@dd/tests/_playwright/helpers/requestedBundlers'; +import type { TestOptions } from '@dd/tests/_playwright/testParams'; +import { ROOT } from '@dd/tools/constants'; +import { defineConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './src/e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? 'html' : 'list', + /* Path to file run before all the tests. See https://playwright.dev/docs/test-global-setup-teardown */ + globalSetup: require.resolve('./src/_playwright/globalSetup.ts'), + use: { + bundlers: getRequestedBundlers(), + trace: 'retain-on-failure', + }, + timeout: 5_000, + /* Configure projects for each bundler */ + // TODO Also build and test for ESM. + projects: FULL_NAME_BUNDLERS.map((bundler) => [ + { + name: `chrome | ${bundler}`, + use: { + ...devices['Desktop Chrome'], + bundler, + }, + }, + { + name: `firefox | ${bundler}`, + use: { + ...devices['Desktop Firefox'], + bundler, + }, + }, + { + name: `edge | ${bundler}`, + use: { + ...devices['Desktop Edge'], + bundler, + }, + }, + { + name: `safari | ${bundler}`, + use: { + ...devices['Desktop Safari'], + bundler, + }, + }, + ]).flat(), + + /* Run your local dev server before starting the tests */ + webServer: { + command: `yarn cli dev-server --root=${PUBLIC_DIR} --port=${DEV_SERVER_PORT}`, + cwd: ROOT, + url: DEV_SERVER_URL, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/packages/tests/src/_jest/helpers/configBundlers.ts b/packages/tests/src/_jest/helpers/configBundlers.ts index 365d4723f..31d65d197 100644 --- a/packages/tests/src/_jest/helpers/configBundlers.ts +++ b/packages/tests/src/_jest/helpers/configBundlers.ts @@ -7,22 +7,28 @@ import { datadogRollupPlugin } from '@datadog/rollup-plugin'; import { datadogRspackPlugin } from '@datadog/rspack-plugin'; import { datadogVitePlugin } from '@datadog/vite-plugin'; import type { Options } from '@dd/core/types'; -import commonjs from '@rollup/plugin-commonjs'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; +import { + configEsbuild, + configRollup, + configRspack, + configVite, + configWebpack4, + configWebpack5, +} from '@dd/tools/bundlers'; import type { RspackOptions } from '@rspack/core'; import type { BuildOptions } from 'esbuild'; import path from 'path'; import type { RollupOptions } from 'rollup'; import type { UserConfig } from 'vite'; -import type { Configuration as Configuration4, Plugin } from 'webpack4'; +import type { Configuration as Configuration4 } from 'webpack4'; import webpack4 from 'webpack4'; import type { Configuration } from 'webpack5'; import webpack5 from 'webpack5'; import { getOutDir } from './env'; +import { getWebpackPlugin } from './getWebpackPlugin'; import { defaultEntry, defaultPluginOptions } from './mocks'; import type { BundlerOptionsOverrides } from './types'; -import { getBaseXpackConfig, getWebpackPlugin } from './xpackConfigs'; export const getRspackOptions = ( workingDir: string, @@ -34,9 +40,15 @@ export const getRspackOptions = ( ...pluginOverrides, }; + const plugin = datadogRspackPlugin(newPluginOptions); + return { - ...(getBaseXpackConfig(workingDir, 'rspack') as RspackOptions), - plugins: [datadogRspackPlugin(newPluginOptions)], + ...configRspack({ + workingDir, + entry: { main: path.resolve(workingDir, defaultEntry) }, + outDir: getOutDir(workingDir, 'rspack'), + plugins: [plugin], + }), ...bundlerOverrides, }; }; @@ -54,8 +66,12 @@ export const getWebpack5Options = ( const plugin = getWebpackPlugin(newPluginOptions, webpack5); return { - ...getBaseXpackConfig(workingDir, 'webpack5'), - plugins: [plugin], + ...configWebpack5({ + workingDir, + entry: { main: path.resolve(workingDir, defaultEntry) }, + outDir: getOutDir(workingDir, 'webpack5'), + plugins: [plugin], + }), ...bundlerOverrides, }; }; @@ -73,8 +89,12 @@ export const getWebpack4Options = ( const plugin = getWebpackPlugin(newPluginOptions, webpack4); return { - ...getBaseXpackConfig(workingDir, 'webpack4'), - plugins: [plugin as unknown as Plugin], + ...configWebpack4({ + workingDir, + entry: { main: path.resolve(workingDir, defaultEntry) }, + outDir: getOutDir(workingDir, 'webpack4'), + plugins: [plugin], + }), node: false, ...bundlerOverrides, }; @@ -91,42 +111,18 @@ export const getEsbuildOptions = ( }; return { - absWorkingDir: workingDir, - bundle: true, - chunkNames: 'chunk.[hash]', - entryPoints: { main: defaultEntry }, - entryNames: '[name]', + ...configEsbuild({ + workingDir, + entry: { main: defaultEntry }, + outDir: getOutDir(workingDir, 'esbuild'), + plugins: [datadogEsbuildPlugin(newPluginOptions)], + }), format: 'esm', - outdir: getOutDir(workingDir, 'esbuild'), - plugins: [datadogEsbuildPlugin(newPluginOptions)], - sourcemap: true, splitting: true, ...bundlerOverrides, }; }; -export const getRollupBaseConfig = (workingDir: string, bundlerName: string): RollupOptions => { - const outDir = getOutDir(workingDir, bundlerName); - return { - input: path.resolve(workingDir, defaultEntry), - onwarn: (warning, handler) => { - if ( - !/Circular dependency:/.test(warning.message) && - !/Sourcemap is likely to be incorrect/.test(warning.message) - ) { - return handler(warning); - } - }, - output: { - chunkFileNames: 'chunk.[hash].js', - compact: false, - dir: outDir, - entryFileNames: '[name].js', - sourcemap: true, - }, - }; -}; - export const getRollupOptions = ( workingDir: string, pluginOverrides: Partial = {}, @@ -137,15 +133,15 @@ export const getRollupOptions = ( ...pluginOverrides, }; - const baseConfig = getRollupBaseConfig(workingDir, 'rollup'); + const baseConfig = configRollup({ + workingDir, + entry: { main: defaultEntry }, + outDir: getOutDir(workingDir, 'rollup'), + plugins: [datadogRollupPlugin(newPluginOptions)], + }); return { ...baseConfig, - plugins: [ - commonjs(), - datadogRollupPlugin(newPluginOptions), - nodeResolve({ preferBuiltins: true, browser: true }), - ], ...bundlerOverrides, output: { ...baseConfig.output, @@ -164,23 +160,26 @@ export const getViteOptions = ( ...pluginOverrides, }; - const baseConfig = getRollupBaseConfig(workingDir, 'vite'); + const baseConfig = configVite({ + workingDir, + entry: { main: defaultEntry }, + outDir: getOutDir(workingDir, 'vite'), + plugins: [datadogVitePlugin(newPluginOptions)], + }); return { root: workingDir, + ...baseConfig, build: { - assetsDir: '', // Disable assets dir to simplify the test. - minify: false, + ...baseConfig.build, rollupOptions: { - ...baseConfig, + ...baseConfig.build?.rollupOptions, ...bundlerOverrides, output: { - ...baseConfig.output, + ...baseConfig.build?.rollupOptions?.output, ...bundlerOverrides.output, }, }, }, - logLevel: 'silent', - plugins: [datadogVitePlugin(newPluginOptions)], }; }; diff --git a/packages/tests/src/_jest/helpers/xpackConfigs.ts b/packages/tests/src/_jest/helpers/getWebpackPlugin.ts similarity index 50% rename from packages/tests/src/_jest/helpers/xpackConfigs.ts rename to packages/tests/src/_jest/helpers/getWebpackPlugin.ts index 877442eb8..e54cca53e 100644 --- a/packages/tests/src/_jest/helpers/xpackConfigs.ts +++ b/packages/tests/src/_jest/helpers/getWebpackPlugin.ts @@ -4,36 +4,10 @@ import type { Options } from '@dd/core/types'; import { buildPluginFactory } from '@dd/factory'; -import type { RspackOptions } from '@rspack/core'; -import path from 'path'; import type webpack4 from 'webpack4'; -import type { Configuration as Configuration4 } from 'webpack4'; -import type { Configuration as Configuration5 } from 'webpack5'; import type webpack5 from 'webpack5'; import { PLUGIN_VERSIONS } from './constants'; -import { getOutDir } from './env'; -import { defaultEntry } from './mocks'; - -export const getBaseXpackConfig = ( - workingDir: string, - bundlerName: string, -): Configuration5 & Configuration4 & RspackOptions => { - const outDir = getOutDir(workingDir, bundlerName); - return { - context: workingDir, - entry: path.resolve(workingDir, defaultEntry), - mode: 'production', - output: { - path: outDir, - filename: `[name].js`, - }, - devtool: 'source-map', - optimization: { - minimize: false, - }, - }; -}; // Return the correct plugin for webpack 4 or 5. export const getWebpackPlugin = ( diff --git a/packages/tests/src/_jest/helpers/mocks.ts b/packages/tests/src/_jest/helpers/mocks.ts index 0cd2d5320..520ff73e9 100644 --- a/packages/tests/src/_jest/helpers/mocks.ts +++ b/packages/tests/src/_jest/helpers/mocks.ts @@ -2,26 +2,23 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { outputJsonSync } from '@dd/core/helpers'; import type { BuildReport, File, - GetCustomPlugins, GetPluginsOptions, GlobalContext, - IterableElement, Logger, LogLevel, Options, } from '@dd/core/types'; -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 { 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'; import type { PluginBuild } from 'esbuild'; import path from 'path'; import type { BundlerOptionsOverrides, BundlerOverrides } from './types'; -import { getBaseXpackConfig } from './xpackConfigs'; export const FAKE_URL = 'https://example.com'; export const API_PATH = '/v2/srcmap'; @@ -173,7 +170,7 @@ export const getNodeSafeBuildOverrides = ( 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/dist', 'fake_bundler'); + const baseWebpack = configXpack({ workingDir: 'fake_cwd', outDir: 'dist', entry: {} }); const bundlerOverrides: Required = { rollup: { ...overridesResolved.rollup, @@ -244,82 +241,6 @@ export const getMirroredFixtures = (paths: string[], cwd: string) => { return fixtures; }; -// 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, - }, - ]; -}; - // Filter out stuff from the build report. export const filterOutParticularities = (input: File) => // Vite injects its own preloader helper. diff --git a/packages/tests/src/_jest/helpers/runBundlers.ts b/packages/tests/src/_jest/helpers/runBundlers.ts index 59f8fb7b0..0d74a6958 100644 --- a/packages/tests/src/_jest/helpers/runBundlers.ts +++ b/packages/tests/src/_jest/helpers/runBundlers.ts @@ -4,12 +4,20 @@ 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 { + buildWithEsbuild, + buildWithRollup, + buildWithRspack, + buildWithVite, + buildWithWebpack4, + buildWithWebpack5, +} from '@dd/tools/bundlers'; +import { buildPlugins, green } from '@dd/tools/helpers'; +import type { RspackOptions } from '@rspack/core'; import type { BuildOptions } from 'esbuild'; import type { RollupOptions } from 'rollup'; -import type { Configuration as Configuration4, Stats as Stats4 } from 'webpack4'; -import type { Configuration, Stats } from 'webpack5'; +import type { Configuration as Configuration4 } from 'webpack4'; +import type { Configuration } from 'webpack5'; import { getEsbuildOptions, @@ -32,42 +40,6 @@ import type { // Get the environment variables. const { NO_CLEANUP, NEED_BUILD, REQUESTED_BUNDLERS } = process.env; -const xpackCallback = ( - err: Error | null, - stats: Stats4 | Stats | RspackStats | undefined, - resolve: (value: unknown) => void, - reject: (reason?: any) => void, - delay: number = 0, -) => { - if (err) { - reject(err); - return; - } - - if (!stats) { - reject('No stats returned.'); - return; - } - - const { errors, warnings } = stats.compilation; - if (errors?.length) { - reject(errors[0]); - return; - } - - if (warnings?.length) { - console.warn(warnings.join('\n')); - } - - // Delay the resolve to give time to the bundler to finish writing the files. - // Webpack4 in particular is impacted by this and otherwise triggers a - // "Jest did not exit one second after the test run has completed." warning. - // TODO: Investigate this need for a delay after webpack 4's build. - setTimeout(() => { - resolve(stats); - }, delay); -}; - const getCleanupFunction = (bundlerName: string, outdirs: (string | undefined)[]): CleanupFn => async () => { @@ -95,20 +67,7 @@ export const runRspack: BundlerRunFunction = async ( bundlerOverrides: Partial = {}, ) => { const bundlerConfigs = getRspackOptions(workingDir, pluginOverrides, bundlerOverrides); - const { rspack } = await import('@rspack/core'); - const errors = []; - - try { - await new Promise((resolve, reject) => { - rspack(bundlerConfigs, (err, stats) => { - xpackCallback(err, stats, resolve, reject); - }); - }); - } catch (e: any) { - console.error(`Build failed for Rspack`, e); - errors.push(`[RSPACK] : ${e.message}`); - } - + const { errors } = await buildWithRspack(bundlerConfigs); return { cleanup: getCleanupFunction('Rspack', [bundlerConfigs.output?.path]), errors }; }; @@ -118,20 +77,7 @@ export const runWebpack5: BundlerRunFunction = async ( bundlerOverrides: Partial = {}, ) => { const bundlerConfigs = getWebpack5Options(workingDir, pluginOverrides, bundlerOverrides); - const { webpack } = await import('webpack5'); - const errors = []; - - try { - await new Promise((resolve, reject) => { - webpack(bundlerConfigs, (err, stats) => { - xpackCallback(err, stats, resolve, reject); - }); - }); - } catch (e: any) { - console.error(`Build failed for Webpack 5`, e); - errors.push(`[WEBPACK5] : ${e.message}`); - } - + const { errors } = await buildWithWebpack5(bundlerConfigs); return { cleanup: getCleanupFunction('Webpack 5', [bundlerConfigs.output?.path]), errors }; }; @@ -141,20 +87,7 @@ export const runWebpack4: BundlerRunFunction = async ( bundlerOverrides: Partial = {}, ) => { const bundlerConfigs = getWebpack4Options(workingDir, pluginOverrides, bundlerOverrides); - const webpack = (await import('webpack4')).default; - const errors = []; - - try { - await new Promise((resolve, reject) => { - webpack(bundlerConfigs, (err, stats) => { - xpackCallback(err, stats, resolve, reject, 600); - }); - }); - } catch (e: any) { - console.error(`Build failed for Webpack 4`, e); - errors.push(`[WEBPACK4] : ${e.message}`); - } - + const { errors } = await buildWithWebpack4(bundlerConfigs); return { cleanup: getCleanupFunction('Webpack 4', [bundlerConfigs.output?.path]), errors }; }; @@ -164,16 +97,7 @@ export const runEsbuild: BundlerRunFunction = async ( bundlerOverrides: Partial = {}, ) => { const bundlerConfigs = getEsbuildOptions(workingDir, pluginOverrides, bundlerOverrides); - const { build } = await import('esbuild'); - const errors = []; - - try { - await build(bundlerConfigs); - } catch (e: any) { - console.error(`Build failed for ESBuild`, e); - errors.push(`[ESBUILD] : ${e.message}`); - } - + const { errors } = await buildWithEsbuild(bundlerConfigs); return { cleanup: getCleanupFunction('ESBuild', [bundlerConfigs.outdir]), errors }; }; @@ -183,14 +107,7 @@ export const runVite: BundlerRunFunction = async ( bundlerOverrides: Partial = {}, ) => { const bundlerConfigs = getViteOptions(workingDir, pluginOverrides, bundlerOverrides); - const vite = await import('vite'); - const errors = []; - try { - await vite.build(bundlerConfigs); - } catch (e: any) { - console.error(`Build failed for Vite`, e); - errors.push(`[VITE] : ${e.message}`); - } + const { errors } = await buildWithVite(bundlerConfigs); const outdirs: (string | undefined)[] = []; if (Array.isArray(bundlerConfigs.build?.rollupOptions?.output)) { @@ -208,28 +125,7 @@ export const runRollup: BundlerRunFunction = async ( bundlerOverrides: Partial = {}, ) => { const bundlerConfigs = getRollupOptions(workingDir, pluginOverrides, bundlerOverrides); - const { rollup } = await import('rollup'); - const errors = []; - - try { - const result = await rollup(bundlerConfigs); - - // Write out the results. - if (bundlerConfigs.output) { - const outputProms = []; - const outputOptions = Array.isArray(bundlerConfigs.output) - ? bundlerConfigs.output - : [bundlerConfigs.output]; - for (const outputOption of outputOptions) { - outputProms.push(result.write(outputOption)); - } - - await Promise.all(outputProms); - } - } catch (e: any) { - console.error(`Build failed for Rollup`, e); - errors.push(`[ROLLUP] : ${e.message}`); - } + const { errors } = await buildWithRollup(bundlerConfigs); const outdirs: (string | undefined)[] = []; if (Array.isArray(bundlerConfigs.output)) { @@ -287,15 +183,10 @@ export const BUNDLERS: Bundler[] = allBundlers.filter( // Build only if needed. if (NEED_BUILD) { - const bundlersToBuild = new Set( - BUNDLERS.map((bundler) => `@datadog/${bundler.name.replace(/\d/g, '')}-plugin`), - ); - - 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']); - } + const bundlersToBuild = BUNDLERS.map(({ name }) => name); + console.log(`[BUILD] Building ${green(bundlersToBuild.join(', '))}...`); + buildPlugins(bundlersToBuild); + console.log(`[BUILD] Done.`); } export const runBundlers = async ( diff --git a/packages/tests/src/_playwright/constants.ts b/packages/tests/src/_playwright/constants.ts new file mode 100644 index 000000000..d8018dbf0 --- /dev/null +++ b/packages/tests/src/_playwright/constants.ts @@ -0,0 +1,10 @@ +// 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 { ROOT } from '@dd/tools/constants'; +import path from 'path'; + +export const PUBLIC_DIR = path.resolve(ROOT, 'packages/tests/src/_playwright/public'); +export const DEV_SERVER_PORT = 8000; +export const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`; diff --git a/packages/tests/src/_playwright/globalSetup.ts b/packages/tests/src/_playwright/globalSetup.ts new file mode 100644 index 000000000..ed4bdb914 --- /dev/null +++ b/packages/tests/src/_playwright/globalSetup.ts @@ -0,0 +1,47 @@ +// 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 { ENV_VAR_REQUESTED_BUNDLERS } from '@dd/core/constants'; +import { rm } from '@dd/core/helpers'; +import type { BundlerFullName } from '@dd/core/types'; +import { getRequestedBundlers } from '@dd/tests/_playwright/helpers/requestedBundlers'; +import type { TestOptions } from '@dd/tests/_playwright/testParams'; +import { blue, buildPlugins, dim, green } from '@dd/tools/helpers'; +import type { FullConfig } from '@playwright/test'; +import { glob } from 'glob'; +import path from 'path'; + +// TODO Also build and test for ESM. +const globalSetup = async (config: FullConfig) => { + const getPfx = (name: string) => `[${blue(name)}] `; + const getSubPfx = (name: string) => ` ${dim(getPfx(name))}`; + const globalPfx = getPfx('Global Setup'); + console.time(globalPfx); + const requestedBundlers = getRequestedBundlers(); + // Save the requested bundlers in the env. + process.env[ENV_VAR_REQUESTED_BUNDLERS] = requestedBundlers.join(','); + console.log(`${globalPfx}Setting up tests.`); + + // In the CI we're building before the job starts. + // No need to do it here. + if (!process.env.CI) { + // Build the requested bundler plugins. + const buildPluginsPfx = getSubPfx('Build Plugins'); + console.time(buildPluginsPfx); + console.log(`${buildPluginsPfx}Building ${green(requestedBundlers.join(', '))} plugins...`); + buildPlugins(requestedBundlers as BundlerFullName[]); + console.timeEnd(buildPluginsPfx); + } + + // Delete public dirs. + const cleanPfx = getSubPfx('Clean'); + console.time(cleanPfx); + const publicDirs = await glob('public/*/', { cwd: __dirname }); + await Promise.all(publicDirs.map((dir) => rm(path.resolve(__dirname, dir)))); + console.timeEnd(cleanPfx); + + console.timeEnd(globalPfx); +}; + +export default globalSetup; diff --git a/packages/tests/src/_playwright/helpers/buildProject.ts b/packages/tests/src/_playwright/helpers/buildProject.ts new file mode 100644 index 000000000..32a2a5845 --- /dev/null +++ b/packages/tests/src/_playwright/helpers/buildProject.ts @@ -0,0 +1,128 @@ +// 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 { mkdir, rm } from '@dd/core/helpers'; +import type { BundlerFullName } from '@dd/core/types'; +import { allBundlers } from '@dd/tools/bundlers'; +import { dim } from '@dd/tools/helpers'; +import { allPlugins, fullConfig } from '@dd/tools/plugins'; +import fs from 'fs'; +import path from 'path'; + +// Build a given project with a given bundler. +const buildProject = async (bundler: BundlerFullName, cwd: string) => { + const plugin = allPlugins[bundler](fullConfig); + const build = allBundlers[bundler]; + const buildConfig = build.config({ + workingDir: cwd, + // We'll use the name of the bundler as the name of the entry file. + // eg: For "Webpack 4" we'll have "./projects/dist/webpack4.js". + entry: { [bundler]: './index.js' }, + outDir: path.resolve(cwd, './dist'), + plugins: [plugin], + }); + + return build.run(buildConfig); +}; + +// Build a given project with a list of bundlers. +const buildProjectWithBundlers = async (projectPath: string, bundlers: BundlerFullName[]) => { + const name = projectPath.split(path.sep).pop() || 'unknown'; + + // Clean the dist folders. + await rm(path.resolve(projectPath, 'dist')); + + // Build with all the bundlers. + return Promise.all( + bundlers.map(async (bundler) => { + const buildBundlerPfx = ` [${dim(`Build ${name} with ${bundler}`)}]`; + console.time(buildBundlerPfx); + const { errors } = await buildProject(bundler, projectPath); + console.timeEnd(buildBundlerPfx); + return errors; + }), + ); +}; + +// Wrapper around the buildProjectWithBundlers function. +// - Create the destination folder. +// - Copy the content of the source folder in it. +// - Build the project with all the requested bundlers. +// - Delete the folder if the build failed. +// - Touch a "built" file if the build succeeded. +const handleBuild = async (source: string, destination: string, bundlers: BundlerFullName[]) => { + // Create the project dir. + await mkdir(destination); + // Copy the content of our project in it. + await fs.promises.cp(`${source}/`, `${destination}/`, { + recursive: true, + errorOnExist: true, + force: true, + }); + + // Build it with all the requested bundlers. + const name = destination.split(path.sep).pop() || 'unknown'; + const buildProjectPfx = ` [${dim(name)}] `; + console.time(buildProjectPfx); + const errors = (await buildProjectWithBundlers(destination, bundlers)).flat(); + + if (errors.length) { + console.error(`${buildProjectPfx}Build failed.`, errors); + // Delete the folder, so other tests can try and build it. + await rm(destination); + } else { + // Touch the built file so other tests know it's ready. + await fs.promises.writeFile(`${destination}/built`, ''); + } + + console.timeEnd(buildProjectPfx); +}; + +// Wait for the build to be done or to fail. +// Based on the presence of the "built" file or the disparition of the project folder. +const waitForBuild = async (projectDir: string): Promise<{ built: boolean; error: boolean }> => { + return new Promise((resolve) => { + const builtInterval = setInterval(() => { + if (fs.existsSync(`${projectDir}/built`)) { + clearInterval(builtInterval); + clearInterval(errorInterval); + resolve({ built: true, error: false }); + } + }, 100); + + const errorInterval = setInterval(() => { + if (!fs.existsSync(projectDir)) { + clearInterval(errorInterval); + clearInterval(builtInterval); + resolve({ built: false, error: true }); + } + }, 100); + }); +}; + +// Verify if the project has been built. +// Trigger the build if it's not been done yet. +// Wait for the build to be done. +// Note: This is to be used in a beforeAll hook, +// so all the workers can use the same build of a given suite. +export const verifyProjectBuild = async ( + source: string, + destination: string, + bundlers: BundlerFullName[], +) => { + // Wait a random time to avoid conflicts. + await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 500))); + + // Verify if the build as started, by checking the presence of the public directory. + const dirExists = fs.existsSync(destination); + if (dirExists) { + const result = await waitForBuild(destination); + if (result.error) { + await verifyProjectBuild(source, destination, bundlers); + } + } else { + // Build the project. + await handleBuild(source, destination, bundlers); + } +}; diff --git a/packages/tests/src/_playwright/helpers/requestedBundlers.ts b/packages/tests/src/_playwright/helpers/requestedBundlers.ts new file mode 100644 index 000000000..07fcbde58 --- /dev/null +++ b/packages/tests/src/_playwright/helpers/requestedBundlers.ts @@ -0,0 +1,55 @@ +// 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 { ENV_VAR_REQUESTED_BUNDLERS, FULL_NAME_BUNDLERS } from '@dd/core/constants'; +import type { BundlerFullName } from '@dd/core/types'; +import { yellow } from '@dd/tools/helpers'; + +const getBundlerNameFromProject = (projectName: string) => { + return projectName.split(' | ')[1] as BundlerFullName; +}; + +// Parse and detect all the --project arguments to get the requested bundlers. +export const getRequestedBundlers = ( + baseBundlers: readonly BundlerFullName[] = FULL_NAME_BUNDLERS, +): BundlerFullName[] => { + if (process.env[ENV_VAR_REQUESTED_BUNDLERS]) { + return process.env[ENV_VAR_REQUESTED_BUNDLERS].split(',') as BundlerFullName[]; + } + + const requestedBundlers: Set = new Set(); + let capture = false; + + for (const arg of process.argv) { + let bundlerName: BundlerFullName; + if (arg === '--project') { + // Capture the next argument as the bundler name. + capture = true; + continue; + } + + if (capture === true) { + // Capture pass. + bundlerName = getBundlerNameFromProject(arg.trim()); + capture = false; + } else if (arg.startsWith('--project=')) { + // Argument is already in the format --project=... + bundlerName = getBundlerNameFromProject(arg.split('=')[1].trim()); + } else { + continue; + } + + if (baseBundlers.includes(bundlerName) && FULL_NAME_BUNDLERS.includes(bundlerName)) { + requestedBundlers.add(bundlerName); + } else { + console.warn(yellow(`Bundler "${bundlerName}" is not available.`)); + } + } + + const requestBundlers: BundlerFullName[] = requestedBundlers.size + ? Array.from(requestedBundlers) + : [...baseBundlers]; + + return requestBundlers; +}; diff --git a/packages/tests/src/_playwright/public/.gitignore b/packages/tests/src/_playwright/public/.gitignore new file mode 100644 index 000000000..a2632cea4 --- /dev/null +++ b/packages/tests/src/_playwright/public/.gitignore @@ -0,0 +1,6 @@ +# We don't want anything else in this folder. +# It is used for e2e tests to serve their own apps. +* +!.gitignore +!404.html +!index.html diff --git a/packages/tests/src/_playwright/public/404.html b/packages/tests/src/_playwright/public/404.html new file mode 100644 index 000000000..f8d67d86c --- /dev/null +++ b/packages/tests/src/_playwright/public/404.html @@ -0,0 +1,17 @@ + + + + + + + + 404 Not Found + + + +

404 - Page Not Found for {{bundler}}

+

Sorry, the page you are looking for does not exist.

+ Go back to Home Page + + + diff --git a/packages/tests/src/_playwright/public/index.html b/packages/tests/src/_playwright/public/index.html new file mode 100644 index 000000000..c4a889feb --- /dev/null +++ b/packages/tests/src/_playwright/public/index.html @@ -0,0 +1,15 @@ + + + + + + + + Welcome + + + +

Welcome

+ + + diff --git a/packages/tests/src/_playwright/testParams.ts b/packages/tests/src/_playwright/testParams.ts new file mode 100644 index 000000000..66d1dcaf7 --- /dev/null +++ b/packages/tests/src/_playwright/testParams.ts @@ -0,0 +1,42 @@ +// 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 { DEV_SERVER_URL, PUBLIC_DIR } from '@dd/tests/_playwright/constants'; +import { test as base } from '@playwright/test'; +import path from 'path'; + +export type TestOptions = { + bundler: BundlerFullName; + bundlers: BundlerFullName[]; +}; + +type Fixtures = { + devServerUrl: string; + publicDir: string; + suiteName: string; +}; + +export const test = base.extend({ + // Default value, will be overridden by config. + bundler: ['rollup', { option: true }], + bundlers: [[...FULL_NAME_BUNDLERS], { option: true }], + devServerUrl: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use, info) => { + const url = info.config.webServer?.url || DEV_SERVER_URL; + await use(url); + }, + { auto: true }, + ], + suiteName: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use, info) => { + await use(path.dirname(info.file).split(path.sep).pop() || 'unknown'); + }, + { auto: true }, + ], + publicDir: PUBLIC_DIR, +}); diff --git a/packages/tests/src/e2e/smokeTest/project/index.html b/packages/tests/src/e2e/smokeTest/project/index.html new file mode 100644 index 000000000..9d63a04cc --- /dev/null +++ b/packages/tests/src/e2e/smokeTest/project/index.html @@ -0,0 +1,18 @@ + + + + + + + + Basic HTML Page + + + +

Welcome to the {{bundler}} Home page

+

This is just some simple text.

+ + + + + diff --git a/packages/tests/src/e2e/smokeTest/project/index.js b/packages/tests/src/e2e/smokeTest/project/index.js new file mode 100644 index 000000000..d7a4a42ff --- /dev/null +++ b/packages/tests/src/e2e/smokeTest/project/index.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, {{bundler}}!'); diff --git a/packages/tests/src/e2e/smokeTest/smokeTest.spec.ts b/packages/tests/src/e2e/smokeTest/smokeTest.spec.ts new file mode 100644 index 000000000..4b2078f11 --- /dev/null +++ b/packages/tests/src/e2e/smokeTest/smokeTest.spec.ts @@ -0,0 +1,65 @@ +// 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 { verifyProjectBuild } from '@dd/tests/_playwright/helpers/buildProject'; +import type { TestOptions } from '@dd/tests/_playwright/testParams'; +import { test } from '@dd/tests/_playwright/testParams'; +import type { Page } from '@playwright/test'; +import path from 'path'; + +// Have a similar experience to Jest. +const { expect, beforeAll, describe } = test; + +const userFlow = async (url: string, page: Page, bundler: TestOptions['bundler']) => { + // Navigate to our page. + await page.goto(`${url}/index.html?context_bundler=${bundler}`); + await page.waitForSelector('body'); +}; + +describe('Smoke Test', () => { + // Build our fixture project. + beforeAll(async ({ publicDir, bundlers, suiteName }) => { + const source = path.resolve(__dirname, 'project'); + const destination = path.resolve(publicDir, suiteName); + await verifyProjectBuild(source, destination, bundlers); + }); + + test('Should load the page without errors', async ({ + page, + bundler, + browserName, + suiteName, + devServerUrl, + }) => { + const errors: string[] = []; + const testBaseUrl = `${devServerUrl}/${suiteName}`; + + // Listen for errors on the page. + page.on('pageerror', (error) => errors.push(error.message)); + page.on('response', async (response) => { + if (!response.ok()) { + const url = response.request().url(); + const prefix = `[${bundler} ${browserName} ${response.status()}]`; + errors.push(`${prefix} ${url}`); + } + }); + + // Verify that we do log the expected things. + const logs: string[] = []; + page.on('console', async (msg) => { + for (const arg of msg.args()) { + // eslint-disable-next-line no-await-in-loop + logs.push(await arg.jsonValue()); + } + }); + + // It should load the correct bundler file too. + const bundleRequest = page.waitForResponse(`${testBaseUrl}/dist/${bundler}.js`); + await userFlow(testBaseUrl, page, bundler); + expect((await bundleRequest).ok()).toBe(true); + + expect(logs).toEqual([`Hello, ${bundler}!`]); + expect(errors).toHaveLength(0); + }); +}); diff --git a/packages/tests/src/core/helpers.test.ts b/packages/tests/src/unit/core/helpers.test.ts similarity index 100% rename from packages/tests/src/core/helpers.test.ts rename to packages/tests/src/unit/core/helpers.test.ts diff --git a/packages/tests/src/factory/helpers.test.ts b/packages/tests/src/unit/factory/helpers.test.ts similarity index 100% rename from packages/tests/src/factory/helpers.test.ts rename to packages/tests/src/unit/factory/helpers.test.ts diff --git a/packages/tests/src/factory/index.test.ts b/packages/tests/src/unit/factory/index.test.ts similarity index 100% rename from packages/tests/src/factory/index.test.ts rename to packages/tests/src/unit/factory/index.test.ts diff --git a/packages/tests/src/plugins/build-report/helpers.test.ts b/packages/tests/src/unit/plugins/build-report/helpers.test.ts similarity index 100% rename from packages/tests/src/plugins/build-report/helpers.test.ts rename to packages/tests/src/unit/plugins/build-report/helpers.test.ts diff --git a/packages/tests/src/plugins/build-report/index.test.ts b/packages/tests/src/unit/plugins/build-report/index.test.ts similarity index 99% rename from packages/tests/src/plugins/build-report/index.test.ts rename to packages/tests/src/unit/plugins/build-report/index.test.ts index c251fd262..ef9bf1b96 100644 --- a/packages/tests/src/plugins/build-report/index.test.ts +++ b/packages/tests/src/unit/plugins/build-report/index.test.ts @@ -2,6 +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 type { Input, Entry, @@ -11,13 +12,8 @@ import type { BuildReport, SerializedInput, } from '@dd/core/types'; -import { - serializeBuildReport, - unserializeBuildReport, -} from '@dd/internal-build-report-plugin/helpers'; import { generateProject } from '@dd/tests/_jest/helpers/generateMassiveProject'; import { - debugFilesPlugins, defaultEntry, defaultPluginOptions, filterOutParticularities, @@ -29,6 +25,7 @@ import type { CleanupEverythingFn, CleanupFn, } 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) => { diff --git a/packages/tests/src/plugins/bundler-report/index.test.ts b/packages/tests/src/unit/plugins/bundler-report/index.test.ts similarity index 100% rename from packages/tests/src/plugins/bundler-report/index.test.ts rename to packages/tests/src/unit/plugins/bundler-report/index.test.ts diff --git a/packages/tests/src/plugins/error-tracking/index.test.ts b/packages/tests/src/unit/plugins/error-tracking/index.test.ts similarity index 100% rename from packages/tests/src/plugins/error-tracking/index.test.ts rename to packages/tests/src/unit/plugins/error-tracking/index.test.ts diff --git a/packages/tests/src/plugins/error-tracking/sourcemaps/files.test.ts b/packages/tests/src/unit/plugins/error-tracking/sourcemaps/files.test.ts similarity index 100% rename from packages/tests/src/plugins/error-tracking/sourcemaps/files.test.ts rename to packages/tests/src/unit/plugins/error-tracking/sourcemaps/files.test.ts diff --git a/packages/tests/src/plugins/error-tracking/sourcemaps/payload.test.ts b/packages/tests/src/unit/plugins/error-tracking/sourcemaps/payload.test.ts similarity index 100% rename from packages/tests/src/plugins/error-tracking/sourcemaps/payload.test.ts rename to packages/tests/src/unit/plugins/error-tracking/sourcemaps/payload.test.ts diff --git a/packages/tests/src/plugins/error-tracking/sourcemaps/sender.test.ts b/packages/tests/src/unit/plugins/error-tracking/sourcemaps/sender.test.ts similarity index 100% rename from packages/tests/src/plugins/error-tracking/sourcemaps/sender.test.ts rename to packages/tests/src/unit/plugins/error-tracking/sourcemaps/sender.test.ts diff --git a/packages/tests/src/plugins/error-tracking/testHelpers.ts b/packages/tests/src/unit/plugins/error-tracking/testHelpers.ts similarity index 100% rename from packages/tests/src/plugins/error-tracking/testHelpers.ts rename to packages/tests/src/unit/plugins/error-tracking/testHelpers.ts diff --git a/packages/tests/src/plugins/error-tracking/validate.test.ts b/packages/tests/src/unit/plugins/error-tracking/validate.test.ts similarity index 100% rename from packages/tests/src/plugins/error-tracking/validate.test.ts rename to packages/tests/src/unit/plugins/error-tracking/validate.test.ts diff --git a/packages/tests/src/plugins/git/helpers.test.ts b/packages/tests/src/unit/plugins/git/helpers.test.ts similarity index 100% rename from packages/tests/src/plugins/git/helpers.test.ts rename to packages/tests/src/unit/plugins/git/helpers.test.ts diff --git a/packages/tests/src/plugins/git/index.test.ts b/packages/tests/src/unit/plugins/git/index.test.ts similarity index 97% rename from packages/tests/src/plugins/git/index.test.ts rename to packages/tests/src/unit/plugins/git/index.test.ts index 30ab11a1a..c308bafca 100644 --- a/packages/tests/src/plugins/git/index.test.ts +++ b/packages/tests/src/unit/plugins/git/index.test.ts @@ -9,7 +9,7 @@ import { TrackedFilesMatcher } from '@dd/internal-git-plugin/trackedFilesMatcher import { API_PATH, FAKE_URL, 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 { getSourcemapsConfiguration } from '@dd/tests/plugins/error-tracking/testHelpers'; +import { getSourcemapsConfiguration } from '@dd/tests/unit/plugins/error-tracking/testHelpers'; import nock from 'nock'; jest.mock('@dd/internal-git-plugin/helpers', () => { diff --git a/packages/tests/src/plugins/git/trackedFilesMatcher.test.ts b/packages/tests/src/unit/plugins/git/trackedFilesMatcher.test.ts similarity index 100% rename from packages/tests/src/plugins/git/trackedFilesMatcher.test.ts rename to packages/tests/src/unit/plugins/git/trackedFilesMatcher.test.ts diff --git a/packages/tests/src/plugins/injection/helpers.test.ts b/packages/tests/src/unit/plugins/injection/helpers.test.ts similarity index 100% rename from packages/tests/src/plugins/injection/helpers.test.ts rename to packages/tests/src/unit/plugins/injection/helpers.test.ts diff --git a/packages/tests/src/plugins/injection/index.test.ts b/packages/tests/src/unit/plugins/injection/index.test.ts similarity index 98% rename from packages/tests/src/plugins/injection/index.test.ts rename to packages/tests/src/unit/plugins/injection/index.test.ts index 598b737df..172af7ac4 100644 --- a/packages/tests/src/plugins/injection/index.test.ts +++ b/packages/tests/src/unit/plugins/injection/index.test.ts @@ -6,15 +6,11 @@ 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, - getNodeSafeBuildOverrides, -} from '@dd/tests/_jest/helpers/mocks'; +import { getComplexBuildOverrides, getNodeSafeBuildOverrides } 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 { debugFilesPlugins, execute } from '@dd/tools/helpers'; import { readFileSync, writeFileSync } from 'fs'; import { glob } from 'glob'; import nock from 'nock'; diff --git a/packages/tests/src/plugins/telemetry/.eslintrc.js b/packages/tests/src/unit/plugins/telemetry/.eslintrc.js similarity index 100% rename from packages/tests/src/plugins/telemetry/.eslintrc.js rename to packages/tests/src/unit/plugins/telemetry/.eslintrc.js diff --git a/packages/tests/src/plugins/telemetry/common/aggregator.test.ts b/packages/tests/src/unit/plugins/telemetry/common/aggregator.test.ts similarity index 87% rename from packages/tests/src/plugins/telemetry/common/aggregator.test.ts rename to packages/tests/src/unit/plugins/telemetry/common/aggregator.test.ts index 027459d46..b713ceb45 100644 --- a/packages/tests/src/plugins/telemetry/common/aggregator.test.ts +++ b/packages/tests/src/unit/plugins/telemetry/common/aggregator.test.ts @@ -4,7 +4,7 @@ import { addMetrics } from '@dd/telemetry-plugin/common/aggregator'; import { getContextMock } from '@dd/tests/_jest/helpers/mocks'; -import { mockOptionsDD, mockReport } from '@dd/tests/plugins/telemetry/testHelpers'; +import { mockOptionsDD, mockReport } from '@dd/tests/unit/plugins/telemetry/testHelpers'; describe('Telemetry Aggregator', () => { test('Should aggregate metrics without throwing.', () => { diff --git a/packages/tests/src/plugins/telemetry/common/helpers.test.ts b/packages/tests/src/unit/plugins/telemetry/common/helpers.test.ts similarity index 100% rename from packages/tests/src/plugins/telemetry/common/helpers.test.ts rename to packages/tests/src/unit/plugins/telemetry/common/helpers.test.ts diff --git a/packages/tests/src/plugins/telemetry/common/output/files.test.ts b/packages/tests/src/unit/plugins/telemetry/common/output/files.test.ts similarity index 97% rename from packages/tests/src/plugins/telemetry/common/output/files.test.ts rename to packages/tests/src/unit/plugins/telemetry/common/output/files.test.ts index 7badb15a0..3a406a942 100644 --- a/packages/tests/src/plugins/telemetry/common/output/files.test.ts +++ b/packages/tests/src/unit/plugins/telemetry/common/output/files.test.ts @@ -5,7 +5,7 @@ import { outputFiles } from '@dd/telemetry-plugin/common/output/files'; import type { OutputOptions } from '@dd/telemetry-plugin/types'; import { mockLogger } from '@dd/tests/_jest/helpers/mocks'; -import { mockReport } from '@dd/tests/plugins/telemetry/testHelpers'; +import { mockReport } from '@dd/tests/unit/plugins/telemetry/testHelpers'; import { vol } from 'memfs'; import path from 'path'; diff --git a/packages/tests/src/plugins/telemetry/esbuild-plugin/plugins.test.ts b/packages/tests/src/unit/plugins/telemetry/esbuild-plugin/plugins.test.ts similarity index 94% rename from packages/tests/src/plugins/telemetry/esbuild-plugin/plugins.test.ts rename to packages/tests/src/unit/plugins/telemetry/esbuild-plugin/plugins.test.ts index 0ddd95dc9..df16f3b1b 100644 --- a/packages/tests/src/plugins/telemetry/esbuild-plugin/plugins.test.ts +++ b/packages/tests/src/unit/plugins/telemetry/esbuild-plugin/plugins.test.ts @@ -3,7 +3,7 @@ // Copyright 2019-Present Datadog, Inc. import { wrapPlugins, getResults } from '@dd/telemetry-plugin/esbuild-plugin/plugins'; -import { getMockBuild } from '@dd/tests/plugins/telemetry/testHelpers'; +import { getMockBuild } from '@dd/tests/unit/plugins/telemetry/testHelpers'; import type { PluginBuild, Plugin } from 'esbuild'; describe('Telemetry ESBuild Plugins', () => { diff --git a/packages/tests/src/plugins/telemetry/index.test.ts b/packages/tests/src/unit/plugins/telemetry/index.test.ts similarity index 99% rename from packages/tests/src/plugins/telemetry/index.test.ts rename to packages/tests/src/unit/plugins/telemetry/index.test.ts index ee80defa6..28b963d79 100644 --- a/packages/tests/src/plugins/telemetry/index.test.ts +++ b/packages/tests/src/unit/plugins/telemetry/index.test.ts @@ -7,12 +7,12 @@ import { addMetrics } from '@dd/telemetry-plugin/common/aggregator'; import type { MetricToSend } from '@dd/telemetry-plugin/types'; import { FAKE_URL, - debugFilesPlugins, filterOutParticularities, getComplexBuildOverrides, } from '@dd/tests/_jest/helpers/mocks'; import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers'; import type { Bundler, CleanupFn } from '@dd/tests/_jest/helpers/types'; +import { debugFilesPlugins } from '@dd/tools/helpers'; import nock from 'nock'; // Used to intercept metrics. diff --git a/packages/tests/src/plugins/telemetry/testHelpers.ts b/packages/tests/src/unit/plugins/telemetry/testHelpers.ts similarity index 100% rename from packages/tests/src/plugins/telemetry/testHelpers.ts rename to packages/tests/src/unit/plugins/telemetry/testHelpers.ts diff --git a/packages/tests/src/tools/src/commands/create-plugin/ask.test.ts b/packages/tests/src/unit/tools/src/commands/create-plugin/ask.test.ts similarity index 100% rename from packages/tests/src/tools/src/commands/create-plugin/ask.test.ts rename to packages/tests/src/unit/tools/src/commands/create-plugin/ask.test.ts diff --git a/packages/tests/src/tools/src/commands/create-plugin/hooks.test.ts b/packages/tests/src/unit/tools/src/commands/create-plugin/hooks.test.ts similarity index 100% rename from packages/tests/src/tools/src/commands/create-plugin/hooks.test.ts rename to packages/tests/src/unit/tools/src/commands/create-plugin/hooks.test.ts diff --git a/packages/tests/src/tools/src/commands/create-plugin/index.test.ts b/packages/tests/src/unit/tools/src/commands/create-plugin/index.test.ts similarity index 100% rename from packages/tests/src/tools/src/commands/create-plugin/index.test.ts rename to packages/tests/src/unit/tools/src/commands/create-plugin/index.test.ts diff --git a/packages/tests/src/tools/src/commands/integrity/files.test.ts b/packages/tests/src/unit/tools/src/commands/integrity/files.test.ts similarity index 100% rename from packages/tests/src/tools/src/commands/integrity/files.test.ts rename to packages/tests/src/unit/tools/src/commands/integrity/files.test.ts diff --git a/packages/tests/src/tools/src/helpers.test.ts b/packages/tests/src/unit/tools/src/helpers.test.ts similarity index 100% rename from packages/tests/src/tools/src/helpers.test.ts rename to packages/tests/src/unit/tools/src/helpers.test.ts diff --git a/packages/tests/src/tools/src/rollupConfig.test.ts b/packages/tests/src/unit/tools/src/rollupConfig.test.ts similarity index 94% rename from packages/tests/src/tools/src/rollupConfig.test.ts rename to packages/tests/src/unit/tools/src/rollupConfig.test.ts index ea5f9774c..9fd346efb 100644 --- a/packages/tests/src/tools/src/rollupConfig.test.ts +++ b/packages/tests/src/unit/tools/src/rollupConfig.test.ts @@ -17,6 +17,7 @@ import { } from '@dd/tests/_jest/helpers/configBundlers'; import { BUNDLER_VERSIONS } from '@dd/tests/_jest/helpers/constants'; import { getOutDir, prepareWorkingDir } from '@dd/tests/_jest/helpers/env'; +import { getWebpackPlugin } from '@dd/tests/_jest/helpers/getWebpackPlugin'; import { API_PATH, FAKE_URL, @@ -33,9 +34,8 @@ import { runWebpack5, } from '@dd/tests/_jest/helpers/runBundlers'; import type { CleanupFn } from '@dd/tests/_jest/helpers/types'; -import { getWebpackPlugin } from '@dd/tests/_jest/helpers/xpackConfigs'; import { ROOT } from '@dd/tools/constants'; -import { bgYellow, execute, green } from '@dd/tools/helpers'; +import { bgGreen, bgYellow, execute, green } from '@dd/tools/helpers'; import type { BuildOptions } from 'esbuild'; import fs from 'fs'; import { glob } from 'glob'; @@ -60,8 +60,9 @@ jest.mock('@datadog/webpack-plugin', () => ({ })); // Mock the plugin configuration of webpack to actually use the bundled plugin. -jest.mock('@dd/tests/_jest/helpers/xpackConfigs', () => { - const actual = jest.requireActual('@dd/tests/_jest/helpers/xpackConfigs'); +// And pass the correct bundler to it (webpack4 or webpack5). +jest.mock('@dd/tests/_jest/helpers/getWebpackPlugin', () => { + const actual = jest.requireActual('@dd/tests/_jest/helpers/getWebpackPlugin'); return { ...actual, getWebpackPlugin: jest.fn(), @@ -97,6 +98,17 @@ const getPackageDestination = (bundlerName: string) => { const stats = fs.statSync(packageDestination); const lastUpdateDuration = Math.ceil((new Date().getTime() - stats.mtimeMs) / 1000) * 1000; + // If we're in the CI it means we're using cached files. + if (process.env.CI) { + console.log( + bgGreen( + ` [CACHED] ${bundlerName}-plugin was built ${formatDuration(lastUpdateDuration)} ago.\n`, + ), + ); + // We don't want to block/alert on builds in CI. + return packageDestination; + } + // If last build was more than 10 minutes ago, warn the user. if (lastUpdateDuration > 1000 * 60 * 10) { console.log( @@ -208,7 +220,7 @@ describe('Bundling', () => { const actualConsoleError = jest.requireActual('console').error; // Filter out the errors we expect. const ignoredErrors = [ - // Used for Jest runtime in "yarn test". + // Used for Jest runtime in "yarn test:unit". 'ExperimentalWarning: VM Modules', // Used in our sourcemaps sender, to build a stream of our zipped sourcemaps. 'ExperimentalWarning: buffer.File', @@ -378,24 +390,25 @@ describe('Bundling', () => { }; // Webpack triggers some deprecations warnings only when we have multi-entry entries. - const webpackEntries = { + // Use a function to generate a new object each time. + const xpackEntries = () => ({ 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(rootDir, {}, overrides.rspack), - entry: webpackEntries, + entry: xpackEntries(), }; const webpack5Config = { ...getWebpack5Options(rootDir, {}, overrides.webpack5), - entry: webpackEntries, + entry: xpackEntries(), }; const webpack4Config = { ...getWebpack4Options(rootDir, {}, overrides.webpack4), - entry: webpackEntries, + entry: xpackEntries(), }; // Build the sequence. diff --git a/packages/tools/package.json b/packages/tools/package.json index 8d746da09..bb0697d86 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -14,28 +14,48 @@ "exports": { "./rollupConfig.mjs": "./src/rollupConfig.mjs", "./commands/oss/templates": "./src/commands/oss/templates.ts", + "./bundlers": "./src/bundlers.ts", "./*": "./src/*.ts" }, "scripts": { "cli": "ts-node -T --project ./tsconfig.json ./src/index.ts" }, "devDependencies": { + "@rollup/plugin-esm-shim": "0.1.7", "@types/chalk": "2.2.0", "@types/glob": "7.1.4", + "@types/lodash.template": "^4", "@types/node": "^18", + "@types/webpack4": "npm:@types/webpack@4.41.38", + "lodash.template": "4.5.0", "ts-node": "10.9.2", "typescript": "5.4.3" }, "dependencies": { + "@datadog/esbuild-plugin": "workspace:*", + "@datadog/rollup-plugin": "workspace:*", + "@datadog/rspack-plugin": "workspace:*", + "@datadog/vite-plugin": "workspace:*", + "@datadog/webpack-plugin": "workspace:*", "@dd/assets": "workspace:*", "@dd/core": "workspace:*", + "@dd/error-tracking-plugin": "workspace:*", + "@dd/telemetry-plugin": "workspace:*", "@inquirer/checkbox": "2.3.3", "@inquirer/input": "2.1.7", "@inquirer/select": "2.3.3", + "@rollup/plugin-commonjs": "28.0.1", + "@rollup/plugin-node-resolve": "15.3.0", + "@rspack/core": "1.1.2", "chalk": "2.3.1", "clipanion": "4.0.0-rc.3", + "esbuild": "0.24.0", "glob": "7.1.6", "outdent": "0.8.0", - "typanion": "3.14.0" + "typanion": "3.14.0", + "vite": "5.4.10", + "webpack": "5.92.1", + "webpack4": "npm:webpack@4.47.0", + "webpack5": "npm:webpack@5.92.1" } } diff --git a/packages/tools/src/bundlers.ts b/packages/tools/src/bundlers.ts new file mode 100644 index 000000000..ecdf356fe --- /dev/null +++ b/packages/tools/src/bundlers.ts @@ -0,0 +1,306 @@ +// 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 { BundlerFullName } from '@dd/core/types'; +import commonjs from '@rollup/plugin-commonjs'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import type { + RspackOptions, + Stats as RspackStats, + StatsCompilation as RspackStatsCompilation, +} from '@rspack/core'; +import type { BuildOptions, BuildResult } from 'esbuild'; +import path from 'path'; +import type { RollupOptions, RollupOutput } from 'rollup'; +import type { InlineConfig } from 'vite'; +import type { Configuration as Configuration4, Stats as Stats4 } from 'webpack4'; +import type { + Configuration as Configuration5, + Stats as Stats5, + StatsCompilation as StatsCompilation5, +} from 'webpack5'; + +export type BundlerOptions = + | RspackOptions + | Configuration4 + | Configuration5 + | BuildOptions + | RollupOptions + | InlineConfig; +export type BundlerConfig = { + workingDir: string; + outDir: string; + entry: { [name: string]: string }; + plugins?: any[]; +}; +export type BundlerConfigFunction = (config: BundlerConfig) => BundlerOptions; +export type BundlerRunFunction = ( + bundlerConfig: any, +) => Promise<{ errors: string[]; result?: any }>; + +const xpackCallback = ( + err: Error | null, + stats: Stats4 | Stats5 | RspackStats | undefined, + resolve: (value: unknown) => void, + reject: (reason?: any) => void, + delay: number = 0, +) => { + if (err) { + reject(err); + return; + } + + if (!stats) { + reject('No stats returned.'); + return; + } + + const { errors, warnings } = stats.compilation; + if (errors?.length) { + reject(errors[0]); + return; + } + + if (warnings?.length) { + console.warn(warnings.join('\n')); + } + + // Delay the resolve to give time to the bundler to finish writing the files. + // Webpack4 in particular is impacted by this and otherwise triggers a + // "Jest did not exit one second after the test run has completed." warning. + // TODO: Investigate this need for a delay after webpack 4's build. + setTimeout(() => { + resolve(stats); + }, delay); +}; + +export const buildWithRspack: BundlerRunFunction = async (bundlerConfig: RspackOptions) => { + const { rspack } = await import('@rspack/core'); + const errors = []; + let result: RspackStatsCompilation | undefined; + + try { + await new Promise((resolve, reject) => { + rspack(bundlerConfig, (err, stats) => { + result = stats?.toJson(); + xpackCallback(err, stats, resolve, reject); + }); + }); + } catch (e: any) { + errors.push(`[RSPACK] : ${e.message}`); + } + + return { errors, result }; +}; + +export const buildWithWebpack5: BundlerRunFunction = async (bundlerConfig: Configuration5) => { + const { default: webpack } = await import('webpack5'); + const errors = []; + let result: StatsCompilation5 | undefined; + + try { + await new Promise((resolve, reject) => { + webpack(bundlerConfig, (err, stats) => { + result = stats?.toJson(); + xpackCallback(err, stats, resolve, reject); + }); + }); + } catch (e: any) { + errors.push(`[WEBPACK5] : ${e.message}`); + } + + return { errors, result }; +}; + +export const buildWithWebpack4: BundlerRunFunction = async (bundlerConfig: Configuration4) => { + const webpack = (await import('webpack4')).default; + const errors = []; + let result: Stats4.ToJsonOutput | undefined; + + try { + await new Promise((resolve, reject) => { + webpack(bundlerConfig, (err, stats) => { + result = stats?.toJson(); + xpackCallback(err, stats, resolve, reject, 600); + }); + }); + } catch (e: any) { + errors.push(`[WEBPACK4] : ${e.message}`); + } + + return { errors, result }; +}; + +export const buildWithEsbuild: BundlerRunFunction = async (bundlerConfigs: BuildOptions) => { + const { build } = await import('esbuild'); + let result: BuildResult | undefined; + const errors = []; + + try { + result = await build(bundlerConfigs); + } catch (e: any) { + errors.push(`[ESBUILD] : ${e.message}`); + } + + return { errors, result }; +}; + +export const buildWithVite: BundlerRunFunction = async (bundlerConfig: InlineConfig) => { + const vite = await import('vite'); + const errors = []; + let result: Awaited> | undefined; + + try { + result = await vite.build(bundlerConfig); + } catch (e: any) { + errors.push(`[VITE] : ${e.message}`); + } + + return { errors, result }; +}; + +export const buildWithRollup: BundlerRunFunction = async (bundlerConfig: RollupOptions) => { + const { rollup } = await import('rollup'); + const errors = []; + let results: RollupOutput[] | undefined; + + try { + const result = await rollup(bundlerConfig); + + // Write out the results. + if (bundlerConfig.output) { + const outputProms = []; + const outputOptions = Array.isArray(bundlerConfig.output) + ? bundlerConfig.output + : [bundlerConfig.output]; + for (const outputOption of outputOptions) { + outputProms.push(result.write(outputOption)); + } + + results = await Promise.all(outputProms); + } + } catch (e: any) { + errors.push(`[ROLLUP] : ${e.message}`); + } + + return { errors, result: results }; +}; + +export const configXpack = ( + config: BundlerConfig, +): Configuration5 & Configuration4 & RspackOptions => { + return { + context: config.workingDir, + entry: config.entry, + mode: 'production', + output: { + path: config.outDir, + filename: `[name].js`, + }, + devtool: 'source-map', + optimization: { + minimize: false, + }, + plugins: config.plugins, + }; +}; + +const configRollupBase = (config: BundlerConfig): RollupOptions => { + // Rollup doesn't have a working dir option. + // So we change the entry name to include the working dir. + const input: RollupOptions['input'] = {}; + for (const [name, entry] of Object.entries(config.entry)) { + input[name] = path.resolve(config.workingDir, entry); + } + + return { + input, + onwarn: (warning, handler) => { + if ( + !/Circular dependency:/.test(warning.message) && + !/Sourcemap is likely to be incorrect/.test(warning.message) + ) { + return handler(warning); + } + }, + output: { + chunkFileNames: 'chunk.[hash].js', + compact: false, + dir: config.outDir, + entryFileNames: '[name].js', + sourcemap: true, + }, + }; +}; + +export const configRspack = (config: BundlerConfig): RspackOptions => { + return configXpack(config); +}; + +export const configWebpack5 = (config: BundlerConfig): Configuration5 => { + return configXpack(config); +}; + +export const configWebpack4 = (config: BundlerConfig): Configuration4 => { + return configXpack(config); +}; + +export const configEsbuild = (config: BundlerConfig): BuildOptions => { + return { + absWorkingDir: config.workingDir, + bundle: true, + chunkNames: 'chunk.[hash]', + entryPoints: config.entry, + entryNames: '[name]', + format: 'cjs', + outdir: config.outDir, + sourcemap: true, + splitting: false, + plugins: config.plugins, + }; +}; + +export const configRollup = (config: BundlerConfig): RollupOptions => { + const baseConfig = configRollupBase(config); + return { + ...baseConfig, + plugins: [ + commonjs(), + nodeResolve({ preferBuiltins: true, browser: true }), + ...(config.plugins || []), + ], + }; +}; + +export const configVite = (config: BundlerConfig): InlineConfig => { + const baseConfig = configRollupBase({ + ...config, + // Remove the plugins to only have Vite ones. + plugins: undefined, + }); + + return { + root: config.workingDir, + build: { + emptyOutDir: false, + assetsDir: '', // Disable assets dir to simplify the test. + minify: false, + rollupOptions: baseConfig, + }, + logLevel: 'silent', + plugins: config.plugins, + }; +}; + +export const allBundlers: Record< + BundlerFullName, + { run: BundlerRunFunction; config: BundlerConfigFunction } +> = { + rspack: { run: buildWithRspack, config: configRspack }, + webpack5: { run: buildWithWebpack5, config: configWebpack5 }, + webpack4: { run: buildWithWebpack4, config: configWebpack4 }, + esbuild: { run: buildWithEsbuild, config: configEsbuild }, + vite: { run: buildWithVite, config: configVite }, + rollup: { run: buildWithRollup, config: configRollup }, +}; diff --git a/packages/tools/src/commands/dev-server/index.ts b/packages/tools/src/commands/dev-server/index.ts new file mode 100644 index 000000000..7c29194cf --- /dev/null +++ b/packages/tools/src/commands/dev-server/index.ts @@ -0,0 +1,173 @@ +// 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 { ROOT } from '@dd/tools/constants'; +import chalk from 'chalk'; +import { Command, Option } from 'clipanion'; +import fs from 'fs'; +import http from 'http'; +import template from 'lodash.template'; +import path from 'path'; + +const MIME_TYPES = { + default: 'application/octet-stream', + html: 'text/html; charset=UTF-8', + js: 'application/javascript', + css: 'text/css', + png: 'image/png', + jpg: 'image/jpg', + gif: 'image/gif', + ico: 'image/x-icon', + svg: 'image/svg+xml', +} as const; + +// Some context to use for templating content with {{something}}. +const CONTEXT: Record = { + bundler: FULL_NAME_BUNDLERS, +}; + +// Templating regex. +const INTERPOLATE_RX = /{{([\s\S]+?)}}/g; + +// Promise to boolean. +const toBool = [() => true, () => false]; + +type File = { + found: boolean; + ext: keyof typeof MIME_TYPES; + content: string; +}; + +class DevServer extends Command { + static paths = [['dev-server']]; + + static usage = Command.Usage({ + category: `Contribution`, + description: `Run a basic dev server over a specific directory.`, + details: ` + This command will change the package.json values of "exports" so they can be used from another project. + + This is necessary to be sure that the outside project loads the built files and not the dev files. + `, + examples: [ + [`Prepare for link`, `$0 prepare-link`], + [`Revert change`, `$0 prepare-link --revert`], + ], + }); + + port = Option.String('--port', '8000', { + description: 'On which port will the server run.', + }); + + root = Option.String('--root', ROOT, { + description: 'The root directory the server will serve.', + }); + + parseCookie(cookieHeader?: string): Record { + if (!cookieHeader) { + return {}; + } + + const cookieString = cookieHeader + .split(';') + .find((c) => c.trim().startsWith('context_cookie=')); + + if (!cookieString) { + return {}; + } + + const [name, ...rest] = cookieString.split('='); + if (!name || !name.trim()) { + return {}; + } + + const value = rest.join('=').trim(); + if (!value) { + return {}; + } + + try { + return JSON.parse(decodeURIComponent(value)); + } catch (e: any) { + throw new Error(`Error parsing cookie: ${e.message}`); + } + } + + getContext(req: http.IncomingMessage): Record { + const url = new URL(req.url || '/', 'http://127.0.0.1'); + // Get the initial context from the cookie. + const fileContext: Record = this.parseCookie(req.headers.cookie); + + // Verify if we have context passed as parameters (?context_bundler=vite). + for (const [key, value] of url.searchParams) { + if (key.startsWith('context_')) { + const contextKey = key.replace(/^context_/, '') as keyof typeof CONTEXT; + if (Object.keys(CONTEXT).includes(contextKey)) { + if (CONTEXT[contextKey].includes(value)) { + fileContext[contextKey] = value; + } + } + } + } + + return fileContext; + } + + async prepareFile(requestUrl: string, context: Record): Promise { + const staticPath = this.root + ? path.isAbsolute(this.root) + ? this.root + : path.resolve(ROOT, this.root) + : ROOT; + const url = new URL(requestUrl, 'http://127.0.0.1'); + const paths = [staticPath, url.pathname]; + + if (url.pathname.endsWith('/')) { + paths.push('index.html'); + } + + const filePath = path.join(...paths); + const pathTraversal = !filePath.startsWith(staticPath); + const exists = await fs.promises.access(filePath).then(...toBool); + const found = !pathTraversal && exists; + const finalPath = found ? filePath : `${staticPath}/404.html`; + const ext = path.extname(finalPath).substring(1).toLowerCase() as File['ext']; + const fileContent = template(await fs.promises.readFile(finalPath, { encoding: 'utf-8' }), { + interpolate: INTERPOLATE_RX, + })(context); + + return { found, ext, content: fileContent }; + } + + async execute() { + http.createServer(async (req, res) => { + try { + const context = this.getContext(req); + const file = await this.prepareFile(req.url || '/', context); + const statusCode = file.found ? 200 : 404; + const mimeType = MIME_TYPES[file.ext] || MIME_TYPES.default; + const c = statusCode === 200 ? chalk.green : chalk.yellow.bold; + + res.writeHead(statusCode, { + 'Set-Cookie': `context_cookie=${encodeURIComponent(JSON.stringify(context))};SameSite=Strict;`, + 'Content-Type': mimeType, + }); + + res.end(file.content); + + console.log(` -> [${c(statusCode.toString())}] ${req.method} ${req.url}`); + } catch (e: any) { + res.writeHead(500, { 'Content-Type': MIME_TYPES.html }); + res.end('Internal Server Error'); + const c = chalk.red.bold; + console.log(` -> [${c('500')}] ${req.method} ${req.url}: ${e.message}`); + console.log(e); + } + }).listen(this.port); + console.log(`Server running at http://127.0.0.1:${this.port}/`); + } +} + +export default [DevServer]; diff --git a/packages/tools/src/helpers.ts b/packages/tools/src/helpers.ts index 25ef19a88..903ce6a63 100644 --- a/packages/tools/src/helpers.ts +++ b/packages/tools/src/helpers.ts @@ -3,8 +3,17 @@ // Copyright 2019-Present Datadog, Inc. import { ALL_BUNDLERS, SUPPORTED_BUNDLERS } from '@dd/core/constants'; -import { readJsonSync } from '@dd/core/helpers'; -import type { BuildReport, GetPlugins, Logger } from '@dd/core/types'; +import { outputJsonSync, readJsonSync, serializeBuildReport } from '@dd/core/helpers'; +import type { + BuildReport, + BundlerFullName, + BundlerName, + GetCustomPlugins, + GetPlugins, + GlobalContext, + IterableElement, + Logger, +} from '@dd/core/types'; import chalk from 'chalk'; import { execFile, execFileSync } from 'child_process'; import path from 'path'; @@ -18,6 +27,7 @@ export const yellow = chalk.bold.yellow; export const grey = chalk.bold.grey; export const red = chalk.bold.red; export const bgYellow = chalk.bold.bgYellow.black; +export const bgGreen = chalk.bold.bgGreen.black; export const blue = chalk.bold.cyan; export const bold = chalk.bold; export const dim = chalk.dim; @@ -114,6 +124,21 @@ export const runAutoFixes = async () => { return errors; }; +export const buildPlugins = (bundlerNames: (BundlerName | BundlerFullName)[]) => { + const bundlersToBuild = Array.from( + new Set(bundlerNames.map((name) => name.replace(/\d/g, ''))), + ); + + return executeSync('yarn', [ + 'workspaces', + 'foreach', + '-Apti', + ...bundlersToBuild.map((bundler) => ['--include', `@datadog/${bundler}-plugin`]).flat(), + 'run', + 'build', + ]); +}; + export const getWorkspaces = async ( filter?: (workspace: SlugLessWorkspace) => boolean, ): Promise => { @@ -224,3 +249,79 @@ 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/plugins.ts b/packages/tools/src/plugins.ts new file mode 100644 index 000000000..6cdf93c9c --- /dev/null +++ b/packages/tools/src/plugins.ts @@ -0,0 +1,100 @@ +// 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 { BundlerFullName, Options } from '@dd/core/types'; +import { CONFIG_KEY as ERROR_TRACKING } from '@dd/error-tracking-plugin'; +import { CONFIG_KEY as TELEMETRY } from '@dd/telemetry-plugin'; +import fs from 'fs'; +import path from 'path'; + +import { ROOT } from './constants'; + +export const defaultConfig: Options = { + auth: { + apiKey: process.env.DATADOG_API_KEY, + appKey: process.env.DATADOG_APP_KEY, + }, +}; + +export const fullConfig: Options = { + ...defaultConfig, + [ERROR_TRACKING]: { + sourcemaps: { + bailOnError: false, + dryRun: false, + maxConcurrency: 10, + minifiedPathPrefix: '/', + releaseVersion: '1.0.0', + service: 'error-tracking-build-plugin-sourcemaps', + }, + }, + [TELEMETRY]: { + enableTracing: true, + timestamp: new Date().getTime(), + }, +}; + +// We load the plugins dynamically to avoid esm/cjs issues. +// Using '*/dist/src' to specifically target the bundled files. + +export const getEsbuildPlugin = (config: Options) => { + // eslint-disable-next-line import/no-unresolved + const { datadogEsbuildPlugin } = require('@datadog/esbuild-plugin/dist/src'); + return datadogEsbuildPlugin(config); +}; + +export const getRollupPlugin = (config: Options) => { + // eslint-disable-next-line import/no-unresolved + const { datadogRollupPlugin } = require('@datadog/rollup-plugin/dist/src'); + return datadogRollupPlugin(config); +}; + +export const getRspackPlugin = (config: Options) => { + // eslint-disable-next-line import/no-unresolved + const { datadogRspackPlugin } = require('@datadog/rspack-plugin/dist/src'); + return datadogRspackPlugin(config); +}; + +export const getVitePlugin = (config: Options) => { + // eslint-disable-next-line import/no-unresolved + const { datadogVitePlugin } = require('@datadog/vite-plugin/dist/src'); + return datadogVitePlugin(config); +}; + +export const getWebpack4Plugin = (config: Options) => { + // We'll write a plugin specifically for Webpack4. + const webpackPluginRoot = path.resolve(ROOT, 'packages/published/webpack-plugin/dist/src'); + const webpack4PluginPath = path.resolve(webpackPluginRoot, 'index4.js'); + const webpack5PluginPath = path.resolve(webpackPluginRoot, 'index.js'); + + // First verify if it exists already or not. + if (!fs.existsSync(webpack4PluginPath)) { + // Create the file with the correct imports of Webpack4. + fs.writeFileSync( + webpack4PluginPath, + fs + .readFileSync(webpack5PluginPath, { encoding: 'utf-8' }) + .replace(/require\(('|")webpack("|')\)/g, "require('webpack4')"), + ); + } + + // eslint-disable-next-line import/no-unresolved + const { datadogWebpackPlugin } = require('@datadog/webpack-plugin/dist/src/index4.js'); + return datadogWebpackPlugin(config); +}; + +export const getWebpack5Plugin = (config: Options) => { + // eslint-disable-next-line import/no-unresolved + const { datadogWebpackPlugin } = require('@datadog/webpack-plugin/dist/src/index.js'); + return datadogWebpackPlugin(config); +}; + +export const allPlugins: Record any> = { + esbuild: getEsbuildPlugin, + rollup: getRollupPlugin, + rspack: getRspackPlugin, + vite: getVitePlugin, + webpack4: getWebpack4Plugin, + webpack5: getWebpack5Plugin, +}; diff --git a/packages/tools/src/rollupConfig.mjs b/packages/tools/src/rollupConfig.mjs index a424b539a..0f92613dd 100644 --- a/packages/tools/src/rollupConfig.mjs +++ b/packages/tools/src/rollupConfig.mjs @@ -4,6 +4,7 @@ import babel from '@rollup/plugin-babel'; import commonjs from '@rollup/plugin-commonjs'; +import esmShim from '@rollup/plugin-esm-shim'; import json from '@rollup/plugin-json'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import terser from '@rollup/plugin-terser'; @@ -63,13 +64,23 @@ export const bundle = (packageJson, config) => ({ */ const getOutput = (packageJson, overrides = {}) => { const filename = overrides.format === 'esm' ? packageJson.module : packageJson.main; + const plugins = [terser()]; + + // Inject ESM shims to support __dirname and co. + if (overrides.format === 'esm') { + plugins.push(esmShim()); + } + return { exports: 'named', sourcemap: true, entryFileNames: `[name]${path.extname(filename)}`, dir: path.dirname(filename), - plugins: [terser()], + plugins, format: 'cjs', + globals: { + globalThis: 'window', + }, // No chunks. manualChunks: () => '[name]', ...overrides, diff --git a/yarn.lock b/yarn.lock index 6855f6bcd..f8690b8fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1449,6 +1449,7 @@ __metadata: "@dd/tools": "workspace:*" "@rollup/plugin-babel": "npm:6.0.4" "@rollup/plugin-commonjs": "npm:28.0.1" + "@rollup/plugin-esm-shim": "npm:0.1.7" "@rollup/plugin-json": "npm:6.1.0" "@rollup/plugin-node-resolve": "npm:15.3.0" "@rollup/plugin-terser": "npm:0.4.4" @@ -1483,6 +1484,7 @@ __metadata: "@dd/tools": "workspace:*" "@rollup/plugin-babel": "npm:6.0.4" "@rollup/plugin-commonjs": "npm:28.0.1" + "@rollup/plugin-esm-shim": "npm:0.1.7" "@rollup/plugin-json": "npm:6.1.0" "@rollup/plugin-node-resolve": "npm:15.3.0" "@rollup/plugin-terser": "npm:0.4.4" @@ -1517,6 +1519,7 @@ __metadata: "@dd/tools": "workspace:*" "@rollup/plugin-babel": "npm:6.0.4" "@rollup/plugin-commonjs": "npm:28.0.1" + "@rollup/plugin-esm-shim": "npm:0.1.7" "@rollup/plugin-json": "npm:6.1.0" "@rollup/plugin-node-resolve": "npm:15.3.0" "@rollup/plugin-terser": "npm:0.4.4" @@ -1551,6 +1554,7 @@ __metadata: "@dd/tools": "workspace:*" "@rollup/plugin-babel": "npm:6.0.4" "@rollup/plugin-commonjs": "npm:28.0.1" + "@rollup/plugin-esm-shim": "npm:0.1.7" "@rollup/plugin-json": "npm:6.1.0" "@rollup/plugin-node-resolve": "npm:15.3.0" "@rollup/plugin-terser": "npm:0.4.4" @@ -1585,6 +1589,7 @@ __metadata: "@dd/tools": "workspace:*" "@rollup/plugin-babel": "npm:6.0.4" "@rollup/plugin-commonjs": "npm:28.0.1" + "@rollup/plugin-esm-shim": "npm:0.1.7" "@rollup/plugin-json": "npm:6.1.0" "@rollup/plugin-node-resolve": "npm:15.3.0" "@rollup/plugin-terser": "npm:0.4.4" @@ -1728,6 +1733,8 @@ __metadata: "@dd/internal-git-plugin": "workspace:*" "@dd/internal-injection-plugin": "workspace:*" "@dd/telemetry-plugin": "workspace:*" + "@dd/tools": "workspace:*" + "@playwright/test": "npm:1.49.1" "@rollup/plugin-commonjs": "npm:28.0.1" "@rspack/core": "npm:1.1.2" "@types/faker": "npm:5.5.9" @@ -1756,21 +1763,40 @@ __metadata: version: 0.0.0-use.local resolution: "@dd/tools@workspace:packages/tools" dependencies: + "@datadog/esbuild-plugin": "workspace:*" + "@datadog/rollup-plugin": "workspace:*" + "@datadog/rspack-plugin": "workspace:*" + "@datadog/vite-plugin": "workspace:*" + "@datadog/webpack-plugin": "workspace:*" "@dd/assets": "workspace:*" "@dd/core": "workspace:*" + "@dd/error-tracking-plugin": "workspace:*" + "@dd/telemetry-plugin": "workspace:*" "@inquirer/checkbox": "npm:2.3.3" "@inquirer/input": "npm:2.1.7" "@inquirer/select": "npm:2.3.3" + "@rollup/plugin-commonjs": "npm:28.0.1" + "@rollup/plugin-esm-shim": "npm:0.1.7" + "@rollup/plugin-node-resolve": "npm:15.3.0" + "@rspack/core": "npm:1.1.2" "@types/chalk": "npm:2.2.0" "@types/glob": "npm:7.1.4" + "@types/lodash.template": "npm:^4" "@types/node": "npm:^18" + "@types/webpack4": "npm:@types/webpack@4.41.38" chalk: "npm:2.3.1" clipanion: "npm:4.0.0-rc.3" + esbuild: "npm:0.24.0" glob: "npm:7.1.6" + lodash.template: "npm:4.5.0" outdent: "npm:0.8.0" ts-node: "npm:10.9.2" typanion: "npm:3.14.0" typescript: "npm:5.4.3" + vite: "npm:5.4.10" + webpack: "npm:5.92.1" + webpack4: "npm:webpack@4.47.0" + webpack5: "npm:webpack@5.92.1" languageName: unknown linkType: soft @@ -2692,6 +2718,17 @@ __metadata: languageName: node linkType: hard +"@playwright/test@npm:1.49.1": + version: 1.49.1 + resolution: "@playwright/test@npm:1.49.1" + dependencies: + playwright: "npm:1.49.1" + bin: + playwright: cli.js + checksum: 10/bb0d5eda58ee0b5bbca732d2aa57782fadf420d101e08e16d5760179459c667907bd8d224ee3d6f43f3088378e377ef63d32ed605fec37605debf217c3efe8da + languageName: node + linkType: hard + "@rollup/plugin-babel@npm:6.0.4": version: 6.0.4 resolution: "@rollup/plugin-babel@npm:6.0.4" @@ -2731,6 +2768,20 @@ __metadata: languageName: node linkType: hard +"@rollup/plugin-esm-shim@npm:0.1.7": + version: 0.1.7 + resolution: "@rollup/plugin-esm-shim@npm:0.1.7" + dependencies: + magic-string: "npm:^0.30.3" + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 10/c3cc762ce729d2f87b9b2d4d3e90af49dc91036860cca95dd103d175cf5425e72068219d7300d2a006b0408cb0a9575a8c79f1534e8625663ed996d9ea58b397 + languageName: node + linkType: hard + "@rollup/plugin-json@npm:6.1.0": version: 6.1.0 resolution: "@rollup/plugin-json@npm:6.1.0" @@ -3281,6 +3332,22 @@ __metadata: languageName: node linkType: hard +"@types/lodash.template@npm:^4": + version: 4.5.3 + resolution: "@types/lodash.template@npm:4.5.3" + dependencies: + "@types/lodash": "npm:*" + checksum: 10/7c9d32b8d81959c8131223da191d2dd55d8ddfd4f0997bf18565b65cb6ba3b8087d39f458071031494b23f5dac857b9cbdba00df8b32c2d8178e89478b0b44ee + languageName: node + linkType: hard + +"@types/lodash@npm:*": + version: 4.17.14 + resolution: "@types/lodash@npm:4.17.14" + checksum: 10/6ee40725f3e192f5ef1f493caca19210aa7acd7adc3136b8dba84d418a35be0abea0668105aed9f696ad62a54310a9c0d328971ad4b157f5bcda700424ed5aae + languageName: node + linkType: hard + "@types/minimatch@npm:*": version: 5.1.2 resolution: "@types/minimatch@npm:5.1.2" @@ -6772,6 +6839,16 @@ __metadata: languageName: node linkType: hard +"fsevents@npm:2.3.2": + version: 2.3.2 + resolution: "fsevents@npm:2.3.2" + dependencies: + node-gyp: "npm:latest" + checksum: 10/6b5b6f5692372446ff81cf9501c76e3e0459a4852b3b5f1fc72c103198c125a6b8c72f5f166bdd76ffb2fca261e7f6ee5565daf80dca6e571e55bcc589cc1256 + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@npm:^1.2.7": version: 1.2.13 resolution: "fsevents@npm:1.2.13" @@ -6793,6 +6870,15 @@ __metadata: languageName: node linkType: hard +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin": + version: 2.3.2 + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@patch:fsevents@npm%3A^1.2.7#optional!builtin": version: 1.2.13 resolution: "fsevents@patch:fsevents@npm%3A1.2.13#optional!builtin::version=1.2.13&hash=d11327" @@ -8661,6 +8747,13 @@ __metadata: languageName: node linkType: hard +"lodash._reinterpolate@npm:^3.0.0": + version: 3.0.0 + resolution: "lodash._reinterpolate@npm:3.0.0" + checksum: 10/06d2d5f33169604fa5e9f27b6067ed9fb85d51a84202a656901e5ffb63b426781a601508466f039c720af111b0c685d12f1a5c14ff8df5d5f27e491e562784b2 + languageName: node + linkType: hard + "lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "lodash.debounce@npm:4.0.8" @@ -8682,6 +8775,25 @@ __metadata: languageName: node linkType: hard +"lodash.template@npm:4.5.0": + version: 4.5.0 + resolution: "lodash.template@npm:4.5.0" + dependencies: + lodash._reinterpolate: "npm:^3.0.0" + lodash.templatesettings: "npm:^4.0.0" + checksum: 10/56d18ba410ff591f22e4dd2974d21fdcfcba392f2d462ee4b7a7368c3a28ac1cb38a73f1d1c9eb8b8cae26f8e0ae2c28058f7488b4ffa9da84a6096bc77691db + languageName: node + linkType: hard + +"lodash.templatesettings@npm:^4.0.0": + version: 4.2.0 + resolution: "lodash.templatesettings@npm:4.2.0" + dependencies: + lodash._reinterpolate: "npm:^3.0.0" + checksum: 10/ef470fa8b66b6370b08fb0709c1577e4bf72cc3d1e8639196577db827915808ec138861cbc791b295a24fbfe7b78dd26bcfc8f237e5d94df383a3125ae6f5339 + languageName: node + linkType: hard + "log-symbols@npm:^3.0.0": version: 3.0.0 resolution: "log-symbols@npm:3.0.0" @@ -9734,6 +9846,30 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.49.1": + version: 1.49.1 + resolution: "playwright-core@npm:1.49.1" + bin: + playwright-core: cli.js + checksum: 10/baa39a53024ec7744708410f2b952ac3aa2e1a6d311dabfa303523712848eba142fce5c20f1b2ed2a66fbd9a415d22ea8642b0f70423360aaebd4b41c47d364e + languageName: node + linkType: hard + +"playwright@npm:1.49.1": + version: 1.49.1 + resolution: "playwright@npm:1.49.1" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.49.1" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 10/49fb063f4a107b8090f66d2d351ebd51fbb66843a8f95a161fa0c0e0b5156515961e75cc10f4249f61b9d2af51f762dda505c62b096d8f61cd47d1ff73ab39d2 + languageName: node + linkType: hard + "please-upgrade-node@npm:^3.2.0": version: 3.2.0 resolution: "please-upgrade-node@npm:3.2.0"