@jterrazz/typescript 8.0.0 → 8.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/bin/commands/check.sh +166 -22
- package/bin/typescript.sh +45 -4
- package/lib/merge-knip-config.js +27 -11
- package/lib/workspace-members.js +125 -0
- package/package.json +1 -1
- package/presets/oxlint/plugins/codestyle.js +38 -1
- package/presets/tsconfig/expo.json +2 -2
- package/presets/tsconfig/node.json +2 -2
package/bin/commands/check.sh
CHANGED
|
@@ -68,11 +68,110 @@ OXFMT=$(find_binary oxfmt)
|
|
|
68
68
|
KNIP=$(find_binary knip)
|
|
69
69
|
CHECKER=$(find_binary jterrazz-test-check)
|
|
70
70
|
|
|
71
|
+
# ── The unit is the workspace package, not the repository ────────────────────
|
|
72
|
+
# Every gate measures from the NEAREST package.json. A single-package project
|
|
73
|
+
# has exactly one — the cwd — and nothing below changes for it. A workspace
|
|
74
|
+
# root has one per member, and the per-member gates run once per member
|
|
75
|
+
# instead of once for a root that owns neither the specs nor the docs.
|
|
76
|
+
#
|
|
77
|
+
# Root-only by nature, and deliberately left alone: tsc, oxlint and oxfmt
|
|
78
|
+
# measure from their CONFIG file, not from a package, and each already walks
|
|
79
|
+
# the whole tree from the cwd; knip is natively workspace-aware, so a member's
|
|
80
|
+
# knip config belongs under the root config's `workspaces` key, not in a second
|
|
81
|
+
# invocation.
|
|
82
|
+
WORKSPACE_MEMBERS=()
|
|
83
|
+
while IFS= read -r workspace_member; do
|
|
84
|
+
[ -n "$workspace_member" ] && WORKSPACE_MEMBERS+=("$workspace_member")
|
|
85
|
+
done < <(node "$PACKAGE_ROOT/lib/workspace-members.js" 2>/dev/null)
|
|
86
|
+
|
|
87
|
+
# The nearest package.json OWNS a directory. Walk up from the given path and
|
|
88
|
+
# stop at the cwd — a gate never asks a question above the project it runs in.
|
|
89
|
+
nearest_package_dir() {
|
|
90
|
+
local dir="$1"
|
|
91
|
+
while true; do
|
|
92
|
+
if [ -f "$dir/package.json" ]; then
|
|
93
|
+
printf '%s\n' "$dir"
|
|
94
|
+
return 0
|
|
95
|
+
fi
|
|
96
|
+
[ "$dir" = "." ] && return 1
|
|
97
|
+
dir=$(dirname "$dir")
|
|
98
|
+
done
|
|
99
|
+
}
|
|
100
|
+
|
|
71
101
|
# The @jterrazz/test conventions checker (D4 tokens, C8/C9 fixtures) runs only when the
|
|
72
|
-
#
|
|
102
|
+
# owning package depends on @jterrazz/test — auto-detected from its package.json.
|
|
73
103
|
project_uses_jterrazz_test() {
|
|
74
|
-
|
|
75
|
-
|
|
104
|
+
local dir="${1:-.}"
|
|
105
|
+
[ -f "$dir/package.json" ] || return 1
|
|
106
|
+
node -e 'const {readFileSync}=require("node:fs");const p=JSON.parse(readFileSync(process.argv[1],"utf8"));const d={...p.dependencies,...p.devDependencies,...p.peerDependencies};process.exit(d["@jterrazz/test"]?0:1)' "$dir/package.json" 2>/dev/null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# In a workspace the dependency may sit on a member alone — the warning below
|
|
110
|
+
# is about the ROOT oxlint config, but the reason to print it is anywhere.
|
|
111
|
+
workspace_uses_jterrazz_test() {
|
|
112
|
+
project_uses_jterrazz_test "." && return 0
|
|
113
|
+
local member
|
|
114
|
+
for member in "${WORKSPACE_MEMBERS[@]}"; do
|
|
115
|
+
project_uses_jterrazz_test "$member" && return 0
|
|
116
|
+
done
|
|
117
|
+
return 1
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# A path git has been told to forget is not this workspace's source. Clones,
|
|
121
|
+
# workbenches and build output live under gitignored paths, and the conventions
|
|
122
|
+
# checker walks whatever root it is handed — so the filter belongs here, before
|
|
123
|
+
# the handing over. Outside a git tree the question has no answer, and the
|
|
124
|
+
# non-zero exit reads as "not ignored", which is the right default.
|
|
125
|
+
path_is_gitignored() {
|
|
126
|
+
git check-ignore --quiet "$1" 2>/dev/null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# A discovered root must belong to the member that produced it. Walk back up
|
|
130
|
+
# from the candidate: a nested `.git`, or a package.json no workspace glob
|
|
131
|
+
# claims, means the walk crossed OUT of this workspace into a foreign tree —
|
|
132
|
+
# a vendored dependency, a sibling clone — whose conventions are not ours.
|
|
133
|
+
inside_owning_member() {
|
|
134
|
+
local dir member="$2"
|
|
135
|
+
dir=$(dirname "$1")
|
|
136
|
+
while [ "$dir" != "$member" ] && [ "$dir" != "." ] && [ "$dir" != "/" ]; do
|
|
137
|
+
if [ -e "$dir/.git" ] || [ -f "$dir/package.json" ]; then
|
|
138
|
+
return 1
|
|
139
|
+
fi
|
|
140
|
+
dir=$(dirname "$dir")
|
|
141
|
+
done
|
|
142
|
+
return 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Every specs root the workspace owns: the root's own, plus the first one found
|
|
146
|
+
# At or below each member (a member that nests its facet — web/specs — counts).
|
|
147
|
+
# Never descends INTO a specs tree: the fixtures under it are not specs roots.
|
|
148
|
+
discover_specs_roots() {
|
|
149
|
+
{
|
|
150
|
+
[ -d "specs" ] && ! path_is_gitignored "specs" && printf '%s\n' "specs"
|
|
151
|
+
local member candidate
|
|
152
|
+
for member in "${WORKSPACE_MEMBERS[@]}"; do
|
|
153
|
+
while IFS= read -r candidate; do
|
|
154
|
+
[ -n "$candidate" ] || continue
|
|
155
|
+
path_is_gitignored "$candidate" && continue
|
|
156
|
+
inside_owning_member "$candidate" "$member" || continue
|
|
157
|
+
printf '%s\n' "$candidate"
|
|
158
|
+
done < <(find "$member" \
|
|
159
|
+
\( -name node_modules -o -name dist -o -name .git \) -prune -o \
|
|
160
|
+
-type d -name specs -prune -print 2>/dev/null)
|
|
161
|
+
done
|
|
162
|
+
} | LC_ALL=C sort -u
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Every package that owns a committed docs projection. A package's docs sit at
|
|
166
|
+
# its own root — that IS the nearest-package.json rule, so no walk is needed.
|
|
167
|
+
discover_docs_roots() {
|
|
168
|
+
{
|
|
169
|
+
[ -d "docs/reference" ] && printf '%s\n' "."
|
|
170
|
+
local member
|
|
171
|
+
for member in "${WORKSPACE_MEMBERS[@]}"; do
|
|
172
|
+
[ -d "$member/docs/reference" ] && printf '%s\n' "$member"
|
|
173
|
+
done
|
|
174
|
+
} | LC_ALL=C sort -u
|
|
76
175
|
}
|
|
77
176
|
|
|
78
177
|
# The @jterrazz/test oxlint plugin is ESM-only. A CommonJS oxlint config silently drops
|
|
@@ -144,7 +243,7 @@ run_checks() {
|
|
|
144
243
|
|
|
145
244
|
printf "${CYAN_BG}${BRIGHT_WHITE} START ${NC} ${LABEL}\n"
|
|
146
245
|
|
|
147
|
-
if
|
|
246
|
+
if workspace_uses_jterrazz_test; then
|
|
148
247
|
warn_cjs_oxlint_config
|
|
149
248
|
fi
|
|
150
249
|
|
|
@@ -167,7 +266,9 @@ run_checks() {
|
|
|
167
266
|
local format_pid=$!
|
|
168
267
|
|
|
169
268
|
# Knip: only run in check mode (fix mode is destructive)
|
|
170
|
-
# Merge base config (from this package) with optional project-local knip.json
|
|
269
|
+
# Merge base config (from this package) with optional project-local knip.json.
|
|
270
|
+
# Root-only on purpose: knip reads the workspace globs itself and reports per
|
|
271
|
+
# member from one run — a second invocation per member would double-report.
|
|
171
272
|
local knip_pid=""
|
|
172
273
|
local knip_status=0
|
|
173
274
|
if [ "$FIX_MODE" = false ]; then
|
|
@@ -181,24 +282,43 @@ run_checks() {
|
|
|
181
282
|
knip_pid=$!
|
|
182
283
|
fi
|
|
183
284
|
|
|
184
|
-
# Conventions checker: only in check mode,
|
|
185
|
-
#
|
|
186
|
-
|
|
285
|
+
# Conventions checker: only in check mode, once per specs root the workspace
|
|
286
|
+
# owns, gated by the package that OWNS that root — a member may depend on
|
|
287
|
+
# @jterrazz/test while the root does not, and the reverse.
|
|
288
|
+
local checker_pids=()
|
|
289
|
+
local checker_logs=()
|
|
187
290
|
local checker_status=0
|
|
188
|
-
if [ "$FIX_MODE" = false ]
|
|
189
|
-
|
|
190
|
-
|
|
291
|
+
if [ "$FIX_MODE" = false ]; then
|
|
292
|
+
local checker_index=0
|
|
293
|
+
while IFS= read -r specs_root; do
|
|
294
|
+
[ -n "$specs_root" ] || continue
|
|
295
|
+
local owner
|
|
296
|
+
owner=$(nearest_package_dir "$(dirname "$specs_root")") || continue
|
|
297
|
+
project_uses_jterrazz_test "$owner" || continue
|
|
298
|
+
"$CHECKER" "$specs_root" > "$tmp_dir/checker-$checker_index.log" 2>&1 &
|
|
299
|
+
checker_pids+=($!)
|
|
300
|
+
checker_logs+=("$tmp_dir/checker-$checker_index.log")
|
|
301
|
+
checker_index=$((checker_index + 1))
|
|
302
|
+
done < <(discover_specs_roots)
|
|
191
303
|
fi
|
|
192
304
|
|
|
193
|
-
# Docs (sync): only in check mode, and only
|
|
305
|
+
# Docs (sync): only in check mode, and only for a package that has generated
|
|
194
306
|
# its committed docs (docs/reference/ exists — opt-in by first generation).
|
|
195
307
|
# Delegates to docs.sh --check: regenerate into a temp dir, diff the
|
|
196
308
|
# committed projections. Never duplicates the compiler's logic.
|
|
197
|
-
local
|
|
309
|
+
local docs_pids=()
|
|
310
|
+
local docs_logs=()
|
|
198
311
|
local docs_status=0
|
|
199
|
-
if [ "$FIX_MODE" = false ]
|
|
200
|
-
|
|
201
|
-
|
|
312
|
+
if [ "$FIX_MODE" = false ]; then
|
|
313
|
+
local docs_index=0
|
|
314
|
+
while IFS= read -r docs_root; do
|
|
315
|
+
[ -n "$docs_root" ] || continue
|
|
316
|
+
bash "$SCRIPT_DIR/docs.sh" "$(cd "$docs_root" && pwd)" "$PACKAGE_ROOT" --check \
|
|
317
|
+
> "$tmp_dir/docs-$docs_index.log" 2>&1 &
|
|
318
|
+
docs_pids+=($!)
|
|
319
|
+
docs_logs+=("$tmp_dir/docs-$docs_index.log")
|
|
320
|
+
docs_index=$((docs_index + 1))
|
|
321
|
+
done < <(discover_docs_roots)
|
|
202
322
|
fi
|
|
203
323
|
|
|
204
324
|
# Wait and collect statuses
|
|
@@ -206,8 +326,28 @@ run_checks() {
|
|
|
206
326
|
wait $lint_pid; local lint_status=$?
|
|
207
327
|
wait $format_pid; local format_status=$?
|
|
208
328
|
[ -n "$knip_pid" ] && { wait $knip_pid; knip_status=$?; }
|
|
209
|
-
|
|
210
|
-
|
|
329
|
+
|
|
330
|
+
# One pass, N runs: the pass fails if any run failed, and only the logs of
|
|
331
|
+
# the runs that FAILED are printed — a green member stays silent.
|
|
332
|
+
local checker_failed_logs=()
|
|
333
|
+
local index=0
|
|
334
|
+
for pid in "${checker_pids[@]}"; do
|
|
335
|
+
if ! wait "$pid"; then
|
|
336
|
+
checker_status=1
|
|
337
|
+
checker_failed_logs+=("${checker_logs[$index]}")
|
|
338
|
+
fi
|
|
339
|
+
index=$((index + 1))
|
|
340
|
+
done
|
|
341
|
+
|
|
342
|
+
local docs_failed_logs=()
|
|
343
|
+
index=0
|
|
344
|
+
for pid in "${docs_pids[@]}"; do
|
|
345
|
+
if ! wait "$pid"; then
|
|
346
|
+
docs_status=1
|
|
347
|
+
docs_failed_logs+=("${docs_logs[$index]}")
|
|
348
|
+
fi
|
|
349
|
+
index=$((index + 1))
|
|
350
|
+
done
|
|
211
351
|
|
|
212
352
|
# Print results — quiet on success, verbose on failure: a tool's captured log
|
|
213
353
|
# is shown only when it failed, so green output stays byte-identical across
|
|
@@ -249,20 +389,24 @@ run_checks() {
|
|
|
249
389
|
printf "${GREEN}✓ Passed${NC}\n"
|
|
250
390
|
fi
|
|
251
391
|
|
|
252
|
-
if [ -
|
|
392
|
+
if [ ${#checker_pids[@]} -gt 0 ]; then
|
|
253
393
|
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Test Conventions (@jterrazz/test)\n\n"
|
|
254
394
|
if [ $checker_status -ne 0 ]; then
|
|
255
|
-
|
|
395
|
+
for log in "${checker_failed_logs[@]}"; do
|
|
396
|
+
[ -s "$log" ] && cat "$log"
|
|
397
|
+
done
|
|
256
398
|
printf "${RED}✗ Failed with exit code %d${NC}\n" $checker_status
|
|
257
399
|
else
|
|
258
400
|
printf "${GREEN}✓ Passed${NC}\n"
|
|
259
401
|
fi
|
|
260
402
|
fi
|
|
261
403
|
|
|
262
|
-
if [ -
|
|
404
|
+
if [ ${#docs_pids[@]} -gt 0 ]; then
|
|
263
405
|
printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Docs (sync)\n\n"
|
|
264
406
|
if [ $docs_status -ne 0 ]; then
|
|
265
|
-
|
|
407
|
+
for log in "${docs_failed_logs[@]}"; do
|
|
408
|
+
[ -s "$log" ] && cat "$log"
|
|
409
|
+
done
|
|
266
410
|
printf "${RED}✗ Failed with exit code %d${NC}\n" $docs_status
|
|
267
411
|
else
|
|
268
412
|
printf "${GREEN}✓ Passed${NC}\n"
|
package/bin/typescript.sh
CHANGED
|
@@ -88,18 +88,59 @@ case "$COMMAND" in
|
|
|
88
88
|
;;
|
|
89
89
|
|
|
90
90
|
docs)
|
|
91
|
+
# The unit is the PACKAGE. A projection is compiled from a package's own
|
|
92
|
+
# Barrel against its own tsconfig into its own docs/ — three things a
|
|
93
|
+
# Workspace root does not have, and each member does. docs.sh was always
|
|
94
|
+
# Parameterised by a project root; what was missing is the list.
|
|
95
|
+
owns_a_projection() {
|
|
96
|
+
local root="$1"
|
|
97
|
+
[ -d "$root/docs" ] || return 1
|
|
98
|
+
[ -f "$root/src/index.ts" ] || [ -f "$root/src/index.d.ts" ]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
DOCS_UNITS=()
|
|
102
|
+
if owns_a_projection "$PROJECT_ROOT"; then
|
|
103
|
+
DOCS_UNITS+=("$PROJECT_ROOT")
|
|
104
|
+
fi
|
|
105
|
+
while IFS= read -r docs_member; do
|
|
106
|
+
[ -n "$docs_member" ] || continue
|
|
107
|
+
if owns_a_projection "$PROJECT_ROOT/$docs_member"; then
|
|
108
|
+
DOCS_UNITS+=("$PROJECT_ROOT/$docs_member")
|
|
109
|
+
fi
|
|
110
|
+
done < <(node "$PACKAGE_ROOT/lib/workspace-members.js" "$PROJECT_ROOT" 2>/dev/null)
|
|
111
|
+
|
|
112
|
+
# Nothing qualified: hand the project root over anyway, so the compiler's
|
|
113
|
+
# Own diagnostic is what the operator reads.
|
|
114
|
+
if [ ${#DOCS_UNITS[@]} -eq 0 ]; then
|
|
115
|
+
DOCS_UNITS=("$PROJECT_ROOT")
|
|
116
|
+
fi
|
|
117
|
+
|
|
91
118
|
if [ "${1:-}" = "--check" ]; then
|
|
92
119
|
printf "${CYAN_BG}${BRIGHT_WHITE} TYPESCRIPT ${NC} Checking docs are in sync...\n\n"
|
|
93
120
|
|
|
94
|
-
|
|
121
|
+
for docs_unit in "${DOCS_UNITS[@]}"; do
|
|
122
|
+
if [ ${#DOCS_UNITS[@]} -gt 1 ]; then
|
|
123
|
+
printf "%s\n" "${docs_unit#"$PROJECT_ROOT/"}"
|
|
124
|
+
fi
|
|
125
|
+
bash "$SCRIPT_DIR/commands/docs.sh" "$docs_unit" "$PACKAGE_ROOT" --check
|
|
126
|
+
done
|
|
95
127
|
|
|
96
128
|
printf "${GREEN}Docs are in sync${NC}\n"
|
|
97
129
|
else
|
|
98
130
|
printf "${CYAN_BG}${BRIGHT_WHITE} TYPESCRIPT ${NC} Generating API docs...\n\n"
|
|
99
131
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
132
|
+
for docs_unit in "${DOCS_UNITS[@]}"; do
|
|
133
|
+
if [ ${#DOCS_UNITS[@]} -gt 1 ]; then
|
|
134
|
+
printf "%s\n" "${docs_unit#"$PROJECT_ROOT/"}"
|
|
135
|
+
fi
|
|
136
|
+
bash "$SCRIPT_DIR/commands/docs.sh" "$docs_unit" "$PACKAGE_ROOT"
|
|
137
|
+
done
|
|
138
|
+
|
|
139
|
+
if [ ${#DOCS_UNITS[@]} -gt 1 ]; then
|
|
140
|
+
printf "\n${GREEN}Docs generated for %d packages${NC}\n" ${#DOCS_UNITS[@]}
|
|
141
|
+
else
|
|
142
|
+
printf "\n${GREEN}Docs generated at docs/${NC}\n"
|
|
143
|
+
fi
|
|
103
144
|
fi
|
|
104
145
|
;;
|
|
105
146
|
|
package/lib/merge-knip-config.js
CHANGED
|
@@ -14,12 +14,21 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Dynamic detection:
|
|
16
16
|
* - Published library (main/exports/publishConfig) → disable exports/types/files rules
|
|
17
|
-
* - Auto-ignore docs/**, fixtures/**, expected/** convention paths
|
|
17
|
+
* - Auto-ignore docs/**, fixtures/**, expected/** convention paths — at the root
|
|
18
|
+
* AND inside every workspace member, because the convention is the package's
|
|
19
|
+
* and knip resolves `ignore` globs from the root
|
|
18
20
|
* - Vitest workspace workaround → explicit vitest config when vitest is a dependency
|
|
21
|
+
*
|
|
22
|
+
* Knip itself is workspace-aware: it reads the `workspaces` globs and reports per
|
|
23
|
+
* member from one run. A consumer that needs per-member knip settings spells them
|
|
24
|
+
* under a `workspaces` key in its own knip.json — the base preset has none, so the
|
|
25
|
+
* key passes through the merge untouched.
|
|
19
26
|
*/
|
|
20
27
|
|
|
21
28
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
22
29
|
|
|
30
|
+
import { workspaceMembers } from './workspace-members.js';
|
|
31
|
+
|
|
23
32
|
const basePath = process.argv[2];
|
|
24
33
|
const projectPath = process.argv[3];
|
|
25
34
|
|
|
@@ -68,19 +77,26 @@ if (pkg.main || pkg.exports || pkg.publishConfig) {
|
|
|
68
77
|
merged.rules = { exports: 'off', files: 'off', types: 'off', ...merged.rules };
|
|
69
78
|
}
|
|
70
79
|
|
|
71
|
-
// 2. Auto-ignore convention paths that exist on disk
|
|
80
|
+
// 2. Auto-ignore convention paths that exist on disk.
|
|
81
|
+
// The convention belongs to a PACKAGE, so the scan runs once per unit — the
|
|
82
|
+
// Root and each workspace member. Knip resolves `ignore` globs from the root,
|
|
83
|
+
// So a member's paths are emitted with their member prefix.
|
|
72
84
|
const conventionIgnores = [];
|
|
73
|
-
|
|
74
|
-
conventionIgnores.push('docs/**');
|
|
75
|
-
}
|
|
85
|
+
const units = ['.', ...workspaceMembers('.')];
|
|
76
86
|
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
87
|
+
for (const unit of units) {
|
|
88
|
+
const prefix = unit === '.' ? '' : `${unit}/`;
|
|
89
|
+
|
|
90
|
+
if (existsSync(`${prefix}docs`)) {
|
|
91
|
+
conventionIgnores.push(`${prefix}docs/**`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Scan for fixtures/** and expected/** directories anywhere in the unit (up to 3 levels)
|
|
95
|
+
for (const scanDir of ['specs', 'tests', 'test', 'src']) {
|
|
96
|
+
if (existsSync(`${prefix}${scanDir}`)) {
|
|
97
|
+
findConventionDirs(`${prefix}${scanDir}`, 0);
|
|
98
|
+
}
|
|
82
99
|
}
|
|
83
|
-
findConventionDirs(root, 0);
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
function findConventionDirs(dir, depth) {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The workspace members declared by a package.json — the unit every
|
|
5
|
+
* per-package gate measures from.
|
|
6
|
+
*
|
|
7
|
+
* As a module: `workspaceMembers(root)` returns the member directories,
|
|
8
|
+
* relative to the root, no trailing slash, sorted. As a CLI, the same list on
|
|
9
|
+
* stdout, one per line:
|
|
10
|
+
*
|
|
11
|
+
* node workspace-members.js [root]
|
|
12
|
+
*
|
|
13
|
+
* The toolchain's unit is the workspace PACKAGE, not the repository: a gate
|
|
14
|
+
* that reads `specs/` or `docs/` at cwd sees only the root's, and a monorepo's
|
|
15
|
+
* members are silently never checked. Callers ask here what the members are;
|
|
16
|
+
* the list is empty when the package declares no `workspaces`, and the caller
|
|
17
|
+
* falls back to the root itself.
|
|
18
|
+
*
|
|
19
|
+
* Supported declarations (the npm/yarn/bun surface):
|
|
20
|
+
* "workspaces": ["apps/*", "packages/*"]
|
|
21
|
+
* "workspaces": { "packages": ["apps/*"] } // yarn classic
|
|
22
|
+
*
|
|
23
|
+
* A member is a directory matching a glob AND holding a package.json — the
|
|
24
|
+
* same rule the package managers apply. Globs support `*` (one segment) and
|
|
25
|
+
* `**` (any depth); `node_modules`, `dist` and dot-directories are never
|
|
26
|
+
* descended into.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
30
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
31
|
+
import { argv, stdout } from 'node:process';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
|
|
34
|
+
const SKIPPED = new Set(['dist', 'node_modules']);
|
|
35
|
+
|
|
36
|
+
/** A glob over path segments — `*` stops at a separator, `**` does not. */
|
|
37
|
+
function toPattern(glob) {
|
|
38
|
+
const escaped = glob
|
|
39
|
+
.replaceAll(/[.+^${}()|[\]\\]/g, String.raw`\$&`)
|
|
40
|
+
.replaceAll('**', ' ')
|
|
41
|
+
.replaceAll('*', '[^/]*')
|
|
42
|
+
.replaceAll(' ', '.*');
|
|
43
|
+
|
|
44
|
+
return new RegExp(`^${escaped}$`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The `workspaces` globs of a package.json, in either declared form. */
|
|
48
|
+
function declaredGlobs(root) {
|
|
49
|
+
const manifest = join(root, 'package.json');
|
|
50
|
+
if (!existsSync(manifest)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let declared;
|
|
55
|
+
try {
|
|
56
|
+
const pkg = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
57
|
+
declared = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Array.isArray(declared) ? declared : [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The member directories of the workspace rooted at `root` — empty for a
|
|
67
|
+
* single-package project, which every caller reads as "the root is the unit".
|
|
68
|
+
*/
|
|
69
|
+
export function workspaceMembers(root = '.') {
|
|
70
|
+
const absolute = resolve(root);
|
|
71
|
+
const patterns = declaredGlobs(absolute)
|
|
72
|
+
.filter((glob) => typeof glob === 'string' && !glob.startsWith('!'))
|
|
73
|
+
.map((glob) => toPattern(glob.replace(/\/+$/, '')));
|
|
74
|
+
|
|
75
|
+
if (patterns.length === 0) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const members = new Set();
|
|
80
|
+
|
|
81
|
+
/** Walk the tree once, keeping every directory a pattern claims as a package. */
|
|
82
|
+
function walk(dir, depth) {
|
|
83
|
+
if (depth > 6) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let entries;
|
|
88
|
+
try {
|
|
89
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
90
|
+
} catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || SKIPPED.has(entry.name)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const child = join(dir, entry.name);
|
|
100
|
+
const path = relative(absolute, child).split(sep).join('/');
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
patterns.some((pattern) => pattern.test(path)) &&
|
|
104
|
+
existsSync(join(child, 'package.json'))
|
|
105
|
+
) {
|
|
106
|
+
members.add(path);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
walk(child, depth + 1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
walk(absolute, 0);
|
|
115
|
+
|
|
116
|
+
return [...members].sort();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// CLI form — only when this file IS the process entry, never on import.
|
|
120
|
+
if (argv[1] && resolve(argv[1]) === fileURLToPath(import.meta.url)) {
|
|
121
|
+
const members = workspaceMembers(argv[2] ?? '.');
|
|
122
|
+
if (members.length > 0) {
|
|
123
|
+
stdout.write(`${members.join('\n')}\n`);
|
|
124
|
+
}
|
|
125
|
+
}
|
package/package.json
CHANGED
|
@@ -78,6 +78,41 @@ function createHexagonalRule() {
|
|
|
78
78
|
// Imports-with-ext - Require .js extensions
|
|
79
79
|
// ============================================
|
|
80
80
|
|
|
81
|
+
// The extensions a bundler or Node actually resolves from an import specifier.
|
|
82
|
+
// Anything else after a dot is part of the module NAME (`user.entity`,
|
|
83
|
+
// `dashboard.post`, `article.repository`) and still needs its `.js`.
|
|
84
|
+
const KNOWN_IMPORT_EXTENSIONS = [
|
|
85
|
+
'cjs',
|
|
86
|
+
'css',
|
|
87
|
+
'cts',
|
|
88
|
+
'graphql',
|
|
89
|
+
'gql',
|
|
90
|
+
'html',
|
|
91
|
+
'jpeg',
|
|
92
|
+
'jpg',
|
|
93
|
+
'js',
|
|
94
|
+
'json',
|
|
95
|
+
'jsx',
|
|
96
|
+
'less',
|
|
97
|
+
'md',
|
|
98
|
+
'mdx',
|
|
99
|
+
'mjs',
|
|
100
|
+
'mts',
|
|
101
|
+
'node',
|
|
102
|
+
'png',
|
|
103
|
+
'sass',
|
|
104
|
+
'scss',
|
|
105
|
+
'svg',
|
|
106
|
+
'toml',
|
|
107
|
+
'ts',
|
|
108
|
+
'tsx',
|
|
109
|
+
'txt',
|
|
110
|
+
'wasm',
|
|
111
|
+
'webp',
|
|
112
|
+
'yaml',
|
|
113
|
+
'yml',
|
|
114
|
+
];
|
|
115
|
+
|
|
81
116
|
const importsWithExtRule = {
|
|
82
117
|
meta: {
|
|
83
118
|
type: 'problem',
|
|
@@ -89,7 +124,9 @@ const importsWithExtRule = {
|
|
|
89
124
|
schema: [],
|
|
90
125
|
},
|
|
91
126
|
create(context) {
|
|
92
|
-
|
|
127
|
+
// Only a KNOWN extension counts as already-extended — "anything after the
|
|
128
|
+
// Last dot" read `./entities/dashboard.post` as extended and skipped it.
|
|
129
|
+
const hasExtension = new RegExp(`\\.(?:${KNOWN_IMPORT_EXTENSIONS.join('|')})$`, 'i');
|
|
93
130
|
|
|
94
131
|
function checkNode(node) {
|
|
95
132
|
if (!node.source || !node.source.value) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"display": "Expo",
|
|
3
|
-
"include": ["
|
|
4
|
-
"exclude": ["
|
|
3
|
+
"include": ["${configDir}/**/*.ts", "${configDir}/**/*.tsx"],
|
|
4
|
+
"exclude": ["${configDir}/node_modules"],
|
|
5
5
|
|
|
6
6
|
"compilerOptions": {
|
|
7
7
|
"allowJs": true,
|