@jterrazz/typescript 6.1.0 → 6.2.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.
@@ -33,16 +33,30 @@ find_binary() {
33
33
  fi
34
34
  }
35
35
 
36
- # The Go compiler (official typescript@7) is installed under the
37
- # "typescript-go" npm alias: typedoc and eslint-plugin-perfectionist need the
38
- # JS-API typescript 5/6 to resolve under the name "typescript", so the two
39
- # must coexist. Resolve the aliased package's binary directly both packages
40
- # link a `tsc` bin, so .bin/tsc would be ambiguous.
36
+ # Type checking uses the official TypeScript 7 Go compiler, pulled in through
37
+ # the per-platform @typescript/typescript-* packages instead of a second
38
+ # package named "typescript": typedoc and eslint-plugin-perfectionist load
39
+ # the JS API from the "typescript" name (v6 here), and any typescript@7 in
40
+ # the tree can hijack that lookup under pnpm's hoist fallback.
41
41
  find_tsc() {
42
- if [ -x "$PACKAGE_ROOT/node_modules/typescript-go/bin/tsc" ]; then
43
- echo "$PACKAGE_ROOT/node_modules/typescript-go/bin/tsc"
44
- elif [ -x "$PACKAGE_ROOT/../../typescript-go/bin/tsc" ]; then
45
- echo "$PACKAGE_ROOT/../../typescript-go/bin/tsc"
42
+ local os arch
43
+ case "$(uname -s)" in
44
+ Darwin) os="darwin" ;;
45
+ Linux) os="linux" ;;
46
+ MINGW*|MSYS*|CYGWIN*) os="win32" ;;
47
+ *) os="linux" ;;
48
+ esac
49
+ case "$(uname -m)" in
50
+ arm64|aarch64) arch="arm64" ;;
51
+ armv7l) arch="arm" ;;
52
+ *) arch="x64" ;;
53
+ esac
54
+
55
+ local pkg="@typescript/typescript-$os-$arch"
56
+ if [ -x "$PACKAGE_ROOT/node_modules/$pkg/lib/tsc" ]; then
57
+ echo "$PACKAGE_ROOT/node_modules/$pkg/lib/tsc"
58
+ elif [ -x "$PACKAGE_ROOT/../../$pkg/lib/tsc" ]; then
59
+ echo "$PACKAGE_ROOT/../../$pkg/lib/tsc"
46
60
  else
47
61
  find_binary tsc
48
62
  fi
@@ -52,6 +66,41 @@ TSC=$(find_tsc)
52
66
  OXLINT=$(find_binary oxlint)
53
67
  OXFMT=$(find_binary oxfmt)
54
68
  KNIP=$(find_binary knip)
69
+ CHECKER=$(find_binary jterrazz-test-check)
70
+
71
+ # The @jterrazz/test conventions checker (D4 tokens, C8/C9 fixtures) runs only when the
72
+ # consuming project depends on @jterrazz/test — auto-detected from its package.json.
73
+ project_uses_jterrazz_test() {
74
+ [ -f "package.json" ] || return 1
75
+ node -e 'const p=require("./package.json");const d={...p.dependencies,...p.devDependencies,...p.peerDependencies};process.exit(d["@jterrazz/test"]?0:1)' 2>/dev/null
76
+ }
77
+
78
+ # The @jterrazz/test oxlint plugin is ESM-only. A CommonJS oxlint config silently drops
79
+ # it (oxlint prints a load warning and still exits 0) — none of the jterrazz/* rules run.
80
+ # Warn loudly when that pitfall is detectable.
81
+ warn_cjs_oxlint_config() {
82
+ local cfg=""
83
+ for c in oxlint.config.ts oxlint.config.mjs oxlint.config.cjs oxlint.config.js; do
84
+ [ -f "$c" ] && { cfg="$c"; break; }
85
+ done
86
+ [ -z "$cfg" ] && return 0
87
+
88
+ local is_cjs=false
89
+ case "$cfg" in
90
+ *.cjs) is_cjs=true ;;
91
+ *.js)
92
+ if ! node -e 'process.exit(require("./package.json").type==="module"?0:1)' 2>/dev/null; then
93
+ is_cjs=true
94
+ fi
95
+ ;;
96
+ esac
97
+
98
+ if [ "$is_cjs" = true ]; then
99
+ printf "${RED} WARNING ${NC} @jterrazz/test is installed but %s is CommonJS.\n" "$cfg"
100
+ printf " The @jterrazz/test oxlint plugin is ESM-only and will be SILENTLY DROPPED —\n"
101
+ printf " none of the jterrazz/* rules will run. Switch to an ESM config (oxlint.config.ts or .mjs).\n\n"
102
+ fi
103
+ }
55
104
 
56
105
  # Parse command and args
57
106
  COMMAND=""
@@ -95,6 +144,10 @@ run_checks() {
95
144
 
96
145
  printf "${CYAN_BG}${BRIGHT_WHITE} START ${NC} ${LABEL}\n"
97
146
 
147
+ if project_uses_jterrazz_test; then
148
+ warn_cjs_oxlint_config
149
+ fi
150
+
98
151
  # Run all tools in parallel
99
152
  "$TSC" --noEmit > "$tmp_dir/type.log" 2>&1 &
100
153
  local type_pid=$!
@@ -128,33 +181,71 @@ run_checks() {
128
181
  knip_pid=$!
129
182
  fi
130
183
 
184
+ # Conventions checker: only in check mode, only when the project uses @jterrazz/test
185
+ # and has a specs/ directory to validate.
186
+ local checker_pid=""
187
+ local checker_status=0
188
+ if [ "$FIX_MODE" = false ] && [ -d "specs" ] && project_uses_jterrazz_test; then
189
+ "$CHECKER" specs > "$tmp_dir/checker.log" 2>&1 &
190
+ checker_pid=$!
191
+ fi
192
+
131
193
  # Wait and collect statuses
132
194
  wait $type_pid; local type_status=$?
133
195
  wait $lint_pid; local lint_status=$?
134
196
  wait $format_pid; local format_status=$?
135
197
  [ -n "$knip_pid" ] && { wait $knip_pid; knip_status=$?; }
198
+ [ -n "$checker_pid" ] && { wait $checker_pid; checker_status=$?; }
136
199
 
137
- # Print results
200
+ # Print results — quiet on success, verbose on failure: a tool's captured log
201
+ # is shown only when it failed, so green output stays byte-identical across
202
+ # platforms (some tool builds print success chatter on Linux but not macOS).
138
203
  printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} TypeScript Check\n\n"
139
- [ -s "$tmp_dir/type.log" ] && cat "$tmp_dir/type.log"
140
- [ $type_status -ne 0 ] && printf "${RED}✗ Failed with exit code %d${NC}\n" $type_status || printf "${GREEN}✓ Passed${NC}\n"
204
+ if [ $type_status -ne 0 ]; then
205
+ [ -s "$tmp_dir/type.log" ] && cat "$tmp_dir/type.log"
206
+ printf "${RED}✗ Failed with exit code %d${NC}\n" $type_status
207
+ else
208
+ printf "${GREEN}✓ Passed${NC}\n"
209
+ fi
141
210
 
142
211
  local lint_label="Oxlint Check"
143
212
  [ "$FIX_MODE" = true ] && lint_label="Oxlint Fix"
144
213
  printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} ${lint_label}\n\n"
145
- [ -s "$tmp_dir/lint.log" ] && cat "$tmp_dir/lint.log"
146
- [ $lint_status -ne 0 ] && printf "${RED}✗ Failed with exit code %d${NC}\n" $lint_status || printf "${GREEN}✓ Passed${NC}\n"
214
+ if [ $lint_status -ne 0 ]; then
215
+ [ -s "$tmp_dir/lint.log" ] && cat "$tmp_dir/lint.log"
216
+ printf "${RED}✗ Failed with exit code %d${NC}\n" $lint_status
217
+ else
218
+ printf "${GREEN}✓ Passed${NC}\n"
219
+ fi
147
220
 
148
221
  local format_label="Oxfmt Check"
149
222
  [ "$FIX_MODE" = true ] && format_label="Oxfmt Format"
150
223
  printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} ${format_label}\n\n"
151
- [ -s "$tmp_dir/format.log" ] && cat "$tmp_dir/format.log"
152
- [ $format_status -ne 0 ] && printf "${RED}✗ Failed with exit code %d${NC}\n" $format_status || printf "${GREEN}✓ Passed${NC}\n"
224
+ if [ $format_status -ne 0 ]; then
225
+ [ -s "$tmp_dir/format.log" ] && cat "$tmp_dir/format.log"
226
+ printf "${RED}✗ Failed with exit code %d${NC}\n" $format_status
227
+ else
228
+ printf "${GREEN}✓ Passed${NC}\n"
229
+ fi
153
230
 
154
231
  if [ "$FIX_MODE" = false ]; then
155
232
  printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Knip (unused code)\n\n"
156
- [ -s "$tmp_dir/knip.log" ] && cat "$tmp_dir/knip.log"
157
- [ $knip_status -ne 0 ] && printf "${RED}✗ Failed with exit code %d${NC}\n" $knip_status || printf "${GREEN}✓ Passed${NC}\n"
233
+ if [ $knip_status -ne 0 ]; then
234
+ [ -s "$tmp_dir/knip.log" ] && cat "$tmp_dir/knip.log"
235
+ printf "${RED}✗ Failed with exit code %d${NC}\n" $knip_status
236
+ else
237
+ printf "${GREEN}✓ Passed${NC}\n"
238
+ fi
239
+
240
+ if [ -n "$checker_pid" ]; then
241
+ printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Test Conventions (@jterrazz/test)\n\n"
242
+ if [ $checker_status -ne 0 ]; then
243
+ [ -s "$tmp_dir/checker.log" ] && cat "$tmp_dir/checker.log"
244
+ printf "${RED}✗ Failed with exit code %d${NC}\n" $checker_status
245
+ else
246
+ printf "${GREEN}✓ Passed${NC}\n"
247
+ fi
248
+ fi
158
249
  fi
159
250
 
160
251
  # Summary
@@ -164,7 +255,7 @@ run_checks() {
164
255
  printf "\n${CYAN_BG}${BRIGHT_WHITE} END ${NC} Finalizing quality checks\n\n"
165
256
  fi
166
257
 
167
- if [ $type_status -eq 0 ] && [ $lint_status -eq 0 ] && [ $format_status -eq 0 ] && [ $knip_status -eq 0 ]; then
258
+ if [ $type_status -eq 0 ] && [ $lint_status -eq 0 ] && [ $format_status -eq 0 ] && [ $knip_status -eq 0 ] && [ $checker_status -eq 0 ]; then
168
259
  printf "${GREEN}✓ All checks passed${NC}\n"
169
260
  exit 0
170
261
  else
@@ -75,7 +75,7 @@ if (existsSync('docs')) {
75
75
  }
76
76
 
77
77
  // Scan for fixtures/** and expected/** directories anywhere in the tree (up to 3 levels)
78
- const scanDirs = ['tests', 'test', 'src'];
78
+ const scanDirs = ['specs', 'tests', 'test', 'src'];
79
79
  for (const root of scanDirs) {
80
80
  if (!existsSync(root)) {
81
81
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/typescript",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
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
+ "./oxlint": {
27
+ "types": "./src/oxlint.d.ts",
28
+ "default": "./src/oxlint.js"
29
+ },
26
30
  "./tsconfig/*": "./presets/tsconfig/*.json",
27
31
  "./tsconfig/*.json": "./presets/tsconfig/*.json",
28
32
  "./tsdown/*": "./presets/tsdown/*.js",
@@ -49,12 +53,20 @@
49
53
  "tsdown": "^0.22.9",
50
54
  "typedoc": "^0.28.20",
51
55
  "typedoc-plugin-markdown": "^4.12.0",
52
- "typescript": "^6.0.0",
53
- "typescript-go": "npm:typescript@^7.0.2"
56
+ "typescript": "^6.0.0"
54
57
  },
55
58
  "devDependencies": {
56
- "@jterrazz/test": "^8.0.0",
59
+ "@jterrazz/test": "^9.0.0",
57
60
  "@types/node": "^26.1.1",
58
61
  "vitest": "^4.1.10"
62
+ },
63
+ "optionalDependencies": {
64
+ "@typescript/typescript-darwin-arm64": "^7.0.2",
65
+ "@typescript/typescript-darwin-x64": "^7.0.2",
66
+ "@typescript/typescript-linux-arm": "^7.0.2",
67
+ "@typescript/typescript-linux-arm64": "^7.0.2",
68
+ "@typescript/typescript-linux-x64": "^7.0.2",
69
+ "@typescript/typescript-win32-arm64": "^7.0.2",
70
+ "@typescript/typescript-win32-x64": "^7.0.2"
59
71
  }
60
72
  }
@@ -4,6 +4,19 @@ import { defineConfig } from 'oxlint';
4
4
  const require = createRequire(import.meta.url);
5
5
  const perfectionistPath = require.resolve('eslint-plugin-perfectionist');
6
6
 
7
+ /*
8
+ * Base lint preset — tooling rules only. Wiring is EXPLICIT: this preset never
9
+ * auto-detects other jterrazz packages. A project using @jterrazz/test composes
10
+ * its `testing` fragment itself:
11
+ *
12
+ * import { testing } from '@jterrazz/test/oxlint';
13
+ * import { compose, node } from '@jterrazz/typescript/oxlint';
14
+ *
15
+ * export default compose(node, testing);
16
+ *
17
+ * (The @jterrazz/test plugin is ESM-only: a CommonJS oxlint config silently
18
+ * drops it — `typescript check` still warns loudly about that pitfall.)
19
+ */
7
20
  export default defineConfig({
8
21
  plugins: ['typescript', 'import', 'oxc', 'unicorn'],
9
22
  jsPlugins: [perfectionistPath],
@@ -0,0 +1,15 @@
1
+ type ConfigObject = Record<string, unknown>;
2
+
3
+ declare const expo: ConfigObject;
4
+ declare const hexagonal: ConfigObject;
5
+ declare const next: ConfigObject;
6
+ declare const node: ConfigObject;
7
+
8
+ /**
9
+ * Deterministic merge of oxlint config fragments, left to right:
10
+ * jsPlugins/plugins/ignorePatterns/extends concatenated + deduped,
11
+ * rules/categories shallow-merged (last wins), overrides concatenated.
12
+ */
13
+ declare function compose(...fragments: ConfigObject[]): ConfigObject;
14
+
15
+ export { compose, expo, hexagonal, next, node };
package/src/oxlint.js ADDED
@@ -0,0 +1,56 @@
1
+ /*
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:
5
+ *
6
+ * import { testing } from '@jterrazz/test/oxlint';
7
+ * import { compose, node } from '@jterrazz/typescript/oxlint';
8
+ *
9
+ * export default compose(node, testing);
10
+ */
11
+
12
+ /** Config keys concatenated across fragments, duplicates dropped (===). */
13
+ const CONCAT_DEDUPE = new Set(['extends', 'ignorePatterns', 'jsPlugins', 'plugins']);
14
+ /** Config keys concatenated verbatim (order matters, no dedupe). */
15
+ const CONCAT = new Set(['overrides']);
16
+ /** Config keys shallow-merged as objects — the LAST fragment wins per key. */
17
+ const SHALLOW_MERGE = new Set(['categories', 'env', 'globals', 'rules', 'settings']);
18
+
19
+ /**
20
+ * Deterministic merge of oxlint config fragments, left to right:
21
+ * `jsPlugins` / `plugins` / `ignorePatterns` / `extends` are concatenated and
22
+ * deduped, `rules` / `categories` (and env/globals/settings) are shallow-merged
23
+ * with last-wins per key, `overrides` are concatenated, and any other key is
24
+ * taken from the last fragment that sets it.
25
+ */
26
+ export function compose(...fragments) {
27
+ const merged = {};
28
+ for (const fragment of fragments) {
29
+ if (!fragment || typeof fragment !== 'object') {
30
+ continue;
31
+ }
32
+ for (const [key, value] of Object.entries(fragment)) {
33
+ if (value === undefined) {
34
+ continue;
35
+ }
36
+ if (CONCAT_DEDUPE.has(key)) {
37
+ const previous = Array.isArray(merged[key]) ? merged[key] : [];
38
+ const combined = [...previous, ...(Array.isArray(value) ? value : [value])];
39
+ merged[key] = combined.filter((entry, index) => combined.indexOf(entry) === index);
40
+ } else if (CONCAT.has(key)) {
41
+ const previous = Array.isArray(merged[key]) ? merged[key] : [];
42
+ merged[key] = [...previous, ...(Array.isArray(value) ? value : [value])];
43
+ } else if (SHALLOW_MERGE.has(key)) {
44
+ merged[key] = { ...merged[key], ...value };
45
+ } else {
46
+ merged[key] = value;
47
+ }
48
+ }
49
+ }
50
+ return merged;
51
+ }
52
+
53
+ export { default as hexagonal } from '../presets/oxlint/architectures/hexagonal.js';
54
+ export { default as expo } from '../presets/oxlint/expo.js';
55
+ export { default as next } from '../presets/oxlint/next.js';
56
+ export { default as node } from '../presets/oxlint/node.js';
@@ -0,0 +1,79 @@
1
+ import { expect, test } from 'vitest';
2
+
3
+ import { compose, expo, hexagonal, next, node } from './oxlint.js';
4
+
5
+ test('concatenates and dedupes plugin lists', () => {
6
+ // Given - two fragments sharing one jsPlugin
7
+ const merged = compose(
8
+ { jsPlugins: ['a', 'b'], plugins: ['typescript'] },
9
+ { jsPlugins: ['b', 'c'], plugins: ['typescript', 'import'] },
10
+ );
11
+
12
+ // Then - order preserved, duplicates dropped
13
+ expect(merged.jsPlugins).toEqual(['a', 'b', 'c']);
14
+ expect(merged.plugins).toEqual(['typescript', 'import']);
15
+ });
16
+
17
+ test('shallow-merges rules with last fragment winning', () => {
18
+ // Given - two fragments disagreeing on one rule
19
+ const merged = compose(
20
+ { rules: { curly: 'error', 'no-ternary': 'off' } },
21
+ { rules: { curly: 'off', 'jterrazz/b4-given-then': 'error' } },
22
+ );
23
+
24
+ // Then - the later fragment wins per key, others survive
25
+ expect(merged.rules).toEqual({
26
+ curly: 'off',
27
+ 'jterrazz/b4-given-then': 'error',
28
+ 'no-ternary': 'off',
29
+ });
30
+ });
31
+
32
+ test('concatenates overrides in order without deduping', () => {
33
+ // Given - two fragments each shipping an override
34
+ const first = { files: ['**/*.specification.ts'], rules: {} };
35
+ const second = { files: ['src/**'], rules: {} };
36
+ const merged = compose({ overrides: [first] }, { overrides: [second] });
37
+
38
+ // Then - both overrides survive, in composition order
39
+ expect(merged.overrides).toEqual([first, second]);
40
+ });
41
+
42
+ test('concatenates and dedupes ignorePatterns', () => {
43
+ // Given - overlapping ignore lists
44
+ const merged = compose(
45
+ { ignorePatterns: ['dist/**', 'node_modules/**'] },
46
+ { ignorePatterns: ['node_modules/**', '**/fixtures/**'] },
47
+ );
48
+
49
+ // Then - one entry each
50
+ expect(merged.ignorePatterns).toEqual(['dist/**', 'node_modules/**', '**/fixtures/**']);
51
+ });
52
+
53
+ test('takes unknown scalar keys from the last fragment', () => {
54
+ // Given - fragments disagreeing on a scalar key
55
+ const merged = compose({ somethingElse: 1 }, { somethingElse: 2 });
56
+
57
+ // Then - last wins
58
+ expect(merged.somethingElse).toBe(2);
59
+ });
60
+
61
+ test('ignores null and undefined fragments', () => {
62
+ // Given - a composition with holes (conditional fragments)
63
+ const merged = compose(
64
+ undefined as unknown as Record<string, unknown>,
65
+ { rules: { curly: 'error' } },
66
+ null as unknown as Record<string, unknown>,
67
+ );
68
+
69
+ // Then - the holes contribute nothing
70
+ expect(merged.rules).toEqual({ curly: 'error' });
71
+ });
72
+
73
+ test('exports the named presets', () => {
74
+ // Given - the tool-facing entry
75
+ // Then - every preset is a config object
76
+ for (const preset of [node, expo, next, hexagonal]) {
77
+ expect(typeof preset).toBe('object');
78
+ }
79
+ });