Skip to content
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ jobs:

- name: Build CLI (test)
run: bun run build:cli

- name: Run tests
run: bun run test
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"lint": "biome lint",
"format": "biome check --write --linter-enabled=false",
"build:app": "bun run -F './apps/registry' build",
"build:cli": "bun run -F './packages/usts' build"
"build:cli": "bun run -F './packages/usts' build",
"test": "bun run -F './packages/usts' test"
},
"devDependencies": {
"@biomejs/biome": "2.3.11",
Expand Down
3 changes: 2 additions & 1 deletion packages/usts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"license": "MIT",
"scripts": {
"build": "bunx --bun tsdown",
"prepublishOnly": "bun run build"
"prepublishOnly": "bun run build",
"test": "bun test"
},
"repository": {
"type": "git",
Expand Down
6 changes: 2 additions & 4 deletions packages/usts/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,7 @@ const UserscriptMetaHeaderConfigSchema: z.ZodObject<{
>
>;
require: z.ZodOptional<z.ZodArray<z.ZodString>>;
resource: z.ZodOptional<
z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>
>;
resource: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
grant: z.ZodOptional<z.ZodArray<z.ZodString>>;
downloadURL: z.ZodOptional<z.ZodString>;
updateURL: z.ZodOptional<z.ZodString>;
Expand All @@ -88,7 +86,7 @@ const UserscriptMetaHeaderConfigSchema: z.ZodObject<{
),

require: z.optional(z.array(z.string())),
resource: z.optional(z.union([z.string(), z.array(z.string())])),
resource: z.optional(z.record(z.string(), z.string())),

grant: z.optional(z.array(z.string())),

Expand Down
13 changes: 10 additions & 3 deletions packages/usts/src/core/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ const USERSCRIPT_OUTPUT_FILE_NAME = "index.user.js";

async function buildUserscript(
config: ResolvedUserscriptConfig,
): Promise<void> {
options?: { write?: boolean },
): Promise<string> {
console.log("\n⚒️ Building userscript");

const header = serializeMetaHeader(config.header).serializedHeader;
const header = serializeMetaHeader(config.header);

const bundle = await rolldown({ input: config.entryPoint, tsconfig: true });
const result = await bundle.generate({
Expand All @@ -28,7 +29,11 @@ async function buildUserscript(
}

const bundledCode = result.output[0].code;
const fullCode = `${header}\n\n${bundledCode}` as const;
const fullCode = `${header}\n\n${bundledCode}`;

if (!options?.write) {
return fullCode;
}

const outDir = config.outDir;
if (config.clean) {
Expand All @@ -39,6 +44,8 @@ async function buildUserscript(
const outFile = path.join(outDir, USERSCRIPT_OUTPUT_FILE_NAME);
await fs.writeFile(outFile, fullCode, "utf-8");
console.log("\n🎉 Build process complete!");

return fullCode;
}

export { buildUserscript };
2 changes: 1 addition & 1 deletion packages/usts/src/core/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ export default async function build(): Promise<void> {
);
}

await buildUserscript(userscriptConfig);
await buildUserscript(userscriptConfig, { write: true });
}
34 changes: 16 additions & 18 deletions packages/usts/src/core/build/meta-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,39 @@ import type { UserscriptMetaHeaderConfig } from "~/config/schema";
const headerStart = "// ==UserScript==" as const;
const headerEnd = "// ==/UserScript==" as const;

type HeaderLine = `// @${string}`;

function getHeaderLine(key: string, val: string | boolean): HeaderLine {
function getHeaderLine(
key: string,
val: string | boolean | readonly [string, string],
) {
if (typeof val === "boolean") return `// @${key}`;
const paddedKey = key.padEnd(16);
return `// @${paddedKey} ${val}`;
if (typeof val === "string") return `// @${paddedKey} ${val}`;
const paddedSubKey = val[0].padEnd(8);
return `// @${paddedKey} ${paddedSubKey} ${val[1]}`;
}

function getHeaderLines(
key: string,
val: string | string[] | boolean,
): HeaderLine[] {
val: string | string[] | boolean | Record<string, string>,
) {
if (Array.isArray(val)) return val.map((v) => getHeaderLine(key, v));
if (typeof val === "string") return [getHeaderLine(key, val)];
if (typeof val === "boolean") return [getHeaderLine(key, val)];
if (typeof val === "object") {
return Object.entries(val).map((v) => getHeaderLine(key, v));
}
throw new Error(`Unknown header value type: ${typeof val}`);
}

type SerializeMetaHeader = `// ==UserScript==
${string}
// ==/UserScript==`;

interface SerializeMetaHeaderResult {
serializedHeader: SerializeMetaHeader;
}

export function serializeMetaHeader(
headerConfig: UserscriptMetaHeaderConfig,
): SerializeMetaHeaderResult {
): string {
const headerConfigEntries = Object.entries(headerConfig);
const headerLines = headerConfigEntries.flatMap(([key, val]) =>
getHeaderLines(key, val),
const headerLines = headerConfigEntries.flatMap(([kwy, val]) =>
getHeaderLines(kwy, val),
);
const serializedHeaderLines = headerLines.join("\n");
const serializedHeader =
`${headerStart}\n${serializedHeaderLines}\n${headerEnd}` as const;
return { serializedHeader };
return serializedHeader;
}
16 changes: 16 additions & 0 deletions packages/usts/tests/__snapshots__/basic.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots

exports[`Basic userscript Correctly bundles a basic userscript 1`] = `
"// ==UserScript==
// @name Userscript name
// @namespace fixtures
// @match https://example.com/*
// @version 1.0.0
// @description Userscript description
// ==/UserScript==

(function() {
console.log("Hello world!");
})();
"
`;
12 changes: 12 additions & 0 deletions packages/usts/tests/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, expect, it } from "bun:test";

import { buildUserscript } from "../src/core/build/build";
import { resolveFixture } from "./helpers/resolve";

describe("Basic userscript", () => {
it("Correctly bundles a basic userscript", async () => {
const fixture = resolveFixture("fixtures/basic/index.ts");
const result = await buildUserscript(fixture);
expect(result).toMatchSnapshot();
});
});
1 change: 1 addition & 0 deletions packages/usts/tests/fixtures/basic/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("Hello world!");
23 changes: 23 additions & 0 deletions packages/usts/tests/helpers/resolve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { fileURLToPath } from "bun";
import {
type ResolvedUserscriptConfig,
UserscriptConfigSchema,
type UserscriptMetaHeaderConfig,
} from "../../src/config/schema";

export function resolveFixture(
entryPoint: string,
header?: Partial<UserscriptMetaHeaderConfig>,
): ResolvedUserscriptConfig {
return UserscriptConfigSchema.decode({
entryPoint: fileURLToPath(new URL(`../${entryPoint}`, import.meta.url)),
header: {
name: "Userscript name",
namespace: "fixtures",
description: "Userscript description",
match: "https://example.com/*",
version: "1.0.0",
...header,
},
});
}
4 changes: 2 additions & 2 deletions packages/usts/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"moduleResolution": "bundler",
"moduleDetection": "force",

"rootDir": "./src",
"rootDir": "./",

"paths": { "~/*": ["./src/*"] },

Expand All @@ -27,5 +27,5 @@
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true
},
"include": ["src/**/*"]
"include": ["src", "tests"]
}