Skip to content
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
117 changes: 74 additions & 43 deletions packages/bundler-plugin-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import { normalizeUserOptions, validateOptions } from "./options-mapping";
import { debugIdUploadPlugin } from "./plugins/debug-id-upload";
import { releaseManagementPlugin } from "./plugins/release-management";
import { telemetryPlugin } from "./plugins/telemetry";
import { getSentryCli } from "./sentry/cli";
import { createLogger } from "./sentry/logger";
import { createSentryInstance, allowedToSendTelemetry } from "./sentry/telemetry";
import { allowedToSendTelemetry, createSentryInstance } from "./sentry/telemetry";
import { Options } from "./types";
import {
determineReleaseName,
generateGlobalInjectorCode,
getDependencies,
getPackageJson,
Expand Down Expand Up @@ -58,6 +56,14 @@ export function sentryUnpluginFactory({
return createUnplugin<Options, true>((userOptions, unpluginMetaContext) => {
const options = normalizeUserOptions(userOptions);

if (unpluginMetaContext.watchMode || options.disable) {
return [
{
name: "sentry-noop-plugin",
},
];
}

const shouldSendTelemetry = allowedToSendTelemetry(options);
const { sentryHub, sentryClient } = createSentryInstance(
options,
Expand Down Expand Up @@ -95,15 +101,6 @@ export function sentryUnpluginFactory({
);
}

const cli = getSentryCli(options, logger);

const releaseName = options.release ?? determineReleaseName();
if (!releaseName) {
handleRecoverableError(
new Error("Unable to determine a release name. Please set the `release` option.")
);
}

if (process.cwd().match(/\\node_modules\\|\/node_modules\//)) {
logger.warn(
"Running Sentry plugin from within a `node_modules` folder. Some features may not work."
Expand All @@ -121,53 +118,87 @@ export function sentryUnpluginFactory({
})
);

if (options.injectRelease && releaseName) {
if (!options.release.inject) {
logger.debug("Release injection disabled via `release.inject`. Will not inject.");
} else if (!options.release.name) {
logger.warn("No release name provided. Will not inject release.");
} else {
const injectionCode = generateGlobalInjectorCode({
release: releaseName,
release: options.release.name,
injectBuildInformation: options._experiments.injectBuildInformation || false,
});

plugins.push(releaseInjectionPlugin(injectionCode));
}

if (options.sourcemaps?.assets) {
plugins.push(debugIdInjectionPlugin());
}

if (releaseName) {
if (!options.release.name) {
logger.warn("No release name provided. Will not manage release.");
} else if (!options.authToken) {
logger.warn("No auth token provided. Will not manage release.");
} else if (!options.org) {
logger.warn("No org provided. Will not manage release.");
} else if (!options.project) {
logger.warn("No project provided. Will not manage release.");
} else {
plugins.push(
releaseManagementPlugin({
logger,
cliInstance: cli,
releaseName: releaseName,
shouldCleanArtifacts: options.cleanArtifacts,
shouldUploadSourceMaps: options.uploadSourceMaps,
shouldFinalizeRelease: options.finalize,
include: options.include,
setCommitsOption: options.setCommits,
deployOptions: options.deploy,
dist: options.dist,
releaseName: options.release.name,
shouldCreateRelease: options.release.create,
shouldCleanArtifacts: options.release.cleanArtifacts,
shouldFinalizeRelease: options.release.finalize,
include: options.release.uploadLegacySourcemaps,
setCommitsOption: options.release.setCommits,
deployOptions: options.release.deploy,
dist: options.release.dist,
handleRecoverableError: handleRecoverableError,
sentryHub,
sentryClient,
sentryCliOptions: {
authToken: options.authToken,
org: options.org,
project: options.project,
silent: options.silent,
url: options.url,
vcsRemote: options.release.vcsRemote,
headers: options.headers,
},
})
);
}

if (!unpluginMetaContext.watchMode && options.sourcemaps?.assets !== undefined) {
plugins.push(
debugIdUploadPlugin({
assets: options.sourcemaps.assets,
ignore: options.sourcemaps.ignore,
dist: options.dist,
releaseName: releaseName,
logger: logger,
cliInstance: cli,
handleRecoverableError: handleRecoverableError,
sentryHub,
sentryClient,
})
);
if (options.sourcemaps) {
if (!options.authToken) {
logger.warn("No auth token provided. Will not upload source maps.");
} else if (!options.org) {
logger.warn("No org provided. Will not upload source maps.");
} else if (!options.project) {
logger.warn("No project provided. Will not upload source maps.");
} else if (!options.sourcemaps.assets) {
logger.warn("No assets defined. Will not upload source maps.");
} else {
Comment on lines +169 to +178
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Personally I'd prefer to extract this validation to a function (per plugin) to declutter this one a little but feel free to leave it as is as.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently has a use for TS. We could put it into a helper but I am not sure if it will actually simplify the code by a lot. Gonna leave this for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh I see, yeah, then let's just leave it.

plugins.push(debugIdInjectionPlugin());
plugins.push(
debugIdUploadPlugin({
assets: options.sourcemaps.assets,
ignore: options.sourcemaps.ignore,
dist: options.release.dist,
releaseName: options.release.name,
logger: logger,
handleRecoverableError: handleRecoverableError,
sentryHub,
sentryClient,
sentryCliOptions: {
authToken: options.authToken,
org: options.org,
project: options.project,
silent: options.silent,
url: options.url,
vcsRemote: options.release.vcsRemote,
headers: options.headers,
},
})
);
}
}

return plugins;
Expand Down
193 changes: 18 additions & 175 deletions packages/bundler-plugin-core/src/options-mapping.ts
Original file line number Diff line number Diff line change
@@ -1,196 +1,39 @@
import { Logger } from "./sentry/logger";
import { IncludeEntry as UserIncludeEntry, Options as UserOptions } from "./types";
import { arrayify } from "./utils";
import { Options as UserOptions } from "./types";
import { determineReleaseName } from "./utils";

type RequiredInternalOptions = Required<
Pick<
UserOptions,
| "finalize"
| "dryRun"
| "debug"
| "silent"
| "cleanArtifacts"
| "telemetry"
| "_experiments"
| "injectRelease"
| "uploadSourceMaps"
>
>;

type OptionalInternalOptions = Partial<
Pick<
UserOptions,
| "org"
| "project"
| "authToken"
| "url"
| "vcsRemote"
| "dist"
| "errorHandler"
| "setCommits"
| "deploy"
| "configFile"
| "headers"
| "sourcemaps"
| "release"
>
>;

type NormalizedInternalOptions = {
releaseInjectionTargets: (string | RegExp)[] | ((filePath: string) => boolean) | undefined;
include: InternalIncludeEntry[];
};

export type NormalizedOptions = RequiredInternalOptions &
OptionalInternalOptions &
NormalizedInternalOptions;

type RequiredInternalIncludeEntry = Required<
Pick<
UserIncludeEntry,
"paths" | "ext" | "stripCommonPrefix" | "sourceMapReference" | "rewrite" | "validate"
>
>;

type OptionalInternalIncludeEntry = Partial<
Pick<UserIncludeEntry, "ignoreFile" | "urlPrefix" | "urlSuffix" | "stripPrefix">
>;

export type InternalIncludeEntry = RequiredInternalIncludeEntry &
OptionalInternalIncludeEntry & {
ignore: string[];
};
export type NormalizedOptions = ReturnType<typeof normalizeUserOptions>;

export const SENTRY_SAAS_URL = "https://sentry.io";

export function normalizeUserOptions(userOptions: UserOptions) {
const options = {
// include is the only strictly required option
// (normalizeInclude needs all userOptions to access top-level include options)
include: normalizeInclude(userOptions),

// These options must be set b/c we need them for release injection.
// They can also be set as environment variables. Technically, they
// could be set in the config file but this would be too late for
// release injection because we only pass the config file path
// to the CLI
org: userOptions.org ?? process.env["SENTRY_ORG"],
project: userOptions.project ?? process.env["SENTRY_PROJECT"],
// Falling back to the empty string here b/c at a later point, we use
// Sentry CLI to determine a release if none was specified via options
// or env vars. In case we don't find one, we'll bail at that point.
release: userOptions.release ?? process.env["SENTRY_RELEASE"],
// We technically don't need the URL for anything release-specific
// but we want to make sure that we're only sending Sentry data
// of SaaS customers. Hence we want to read it anyway.
authToken: userOptions.authToken ?? process.env["SENTRY_AUTH_TOKEN"],
url: userOptions.url ?? process.env["SENTRY_URL"] ?? SENTRY_SAAS_URL,

// Options with default values
finalize: userOptions.finalize ?? true,
cleanArtifacts: userOptions.cleanArtifacts ?? false,
dryRun: userOptions.dryRun ?? false,
headers: userOptions.headers,
debug: userOptions.debug ?? false,
silent: userOptions.silent ?? false,
errorHandler: userOptions.errorHandler,
telemetry: userOptions.telemetry ?? true,
injectRelease: userOptions.injectRelease ?? true,
uploadSourceMaps: userOptions.uploadSourceMaps ?? true,
disable: userOptions.disable ?? false,
sourcemaps: userOptions.sourcemaps,
release: {
name: determineReleaseName(),
inject: true,
create: true,
finalize: true,
vcsRemote: process.env["SENTRY_VSC_REMOTE"] ?? "origin",
cleanArtifacts: false,
...userOptions.release,
},
_experiments: userOptions._experiments ?? {},

// These options and can also be set via env variables or the config file.
// If they're set in the options, we simply pass them to the CLI constructor.
// Sentry CLI will internally query env variables and read its config file if
// the passed options are undefined.
authToken: userOptions.authToken, // env var: `SENTRY_AUTH_TOKEN`

headers: userOptions.headers,

vcsRemote: userOptions.vcsRemote, // env var: `SENTRY_VSC_REMOTE`

// Optional options
setCommits: userOptions.setCommits,
deploy: userOptions.deploy,
releaseInjectionTargets: normalizeReleaseInjectionTargets(userOptions.releaseInjectionTargets),
dist: userOptions.dist,
errorHandler: userOptions.errorHandler,
configFile: userOptions.configFile,
};

return options;
}

/**
* Converts the user-facing `releaseInjectionTargets` option to the internal
* `releaseInjectionTargets` option
*/
function normalizeReleaseInjectionTargets(
userReleaseInjectionTargets: UserOptions["releaseInjectionTargets"]
): (string | RegExp)[] | ((filePath: string) => boolean) | undefined {
if (userReleaseInjectionTargets === undefined) {
return undefined;
} else if (typeof userReleaseInjectionTargets === "function") {
return userReleaseInjectionTargets;
} else {
return arrayify(userReleaseInjectionTargets);
}
}

/**
* Converts the user-facing `include` option to the internal `include` option,
* resulting in an array of `InternalIncludeEntry` objects. This later on lets us
* work with only one type of include data structure instead of multiple.
*
* During the process, we hoist top-level include options (e.g. urlPrefix) into each
* object if they were not alrady specified in an `IncludeEntry`, making every object
* fully self-contained. This is also the reason why we pass the entire options
* object and not just `include`.
*
* @param userOptions the entire user-facing `options` object
*
* @return an array of `InternalIncludeEntry` objects.
*/
function normalizeInclude(userOptions: UserOptions): InternalIncludeEntry[] {
if (!userOptions.include) {
return [];
}

return arrayify(userOptions.include)
.map((includeItem) =>
typeof includeItem === "string" ? { paths: [includeItem] } : includeItem
)
.map((userIncludeEntry) => normalizeIncludeEntry(userOptions, userIncludeEntry));
}

/**
* Besides array-ifying the `ignore` option, this function hoists top level options into the items of the `include`
* option. This is to simplify the handling of of the `include` items later on.
*/
function normalizeIncludeEntry(
userOptions: UserOptions,
includeEntry: UserIncludeEntry
): InternalIncludeEntry {
const ignoreOption = includeEntry.ignore ?? userOptions.ignore ?? ["node_modules"];
const ignore = Array.isArray(ignoreOption) ? ignoreOption : [ignoreOption];

// We're prefixing all entries in the `ext` option with a `.` (if it isn't already) to align with Node.js' `path.extname()`
const ext = includeEntry.ext ?? userOptions.ext ?? ["js", "map", "jsbundle", "bundle"];
const dotPrefixedExt = ext.map((extension) => `.${extension.replace(/^\./, "")}`);

return {
paths: includeEntry.paths,
ignore,
ignoreFile: includeEntry.ignoreFile ?? userOptions.ignoreFile,
ext: dotPrefixedExt,
urlPrefix: includeEntry.urlPrefix ?? userOptions.urlPrefix,
urlSuffix: includeEntry.urlSuffix ?? userOptions.urlSuffix,
stripPrefix: includeEntry.stripPrefix ?? userOptions.stripPrefix,
stripCommonPrefix: includeEntry.stripCommonPrefix ?? userOptions.stripCommonPrefix ?? false,
sourceMapReference: includeEntry.sourceMapReference ?? userOptions.sourceMapReference ?? true,
rewrite: includeEntry.rewrite ?? userOptions.rewrite ?? true,
validate: includeEntry.validate ?? userOptions.validate ?? false,
};
}

/**
* Validates a few combinations of options that are not checked by Sentry CLI.
*
Expand All @@ -204,7 +47,7 @@ function normalizeIncludeEntry(
* @returns `true` if the options are valid, `false` otherwise
*/
export function validateOptions(options: NormalizedOptions, logger: Logger): boolean {
const setCommits = options.setCommits;
const setCommits = options.release?.setCommits;
if (setCommits) {
if (!setCommits.auto && !(setCommits.repo && setCommits.commit)) {
logger.error(
Expand All @@ -222,7 +65,7 @@ export function validateOptions(options: NormalizedOptions, logger: Logger): boo
}
}

if (options.deploy && !options.deploy.env) {
if (options.release?.deploy && !options.release?.deploy.env) {
logger.error(
"The `deploy` option was specified but is missing the required `env` property.",
"Please set the `env` property."
Expand Down
Loading