Skip to content

Commit

Permalink
refactor(viteroll): use environment api (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa authored Nov 8, 2024
1 parent e12111d commit f950271
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 86 deletions.
20 changes: 1 addition & 19 deletions viteroll/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Integrating [Rolldown HMR](https://github.com/rolldown/rolldown/tree/hmr-poc) as

```sh
pnpm dev
pnpm -C examples/ssr dev
pnpm -C examples/react dev
pnpm -C examples/mpa dev
```
Expand All @@ -12,22 +13,3 @@ pnpm -C examples/mpa dev

- https://github.com/users/hi-ogawa/projects/4/views/1?pane=issue&itemId=84997064
- https://github.com/hi-ogawa/rolldown/tree/feat-vite-like

## todo

- [x] serve Rolldown build output via middleware
- [x] reuse Vite dev server for file watcher and websocket server/client
- [x] tweak runtime to initiate full build from client
- [x] basic index.html support
- [x] react spa example
- [ ] css
- [ ] assets
- [ ] ssr

## tbd

- module graph api e.g. invalidation
- runtime api (missing hot api)
- module runner usage
- how to deal with non-static import which is out of import analysis?
- they are not necessary "external" for vite dev as it can go through tranform pipeline as requested.
8 changes: 6 additions & 2 deletions viteroll/examples/ssr/e2e/basic.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test } from "@playwright/test";
import { expect, test } from "@playwright/test";
import { createEditor, testNoJs } from "../../../e2e/helper";

testNoJs("ssr", async ({ page }) => {
Expand All @@ -11,7 +11,7 @@ test("csr", async ({ page }) => {
await page.getByText("hydrated: true").click();
});

test("hmr", async ({ page }) => {
test("hmr", async ({ page, request }) => {
await page.goto("/");
await page.getByText("hydrated: true").click();

Expand All @@ -24,4 +24,8 @@ test("hmr", async ({ page }) => {

file.edit((s) => s.replace("Count-EDIT:", "Count-EDIT-EDIT:"));
await page.getByRole("button", { name: "Count-EDIT-EDIT: 2" }).click();

// server module is also invalidated
const res = await request.get("/");
expect(await res.text()).toContain("Count-EDIT-EDIT");
});
13 changes: 6 additions & 7 deletions viteroll/examples/ssr/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import path from "node:path";
import { defineConfig } from "vite";
import { viteroll } from "../../viteroll";
import { RolldownEnvironment, viteroll } from "../../viteroll";

export default defineConfig({
environments: {
Expand All @@ -16,7 +15,9 @@ export default defineConfig({
build: {
outDir: "dist/server",
rollupOptions: {
input: "./src/entry-server",
input: {
index: "./src/entry-server",
},
},
},
},
Expand All @@ -34,12 +35,10 @@ export default defineConfig({
},
configureServer(server) {
return () => {
const devEnv = server.environments.ssr as RolldownEnvironment;
server.middlewares.use(async (req, res, next) => {
try {
// TODO: move to environment api
const entry = path.resolve("dist/server/entry-server.js");
// TODO: invalidate on build update
const mod = await import(entry + "?t=" + Date.now());
const mod = (await devEnv.import("index")) as any;
await mod.default(req, res);
} catch (e) {
next(e);
Expand Down
1 change: 1 addition & 0 deletions viteroll/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitOverride": true,
"verbatimModuleSyntax": true,
"moduleResolution": "Bundler",
"module": "ESNext",
Expand Down
130 changes: 72 additions & 58 deletions viteroll/viteroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import assert from "node:assert";
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { pathToFileURL } from "node:url";
import MagicString from "magic-string";
import * as rolldown from "rolldown";
import * as rolldownExperimental from "rolldown/experimental";
import sirv from "sirv";
import {
type Environment,
DevEnvironment,
type DevEnvironmentOptions,
type HmrContext,
type Plugin,
type PluginOption,
Expand All @@ -30,11 +32,11 @@ const logger = createLogger("info", {

export function viteroll(viterollOptions: ViterollOptions = {}): Plugin {
let server: ViteDevServer;
let managers: { client: RolldownManager; ssr: RolldownManager };
let environments: Record<"client" | "ssr", RolldownEnvironment>;

return {
name: viteroll.name,
config() {
config(config) {
return {
appType: "custom",
optimizeDeps: {
Expand All @@ -44,43 +46,43 @@ export function viteroll(viterollOptions: ViterollOptions = {}): Plugin {
// TODO: copy vite:define plugin
"process.env.NODE_ENV": "'development'",
},
};
},
configEnvironment(name, config, _env) {
if (name === "client") {
if (!config.build?.rollupOptions?.input) {
return {
environments: {
client: {
dev: {
createEnvironment:
RolldownEnvironment.createFactory(viterollOptions),
},
build: {
rollupOptions: {
input: "./index.html",
input:
config.build?.rollupOptions?.input ??
config.environments?.client.build?.rollupOptions?.input ??
"./index.html",
},
},
};
}
}
},
ssr: {
dev: {
createEnvironment:
RolldownEnvironment.createFactory(viterollOptions),
},
},
},
};
},
configureServer(server_) {
server = server_;
managers = {
client: new RolldownManager(
server.environments.client,
viterollOptions,
),
ssr: new RolldownManager(server.environments.ssr, {
...viterollOptions,
reactRefresh: false,
}),
};
environments = server.environments as any;

// rolldown server as middleware
server.middlewares.use(
sirv(managers.client.outDir, { dev: true, extensions: ["html"] }),
sirv(environments.client.outDir, { dev: true, extensions: ["html"] }),
);

// full build on non self accepting entry
server.ws.on("rolldown:hmr-deadend", async (data) => {
logger.info(`hmr-deadend '${data.moduleId}'`, { timestamp: true });
await managers.client.build();
await environments.client.build();
server.ws.send({ type: "full-reload" });
});

Expand All @@ -101,17 +103,9 @@ export function viteroll(viterollOptions: ViterollOptions = {}): Plugin {
oldSend.apply(this, args);
};
},
async buildStart() {
await managers.client.build();
await managers.ssr.build();
},
async buildEnd() {
await managers.client.close();
await managers.ssr.close();
},
async handleHotUpdate(ctx) {
await managers.ssr.handleUpdate(ctx);
await managers.client.handleUpdate(ctx);
await environments.ssr.handleUpdate(ctx);
await environments.client.handleUpdate(ctx);
},
transform(code, id) {
// remove unnecessary /@vite/env
Expand All @@ -123,25 +117,35 @@ export function viteroll(viterollOptions: ViterollOptions = {}): Plugin {
};
}

// TODO: RolldownEnvironment?
class RolldownManager {
export class RolldownEnvironment extends DevEnvironment {
instance!: rolldown.RolldownBuild;
result!: rolldown.RolldownOutput;
outDir: string;
outDir!: string;
buildTimestamp = Date.now();

static createFactory(
viterollOptions: ViterollOptions,
): NonNullable<DevEnvironmentOptions["createEnvironment"]> {
return (name, config) =>
new RolldownEnvironment(viterollOptions, name, config);
}

constructor(
public environment: Environment,
public viterollOptions: ViterollOptions,
name: ConstructorParameters<typeof DevEnvironment>[0],
config: ConstructorParameters<typeof DevEnvironment>[1],
) {
this.outDir = path.resolve(this.config.root, this.config.build.outDir);
super(name, config, { hot: false });
this.outDir = path.join(this.config.root, this.config.build.outDir);
}

get config() {
return this.environment.config;
override async init() {
await super.init();
await this.build();
}

get name() {
return this.environment.name;
override async close() {
await this.instance?.close();
}

async build() {
Expand All @@ -151,7 +155,7 @@ class RolldownManager {

await this.instance?.close();

if (this.config.build.emptyOutDir) {
if (this.config.build.emptyOutDir !== false) {
fs.rmSync(this.outDir, { recursive: true, force: true });
}

Expand All @@ -170,8 +174,8 @@ class RolldownManager {
) ?? [];
}

console.time(`[rolldown:${this.environment.name}:build]`);
this.instance = await rolldown.rolldown({
console.time(`[rolldown:${this.name}:build]`);
const inputOptions: rolldown.InputOptions = {
// TODO: no dev ssr for now
dev: this.name === "client",
// NOTE:
Expand All @@ -191,28 +195,31 @@ class RolldownManager {
viterollEntryPlugin(this.config, this.viterollOptions),
// TODO: how to use jsx-dev-runtime?
rolldownExperimental.transformPlugin({
reactRefresh: this.viterollOptions?.reactRefresh,
reactRefresh:
this.name === "client" && this.viterollOptions?.reactRefresh,
}),
this.viterollOptions?.reactRefresh ? reactRefreshPlugin() : [],
this.name === "client" && this.viterollOptions?.reactRefresh
? reactRefreshPlugin()
: [],
rolldownExperimental.aliasPlugin({
entries: this.config.resolve.alias,
}),
...(plugins as any),
],
});
};
this.instance = await rolldown.rolldown(inputOptions);

// `generate` should work but we use `write` so it's easier to see output and debug
this.result = await this.instance.write({
const outputOptions: rolldown.OutputOptions = {
dir: this.outDir,
format: this.name === "client" ? "app" : "es",
// TODO: hmr_rebuild returns source map file when `sourcemap: true`
sourcemap: "inline",
});
console.timeEnd(`[rolldown:${this.environment.name}:build]`);
}
};
this.result = await this.instance.write(outputOptions);

async close() {
await this.instance?.close();
this.buildTimestamp = Date.now();
console.timeEnd(`[rolldown:${this.name}:build]`);
}

async handleUpdate(ctx: HmrContext) {
Expand All @@ -227,13 +234,20 @@ class RolldownManager {
await this.build();
} else {
logger.info(`hmr '${ctx.file}'`, { timestamp: true });
console.time(`[rolldown:${this.environment.name}:hmr]`);
console.time(`[rolldown:${this.name}:hmr]`);
const result = await this.instance.experimental_hmr_rebuild([ctx.file]);
console.timeEnd(`[rolldown:${this.environment.name}:hmr]`);
console.timeEnd(`[rolldown:${this.name}:hmr]`);
ctx.server.ws.send("rolldown:hmr", result);
}
return true;
}

async import(input: string): Promise<unknown> {
const output = this.result.output.find((o) => o.name === input);
assert(output, `invalid import input '${input}'`);
const filepath = path.join(this.outDir, output.fileName);
return import(`${pathToFileURL(filepath)}?t=${this.buildTimestamp}`);
}
}

// TODO: copy vite:build-html plugin
Expand Down

0 comments on commit f950271

Please sign in to comment.