@doikayt/typescript-build-config 0.1.14 → 0.1.15

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.
package/README.md CHANGED
@@ -110,6 +110,8 @@ small single-purpose scripts, and the check side mirrors the write side:
110
110
  | Script | What it does |
111
111
  | --- | --- |
112
112
  | `ci` | The release gate: `check-all-format` then `test` (plus `test:e2e` for UI). |
113
+ | `build` | Compile TypeScript to `dist/` (`tsc`). |
114
+ | `prepack` | Runs `build` before packing, so `changeset publish` ships compiled `dist/`. |
113
115
  | `test` | Unit / integration tests (`vitest run`). |
114
116
  | `test:e2e` | End-to-end tests (`playwright test`) — UI projects only. |
115
117
  | `update-all-format` | Reformat everything: code + markdown docs (write). |
@@ -117,6 +119,12 @@ small single-purpose scripts, and the check side mirrors the write side:
117
119
  | `update-code-formatting` / `check-code-formatting` | Prettier write / check. |
118
120
  | `update-markdown-docs` / `check-markdown-docs` | Regenerate / verify generated markdown (TOC, UML, …). |
119
121
 
122
+ Alongside the scripts, `init` sets the publish config a fresh package needs —
123
+ `type: "module"`, `main` / `types` / `exports` pointing at `dist/index.js`, and
124
+ `files: ["dist"]` — each only if you have not already set it. Combined with
125
+ `prepack`, a brand-new project builds and publishes a compiled, typed ESM
126
+ package on push with no extra wiring.
127
+
120
128
  ### Console / CLI project
121
129
 
122
130
  ```bash
@@ -126,9 +134,10 @@ npx @doikayt/typescript-build-config init # answer "n" to the Playwr
126
134
  npm install # fetch the declared devDependencies
127
135
  ```
128
136
 
129
- `init` writes the canonical scripts (`ci`, `test`, `update-all-format`,
130
- `check-all-format`, and their sub-tasks), declares `vitest` +
131
- `@doikayt/autogen-markdown-doc`, and seeds a commented `vitest.config.ts`.
137
+ `init` writes the canonical scripts (`ci`, `build`, `test`, `update-all-format`,
138
+ `check-all-format`, `prepack`, and their sub-tasks), sets the `dist/`-based
139
+ publish config, declares `vitest` + `@doikayt/autogen-markdown-doc` +
140
+ `typescript`, and seeds a commented `vitest.config.ts`.
132
141
 
133
142
  ### UI / web project (with Playwright e2e)
134
143
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doikayt/typescript-build-config",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Shared build configuration for TypeScript projects — ESLint, Prettier, tsconfig, and Playwright presets",
5
5
  "_comments": {
6
6
  "exports": "This block lists the config files that will be extended by files in in src/toplevel - which are copied to consuming project root folder. An example: eslint.config.js imports from '@doikayt/typescript-build-config/eslint'.",
@@ -28,8 +28,8 @@
28
28
  "postinstall": "node src/postinstall.js",
29
29
  "update-all-format": "prettier --write src/ && update-markdown-toc README.md",
30
30
  "check-format": "prettier --check src/ && update-markdown-toc --check README.md",
31
- "ci": "npm run check-format && node --test",
32
- "test": "node --test"
31
+ "ci": "npm run check-format && node --test tests/*.test.js",
32
+ "test": "node --test tests/*.test.js"
33
33
  },
34
34
  "publishConfig": {
35
35
  "access": "public"
@@ -11,6 +11,7 @@ export function canonicalScripts({ ui = false } = {}) {
11
11
 
12
12
  const scripts = {
13
13
  ci,
14
+ build: "tsc",
14
15
  test: "vitest run",
15
16
  "update-all-format":
16
17
  "npm run update-code-formatting && npm run update-markdown-docs",
@@ -20,6 +21,9 @@ export function canonicalScripts({ ui = false } = {}) {
20
21
  "npm run check-code-formatting && npm run check-markdown-docs",
21
22
  "check-code-formatting": "prettier --check src/",
22
23
  "check-markdown-docs": "autogen-markdown-doc check",
24
+ // Build at publish time so `changeset publish` ships compiled dist/ without
25
+ // the release workflow needing a build step.
26
+ prepack: "npm run build",
23
27
  };
24
28
 
25
29
  if (ui) scripts["test:e2e"] = "doikayt-playwright-install && playwright test";
@@ -28,7 +32,26 @@ export function canonicalScripts({ ui = false } = {}) {
28
32
  }
29
33
 
30
34
  export function devDependencyNames({ ui = false } = {}) {
31
- const names = ["vitest", "@doikayt/autogen-markdown-doc"];
35
+ const names = ["vitest", "@doikayt/autogen-markdown-doc", "typescript"];
32
36
  if (ui) names.push("@playwright/test");
33
37
  return names;
34
38
  }
39
+
40
+ // Top-level package.json fields init sets (non-destructively) so a fresh project
41
+ // builds to dist/ and publishes a compiled, importable ESM package. Assumes the
42
+ // canonical src/index.ts entry point; a consumer that wants a different shape
43
+ // sets these first and init leaves them untouched.
44
+ export function packageFields() {
45
+ return {
46
+ type: "module",
47
+ main: "dist/index.js",
48
+ types: "dist/index.d.ts",
49
+ exports: {
50
+ ".": {
51
+ types: "./dist/index.d.ts",
52
+ default: "./dist/index.js",
53
+ },
54
+ },
55
+ files: ["dist"],
56
+ };
57
+ }
package/src/init.js CHANGED
@@ -1,8 +1,19 @@
1
- import { readFileSync, writeFileSync, existsSync, copyFileSync } from "fs";
1
+ import {
2
+ readFileSync,
3
+ writeFileSync,
4
+ existsSync,
5
+ copyFileSync,
6
+ mkdirSync,
7
+ readdirSync,
8
+ } from "fs";
2
9
  import { resolve } from "path";
3
10
  import { fileURLToPath } from "url";
4
11
  import { createInterface } from "readline";
5
- import { canonicalScripts, devDependencyNames } from "./canonical-scripts.js";
12
+ import {
13
+ canonicalScripts,
14
+ devDependencyNames,
15
+ packageFields,
16
+ } from "./canonical-scripts.js";
6
17
  import { applyToPackageJson } from "./package-json.js";
7
18
  import { resolveVersions } from "./dep-versions.js";
8
19
 
@@ -16,6 +27,21 @@ function seedFile(src, destPath) {
16
27
  return "seeded";
17
28
  }
18
29
 
30
+ // Recursively copy a template directory tree into the project.
31
+ function copyDir(srcDir, destDir) {
32
+ mkdirSync(destDir, { recursive: true });
33
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
34
+ const from = resolve(srcDir, entry.name);
35
+ const to = resolve(destDir, entry.name);
36
+ if (entry.isDirectory()) copyDir(from, to);
37
+ else copyFileSync(from, to);
38
+ }
39
+ }
40
+
41
+ function dirIsEmpty(dir) {
42
+ return !existsSync(dir) || readdirSync(dir).length === 0;
43
+ }
44
+
19
45
  // Default interactive prompt. Injectable so runInit is testable without stdin.
20
46
  async function askYesNo(question) {
21
47
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -46,6 +72,7 @@ export async function runInit({
46
72
  const { text, added, skipped } = applyToPackageJson(raw, {
47
73
  scripts,
48
74
  devDependencies,
75
+ fields: packageFields(),
49
76
  });
50
77
  writeFileSync(pkgPath, text);
51
78
 
@@ -65,10 +92,68 @@ export async function runInit({
65
92
  report("Kept existing scripts", skipped.scripts);
66
93
  report("Declared devDependencies", added.devDependencies);
67
94
  report("Kept existing devDependencies", skipped.devDependencies);
95
+ report("Set package fields", added.fields);
96
+ report("Kept existing package fields", skipped.fields);
68
97
  report("Seeded config files", seededConfigs);
69
98
  report("Kept existing config files", keptConfigs);
70
99
  log("");
71
100
  log("Next: run `npm install` to fetch the newly declared devDependencies.");
72
101
 
73
- return { added, skipped, seededConfigs, keptConfigs };
102
+ const demo = await maybeScaffoldDemo({ cwd, prompt, log });
103
+
104
+ return { added, skipped, seededConfigs, keptConfigs, demo };
105
+ }
106
+
107
+ // Opt-in starter scaffold for a new project. Seeds a minimal src/ module, a
108
+ // README with doc-generator markers, and a project.json — each independently and
109
+ // only when its slot is empty, so nothing a consumer already has is overwritten.
110
+ // It writes files only; the user runs npm install / test / push themselves.
111
+ async function maybeScaffoldDemo({ cwd, prompt, log }) {
112
+ const wantDemo = await prompt(
113
+ "Scaffold a starter demo (minimal src module + README + project.json) so you\n" +
114
+ "can build, test, and publish immediately? Existing files are never\n" +
115
+ "overwritten. [y/N] ",
116
+ );
117
+ if (!wantDemo) {
118
+ return { scaffolded: false, reason: "declined", wrote: [], skipped: [] };
119
+ }
120
+
121
+ const demoTemplate = resolve(templatesDir, "demo");
122
+ const wrote = [];
123
+ const skipped = [];
124
+ const paths = [];
125
+
126
+ // src/: seed the module only when src/ has no files, so starter code never
127
+ // gets mixed into a project that already has source.
128
+ const srcDir = resolve(cwd, "src");
129
+ if (dirIsEmpty(srcDir)) {
130
+ copyDir(resolve(demoTemplate, "src"), srcDir);
131
+ for (const entry of readdirSync(resolve(demoTemplate, "src"))) {
132
+ paths.push(`src/${entry}`);
133
+ }
134
+ wrote.push("src/ module");
135
+ } else {
136
+ skipped.push("src/ (already has files)");
137
+ }
138
+
139
+ // README.md and project.json: single files, seeded only when absent.
140
+ for (const name of ["README.md", "project.json"]) {
141
+ if (existsSync(resolve(cwd, name))) {
142
+ skipped.push(name);
143
+ } else {
144
+ copyFileSync(resolve(demoTemplate, name), resolve(cwd, name));
145
+ wrote.push(name);
146
+ paths.push(name);
147
+ }
148
+ }
149
+
150
+ log("");
151
+ if (wrote.length) log(`Demo: seeded ${wrote.join(", ")}.`);
152
+ if (skipped.length) log(`Demo: kept existing ${skipped.join(", ")}.`);
153
+ if (paths.length) {
154
+ log(`To remove the starter files later: rm -r ${paths.join(" ")}`);
155
+ log("Then: npm install → npm test → npm run update-all-format → push.");
156
+ }
157
+
158
+ return { scaffolded: paths.length > 0, reason: null, wrote, skipped, paths };
74
159
  }
@@ -28,24 +28,29 @@ export function detectIndent(text) {
28
28
  * not touch disk — returns the new text plus a report of what changed.
29
29
  *
30
30
  * @param {string} rawText - current package.json contents
31
- * @param {{scripts?: Record<string,string>, devDependencies?: Record<string,string>}} additions
31
+ * @param {{
32
+ * scripts?: Record<string,string>,
33
+ * devDependencies?: Record<string,string>,
34
+ * fields?: Record<string,unknown>
35
+ * }} additions - `fields` are top-level keys (main, type, files, …)
32
36
  * @returns {{
33
37
  * text: string,
34
- * added: { scripts: string[], devDependencies: string[] },
35
- * skipped: { scripts: string[], devDependencies: string[] }
38
+ * added: { scripts: string[], devDependencies: string[], fields: string[] },
39
+ * skipped: { scripts: string[], devDependencies: string[], fields: string[] }
36
40
  * }} new file text, plus keys added vs. skipped (already present)
37
41
  */
38
42
  export function applyToPackageJson(
39
43
  rawText,
40
- { scripts = {}, devDependencies = {} } = {},
44
+ { scripts = {}, devDependencies = {}, fields = {} } = {},
41
45
  ) {
42
46
  const pkg = JSON.parse(rawText);
43
47
  const indent = detectIndent(rawText);
44
48
 
45
49
  const s = mergeAbsent(pkg.scripts, scripts);
46
50
  const d = mergeAbsent(pkg.devDependencies, devDependencies);
51
+ const f = mergeAbsent(pkg, fields);
47
52
 
48
- const next = { ...pkg };
53
+ const next = { ...f.merged };
49
54
  if (Object.keys(s.merged).length) next.scripts = s.merged;
50
55
  if (Object.keys(d.merged).length) next.devDependencies = d.merged;
51
56
 
@@ -54,7 +59,11 @@ export function applyToPackageJson(
54
59
 
55
60
  return {
56
61
  text,
57
- added: { scripts: s.added, devDependencies: d.added },
58
- skipped: { scripts: s.skipped, devDependencies: d.skipped },
62
+ added: { scripts: s.added, devDependencies: d.added, fields: f.added },
63
+ skipped: {
64
+ scripts: s.skipped,
65
+ devDependencies: d.skipped,
66
+ fields: f.skipped,
67
+ },
59
68
  };
60
69
  }
@@ -0,0 +1,42 @@
1
+ <!-- TOC:START -->
2
+ <!-- TOC:END -->
3
+
4
+ # Starter project
5
+
6
+ > **Seeded by `init` — safe to replace.** These starter files (this README, a
7
+ > `math-engine` component under `src/`, and `project.json`) exist so a brand-new
8
+ > project can run its tests, publish on push, and watch the doc generator work
9
+ > before you write any code. Replace them with your own whenever you like —
10
+ > `init` printed the exact paths to delete.
11
+
12
+ Run `npm run update-all-format` to fill the blocks below; GitHub renders the
13
+ Mermaid diagrams inline from the fenced blocks.
14
+
15
+ ## Build Pipeline
16
+
17
+ <!-- NX_GRAPH:START -->
18
+ <!-- NX_GRAPH:END -->
19
+
20
+ ## Component Diagram
21
+
22
+ <!-- UML:components:START -->
23
+ <!-- UML:components:END -->
24
+
25
+ ## Components Table
26
+
27
+ <!-- UML:components-table:START -->
28
+ <!-- UML:components-table:END -->
29
+
30
+ ## Component Details
31
+
32
+ <!-- UML:component-details:START -->
33
+ <!-- UML:component-details:END -->
34
+
35
+ ## Try it
36
+
37
+ ```bash
38
+ npm install # fetch the dev deps init declared
39
+ npm test # the seeded vitest test passes
40
+ npm run update-all-format # fills the doc blocks above
41
+ git commit -am "feat: initial version" && git push # feat:/fix: trigger a publish
42
+ ```
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "demo",
3
+ "targets": {
4
+ "install": {
5
+ "description": "Install npm dependencies"
6
+ },
7
+ "build": {
8
+ "description": "Compile TypeScript source to JavaScript",
9
+ "dependsOn": ["install"]
10
+ },
11
+ "lint": {
12
+ "description": "Run ESLint on TypeScript source files",
13
+ "dependsOn": ["install"]
14
+ },
15
+ "type-check": {
16
+ "description": "Type-check source without emitting",
17
+ "dependsOn": ["build"]
18
+ },
19
+ "test": {
20
+ "description": "Run unit tests with vitest",
21
+ "dependsOn": ["build"]
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,7 @@
1
+ // Starter file from `init` — replace with your own code.
2
+ // Package entry point: re-exports the demo component so the built package has a
3
+ // public surface to publish.
4
+ export { MathEngine } from "./math-engine/MathEngine.js";
5
+ export { MathError } from "./math-engine/MathError.js";
6
+ export type { MathResult } from "./math-engine/MathResult.js";
7
+ export type { Operation } from "./math-engine/Operation.js";
@@ -0,0 +1,24 @@
1
+ // Starter file from `init` — replace with your own code.
2
+ import { describe, test, expect } from "vitest";
3
+ import { MathEngine } from "./MathEngine.js";
4
+ import { MathError } from "./MathError.js";
5
+
6
+ describe("MathEngine", () => {
7
+ const engine = new MathEngine();
8
+
9
+ test("add returns the sum and echoes the operation", () => {
10
+ expect(engine.add(3, 4)).toEqual({
11
+ value: 7,
12
+ operation: "add",
13
+ operands: [3, 4],
14
+ });
15
+ });
16
+
17
+ test("subtract returns the difference", () => {
18
+ expect(engine.subtract(10, 4).value).toBe(6);
19
+ });
20
+
21
+ test("rejects non-finite operands with a MathError", () => {
22
+ expect(() => engine.add(1, Infinity)).toThrow(MathError);
23
+ });
24
+ });
@@ -0,0 +1,22 @@
1
+ // Starter file from `init` — replace with your own code.
2
+ import { MathError } from "./MathError.js";
3
+ import type { MathResult } from "./MathResult.js";
4
+ import type { Operation } from "./Operation.js";
5
+
6
+ export class MathEngine {
7
+ add(a: number, b: number): MathResult {
8
+ this.validate("add", a, b);
9
+ return { value: a + b, operation: "add", operands: [a, b] };
10
+ }
11
+
12
+ subtract(a: number, b: number): MathResult {
13
+ this.validate("subtract", a, b);
14
+ return { value: a - b, operation: "subtract", operands: [a, b] };
15
+ }
16
+
17
+ private validate(op: Operation, a: number, b: number): void {
18
+ if (!Number.isFinite(a) || !Number.isFinite(b)) {
19
+ throw new MathError(op, "Operands must be finite numbers");
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,11 @@
1
+ import type { Operation } from "./Operation.js";
2
+
3
+ export class MathError extends Error {
4
+ readonly operation: Operation;
5
+
6
+ constructor(operation: Operation, message: string) {
7
+ super(message);
8
+ this.name = "MathError";
9
+ this.operation = operation;
10
+ }
11
+ }
@@ -0,0 +1,7 @@
1
+ import type { Operation } from "./Operation.js";
2
+
3
+ export interface MathResult {
4
+ value: number;
5
+ operation: Operation;
6
+ operands: [number, number];
7
+ }
@@ -0,0 +1 @@
1
+ export type Operation = "add" | "subtract";
@@ -0,0 +1,2 @@
1
+ The math-engine component: a small, dependency-free arithmetic core. It validates
2
+ operands and returns a structured result describing the operation performed.
@@ -4,5 +4,6 @@
4
4
  "outDir": "dist",
5
5
  "rootDir": "src"
6
6
  },
7
- "include": ["src"]
7
+ "include": ["src"],
8
+ "exclude": ["node_modules", "dist", "src/**/*.test.ts"]
8
9
  }