Skip to content

Commit 7727cd5

Browse files
authored
feat: add watch mode (#7)
1 parent b68bb3c commit 7727cd5

11 files changed

Lines changed: 244 additions & 162 deletions

File tree

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/2.3.12/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
33
"vcs": {
44
"enabled": true,
55
"clientKind": "git",

bun.lock

Lines changed: 98 additions & 112 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@
77
"packages/*"
88
],
99
"catalog": {
10-
"@astrojs/cloudflare": "13.0.0-beta.1",
11-
"@astrojs/solid-js": "6.0.0-beta.2",
12-
"@better-auth/cli": "^1.4.17",
13-
"@better-auth/passkey": "^1.4.17",
10+
"@astrojs/cloudflare": "^13.0.0-beta.6",
11+
"@astrojs/solid-js": "^6.0.0-beta.2",
12+
"@better-auth/cli": "^1.4.18",
13+
"@better-auth/passkey": "^1.4.18",
1414
"@tailwindcss/vite": "^4.1.18",
15-
"astro": "6.0.0-beta.3",
16-
"better-auth": "^1.4.17",
15+
"astro": "^6.0.0-beta.10",
16+
"better-auth": "^1.4.18",
1717
"drizzle-orm": "^0.45.1",
18-
"elysia": "^1.4.22",
19-
"rolldown": "1.0.0-rc.1",
18+
"elysia": "^1.4.24",
19+
"rolldown": "1.0.0-rc.3",
2020
"solid-js": "^1.9.11",
2121
"tailwindcss": "^4.1.18",
22-
"tsdown": "^0.20.1",
23-
"wrangler": "^4.59.2",
22+
"tsdown": "^0.20.3",
23+
"wrangler": "^4.64.0",
2424
"zod": "^4.3.6"
2525
}
2626
},
@@ -29,10 +29,11 @@
2929
"format": "biome check --write --linter-enabled=false",
3030
"build:app": "bun run --cwd ./apps/registry build",
3131
"build:cli": "bun run --cwd ./packages/usts build",
32+
"build": "bun run build:cli",
3233
"test": "bun run --cwd ./packages/usts test"
3334
},
3435
"devDependencies": {
35-
"@biomejs/biome": "2.3.12"
36+
"@biomejs/biome": "2.3.14"
3637
},
3738
"overrides": {
3839
"esbuild": "^0.27.2"

packages/usts/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
"usts": "./dist/bin/cli.mjs"
2929
},
3030
"engines": {
31-
"node": ">=22.0.0",
32-
"bun": ">=1.2.20"
31+
"node": ">=24.0.0",
32+
"bun": ">=1.3.0"
3333
},
3434
"dependencies": {
3535
"rolldown": "catalog:",

packages/usts/src/bin/cli.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ async function main(): Promise<void> {
66
if (bunVersion) {
77
try {
88
const { semver } = await import("bun");
9-
const supportedBunVersion = ">=1.2.20";
9+
const supportedBunVersion = ">=1.3.0";
1010
if (!semver.satisfies(bunVersion, supportedBunVersion)) {
1111
throw new Error("Unsupported Bun version. Please upgrade Bun.");
1212
}
@@ -16,7 +16,7 @@ async function main(): Promise<void> {
1616
}
1717
} else {
1818
const version = parseInt(process.versions.node, 10) || 0;
19-
const minSupportedNodeVersion = 22;
19+
const minSupportedNodeVersion = 24;
2020
if (version < minSupportedNodeVersion) {
2121
throw new Error("Unsupported Node version. Please upgrade Node.\n");
2222
}

packages/usts/src/cli/build/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { resolveConfig } from "~/config/resolve";
22
import { buildUserscript } from "~/core/build";
3+
import { watchUserscript } from "~/core/build/watch";
34

4-
async function build(): Promise<void> {
5+
async function build(options: { watch?: boolean }): Promise<void> {
56
const { userscriptConfig, root } = await resolveConfig();
67

78
const outDir = userscriptConfig.outDir;
@@ -12,7 +13,13 @@ async function build(): Promise<void> {
1213
);
1314
}
1415

15-
await buildUserscript(userscriptConfig, { write: true });
16+
if (!options.watch) {
17+
await buildUserscript(userscriptConfig, { write: true });
18+
}
19+
20+
if (options.watch) {
21+
await watchUserscript(userscriptConfig);
22+
}
1623
}
1724

1825
export { build };

packages/usts/src/cli/index.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,25 @@ import { parseArgs } from "node:util";
22

33
type CLICommand = "help" | "build";
44

5-
type Flags = {} & Record<string, never>;
5+
type Flags = { watch: boolean };
66

77
interface Args {
88
values: Flags;
99
positionals: string[];
1010
}
1111

1212
async function printHelp() {
13-
console.log("Run `usts build` to build a userscript");
13+
console.log(`
14+
usts build - build a userscript
15+
usts build --watch - build userscript in watch mode
16+
`);
17+
}
18+
19+
function isSupportedCommand<T extends CLICommand>(
20+
supportedCommands: T[],
21+
cmd: string,
22+
): cmd is T {
23+
return new Set<string>(supportedCommands).has(cmd);
1424
}
1525

1626
function resolveCommand(parsedArgs: Args): CLICommand {
@@ -20,30 +30,33 @@ function resolveCommand(parsedArgs: Args): CLICommand {
2030
return "help";
2131
}
2232

23-
const supportedCommands = new Set(["build"]);
24-
if (supportedCommands.has(cmd)) {
25-
return cmd as CLICommand;
33+
if (isSupportedCommand(["build"], cmd)) {
34+
return cmd;
2635
}
2736

2837
return "help";
2938
}
3039

31-
async function runCommand(cmd: CLICommand) {
40+
async function runCommand(cmd: CLICommand, flags: Flags) {
3241
switch (cmd) {
3342
case "help": {
3443
await printHelp();
3544
return;
3645
}
3746
case "build": {
3847
const { build } = await import("./build/index.js");
39-
await build();
48+
await build({ watch: flags.watch });
4049
return;
4150
}
4251
}
4352
}
4453

4554
export async function cli(argv: string[]): Promise<void> {
46-
const parsedArgs = parseArgs({ args: argv, allowPositionals: true });
55+
const parsedArgs = parseArgs({
56+
args: argv,
57+
allowPositionals: true,
58+
options: { watch: { type: "boolean", default: false } },
59+
});
4760
const cmd = resolveCommand(parsedArgs);
48-
await runCommand(cmd);
61+
await runCommand(cmd, parsedArgs.values);
4962
}

packages/usts/src/core/build/index.ts

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,15 @@
1-
import * as path from "node:path";
2-
31
import * as rolldown from "rolldown";
42

53
import type { ResolvedUserscriptConfig } from "~/config/schema";
64

7-
import { serializeMetaHeader } from "./meta-header";
5+
import { resolveOptions } from "./options";
86

97
async function buildUserscript(
108
config: ResolvedUserscriptConfig,
11-
options?: { write?: boolean; watch?: boolean },
9+
options?: { write?: boolean },
1210
): Promise<string> {
13-
const header = serializeMetaHeader(config.header);
14-
15-
const USERSCRIPT_OUTPUT_FILE_NAME = "index.user.js";
16-
const outFile = path.join(config.outDir, USERSCRIPT_OUTPUT_FILE_NAME);
17-
18-
const result = await rolldown.build({
19-
input: config.entryPoint,
20-
tsconfig: true,
21-
plugins: [config.plugins],
22-
output: {
23-
format: "iife",
24-
sourcemap: false,
25-
minify: "dce-only",
26-
postBanner: `${header}\n`,
27-
cleanDir: config.clean,
28-
file: outFile,
29-
},
30-
write: options?.write ?? false,
31-
});
11+
const buildOptions = resolveOptions(config, options);
12+
const result = await rolldown.build(buildOptions);
3213

3314
if (result.output.length !== 1) {
3415
throw new Error(`❌ Unexpected userscript build output`);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as path from "node:path";
2+
import type { InputOptions, OutputOptions } from "rolldown";
3+
import type { ResolvedUserscriptConfig } from "~/config/schema";
4+
import { serializeMetaHeader } from "./meta-header";
5+
6+
interface ResolvedOutputOptions extends OutputOptions {
7+
readonly file: string;
8+
}
9+
10+
interface ResolvedOptions extends InputOptions {
11+
/** @default false */
12+
readonly write: boolean;
13+
readonly output: ResolvedOutputOptions;
14+
}
15+
16+
export function resolveOptions(
17+
config: ResolvedUserscriptConfig,
18+
options?: { write?: boolean },
19+
): ResolvedOptions {
20+
const header = serializeMetaHeader(config.header);
21+
22+
const USERSCRIPT_OUTPUT_FILE_NAME = "index.user.js";
23+
const outFile = path.join(config.outDir, USERSCRIPT_OUTPUT_FILE_NAME);
24+
25+
return {
26+
input: config.entryPoint,
27+
tsconfig: true,
28+
plugins: [config.plugins],
29+
output: {
30+
format: "iife",
31+
sourcemap: false,
32+
minify: "dce-only",
33+
postBanner: `${header}\n`,
34+
cleanDir: config.clean,
35+
file: outFile,
36+
},
37+
write: options?.write ?? false,
38+
};
39+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { readFile } from "node:fs/promises";
2+
import { createServer } from "node:http";
3+
4+
export async function serve(options: {
5+
port: number;
6+
outDir: string;
7+
fileName: string;
8+
}): Promise<void> {
9+
return new Promise<void>(() => {
10+
const server = createServer(async (req, res) => {
11+
if (req.url !== `/${options.fileName}`) {
12+
res.writeHead(404, { "Content-Type": "text/plain" });
13+
res.end("Not found");
14+
return;
15+
}
16+
17+
const bundle = await readFile(`${options.outDir}/${options.fileName}`);
18+
19+
res.writeHead(200, {
20+
"Content-Type": "application/javascript",
21+
"Cache-Control": "no-cache, no-store, must-revalidate",
22+
Pragma: "no-cache",
23+
Expires: "0",
24+
});
25+
26+
res.end(bundle);
27+
});
28+
29+
server.listen(options.port, () => {
30+
console.log(`http://localhost:${options.port}/${options.fileName}`);
31+
});
32+
});
33+
}

0 commit comments

Comments
 (0)