Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: vercel output #263

Merged
merged 15 commits into from
Dec 14, 2023
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"examples:prd:12_css": "NAME=12_css pnpm run examples:prd",
"website:dev": "(cd packages/website && pnpm run dev)",
"website:build": "(cd packages/website && pnpm run build)",
"website:vercel": "pnpm run website:build && mkdir -p .vercel && cp -Lr packages/website/dist/.vercel/output .vercel/ && cp -r README.md packages/website/src/contents .vercel/output/functions/RSC.func/",
"website:vercel": "pnpm run website:build && mv packages/website/.vercel/output .vercel/ && cp -r README.md packages/website/src/contents .vercel/output/functions/RSC.func/",
"website:prd": "pnpm run website:build && (cd packages/website && pnpm start)"
},
"prettier": {
Expand Down
2 changes: 1 addition & 1 deletion packages/waku/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { serveStatic } from '@hono/node-server/serve-static';
import { resolveConfig } from './lib/config.js';
import { honoMiddleware as honoDevMiddleware } from './lib/middleware/hono-dev.js';
import { honoMiddleware as honoPrdMiddleware } from './lib/middleware/hono-prd.js';
import { build } from './lib/builder.js';
import { build } from './lib/builder/build.js';

const require = createRequire(new URL('.', import.meta.url));

Expand Down
2 changes: 1 addition & 1 deletion packages/waku/src/dev.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { honoMiddleware } from './lib/middleware/hono-dev.js';
export { connectMiddleware } from './lib/middleware/connect-dev.js';
export { createHandler as unstable_createHandler } from './lib/rsc/handler-dev.js';
export { build } from './lib/builder.js';
export { build } from './lib/builder/build.js';
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { build as buildVite, resolveConfig as resolveViteConfig } from 'vite';
import viteReact from '@vitejs/plugin-react';
import type { RollupLog, LoggingFunction } from 'rollup';

import { resolveConfig } from './config.js';
import type { Config, ResolvedConfig } from './config.js';
import { joinPath, extname, filePathToFileURL } from './utils/path.js';
import { resolveConfig } from '../config.js';
import type { Config, ResolvedConfig } from '../config.js';
import { joinPath, extname, filePathToFileURL } from '../utils/path.js';
import {
createReadStream,
createWriteStream,
Expand All @@ -18,15 +18,15 @@ import {
readFile,
writeFile,
appendFile,
} from './utils/node-fs.js';
import { streamToString } from './utils/stream.js';
import { encodeInput, generatePrefetchCode } from './rsc/utils.js';
} from '../utils/node-fs.js';
import { streamToString } from '../utils/stream.js';
import { encodeInput, generatePrefetchCode } from '../rsc/utils.js';
import {
RSDW_SERVER_MODULE,
RSDW_SERVER_MODULE_VALUE,
renderRsc,
getBuildConfig,
} from './rsc/rsc-renderer.js';
} from '../rsc/rsc-renderer.js';
import {
REACT_MODULE,
REACT_MODULE_VALUE,
Expand All @@ -37,12 +37,13 @@ import {
WAKU_CLIENT_MODULE,
WAKU_CLIENT_MODULE_VALUE,
renderHtml,
} from './rsc/html-renderer.js';
import { rscIndexPlugin } from './plugins/vite-plugin-rsc-index.js';
import { rscAnalyzePlugin } from './plugins/vite-plugin-rsc-analyze.js';
import { nonjsResolvePlugin } from './plugins/vite-plugin-nonjs-resolve.js';
import { rscTransformPlugin } from './plugins/vite-plugin-rsc-transform.js';
import { patchReactRefresh } from './plugins/patch-react-refresh.js';
} from '../rsc/html-renderer.js';
import { rscIndexPlugin } from '../plugins/vite-plugin-rsc-index.js';
import { rscAnalyzePlugin } from '../plugins/vite-plugin-rsc-analyze.js';
import { nonjsResolvePlugin } from '../plugins/vite-plugin-nonjs-resolve.js';
import { rscTransformPlugin } from '../plugins/vite-plugin-rsc-transform.js';
import { patchReactRefresh } from '../plugins/patch-react-refresh.js';
import { emitVercelOutput } from './output-vercel.js';

// TODO this file and functions in it are too long. will fix.

Expand Down Expand Up @@ -467,118 +468,6 @@ export function loadHtml(pathStr) {
return { htmlFiles };
};

const emitVercelOutput = async (
rootDir: string,
config: ResolvedConfig,
clientBuildOutput: Awaited<ReturnType<typeof buildClientBundle>>,
rscFiles: string[],
htmlFiles: string[],
ssr: boolean,
) => {
// FIXME somehow utils/(path,node-fs).ts doesn't work
const [
path,
{ existsSync, mkdirSync, readdirSync, symlinkSync, writeFileSync },
] = await Promise.all([import('node:path'), import('node:fs')]);
const clientFiles = clientBuildOutput.output.map(({ fileName }) =>
path.join(rootDir, config.distDir, config.publicDir, fileName),
);
const srcDir = path.join(rootDir, config.distDir, config.publicDir);
const dstDir = path.join(rootDir, config.distDir, '.vercel', 'output');
for (const file of [...clientFiles, ...rscFiles, ...htmlFiles]) {
const dstFile = path.join(dstDir, 'static', path.relative(srcDir, file));
if (!existsSync(dstFile)) {
mkdirSync(path.dirname(dstFile), { recursive: true });
symlinkSync(path.relative(path.dirname(dstFile), file), dstFile);
}
}

// for serverless function
const serverlessDir = path.join(
dstDir,
'functions',
config.rscPath + '.func',
);
mkdirSync(path.join(serverlessDir, config.distDir), {
recursive: true,
});
mkdirSync(path.join(serverlessDir, 'node_modules'));
symlinkSync(
path.relative(
path.join(serverlessDir, 'node_modules'),
path.join(rootDir, 'node_modules', 'waku'),
),
path.join(serverlessDir, 'node_modules', 'waku'),
);
for (const file of readdirSync(path.join(rootDir, config.distDir))) {
if (['.vercel'].includes(file)) {
continue;
}
symlinkSync(
path.relative(
path.join(serverlessDir, config.distDir),
path.join(rootDir, config.distDir, file),
),
path.join(serverlessDir, config.distDir, file),
);
}
const vcConfigJson = {
runtime: 'nodejs18.x',
handler: 'serve.js',
launcherType: 'Nodejs',
};
writeFileSync(
path.join(serverlessDir, '.vc-config.json'),
JSON.stringify(vcConfigJson, null, 2),
);
writeFileSync(
path.join(serverlessDir, 'package.json'),
JSON.stringify({ type: 'module' }, null, 2),
);
writeFileSync(
path.join(serverlessDir, 'serve.js'),
`
import path from 'node:path';
import { connectMiddleware } from 'waku';
const entries = import(path.resolve('${config.distDir}', '${config.entriesJs}'));
export default async function handler(req, res) {
connectMiddleware({ entries, ssr: ${ssr} })(req, res, () => {
res.statusCode = 404;
res.end();
});
}
`,
);

const overrides = Object.fromEntries(
rscFiles
.filter((file) => !path.extname(file))
.map((file) => [
path.relative(srcDir, file),
{ contentType: 'text/plain' },
]),
);
const basePrefix = config.basePath + config.rscPath + '/';
const routes = [
{ src: basePrefix + '(.*)', dest: basePrefix },
...(ssr
? htmlFiles.map((htmlFile) => {
const file = config.basePath + path.relative(srcDir, htmlFile);
const src = file.endsWith('/' + config.indexHtml)
? file.slice(0, -('/' + config.indexHtml).length) || '/'
: file;
return { src, dest: basePrefix };
})
: []),
];
const configJson = { version: 3, overrides, routes };
mkdirSync(dstDir, { recursive: true });
writeFileSync(
path.join(dstDir, 'config.json'),
JSON.stringify(configJson, null, 2),
);
};

const resolveFileName = (fname: string) => {
for (const ext of ['.js', '.ts', '.tsx', '.jsx']) {
const resolvedName = fname.slice(0, -extname(fname).length) + ext;
Expand All @@ -589,7 +478,11 @@ const resolveFileName = (fname: string) => {
return fname; // returning the default one
};

export async function build(options: { config?: Config; ssr?: boolean }) {
export async function build(options: {
config?: Config;
ssr?: boolean;
vercel?: boolean;
}) {
const config = await resolveConfig(options.config || {});
const rootDir = (
await resolveViteConfig({}, 'build', 'production', 'production')
Expand All @@ -612,7 +505,7 @@ export async function build(options: { config?: Config; ssr?: boolean }) {
clientEntryFiles,
serverEntryFiles,
);
const clientBuildOutput = await buildClientBundle(
await buildClientBundle(
rootDir,
config,
commonEntryFiles,
Expand All @@ -634,13 +527,13 @@ export async function build(options: { config?: Config; ssr?: boolean }) {
!!options?.ssr,
);

// https://vercel.com/docs/build-output-api/v3
await emitVercelOutput(
rootDir,
config,
clientBuildOutput,
rscFiles,
htmlFiles,
!!options?.ssr,
);
if (options?.vercel ?? process.env.VERCEL) {
await emitVercelOutput(
rootDir,
config,
rscFiles,
htmlFiles,
!!options?.ssr,
);
}
}
99 changes: 99 additions & 0 deletions packages/waku/src/lib/builder/output-vercel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import path from 'node:path';
import { cpSync, mkdirSync, writeFileSync } from 'node:fs';

import type { ResolvedConfig } from '../config.js';

// https://vercel.com/docs/build-output-api/v3
export const emitVercelOutput = async (
rootDir: string,
config: ResolvedConfig,
rscFiles: string[],
htmlFiles: string[],
ssr: boolean,
) => {
const publicDir = path.join(rootDir, config.distDir, config.publicDir);
const outputDir = path.resolve('.vercel', 'output');
cpSync(
path.join(rootDir, config.distDir, config.publicDir),
path.join(outputDir, 'static'),
{ recursive: true },
);

// for serverless function
const serverlessDir = path.join(
outputDir,
'functions',
config.rscPath + '.func',
);
mkdirSync(path.join(serverlessDir, config.distDir), {
recursive: true,
});
mkdirSync(path.join(serverlessDir, 'node_modules'), {
recursive: true,
});
cpSync(
path.join(rootDir, 'node_modules', 'waku'),
path.join(serverlessDir, 'node_modules', 'waku'),
{ dereference: true, recursive: true },
);
cpSync(
path.join(rootDir, config.distDir),
path.join(serverlessDir, config.distDir),
{ recursive: true },
);
const vcConfigJson = {
runtime: 'nodejs18.x',
handler: 'serve.js',
launcherType: 'Nodejs',
};
writeFileSync(
path.join(serverlessDir, '.vc-config.json'),
JSON.stringify(vcConfigJson, null, 2),
);
writeFileSync(
path.join(serverlessDir, 'package.json'),
JSON.stringify({ type: 'module' }, null, 2),
);
writeFileSync(
path.join(serverlessDir, 'serve.js'),
`
import path from 'node:path';
import { connectMiddleware } from 'waku';
const entries = import(path.resolve('${config.distDir}', '${config.entriesJs}'));
export default async function handler(req, res) {
connectMiddleware({ entries, ssr: ${ssr} })(req, res, () => {
res.statusCode = 404;
res.end();
});
}
`,
);

const overrides = Object.fromEntries(
rscFiles
.filter((file) => !path.extname(file))
.map((file) => [
path.relative(publicDir, file),
{ contentType: 'text/plain' },
]),
);
const basePrefix = config.basePath + config.rscPath + '/';
const routes = [
{ src: basePrefix + '(.*)', dest: basePrefix },
...(ssr
? htmlFiles.map((htmlFile) => {
const file = config.basePath + path.relative(publicDir, htmlFile);
const src = file.endsWith('/' + config.indexHtml)
? file.slice(0, -('/' + config.indexHtml).length) || '/'
: file;
return { src, dest: basePrefix };
})
: []),
];
const configJson = { version: 3, overrides, routes };
mkdirSync(outputDir, { recursive: true });
writeFileSync(
path.join(outputDir, 'config.json'),
JSON.stringify(configJson, null, 2),
);
};
Loading