Skip to content
Open
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
10 changes: 9 additions & 1 deletion packages/pack/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
resolveBundleOptions,
type WebpackConfig,
} from "../config/webpackCompat";
import type { HotReloaderInterface } from "../core/hmr";
import type { HotReloaderInterface, OnCompileDone } from "../core/hmr";
import { createHotReloader } from "../core/hmr";
import { createHttpProxyMiddleware } from "../core/proxyHono";
import { getOutputPath } from "../utils/cleanOutput";
Expand Down Expand Up @@ -114,6 +114,11 @@ export interface StartServerOptions {
port: number;
hostname: string;
}) => void | Promise<void>;
/**
* Called after the initial dev compilation and every following rebuild with
* the same structured errors and warnings sent through HMR.
*/
onCompileDone?: OnCompileDone;
}

/** Options for honoServe excluding fetch; we only use HTTP or HTTPS (no HTTP/2). */
Expand Down Expand Up @@ -243,6 +248,9 @@ async function runDev(
bundleOptions,
projectPathResolved,
rootPathResolved,
{
onCompileDone: serverOptions?.onCompileDone,
},
);
await hotReloader.start();

Expand Down
152 changes: 152 additions & 0 deletions packages/pack/src/core/hmr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import type { Issue } from "@utoo/pack-shared";
import { beforeEach, describe, expect, it, vi } from "vitest";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep tests out of the package build

Because packages/pack/tsconfig.json includes all of src and only excludes src/__test__, this new src/core/hmr.test.ts file is compiled by npm -w @utoo/pack run build:js into both cjs/ and esm/. The published package also exports ./cjs/* and ./esm/*, so this ships a test module that imports vitest even though @utoo/pack does not declare it; moving the test under the existing excluded src/__test__ tree or excluding *.test.ts avoids leaking test-only code into the distributable.

Useful? React with 👍 / 👎.

import type { Endpoint, Project } from "./types";

let mockProject: Project;
let mockEndpoint: Endpoint;

vi.mock("./project", () => ({
projectFactory: () => async () => mockProject,
}));

function createIssue(): Issue {
return {
severity: "error",
stage: "build",
filePath: "[project]/src/index.less",
title: {
type: "text",
value: "Unrecognised input",
},
description: {
type: "text",
value: "Less parser failed",
},
documentationLink: "",
importTraces: [],
};
}

async function* once<T>(value: T) {
yield value;
}

async function* empty<T>() {}

describe("createHotReloader", () => {
beforeEach(() => {
mockEndpoint = {
writeToDisk: vi.fn(async () => ({
issues: [createIssue()],
})) as any,
clientChanged: vi.fn(async () => empty<TurbopackResult>()),
serverChanged: vi.fn(async () => empty<TurbopackResult>()),
};

mockProject = {
update: vi.fn(),
writeAllEntrypointsToDisk: vi.fn(async () => ({
apps: [mockEndpoint],
appPaths: [],
issues: [createIssue()],
})),
entrypointsSubscribe: vi.fn(() =>
once({
apps: [mockEndpoint],
issues: [],
}),
),
hmrEvents: vi.fn(),
hmrIdentifiersSubscribe: vi.fn(),
getSourceForAsset: vi.fn(),
getSourceMap: vi.fn(),
getSourceMapSync: vi.fn(),
traceSource: vi.fn(),
updateInfoSubscribe: vi.fn(() => empty()),
shutdown: vi.fn(async () => {}),
onExit: vi.fn(async () => {}),
};
});

async function createStartedHotReloader(
stats = false,
onCompileDone = vi.fn(),
) {
const { createHotReloader } = await import("./hmr");
const hotReloader = await createHotReloader(
{
config: {
entry: [],
output: {
clean: false,
path: "dist",
},
optimization: {},
persistentCaching: false,
stats,
},
} as any,
undefined,
undefined,
{ onCompileDone },
);

await hotReloader.start();
return { hotReloader, onCompileDone };
}

function collectSyncErrors(
hotReloader: Awaited<
ReturnType<typeof createStartedHotReloader>
>["hotReloader"],
) {
const sent: any[] = [];
hotReloader.registerClient({
send(data) {
sent.push(JSON.parse(data));
},
close: vi.fn(),
});

const sync = sent.find((message) => message.action === "sync");
return sync?.errors;
}

it("surfaces initial endpoint output issues through sync without failing startup", async () => {
const { hotReloader, onCompileDone } = await createStartedHotReloader();

const errors = collectSyncErrors(hotReloader);
expect(errors).toHaveLength(1);
expect(errors[0].message).toContain("Less parser failed");
expect(onCompileDone).toHaveBeenCalledWith({
errors: expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining("Less parser failed"),
}),
]),
warnings: [],
});
expect(mockEndpoint.writeToDisk).toHaveBeenCalled();

await hotReloader.close();
});

it("surfaces initial project output issues through sync without failing startup", async () => {
const { hotReloader, onCompileDone } = await createStartedHotReloader(true);

const errors = collectSyncErrors(hotReloader);
expect(errors).toHaveLength(1);
expect(errors[0].message).toContain("Less parser failed");
expect(onCompileDone).toHaveBeenCalledWith({
errors: expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining("Less parser failed"),
}),
]),
warnings: [],
});
expect(mockProject.writeAllEntrypointsToDisk).toHaveBeenCalled();

await hotReloader.close();
});
});
Loading
Loading