Skip to content

Commit

Permalink
Add support for vite preview (#68)
Browse files Browse the repository at this point in the history
* Initial preview support

* Supports multiple workers and orders the workers array

* Resolved rollupOptions.input paths

* Added build tests

* Added support for assets in preview
  • Loading branch information
jamesopstad authored Nov 21, 2024
1 parent 7d9fe3e commit b022900
Show file tree
Hide file tree
Showing 34 changed files with 388 additions and 190 deletions.
2 changes: 1 addition & 1 deletion packages/vite-plugin-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"dependencies": {
"@hattip/adapter-node": "^0.0.48",
"cjs-module-lexer": "^1.4.1",
"miniflare": "^3.20241004.0"
"miniflare": "^3.20241106.0"
},
"devDependencies": {
"@cloudflare/workers-shared": "^0.7.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { builtinModules } from 'node:module';
import * as path from 'node:path';
import * as vite from 'vite';
import { getNodeCompatExternals } from './node-js-compat';
import { INIT_PATH, invariant, UNKNOWN_HOST } from './shared';
Expand Down Expand Up @@ -123,6 +124,7 @@ const cloudflareBuiltInModules = [

export function createCloudflareEnvironmentOptions(
options: WorkerOptions,
userConfig: vite.UserConfig,
): vite.EnvironmentOptions {
return vite.mergeConfig(
{
Expand All @@ -142,13 +144,17 @@ export function createCloudflareEnvironmentOptions(
createEnvironment(name, config) {
return new vite.BuildEnvironment(name, config);
},
// This is a bit of a hack to make sure the user can't override the output directory at the environment level
outDir: userConfig.build?.outDir ?? 'dist',
ssr: true,
rollupOptions: {
// Note: vite starts dev pre-bundling crawling from either optimizeDeps.entries or rollupOptions.input
// so the input value here serves both as the build input as well as the starting point for
// dev pre-bundling crawling (were we not to set this input field we'd have to appropriately set
// optimizeDeps.entries in the dev config)
input: options.main,
input: userConfig.root
? path.resolve(userConfig.root, options.main)
: path.resolve(options.main),
external: [...cloudflareBuiltInModules, ...getNodeCompatExternals()],
},
},
Expand Down
78 changes: 64 additions & 14 deletions packages/vite-plugin-cloudflare/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as fs from 'node:fs';
import path from 'node:path';
import { createMiddleware } from '@hattip/adapter-node';
import { Miniflare } from 'miniflare';
Expand All @@ -7,7 +8,10 @@ import {
createCloudflareEnvironmentOptions,
initRunners,
} from './cloudflare-environment';
import { getMiniflareOptions } from './miniflare-options';
import {
getDevMiniflareOptions,
getPreviewMiniflareOptions,
} from './miniflare-options';
import {
getNodeCompatAliases,
injectGlobalCode,
Expand All @@ -30,25 +34,41 @@ export function cloudflare<T extends Record<string, WorkerOptions>>(

return {
name: 'vite-plugin-cloudflare',
config() {
config(userConfig) {
return {
resolve: {
alias: getNodeCompatAliases(),
},
appType: 'custom',
builder: {
async buildApp(builder) {
const environments = Object.keys(pluginConfig.workers ?? {}).map(
(name) => {
const environment = builder.environments[name];
invariant(environment, `${name} environment not found`);

return environment;
},
const client = builder.environments.client;
const defaultHtmlPath = path.resolve(
builder.config.root,
'index.html',
);

if (
client &&
(client.config.build.rollupOptions.input ||
fs.existsSync(defaultHtmlPath))
) {
await builder.build(client);
}

const workerEnvironments = Object.keys(
pluginConfig.workers ?? {},
).map((name) => {
const environment = builder.environments[name];
invariant(environment, `${name} environment not found`);

return environment;
});

await Promise.all(
environments.map((environment) => builder.build(environment)),
workerEnvironments.map((environment) =>
builder.build(environment),
),
);
},
},
Expand All @@ -57,16 +77,17 @@ export function cloudflare<T extends Record<string, WorkerOptions>>(
Object.entries(pluginConfig.workers ?? {}).map(
([name, workerOptions]) => [
name,
createCloudflareEnvironmentOptions(workerOptions),
createCloudflareEnvironmentOptions(workerOptions, userConfig),
],
),
),
};
},
configEnvironment(name, options) {
options.build = {
outDir: path.join('dist', name),
...options.build,
// Puts all environment builds in subdirectories of the same build directory
outDir: path.join(options.build?.outDir ?? 'dist', name),
};
},
configResolved(resolvedConfig) {
Expand Down Expand Up @@ -102,7 +123,11 @@ export function cloudflare<T extends Record<string, WorkerOptions>>(
let error: unknown;

const miniflare = new Miniflare(
getMiniflareOptions(normalizedPluginConfig, viteConfig, viteDevServer),
getDevMiniflareOptions(
normalizedPluginConfig,
viteConfig,
viteDevServer,
),
);

await initRunners(normalizedPluginConfig, miniflare, viteDevServer);
Expand All @@ -114,7 +139,7 @@ export function cloudflare<T extends Record<string, WorkerOptions>>(

try {
await miniflare.setOptions(
getMiniflareOptions(
getDevMiniflareOptions(
normalizedPluginConfig,
viteConfig,
viteDevServer,
Expand Down Expand Up @@ -150,6 +175,31 @@ export function cloudflare<T extends Record<string, WorkerOptions>>(
return;
}

middleware(req, res, next);
});
};
},
configurePreviewServer(vitePreviewServer) {
const miniflare = new Miniflare(
getPreviewMiniflareOptions(normalizedPluginConfig, viteConfig),
);

const middleware = createMiddleware(
({ request }) => {
return miniflare.dispatchFetch(toMiniflareRequest(request), {
redirect: 'manual',
}) as any;
},
{ alwaysCallNext: false },
);

return () => {
vitePreviewServer.middlewares.use((req, res, next) => {
if (!middleware) {
next();
return;
}

middleware(req, res, next);
});
};
Expand Down
119 changes: 111 additions & 8 deletions packages/vite-plugin-cloudflare/src/miniflare-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ const ASSET_WORKER_PATH = './assets/asset-worker.js';
const WRAPPER_PATH = '__VITE_WORKER_ENTRY__';
const RUNNER_PATH = './runner/index.js';

export function getMiniflareOptions(
export function getDevMiniflareOptions(
normalizedPluginConfig: NormalizedPluginConfig,
viteConfig: vite.ResolvedConfig,
viteDevServer: vite.ViteDevServer,
Expand Down Expand Up @@ -216,21 +216,19 @@ export function getMiniflareOptions(

const userWorkers = Object.values(normalizedPluginConfig.workers).map(
(worker) => {
const { ratelimits, ...workerOptions } = worker.workerOptions;

return {
...workerOptions,
name: worker.name,
...worker.workerOptions,
modulesRoot: miniflareModulesRoot,
unsafeEvalBinding: '__VITE_UNSAFE_EVAL__',
bindings: {
...workerOptions.bindings,
...worker.workerOptions.bindings,
__VITE_ROOT__: viteConfig.root,
__VITE_ENTRY_PATH__: worker.entryPath,
},
serviceBindings: {
...workerOptions.serviceBindings,
...(worker.assetsBinding
...worker.workerOptions.serviceBindings,
...(worker.workerOptions.name ===
normalizedPluginConfig.entryWorkerName && worker.assetsBinding
? { [worker.assetsBinding]: ASSET_WORKER_NAME }
: {}),
},
Expand Down Expand Up @@ -361,6 +359,111 @@ export function getMiniflareOptions(
};
}

export function getPreviewMiniflareOptions(
normalizedPluginConfig: NormalizedPluginConfig,
viteConfig: vite.ResolvedConfig,
): MiniflareOptions {
const entryWorkerConfig = normalizedPluginConfig.entryWorkerName
? normalizedPluginConfig.workers[normalizedPluginConfig.entryWorkerName]
: undefined;

const assetsDirectory = path.resolve(
viteConfig.root,
viteConfig.build.outDir,
'client',
);
const hasAssets = fs.existsSync(assetsDirectory);
const assetsOptions = hasAssets
? {
assets: {
routingConfig: {
has_user_worker: entryWorkerConfig ? true : false,
},
assetConfig: {
...(normalizedPluginConfig.assets.htmlHandling
? { html_handling: normalizedPluginConfig.assets.htmlHandling }
: {}),
...(normalizedPluginConfig.assets.notFoundHandling
? {
not_found_handling:
normalizedPluginConfig.assets.notFoundHandling,
}
: {}),
},
directory: assetsDirectory,
...(entryWorkerConfig?.assetsBinding
? { binding: entryWorkerConfig.assetsBinding }
: {}),
},
}
: {};

const workers: Array<WorkerOptions> = [
...(entryWorkerConfig
? [
{
...entryWorkerConfig.workerOptions,
...assetsOptions,
modules: [
{
type: 'ESModule',
path: path.resolve(
viteConfig.root,
viteConfig.build.outDir,
entryWorkerConfig.workerOptions.name,
'index.js',
),
} as const,
],
},
]
: [
{
...assetsOptions,
name: 'assets-only',
script: '',
modules: true,
},
]),
...Object.values(normalizedPluginConfig.workers)
.filter(
(config) =>
config.workerOptions.name !== normalizedPluginConfig.entryWorkerName,
)
.map((config) => {
return {
...config.workerOptions,
modules: [
{
type: 'ESModule',
path: path.resolve(
viteConfig.root,
viteConfig.build.outDir,
config.workerOptions.name,
'index.js',
),
} as const,
],
};
}),
];

const logger = new ViteMiniflareLogger(viteConfig);

return {
log: logger,
handleRuntimeStdio(stdout, stderr) {
const decoder = new TextDecoder();
stdout.forEach((data) => logger.info(decoder.decode(data)));
stderr.forEach((error) =>
logger.logWithLevel(LogLevel.ERROR, decoder.decode(error)),
);
},
...getPersistence(normalizedPluginConfig.persistPath),
workers,
};
}

/**
* A Miniflare logger that forwards messages onto a Vite logger.
*/
Expand Down
11 changes: 6 additions & 5 deletions packages/vite-plugin-cloudflare/src/plugin-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,10 @@ export interface NormalizedPluginConfig {
workers: Record<
string,
{
name: string;
entryPath: string;
wranglerConfigPath: string;
assetsBinding?: string;
workerOptions: SourcelessWorkerOptions;
workerOptions: SourcelessWorkerOptions & { name: string };
}
>;
entryWorkerName?: string;
Expand Down Expand Up @@ -64,17 +63,19 @@ export function normalizePluginConfig(

wranglerConfigPaths.add(wranglerConfigPath);

const { workerOptions } =
const miniflareWorkerOptions =
unstable_getMiniflareWorkerOptions(wranglerConfigPath);

const { ratelimits, ...workerOptions } =
miniflareWorkerOptions.workerOptions;

return [
name,
{
name,
entryPath: options.main,
wranglerConfigPath,
assetsBinding: options.assetsBinding,
workerOptions,
workerOptions: { ...workerOptions, name },
},
];
}),
Expand Down
4 changes: 2 additions & 2 deletions playground/durable-objects/__tests__/durable-objects.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, test } from 'vitest';
import { getTextResponse, isBuild } from '../../__test-utils__';
import { getTextResponse } from '../../__test-utils__';

describe.runIf(!isBuild)('in-worker defined durable objects', async () => {
describe('in-worker defined durable objects', async () => {
test('can bind and use a Durable Object defined in the worker', async () => {
expect(await getTextResponse('/?name=my-do')).toEqual(
"Durable Object 'my-do' count: 0",
Expand Down
3 changes: 2 additions & 1 deletion playground/durable-objects/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"scripts": {
"build": "vite build --app",
"check:types": "tsc --build",
"dev": "vite dev"
"dev": "vite dev",
"preview": "vite preview"
},
"devDependencies": {
"@cloudflare/workers-types": "catalog:default",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, test } from 'vitest';
import { getTextResponse, isBuild } from '../../__test-utils__';
import { getTextResponse } from '../../__test-utils__';

describe.runIf(!isBuild)('external durable objects', async () => {
describe('external durable objects', async () => {
test('can use `scriptName` to bind to a Durable Object defined in another Worker', async () => {
expect(await getTextResponse('/?name=my-do')).toEqual(
'From worker-a: {"name":"my-do","count":0}',
Expand Down
Loading

0 comments on commit b022900

Please sign in to comment.