Created
August 11, 2025 16:41
-
-
Save mizchi/9fd1bbfb1a06e4b8192f5ccb20e07b49 to your computer and use it in GitHub Desktop.
Moonbit(JS Backend) - TypeScript Template Generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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