Skip to content
Draft
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
```
The wizard will install the `@sentry/cloudflare` SDK and show next steps. Currently only Cloudflare Workers are supported.

- feat(cloudflare): Enable update of the wrangler file ([#1149](https://github.com/getsentry/sentry-wizard/pull/1149))

<details>
<summary><strong>Internal Changes</strong></summary>

Expand Down
16 changes: 16 additions & 0 deletions e2e-tests/tests/cloudflare-worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as path from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { Integration } from '../../lib/Constants';
import {
checkFileContents,
checkIfBuilds,
checkPackageJson,
cleanupGit,
Expand All @@ -22,13 +23,18 @@ describe('cloudflare-worker', () => {
const integration = Integration.cloudflare;

let wizardExitCode: number;
let expectedCompatibilityDate: string;

beforeAll(async () => {
initGit(projectDir);
revertLocalChanges(projectDir);

// Capture the date before running the wizard (wizard runs in subprocess)
expectedCompatibilityDate = new Date().toISOString().slice(0, 10);

wizardExitCode = await withEnv({
cwd: projectDir,
debug: true,
})
.defineInteraction()
.expectOutput(
Expand Down Expand Up @@ -72,4 +78,14 @@ describe('cloudflare-worker', () => {
it('builds correctly', async () => {
await checkIfBuilds(projectDir);
});

it('wrangler.jsonc file contains Sentry configuration', () => {
checkFileContents(`${projectDir}/wrangler.jsonc`, [
`"compatibility_date": "${expectedCompatibilityDate}"`,
'"global_fetch_strictly_public"',
'"nodejs_als"',
'"version_metadata": {',
'"binding": "CF_VERSION_METADATA"',
]);
});
});
2 changes: 1 addition & 1 deletion e2e-tests/tests/cloudflare-wrangler-sourcemaps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('Cloudflare-Wrangler-Sourcemaps-Wizard', () => {
initGit(projectDir);
revertLocalChanges(projectDir);

wizardExitCode = await withEnv({ cwd: projectDir })
wizardExitCode = await withEnv({ cwd: projectDir, debug: true })
.defineInteraction()
.step('intro', ({ expectOutput }) => {
expectOutput('This wizard will help you upload source maps to Sentry');
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"glob": "9.3.5",
"inquirer": "^6.2.0",
"js-yaml": "^4.1.1",
"jsonc-parser": "^3.3.1",
"magicast": "^0.2.10",
"opn": "^5.4.0",
"read-env": "^1.3.0",
Expand Down
11 changes: 11 additions & 0 deletions src/cloudflare/cloudflare-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { hasPackageInstalled } from '../utils/package-json';
import type { WizardOptions } from '../utils/types';
import { createSentryInitFile } from './sdk-setup';
import { abortIfSpotlightNotSupported } from '../utils/abort-if-sportlight-not-supported';
import { updateWranglerConfig } from './wrangler/update-wrangler-config';

export async function runCloudflareWizard(
options: WizardOptions,
Expand Down Expand Up @@ -52,6 +53,16 @@ async function runCloudflareWizardWithTelemetry(

await ensurePackageIsInstalled(packageJson, 'wrangler', 'Cloudflare');

await traceStep('Update Wrangler config with Sentry requirements', () =>
updateWranglerConfig({
compatibility_flags: ['nodejs_als'],
compatibility_date: new Date().toISOString().slice(0, 10),
version_metadata: {
binding: 'CF_VERSION_METADATA',
},
}),
);

const projectData = await getOrAskForProjectData(
options,
'node-cloudflare-workers',
Expand Down
31 changes: 31 additions & 0 deletions src/cloudflare/wrangler/create-wrangler-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @ts-expect-error - clack is ESM and TS complains about that. It works though
import clack from '@clack/prompts';
import chalk from 'chalk';
import fs from 'node:fs';
import path from 'node:path';

/**
* Creates a basic wrangler.jsonc config file for a Cloudflare Worker
*/
export function createWranglerConfig(): void {
const configPath = path.join(process.cwd(), 'wrangler.jsonc');

const config = {
$schema: 'node_modules/wrangler/config-schema.json',
name: 'my-worker',
main: 'src/index.ts',
};

fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');

clack.log.success(
`Created ${chalk.cyan('wrangler.jsonc')} configuration file.`,
);
clack.log.info(
`Please update the ${chalk.cyan('name')} and ${chalk.cyan(
'main',
)} fields in ${chalk.cyan(
'wrangler.jsonc',
)} to match your worker name and entry point.`,
);
}
22 changes: 22 additions & 0 deletions src/cloudflare/wrangler/ensure-wrangler-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// @ts-expect-error - clack is ESM and TS complains about that. It works though
import clack from '@clack/prompts';
import chalk from 'chalk';
import { findWranglerConfig } from './find-wrangler-config';
import { createWranglerConfig } from './create-wrangler-config';

/**
* Ensures a wrangler config exists, creating one if necessary
*/
export function ensureWranglerConfig(): void {
const existingConfig = findWranglerConfig();

if (existingConfig) {
clack.log.info(
`Found existing Wrangler config: ${chalk.cyan(existingConfig)}`,
);
return;
}

clack.log.step('No Wrangler configuration file found.');
createWranglerConfig();
}
18 changes: 18 additions & 0 deletions src/cloudflare/wrangler/find-wrangler-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import fs from 'node:fs';
import path from 'node:path';

/**
* Checks if a wrangler config file exists in the project
*/
export function findWranglerConfig(): string | undefined {
const possibleConfigs = ['wrangler.jsonc', 'wrangler.json', 'wrangler.toml'];

for (const configFile of possibleConfigs) {
const configPath = path.join(process.cwd(), configFile);
if (fs.existsSync(configPath)) {
return configFile;
}
}

return undefined;
}
43 changes: 43 additions & 0 deletions src/cloudflare/wrangler/get-entry-point-from-wrangler-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import fs from 'node:fs';
import path from 'node:path';
import { findWranglerConfig } from './find-wrangler-config';

/**
* Reads the main entry point from the wrangler config file
* Returns undefined if no config exists or if main field is not specified
*/
export async function getEntryPointFromWranglerConfig(): Promise<
string | undefined
> {
const configFile = findWranglerConfig();

if (!configFile) {
return undefined;
}

const configPath = path.join(process.cwd(), configFile);
const configContent = fs.readFileSync(configPath, 'utf-8');
const extname = path.extname(configFile);

switch (extname) {
case '.toml': {
const mainMatch = configContent.match(/^main\s*=\s*["'](.+)["']/m);

return mainMatch ? mainMatch[1] : undefined;
}

case '.json':
case '.jsonc':
try {
const jsonc = await import('jsonc-parser');
const config = jsonc.parse(configContent) as { main?: string };

return config.main;
} catch {
return undefined;
}

default:
return undefined;
}
}
140 changes: 140 additions & 0 deletions src/cloudflare/wrangler/update-wrangler-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// @ts-expect-error - clack is ESM and TS complains about that. It works though
import clack from '@clack/prompts';
import chalk from 'chalk';
import fs from 'node:fs';
import path from 'node:path';
import { findWranglerConfig } from './find-wrangler-config';
import { makeCodeSnippet, showCopyPasteInstructions } from '../../utils/clack';
import type { FormattingOptions } from 'jsonc-parser';

type WranglerConfigUpdates = {
compatibility_date?: string;
compatibility_flags?: string[];
version_metadata?: {
binding: string;
};
[key: string]: unknown;
};

const getTomlConfigSnippet = () => {
return makeCodeSnippet(true, (unchanged, plus) =>
plus(
`
compatibility_flags = ["nodejs_als"]
compatibility_date = "${new Date().toISOString().slice(0, 10)}"

[version_metadata]
binding = "CF_VERSION_METADATA"`,
),
);
};

/**
* Updates the wrangler config file with the provided configuration
* Handles .toml (instructions only), .json, and .jsonc formats
* For arrays: merges and deduplicates values
* For objects: deep merges
* For other types: overwrites
*/
export async function updateWranglerConfig(
updates: WranglerConfigUpdates,
): Promise<boolean> {
const configFile = findWranglerConfig();

if (!configFile) {
clack.log.warn('No wrangler config file found.');

return false;
}

const configPath = path.join(process.cwd(), configFile);

try {
const configContent = fs.readFileSync(configPath, 'utf-8');
const extname = path.extname(configFile);

switch (extname) {
case '.jsonc':
case '.json':
await updateJsoncConfig(configPath, configContent, updates);
clack.log.success(
`Updated ${chalk.cyan(configFile)} with Sentry configuration.`,
);

break;
case '.toml':
await showCopyPasteInstructions({
filename: configFile,
codeSnippet: getTomlConfigSnippet(),
});
break;
}

return true;
} catch (error) {
clack.log.error(
`Failed to update ${chalk.cyan(configFile)}: ${
error instanceof Error ? error.message : String(error)
}`,
);
return false;
}
}

/**
* Updates a JSON/JSONC config file using jsonc-parser
* Preserves comments and formatting while merging values
*/
async function updateJsoncConfig(
configPath: string,
content: string,
updates: WranglerConfigUpdates,
): Promise<void> {
const jsonc = await import('jsonc-parser');
// Parse the JSONC to get existing values
const existingConfig = jsonc.parse(content) as Record<string, unknown>;

// Apply all modifications using jsonc-parser's modify function
let updatedContent = content;
const formattingOptions: FormattingOptions = {
tabSize: 2,
insertSpaces: true,
eol: '\n',
};

for (const [key, value] of Object.entries(updates)) {
const mergedValue = mergeValue(existingConfig[key], value);
const edits = jsonc.modify(updatedContent, [key], mergedValue, {
formattingOptions,
});

updatedContent = jsonc.applyEdits(updatedContent, edits);
}

fs.writeFileSync(configPath, updatedContent, 'utf-8');
}

/**
* Merges a new value with an existing value
* For arrays: merges and deduplicates
* For objects: shallow merges
* For other types: overwrites
*/
function mergeValue<T = unknown>(existingValue: T, newValue: T): T {
if (Array.isArray(existingValue) && Array.isArray(newValue)) {
return [...new Set([...existingValue, ...newValue])] as T;
}

if (
typeof existingValue === 'object' &&
existingValue !== null &&
!Array.isArray(existingValue) &&
typeof newValue === 'object' &&
newValue !== null &&
!Array.isArray(newValue)
) {
return { ...existingValue, ...newValue };
}

return newValue;
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2570,6 +2570,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz"
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=

jsonc-parser@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz#f2a524b4f7fd11e3d791e559977ad60b98b798b4"
integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==

keyv@^4.5.3:
version "4.5.4"
resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz"
Expand Down
Loading