Skip to content

Commit 4fbc810

Browse files
authored
simplify dual build (#6)
1 parent 0dc02ec commit 4fbc810

7 files changed

Lines changed: 85 additions & 102 deletions

File tree

CHANGELOG.md

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,76 +2,80 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.3.1] - 2025-12-09
6+
7+
### Changed
8+
9+
- **Build Process Simplification**: Removed wrapper script complexity in favor of direct SWC compilation
10+
- **Postbuild Script**: Replaced `insert-shebang.sh` with lightweight `add-shebang.js` Node script
11+
- **CJS Package Generation**: Now generated inline in build script instead of via wrapper
12+
- **README**: Updated "How It Works" section to reflect simplified build architecture
13+
14+
### Technical Details
15+
16+
**Build Process (Simplified):**
17+
18+
1. Prebuild: format:fix → lint → typecheck → clean
19+
2. Compile: SWC compiles source to both `build/esm/src/` and `build/cjs/src/`
20+
3. Path Resolution: `tsc-alias` transforms `@/` imports to relative paths
21+
4. Executables: `add-shebang.js` adds shebangs and sets executable permissions
22+
5. CJS Marker: `package.json` with `"type":"commonjs"` created in `build/cjs/`
23+
24+
**Entry Points:**
25+
26+
- ESM: `build/esm/src/cli.js` (executable)
27+
- CJS: `build/cjs/src/cli.js` (executable)
28+
529
## [0.3.0] - 2025-12-09
630

731
### Added
832

9-
- **Dual Build System**: Project now compiles to both ESM (`build/esm/`) and CommonJS (`build/cjs/`) formats with proper module format markers
10-
- **Path Aliases**: TypeScript path aliases (`@/*`) now work throughout the codebase and are resolved to relative paths in compiled output
11-
- **Build Validation**: Code quality checks (format, lint, typecheck) run automatically in the prebuild step before compilation
33+
- **Dual Build System**: Project compiles to both ESM (`build/esm/`) and CommonJS (`build/cjs/`) with proper module format markers
34+
- **Path Aliases**: TypeScript path aliases (`@/*`) work throughout codebase and resolve to relative paths in output
35+
- **Build Validation**: Code quality checks (format, lint, typecheck) run automatically in prebuild step
1236
- **Source Maps**: Both ESM and CJS builds include source maps for debugging
13-
- **Dynamic Executables**: Shebangs dynamically wrapped for both module formats via `insert-shebang.sh`
1437
- **ESM dirname Utility**: New `getDirname()` utility for ES module-compatible directory resolution
15-
- **Comprehensive Tests**:
16-
- 5 unit tests for CLI and command logic
17-
- 2 unit tests for dirname utility
18-
- 9 integration tests for build outputs (ESM and CJS execution)
19-
- 9 integration tests for module format verification and structure
20-
- Total: 25+ tests with 100% code coverage
21-
- **Integration Test Suite**: Dedicated `tests/integration/` directory with build output and module format validation
22-
- **JSDoc Comments**: Added documentation to all functions for better IDE support and clarity
23-
- **Improved README**: Comprehensive documentation with features list, usage examples, and technical architecture explanation
38+
- **Comprehensive Tests**: 27 total tests (9 unit + 18 integration) with 100% code coverage
39+
- **Integration Test Suite**: Dedicated `tests/integration/` with build output and module format validation
40+
- **JSDoc Comments**: All functions documented for IDE support and clarity
41+
- **Improved README**: Comprehensive documentation with features, usage, and architecture explanation
2442

2543
### Changed
2644

27-
- **Tooling Migration**: Replaced ESLint + Prettier with Biome for unified linting and formatting
28-
- **Node Version Requirement**: Updated from Node 16+ to Node 20+ for stable ESM support
29-
- **Build Scripts**: Restructured build pipeline with separate configurations for ESM and CJS
30-
- **CLI Implementation**: Enhanced with better error handling, version management, and command validation
31-
- **Testing Framework**: Updated from Vite's test to dedicated Vitest with coverage integration
32-
- **Project Structure**: Reorganized to include utilities folder and proper test file placement
45+
- **Tooling Migration**: Replaced ESLint + Prettier with Biome for unified linting/formatting
46+
- **Node Version**: Updated from 16+ to 20+ for stable ESM support without flags
47+
- **Build Scripts**: Restructured with separate SWC configurations for ESM and CJS
48+
- **CLI Implementation**: Enhanced with better error handling and version management
49+
- **Testing Framework**: Upgraded from Vite test to dedicated Vitest with coverage
50+
- **Project Structure**: Added utilities folder and proper test file organization
3351

3452
### Removed
3553

3654
- ESLint configuration and plugins
3755
- Prettier configuration
3856
- Old single build configuration (`.swcrc`)
39-
- Version check code that couldn't be reached in ESM environment
57+
- Unreachable Node version check code
4058

4159
### Fixed
4260

43-
- Test file exclusion from production builds (prevents `.test.js` in output)
61+
- Test file exclusion from production builds
4462
- Path alias transformation for both module formats
4563
- Vitest configuration to prevent test discovery in build directory
46-
- CJS module format declaration via `build/cjs/package.json`
47-
48-
### Technical Details
49-
50-
**Build Process:**
51-
52-
1. `npm run build` triggers prebuild (format:fix → lint → typecheck)
53-
2. Parallel compilation: `.swcrc-esm` and `.swcrc-cjs` compile source
54-
3. Post-compile: `tsc-alias` transforms `@/` imports to relative paths
55-
4. Postbuild: `insert-shebang.sh` creates executable wrappers for both formats
56-
57-
**Module Format Support:**
58-
59-
- ESM: Uses `import()`/`export`, targets modern Node.js
60-
- CJS: Uses `require()`/`exports`, includes `type: "commonjs"` marker for compatibility
64+
- CJS module format declaration
6165

62-
**Dependencies Updated:**
66+
### Dependencies Updated
6367

6468
- TypeScript: 4.9.5 → 5.9.3
6569
- SWC: 1.3.35 → 1.15.3
6670
- Commander: 10.0.0 → 14.0.2
6771
- Vitest: 0.28.5 → 4.0.15
68-
- Biome: Added as replacement for ESLint + Prettier
69-
- tsc-alias: Added for path alias transformation
70-
- @vitest/coverage-v8: Added for coverage reporting
72+
- Biome: 2.3.8 (new)
73+
- tsc-alias: 1.8.16 (new)
74+
- @vitest/coverage-v8: 4.0.15 (new)
7175

7276
### Breaking Changes
7377

7478
- Requires Node.js 20+ (was 16+)
75-
- Single build output replaced with dual ESM/CJS structure
76-
- Package exports field now specifies import/require conditions
79+
- Dual ESM/CJS structure replaces single build output
80+
- Package exports field specifies import/require conditions
7781
- CLI now uses Biome instead of ESLint/Prettier

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,13 @@ Run `npm run clean` to remove build artifacts and node_modules.
5656

5757
## How It Works
5858

59-
The dual build system uses SWC with two separate configurations: `.swcrc-esm` compiles source code to ES modules in `build/esm/`, while `.swcrc-cjs` compiles the same source to CommonJS in `build/cjs/`. Both configs reference `@/` path aliases defined in `tsconfig.json`.
59+
The dual build system compiles source code once, then generates both ESM and CommonJS outputs. SWC reads the same TypeScript source but uses two separate configurations: `.swcrc-esm` produces ES modules in `build/esm/`, while `.swcrc-cjs` produces CommonJS in `build/cjs/`. Both preserve the directory structure and include source maps.
6060

61-
After SWC compiles, `tsc-alias` transforms those `@/` imports to relative paths in both output directories so they resolve correctly without TypeScript at runtime. Finally, `insert-shebang.sh` wraps both compiled `cli.js` files with executable shebangs—the ESM version uses dynamic `import()`, the CJS version uses `require()` and generates a `package.json` marker.
61+
Path aliases (`@/*`) are configured in both SWC configs and resolved to relative imports at compile time via `tsc-alias`, so they work without TypeScript at runtime.
6262

63-
The build validates code quality first: `npm run build` runs the prebuild step (type checking, linting, formatting) before compilation, ensuring only clean code makes it to the output.
63+
The entry points (`build/esm/src/cli.js` and `build/cjs/src/cli.js`) get shebangs added via a simple Node script in the postbuild step. For CJS, a `package.json` file is generated in the output to explicitly declare the module format.
64+
65+
Quality checks run before any compilation: `npm run build` executes the prebuild step (format, lint, typecheck) to ensure only clean code makes it to the output.
6466

6567
## Technologies
6668

add-shebang.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { readFileSync, writeFileSync, chmodSync } from "node:fs";
2+
3+
const shebang = "#!/usr/bin/env node\n";
4+
const files = ["build/esm/src/cli.js", "build/cjs/src/cli.js"];
5+
6+
for (const file of files) {
7+
try {
8+
const content = readFileSync(file, "utf-8");
9+
if (!content.startsWith("#!")) {
10+
writeFileSync(file, shebang + content);
11+
chmodSync(file, 0o755);
12+
console.log(`✓ Added shebang to ${file}`);
13+
}
14+
} catch (err) {
15+
console.error(`Error processing ${file}:`, err.message);
16+
}
17+
}

insert-shebang.sh

Lines changed: 0 additions & 39 deletions
This file was deleted.

package.json

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"name": "hello-cli",
3-
"version": "0.3.0",
3+
"version": "0.3.1",
44
"description": "A basic CLI written in TypeScript meant to be used as an example or project starter",
55
"type": "module",
6-
"bin": "./build/esm/cli.js",
6+
"bin": "./build/esm/src/cli.js",
77
"exports": {
88
".": {
9-
"import": "./build/esm/cli.js",
10-
"require": "./build/cjs/cli.js"
9+
"import": "./build/esm/src/cli.js",
10+
"require": "./build/cjs/src/cli.js"
1111
}
1212
},
1313
"files": [
@@ -31,11 +31,10 @@
3131
},
3232
"scripts": {
3333
"prebuild": "npm run format:fix && npm run lint && npm run typecheck && rm -rf ./build",
34-
"build:esm": "swc --config-file .swcrc-esm -D src -d build/esm --ignore '**/*.test.ts' && tsc-alias -p tsconfig.json --outDir build/esm --resolve-full-paths",
35-
"build:cjs": "swc --config-file .swcrc-cjs -D src -d build/cjs --ignore '**/*.test.ts' && tsc-alias -p tsconfig.json --outDir build/cjs --resolve-full-paths",
36-
"build": "npm run build:esm && npm run build:cjs",
34+
"build:esm": "swc src --config-file .swcrc-esm -d build/esm --ignore '**/*.test.ts' && tsc-alias -p tsconfig.json --outDir build/esm --resolve-full-paths",
35+
"build:cjs": "swc src --config-file .swcrc-cjs -d build/cjs --ignore '**/*.test.ts' && tsc-alias -p tsconfig.json --outDir build/cjs --resolve-full-paths && echo '{\"type\":\"commonjs\"}' > build/cjs/package.json",
36+
"build": "npm run build:esm && npm run build:cjs && node ./add-shebang.js",
3737
"build:debug": "swc --config-file .swcrc-esm -s true -D src -d build/esm",
38-
"postbuild": "./insert-shebang.sh",
3938
"typecheck": "tsc --noEmit",
4039
"prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build",
4140
"clean": "rm -rf ./node_modules ./build; rm package-lock.json; echo 'cleared project artifacts!'",

tests/integration/build-outputs.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,29 @@ import { execSync } from "node:child_process";
33

44
describe("ESM Build Output", () => {
55
test("should execute ESM CLI successfully", () => {
6-
const output = execSync("./build/esm/cli.js hello World", {
6+
const output = execSync("./build/esm/src/cli.js hello World", {
77
encoding: "utf-8",
88
});
99
expect(output.trim()).toBe("Hello, World.");
1010
});
1111

1212
test("should support --version flag", () => {
13-
const output = execSync("./build/esm/cli.js --version", {
13+
const output = execSync("./build/esm/src/cli.js --version", {
1414
encoding: "utf-8",
1515
});
1616
expect(output.trim()).toMatch(/^\d+\.\d+\.\d+$/);
1717
});
1818

1919
test("should support --help flag", () => {
20-
const output = execSync("./build/esm/cli.js --help", {
20+
const output = execSync("./build/esm/src/cli.js --help", {
2121
encoding: "utf-8",
2222
});
2323
expect(output).toContain("Usage: hello-cli");
2424
expect(output).toContain("Commands:");
2525
});
2626

2727
test("should support --yell option", () => {
28-
const output = execSync("./build/esm/cli.js hello world --yell", {
28+
const output = execSync("./build/esm/src/cli.js hello world --yell", {
2929
encoding: "utf-8",
3030
});
3131
expect(output.trim()).toBe("HELLO, WORLD.");
@@ -34,21 +34,21 @@ describe("ESM Build Output", () => {
3434

3535
describe("CJS Build Output", () => {
3636
test("should execute CJS CLI successfully", () => {
37-
const output = execSync("./build/cjs/cli.js hello World", {
37+
const output = execSync("./build/cjs/src/cli.js hello World", {
3838
encoding: "utf-8",
3939
});
4040
expect(output.trim()).toBe("Hello, World.");
4141
});
4242

4343
test("should support --version flag", () => {
44-
const output = execSync("./build/cjs/cli.js --version", {
44+
const output = execSync("./build/cjs/src/cli.js --version", {
4545
encoding: "utf-8",
4646
});
4747
expect(output.trim()).toMatch(/^\d+\.\d+\.\d+$/);
4848
});
4949

5050
test("should support --help flag", () => {
51-
const output = execSync("./build/cjs/cli.js --help", {
51+
const output = execSync("./build/cjs/src/cli.js --help", {
5252
encoding: "utf-8",
5353
});
5454
expect(output).toContain("Usage: hello-cli");
@@ -62,7 +62,7 @@ describe("CJS Build Output", () => {
6262
});
6363

6464
test("should support --yell option", () => {
65-
const output = execSync("./build/cjs/cli.js hello world --yell", {
65+
const output = execSync("./build/cjs/src/cli.js hello world --yell", {
6666
encoding: "utf-8",
6767
});
6868
expect(output.trim()).toBe("HELLO, WORLD.");

tests/integration/module-format.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ describe("Module Format Verification", () => {
5656

5757
describe("Build Structure", () => {
5858
test("should have executables with shebangs", () => {
59-
const esmCli = readFileSync("./build/esm/cli.js", "utf-8");
60-
const cjsCli = readFileSync("./build/cjs/cli.js", "utf-8");
59+
const esmCli = readFileSync("./build/esm/src/cli.js", "utf-8");
60+
const cjsCli = readFileSync("./build/cjs/src/cli.js", "utf-8");
6161

6262
expect(esmCli).toMatch(/^#!\/usr\/bin\/env node/);
6363
expect(cjsCli).toMatch(/^#!\/usr\/bin\/env node/);

0 commit comments

Comments
 (0)