Skip to content

Commit

Permalink
Add injection plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
yoannmoinet committed Sep 30, 2024
1 parent d08919d commit bb7186b
Show file tree
Hide file tree
Showing 14 changed files with 602 additions and 11 deletions.
12 changes: 10 additions & 2 deletions packages/core/src/plugins/build-report/esbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { Logger } from '@dd/core/log';
import type { Entry, GlobalContext, Input, Output, PluginOptions } from '@dd/core/types';
import { glob } from 'glob';

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

// Re-index metafile data for easier access.
const reIndexMeta = <T>(obj: Record<string, T>, cwd: string) =>
Expand Down Expand Up @@ -95,6 +95,10 @@ export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOpt

// Loop through inputs.
for (const [filename, input] of Object.entries(result.metafile.inputs)) {
if (isInjection(filename)) {
continue;
}

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

Expand All @@ -117,6 +121,10 @@ export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOpt
// Get inputs of this output.
const inputFiles: Input[] = [];
for (const inputName of Object.keys(output.inputs)) {
if (isInjection(inputName)) {
continue;
}

const inputFound = reportInputsIndexed[getAbsolutePath(inputName, cwd)];
if (!inputFound) {
warn(`Input ${inputName} not found for output ${cleanedName}`);
Expand Down Expand Up @@ -208,7 +216,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
14 changes: 14 additions & 0 deletions 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 Down Expand Up @@ -188,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 @@ -199,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 @@ -236,6 +242,10 @@ export const cleanPath = (filepath: string) => {
};

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

if (filepath.startsWith(cwd)) {
return filepath;
}
Expand All @@ -244,6 +254,10 @@ export const getAbsolutePath = (filepath: string, cwd: 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
23 changes: 16 additions & 7 deletions packages/core/src/plugins/build-report/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import type { Logger } from '@dd/core/log';
import type { Entry, GlobalContext, Input, Output, PluginOptions } from '@dd/core/types';

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

export const getWebpackPlugin =
(context: GlobalContext, PLUGIN_NAME: string, log: Logger): PluginOptions['webpack'] =>
Expand Down Expand Up @@ -136,6 +136,20 @@ export const getWebpackPlugin =
: 'unknown';
};

const isModuleSupported = (module: (typeof modules)[number]) => {
if (
isInjection(getModulePath(module)) ||
// Do not report runtime modules as they are very specific to webpack.
module.moduleType === 'runtime' ||
module.name?.startsWith('(webpack)') ||
// Also ignore orphan modules
module.type === 'orphan modules'
) {
return false;
}
return true;
};

const getModules = (reason: Reason) => {
const { moduleIdentifier, moduleId } = reason;
if (!moduleIdentifier && !moduleId) {
Expand Down Expand Up @@ -174,12 +188,7 @@ export const getWebpackPlugin =
// Build inputs
const modulesDone = new Set<string>();
for (const module of modules) {
// Do not report runtime modules as they are very specific to webpack.
if (
module.moduleType === 'runtime' ||
module.name?.startsWith('(webpack)') ||
module.type === 'orphan modules'
) {
if (!isModuleSupported(module)) {
continue;
}

Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

import type { GlobalContext, Meta, Options, PluginOptions } from '@dd/core/types';
import type { GlobalContext, Meta, Options, PluginOptions, ToInjectItem } from '@dd/core/types';

import { getBuildReportPlugin } from './build-report';
import { getBundlerReportPlugin } from './bundler-report';
import { getGitPlugin } from './git';
import { getInjectionPlugins } from './injection';

export const getInternalPlugins = (
options: Options,
Expand All @@ -16,6 +17,7 @@ export const getInternalPlugins = (
const variant =
meta.framework === 'webpack' ? (meta.webpack.compiler['webpack'] ? '5' : '4') : '';

const toInject: ToInjectItem[] = [];
const globalContext: GlobalContext = {
auth: options.auth,
bundler: {
Expand All @@ -29,16 +31,20 @@ export const getInternalPlugins = (
warnings: [],
},
cwd,
inject: (item: ToInjectItem) => {
toInject.push(item);
},
start: Date.now(),
version: meta.version,
};

const bundlerReportPlugin = getBundlerReportPlugin(options, globalContext);
const buildReportPlugin = getBuildReportPlugin(options, globalContext);
const gitPlugin = getGitPlugin(options, globalContext);
const injectionPlugins = getInjectionPlugins(options, globalContext, toInject);

return {
globalContext,
internalPlugins: [bundlerReportPlugin, buildReportPlugin, gitPlugin],
internalPlugins: [bundlerReportPlugin, buildReportPlugin, gitPlugin, ...injectionPlugins],
};
};
9 changes: 9 additions & 0 deletions packages/core/src/plugins/injection/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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.

export const PREPARATION_PLUGIN_NAME = 'datadog-injection-preparation-plugin';
export const PLUGIN_NAME = 'datadog-injection-plugin';
export const RESOLUTION_PLUGIN_NAME = 'datadog-injection-resolution-plugin';
export const INJECTED_FILE = '__DATADOG_INJECTION_STUB';
export const DISTANT_FILE_RX = /^https?:\/\//;
86 changes: 86 additions & 0 deletions packages/core/src/plugins/injection/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

import { doRequest, truncateString } from '@dd/core/helpers';
import type { Logger } from '@dd/core/log';
import { getAbsolutePath } from '@dd/core/plugins/build-report/helpers';
import type { ToInjectItem } from '@dd/core/types';
import { readFile } from 'fs/promises';

import { DISTANT_FILE_RX } from './constants';

const MAX_TIMEOUT_IN_MS = 5000;

export const processDistantFile = async (
item: ToInjectItem,
timeout: number = MAX_TIMEOUT_IN_MS,
): Promise<string> => {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
return Promise.race([
doRequest<string>({ url: item.value }).finally(() => {
if (timeout) {
clearTimeout(timeoutId);
}
}),
new Promise<string>((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error('Timeout'));
}, timeout);
}),
]);
};

export const processLocalFile = async (item: ToInjectItem): Promise<string> => {
const absolutePath = getAbsolutePath(item.value, process.cwd());
return readFile(absolutePath, { encoding: 'utf-8' });
};

export const processRawCode = async (item: ToInjectItem): Promise<string> => {
// TODO: Confirm the code actually executes without errors.
return item.value;
};

export const processItem = async (item: ToInjectItem, log: Logger): Promise<string> => {
let result: string;
try {
if (item.type === 'file') {
if (item.value.match(DISTANT_FILE_RX)) {
result = await processDistantFile(item);
} else {
result = await processLocalFile(item);
}
} else if (item.type === 'code') {
result = await processRawCode(item);
} else {
throw new Error(`Invalid item type "${item.type}", only accepts "code" or "file".`);
}
} catch (error: any) {
const itemId = `${item.type} - ${truncateString(item.value)}`;
if (item.fallback) {
// In case of any error, we'll fallback to next item in queue.
log(`Fallback for "${itemId}": ${error.toString()}`, 'warn');
result = await processItem(item.fallback, log);
} else {
// Or return an empty string.
log(`Failed "${itemId}": ${error.toString()}`, 'warn');
result = '';
}
}

return result;
};

export const processInjections = async (
toInject: ToInjectItem[],
log: Logger,
): Promise<string[]> => {
const proms: (Promise<string> | string)[] = [];

for (const item of toInject) {
proms.push(processItem(item, log));
}

const results = await Promise.all(proms);
return results.filter(Boolean);
};
Loading

0 comments on commit bb7186b

Please sign in to comment.