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

feat: preserve module structure on server #493

Merged
merged 10 commits into from
Feb 17, 2024
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Start a new Waku project with the `create` command for your preferred package ma
npm create waku@latest
```

**Node.js version requirement:** `^20.8.0 || ^18.16.0`
**Node.js version requirement:** `^20.8.0 || ^18.17.0`

## Rendering

Expand Down
20 changes: 0 additions & 20 deletions examples/10_dynamicroute/vite.config.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/waku/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
},
"license": "MIT",
"engines": {
"node": "^20.8.0 || ^18.16.0"
"node": "^20.8.0 || ^18.17.0"
},
"dependencies": {
"@hono/node-server": "1.7.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/waku/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export interface Config {
* Defaults to "entries.js".
*/
entriesJs?: string;
/**
* The list of directries to preserve server module structure.
* Relative to srcDir.
* Defaults to ["pages", "templates", "routes", "components"].
*/
preserveModuleDirs?: string[];
/**
* The serve.js file relative distDir.
* This file is used for deployment.
Expand Down
99 changes: 53 additions & 46 deletions packages/waku/src/lib/builder/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
writeFile,
appendFile,
unlink,
readdir,
} from '../utils/node-fs.js';
import { encodeInput, generatePrefetchCode } from '../renderers/utils.js';
import {
Expand All @@ -55,8 +56,6 @@ import { emitAwsLambdaOutput } from './output-aws-lambda.js';

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

const WAKU_CLIENT = 'waku-client';

// Upstream issue: https://github.com/rollup/rollup/issues/4699
const onwarn = (warning: RollupLog, defaultHandler: LoggingFunction) => {
if (
Expand Down Expand Up @@ -87,9 +86,33 @@ const hash = (fname: string) =>
createReadStream(fname).pipe(sha256);
});

const analyzeEntries = async (entriesFile: string) => {
const clientFileSet = new Set<string>();
const analyzeEntries = async (
rootDir: string,
config: ResolvedConfig,
entriesFile: string,
) => {
const wakuClientDist = decodeFilePathFromAbsolute(
joinPath(fileURLToFilePath(import.meta.url), '../../../client.js'),
);
const clientFileSet = new Set<string>([wakuClientDist]);
const serverFileSet = new Set<string>();
const moduleFileMap = new Map<string, string>(); // module id -> full path
for (const preserveModuleDir of config.preserveModuleDirs) {
const dir = joinPath(rootDir, config.srcDir, preserveModuleDir);
if (!existsSync(dir)) {
continue;
}
const files = await readdir(dir, { encoding: 'utf8', recursive: true });
for (const file of files) {
const ext = extname(file);
if (['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs'].includes(ext)) {
moduleFileMap.set(
joinPath(preserveModuleDir, file.slice(0, -ext.length)),
joinPath(dir, file),
);
}
}
}
await buildVite({
plugins: [rscAnalyzePlugin(clientFileSet, serverFileSet)],
ssr: {
Expand All @@ -106,6 +129,7 @@ const analyzeEntries = async (entriesFile: string) => {
rollupOptions: {
onwarn,
input: {
...Object.fromEntries(moduleFileMap),
entries: entriesFile,
},
},
Expand All @@ -114,17 +138,22 @@ const analyzeEntries = async (entriesFile: string) => {
const clientEntryFiles = Object.fromEntries(
await Promise.all(
Array.from(clientFileSet).map(async (fname, i) => [
`rsc${i}-${await hash(fname)}`,
`${config.assetsDir}/rsc${i}-${await hash(fname)}`,
fname,
]),
),
);
const serverEntryFiles = Object.fromEntries(
Array.from(serverFileSet).map((fname, i) => [`rsf${i}`, fname]),
Array.from(serverFileSet).map((fname, i) => [
`${config.assetsDir}/rsf${i}`,
fname,
]),
);
const serverModuleFiles = Object.fromEntries(moduleFileMap);
return {
clientEntryFiles,
serverEntryFiles,
serverModuleFiles,
};
};

Expand All @@ -136,6 +165,7 @@ const buildServerBundle = async (
distEntriesFile: string,
clientEntryFiles: Record<string, string>,
serverEntryFiles: Record<string, string>,
serverModuleFiles: Record<string, string>,
ssr: boolean,
serve:
| 'vercel'
Expand All @@ -152,11 +182,6 @@ const buildServerBundle = async (
nonjsResolvePlugin(),
rscTransformPlugin({
isBuild: true,
assetsDir: config.assetsDir,
wakuClientId: WAKU_CLIENT,
wakuClientPath: decodeFilePathFromAbsolute(
joinPath(fileURLToFilePath(import.meta.url), '../../../client.js'),
),
clientEntryFiles,
serverEntryFiles,
}),
Expand Down Expand Up @@ -207,30 +232,17 @@ const buildServerBundle = async (
input: {
entries: entriesFile,
[RSDW_SERVER_MODULE]: RSDW_SERVER_MODULE_VALUE,
[WAKU_CLIENT]: CLIENT_MODULE_MAP[WAKU_CLIENT],
...serverModuleFiles,
...clientEntryFiles,
...serverEntryFiles,
},
output: {
entryFileNames: (chunkInfo) => {
if (
[WAKU_CLIENT].includes(chunkInfo.name) ||
clientEntryFiles[chunkInfo.name] ||
serverEntryFiles[chunkInfo.name]
) {
return config.assetsDir + '/[name].js';
}
return '[name].js';
},
},
},
},
});
if (!('output' in serverBuildOutput)) {
throw new Error('Unexpected vite server build output');
}
// TODO If ssr === false, we don't need to write ssr entries.
const ssrAssetsDir = joinPath(config.ssrDir, config.assetsDir);
const code = `
export function loadModule(id) {
switch (id) {
Expand All @@ -240,24 +252,22 @@ ${Object.keys(CLIENT_MODULE_MAP)
.map(
(key) => `
case '${CLIENT_PREFIX}${key}':
return import('./${ssrAssetsDir}/${key}.js');
return import('./${config.ssrDir}/${key}.js');
`,
)
.join('')}
case '${ssrAssetsDir}/${WAKU_CLIENT}.js':
return import('./${ssrAssetsDir}/${WAKU_CLIENT}.js');
${Object.entries(clientEntryFiles || {})
${Object.keys(clientEntryFiles || {})
.map(
([k]) => `
case '${ssrAssetsDir}/${k}.js':
return import('./${ssrAssetsDir}/${k}.js');`,
(key) => `
case '${config.ssrDir}/${key}.js':
return import('./${config.ssrDir}/${key}.js');`,
)
.join('')}
${Object.entries(serverEntryFiles || {})
${Object.keys(serverEntryFiles || {})
.map(
([k]) => `
case '${config.assetsDir}/${k}.js':
return import('./${config.assetsDir}/${k}.js');`,
(key) => `
case '${key}.js':
return import('./${key}.js');`,
)
.join('')}
default:
Expand Down Expand Up @@ -310,8 +320,8 @@ const buildSsrBundle = async (
onwarn,
input: {
main: mainJsFile,
...CLIENT_MODULE_MAP,
...clientEntryFiles,
...CLIENT_MODULE_MAP,
},
output: {
entryFileNames: (chunkInfo) => {
Expand All @@ -321,7 +331,7 @@ const buildSsrBundle = async (
] ||
clientEntryFiles[chunkInfo.name]
) {
return config.assetsDir + '/[name].js';
return '[name].js';
}
return config.assetsDir + '/[name]-[hash].js';
},
Expand Down Expand Up @@ -361,17 +371,13 @@ const buildClientBundle = async (
onwarn,
input: {
main: mainJsFile,
[WAKU_CLIENT]: CLIENT_MODULE_MAP[WAKU_CLIENT],
...clientEntryFiles,
},
preserveEntrySignatures: 'exports-only',
output: {
entryFileNames: (chunkInfo) => {
if (
[WAKU_CLIENT].includes(chunkInfo.name) ||
clientEntryFiles[chunkInfo.name]
) {
return config.assetsDir + '/[name].js';
if (clientEntryFiles[chunkInfo.name]) {
return '[name].js';
}
return config.assetsDir + '/[name]-[hash].js';
},
Expand Down Expand Up @@ -622,15 +628,16 @@ export async function build(options: {
options.deploy !== 'partykit' &&
options.deploy !== 'deno';

const { clientEntryFiles, serverEntryFiles } =
await analyzeEntries(entriesFile);
const { clientEntryFiles, serverEntryFiles, serverModuleFiles } =
await analyzeEntries(rootDir, config, entriesFile);
const serverBuildOutput = await buildServerBundle(
rootDir,
config,
entriesFile,
distEntriesFile,
clientEntryFiles,
serverEntryFiles,
serverModuleFiles,
!!options.ssr,
(options.deploy === 'vercel-serverless' ? 'vercel' : false) ||
(options.deploy === 'netlify-functions' ? 'netlify' : false) ||
Expand Down
1 change: 1 addition & 0 deletions packages/waku/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export async function resolveConfig(config: Config) {
indexHtml: 'index.html',
mainJs: 'main.tsx',
entriesJs: 'entries.js',
preserveModuleDirs: ['pages', 'templates', 'routes', 'components'],
serveJs: 'serve.js',
rscPath: 'RSC',
htmlHead: DEFAULT_HTML_HEAD,
Expand Down
10 changes: 2 additions & 8 deletions packages/waku/src/lib/plugins/vite-plugin-rsc-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ export function rscTransformPlugin(
}
| {
isBuild: true;
assetsDir: string;
wakuClientId: string;
wakuClientPath: string;
clientEntryFiles: Record<string, string>;
serverEntryFiles: Record<string, string>;
},
Expand All @@ -19,12 +16,9 @@ export function rscTransformPlugin(
if (!opts.isBuild) {
throw new Error('not buiding');
}
if (id === opts.wakuClientPath) {
return `@id/${opts.assetsDir}/${opts.wakuClientId}.js`;
}
for (const [k, v] of Object.entries(opts.clientEntryFiles)) {
if (v === id) {
return `@id/${opts.assetsDir}/${k}.js`;
return `@id/${k}.js`;
}
}
throw new Error('client id not found: ' + id);
Expand All @@ -35,7 +29,7 @@ export function rscTransformPlugin(
}
for (const [k, v] of Object.entries(opts.serverEntryFiles)) {
if (v === id) {
return `@id/${opts.assetsDir}/${k}.js`;
return `@id/${k}.js`;
}
}
throw new Error('server id not found: ' + id);
Expand Down
8 changes: 8 additions & 0 deletions packages/waku/src/lib/utils/node-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,11 @@ export const stat = (filePath: string) =>

export const unlink = (filePath: string) =>
fsPromises.unlink(filePathToOsPath(filePath));

export const readdir = (
filePath: string,
options?: {
encoding: 'utf8';
recursive: boolean;
},
) => fsPromises.readdir(filePathToOsPath(filePath), options);
Loading