Type-checked macro-expanded scripting for hostile JavaScript runtimes
ts-cmacro lets you write large, maintainable scripts in TypeScript—with full type checking, IntelliSense, and modular structure—then compile them into a single, flat, top‑level TypeScript file that can be further transpiled to JavaScript for environments which do not support modules, bundlers, IIFE wrappers, or modern JS semantics.
If your runtime only understands:
function main(config) { /* ... */ }…but you want to author your script like a real project, this tool is for you.
# Install globally
npm install -g ts-cmacro
# or
pnpm add -g ts-cmacro
# Install as dev dependency
npm install -D ts-cmacro
# or
pnpm add -D ts-cmacro# Basic usage (outputs TypeScript)
ts-cmacro src/main.ts -o dist/script.ts
# Compact output (remove unnecessary whitespace)
ts-cmacro src/main.ts -o dist/script.ts -c
# Output to stdout
ts-cmacro src/main.ts
# Show help
ts-cmacro --helpimport { build } from 'ts-cmacro';
const code = build({
entry: './src/main.ts',
compact: true
});
console.log(code);Many real-world JavaScript runtimes are hostile to modern tooling:
- No
import/export - No module loader
- No filesystem or network I/O
- No control over the execution context
- Fragile or non-standard global scope handling
Examples include:
- Clash Verge / Clash Meta global scripts (boa engine)
- Surge / Loon / Quantumult X scripts
- Embedded JS engines (routers, IoT, NAS)
- WebView / injection-based environments
- Game scripting engines
In these environments, bundlers break, IIFEs break, and even globalThis may be unreliable.
The only stable contract is:
- A single script file
- With top-level declarations
- And a known entry function (e.g.
main)
ts-macro-script embraces this reality instead of fighting it.
Treat TypeScript as a macro language, not a module system.
importis for humans and tooling, not the runtime- The global scope is the linker
- All complexity is resolved at build time
- The runtime receives flat, boring, predictable JavaScript (after transpilation)
Think:
- C + preprocessor + linker
- Lisp macros
- Old-school embedded scripting
…but with modern TypeScript ergonomics.
Given an entry file like:
import { buildRules } from "./rules";
import type { ClashConfig } from "./types";
function main(config: ClashConfig) {
config.rules = buildRules(config.rules ?? []);
return config;
}ts-cmacro will:
- Parse the TypeScript program using the TypeScript Compiler API
- Resolve
importdependencies (relative paths only) - Topologically sort source files
- Remove all
importandexportsyntax - Concatenate declarations into a single output file
Resulting TypeScript:
function buildRules(old: string[]) {
return ["DOMAIN-SUFFIX,baidu.com,DIRECT", ...old];
}
function main(config: ClashConfig) {
config.rules = buildRules(config.rules ?? []);
return config;
}Then transpile to JavaScript using your preferred tool:
# Using tsc
tsc dist/script.ts --outFile dist/script.js --target ES2018
# Using esbuild
esbuild dist/script.ts --outfile=dist/script.js --target=es2018
# Using swc
swc dist/script.ts -o dist/script.jsNo wrappers. No modules. No runtime helpers.
This project intentionally does not:
- ❌ Bundle dependencies like Webpack/Rollup
- ❌ Emit IIFE or UMD wrappers
- ❌ Polyfill runtime features
- ❌ Provide a module loader
- ❌ Modify runtime globals
- ❌ Optimize for browsers or Node
If you want a bundler, use a bundler.
This tool exists specifically for environments where bundlers do not work.
-
Runtime minimalism The output must be as simple as possible.
-
Build-time maximalism Complexity is allowed—encouraged—at build time.
-
Deterministic output The same input always produces the same script.
-
Explicit over clever No magic globals, no hidden runtime behavior.
-
Hostile runtime first If it works in a broken engine, it will work anywhere.
src/
├─ rules.ts
├─ utils.ts
└─ main.ts ← entry
↓
ts-cmacro src/main.ts -o dist/script.ts
↓
dist/script.ts ← flat, top-level TypeScript
↓
tsc dist/script.ts --outFile dist/script.js
↓
dist/script.js ← flat, top-level JavaScript
You keep full IDE support:
- Type checking
- Go-to-definition
- Refactoring
- Code navigation
The runtime gets none of the complexity.
🚧 Early design / MVP stage
The initial version focuses on:
- Single entry point
- Relative imports
- Import/export stripping
- Ordered concatenation
Future features are explicitly out of scope until the core is proven stable.
You may want this tool if:
- You write JS for constrained or embedded environments
- You maintain large configuration scripts
- You are tired of "just copy-paste everything into one file"
- You want TypeScript ergonomics without runtime cost
If your runtime supports modern ESM—you probably don’t need this.
Use modern tools to generate primitive code.
That’s it.
GPLV3