7dece0e 時点
- vite dev 起動時のシーケンスを確認
- react-router になるとはどういうことか
DEVELOPMENT.md
$ pnpm install
$ pnpm build
$ pnpm test:primary
...
Test Suites: 31 passed, 31 total
Tests: 1 skipped, 562 passed, 563 total
Snapshots: 86 passed, 86 total
Time: 45.22 s
it works
REMIX_DEBUG
By default, the Remix rollup
build will strip any console.debug
calls to avoid cluttering up the console during application usage. These console.debug
statements can be preserved by setting REMIX_DEBUG=true
during your local build.
REMIX_DEBUG=true pnpm watch
開発サーバー起動時の CLI のエントリポイントを探す
// packages/remix/index.ts
// This class exists to prevent https://github.com/remix-run/remix/issues/2031 from occurring
export class RemixPackageNotUsedError extends Error {
constructor() {
super(
"The `remix` package is no longer used for Remix modules and should be removed " +
"from your project dependencies. See " +
"https://github.com/remix-run/remix/releases/tag/remix%402.0.0" +
" for more information."
);
}
}
throw new RemixPackageNotUsedError();
違う。
packages/create-remix を覗いたが、これはセットアップ用のCLIヘルパがほとんど
ここで一旦 create-remix を叩いてみて、 bin がどこに刺さってるかを確認する。
npx create-remix
して node_modules/.bin/remix
の向いている先を確認すると、 @remix-run/dev
であることを確認。
packages/remix-dev/cli/index.ts
=>./run.ts
// packages/remix-dev/cli/run.ts L282
case "vite:dev":
await commands.viteDev(input[1], flags);
break;
// packages/remix-dev/cli/commands.ts L212
export async function viteDev(root: string, options: ViteDevOptions = {}) {
let { dev } = await import("../vite/dev");
if (options.profile) {
await profiler.start();
}
exitHook(() => profiler.stop(console.info));
await dev(root, options);
// keep `remix vite-dev` alive by waiting indefinitely
await new Promise(() => {});
}
// packages/remix-dev/vite/dev.ts
export async function dev(/*...*/) {
// Ensure Vite's ESM build is preloaded at the start of the process
// so it can be accessed synchronously via `importViteEsmSync`
await preloadViteEsm();
let vite = await import("vite");
let server = await vite.createServer({
root,
mode,
configFile,
server: { open, cors, host, port, strictPort },
optimizeDeps: { force },
clearScreen,
logLevel,
});
要は vite.createServer
してるだけ。これは vite サーバーをCLIではなく Node から起動するためのオプション。
おそらく server 周りに開発用のヘルパかリローダーが注入されてる可能性があるが、一旦飛ばす。
プラグインを読み解く前に、事前知識として、 vite で SSR するサーバーアセットを吐くための事前知識が必要そう。(自分は知ってた)
https://vitejs.dev/guide/ssr#ssr-specific-plugin-logic
export function mySSRPlugin() {
return {
name: 'my-ssr',
transform(code, id, options) {
if (options?.ssr) {
// perform ssr-specific transform...
}
},
}
}
というわけで、コードを読みに行く。 スキャッフォルドした vite.config.ts を確認。
// $ cat vite.config.ts
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
},
}),
tsconfigPaths(),
],
});
package.json
"main": "dist/index.js",
"typings": "dist/index.d.ts",
rollup.config.js を確認すると、これは packages/remix-dev/index.ts
だった。
import "./modules";
//...
export { vitePlugin, cloudflareDevProxyVitePlugin } from "./vite";
// packages/remix-dev/vite/index.ts
export const vitePlugin: RemixVitePlugin = (...args) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
let { remixVitePlugin } = require("./plugin") as typeof import("./plugin");
return remixVitePlugin(...args);
};
// packages/remix-dev/vite/plugin.ts L604
export type RemixVitePlugin = (config?: VitePluginConfig) => Vite.Plugin[];
export const remixVitePlugin: RemixVitePlugin = (remixUserConfig = {}) => {
// Prevent mutations to the user config
remixUserConfig = deepFreeze(remixUserConfig);
ここで Vite Plugin を生成している。ここに SSR 分岐とクライアント分岐もあるはず。 雑に上から読んだだけだと、難しい!
// L619
let ctx: RemixPluginContext;
/** Mutates `ctx` as a side-effect */
let updateRemixPluginContext = async (): Promise<void> => {
// ...リロード時に config の変更がないか確認して適応する処理
ctx = {
remixConfig,
rootDirectory,
entryClientFilePath,
entryServerFilePath,
viteManifestEnabled,
...ssrBuildCtx,
};
}
読み下して、おそらくここに実際のビルド時の処理が注入されている。
// L853
let generateRemixManifestsForBuild = async (): Promise<{
remixBrowserManifest: RemixManifest;
remixServerManifest: RemixManifest;
}> => {
// L872
let routeManifestExports = await getRouteManifestModuleExports(
viteChildCompiler,
ctx
);
// ここが多分重要な部分
// ファイルごとに静的解析して、その export で必要な処理のフラグを立てているように見える
// あとで重点的に読む
for (let [key, route] of Object.entries(ctx.remixConfig.routes)) {
let routeFilePath = path.join(ctx.remixConfig.appDirectory, route.file);
let sourceExports = routeManifestExports[key];
let isRootRoute = route.parentId === undefined;
let routeManifestEntry = {
id: route.id,
parentId: route.parentId,
path: route.path,
index: route.index,
caseSensitive: route.caseSensitive,
hasAction: sourceExports.includes("action"),
hasLoader: sourceExports.includes("loader"),
hasClientAction: sourceExports.includes("clientAction"),
hasClientLoader: sourceExports.includes("clientLoader"),
hasErrorBoundary: sourceExports.includes("ErrorBoundary"),
...getRemixManifestBuildAssets(
ctx,
viteManifest,
routeFilePath,
// If this is the root route, we also need to include assets from the
// client entry file as this is a common way for consumers to import
// global reset styles, etc.
isRootRoute ? [ctx.entryClientFilePath] : []
),
};
browserRoutes[key] = routeManifestEntry;
let serverBundleRoutes = ctx.serverBundleBuildConfig?.routes;
if (!serverBundleRoutes || serverBundleRoutes[key]) {
serverRoutes[key] = routeManifestEntry;
}
}
// L926
// Write the browser manifest to disk as part of the build process
await writeFileSafe(
path.join(getClientBuildDirectory(ctx.remixConfig), manifestPath),
`window.__remixManifest=${JSON.stringify(remixBrowserManifest)};`
);
// ...
return {
remixBrowserManifest,
remixServerManifest,
};
}
この読み飛ばした中身の routeManifestExports
が多分大事
TODO: config に明示的にレイアウトを指定するということだが、これ新しい layout 用じゃないか?あとで確認する
// L946
// In dev, the server and browser Remix manifests are the same
let getRemixManifestForDev = async (): Promise<RemixManifest> => {
1002行から Vite Plugin の配列を返す。これは注入された順に実行される。
Vite Plugin は Rollup Plugin の拡張インターフェースで、次のルールに従うことを念頭に置く。
https://rollupjs.org/plugin-development/
とりあえず最初の remix 本体?っぽい plugin を読む。
// L1002
return [
{
name: "remix",
config: async (_viteUserConfig, _viteConfigEnv) => {
// 本体プラグイン
return {
__remixPluginContext: ctx,
// ...
// ビルド時の設定
...(viteCommand === "build"
? {
build: {
cssMinify: viteUserConfig.build?.cssMinify ?? true,
...(!viteConfigEnv.isSsrBuild
? {
// ブラウザ側のビルド設定
manifest: true,
outDir: getClientBuildDirectory(ctx.remixConfig),
rollupOptions: {
...baseRollupOptions,
preserveEntrySignatures: "exports-only",
input: [
ctx.entryClientFilePath,
...Object.values(ctx.remixConfig.routes).map(
(route) =>
`${path.resolve(
ctx.remixConfig.appDirectory,
route.file
)}${BUILD_CLIENT_ROUTE_QUERY_STRING}`
),
],
},
}
: {
// サーバー側のビルド設定
// We move SSR-only assets to client assets. Note that the
// SSR build can also emit code-split JS files (e.g. by
// dynamic import) under the same assets directory
// regardless of "ssrEmitAssets" option, so we also need to
// keep these JS files have to be kept as-is.
ssrEmitAssets: true,
copyPublicDir: false, // Assets in the public directory are only used by the client
manifest: true, // We need the manifest to detect SSR-only assets
outDir: getServerBuildDirectory(ctx),
rollupOptions: {
...baseRollupOptions,
preserveEntrySignatures: "exports-only",
input: serverBuildId,
output: {
entryFileNames: ctx.remixConfig.serverBuildFile,
format: ctx.remixConfig.serverModuleFormat,
},
},
}),
},
}
: undefined),
// ...
};
},
/// vite の開発サーバー上の振る舞い動き。ここで開発時の SSR 実装が行われる
async configureServer(viteDevServer) {
// ...
return () => {
// SSR した 初期HTMLをミドルウェアとして返却するサーバーの処理
if (!viteDevServer.config.server.middlewareMode) {
viteDevServer.middlewares.use(async (req, res, next) => {
// SSR 用のモジュールチャンクを取得し、たぶん HTML に展開
// それを node.js 用のリクエストに変換
try {
let build = (await viteDevServer.ssrLoadModule(
serverBuildId
)) as ServerBuild;
let handler = createRequestHandler(build, "development");
let nodeHandler: NodeRequestHandler = async (
nodeReq,
nodeRes
) => {
let req = fromNodeRequest(nodeReq);
let res = await handler(req, await remixDevLoadContext(req));
await toNodeRequest(res, nodeRes);
};
await nodeHandler(req, res);
} catch (error) {
next(error);
}
});
}
};
},
// アセットを書き出す設定
writeBundle: {
// After the SSR build is finished, we inspect the Vite manifest for
// the SSR build and move server-only assets to client assets directory
// ...
},
async buildEnd() {
await viteChildCompiler?.close();
},
},
大事なのは configureServer
の vite.ssrLoadModule
で https://ja.vitejs.dev/guide/ssr の SSR 実装サンプルのここに相当する。(のを知っていた)
// 2. Vite の HTML の変換を適用します。これにより Vite の HMR クライアントが定義され
// Vite プラグインからの HTML 変換も適用します。 e.g. global preambles
// from @vitejs/plugin-react
template = await vite.transformIndexHtml(url, template)
// 3. サーバーサイドのエントリーポイントを読み込みます。 ssrLoadModule は自動的に
// ESM を Node.js で使用できるコードに変換します! ここではバンドルは必要ありません
// さらに HMR と同様な効率的な無効化を提供します。
const { render } = await vite.ssrLoadModule('/src/entry-server.js')
ここでクライアント・サーバーで結果整合な感じで頑張ってアセットを吐き出している。
続きを見る。
{
name: "remix-virtual-modules",
// vite plugin で他のものより優先する設定
enforce: "pre",
// rollup で id からソースコード実体をロードするフック
async load(id) {
switch (id) {
// ...
// Remix アプリケーション用の動的データをここで作って返している
case VirtualModule.resolve(serverManifestId): {
let remixManifest = ctx.isSsrBuild
? await ctx.getRemixServerManifest()
: await getRemixManifestForDev();
return `export default ${jsesc(remixManifest, { es6: true })};`;
}
// マニフェストファイルをここでグローバル変数に注入アセットとして返す
case VirtualModule.resolve(browserManifestId): {
// ...
return `window.__remixManifest=${remixManifestString};`;
}
}
},
},
{
// `.server.tsx` の解決
name: "remix-dot-server",
enforce: "pre",
async resolveId(id, importer, options) {
//...
},
},
{
// `.client.tsx` の解決
name: "remix-dot-client",
async transform(code, id, options) {
// ...
}
},
{
/// routes/* ごとの処理
name: "remix-route-exports",
async transform(code, id, options) {
if (options?.ssr) return;
let route = getRoute(ctx.remixConfig, id);
if (!route) return;
/// ...
/// 規約に無関係な export を削除
return removeExports(code, SERVER_ONLY_ROUTE_EXPORTS, {
sourceMaps: true,
filename: id,
sourceFileName: filepath,
});
},
},
{
// HMR用のフックの挿入
name: "remix-inject-hmr-runtime",
enforce: "pre",
// ...
async load(id) {
//...
return [
`import RefreshRuntime from "${hmrRuntimeId}"`,
"RefreshRuntime.injectIntoGlobalHook(window)",
"window.$RefreshReg$ = () => {}",
"window.$RefreshSig$ = () => (type) => type",
"window.__vite_plugin_react_preamble_installed__ = true",
].join("\n");
},
},
{
/// HMR のランタイムホストの注入
name: "remix-hmr-runtime",
/// ...
async load(id) {
/// ...
return [
"const exports = {}",
await fse.readFile(reactRefreshRuntimePath, "utf8"),
await fse.readFile(
require.resolve("./static/refresh-utils.cjs"),
"utf8"
),
"export default exports",
].join("\n");
},
},
{
/// babel のHMR用の変形
name: "remix-react-refresh-babel",
async transform(code, id, options) {
/// ...
},
},
{
/// HMR 受付時のフック
name: "remix-hmr-updates",
async handleHotUpdate({ server, file, modules, read }) {
/// ...色々コードを返す
/// 開発サーバーの websocket へHMRデータを送信
server.ws.send({
type: "custom",
event: "remix:hmr",
data: hmrEventData,
});
return modules;
},
},
];