Skip to content

Commit d28d0d8

Browse files
authored
Merge pull request #1 from hyperweb-io/makage
makage
2 parents 1edb590 + 7c34643 commit d28d0d8

File tree

20 files changed

+3270
-5028
lines changed

20 files changed

+3270
-5028
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ jobs:
5151
- find-pkg
5252
- http-errors
5353
- jsonld-tools
54+
- makage
5455
- nested-obj
5556
- node-api-client
5657
- schema-sdk

packages/makage/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Changelog
2+
3+
## 0.1.0 - 2024-11-22
4+
5+
### Added
6+
7+
- Initial release
8+
- `makage clean` command for cross-platform directory removal
9+
- `makage copy` command with `--flat` option
10+
- `makage readme-footer` command for concatenating README with footer
11+
- `makage assets` command for common asset copying
12+
- `makage build-ts` command for TypeScript compilation
13+
- Zero dependencies - uses only Node.js built-in modules

packages/makage/README.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# makage
2+
3+
<p align="center">
4+
<img src="https://raw.githubusercontent.com/hyperweb-io/dev-utils/refs/heads/main/docs/img/logo.svg" width="80">
5+
<br />
6+
Tiny build helper for monorepo packages
7+
<br />
8+
<a href="https://github.com/hyperweb-io/dev-utils/actions/workflows/ci.yml">
9+
<img height="20" src="https://github.com/hyperweb-io/dev-utils/actions/workflows/ci.yml/badge.svg" />
10+
</a>
11+
<a href="https://github.com/hyperweb-io/dev-utils/blob/main/LICENSE">
12+
<img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/>
13+
</a>
14+
</p>
15+
16+
`makage` is a tiny, cross-platform build helper that replaces common build tools like `cpy` and `rimraf` with zero dependencies. It provides essential commands for managing package builds in monorepos.
17+
18+
> **makage** = `make` + `package`. A delightful portmanteau, like brunch for build tools—except makage actually gets things done.
19+
20+
## Assumptions
21+
22+
This tool is designed for monorepos that follow a specific package structure. If you use packages the way we do, `makage` assumes:
23+
24+
- **`dist/` folder** - Your build output goes to a `dist/` directory
25+
- **pnpm workspaces** - You're using pnpm workspace protocol for internal dependencies
26+
- **`publishConfig.directory` set to `dist`** - Your `package.json` publishes from the `dist/` folder
27+
- This enables tree-shaking with deep imports and modular development
28+
- **Shared LICENSE** - The `LICENSE` file in the monorepo root is copied to each package when published
29+
- **Assets copied to `dist/`** - Before publishing, assets (LICENSE, README, package.json) are copied into `dist/` so pnpm can publish them from there
30+
- **Optional: `FOOTER.md`** - If present in a package directory, it will be appended to `README.md` before being copied to `dist/`
31+
32+
These conventions allow for clean package distribution while maintaining a modular development structure.
33+
34+
## Features
35+
36+
- **Cross-platform copy** - Copy files with `--flat` option (replacement for `cpy`)
37+
- **Cross-platform clean** - Recursively remove directories (replacement for `rimraf`)
38+
- **README + Footer concatenation** - Combine README with footer content before publishing
39+
- **Assets helper** - One-command copying of LICENSE, README, and package.json
40+
- **Build TypeScript helper** - Run both CJS and ESM TypeScript builds
41+
- **Update workspace dependencies** - Automatically convert internal package references to `workspace:*`
42+
- **Zero dependencies** - Uses only Node.js built-in modules
43+
44+
## Install
45+
46+
```sh
47+
npm install makage
48+
```
49+
50+
## Usage
51+
52+
### CLI Commands
53+
54+
```bash
55+
# Clean build directories
56+
makage clean dist
57+
58+
# Copy files to destination
59+
makage copy ../../LICENSE README.md package.json dist --flat
60+
61+
# Concatenate README with footer
62+
makage readme-footer --source README.md --footer FOOTER.md --dest dist/README.md
63+
64+
# Copy standard assets (LICENSE, package.json, README+FOOTER)
65+
makage assets
66+
67+
# Build TypeScript (both CJS and ESM)
68+
makage build-ts
69+
70+
# Update workspace dependencies
71+
makage update-workspace
72+
```
73+
74+
### Package.json Scripts
75+
76+
Replace your existing build scripts with `makage`:
77+
78+
```json
79+
{
80+
"scripts": {
81+
"clean": "makage clean dist",
82+
"build": "makage clean dist && makage build-ts && makage assets",
83+
"prepublishOnly": "npm run build"
84+
}
85+
}
86+
```
87+
88+
Or using the simplified pattern:
89+
90+
```json
91+
{
92+
"scripts": {
93+
"copy": "makage assets",
94+
"clean": "makage clean dist",
95+
"build": "npm run clean && tsc && tsc -p tsconfig.esm.json && npm run copy"
96+
}
97+
}
98+
```
99+
100+
## Commands
101+
102+
### `makage clean <path...>`
103+
104+
Recursively removes one or more paths.
105+
106+
```bash
107+
makage clean dist build temp
108+
```
109+
110+
### `makage copy [...sources] <dest> [--flat]`
111+
112+
Copy files to a destination directory.
113+
114+
- Use `--flat` to copy files directly into the destination without preserving directory structure
115+
- Last argument is the destination, all others are sources
116+
117+
```bash
118+
makage copy ../../LICENSE README.md dist --flat
119+
```
120+
121+
### `makage readme-footer --source <file> --footer <file> --dest <file>`
122+
123+
Concatenate a README with a footer file, separated by a horizontal rule.
124+
125+
```bash
126+
makage readme-footer --source README.md --footer FOOTER.md --dest dist/README.md
127+
```
128+
129+
### `makage assets`
130+
131+
Combines common asset copying tasks:
132+
1. Copies `../../LICENSE` to `dist/`
133+
2. Copies `package.json` to `dist/`
134+
3. Concatenates `README.md` + `FOOTER.md` into `dist/README.md`
135+
136+
```bash
137+
makage assets
138+
```
139+
140+
### `makage build-ts`
141+
142+
Runs TypeScript compilation for both CommonJS and ESM:
143+
1. `tsc` (CommonJS)
144+
2. `tsc -p tsconfig.esm.json` (ESM)
145+
146+
```bash
147+
makage build-ts
148+
```
149+
150+
### `makage update-workspace`
151+
152+
Updates all internal package dependencies to use the `workspace:*` protocol. This is useful in monorepos when you want to ensure all cross-package references use workspace linking.
153+
154+
Run from the monorepo root:
155+
156+
```bash
157+
makage update-workspace
158+
```
159+
160+
This will:
161+
1. Scan all packages in the `packages/` directory
162+
2. Identify internal package names
163+
3. Update all dependencies, devDependencies, peerDependencies, and optionalDependencies
164+
4. Convert version numbers to `workspace:*` for internal packages
165+
166+
## Programmatic Usage
167+
168+
You can also use `makage` commands programmatically:
169+
170+
```typescript
171+
import { runCopy, runClean, runAssets } from 'makage';
172+
173+
async function build() {
174+
await runClean(['dist']);
175+
// ... your build steps
176+
await runAssets([]);
177+
}
178+
```
179+
180+
## Why makage?
181+
182+
Most monorepo packages need the same basic build operations:
183+
- Clean output directories
184+
- Copy LICENSE and README to distribution
185+
- Build TypeScript for both CJS and ESM
186+
187+
Instead of installing multiple dependencies (`cpy`, `rimraf`, etc.) in every package, `makage` provides these essentials with zero dependencies, using only Node.js built-in modules.
188+
189+
## Development
190+
191+
When first cloning the repo:
192+
193+
```bash
194+
pnpm install
195+
pnpm build
196+
```
197+
198+
Run tests:
199+
200+
```bash
201+
pnpm test
202+
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import fs from 'node:fs/promises';
2+
import { runClean } from '../src/commands/clean';
3+
4+
jest.mock('node:fs/promises');
5+
6+
const mockedFs = fs as jest.Mocked<typeof fs>;
7+
8+
describe('runClean', () => {
9+
beforeEach(() => {
10+
jest.clearAllMocks();
11+
mockedFs.rm.mockResolvedValue(undefined);
12+
});
13+
14+
it('should remove a single path', async () => {
15+
await runClean(['dist']);
16+
17+
expect(mockedFs.rm).toHaveBeenCalledWith('dist', { recursive: true, force: true });
18+
});
19+
20+
it('should remove multiple paths', async () => {
21+
await runClean(['dist', 'build', 'temp']);
22+
23+
expect(mockedFs.rm).toHaveBeenCalledWith('dist', { recursive: true, force: true });
24+
expect(mockedFs.rm).toHaveBeenCalledWith('build', { recursive: true, force: true });
25+
expect(mockedFs.rm).toHaveBeenCalledWith('temp', { recursive: true, force: true });
26+
});
27+
28+
it('should throw error if no paths provided', async () => {
29+
await expect(runClean([])).rejects.toThrow('clean requires at least one path');
30+
});
31+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import fs from 'node:fs/promises';
2+
import { runCopy } from '../src/commands/copy';
3+
4+
jest.mock('node:fs/promises');
5+
6+
const mockedFs = fs as jest.Mocked<typeof fs>;
7+
8+
describe('runCopy', () => {
9+
beforeEach(() => {
10+
jest.clearAllMocks();
11+
mockedFs.mkdir.mockResolvedValue(undefined);
12+
mockedFs.copyFile.mockResolvedValue(undefined);
13+
mockedFs.stat.mockResolvedValue({
14+
isDirectory: () => false,
15+
} as any);
16+
});
17+
18+
it('should copy a single file to destination', async () => {
19+
await runCopy(['README.md', 'dist', '--flat']);
20+
21+
expect(mockedFs.mkdir).toHaveBeenCalledWith('dist', { recursive: true });
22+
expect(mockedFs.copyFile).toHaveBeenCalledWith('README.md', 'dist/README.md');
23+
});
24+
25+
it('should copy multiple files to destination', async () => {
26+
await runCopy(['file1.txt', 'file2.txt', 'dist', '--flat']);
27+
28+
expect(mockedFs.copyFile).toHaveBeenCalledWith('file1.txt', 'dist/file1.txt');
29+
expect(mockedFs.copyFile).toHaveBeenCalledWith('file2.txt', 'dist/file2.txt');
30+
});
31+
32+
it('should throw error if less than 2 arguments', async () => {
33+
await expect(runCopy(['onlyDest'])).rejects.toThrow(
34+
'copy requires at least one source and one destination'
35+
);
36+
});
37+
38+
it('should throw error if source is a directory', async () => {
39+
mockedFs.stat.mockResolvedValue({
40+
isDirectory: () => true,
41+
} as any);
42+
43+
await expect(runCopy(['somedir', 'dist'])).rejects.toThrow(
44+
'Directories not yet supported as sources: somedir'
45+
);
46+
});
47+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import fs from 'node:fs/promises';
2+
import { runReadmeFooter } from '../src/commands/readmeFooter';
3+
4+
jest.mock('node:fs/promises');
5+
6+
const mockedFs = fs as jest.Mocked<typeof fs>;
7+
8+
describe('runReadmeFooter', () => {
9+
beforeEach(() => {
10+
jest.clearAllMocks();
11+
mockedFs.mkdir.mockResolvedValue(undefined);
12+
mockedFs.writeFile.mockResolvedValue(undefined);
13+
});
14+
15+
it('should concatenate README and FOOTER files', async () => {
16+
mockedFs.readFile
17+
.mockResolvedValueOnce('# README Content\n\nSome text' as any)
18+
.mockResolvedValueOnce('## Footer\n\nFooter text' as any);
19+
20+
await runReadmeFooter([
21+
'--source',
22+
'README.md',
23+
'--footer',
24+
'FOOTER.md',
25+
'--dest',
26+
'dist/README.md',
27+
]);
28+
29+
expect(mockedFs.readFile).toHaveBeenCalledWith('README.md', 'utf8');
30+
expect(mockedFs.readFile).toHaveBeenCalledWith('FOOTER.md', 'utf8');
31+
expect(mockedFs.writeFile).toHaveBeenCalledWith(
32+
'dist/README.md',
33+
'# README Content\n\nSome text\n\n---\n\n## Footer\n\nFooter text\n',
34+
'utf8'
35+
);
36+
});
37+
38+
it('should throw error if missing required arguments', async () => {
39+
await expect(runReadmeFooter(['--source', 'README.md'])).rejects.toThrow(
40+
'readme-footer requires --source <file> --footer <file> --dest <file>'
41+
);
42+
});
43+
});

packages/makage/jest.config.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
transform: {
6+
'^.+\\.tsx?$': [
7+
'ts-jest',
8+
{
9+
babelConfig: false,
10+
tsconfig: 'tsconfig.json',
11+
},
12+
],
13+
},
14+
transformIgnorePatterns: [`/node_modules/*`],
15+
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
16+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
17+
modulePathIgnorePatterns: ['dist/*'],
18+
};

0 commit comments

Comments
 (0)