create-packkit 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,175 @@
1
+ export default {
2
+ id: 'community',
3
+ active: (cfg) => cfg.license !== 'none' || cfg.community,
4
+ apply(cfg) {
5
+ const files = {};
6
+ const year = new Date().getFullYear();
7
+ const holder = cfg.author || 'the authors';
8
+
9
+ if (cfg.license === 'MIT') files['LICENSE'] = mit(year, holder);
10
+ else if (cfg.license === 'ISC') files['LICENSE'] = isc(year, holder);
11
+ else if (cfg.license === 'Apache-2.0') files['LICENSE'] = apacheStub(year, holder);
12
+
13
+ if (cfg.community) {
14
+ files['CONTRIBUTING.md'] = contributing(cfg);
15
+ files['CODE_OF_CONDUCT.md'] = codeOfConduct(cfg);
16
+ files['SECURITY.md'] = security();
17
+ files['.github/ISSUE_TEMPLATE/bug_report.md'] = bugReport();
18
+ files['.github/ISSUE_TEMPLATE/feature_request.md'] = featureRequest();
19
+ files['.github/PULL_REQUEST_TEMPLATE.md'] = prTemplate();
20
+ }
21
+
22
+ return { files, pkg: {} };
23
+ },
24
+ };
25
+
26
+ function mit(year, holder) {
27
+ return `MIT License
28
+
29
+ Copyright (c) ${year} ${holder}
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining a copy
32
+ of this software and associated documentation files (the "Software"), to deal
33
+ in the Software without restriction, including without limitation the rights
34
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
35
+ copies of the Software, and to permit persons to whom the Software is
36
+ furnished to do so, subject to the following conditions:
37
+
38
+ The above copyright notice and this permission notice shall be included in all
39
+ copies or substantial portions of the Software.
40
+
41
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
42
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
43
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
44
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
45
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
46
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
47
+ SOFTWARE.
48
+ `;
49
+ }
50
+
51
+ function isc(year, holder) {
52
+ return `ISC License
53
+
54
+ Copyright (c) ${year} ${holder}
55
+
56
+ Permission to use, copy, modify, and/or distribute this software for any
57
+ purpose with or without fee is hereby granted, provided that the above
58
+ copyright notice and this permission notice appear in all copies.
59
+
60
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
61
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
62
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
63
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
64
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
65
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
66
+ PERFORMANCE OF THIS SOFTWARE.
67
+ `;
68
+ }
69
+
70
+ function apacheStub(year, holder) {
71
+ return `Copyright ${year} ${holder}
72
+
73
+ Licensed under the Apache License, Version 2.0 (the "License");
74
+ you may not use this file except in compliance with the License.
75
+ You may obtain a copy of the License at
76
+
77
+ http://www.apache.org/licenses/LICENSE-2.0
78
+
79
+ Unless required by applicable law or agreed to in writing, software
80
+ distributed under the License is distributed on an "AS IS" BASIS,
81
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
82
+ See the License for the specific language governing permissions and
83
+ limitations under the License.
84
+
85
+ Full text: https://www.apache.org/licenses/LICENSE-2.0.txt
86
+ `;
87
+ }
88
+
89
+ function contributing(cfg) {
90
+ const pm = cfg.packageManager;
91
+ const install = pm === 'npm' ? 'npm install' : `${pm} install`;
92
+ return `# Contributing
93
+
94
+ Thanks for your interest in contributing!
95
+
96
+ ## Development
97
+
98
+ \`\`\`sh
99
+ ${install}
100
+ ${pm === 'npm' ? 'npm test' : pm + ' test'}
101
+ \`\`\`
102
+
103
+ ## Pull requests
104
+
105
+ - Create a branch, make your change, and open a PR against \`main\`.
106
+ - Make sure lint, types and tests pass.
107
+ ${cfg.release === 'changesets' ? '- Run `npx changeset` to describe your change for the changelog.\n' : ''}
108
+ `;
109
+ }
110
+
111
+ function codeOfConduct() {
112
+ return `# Code of Conduct
113
+
114
+ This project follows the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
115
+ By participating, you are expected to uphold this code. Report unacceptable
116
+ behavior to the maintainers.
117
+ `;
118
+ }
119
+
120
+ function security() {
121
+ return `# Security Policy
122
+
123
+ If you discover a security vulnerability, please **do not** open a public issue.
124
+ Instead, report it privately to the maintainers (e.g. via GitHub Security
125
+ Advisories). We will respond as quickly as possible.
126
+ `;
127
+ }
128
+
129
+ function bugReport() {
130
+ return `---
131
+ name: Bug report
132
+ about: Report a problem
133
+ labels: bug
134
+ ---
135
+
136
+ **Describe the bug**
137
+
138
+ **To reproduce**
139
+
140
+ **Expected behavior**
141
+
142
+ **Environment**
143
+ - Version:
144
+ - Node:
145
+ - OS:
146
+ `;
147
+ }
148
+
149
+ function featureRequest() {
150
+ return `---
151
+ name: Feature request
152
+ about: Suggest an idea
153
+ labels: enhancement
154
+ ---
155
+
156
+ **Problem**
157
+
158
+ **Proposed solution**
159
+
160
+ **Alternatives considered**
161
+ `;
162
+ }
163
+
164
+ function prTemplate() {
165
+ return `## Summary
166
+
167
+ <!-- What does this change and why? -->
168
+
169
+ ## Checklist
170
+
171
+ - [ ] Tests pass
172
+ - [ ] Lint & types pass
173
+ - [ ] Docs updated if needed
174
+ `;
175
+ }
@@ -0,0 +1,50 @@
1
+ // .gitignore, .editorconfig, .npmrc — always-on repo hygiene.
2
+
3
+ const GITIGNORE = `# dependencies
4
+ node_modules/
5
+ .pnp.*
6
+ .yarn/*
7
+
8
+ # build output
9
+ dist/
10
+ coverage/
11
+ *.tsbuildinfo
12
+
13
+ # logs
14
+ *.log
15
+ npm-debug.log*
16
+
17
+ # env & editor
18
+ .env
19
+ .env.*
20
+ !.env.example
21
+ .DS_Store
22
+ .vscode/*
23
+ !.vscode/settings.json
24
+ !.vscode/extensions.json
25
+ .idea/
26
+ `;
27
+
28
+ const EDITORCONFIG = `root = true
29
+
30
+ [*]
31
+ charset = utf-8
32
+ end_of_line = lf
33
+ insert_final_newline = true
34
+ trim_trailing_whitespace = true
35
+ indent_style = tab
36
+
37
+ [*.{json,yml,yaml,md}]
38
+ indent_style = space
39
+ indent_size = 2
40
+ `;
41
+
42
+ export default {
43
+ id: 'gitfiles',
44
+ active: () => true,
45
+ apply(cfg) {
46
+ const files = { '.gitignore': GITIGNORE };
47
+ if (cfg.editorconfig) files['.editorconfig'] = EDITORCONFIG;
48
+ return { files, pkg: {} };
49
+ },
50
+ };
@@ -0,0 +1,59 @@
1
+ export default {
2
+ id: 'githooks',
3
+ active: (cfg) => cfg.gitHooks !== 'none',
4
+ apply(cfg) {
5
+ const files = {};
6
+ const pkg = { scripts: {}, devDependencies: {} };
7
+ const codeGlob = cfg.isTs ? '*.{js,ts}' : '*.js';
8
+
9
+ // What runs on staged files depends on the linter choice.
10
+ const staged =
11
+ cfg.lint === 'biome'
12
+ ? { [codeGlob]: 'biome check --write --no-errors-on-unmatched' }
13
+ : cfg.lint === 'eslint-prettier'
14
+ ? { [codeGlob]: ['prettier --write', 'eslint --fix'], '*.{json,md,yml}': 'prettier --write' }
15
+ : cfg.lint === 'oxlint'
16
+ ? { [codeGlob]: ['prettier --write', 'oxlint'], '*.{json,md,yml}': 'prettier --write' }
17
+ : null;
18
+
19
+ const needsLintStaged = cfg.gitHooks !== 'lefthook' && staged;
20
+
21
+ if (cfg.gitHooks === 'simple-git-hooks') {
22
+ pkg['simple-git-hooks'] = { 'pre-commit': needsLintStaged ? 'npx lint-staged' : 'npm test' };
23
+ pkg.scripts.prepare = 'simple-git-hooks';
24
+ pkg.devDependencies['simple-git-hooks'] = '^2.11.0';
25
+ } else if (cfg.gitHooks === 'husky') {
26
+ files['.husky/pre-commit'] = (needsLintStaged ? 'npx lint-staged\n' : 'npm test\n');
27
+ pkg.scripts.prepare = 'husky';
28
+ pkg.devDependencies.husky = '^9.1.0';
29
+ } else if (cfg.gitHooks === 'lefthook') {
30
+ files['lefthook.yml'] = lefthookYml(cfg, staged);
31
+ pkg.scripts.prepare = 'lefthook install';
32
+ pkg.devDependencies.lefthook = '^1.7.0';
33
+ }
34
+
35
+ if (needsLintStaged) {
36
+ pkg['lint-staged'] = staged;
37
+ pkg.devDependencies['lint-staged'] = '^15.2.0';
38
+ }
39
+
40
+ return { files, pkg };
41
+ },
42
+ };
43
+
44
+ function lefthookYml(cfg, staged) {
45
+ const cmd =
46
+ cfg.lint === 'biome'
47
+ ? 'biome check --write --no-errors-on-unmatched {staged_files}'
48
+ : cfg.lint === 'none'
49
+ ? 'npm test'
50
+ : 'prettier --write {staged_files}';
51
+ return [
52
+ 'pre-commit:',
53
+ ' commands:',
54
+ ' format:',
55
+ ` glob: '*.{js,ts,json,md}'`,
56
+ ` run: ${cmd}`,
57
+ '',
58
+ ].join('\n');
59
+ }
@@ -0,0 +1,32 @@
1
+ // Feature registry. Order matters only for how package.json fragments merge
2
+ // (later features deep-merge over earlier ones); files are keyed by path.
3
+
4
+ import meta from './meta.js';
5
+ import bundler from './bundler.js';
6
+ import typescript from './typescript.js';
7
+ import test from './test.js';
8
+ import lint from './lint.js';
9
+ import githooks from './githooks.js';
10
+ import release from './release.js';
11
+ import cli from './cli.js';
12
+ import workflows from './workflows.js';
13
+ import community from './community.js';
14
+ import agents from './agents.js';
15
+ import vscode from './vscode.js';
16
+ import gitfiles from './gitfiles.js';
17
+
18
+ export default [
19
+ meta,
20
+ bundler,
21
+ typescript,
22
+ test,
23
+ lint,
24
+ githooks,
25
+ release,
26
+ cli,
27
+ workflows,
28
+ community,
29
+ agents,
30
+ vscode,
31
+ gitfiles,
32
+ ];
@@ -0,0 +1,76 @@
1
+ import { toJson } from '../render.js';
2
+
3
+ export default {
4
+ id: 'lint',
5
+ active: (cfg) => cfg.lint !== 'none',
6
+ apply(cfg) {
7
+ const files = {};
8
+ const pkg = { scripts: {}, devDependencies: {} };
9
+
10
+ if (cfg.lint === 'eslint-prettier' || cfg.lint === 'oxlint') {
11
+ // Prettier is shared by both.
12
+ files['.prettierrc.json'] = toJson({
13
+ useTabs: true,
14
+ singleQuote: true,
15
+ semi: true,
16
+ printWidth: 100,
17
+ trailingComma: 'all',
18
+ });
19
+ files['.prettierignore'] = 'dist\ncoverage\n';
20
+ pkg.scripts.format = 'prettier --write .';
21
+ pkg.scripts['format:check'] = 'prettier --check .';
22
+ pkg.devDependencies.prettier = '^3.3.0';
23
+ }
24
+
25
+ if (cfg.lint === 'eslint-prettier') {
26
+ files['eslint.config.js'] = eslintFlatConfig(cfg);
27
+ pkg.scripts.lint = 'eslint .';
28
+ pkg.scripts['lint:fix'] = 'eslint . --fix';
29
+ pkg.devDependencies.eslint = '^9.0.0';
30
+ pkg.devDependencies['@eslint/js'] = '^9.0.0';
31
+ if (cfg.isTs) pkg.devDependencies['typescript-eslint'] = '^8.0.0';
32
+ } else if (cfg.lint === 'oxlint') {
33
+ pkg.scripts.lint = 'oxlint';
34
+ pkg.devDependencies.oxlint = '^0.9.0';
35
+ } else if (cfg.lint === 'biome') {
36
+ files['biome.json'] = toJson({
37
+ $schema: 'https://biomejs.dev/schemas/1.8.0/schema.json',
38
+ formatter: { enabled: true, indentStyle: 'tab', lineWidth: 100 },
39
+ linter: { enabled: true, rules: { recommended: true } },
40
+ javascript: { formatter: { quoteStyle: 'single', trailingCommas: 'all' } },
41
+ });
42
+ pkg.scripts.lint = 'biome check .';
43
+ pkg.scripts['lint:fix'] = 'biome check --write .';
44
+ pkg.scripts.format = 'biome format --write .';
45
+ pkg.devDependencies['@biomejs/biome'] = '^1.8.0';
46
+ }
47
+
48
+ return { files, pkg };
49
+ },
50
+ };
51
+
52
+ function eslintFlatConfig(cfg) {
53
+ if (cfg.isTs) {
54
+ return [
55
+ `import js from '@eslint/js';`,
56
+ `import tseslint from 'typescript-eslint';`,
57
+ ``,
58
+ `export default tseslint.config(`,
59
+ `\tjs.configs.recommended,`,
60
+ `\t...tseslint.configs.recommended,`,
61
+ `\t{ ignores: ['dist', 'coverage'] },`,
62
+ `);`,
63
+ ``,
64
+ ].join('\n');
65
+ }
66
+ return [
67
+ `import js from '@eslint/js';`,
68
+ ``,
69
+ `export default [`,
70
+ `\tjs.configs.recommended,`,
71
+ `\t{ languageOptions: { ecmaVersion: 'latest', sourceType: 'module' } },`,
72
+ `\t{ ignores: ['dist', 'coverage'] },`,
73
+ `];`,
74
+ ``,
75
+ ].join('\n');
76
+ }
@@ -0,0 +1,99 @@
1
+ // Always-on: base package.json descriptive fields + the source entry + README.
2
+
3
+ export default {
4
+ id: 'meta',
5
+ active: () => true,
6
+ apply(cfg) {
7
+ const files = {};
8
+ const pkg = {
9
+ name: cfg.name,
10
+ version: '0.0.0',
11
+ description: cfg.description || '',
12
+ type: cfg.moduleFormat === 'cjs' ? 'commonjs' : 'module',
13
+ engines: { node: `>=${cfg.nodeVersion}` },
14
+ scripts: {},
15
+ };
16
+
17
+ const kw = String(cfg.keywords || '').split(',').map((s) => s.trim()).filter(Boolean);
18
+ if (kw.length) pkg.keywords = kw;
19
+ if (cfg.author) pkg.author = cfg.author;
20
+ if (cfg.repo) {
21
+ pkg.repository = { type: 'git', url: `git+${cfg.repo.replace(/\.git$/, '')}.git` };
22
+ pkg.bugs = { url: `${cfg.repo.replace(/\.git$/, '')}/issues` };
23
+ pkg.homepage = `${cfg.repo.replace(/\.git$/, '')}#readme`;
24
+ }
25
+
26
+ // Source entry point.
27
+ const ext = cfg.ext;
28
+ files[`src/index.${ext}`] = cfg.hasLibrary
29
+ ? libraryEntry(cfg)
30
+ : `// ${cfg.name}\n`;
31
+
32
+ // README
33
+ files['README.md'] = readme(cfg);
34
+
35
+ // Node version pin
36
+ files['.nvmrc'] = `${cfg.nodeVersion}\n`;
37
+
38
+ // A typecheck script for TS projects regardless of bundler.
39
+ if (cfg.isTs) pkg.scripts.typecheck = 'tsc --noEmit';
40
+
41
+ return { files, pkg };
42
+ },
43
+ };
44
+
45
+ function libraryEntry(cfg) {
46
+ if (cfg.isTs) {
47
+ return [
48
+ `/** Greet someone by name. */`,
49
+ `export function greet(name: string): string {`,
50
+ `\treturn \`Hello, \${name}!\`;`,
51
+ `}`,
52
+ ``,
53
+ ].join('\n');
54
+ }
55
+ return [
56
+ `/**`,
57
+ ` * Greet someone by name.`,
58
+ ` * @param {string} name`,
59
+ ` * @returns {string}`,
60
+ ` */`,
61
+ `export function greet(name) {`,
62
+ `\treturn \`Hello, \${name}!\`;`,
63
+ `}`,
64
+ ``,
65
+ ].join('\n');
66
+ }
67
+
68
+ function readme(cfg) {
69
+ const install = {
70
+ npm: `npm install ${cfg.name}`,
71
+ pnpm: `pnpm add ${cfg.name}`,
72
+ yarn: `yarn add ${cfg.name}`,
73
+ bun: `bun add ${cfg.name}`,
74
+ }[cfg.packageManager];
75
+
76
+ const lines = [
77
+ `# ${cfg.name}`,
78
+ '',
79
+ cfg.description || '_A modern package scaffolded with [Packkit](https://danmat.github.io/create-packkit/)._',
80
+ '',
81
+ '## Install',
82
+ '',
83
+ '```sh',
84
+ install,
85
+ '```',
86
+ '',
87
+ ];
88
+
89
+ if (cfg.hasLibrary) {
90
+ const imp = cfg.isTs || cfg.hasEsm ? `import { greet } from '${cfg.name}';` : `const { greet } = require('${cfg.name}');`;
91
+ lines.push('## Usage', '', '```' + (cfg.isTs ? 'ts' : 'js'), imp, '', `greet('world'); // "Hello, world!"`, '```', '');
92
+ }
93
+ if (cfg.hasCli) {
94
+ lines.push('## CLI', '', '```sh', `npx ${cfg.name} --help`, '```', '');
95
+ }
96
+
97
+ lines.push('## License', '', cfg.license === 'none' ? 'Unlicensed.' : `${cfg.license}${cfg.author ? ' © ' + cfg.author : ''}`, '');
98
+ return lines.join('\n');
99
+ }
@@ -0,0 +1,44 @@
1
+ import { toJson } from '../render.js';
2
+
3
+ export default {
4
+ id: 'release',
5
+ active: (cfg) => cfg.release !== 'none',
6
+ apply(cfg) {
7
+ const files = {};
8
+ const pkg = { scripts: {}, devDependencies: {} };
9
+
10
+ if (cfg.release === 'changesets') {
11
+ files['.changeset/config.json'] = toJson({
12
+ $schema: 'https://unpkg.com/@changesets/config@3.0.0/schema.json',
13
+ changelog: '@changesets/cli/changelog',
14
+ commit: false,
15
+ access: 'public',
16
+ baseBranch: 'main',
17
+ updateInternalDependencies: 'patch',
18
+ });
19
+ files['.changeset/README.md'] =
20
+ '# Changesets\n\nRun `npx changeset` to record a version bump for your next release.\n';
21
+ pkg.scripts.changeset = 'changeset';
22
+ pkg.scripts.version = 'changeset version';
23
+ pkg.scripts.release = `${buildThen(cfg)}changeset publish`;
24
+ pkg.devDependencies['@changesets/cli'] = '^2.27.0';
25
+ } else if (cfg.release === 'release-it') {
26
+ files['.release-it.json'] = toJson({
27
+ git: { commitMessage: 'chore: release v${version}' },
28
+ github: { release: true },
29
+ npm: { publish: true },
30
+ });
31
+ pkg.scripts.release = 'release-it';
32
+ pkg.devDependencies['release-it'] = '^17.0.0';
33
+ } else if (cfg.release === 'np') {
34
+ pkg.scripts.release = 'np';
35
+ pkg.devDependencies.np = '^10.0.0';
36
+ }
37
+
38
+ return { files, pkg };
39
+ },
40
+ };
41
+
42
+ function buildThen(cfg) {
43
+ return cfg.bundler !== 'none' || cfg.isTs ? 'npm run build && ' : '';
44
+ }
@@ -0,0 +1,104 @@
1
+ import { toJson } from '../render.js';
2
+
3
+ export default {
4
+ id: 'test',
5
+ active: (cfg) => cfg.test !== 'none',
6
+ apply(cfg) {
7
+ const files = {};
8
+ const pkg = { scripts: {}, devDependencies: {} };
9
+ const ext = cfg.ext;
10
+
11
+ if (cfg.test === 'vitest') {
12
+ files[`vitest.config.${ext}`] = [
13
+ `import { defineConfig } from 'vitest/config';`,
14
+ ``,
15
+ `export default defineConfig({`,
16
+ `\ttest: {`,
17
+ cfg.coverage ? `\t\tcoverage: { provider: 'v8', reporter: ['text', 'lcov'] },` : null,
18
+ `\t},`,
19
+ `});`,
20
+ ``,
21
+ ].filter((l) => l !== null).join('\n');
22
+ pkg.scripts.test = 'vitest run';
23
+ pkg.scripts['test:watch'] = 'vitest';
24
+ pkg.devDependencies.vitest = '^2.0.0';
25
+ if (cfg.coverage) {
26
+ pkg.scripts.coverage = 'vitest run --coverage';
27
+ pkg.devDependencies['@vitest/coverage-v8'] = '^2.0.0';
28
+ }
29
+ files[`src/index.test.${ext}`] = exampleTest('vitest', cfg);
30
+ } else if (cfg.test === 'jest') {
31
+ files['jest.config.js'] = jestConfig(cfg);
32
+ pkg.scripts.test = 'jest';
33
+ pkg.scripts['test:watch'] = 'jest --watch';
34
+ pkg.devDependencies.jest = '^29.0.0';
35
+ if (cfg.isTs) {
36
+ pkg.devDependencies['ts-jest'] = '^29.0.0';
37
+ pkg.devDependencies['@types/jest'] = '^29.0.0';
38
+ }
39
+ if (cfg.coverage) pkg.scripts.coverage = 'jest --coverage';
40
+ files[`src/index.test.${ext}`] = exampleTest('jest', cfg);
41
+ } else if (cfg.test === 'node') {
42
+ pkg.scripts.test = cfg.isTs ? 'node --import tsx --test "src/**/*.test.ts"' : 'node --test';
43
+ if (cfg.isTs) pkg.devDependencies.tsx = '^4.0.0';
44
+ files[`src/index.test.${ext}`] = exampleTest('node', cfg);
45
+ }
46
+
47
+ return { files, pkg };
48
+ },
49
+ };
50
+
51
+ function importPath(runner, cfg) {
52
+ if (!cfg.isTs) return './index.js';
53
+ if (runner === 'node') return './index.ts';
54
+ return './index.js';
55
+ }
56
+
57
+ function exampleTest(runner, cfg) {
58
+ const imp = importPath(runner, cfg);
59
+ if (runner === 'node') {
60
+ return [
61
+ `import { test } from 'node:test';`,
62
+ `import assert from 'node:assert/strict';`,
63
+ `import { greet } from '${imp}';`,
64
+ ``,
65
+ `test('greet', () => {`,
66
+ `\tassert.equal(greet('world'), 'Hello, world!');`,
67
+ `});`,
68
+ ``,
69
+ ].join('\n');
70
+ }
71
+ const api = runner === 'jest' ? `` : `import { describe, it, expect } from 'vitest';\n`;
72
+ const expectApi = runner === 'jest' ? '' : '';
73
+ return [
74
+ api + `import { greet } from '${imp}';`,
75
+ ``,
76
+ `describe('greet', () => {`,
77
+ `\tit('greets by name', () => {`,
78
+ `\t\texpect(greet('world')).toBe('Hello, world!');`,
79
+ `\t});`,
80
+ `});`,
81
+ ``,
82
+ ].join('\n');
83
+ }
84
+
85
+ function jestConfig(cfg) {
86
+ if (cfg.isTs) {
87
+ return [
88
+ `/** @type {import('jest').Config} */`,
89
+ `export default {`,
90
+ `\tpreset: 'ts-jest/presets/default-esm',`,
91
+ `\ttestEnvironment: 'node',`,
92
+ `\textensionsToTreatAsEsm: ['.ts'],`,
93
+ `};`,
94
+ ``,
95
+ ].join('\n');
96
+ }
97
+ return [
98
+ `/** @type {import('jest').Config} */`,
99
+ `export default {`,
100
+ `\ttestEnvironment: 'node',`,
101
+ `};`,
102
+ ``,
103
+ ].join('\n');
104
+ }
@@ -0,0 +1,48 @@
1
+ import { toJson } from '../render.js';
2
+
3
+ export default {
4
+ id: 'typescript',
5
+ active: (cfg) => cfg.isTs,
6
+ apply(cfg) {
7
+ const noBuild = cfg.bundler === 'none';
8
+ const compilerOptions = {
9
+ target: 'ES2022',
10
+ module: 'ESNext',
11
+ moduleResolution: 'Bundler',
12
+ lib: ['ES2022'],
13
+ strict: true,
14
+ noUncheckedIndexedAccess: true,
15
+ esModuleInterop: true,
16
+ skipLibCheck: true,
17
+ forceConsistentCasingInFileNames: true,
18
+ verbatimModuleSyntax: cfg.bundler !== 'none',
19
+ declaration: true,
20
+ };
21
+ if (noBuild) {
22
+ // tsc is the build: emit to dist.
23
+ compilerOptions.moduleResolution = 'NodeNext';
24
+ compilerOptions.module = 'NodeNext';
25
+ compilerOptions.outDir = 'dist';
26
+ compilerOptions.rootDir = 'src';
27
+ } else {
28
+ compilerOptions.noEmit = true;
29
+ }
30
+
31
+ return {
32
+ files: {
33
+ 'tsconfig.json': toJson({
34
+ $schema: 'https://json.schemastore.org/tsconfig',
35
+ compilerOptions,
36
+ include: ['src'],
37
+ exclude: ['dist', 'node_modules'],
38
+ }),
39
+ },
40
+ pkg: {
41
+ devDependencies: {
42
+ typescript: '^5.5.0',
43
+ '@types/node': `^${cfg.nodeVersion}.0.0`,
44
+ },
45
+ },
46
+ };
47
+ },
48
+ };