Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[internal] Injection plugin #98

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
726d17f
Aggregate some helpers
yoannmoinet Sep 20, 2024
fe4c83b
Pass the bundler to the factory
yoannmoinet Sep 20, 2024
5de0279
Fix webpack4 test run to actually use webpack4 within
yoannmoinet Sep 20, 2024
7e4f723
Better logs in test runs
yoannmoinet Sep 20, 2024
61aeea5
Separate context from bundler-report
yoannmoinet Sep 20, 2024
277ef08
Unify absolute path creation
yoannmoinet Sep 20, 2024
113d441
Move request helper into core
yoannmoinet Sep 23, 2024
45ff790
Move string truncate helper
yoannmoinet Sep 24, 2024
4f8bf29
Fix imports
yoannmoinet Sep 24, 2024
43f1868
Better error handling for webpack bundlers
yoannmoinet Sep 24, 2024
89a48d3
Add injection plugin
yoannmoinet Sep 24, 2024
c0d64c7
Add new jest matchers
yoannmoinet Sep 25, 2024
e110af0
Fix injection for webpack4
yoannmoinet Sep 25, 2024
9c9b352
Finalise injection tests
yoannmoinet Sep 25, 2024
67ab329
Add test to injection helpers
yoannmoinet Sep 25, 2024
51b1d43
Integrity
yoannmoinet Sep 25, 2024
39907d8
Better handle injection in build report
yoannmoinet Sep 26, 2024
a262201
Do not try to send metrics in tests
yoannmoinet Sep 26, 2024
ba0c616
Fix circular stringification
yoannmoinet Sep 26, 2024
cc5cbb5
Fix git plugin tests
yoannmoinet Sep 26, 2024
a7091ff
Fix async-retry mock
yoannmoinet Sep 26, 2024
3b25144
Fix sender tests
yoannmoinet Sep 26, 2024
20342ae
Fix build report tests
yoannmoinet Sep 26, 2024
179b2e1
Seed bundlers builds and return cleanup
yoannmoinet Sep 27, 2024
302191a
Update tests for seeded builds
yoannmoinet Sep 27, 2024
bf812f9
Integrity
yoannmoinet Sep 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@
"watch": "tsc -w"
},
"dependencies": {
"async-retry": "1.3.3",
"chalk": "2.3.1",
"glob": "11.0.0",
"simple-git": "3.25.0",
"unplugin": "1.11.0"
},
"devDependencies": {
"@types/async-retry": "1.4.8",
"@types/chalk": "2.2.0",
"@types/node": "^18",
"typescript": "5.4.3"
Expand Down
99 changes: 99 additions & 0 deletions packages/core/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

import retry from 'async-retry';
import type { RequestInit } from 'undici-types';

import type { RequestOpts } from './types';

// Format a duration 0h 0m 0s 0ms
export const formatDuration = (duration: number) => {
const days = Math.floor(duration / 1000 / 60 / 60 / 24);
Expand All @@ -15,3 +20,97 @@ export const formatDuration = (duration: number) => {
seconds ? `${seconds}s ` : ''
}${milliseconds}ms`.trim();
};

export const getResolvedPath = (source: string) => {
let resolvedPath = source;
try {
resolvedPath = require.resolve(source);
} catch (e) {
// No big deal.
}
return resolvedPath;
};

export const ERROR_CODES_NO_RETRY = [400, 403, 413];
export const NB_RETRIES = 5;
// Do a retriable fetch.
export const doRequest = <T>(opts: RequestOpts): Promise<T> => {
const { url, method = 'GET', getData, onRetry, type = 'text' } = opts;
return retry(
async (bail: (e: Error) => void, attempt: number) => {
let response: Response;
try {
const requestInit: RequestInit = {
method,
// This is needed for sending body in NodeJS' Fetch.
// https://github.com/nodejs/node/issues/46221
duplex: 'half',
};

if (typeof getData === 'function') {
const { data, headers } = await getData();
requestInit.body = data;
requestInit.headers = headers;
}

response = await fetch(url, requestInit);
} catch (error: any) {
// We don't want to retry if there is a non-fetch related error.
bail(error);
// bail(error) throws so the return is never executed.
return {} as T;
}

if (!response.ok) {
// Not instantiating the error here, as it will make Jest throw in the tests.
const errorMessage = `HTTP ${response.status} ${response.statusText}`;
if (ERROR_CODES_NO_RETRY.includes(response.status)) {
bail(new Error(errorMessage));
// bail(error) throws so the return is never executed.
return {} as T;
} else {
// Trigger the retry.
throw new Error(errorMessage);
}
}

try {
let result;
// Await it so we catch any parsing error and bail.
if (type === 'json') {
result = await response.json();
} else {
result = await response.text();
}

return result as T;
} catch (error: any) {
// We don't want to retry on parsing errors.
bail(error);
// bail(error) throws so the return is never executed.
return {} as T;
}
},
{
retries: NB_RETRIES,
onRetry,
},
);
};

// Truncate a string to a certain length.
// Placing a [...] placeholder in the middle.
// "A way too long sentence could be truncated a bit." => "A way too[...]could be truncated a bit."
export const truncateString = (
str: string,
maxLength: number = 60,
placeholder: string = '[...]',
) => {
if (str.length <= maxLength) {
return str;
}

const leftStop = Math.min(10, Math.floor(maxLength / 2));
const rightStop = -maxLength - (leftStop + placeholder.length);
return `${str.slice(0, leftStop)}${placeholder}${str.slice(rightStop)}`;
};
52 changes: 30 additions & 22 deletions packages/core/src/plugins/build-report/esbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,8 @@
import type { Logger } from '@dd/core/log';
import type { Entry, GlobalContext, Input, Output, PluginOptions } from '@dd/core/types';
import { glob } from 'glob';
import path from 'path';

import { cleanName, getResolvedPath, getType } from './helpers';

// Re-index metafile data for easier access.
const reIndexMeta = <T>(obj: Record<string, T>, cwd: string) =>
Object.fromEntries(
Object.entries(obj).map(([key, value]) => {
const newKey = path.join(cwd, key);
return [newKey, value];
}),
);
import { cleanName, getAbsolutePath, getResolvedPath, getType, isInjection } from './helpers';

// https://esbuild.github.io/api/#glob-style-entry-points
const getAllEntryFiles = (filepath: string, cwd: string): string[] => {
Expand Down Expand Up @@ -90,14 +80,28 @@ export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOpt
const reportInputsIndexed: Record<string, Input> = {};
const reportOutputsIndexed: Record<string, Output> = {};

const metaInputsIndexed = reIndexMeta(result.metafile.inputs, cwd);
const metaOutputsIndexed = reIndexMeta(result.metafile.outputs, cwd);
// Re-index metafile data for easier access.
const reIndexMeta = <T>(obj: Record<string, T>) =>
Object.fromEntries(
Object.entries(obj).map(([key, value]) => {
const newKey = getAbsolutePath(key, cwd);
return [newKey, value];
}),
);

const metaInputsIndexed = reIndexMeta(result.metafile.inputs);
const metaOutputsIndexed = reIndexMeta(result.metafile.outputs);
// Loop through inputs.
for (const [filename, input] of Object.entries(result.metafile.inputs)) {
const filepath = path.join(cwd, filename);
if (isInjection(filename)) {
continue;
}

const filepath = getAbsolutePath(filename, cwd);
const name = cleanName(context, filename);

const file: Input = {
name: cleanName(context, filename),
name,
filepath,
dependents: new Set(),
dependencies: new Set(),
Expand All @@ -110,12 +114,15 @@ export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOpt

// Loop through outputs.
for (const [filename, output] of Object.entries(result.metafile.outputs)) {
const fullPath = path.join(cwd, filename);
const fullPath = getAbsolutePath(filename, cwd);
const cleanedName = cleanName(context, fullPath);
// Get inputs of this output.
const inputFiles: Input[] = [];
for (const inputName of Object.keys(output.inputs)) {
const inputFound = reportInputsIndexed[path.join(cwd, inputName)];
if (isInjection(inputName)) {
continue;
}
const inputFound = reportInputsIndexed[getAbsolutePath(inputName, cwd)];
if (!inputFound) {
warn(`Input ${inputName} not found for output ${cleanedName}`);
continue;
Expand All @@ -127,7 +134,8 @@ export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOpt
// When splitting, esbuild creates an empty entryPoint wrapper for the chunk.
// It has no inputs, but still relates to its entryPoint.
if (output.entryPoint && !inputFiles.length) {
const inputFound = reportInputsIndexed[path.join(cwd, output.entryPoint!)];
const inputFound =
reportInputsIndexed[getAbsolutePath(output.entryPoint!, cwd)];
if (!inputFound) {
warn(`Input ${output.entryPoint} not found for output ${cleanedName}`);
continue;
Expand Down Expand Up @@ -156,7 +164,7 @@ export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOpt
continue;
}

const inputFile = reportInputsIndexed[path.join(cwd, output.entryPoint!)];
const inputFile = reportInputsIndexed[getAbsolutePath(output.entryPoint!, cwd)];

if (inputFile) {
// In the case of "splitting: true", all the files are considered entries to esbuild.
Expand Down Expand Up @@ -205,7 +213,7 @@ export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOpt
// There are some exceptions we want to ignore.
const FILE_EXCEPTIONS_RX = /(<runtime>|https:|file:|data:|#)/g;
const isFileSupported = (filePath: string) => {
if (filePath.match(FILE_EXCEPTIONS_RX)) {
if (isInjection(filePath) || filePath.match(FILE_EXCEPTIONS_RX)) {
return false;
}
return true;
Expand Down Expand Up @@ -246,7 +254,7 @@ export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOpt
}

for (const imported of metaFile.imports) {
const importPath = path.join(cwd, imported.path);
const importPath = getAbsolutePath(imported.path, cwd);
// Look for the other inputs.
getAllImports<T>(importPath, ref, allImports);
}
Expand Down Expand Up @@ -296,7 +304,7 @@ export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOpt
if (!isFileSupported(dependency.path)) {
continue;
}
const dependencyPath = path.join(cwd, dependency.path);
const dependencyPath = getAbsolutePath(dependency.path, cwd);
const dependencyFile = references.inputs.report[dependencyPath];

if (!dependencyFile) {
Expand Down
24 changes: 23 additions & 1 deletion packages/core/src/plugins/build-report/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

import { INJECTED_FILE } from '@dd/core/plugins/injection/constants';
import type {
BuildReport,
SerializedEntry,
Expand All @@ -13,7 +14,8 @@ import type {
Entry,
Input,
Output,
} from '../../types';
} from '@dd/core/types';
import path from 'path';

// Will match any last part of a path after a dot or slash and is a word character.
const EXTENSION_RX = /\.(?!.*(?:\.|\/|\\))(\w{1,})/g;
Expand Down Expand Up @@ -187,6 +189,9 @@ export const unserializeBuildReport = (report: SerializedBuildReport): BuildRepo
};
};

// Is the file coming from the injection plugin?
export const isInjection = (filename: string) => filename.includes(INJECTED_FILE);

const BUNDLER_SPECIFICS = ['unknown', 'commonjsHelpers.js', 'vite/preload-helper.js'];
// Make list of paths unique, remove the current file and particularities.
export const cleanReport = <T = string>(
Expand All @@ -198,6 +203,8 @@ export const cleanReport = <T = string>(
for (const reportFilepath of report) {
const cleanedPath = cleanPath(reportFilepath);
if (
// Don't add injections.
isInjection(reportFilepath) ||
// Don't add itself into it.
cleanedPath === filepath ||
// Remove common specific files injected by bundlers.
Expand Down Expand Up @@ -234,6 +241,17 @@ export const cleanPath = (filepath: string) => {
);
};

export const getAbsolutePath = (filepath: string, cwd: string) => {
if (isInjection(filepath)) {
return INJECTED_FILE;
}

if (filepath.startsWith(cwd)) {
return filepath;
}
return path.resolve(cwd, filepath);
};

export const getResolvedPath = (filepath: string) => {
try {
return require.resolve(filepath);
Expand All @@ -244,6 +262,10 @@ export const getResolvedPath = (filepath: string) => {

// Extract a name from a path based on the context (out dir and cwd).
export const cleanName = (context: GlobalContext, filepath: string) => {
if (isInjection(filepath)) {
return INJECTED_FILE;
}

if (filepath === 'unknown') {
return filepath;
}
Expand Down
8 changes: 3 additions & 5 deletions packages/core/src/plugins/build-report/rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

import path from 'path';

import type { Logger } from '../../log';
import type { Entry, GlobalContext, Input, Output, PluginOptions } from '../../types';

import { cleanName, cleanPath, cleanReport, getType } from './helpers';
import { cleanName, cleanPath, cleanReport, getAbsolutePath, getType } from './helpers';

export const getRollupPlugin = (context: GlobalContext, log: Logger): PluginOptions['rollup'] => {
const importsReport: Record<
Expand Down Expand Up @@ -109,7 +107,7 @@ export const getRollupPlugin = (context: GlobalContext, log: Logger): PluginOpti

// Fill in inputs and outputs.
for (const [filename, asset] of Object.entries(bundle)) {
const filepath = path.join(context.bundler.outDir, filename);
const filepath = getAbsolutePath(filename, context.bundler.outDir);
const size =
'code' in asset
? Buffer.byteLength(asset.code, 'utf8')
Expand Down Expand Up @@ -240,7 +238,7 @@ export const getRollupPlugin = (context: GlobalContext, log: Logger): PluginOpti
}

for (const importName of imports) {
getAllOutputs(path.join(context.bundler.outDir, importName), allOutputs);
getAllOutputs(getAbsolutePath(importName, context.bundler.outDir), allOutputs);
}

return allOutputs;
Expand Down
Loading
Loading