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
5 changes: 5 additions & 0 deletions .changeset/worker-wrangler-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@gradientedge/cdk-utils-cloudflare': patch
---

Add reusable Worker asset path resolution, Wrangler base configuration translation, and opt-in workers.dev subdomain control to CloudflareWorkerSite.
1 change: 1 addition & 0 deletions packages/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@pulumi/std": "2.3.2",
"@types/lodash": "4.17.25",
"app-root-path": "3.1.0",
"jsonc-parser": "3.3.1",
"lodash": "4.18.1"
},
"keywords": [
Expand Down
116 changes: 115 additions & 1 deletion packages/cloudflare/src/construct/worker-site/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ import * as path from 'path'

import * as aws from '@pulumi/aws'
import * as azure from '@pulumi/azure-native'
import { GetZoneResult, Ruleset, WorkersCustomDomain, WorkersScript, Zone, ZoneSetting } from '@pulumi/cloudflare'
import {
GetZoneResult,
Ruleset,
WorkersCustomDomain,
WorkersScript,
WorkersScriptSubdomain,
Zone,
ZoneSetting,
} from '@pulumi/cloudflare'
import { WorkersScriptBinding } from '@pulumi/cloudflare/types/input.js'
import { ComponentResourceOptions, Output } from '@pulumi/pulumi'
import * as std from '@pulumi/std'
import { parse } from 'jsonc-parser'

import { CommonCloudflareConstruct } from '../../common/index.js'

Expand Down Expand Up @@ -38,6 +47,8 @@ export class CloudflareWorkerSite extends CommonCloudflareConstruct {
siteWorkerScript: WorkersScript
/** The Cloudflare Workers custom domain resource */
siteWorkerDomain: WorkersCustomDomain
/** Optional workers.dev subdomain configuration for the script */
siteWorkerSubdomain: WorkersScriptSubdomain
/** The Cloudflare ruleset resource for the worker site */
siteRuleSet: Ruleset
/** The Cloudflare zone setting resource for the worker site */
Expand All @@ -64,10 +75,15 @@ export class CloudflareWorkerSite extends CommonCloudflareConstruct {
* @summary Initialise and provision resources
*/
protected initResources() {
this.translateWorkersBaseConfig()
this.resolveWorkerAssetPath()
this.resolveZone()
this.createWorkerPlainTextBindingEnvironmentVariables()
this.createWorkerSecretTextBindingEnvironmentVariables()
this.resolveEnvironmentVariables()
this.createWorker()
this.createWorkerDomain()
this.createWorkerSubdomain()
this.createRuleset()
this.createZoneSetting()
}
Expand Down Expand Up @@ -96,6 +112,91 @@ export class CloudflareWorkerSite extends CommonCloudflareConstruct {
}
}

protected resolveWorkerAssetPath() {
const cdkParentDir = path.resolve(process.cwd(), '..')
const fromCwd = path.resolve(cdkParentDir, this.props.siteWorkerAsset)
const fromDist = path.resolve(cdkParentDir, '..', this.props.siteWorkerAsset)
const workerAsset = [fromCwd, fromDist].find(candidate => fs.existsSync(candidate)) ?? fromCwd
this.props.siteWorkerAsset = path.relative(cdkParentDir, workerAsset)
}

protected translateWorkersBaseConfig() {
const configSource = this.props.siteWranglerConfig || this.props.siteSource
if (!configSource) return
const configRelPath = configSource.replace(/^\.\.\//, '')
const fromCwd = path.resolve(process.cwd(), '..', configRelPath, 'wrangler.jsonc')
const fromDist = path.resolve(process.cwd(), '..', '..', configRelPath, 'wrangler.jsonc')
const configPath = fs.existsSync(fromCwd) ? fromCwd : fromDist
const configText = fs.readFileSync(configPath, 'utf8')
const wranglerConfig = parse(configText) as {
name?: string
main?: string
compatibility_date?: string
compatibility_flags?: string[]
cache?: { enabled?: boolean }
observability?: {
enabled?: boolean
logs?: { enabled?: boolean; invocation_logs?: boolean; head_sampling_rate?: number }
traces?: { enabled?: boolean; head_sampling_rate?: number }
}
assets?: { directory?: string; binding?: string; not_found_handling?: string }
vars?: Record<string, string>
}

this.workerPlainTextBindingEnvironmentVariables = [
...Object.entries(wranglerConfig.vars ?? {}).map(([name, text]) => ({ name, text, type: 'plain_text' })),
...(wranglerConfig.assets?.binding ? [{ name: wranglerConfig.assets.binding, type: 'assets' }] : []),
]
this.props.siteWorkerScript = {
...this.props.siteWorkerScript,
scriptName: this.props.siteWorkerScript.scriptName ?? wranglerConfig.name,
mainModule: wranglerConfig.main ?? this.props.siteWorkerScript?.mainModule,
compatibilityDate: wranglerConfig.compatibility_date ?? this.props.siteWorkerScript?.compatibilityDate,
compatibilityFlags: wranglerConfig.compatibility_flags ?? this.props.siteWorkerScript?.compatibilityFlags,
cacheOptions: wranglerConfig.cache
? { enabled: wranglerConfig.cache.enabled }
: this.props.siteWorkerScript?.cacheOptions,
observability: wranglerConfig.observability
? {
enabled: wranglerConfig.observability.enabled ?? false,
logs: wranglerConfig.observability.logs
? {
enabled: wranglerConfig.observability.logs.enabled ?? wranglerConfig.observability.enabled ?? false,
invocationLogs: wranglerConfig.observability.logs.invocation_logs ?? true,
headSamplingRate: wranglerConfig.observability.logs.head_sampling_rate,
}
: undefined,
traces: wranglerConfig.observability.traces
? {
enabled: wranglerConfig.observability.traces.enabled,
headSamplingRate: wranglerConfig.observability.traces.head_sampling_rate,
}
: undefined,
}
: this.props.siteWorkerScript?.observability,
assets: wranglerConfig.assets
? {
directory: wranglerConfig.assets.directory
? path.resolve(path.dirname(configPath), wranglerConfig.assets.directory)
: undefined,
config: {
notFoundHandling: wranglerConfig.assets.not_found_handling,
},
}
: this.props.siteWorkerScript?.assets,
}
}

/**
* @summary Create the plain text binding environment variables for the static site
*/
protected createWorkerPlainTextBindingEnvironmentVariables() {}

/**
* @summary Create the secret text binding environment variables for the static site
*/
protected createWorkerSecretTextBindingEnvironmentVariables() {}

/**
* @summary Create the worker
*/
Expand Down Expand Up @@ -126,6 +227,19 @@ export class CloudflareWorkerSite extends CommonCloudflareConstruct {
)
}

protected createWorkerSubdomain() {
if (!this.props.siteWorkerSubdomain) return
this.siteWorkerSubdomain = new WorkersScriptSubdomain(
`${this.id}-worker-subdomain`,
{
...this.props.siteWorkerSubdomain,
accountId: this.props.accountId,
scriptName: this.siteWorkerScript.scriptName,
},
{ parent: this, dependsOn: [this.siteWorkerScript] }
)
}

/**
* @summary Resolve secrets from AWS Secrets Manager
* @param secretName the secret name
Expand Down
7 changes: 7 additions & 0 deletions packages/cloudflare/src/construct/worker-site/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CommonCloudflareStackProps } from '../../common/index.js'
import {
WorkerDomainProps,
WorkerScriptProps,
WorkerSubdomainProps,
RulesetProps,
ZoneProps,
ZoneSettingProps,
Expand All @@ -20,8 +21,14 @@ export interface CloudflareWorkerSiteProps extends CommonCloudflareStackProps {
siteWorkerScript: WorkerScriptProps
/** The Cloudflare Workers custom domain properties */
siteWorkerDomain: WorkerDomainProps
/** Optional workers.dev and preview URL settings */
siteWorkerSubdomain?: WorkerSubdomainProps
/** The relative path to the worker script asset file */
siteWorkerAsset: string
/** Optional path to the Worker project's base Wrangler configuration directory */
siteWranglerConfig?: string
/** Legacy path to the Worker project's base Wrangler configuration directory */
siteSource?: string
/** The Cloudflare ruleset properties for the worker site */
siteRuleSet: RulesetProps
/** The Cloudflare zone setting properties for the worker site */
Expand Down
8 changes: 8 additions & 0 deletions packages/cloudflare/src/services/worker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
WorkersKvNamespaceArgs,
WorkersRouteArgs,
WorkersScriptArgs,
WorkersScriptSubdomainArgs,
} from '@pulumi/cloudflare'

/**
Expand All @@ -14,6 +15,13 @@ import {
*/
export interface WorkerDomainProps extends WorkersCustomDomainArgs {}

/**
* Properties for configuring a Cloudflare Worker's workers.dev subdomain
* @see [Pulumi Cloudflare WorkersScriptSubdomain]{@link https://www.pulumi.com/registry/packages/cloudflare/api-docs/workersscriptsubdomain/}
* @category Interface
*/
export interface WorkerSubdomainProps extends WorkersScriptSubdomainArgs {}

/**
* Properties for creating a Cloudflare Workers Route
* @see [Pulumi Cloudflare WorkersRoute]{@link https://www.pulumi.com/registry/packages/cloudflare/api-docs/workerroute/}
Expand Down
81 changes: 81 additions & 0 deletions packages/cloudflare/test/construct/worker-site.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import fs from 'fs'
import os from 'os'
import * as path from 'path'
import { fileURLToPath } from 'url'

import { GetZoneResult, Zone } from '@pulumi/cloudflare'
import * as pulumi from '@pulumi/pulumi'
import { Output } from '@pulumi/pulumi'
import { vi } from 'vitest'
import { CloudflareWorkerSite, CloudflareWorkerSiteProps, CommonCloudflareStack } from '../../src/index.js'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
Expand Down Expand Up @@ -112,6 +115,14 @@ class TestCommonConstruct extends CloudflareWorkerSite {
super(name, props)
this.initResources()
}

public testResolveWorkerAssetPath() {
this.resolveWorkerAssetPath()
}

public testTranslateWorkersBaseConfig() {
this.translateWorkersBaseConfig()
}
}

class TestExistingZoneConstruct extends CloudflareWorkerSite {
Expand Down Expand Up @@ -164,6 +175,76 @@ describe('TestCloudflareWorkerSite', () => {
test('handles mis-configurations as expected', () => {
expect(() => new TestInvalidCommonCloudflareStack('test-stack', testStackProps)).toThrow()
})

test('only creates a workers.dev subdomain when configured', async () => {
expect(stack.construct.siteWorkerSubdomain).toBeUndefined()
const worker = new TestCommonConstruct('test-subdomain-worker', {
...stack.construct.props,
siteWorkerSubdomain: {
accountId: 'test-account',
scriptName: 'test-script-dev',
enabled: false,
previewsEnabled: false,
},
})
const accountId = worker.siteWorkerSubdomain.accountId as Output<string> & {
promise: (withUnknowns?: boolean) => Promise<string>
}
const scriptName = worker.siteWorkerSubdomain.scriptName as Output<string> & {
promise: (withUnknowns?: boolean) => Promise<string>
}
const enabled = worker.siteWorkerSubdomain.enabled as Output<boolean> & {
promise: (withUnknowns?: boolean) => Promise<boolean>
}
const previewsEnabled = worker.siteWorkerSubdomain.previewsEnabled as Output<boolean> & {
promise: (withUnknowns?: boolean) => Promise<boolean>
}
expect(await accountId.promise(true)).toBe('123456789012')
expect(await scriptName.promise(true)).toBe('test-script-dev')
expect(await enabled.promise(true)).toBe(false)
expect(await previewsEnabled.promise(true)).toBe(false)
})

test('resolves the Worker asset from a dist working directory', () => {
const originalAsset = stack.construct.props.siteWorkerAsset
const workingDirectory = process.cwd()
const expectedAsset = path.relative(workingDirectory, path.resolve(workingDirectory, '..', originalAsset))
const cwd = vi.spyOn(process, 'cwd').mockReturnValue(path.join(workingDirectory, 'dist'))
try {
stack.construct.testResolveWorkerAssetPath()
expect(stack.construct.props.siteWorkerAsset).toEqual(expectedAsset)
} finally {
cwd.mockRestore()
stack.construct.props.siteWorkerAsset = originalAsset
}
})

test('translates a base Wrangler JSONC config', () => {
const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'worker-site-config-'))
const originalScript = stack.construct.props.siteWorkerScript
const originalBindings = stack.construct.workerPlainTextBindingEnvironmentVariables
fs.writeFileSync(
path.join(configDirectory, 'wrangler.jsonc'),
'{ // Worker base config\n "main": "worker.js", "compatibility_date": "2026-04-19", "vars": {"HELLO": "world"}, "assets": {"directory": "./public", "binding": "ASSETS", "not_found_handling": "404-page"} }'
)
try {
stack.construct.props.siteWranglerConfig = configDirectory
stack.construct.testTranslateWorkersBaseConfig()
expect(stack.construct.props.siteWorkerScript.mainModule).toEqual('worker.js')
expect(stack.construct.props.siteWorkerScript.compatibilityDate).toEqual('2026-04-19')
const assets = stack.construct.props.siteWorkerScript.assets as { directory?: string } | undefined
expect(assets?.directory).toEqual(path.join(configDirectory, 'public'))
expect(stack.construct.workerPlainTextBindingEnvironmentVariables).toEqual([
{ name: 'HELLO', text: 'world', type: 'plain_text' },
{ name: 'ASSETS', type: 'assets' },
])
} finally {
stack.construct.props.siteWranglerConfig = undefined
stack.construct.props.siteWorkerScript = originalScript
stack.construct.workerPlainTextBindingEnvironmentVariables = originalBindings
fs.rmSync(configDirectory, { recursive: true, force: true })
}
})
})

describe('TestCloudflareWorkerSite', () => {
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading