Skip to content

Commit adf8f62

Browse files
committed
✨ Initial version
1 parent e74541c commit adf8f62

32 files changed

+7372
-0
lines changed

.eslintrc.cjs

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
module.exports = {
2+
root: true,
3+
extends: [
4+
'eslint:recommended',
5+
],
6+
parserOptions: {
7+
ecmaVersion: 2021,
8+
sourceType: 'module',
9+
},
10+
rules: {
11+
'comma-dangle': ['error', 'always-multiline'],
12+
indent: ['error', 'tab', { SwitchCase: 1 }],
13+
'no-console': 'warn',
14+
'no-warning-comments': ['warn', { terms: ['TODO', 'FIXME', 'XXX'], location: 'start' }],
15+
quotes: ['error', 'single'],
16+
'quote-props': ['error', 'as-needed'],
17+
semi: ['error', 'always'],
18+
},
19+
overrides: [
20+
{
21+
files: ['**/.eslintrc.js', '**/.eslintrc.cjs'],
22+
env: {
23+
node: true,
24+
},
25+
parserOptions: {
26+
ecmaVersion: 'latest',
27+
sourceType: 'module',
28+
},
29+
},
30+
{
31+
files: ['**/*.ts'],
32+
plugins: ['@typescript-eslint'],
33+
parser: '@typescript-eslint/parser',
34+
extends: ['plugin:@typescript-eslint/recommended'],
35+
rules: {
36+
'@typescript-eslint/member-delimiter-style': 'error',
37+
'@typescript-eslint/no-empty-interface': 'warn',
38+
'@typescript-eslint/no-explicit-any': 'error',
39+
'no-shadow': 'off',
40+
'@typescript-eslint/no-shadow': 'error',
41+
semi: 'off',
42+
'@typescript-eslint/semi': 'error',
43+
},
44+
},
45+
{
46+
files: ['**/*.ts'],
47+
plugins: ['@typescript-eslint'],
48+
parser: '@typescript-eslint/parser',
49+
extends: ['plugin:@typescript-eslint/recommended-requiring-type-checking'],
50+
rules: {
51+
'@typescript-eslint/switch-exhaustiveness-check': 'error',
52+
},
53+
parserOptions: {
54+
tsconfigRootDir: __dirname,
55+
project: ['./tsconfig.json', './examples/basic/tsconfig.json'],
56+
},
57+
},
58+
],
59+
};

.github/workflows/CI.yaml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
validate:
13+
runs-on: ubuntu-latest
14+
15+
strategy:
16+
matrix:
17+
node: [ current, lts/*, lts/-1 ]
18+
19+
steps:
20+
- uses: actions/checkout@v3
21+
22+
- name: Use Node.js (${{ matrix.node }})
23+
uses: actions/setup-node@v3
24+
with:
25+
node-version: ${{ matrix.node }}
26+
cache: 'npm'
27+
28+
- name: npm ci
29+
run: npm ci
30+
31+
- name: Validate
32+
run: npm run validate

LICENSE

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
ISC License
2+
3+
Copyright (c) 2022, @MethodGrab
4+
5+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

README.md

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# initializer-utils
2+
3+
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/MethodGrab/initializer-utils/CI?style=flat-square)](https://github.com/MethodGrab/initializer-utils/actions/workflows/CI.yaml)
4+
[![npm version](https://img.shields.io/npm/v/methodgrab/initializer-utils?style=flat-square)](https://www.npmjs.com/package/@methodgrab/initializer-utils)
5+
6+
> Utilities for creating [npm initializers](https://docs.npmjs.com/cli/commands/npm-init).
7+
8+
9+
## Usage
10+
11+
1.
12+
```
13+
npm install @methodgrab/initializer-utils --save
14+
````
15+
1. Import the utilities for creating your initializer CLI.
16+
```typescript
17+
// cli.ts
18+
import { askFor, copyFiles } from '@methodgrab/initializer-utils';
19+
20+
// ...
21+
```
22+
1. Import the utilities for testing your initializer CLI.
23+
```typescript
24+
// cli.test.ts
25+
import { fileExists, runCLI } from '@methodgrab/initializer-utils/testing';
26+
27+
// ...
28+
```
29+
30+
For a full example checkout the [examples](./examples/basic) folder.
31+
32+
33+
## Project Goals
34+
35+
What this is:
36+
37+
- :white_check_mark: A simple, lightweight, collection of utilities for building new project initializers.
38+
39+
What this is **not**:
40+
41+
- :x: A fully fledged generator/skaffolding tool.
42+
There are plenty of great tools like Yeoman & Hygen that already do this.
43+
44+
45+
## What's included
46+
47+
### `@methodgrab/initializer-utils`
48+
49+
These are utilities to help you _create_ your initializer.
50+
51+
52+
#### `askFor`
53+
54+
Use interactive prompts to gather information from a user.
55+
56+
57+
##### `validateAll`, `validator`, `required`, `minLength`, `maxLength`
58+
59+
These are validation helpers that can be used with the `validate` property in `askFor` prompts.
60+
61+
62+
#### `copyFiles`, `copyFile`
63+
64+
Copy a directory of templates, or a single template, to the CWD the user ran the initializer in.
65+
66+
Variables defined using curly braces (`{{ foo }}`) will be replaced with the values in the supplied `data` object.
67+
When combined with `askFor` this lets you easily include the users answers in the copied files.
68+
69+
70+
### `@methodgrab/initializer-utils/testing`
71+
72+
These are utilities to help you _test_ your initializer.
73+
74+
#### `runCLI`
75+
76+
This runs your initializer (by default in a temp directory) with any prompt answers you specify.
77+
78+
79+
#### `fileExists`
80+
81+
This is a very basic utility to assert that a file in the output directory `runCLI` ran in exists.

ava.config.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export default {
2+
extensions: {
3+
ts: 'module',
4+
},
5+
nodeArguments: [
6+
'--loader=ts-node/esm',
7+
],
8+
files: [
9+
'src/**/*.test.ts',
10+
'examples/**/src/**/*.test.ts',
11+
],
12+
// Disabling worker threads is required to use process.chdir() otherwise you get:
13+
// TypeError { code: 'ERR_WORKER_UNSUPPORTED_OPERATION', message: 'process.chdir() is not supported in workers' }
14+
// https://github.com/avajs/ava/issues/2956#issuecomment-1023770164
15+
workerThreads: false,
16+
};

examples/basic/src/cli.test.ts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { readFile } from 'node:fs/promises';
2+
import { join } from 'node:path';
3+
import anyTest, { TestFn } from 'ava';
4+
import { fileExists, runCLI } from '@methodgrab/initializer-utils/testing';
5+
import { Answers } from './cli.js';
6+
7+
const test = anyTest as TestFn<Context>;
8+
9+
type Context = {
10+
result: Awaited<ReturnType<typeof runCLI>>;
11+
};
12+
13+
const answers: Answers = {
14+
foo: 'test-foo',
15+
bar: 'test-bar',
16+
baz: 'test-baz',
17+
};
18+
19+
const run = async () =>
20+
await runCLI(new URL('../dist/cli.js', import.meta.url), {
21+
answers: Object.values(answers),
22+
});
23+
24+
test.before('Run the CLI', async t => {
25+
t.context.result = await run();
26+
});
27+
28+
test.after.always('Clean up temp files', async t => {
29+
if (t.context.result && typeof t.context.result.cleanup === 'function') {
30+
await t.context.result.cleanup();
31+
}
32+
});
33+
34+
test('it creates the expected files', async t => {
35+
const files = [
36+
'foo.json',
37+
'bar.json',
38+
'baz.json',
39+
];
40+
41+
for (const file of files) {
42+
t.true(await fileExists(t.context.result.outputDir, file), `Could not find \`${file}\` in the output dir.`);
43+
}
44+
});
45+
46+
test('it replaces the template variables in the files with the prompt answers', async t => {
47+
const foo = (await readFileJSON(outputFile(t.context, 'foo.json'))) as Record<string, unknown>;
48+
t.is(foo.name, answers.foo);
49+
50+
const bar = (await readFileJSON(outputFile(t.context, 'bar.json'))) as Record<string, unknown>;
51+
t.is(bar.name, answers.bar);
52+
53+
const baz = (await readFileJSON(outputFile(t.context, 'baz.json'))) as Record<string, unknown>;
54+
t.is(baz.name, answers.baz);
55+
t.is(baz.custom, true);
56+
});
57+
58+
const outputFile = (context: Context, file: string): string =>
59+
join(context.result.outputDir, file);
60+
61+
const readFileString = async (path: string): Promise<string> =>
62+
await readFile(path, 'utf-8');
63+
64+
const readFileJSON = async (path: string): Promise<unknown> =>
65+
JSON.parse(await readFileString(path)) as unknown;

examples/basic/src/cli.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env node
2+
3+
import { URL } from 'node:url';
4+
import { type Answers as CreateAnswers, askFor, definePrompts, copyFile, copyFiles, required } from '@methodgrab/initializer-utils';
5+
6+
const prompts = definePrompts({
7+
foo: {
8+
type: 'string',
9+
message: 'Foo name?',
10+
validate: required(),
11+
},
12+
bar: {
13+
type: 'string',
14+
message: 'Bar name?',
15+
validate: required(),
16+
},
17+
baz: {
18+
type: 'string',
19+
message: 'Baz name?',
20+
default: 'default baz name',
21+
},
22+
});
23+
24+
export type Answers = CreateAnswers<typeof prompts>;
25+
26+
try {
27+
const answers = await askFor(prompts);
28+
29+
await copyFiles({
30+
from: new URL('../templates/base', import.meta.url),
31+
data: answers,
32+
});
33+
34+
const bazFile = answers.baz === prompts.baz.default ? 'default' : 'custom';
35+
36+
await copyFile({
37+
from: new URL(`../templates/baz/baz-${bazFile}.json`, import.meta.url),
38+
to: 'baz.json',
39+
data: answers,
40+
});
41+
} catch (error) {
42+
const message = error instanceof Error ? error.message : 'Unknown error';
43+
// eslint-disable-next-line no-console
44+
console.error(message);
45+
process.exitCode = 1;
46+
}
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "{{bar}}",
3+
"version": "1.0.0"
4+
}
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "{{foo}}",
3+
"version": "1.0.0"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "{{baz}}",
3+
"version": "1.0.0",
4+
"custom": true
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "{{baz}}",
3+
"version": "1.0.0",
4+
"custom": false
5+
}

examples/basic/tsconfig.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../../tsconfig.opts.json",
3+
"compilerOptions": {
4+
"outDir": "dist",
5+
"rootDir": "./src"
6+
},
7+
"include": ["src/**/*.ts"],
8+
"exclude": [
9+
"**/dist/*",
10+
"**/node_modules/*"
11+
]
12+
}

0 commit comments

Comments
 (0)