@integr8-media/config 1.0.0 → 1.1.0

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
@@ -29,9 +29,17 @@ npm install --save-dev typescript @biomejs/biome vitest @vitest/coverage-v8 tsdo
29
29
 
30
30
  ```jsonc
31
31
  // tsconfig.json
32
- { "extends": "@integr8-media/config/tsconfig/react-library.json", "include": ["src"] }
32
+ {
33
+ "extends": "@integr8-media/config/tsconfig/react-library.json",
34
+ "include": ["src"],
35
+ "exclude": ["**/*.config.ts", "playground/dist"]
36
+ }
33
37
  ```
34
38
 
39
+ `include` and `exclude` always go in the repo's own `tsconfig.json`. A preset can't carry them,
40
+ because TypeScript resolves those paths relative to the preset file, which sits inside
41
+ `node_modules`.
42
+
35
43
  | Preset | Use for |
36
44
  | --- | --- |
37
45
  | `@integr8-media/config/tsconfig/library.json` | Node or universal library: NodeNext, declarations, isolatedDeclarations |
@@ -63,10 +71,23 @@ Notes:
63
71
  - Linter: recommended rules, plus as errors: no default exports (allowed in `*.config.ts`),
64
72
  camelCase or PascalCase file names, no explicit `any`, `import type` for type-only imports,
65
73
  no non-null assertions. `console` is a warning.
66
- - Ignores `dist/`, `coverage/`, `playground/dist/` and Markdown files (Biome 2.6 formats
67
- Markdown, which would rewrite docs such as `docs/SPEC.md`). Needs Biome 2.6 or newer.
74
+ - Ignores `dist/`, `coverage/`, `playground/dist/`, `docs/api/` (TypeDoc output),
75
+ `test-results/` and `playwright-report/` (Playwright output), `.vscode/` and Markdown files
76
+ (Biome 2.6 formats Markdown, which would rewrite docs such as `docs/SPEC.md`). Needs Biome
77
+ 2.6 or newer.
78
+ - `vcs` is on (`clientKind: "git"`, `useIgnoreFile: true`), so Biome also skips everything in
79
+ `.gitignore`. Biome fails if there is no `.gitignore` in the folder where it runs, so every
80
+ repo needs one (any git repo has one already).
81
+ - The CSS parser accepts Tailwind v4 directives (`@theme`, `@apply`, `@source`...). Repos
82
+ without Tailwind are not affected.
83
+ - Overrides that turn rules off:
84
+ - `*.config.{ts,mts,js,mjs}`: `style.noDefaultExport`.
85
+ - `.size-limit.{js,mjs,ts}`: `style.noDefaultExport` and `style.useFilenamingConvention`
86
+ (size-limit needs a default export, and the file name starts with a dot).
87
+ - `playground/config.{js,mjs,ts}`: `style.noDefaultExport` (the Ladle config).
88
+ - `scripts/**`: `suspicious.noConsole` (build scripts print progress).
68
89
  - If your `biome.json` sets `files.includes`, Biome adds it to the preset's list, so list only
69
- your extra ignores (for example `"!!.vscode"`) and don't repeat `"**"`.
90
+ your extra ignores (for example `"!!.idea"`) and don't repeat `"**"`.
70
91
 
71
92
  ### Vitest
72
93
 
@@ -74,12 +95,39 @@ Notes:
74
95
  // vitest.config.ts
75
96
  import { integr8Vitest } from '@integr8-media/config/vitest';
76
97
 
77
- export default integr8Vitest({ environment: 'node', coverageThreshold: 80 });
98
+ export default integr8Vitest({
99
+ environment: 'node',
100
+ coverageThreshold: 80,
101
+ setupFiles: './test/setup.ts', // optional: string or list of strings
102
+ });
78
103
  ```
79
104
 
80
105
  v8 coverage on `src/**` (tests, `src/index.ts` and `playground/` excluded). The run fails when
81
- line coverage is below the threshold. `environment: 'jsdom'` needs `jsdom` installed. To add
82
- your own settings, wrap it with `mergeConfig` from `vitest/config`.
106
+ line coverage is below the threshold. `environment: 'jsdom'` needs `jsdom` installed.
107
+
108
+ - `setupFiles` goes to `test.setupFiles` (for example a Testing Library setup file). It takes a
109
+ non-empty string or a non-empty list of non-empty strings; anything else throws
110
+ `ConfigError` with `INVALID_OPTION`.
111
+ - Test collection excludes Vitest's defaults plus `playground/**`, so Playwright specs such as
112
+ `playground/smoke.spec.ts` are not run by `vitest run`.
113
+
114
+ For settings the preset doesn't have, wrap it with `mergeConfig` from `vitest/config`:
115
+
116
+ ```ts
117
+ // vitest.config.ts
118
+ import { integr8Vitest } from '@integr8-media/config/vitest';
119
+ import { defineConfig, mergeConfig } from 'vitest/config';
120
+
121
+ export default mergeConfig(
122
+ integr8Vitest({ environment: 'jsdom', setupFiles: './test/setup.ts' }),
123
+ defineConfig({
124
+ test: {
125
+ // Process CSS files that tests import with ?raw.
126
+ css: { include: [/\.css$/] },
127
+ },
128
+ }),
129
+ );
130
+ ```
83
131
 
84
132
  ### tsdown
85
133
 
@@ -100,15 +148,34 @@ dependencies stay external. Files are always named `.mjs` / `.d.mts`, so point `
100
148
  }
101
149
  ```
102
150
 
103
- ### size-limit
151
+ Option `unbundle?: boolean` (default `false`): with `true`, every source file is built as its own
152
+ output file instead of one bundle. Use it for React libraries whose files start with
153
+ `'use client'`. Each file keeps its own directive, so a Next.js server component can import the
154
+ non-client exports from the same package. Rolldown warns `MODULE_LEVEL_DIRECTIVE` for every such
155
+ file; with `unbundle: true` the preset hides that warning, because the directive is kept. With
156
+ `false` the warning stays, because it is real there. A value that is not a boolean throws
157
+ `ConfigError` with `INVALID_OPTION`.
104
158
 
105
159
  ```ts
106
- // .size-limit.ts
160
+ export default integr8Tsdown({
161
+ entry: ['src/index.ts', 'src/Button.tsx'],
162
+ platform: 'browser',
163
+ unbundle: true,
164
+ });
165
+ ```
166
+
167
+ ### size-limit
168
+
169
+ ```js
170
+ // .size-limit.mjs
107
171
  import { integr8SizeLimit } from '@integr8-media/config/size-limit';
108
172
 
109
173
  export default integr8SizeLimit([{ path: 'dist/index.mjs', limit: '10 kB' }]);
110
174
  ```
111
175
 
176
+ Use `.size-limit.mjs`: it loads without a TypeScript loader, and the Biome preset has an override
177
+ for it (a default export and a dotted file name).
178
+
112
179
  Each entry takes `path`, `limit`, and optionally `name` and `import`. size-limit needs a
113
180
  plugin: `@size-limit/file` measures built files; `@size-limit/esbuild` is needed for `import`.
114
181
 
@@ -121,8 +188,22 @@ extends:
121
188
  ```
122
189
 
123
190
  - pre-commit: `biome check --write` on staged files, fixes re-staged.
124
- - pre-push, in order, stopping at the first failure: `typecheck`, `test:coverage`, `build`, and
125
- `playground:smoke` when that script exists.
191
+ - pre-push, in order, stopping at the first failure: `typecheck`, `test:coverage`, `build`.
192
+
193
+ `playground:smoke` is not in the base: it builds the whole playground and runs Playwright, which
194
+ is too slow for every push. A repo that wants it adds it back in its own `lefthook.yml`; the
195
+ base's `piped: true` then runs it last:
196
+
197
+ ```yaml
198
+ # lefthook.yml
199
+ extends:
200
+ - node_modules/@integr8-media/config/lefthook/base.yml
201
+ pre-push:
202
+ commands:
203
+ playground-smoke:
204
+ priority: 4
205
+ run: npm run playground:smoke
206
+ ```
126
207
 
127
208
  Add `"prepare": "lefthook install"` to your scripts so hooks are installed after `npm install`.
128
209
 
@@ -136,8 +217,9 @@ Add `"prepare": "lefthook install"` to your scripts so hooks are installed after
136
217
  "test": "vitest run",
137
218
  "test:coverage": "vitest run --coverage",
138
219
  "build": "tsdown",
139
- "playground": "ladle serve",
140
- "playground:smoke": "playwright test",
220
+ "playground": "ladle serve --config playground",
221
+ "playground:build": "ladle build --config playground --outDir playground/dist",
222
+ "playground:smoke": "npm run playground:build && playwright test",
141
223
  "size": "size-limit",
142
224
  "check:exports": "publint && attw --pack --profile esm-only"
143
225
  }
@@ -146,9 +228,14 @@ Add `"prepare": "lefthook install"` to your scripts so hooks are installed after
146
228
  `--profile esm-only` tells attw not to report the CommonJS problems that every ESM-only package
147
229
  has.
148
230
 
231
+ A package that exports CSS files must exclude them from attw, which only knows JavaScript and
232
+ types: `attw --pack --profile esm-only --exclude-entrypoints ./styles.css`.
233
+
149
234
  ### GitHub Actions
150
235
 
151
- The workflows live in this GitHub repo, not in the npm package. Because the repo is private, its
236
+ The workflows live in this GitHub repo, not in the npm package. Until a `v1` tag exists, point
237
+ callers at `@main` (`.../pr-checks.yml@main`); once it exists, switch them to `@v1`. The examples
238
+ below show `@v1`. Because the repo is private, its
152
239
  **Settings → Actions → General → Access** must allow access from your other repositories.
153
240
 
154
241
  ```yaml
@@ -168,13 +255,14 @@ jobs:
168
255
  on:
169
256
  push:
170
257
  tags: ["v*.*.*"]
258
+ permissions:
259
+ contents: read
260
+ id-token: write # npm Trusted Publishing
171
261
  jobs:
172
262
  publish:
173
263
  uses: <owner>/integr8-config/.github/workflows/publish.yml@v1
174
264
  with:
175
- provenance: false # true only for public repos (then also grant id-token: write)
176
- secrets:
177
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
265
+ provenance: false # true only for public repos
178
266
  ```
179
267
 
180
268
  - `pr-checks.yml`: install, lint, typecheck, test:coverage, build, size, check:exports.
@@ -182,6 +270,27 @@ jobs:
182
270
  - `publish.yml`: checks the tag is `vX.Y.Z`, is on main and matches `package.json`; then
183
271
  install, build, test and `npm publish --access public`.
184
272
 
273
+ #### Publishing with npm Trusted Publishing
274
+
275
+ No npm token is stored in GitHub. The first version of a package has to be published by hand
276
+ (`npm publish --access public`), because npm only offers Trusted Publishing for existing
277
+ packages. Then, once per package:
278
+
279
+ 1. On npmjs.com open the package → **Settings → Trusted Publisher → GitHub Actions**.
280
+ 2. Enter the GitHub owner, the repo, and the workflow file the tag starts (`release.yml`).
281
+ 3. After one release works, set **Publishing access** to "Require two-factor authentication
282
+ and disallow tokens".
283
+
284
+ To release: bump `version` in `package.json` by hand, merge to `main`, then push the tag:
285
+
286
+ ```sh
287
+ git tag v1.0.1
288
+ git push origin v1.0.1
289
+ ```
290
+
291
+ A package without Trusted Publishing can still pass `secrets: { NPM_TOKEN: ... }` to
292
+ `publish.yml`.
293
+
185
294
  ## API
186
295
 
187
296
  | Import | Exports |
package/biome/biome.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
2
  "files": {
3
- "includes": ["**", "!**/dist", "!**/coverage", "!**/playground/dist", "!**/*.md"]
3
+ "includes": [
4
+ "**",
5
+ "!**/dist",
6
+ "!**/coverage",
7
+ "!**/playground/dist",
8
+ "!**/docs/api",
9
+ "!**/test-results",
10
+ "!**/playwright-report",
11
+ "!**/.vscode",
12
+ "!**/*.md"
13
+ ]
14
+ },
15
+ "vcs": {
16
+ "enabled": true,
17
+ "clientKind": "git",
18
+ "useIgnoreFile": true
19
+ },
20
+ "css": {
21
+ "parser": {
22
+ "tailwindDirectives": true
23
+ }
4
24
  },
5
25
  "formatter": {
6
26
  "enabled": true,
@@ -56,6 +76,37 @@
56
76
  }
57
77
  }
58
78
  }
79
+ },
80
+ {
81
+ "includes": ["**/.size-limit.js", "**/.size-limit.mjs", "**/.size-limit.ts"],
82
+ "linter": {
83
+ "rules": {
84
+ "style": {
85
+ "noDefaultExport": "off",
86
+ "useFilenamingConvention": "off"
87
+ }
88
+ }
89
+ }
90
+ },
91
+ {
92
+ "includes": ["**/playground/config.js", "**/playground/config.mjs", "**/playground/config.ts"],
93
+ "linter": {
94
+ "rules": {
95
+ "style": {
96
+ "noDefaultExport": "off"
97
+ }
98
+ }
99
+ }
100
+ },
101
+ {
102
+ "includes": ["**/scripts/**"],
103
+ "linter": {
104
+ "rules": {
105
+ "suspicious": {
106
+ "noConsole": "off"
107
+ }
108
+ }
109
+ }
59
110
  }
60
111
  ]
61
112
  }
package/dist/tsdown.d.mts CHANGED
@@ -10,19 +10,32 @@ interface Integr8TsdownOptions {
10
10
  entry: Integr8TsdownEntry;
11
11
  /** Target platform. `node` builds for `node24`; the others build for `es2024`. */
12
12
  platform: Integr8TsdownPlatform;
13
+ /**
14
+ * Build every source file as its own output file instead of one bundle. Defaults to `false`.
15
+ *
16
+ * Use it for React libraries whose files start with `'use client'`: each file is built on its
17
+ * own, so the directive stays on it and a Next.js server component can import the
18
+ * non-client exports from the same package. The preset then hides rolldown's
19
+ * `MODULE_LEVEL_DIRECTIVE` warning, which is only noise in that mode. With `false` the
20
+ * warning stays on, because there it points at a directive that the bundle drops.
21
+ */
22
+ unbundle?: boolean;
13
23
  }
14
24
  /**
15
25
  * Builds the shared tsdown config: ESM only, `.d.ts` generation (fast path through
16
26
  * isolatedDeclarations), no source maps, a clean output folder and tree-shaking.
17
27
  *
28
+ * With `unbundle: true` each source file becomes its own output file (see
29
+ * {@link Integr8TsdownOptions.unbundle}).
30
+ *
18
31
  * Output files always use `.mjs` and `.d.mts`, whatever the platform, so a package's
19
32
  * `exports` map is the same everywhere. Dependencies and peer dependencies stay external
20
33
  * (tsdown's default).
21
34
  *
22
- * @param options - Entry points and target platform.
35
+ * @param options - Entry points, target platform and the optional `unbundle` switch.
23
36
  * @returns A tsdown config to default-export from `tsdown.config.ts`.
24
- * @throws {@link ConfigError} with code `INVALID_OPTION` for an empty entry or an unknown
25
- * platform.
37
+ * @throws {@link ConfigError} with code `INVALID_OPTION` for an empty entry, an unknown
38
+ * platform or an `unbundle` that is not a boolean.
26
39
  *
27
40
  * @example
28
41
  * ```ts
package/dist/tsdown.mjs CHANGED
@@ -9,14 +9,17 @@ const PLATFORMS = [
9
9
  * Builds the shared tsdown config: ESM only, `.d.ts` generation (fast path through
10
10
  * isolatedDeclarations), no source maps, a clean output folder and tree-shaking.
11
11
  *
12
+ * With `unbundle: true` each source file becomes its own output file (see
13
+ * {@link Integr8TsdownOptions.unbundle}).
14
+ *
12
15
  * Output files always use `.mjs` and `.d.mts`, whatever the platform, so a package's
13
16
  * `exports` map is the same everywhere. Dependencies and peer dependencies stay external
14
17
  * (tsdown's default).
15
18
  *
16
- * @param options - Entry points and target platform.
19
+ * @param options - Entry points, target platform and the optional `unbundle` switch.
17
20
  * @returns A tsdown config to default-export from `tsdown.config.ts`.
18
- * @throws {@link ConfigError} with code `INVALID_OPTION` for an empty entry or an unknown
19
- * platform.
21
+ * @throws {@link ConfigError} with code `INVALID_OPTION` for an empty entry, an unknown
22
+ * platform or an `unbundle` that is not a boolean.
20
23
  *
21
24
  * @example
22
25
  * ```ts
@@ -29,6 +32,8 @@ function integr8Tsdown(options) {
29
32
  if (typeof options !== "object" || options === null) throw new ConfigError("INVALID_OPTION", "integr8Tsdown: options are required.");
30
33
  const { platform } = options;
31
34
  if (!PLATFORMS.includes(platform)) throw new ConfigError("INVALID_OPTION", `integr8Tsdown: "platform" must be one of ${PLATFORMS.join(", ")}; got ${String(platform)}.`);
35
+ const { unbundle = false } = options;
36
+ if (typeof unbundle !== "boolean") throw new ConfigError("INVALID_OPTION", `integr8Tsdown: "unbundle" must be a boolean; got ${String(unbundle)}.`);
32
37
  return {
33
38
  entry: copyEntry(options.entry),
34
39
  platform,
@@ -38,9 +43,17 @@ function integr8Tsdown(options) {
38
43
  dts: true,
39
44
  sourcemap: false,
40
45
  clean: true,
41
- treeshake: true
46
+ treeshake: true,
47
+ ...unbundle ? {
48
+ unbundle: true,
49
+ inputOptions: { onLog: dropDirectiveWarning }
50
+ } : {}
42
51
  };
43
52
  }
53
+ function dropDirectiveWarning(...[level, log, defaultHandler]) {
54
+ if (log.code === "MODULE_LEVEL_DIRECTIVE") return;
55
+ defaultHandler(level, log);
56
+ }
44
57
  function copyEntry(entry) {
45
58
  if (typeof entry === "string") {
46
59
  if (entry.trim() === "") throw invalidEntry();
package/dist/vitest.d.mts CHANGED
@@ -8,15 +8,22 @@ interface Integr8VitestOptions {
8
8
  environment?: Integr8VitestEnvironment;
9
9
  /** Minimum line coverage in percent (0–100). Defaults to `80`. */
10
10
  coverageThreshold?: number;
11
+ /**
12
+ * Setup file(s) run before each test file, passed to `test.setupFiles` (for example
13
+ * `'./test/setup.ts'` for Testing Library). Non-empty strings only.
14
+ */
15
+ setupFiles?: string | string[];
11
16
  }
12
17
  /**
13
18
  * Builds the shared Vitest config: v8 coverage on `src/**` (tests, `src/index.ts` and
14
- * `playground/` excluded) that fails when line coverage is below the threshold.
19
+ * `playground/` excluded) that fails when line coverage is below the threshold. Test collection
20
+ * skips `playground/**` too, so Playwright specs there are not run by Vitest.
15
21
  *
16
- * @param options - Environment and coverage threshold.
22
+ * @param options - Environment, coverage threshold and setup files.
17
23
  * @returns A Vitest config to default-export from `vitest.config.ts`.
18
24
  * @throws {@link ConfigError} with code `INVALID_OPTION` for an unknown environment or a
19
- * threshold outside 0–100.
25
+ * threshold outside 0–100, or for `setupFiles` that is not a non-empty string or a non-empty
26
+ * list of non-empty strings.
20
27
  *
21
28
  * @example
22
29
  * ```ts
package/dist/vitest.mjs CHANGED
@@ -1,15 +1,18 @@
1
1
  import { t as ConfigError } from "./configError-Cap5gwBB.mjs";
2
+ import { configDefaults } from "vitest/config";
2
3
  // src/vitest/integr8Vitest.ts
3
4
  const ENVIRONMENTS = ["node", "jsdom"];
4
5
  const DEFAULT_COVERAGE_THRESHOLD = 80;
5
6
  /**
6
7
  * Builds the shared Vitest config: v8 coverage on `src/**` (tests, `src/index.ts` and
7
- * `playground/` excluded) that fails when line coverage is below the threshold.
8
+ * `playground/` excluded) that fails when line coverage is below the threshold. Test collection
9
+ * skips `playground/**` too, so Playwright specs there are not run by Vitest.
8
10
  *
9
- * @param options - Environment and coverage threshold.
11
+ * @param options - Environment, coverage threshold and setup files.
10
12
  * @returns A Vitest config to default-export from `vitest.config.ts`.
11
13
  * @throws {@link ConfigError} with code `INVALID_OPTION` for an unknown environment or a
12
- * threshold outside 0–100.
14
+ * threshold outside 0–100, or for `setupFiles` that is not a non-empty string or a non-empty
15
+ * list of non-empty strings.
13
16
  *
14
17
  * @example
15
18
  * ```ts
@@ -23,8 +26,11 @@ function integr8Vitest(options = {}) {
23
26
  const coverageThreshold = options.coverageThreshold ?? DEFAULT_COVERAGE_THRESHOLD;
24
27
  if (!ENVIRONMENTS.includes(environment)) throw new ConfigError("INVALID_OPTION", `integr8Vitest: "environment" must be one of ${ENVIRONMENTS.join(", ")}; got ${String(environment)}.`);
25
28
  if (typeof coverageThreshold !== "number" || !Number.isFinite(coverageThreshold) || coverageThreshold < 0 || coverageThreshold > 100) throw new ConfigError("INVALID_OPTION", `integr8Vitest: "coverageThreshold" must be a number from 0 to 100; got ${String(coverageThreshold)}.`);
29
+ const setupFiles = copySetupFiles(options.setupFiles);
26
30
  return { test: {
27
31
  environment,
32
+ exclude: [...configDefaults.exclude, "playground/**"],
33
+ ...setupFiles === void 0 ? {} : { setupFiles },
28
34
  coverage: {
29
35
  provider: "v8",
30
36
  include: ["src/**/*.{ts,tsx}"],
@@ -39,4 +45,13 @@ function integr8Vitest(options = {}) {
39
45
  }
40
46
  } };
41
47
  }
48
+ function copySetupFiles(setupFiles) {
49
+ if (setupFiles === void 0) return;
50
+ if (isNonEmptyString(setupFiles)) return setupFiles;
51
+ if (Array.isArray(setupFiles) && setupFiles.length > 0 && setupFiles.every(isNonEmptyString)) return [...setupFiles];
52
+ throw new ConfigError("INVALID_OPTION", "integr8Vitest: \"setupFiles\" must be a non-empty path or a non-empty list of non-empty paths.");
53
+ }
54
+ function isNonEmptyString(value) {
55
+ return typeof value === "string" && value.trim() !== "";
56
+ }
42
57
  export { ConfigError, integr8Vitest };
package/lefthook/base.yml CHANGED
@@ -23,6 +23,3 @@ pre-push:
23
23
  build:
24
24
  priority: 3
25
25
  run: npm run build
26
- playground-smoke:
27
- priority: 4
28
- run: npm run playground:smoke --if-present
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integr8-media/config",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Shared tooling presets for every @integr8-media repo: TypeScript, Biome, Vitest, tsdown, Lefthook and size-limit.",
5
5
  "license": "MIT",
6
6
  "author": "Mina Louis Leon",