@jterrazz/typescript 9.0.1 → 9.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.
@@ -6,6 +6,11 @@
6
6
  *
7
7
  * Usage: node merge-knip-config.js <base.json> [project-knip.json]
8
8
  *
9
+ * The project file is read as JSONC — comments and trailing commas — so an
10
+ * ignore can carry the reason it exists. Every entry knip is told to overlook
11
+ * is a judgement, and a judgement with no reason beside it is re-litigated by
12
+ * the next reader.
13
+ *
9
14
  * Merge rules:
10
15
  * - Arrays (ignoreDependencies, ignoreBinaries, ignore): concatenated and deduplicated
11
16
  * - Objects (rules, vitest, etc.): project values override base
@@ -29,14 +34,101 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
29
34
 
30
35
  import { workspaceMembers } from './workspace-members.js';
31
36
 
37
+ /**
38
+ * Drops `//` and block comments, leaving everything inside a string literal
39
+ * alone — a comment marker in a glob (`ignore: ["**\/*.js"]`) is data.
40
+ */
41
+ function withoutComments(text) {
42
+ let output = '',
43
+ index = 0,
44
+ inString = false;
45
+
46
+ while (index < text.length) {
47
+ const char = text[index];
48
+
49
+ if (inString) {
50
+ const escaped = char === '\\';
51
+ output += escaped ? char + (text[index + 1] ?? '') : char;
52
+ inString = escaped || char !== '"';
53
+ index += escaped ? 2 : 1;
54
+ } else if (char === '"') {
55
+ inString = true;
56
+ output += char;
57
+ index += 1;
58
+ } else if (char === '/' && text[index + 1] === '/') {
59
+ while (index < text.length && text[index] !== '\n') {
60
+ index += 1;
61
+ }
62
+ } else if (char === '/' && text[index + 1] === '*') {
63
+ const end = text.indexOf('*/', index + 2);
64
+ index = end === -1 ? text.length : end + 2;
65
+ } else {
66
+ output += char;
67
+ index += 1;
68
+ }
69
+ }
70
+
71
+ return output;
72
+ }
73
+
74
+ /** Whether the comma at `index` is the last one of its collection — `["a",]`. */
75
+ function closesCollection(text, index) {
76
+ if (text[index] !== ',') {
77
+ return false;
78
+ }
79
+
80
+ let next = index + 1;
81
+ while (next < text.length && text[next].trim() === '') {
82
+ next += 1;
83
+ }
84
+
85
+ return text[next] === ']' || text[next] === '}';
86
+ }
87
+
88
+ /** Drops a comma that closes its collection, outside string literals. */
89
+ function withoutTrailingCommas(text) {
90
+ let output = '',
91
+ index = 0,
92
+ inString = false;
93
+
94
+ while (index < text.length) {
95
+ const char = text[index];
96
+ const closes = closesCollection(text, index);
97
+
98
+ if (inString) {
99
+ const escaped = char === '\\';
100
+ output += escaped ? char + (text[index + 1] ?? '') : char;
101
+ inString = escaped || char !== '"';
102
+ index += escaped ? 2 : 1;
103
+ } else {
104
+ inString = char === '"';
105
+ output += closes ? '' : char;
106
+ index += 1;
107
+ }
108
+ }
109
+
110
+ return output;
111
+ }
112
+
113
+ /**
114
+ * Reads a JSONC document. Hand-written on purpose: no JSONC parser is in this
115
+ * package's tree, and two token classes do not earn a dependency.
116
+ */
117
+ function readJsonc(path) {
118
+ const document = readFileSync(path, 'utf8'),
119
+ json = withoutTrailingCommas(withoutComments(document));
120
+
121
+ return JSON.parse(json);
122
+ }
123
+
32
124
  const basePath = process.argv[2];
33
125
  const projectPath = process.argv[3];
34
126
 
35
- const base = JSON.parse(readFileSync(basePath, 'utf8'));
127
+ const base = readJsonc(basePath);
36
128
 
37
129
  let project = {};
38
130
  if (projectPath) {
39
- project = JSON.parse(readFileSync(projectPath, 'utf8'));
131
+ project = readJsonc(projectPath);
40
132
  }
41
133
 
42
134
  const ARRAY_KEYS = new Set(['ignore', 'ignoreBinaries', 'ignoreDependencies', 'entry', 'project']);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/typescript",
3
- "version": "9.0.1",
3
+ "version": "9.1.1",
4
4
  "author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,6 +23,10 @@
23
23
  "types": "./src/index.d.ts",
24
24
  "default": "./src/index.js"
25
25
  },
26
+ "./oxfmt": {
27
+ "types": "./src/oxfmt.d.ts",
28
+ "default": "./src/oxfmt.js"
29
+ },
26
30
  "./oxlint": {
27
31
  "types": "./src/oxlint.d.ts",
28
32
  "default": "./src/oxlint.js"
@@ -1,5 +1,26 @@
1
1
  {
2
2
  "$schema": "https://unpkg.com/knip@6/schema.json",
3
3
  "ignoreDependencies": ["@jterrazz/test", "@jterrazz/typescript"],
4
- "ignoreBinaries": ["oxlint", "oxfmt"]
4
+ "ignoreBinaries": ["oxlint", "oxfmt"],
5
+ // Knip's own oxlint/oxfmt/vitest/tsdown plugins auto-enable only when
6
+ // that PACKAGE is a direct dependency of the project (`hasDependency`
7
+ // reads package.json). A consumer of this preset declares only
8
+ // `@jterrazz/typescript`, so those tools are never direct dependencies
9
+ // of its own — the plugins stay off and their config files read as
10
+ // unused. Naming the toolchain's own config files here, by hand, is
11
+ // the fix: an explicit `entry` always wins, regardless of the plugin.
12
+ //
13
+ // Setting `entry` at all REPLACES knip's own default (it does not add
14
+ // to it) — so the first two patterns below are knip's own defaults,
15
+ // repeated here on purpose, or a consumer with a bare `index.ts` and no
16
+ // `entry` of its own would lose it the moment this preset's `entry`
17
+ // took over.
18
+ "entry": [
19
+ "{index,cli,main}.{js,mjs,cjs,jsx,ts,tsx,mts,cts}!",
20
+ "src/{index,cli,main}.{js,mjs,cjs,jsx,ts,tsx,mts,cts}!",
21
+ "oxlint.config.ts",
22
+ "oxfmt.config.ts",
23
+ "vitest.config.{js,mjs,ts,cjs,mts,cts}",
24
+ "tsdown.config.{ts,mts,cts,js,mjs,cjs}"
25
+ ]
5
26
  }
package/src/oxfmt.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ type ConfigObject = Record<string, unknown>;
2
+
3
+ declare const base: ConfigObject;
4
+
5
+ export { defineConfig } from 'oxfmt';
6
+ export { base };
package/src/oxfmt.js ADDED
@@ -0,0 +1,14 @@
1
+ /*
2
+ * The tool-facing oxfmt entry (`@jterrazz/typescript/oxfmt`): the shared
3
+ * formatting preset plus oxfmt's own `defineConfig`, for the same reason
4
+ * `oxlint.js` re-exports its tool's — a consumer's config names this package
5
+ * and nothing else:
6
+ *
7
+ * import { base, defineConfig } from '@jterrazz/typescript/oxfmt';
8
+ *
9
+ * export default defineConfig(base);
10
+ */
11
+
12
+ export { defineConfig } from 'oxfmt';
13
+
14
+ export { default as base } from '../presets/oxfmt/index.js';
@@ -0,0 +1,17 @@
1
+ import { expect, test } from 'vitest';
2
+
3
+ import { base, defineConfig } from './oxfmt.js';
4
+
5
+ test('exports the shared formatting preset', () => {
6
+ // Given - the tool-facing entry
7
+ // Then - the preset carries the ecosystem's format: four spaces, single quotes, 100 columns
8
+ expect(base).toMatchObject({ printWidth: 100, singleQuote: true, tabWidth: 4 });
9
+ });
10
+
11
+ test("re-exports oxfmt's own defineConfig", () => {
12
+ // Given - a config a consumer would write, with oxfmt declared nowhere in its project
13
+ const config = { printWidth: 80 };
14
+
15
+ // Then - the entry carries the tool's helper, which returns the config unchanged
16
+ expect(defineConfig(config)).toBe(config);
17
+ });
package/src/oxlint.d.ts CHANGED
@@ -12,4 +12,5 @@ declare const node: ConfigObject;
12
12
  */
13
13
  declare function compose(...fragments: ConfigObject[]): ConfigObject;
14
14
 
15
+ export { defineConfig } from 'oxlint';
15
16
  export { compose, expo, hexagonal, next, node };
package/src/oxlint.js CHANGED
@@ -1,7 +1,8 @@
1
1
  /*
2
2
  * The tool-facing oxlint entry (`@jterrazz/typescript/oxlint`): the named
3
- * presets plus the `compose()` helper. Wiring is EXPLICIT a consumer
4
- * composes exactly the fragments it wants, nothing is auto-detected:
3
+ * presets, the `compose()` helper, and oxlint's own `defineConfig`. Wiring is
4
+ * EXPLICIT — a consumer composes exactly the fragments it wants, nothing is
5
+ * auto-detected:
5
6
  *
6
7
  * import { testing } from '@jterrazz/test/oxlint';
7
8
  * import { compose, node } from '@jterrazz/typescript/oxlint';
@@ -50,6 +51,13 @@ export function compose(...fragments) {
50
51
  return merged;
51
52
  }
52
53
 
54
+ /* Oxlint's own `defineConfig`, re-exported from here: a bare
55
+ * `import { defineConfig } from 'oxlint'` in a consumer's config resolves only
56
+ * where the consumer declares oxlint itself, which pnpm's strict node_modules
57
+ * refuses to assume. Resolved from THIS package — where oxlint is a real
58
+ * dependency — the one-devDependency shape holds on every package manager. */
59
+ export { defineConfig } from 'oxlint';
60
+
53
61
  export { default as hexagonal } from '../presets/oxlint/architectures/hexagonal.js';
54
62
  export { default as expo } from '../presets/oxlint/expo.js';
55
63
  export { default as next } from '../presets/oxlint/next.js';
@@ -1,6 +1,6 @@
1
1
  import { expect, test } from 'vitest';
2
2
 
3
- import { compose, expo, hexagonal, next, node } from './oxlint.js';
3
+ import { compose, defineConfig, expo, hexagonal, next, node } from './oxlint.js';
4
4
 
5
5
  test('concatenates and dedupes plugin lists', () => {
6
6
  // Given - two fragments sharing one jsPlugin
@@ -77,3 +77,11 @@ test('exports the named presets', () => {
77
77
  expect(typeof preset).toBe('object');
78
78
  }
79
79
  });
80
+
81
+ test("re-exports oxlint's own defineConfig", () => {
82
+ // Given - a config a consumer would write, with oxlint declared nowhere in its project
83
+ const config = { rules: { curly: 'error' } };
84
+
85
+ // Then - the entry carries the tool's helper, which returns the config unchanged
86
+ expect(defineConfig(config)).toBe(config);
87
+ });