Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created August 11, 2025 16:41
Show Gist options
  • Save mizchi/9fd1bbfb1a06e4b8192f5ccb20e07b49 to your computer and use it in GitHub Desktop.
Save mizchi/9fd1bbfb1a06e4b8192f5ccb20e07b49 to your computer and use it in GitHub Desktop.
Moonbit(JS Backend) - TypeScript Template Generator
/**
* mbts - Moonbit JS Backend Project generator
* Install on deno and usages
* deno install -Afg mbts.ts
*
* # generate boilerplate
* $ mbts new mylib
* $ mbts new mylib --name mizchi/newpkg
* $ mbts new mylib --lib
* $ mbts new mylib --deno
* $ mbts new mylib --deno --lib
* $ mbts new mylib --deno --lib --dnt
*
* watch (deno only)
* $ mbts run -w run.ts
* $ mbts test -w
*/
import { parseArgs } from "node:util";
import path from "node:path";
import $ from "jsr:@david/dax";
const parsed = parseArgs({
args: Deno.args,
options: {
lib: {
type: "boolean",
},
watch: {
type: "boolean",
short: "w",
},
dnt: {
type: "boolean",
},
deno: {
type: "boolean",
},
name: {
type: "string",
},
help: {
type: "boolean",
short: "h",
},
},
allowPositionals: true,
});
if (parsed.values.help) {
console.log("Usage: mbts <command> [options]");
console.log();
console.log("Commands:");
console.log(" new <target> Create a new library");
console.log(" run -w <typescript> Run script with watch");
console.log(" test -w <typescript> Test with watch");
console.log();
console.log("Options:");
console.log(" --lib Create a library");
console.log(" --deno Use Deno");
console.log(" --help Show this help message");
console.log(" --dnt Use dnt to generate npm package");
console.log(" --watch Watch files for changes");
Deno.exit(0);
}
const cmd = parsed.positionals[0];
const GIT_IGNORE = `target
node_modules
.mooncakes
npm
`;
const DENO_LIB_ASSETS = (opts: { name: string; useDnt: boolean }) => {
const assets: Record<string, string> = {
".gitignore": GIT_IGNORE,
"README.md": `# ${opts.name ?? "pkg/lib"}
## Develop
\`\`\`
$ deno task test
$ deno task build
\`\`\`
`,
"moon.mod.json": JSON.stringify(
{
name: opts.name ?? "pkg/lib",
version: "0.1.0",
readme: "README.md",
},
null,
2
),
"lib/top.mbt": `pub fn add(a: Int, b: Int) -> Int {
a + b
}
test "add spec" {
assert_eq(add(2, 3), 5)
}
`,
"lib/moon.pkg.json": JSON.stringify(
{
targets: {
"top.mbt": ["js"],
},
link: {
js: {
exports: ["add"],
},
},
},
null,
2
),
// deno assets
"mod.ts": `export * from "./target/js/release/build/lib/lib.js"`,
"tests/add.test.ts": `import { expect } from "@std/expect"
import { add } from "../mod.ts";
Deno.test("add", () => {
const result = add(2, 3);
expect(result).toBe(5);
});
`,
"deno.json": JSON.stringify({
name: opts.name ? `@${opts.name}` : "@pkg/lib",
version: "0.1.0",
license: "MIT",
description: "",
exports: {
".": "./mod.ts",
},
tasks: {
test: "moon test && moon build --target js && deno test -A",
build: "moon build --target js",
},
imports: {
"@std/expect": "jsr:@std/[email protected]",
},
publish: {
include: [
"README.md",
"LICENSE",
"**/*.ts",
"target/js/release/build/**/*.js",
"target/js/release/build/**/*.d.ts",
],
},
}),
};
if (opts.useDnt) {
assets["build.ts"] = `
import pkg from "./deno.json" with { type: "json" }
import { build, emptyDir } from "@deno/dnt";
await emptyDir("./npm");
await build({
entryPoints: ["./mod.ts"],
outDir: "./npm",
shims: {
deno: true,
},
test: false,
package: {
name: pkg.name,
version: pkg.version,
description: pkg.description,
license: pkg.license,
},
postBuild() {
Deno.copyFileSync("README.md", "npm/README.md");
},
});
`;
assets["deno.json"] = JSON.stringify(
{
name: opts.name ? `@${opts.name}` : "@pkg/lib",
version: "0.1.0",
license: "MIT",
description: "",
exports: {
".": "./mod.ts",
},
tasks: {
test: "moon test && moon build --target js && deno test -A",
build: "moon build --target js && deno run -A ./build.ts",
},
imports: {
"@std/expect": "jsr:@std/[email protected]",
"@deno/dnt": "jsr:@deno/[email protected]",
},
publish: {
include: [
"README.md",
"LICENSE",
"**/*.ts",
"target/js/release/build/**/*.js",
"target/js/release/build/**/*.d.ts",
],
},
},
null,
2
);
}
return assets;
};
const NODE_LIB_ASSETS = (opts: { name: string }) => {
return {
"moon.mod.json": JSON.stringify(
{
name: opts.name ?? "pkg/lib",
version: "0.1.0",
readme: "README.md",
},
null,
2
),
"lib/top.mbt": `pub fn add(a: Int, b: Int) -> Int {
a + b
}
test "add spec" {
assert_eq(add(2, 3), 5)
}
`,
"lib/moon.pkg.json": JSON.stringify(
{
targets: {
"top.mbt": ["js"],
},
link: {
js: {
exports: ["add"],
},
},
},
null,
2
),
// node assets
"src/mod.ts": `export * from "./target/js/release/build/lib/lib.js"`,
"tests/add.test.ts": `import { test, expect } from "vitest";
import { add } from "${opts.name ?? "../target/js/release/build/lib/lib.js"}";
test("add", () => {
const result = add(2, 3);
expect(result).toBe(5);
});
`,
"package.json": JSON.stringify(
{
name: opts.name ? `@${opts.name}` : "@pkg/lib",
version: "0.1.0",
type: "module",
exports: {
".": {
import: "./target/js/release/build/lib/lib.js",
types: "./target/js/release/build/lib/lib.d.ts",
},
},
files: ["target/js/release/build/lib/"],
scripts: {
test: "moon test && moon build --target js && vitest --run",
build: "moon build --target js",
},
dependencies: {
vitest: "^3.2.4",
},
},
null,
2
),
"tsconfig.json": JSON.stringify(
{
compilerOptions: {
target: "ESNext",
module: "ESNext",
moduleResolution: "Bundler",
noEmit: true,
strict: true,
esModuleInterop: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
},
},
null,
2
),
};
};
const DENO_MAIN_ASSETS = (opts: { name: string }) => ({
".gitignore": GIT_IGNORE,
"moon.mod.json": JSON.stringify(
{
name: opts.name ? `@${opts.name}` : "@pkg/lib",
version: "0.1.0",
readme: "README.md",
},
null,
2
),
"main/main.mbt": `fn main {
println("Hello World")
}
test "test-sample" {
assert_eq(1, 1)
}
`,
"main/moon.pkg.json": JSON.stringify(
{
"is-main": true,
},
null,
2
),
// deno assets
"deno.json": JSON.stringify(
{
name: opts.name ? `@${opts.name}` : "@pkg/lib",
version: "0.1.0",
license: "MIT",
description: "",
exports: {
".": "./target/js/release/build/main/main.js",
},
tasks: {
run: "moon build --target js && deno run -A ./target/js/release/build/main/main.js",
test: "moon test && moon build --target js",
},
publish: {
include: [
"README.md",
"LICENSE",
"**/*.ts",
"target/js/release/build/**/*.js",
"target/js/release/build/**/*.d.ts",
],
},
},
null,
2
),
});
const NODE_MAIN_ASSETS = (opts: { name: string }) => ({
"moon.mod.json": JSON.stringify(
{
name: opts.name ?? "@pkg/lib",
version: "0.1.0",
readme: "README.md",
},
null,
2
),
"main/main.mbt": `fn main {
println("Hello World")
}
test "test-sample" {
assert_eq(1, 1)
}
`,
"main/moon.pkg.json": JSON.stringify(
{
"is-main": true,
},
null,
2
),
// deno assets
"package.json": JSON.stringify(
{
name: opts.name ? `@${opts.name}` : "@pkg/lib",
version: "0.1.0",
type: "module",
exports: {
".": {
import: "./target/js/release/build/main/main.js",
types: "./target/js/release/build/main/main.d.ts",
},
},
files: ["target/js/release/build/main"],
scripts: {
dev: "moon build --target js && node ./target/js/release/build/main/main.js",
test: "moon test && moon build --target js && vitest --run",
build: "moon build --target js",
},
dependencies: {
vitest: "^3.2.4",
},
},
null,
2
),
"tsconfig.json": JSON.stringify(
{
compilerOptions: {
target: "ESNext",
module: "ESNext",
moduleResolution: "Bundler",
noEmit: true,
strict: true,
esModuleInterop: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
},
},
null,
2
),
});
switch (cmd) {
case "build": {
const cwd = Deno.cwd();
await $`moon build --target js`.cwd(cwd);
break;
}
case "run": {
const isWatch = parsed.values.watch ?? false;
const target = parsed.positionals[1];
if (!target) {
throw new Error("Please specify a target file to watch.");
}
if (isWatch) {
const moonWatchCmd = new Deno.Command("moon", {
args: ["build", "--target", "js", "--watch"],
stdin: "inherit",
stdout: "inherit",
});
const denoRunWatchCmd = new Deno.Command("deno", {
args: ["run", "--watch", "-A", target],
stdin: "inherit",
stdout: "inherit",
});
const moonWatch = moonWatchCmd.spawn();
const denoRunWatch = denoRunWatchCmd.spawn();
Deno.addSignalListener("SIGINT", () => {
moonWatch.kill();
denoRunWatch.kill();
Deno.exit(0);
});
} else {
new Deno.Command("moon", {
args: ["build", "--target", "js"],
stdin: "inherit",
stdout: "inherit",
}).outputSync();
new Deno.Command("deno", {
args: ["run", "-A", target],
stdin: "inherit",
stdout: "inherit",
}).outputSync();
}
break;
}
case "check": {
const cwd = Deno.cwd();
await $`moon check`.cwd(cwd);
break;
}
case "test": {
const cwd = Deno.cwd();
const isDeno =
Deno.statSync("deno.json").isFile || Deno.statSync("deno.jsonc").isFile;
const isWatch = parsed.values.watch ?? false;
if (isDeno) {
if (isWatch) {
const moonWatchCmd = new Deno.Command("moon", {
args: ["build", "--target", "js", "--watch"],
stdin: "inherit",
stdout: "inherit",
});
const denoTestWatchCmd = new Deno.Command("deno", {
args: ["test", "-A", "--watch"],
stdin: "inherit",
stdout: "inherit",
});
const moonWatch = moonWatchCmd.spawn();
const denoRunWatch = denoTestWatchCmd.spawn();
Deno.addSignalListener("SIGINT", () => {
moonWatch.kill();
denoRunWatch.kill();
Deno.exit(0);
});
} else {
await $`moon build --target js`.cwd(cwd);
await $`deno task test`.cwd(cwd);
}
} else {
await $`moon build --target js`.cwd(cwd);
await $`npm test`.cwd(cwd);
}
break;
}
case "new": {
const target = parsed.positionals[1];
if (!target) {
throw new Error("Please specify a target directory for the new project.");
}
const targetDir = path.join(Deno.cwd(), target);
console.log(
"Creating new project in:",
path.relative(Deno.cwd(), targetDir)
);
Deno.mkdirSync(targetDir, { recursive: true });
// flags
const libname = parsed.values.name ?? "pkg/lib";
// validate name
const splitted = libname.split("/");
if (splitted.length !== 2) {
throw new Error(
"Invalid package name. Please use a scoped package name (e.g., @scope/name)."
);
}
const isLib = parsed.values.lib ?? false;
const isDeno = parsed.values.deno ?? false;
const useDnt = parsed.values.dnt ?? false;
if (isDeno) {
const assets = isLib
? DENO_LIB_ASSETS({ name: libname, useDnt })
: DENO_MAIN_ASSETS({ name: libname });
for (const [file, content] of Object.entries(assets)) {
const filePath = path.join(targetDir, file);
Deno.mkdirSync(path.dirname(filePath), { recursive: true });
Deno.writeTextFileSync(filePath, content);
console.log("Created:", path.relative(Deno.cwd(), filePath));
}
if (isLib) {
await $`deno task build`.cwd(targetDir);
await $`deno task test`.cwd(targetDir);
} else {
await $`deno task run`.cwd(targetDir);
await $`deno task test`.cwd(targetDir);
}
} else {
const assets = isLib
? NODE_LIB_ASSETS({ name: target })
: NODE_MAIN_ASSETS({ name: target });
for (const [file, content] of Object.entries(assets)) {
const filePath = path.join(targetDir, file);
Deno.mkdirSync(path.dirname(filePath), { recursive: true });
Deno.writeTextFileSync(filePath, content);
console.log("Created:", path.relative(Deno.cwd(), filePath));
}
await $`npm install`.cwd(targetDir);
if (isLib) {
await $`npm test`.cwd(targetDir);
} else {
await $`npm run dev`.cwd(targetDir);
}
}
break;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment