diff --git a/packages/cli/src/build/config.ts b/packages/cli/src/build/config.ts index 8ac33e93..69caf420 100644 --- a/packages/cli/src/build/config.ts +++ b/packages/cli/src/build/config.ts @@ -207,5 +207,40 @@ export function getRollupConfigs(pkg: Package, aliases: Aliases) { }); } + let hasWorkerField = pkg.entrypoints[0].json.worker !== undefined; + + if (hasWorkerField) { + configs.push({ + config: getRollupConfig( + pkg, + pkg.entrypoints, + aliases, + "worker", + () => {} + ), + outputs: [ + { + format: "cjs" as const, + entryFileNames: "[name].worker.cjs.js", + chunkFileNames: "dist/[name]-[hash].worker.cjs.js", + dir: pkg.directory, + exports: "named" as const, + interop, + plugins: cjsPlugins, + }, + ...(hasModuleField + ? [ + { + format: "es" as const, + entryFileNames: "[name].worker.esm.js", + chunkFileNames: "dist/[name]-[hash].worker.esm.js", + dir: pkg.directory, + }, + ] + : []), + ], + }); + } + return configs; } diff --git a/packages/cli/src/build/rollup.ts b/packages/cli/src/build/rollup.ts index 564ad484..7ab4bab0 100644 --- a/packages/cli/src/build/rollup.ts +++ b/packages/cli/src/build/rollup.ts @@ -31,7 +31,12 @@ const makeExternalPredicate = (externalArr: string[]) => { return (id: string) => pattern.test(id); }; -export type RollupConfigType = "umd" | "browser" | "node-dev" | "node-prod"; +export type RollupConfigType = + | "umd" + | "browser" + | "node-dev" + | "node-prod" + | "worker"; export let getRollupConfig = ( pkg: Package, @@ -193,6 +198,15 @@ export let getRollupConfig = ( }, preventAssignment: true, }), + type === "worker" && + replace({ + values: { + ["typeof " + "document"]: JSON.stringify("undefined"), + ["typeof " + "window"]: JSON.stringify("undefined"), + ["typeof " + "process"]: JSON.stringify("undefined"), + }, + preventAssignment: true, + }), ].filter((x): x is Plugin => !!x), }; diff --git a/packages/cli/src/dev.ts b/packages/cli/src/dev.ts index b95de07e..86d36e7a 100644 --- a/packages/cli/src/dev.ts +++ b/packages/cli/src/dev.ts @@ -218,6 +218,17 @@ unregister(); ); } } + if (entrypoint.json.worker) { + let workerField = validFields.worker(entrypoint); + for (let key of Object.keys(workerField)) { + promises.push( + fs.symlink( + entrypoint.source, + path.join(entrypoint.directory, workerField[key]) + ) + ); + } + } return Promise.all(promises); }) diff --git a/packages/cli/src/entrypoint.ts b/packages/cli/src/entrypoint.ts index e238fc9b..7f50b8cb 100644 --- a/packages/cli/src/entrypoint.ts +++ b/packages/cli/src/entrypoint.ts @@ -9,6 +9,7 @@ export class Entrypoint extends Item<{ module?: JSONValue; "umd:main"?: JSONValue; browser?: JSONValue; + worker?: JSONValue; preconstruct: { source?: JSONValue; umdName?: JSONValue; diff --git a/packages/cli/src/init.ts b/packages/cli/src/init.ts index 6383cdb9..90d132d3 100644 --- a/packages/cli/src/init.ts +++ b/packages/cli/src/init.ts @@ -93,6 +93,30 @@ async function doInit(pkg: Package) { } } + let someEntrypointsHaveAWorkerField = pkg.entrypoints.some( + (entrypoint) => entrypoint.json.worker !== undefined + ); + + let someEntrypointsHaveAnInvalidWorkerField = pkg.entrypoints.some( + (entrypoint) => !isFieldValid.worker(entrypoint) + ); + if ( + someEntrypointsHaveAWorkerField && + someEntrypointsHaveAnInvalidWorkerField + ) { + let shouldFixWorkerField = await confirms.fixWorkerField(pkg); + if (shouldFixWorkerField) { + pkg.setFieldOnEntrypoints("worker"); + } else { + throw new FixableError( + errors.fieldMustExistInAllEntrypointsIfExistsDeclinedFixDuringInit( + "worker" + ), + pkg.name + ); + } + } + await Promise.all(pkg.entrypoints.map((x) => x.save())); } diff --git a/packages/cli/src/messages.ts b/packages/cli/src/messages.ts index ed5279f5..3e78db25 100644 --- a/packages/cli/src/messages.ts +++ b/packages/cli/src/messages.ts @@ -2,7 +2,7 @@ import { PKG_JSON_CONFIG_FIELD } from "./constants"; import { createPromptConfirmLoader } from "./prompt"; import chalk from "chalk"; -type Field = "main" | "module" | "browser" | "umd:main"; +type Field = "main" | "module" | "browser" | "umd:main" | "worker"; export let errors = { noSource: (source: string) => @@ -38,6 +38,9 @@ export let confirms = { fixBrowserField: createPromptConfirmLoader( "would you like to fix the browser build?" ), + fixWorkerField: createPromptConfirmLoader( + "would you like to fix the worker build?" + ), createEntrypointPkgJson: createPromptConfirmLoader( "A package.json file does not exist for this entrypoint, would you like to create one automatically?" ), diff --git a/packages/cli/src/package.ts b/packages/cli/src/package.ts index a8d1d7e5..8d00c88f 100644 --- a/packages/cli/src/package.ts +++ b/packages/cli/src/package.ts @@ -26,7 +26,7 @@ function getFieldsUsedInEntrypoints( for (let descriptor of descriptors) { if (descriptor.contents !== undefined) { let parsed = jsonParse(descriptor.contents, descriptor.filename); - for (let field of ["module", "umd:main", "browser"] as const) { + for (let field of ["module", "umd:main", "browser", "worker"] as const) { if (parsed[field] !== undefined) { fields.add(field); } @@ -47,7 +47,7 @@ function getPlainEntrypointContent( string | Record >> = {}; for (const field of fields) { - if (field === "browser") { + if (field === "browser" || field === "worker") { obj[field] = validFieldsFromPkg[field]( pkg, fields.has("module"), @@ -264,7 +264,9 @@ export class Package extends Item<{ return pkg; } - setFieldOnEntrypoints(field: "main" | "browser" | "module" | "umd:main") { + setFieldOnEntrypoints( + field: "main" | "browser" | "module" | "umd:main" | "worker" + ) { this.entrypoints.forEach((entrypoint) => { entrypoint.json = setFieldInOrder( entrypoint.json, diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index 2259ad0c..5d2f1c4c 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -15,11 +15,12 @@ let fields = [ "module", "umd:main", "browser", + "worker", ]; export function setFieldInOrder< Obj extends { [key: string]: any }, - Key extends "main" | "module" | "umd:main" | "browser", + Key extends "main" | "module" | "umd:main" | "browser" | "worker", Val extends any >(obj: Obj, field: Key, value: Val): Obj & { [k in Key]: Val } { if (field in obj) { @@ -129,6 +130,22 @@ export const validFieldsFromPkg = { } return obj; }, + worker( + pkg: Package, + hasModuleBuild: boolean, + entrypointName: string, + forceStrategy?: DistFilenameStrategy + ) { + let safeName = getDistName(pkg, entrypointName, forceStrategy); + + let obj = { + [`./dist/${safeName}.cjs.js`]: `./dist/${safeName}.worker.cjs.js`, + }; + if (hasModuleBuild) { + obj[`./dist/${safeName}.esm.js`] = `./dist/${safeName}.worker.esm.js`; + } + return obj; + }, }; export const validFields = { @@ -148,6 +165,14 @@ export const validFields = { entrypoint.name ); }, + worker(entrypoint: Entrypoint, forceStrategy?: DistFilenameStrategy) { + return validFieldsFromPkg.worker( + entrypoint.package, + entrypoint.json.module !== undefined, + entrypoint.name, + forceStrategy + ); + }, }; export function flowTemplate(hasDefaultExport: boolean, relativePath: string) { diff --git a/packages/cli/src/validate-package.ts b/packages/cli/src/validate-package.ts index 65f0b26f..5a253e2f 100644 --- a/packages/cli/src/validate-package.ts +++ b/packages/cli/src/validate-package.ts @@ -15,6 +15,7 @@ export async function fixPackage(pkg: Package) { module: pkg.entrypoints.some((x) => x.json.module !== undefined), "umd:main": pkg.entrypoints.some((x) => x.json["umd:main"] !== undefined), browser: pkg.entrypoints.some((x) => x.json.browser !== undefined), + worker: pkg.entrypoints.some((x) => x.json.worker !== undefined), }; keys(fields) @@ -41,6 +42,7 @@ export function validatePackage(pkg: Package) { module: pkg.entrypoints[0].json.module !== undefined, "umd:main": pkg.entrypoints[0].json["umd:main"] !== undefined, browser: pkg.entrypoints[0].json.browser !== undefined, + worker: pkg.entrypoints[0].json.worker !== undefined, }; pkg.entrypoints.forEach((entrypoint) => { diff --git a/packages/cli/src/validate.ts b/packages/cli/src/validate.ts index e0e65cc3..4dae257c 100644 --- a/packages/cli/src/validate.ts +++ b/packages/cli/src/validate.ts @@ -25,6 +25,9 @@ export const isFieldValid = { browser(entrypoint: Entrypoint): boolean { return equal(entrypoint.json.browser, validFields.browser(entrypoint)); }, + worker(entrypoint: Entrypoint): boolean { + return equal(entrypoint.json.worker, validFields.worker(entrypoint)); + }, }; export function isUmdNameSpecified(entrypoint: Entrypoint) { @@ -38,7 +41,13 @@ function validateEntrypoint(entrypoint: Entrypoint, log: boolean) { logger.info(infos.validEntrypoint, entrypoint.name); } const fatalErrors: FatalError[] = []; - for (const field of ["main", "module", "umd:main", "browser"] as const) { + for (const field of [ + "main", + "module", + "umd:main", + "browser", + "worker", + ] as const) { if (field !== "main" && entrypoint.json[field] === undefined) { continue; } diff --git a/site/src/pages/configuration.mdx b/site/src/pages/configuration.mdx index 7b6d56e5..637d2983 100644 --- a/site/src/pages/configuration.mdx +++ b/site/src/pages/configuration.mdx @@ -183,3 +183,9 @@ Example: The `browser` field specifies alias files exclusive to browsers. This allows you to create different bundles from your source code based on `typeof window` and `typeof document` checks - thanks to that you can, for example, remove server-only code (just for those bundles). **Note:** Those files are not meant to be consumed by browsers "as is". They just assume browser-like environment, but they still can contain for example references to `process.env.NODE_ENV` as that is meant to be replaced by a consuming bundler. + +### `worker` + +The `worker` field specifies alias files exclusive to DOM-less javascript environments like CloudFlare Workers. This allows you to create different bundles from your source code based on `typeof window` and `typeof document` and `typeof process` checks - thanks to that you can, for example, remove broser-only or Node.js-only code (just for those bundles). + +**Note:** Those files are not meant to be consumed by workers "as is". They just assume worker-like environment, but they still can contain for example references to `process.env.NODE_ENV` as that is meant to be replaced by a consuming bundler.