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 #100

Merged
merged 2 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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(cwd, filename);
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(cwd, inputName)];
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 @@ -237,6 +243,10 @@ export const cleanPath = (filepath: string) => {

// Will only prepend the cwd if not already there.
export const getAbsolutePath = (cwd: string, filepath: string) => {
if (isInjection(filepath)) {
return INJECTED_FILE;
}

if (filepath.startsWith(cwd)) {
return filepath;
}
Expand All @@ -245,6 +255,10 @@ export const getAbsolutePath = (cwd: string, 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
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(process.cwd(), item.value);
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
Loading