apdev-js 0.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 +120 -0
- package/dist/cli.js +556 -0
- package/dist/index.cjs +550 -0
- package/dist/index.d.cts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +508 -0
- package/package.json +57 -0
- package/release.sh +542 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# apdev
|
|
2
|
+
|
|
3
|
+
Shared development tools for TypeScript/JavaScript projects - character validation, circular import detection, and more.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add -D apdev-js
|
|
9
|
+
# or
|
|
10
|
+
npm install --save-dev apdev-js
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Available Tools
|
|
14
|
+
|
|
15
|
+
### check-chars
|
|
16
|
+
|
|
17
|
+
Validate files contain only allowed characters (ASCII + emoji + technical symbols).
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx apdev check-chars src/**/*.ts
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### check-imports
|
|
24
|
+
|
|
25
|
+
Detect circular imports in a JS/TS package.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx apdev check-imports --package mylib --src-dir src
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### release
|
|
32
|
+
|
|
33
|
+
Interactive release automation (build, tag, GitHub release, npm publish).
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx apdev release
|
|
37
|
+
npx apdev release 1.0.0
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
Add an `"apdev"` field to your `package.json`:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"apdev": {
|
|
47
|
+
"base_package": "mylib",
|
|
48
|
+
"src_dir": "src"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
With configuration, you can run `check-imports` without flags:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx apdev check-imports
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Integration with lint-staged / Husky
|
|
60
|
+
|
|
61
|
+
Add character checking as a pre-commit hook:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"lint-staged": {
|
|
66
|
+
"*.{ts,tsx,js,jsx}": ["apdev check-chars"]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Programmatic API
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import {
|
|
75
|
+
isAllowedChar,
|
|
76
|
+
checkFile,
|
|
77
|
+
checkPaths,
|
|
78
|
+
checkCircularImports,
|
|
79
|
+
loadConfig,
|
|
80
|
+
} from "apdev-js";
|
|
81
|
+
|
|
82
|
+
// Check a single character
|
|
83
|
+
isAllowedChar("A"); // true
|
|
84
|
+
isAllowedChar("\u4E2D"); // false (CJK)
|
|
85
|
+
|
|
86
|
+
// Check a file
|
|
87
|
+
const problems = checkFile("src/index.ts");
|
|
88
|
+
|
|
89
|
+
// Detect circular imports
|
|
90
|
+
const exitCode = checkCircularImports("src", "mylib");
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
Prerequisites: Node.js >= 18, pnpm.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
cd typescript
|
|
99
|
+
pnpm install # Install dependencies
|
|
100
|
+
pnpm build # Build (ESM + CJS + types)
|
|
101
|
+
pnpm test # Run tests
|
|
102
|
+
pnpm lint # Lint
|
|
103
|
+
pnpm format # Format
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Release
|
|
107
|
+
|
|
108
|
+
Requires npm authentication (`~/.npmrc`) and GitHub CLI (`gh`).
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
./release.sh # Use version from package.json
|
|
112
|
+
./release.sh 0.2.0 # Specify version
|
|
113
|
+
./release.sh --yes # Silent mode (CI/CD)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The script will: verify version → clean → build → pack check → git tag → GitHub release → npm publish.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
Apache-2.0
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
var getFilename = () => fileURLToPath(import.meta.url);
|
|
7
|
+
var getDirname = () => path.dirname(getFilename());
|
|
8
|
+
var __dirname = /* @__PURE__ */ getDirname();
|
|
9
|
+
|
|
10
|
+
// src/cli.ts
|
|
11
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
12
|
+
import { resolve, dirname, join as join3 } from "path";
|
|
13
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
14
|
+
import { execFileSync } from "child_process";
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
|
|
17
|
+
// src/check-chars.ts
|
|
18
|
+
import { readFileSync } from "fs";
|
|
19
|
+
import { extname } from "path";
|
|
20
|
+
var EMOJI_RANGES = [
|
|
21
|
+
[127744, 128511],
|
|
22
|
+
// Symbols and Pictographs
|
|
23
|
+
[128512, 128591],
|
|
24
|
+
// Emoticons
|
|
25
|
+
[128640, 128767],
|
|
26
|
+
// Transport and Map Symbols
|
|
27
|
+
[128896, 129023],
|
|
28
|
+
// Geometric Shapes Extended
|
|
29
|
+
[129280, 129535],
|
|
30
|
+
// Supplemental Symbols and Pictographs
|
|
31
|
+
[9728, 9983],
|
|
32
|
+
// Miscellaneous Symbols
|
|
33
|
+
[9984, 10175]
|
|
34
|
+
// Dingbats
|
|
35
|
+
];
|
|
36
|
+
var EXTRA_ALLOWED_RANGES = [
|
|
37
|
+
[128, 255],
|
|
38
|
+
// Latin-1 Supplement
|
|
39
|
+
[8192, 8303],
|
|
40
|
+
// General Punctuation
|
|
41
|
+
[8448, 8527],
|
|
42
|
+
// Letterlike Symbols
|
|
43
|
+
[8592, 8703],
|
|
44
|
+
// Arrows
|
|
45
|
+
[8704, 8959],
|
|
46
|
+
// Mathematical Operators
|
|
47
|
+
[8960, 9215],
|
|
48
|
+
// Miscellaneous Technical
|
|
49
|
+
[9472, 9599],
|
|
50
|
+
// Box Drawing
|
|
51
|
+
[9632, 9727],
|
|
52
|
+
// Geometric Shapes
|
|
53
|
+
[11008, 11263],
|
|
54
|
+
// Miscellaneous Symbols and Arrows
|
|
55
|
+
[65024, 65039]
|
|
56
|
+
// Variation Selectors
|
|
57
|
+
];
|
|
58
|
+
var ALL_RANGES = [...EMOJI_RANGES, ...EXTRA_ALLOWED_RANGES];
|
|
59
|
+
var DANGEROUS_CODEPOINTS = /* @__PURE__ */ new Map([
|
|
60
|
+
// Bidi control characters (Trojan Source - CVE-2021-42574)
|
|
61
|
+
[8234, "LEFT-TO-RIGHT EMBEDDING"],
|
|
62
|
+
[8235, "RIGHT-TO-LEFT EMBEDDING"],
|
|
63
|
+
[8236, "POP DIRECTIONAL FORMATTING"],
|
|
64
|
+
[8237, "LEFT-TO-RIGHT OVERRIDE"],
|
|
65
|
+
[8238, "RIGHT-TO-LEFT OVERRIDE"],
|
|
66
|
+
[8294, "LEFT-TO-RIGHT ISOLATE"],
|
|
67
|
+
[8295, "RIGHT-TO-LEFT ISOLATE"],
|
|
68
|
+
[8296, "FIRST STRONG ISOLATE"],
|
|
69
|
+
[8297, "POP DIRECTIONAL ISOLATE"],
|
|
70
|
+
// Zero-width characters
|
|
71
|
+
[8203, "ZERO WIDTH SPACE"],
|
|
72
|
+
[8204, "ZERO WIDTH NON-JOINER"],
|
|
73
|
+
[8205, "ZERO WIDTH JOINER"],
|
|
74
|
+
[8206, "LEFT-TO-RIGHT MARK"],
|
|
75
|
+
[8207, "RIGHT-TO-LEFT MARK"],
|
|
76
|
+
[8288, "WORD JOINER"]
|
|
77
|
+
]);
|
|
78
|
+
var PYTHON_SUFFIXES = /* @__PURE__ */ new Set([".py"]);
|
|
79
|
+
var JS_SUFFIXES = /* @__PURE__ */ new Set([".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"]);
|
|
80
|
+
function isAllowedChar(c) {
|
|
81
|
+
const code = c.codePointAt(0);
|
|
82
|
+
if (code <= 127) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
for (const [start, end] of ALL_RANGES) {
|
|
86
|
+
if (code >= start && code <= end) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
function isDangerousChar(c) {
|
|
93
|
+
return DANGEROUS_CODEPOINTS.has(c.codePointAt(0));
|
|
94
|
+
}
|
|
95
|
+
function computeCommentMask(content, suffix) {
|
|
96
|
+
if (PYTHON_SUFFIXES.has(suffix)) {
|
|
97
|
+
return computeCommentMaskPython(content);
|
|
98
|
+
}
|
|
99
|
+
if (JS_SUFFIXES.has(suffix)) {
|
|
100
|
+
return computeCommentMaskJs(content);
|
|
101
|
+
}
|
|
102
|
+
return /* @__PURE__ */ new Set();
|
|
103
|
+
}
|
|
104
|
+
function computeCommentMaskPython(content) {
|
|
105
|
+
const mask = /* @__PURE__ */ new Set();
|
|
106
|
+
let i = 0;
|
|
107
|
+
const n = content.length;
|
|
108
|
+
while (i < n) {
|
|
109
|
+
if (i + 2 < n && (content.slice(i, i + 3) === '"""' || content.slice(i, i + 3) === "'''")) {
|
|
110
|
+
const quote = content.slice(i, i + 3);
|
|
111
|
+
i += 3;
|
|
112
|
+
while (i < n) {
|
|
113
|
+
if (content[i] === "\\" && i + 1 < n) {
|
|
114
|
+
i += 2;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (i + 2 < n && content.slice(i, i + 3) === quote) {
|
|
118
|
+
i += 3;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (content[i] === '"' || content[i] === "'") {
|
|
126
|
+
const quoteChar = content[i];
|
|
127
|
+
i++;
|
|
128
|
+
while (i < n && content[i] !== "\n") {
|
|
129
|
+
if (content[i] === "\\" && i + 1 < n) {
|
|
130
|
+
i += 2;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (content[i] === quoteChar) {
|
|
134
|
+
i++;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (content[i] === "#") {
|
|
142
|
+
while (i < n && content[i] !== "\n") {
|
|
143
|
+
mask.add(i);
|
|
144
|
+
i++;
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
i++;
|
|
149
|
+
}
|
|
150
|
+
return mask;
|
|
151
|
+
}
|
|
152
|
+
function computeCommentMaskJs(content) {
|
|
153
|
+
const mask = /* @__PURE__ */ new Set();
|
|
154
|
+
let i = 0;
|
|
155
|
+
const n = content.length;
|
|
156
|
+
while (i < n) {
|
|
157
|
+
if (content[i] === "`") {
|
|
158
|
+
i++;
|
|
159
|
+
while (i < n) {
|
|
160
|
+
if (content[i] === "\\" && i + 1 < n) {
|
|
161
|
+
i += 2;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (content[i] === "`") {
|
|
165
|
+
i++;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
i++;
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (content[i] === '"' || content[i] === "'") {
|
|
173
|
+
const quoteChar = content[i];
|
|
174
|
+
i++;
|
|
175
|
+
while (i < n && content[i] !== "\n") {
|
|
176
|
+
if (content[i] === "\\" && i + 1 < n) {
|
|
177
|
+
i += 2;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (content[i] === quoteChar) {
|
|
181
|
+
i++;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (i + 1 < n && content[i] === "/" && content[i + 1] === "/") {
|
|
189
|
+
while (i < n && content[i] !== "\n") {
|
|
190
|
+
mask.add(i);
|
|
191
|
+
i++;
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (i + 1 < n && content[i] === "/" && content[i + 1] === "*") {
|
|
196
|
+
while (i < n) {
|
|
197
|
+
if (i + 1 < n && content[i] === "*" && content[i + 1] === "/") {
|
|
198
|
+
mask.add(i);
|
|
199
|
+
mask.add(i + 1);
|
|
200
|
+
i += 2;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
mask.add(i);
|
|
204
|
+
i++;
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
i++;
|
|
209
|
+
}
|
|
210
|
+
return mask;
|
|
211
|
+
}
|
|
212
|
+
function checkFile(filePath, maxProblems = 5) {
|
|
213
|
+
const problems = [];
|
|
214
|
+
try {
|
|
215
|
+
const content = readFileSync(filePath, "utf-8");
|
|
216
|
+
const suffix = extname(filePath).toLowerCase();
|
|
217
|
+
const commentMask = computeCommentMask(content, suffix);
|
|
218
|
+
let position = 0;
|
|
219
|
+
let offset = 0;
|
|
220
|
+
for (const char of content) {
|
|
221
|
+
position++;
|
|
222
|
+
const code = char.codePointAt(0);
|
|
223
|
+
if (isDangerousChar(char)) {
|
|
224
|
+
if (!commentMask.has(offset)) {
|
|
225
|
+
const name = DANGEROUS_CODEPOINTS.get(code);
|
|
226
|
+
const hex = code.toString(16).toUpperCase().padStart(4, "0");
|
|
227
|
+
problems.push(
|
|
228
|
+
`Dangerous character in code at position ${position}: U+${hex} (${name})`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
} else if (!isAllowedChar(char)) {
|
|
232
|
+
const hex = code.toString(16).toUpperCase().padStart(4, "0");
|
|
233
|
+
problems.push(
|
|
234
|
+
`Illegal character at position ${position}: ${JSON.stringify(char)} (U+${hex})`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (problems.length >= maxProblems) {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
offset += char.length;
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
problems.push(`Failed to read file: ${filePath}`);
|
|
244
|
+
}
|
|
245
|
+
return problems;
|
|
246
|
+
}
|
|
247
|
+
function checkPaths(paths) {
|
|
248
|
+
let hasError = false;
|
|
249
|
+
for (const path2 of paths) {
|
|
250
|
+
const problems = checkFile(path2);
|
|
251
|
+
if (problems.length > 0) {
|
|
252
|
+
hasError = true;
|
|
253
|
+
console.log(`
|
|
254
|
+
${path2} contains illegal characters:`);
|
|
255
|
+
for (const p of problems) {
|
|
256
|
+
console.log(` ${p}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return hasError ? 1 : 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/check-imports.ts
|
|
264
|
+
import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
265
|
+
import { join, relative, sep, extname as extname2, basename } from "path";
|
|
266
|
+
import ts from "typescript";
|
|
267
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
268
|
+
".ts",
|
|
269
|
+
".tsx",
|
|
270
|
+
".js",
|
|
271
|
+
".jsx",
|
|
272
|
+
".mjs",
|
|
273
|
+
".cjs"
|
|
274
|
+
]);
|
|
275
|
+
var INDEX_BASENAMES = /* @__PURE__ */ new Set([
|
|
276
|
+
"index.ts",
|
|
277
|
+
"index.tsx",
|
|
278
|
+
"index.js",
|
|
279
|
+
"index.jsx",
|
|
280
|
+
"index.mjs",
|
|
281
|
+
"index.cjs"
|
|
282
|
+
]);
|
|
283
|
+
function fileToModule(filePath, srcDir) {
|
|
284
|
+
const rel = relative(srcDir, filePath);
|
|
285
|
+
const parts = rel.split(sep);
|
|
286
|
+
const last = parts[parts.length - 1];
|
|
287
|
+
if (last === "index.ts" || last === "index.js" || last === "index.tsx" || last === "index.jsx") {
|
|
288
|
+
parts.pop();
|
|
289
|
+
} else {
|
|
290
|
+
const ext = extname2(last);
|
|
291
|
+
parts[parts.length - 1] = basename(last, ext);
|
|
292
|
+
}
|
|
293
|
+
return parts.join(".");
|
|
294
|
+
}
|
|
295
|
+
function extractImports(source, fileName) {
|
|
296
|
+
const imports = /* @__PURE__ */ new Set();
|
|
297
|
+
const sourceFile = ts.createSourceFile(
|
|
298
|
+
fileName,
|
|
299
|
+
source,
|
|
300
|
+
ts.ScriptTarget.Latest,
|
|
301
|
+
true,
|
|
302
|
+
fileName.endsWith(".tsx") || fileName.endsWith(".jsx") ? ts.ScriptKind.TSX : void 0
|
|
303
|
+
);
|
|
304
|
+
function visit(node) {
|
|
305
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
306
|
+
imports.add(node.moduleSpecifier.text);
|
|
307
|
+
}
|
|
308
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
309
|
+
imports.add(node.moduleSpecifier.text);
|
|
310
|
+
}
|
|
311
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.Identifier && node.expression.text === "require" && node.arguments.length === 1 && ts.isStringLiteral(node.arguments[0])) {
|
|
312
|
+
imports.add(node.arguments[0].text);
|
|
313
|
+
}
|
|
314
|
+
ts.forEachChild(node, visit);
|
|
315
|
+
}
|
|
316
|
+
visit(sourceFile);
|
|
317
|
+
return imports;
|
|
318
|
+
}
|
|
319
|
+
function resolveImports(rawImports, basePackage, currentModule, isPackage) {
|
|
320
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
321
|
+
for (const imp of rawImports) {
|
|
322
|
+
if (imp.startsWith("./") || imp.startsWith("../")) {
|
|
323
|
+
if (!currentModule) continue;
|
|
324
|
+
const moduleParts = currentModule.split(".");
|
|
325
|
+
let dirParts;
|
|
326
|
+
if (isPackage) {
|
|
327
|
+
dirParts = [...moduleParts];
|
|
328
|
+
} else {
|
|
329
|
+
dirParts = moduleParts.slice(0, -1);
|
|
330
|
+
}
|
|
331
|
+
const segments = imp.split("/");
|
|
332
|
+
for (const seg of segments) {
|
|
333
|
+
if (seg === ".") continue;
|
|
334
|
+
if (seg === "..") {
|
|
335
|
+
dirParts.pop();
|
|
336
|
+
} else {
|
|
337
|
+
dirParts.push(seg);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const resolvedModule = dirParts.join(".");
|
|
341
|
+
if (resolvedModule === basePackage || resolvedModule.startsWith(basePackage + ".")) {
|
|
342
|
+
resolved.add(resolvedModule);
|
|
343
|
+
}
|
|
344
|
+
} else if (imp === basePackage || imp.startsWith(basePackage + "/")) {
|
|
345
|
+
resolved.add(imp.replaceAll("/", "."));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return resolved;
|
|
349
|
+
}
|
|
350
|
+
function findSourceFiles(dir) {
|
|
351
|
+
const results = [];
|
|
352
|
+
function walk(d) {
|
|
353
|
+
const entries = readdirSync(d);
|
|
354
|
+
for (const entry of entries) {
|
|
355
|
+
if (entry === "node_modules" || entry === "dist" || entry === ".git") {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const full = join(d, entry);
|
|
359
|
+
const stat = statSync(full);
|
|
360
|
+
if (stat.isDirectory()) {
|
|
361
|
+
walk(full);
|
|
362
|
+
} else if (SUPPORTED_EXTENSIONS.has(extname2(entry))) {
|
|
363
|
+
results.push(full);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
walk(dir);
|
|
368
|
+
return results;
|
|
369
|
+
}
|
|
370
|
+
function buildDependencyGraph(srcDir, basePackage) {
|
|
371
|
+
const graph = /* @__PURE__ */ new Map();
|
|
372
|
+
const files = findSourceFiles(srcDir);
|
|
373
|
+
for (const file of files) {
|
|
374
|
+
const moduleName = fileToModule(file, srcDir);
|
|
375
|
+
if (!moduleName) continue;
|
|
376
|
+
let source;
|
|
377
|
+
try {
|
|
378
|
+
source = readFileSync2(file, "utf-8");
|
|
379
|
+
} catch (e) {
|
|
380
|
+
console.error(`Warning: could not read ${file}: ${e}`);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
let rawImports;
|
|
384
|
+
try {
|
|
385
|
+
rawImports = extractImports(source, file);
|
|
386
|
+
} catch (e) {
|
|
387
|
+
console.error(`Warning: could not parse ${file}: ${e}`);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const isPackage = INDEX_BASENAMES.has(basename(file));
|
|
391
|
+
const deps = resolveImports(rawImports, basePackage, moduleName, isPackage);
|
|
392
|
+
deps.delete(moduleName);
|
|
393
|
+
graph.set(moduleName, deps);
|
|
394
|
+
}
|
|
395
|
+
return graph;
|
|
396
|
+
}
|
|
397
|
+
function findCycles(graph) {
|
|
398
|
+
const WHITE = 0;
|
|
399
|
+
const GRAY = 1;
|
|
400
|
+
const BLACK = 2;
|
|
401
|
+
const color = /* @__PURE__ */ new Map();
|
|
402
|
+
const path2 = [];
|
|
403
|
+
const cycles = [];
|
|
404
|
+
function dfs(node) {
|
|
405
|
+
color.set(node, GRAY);
|
|
406
|
+
path2.push(node);
|
|
407
|
+
const neighbors = graph.get(node) ?? /* @__PURE__ */ new Set();
|
|
408
|
+
for (const neighbor of [...neighbors].sort()) {
|
|
409
|
+
if (color.get(neighbor) === GRAY && path2.includes(neighbor)) {
|
|
410
|
+
const idx = path2.indexOf(neighbor);
|
|
411
|
+
cycles.push([...path2.slice(idx), neighbor]);
|
|
412
|
+
} else if ((color.get(neighbor) ?? WHITE) === WHITE) {
|
|
413
|
+
dfs(neighbor);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
path2.pop();
|
|
417
|
+
color.set(node, BLACK);
|
|
418
|
+
}
|
|
419
|
+
for (const node of [...graph.keys()].sort()) {
|
|
420
|
+
if ((color.get(node) ?? WHITE) === WHITE) {
|
|
421
|
+
dfs(node);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const unique = [];
|
|
425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
426
|
+
for (const cycle of cycles) {
|
|
427
|
+
const ring = cycle.slice(0, -1);
|
|
428
|
+
let minVal = ring[0];
|
|
429
|
+
let minIdx = 0;
|
|
430
|
+
for (let i = 1; i < ring.length; i++) {
|
|
431
|
+
if (ring[i] < minVal) {
|
|
432
|
+
minVal = ring[i];
|
|
433
|
+
minIdx = i;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const normalized = [...ring.slice(minIdx), ...ring.slice(0, minIdx)].join(",");
|
|
437
|
+
if (!seen.has(normalized)) {
|
|
438
|
+
seen.add(normalized);
|
|
439
|
+
unique.push(cycle);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return unique;
|
|
443
|
+
}
|
|
444
|
+
function checkCircularImports(srcDir, basePackage) {
|
|
445
|
+
let stat;
|
|
446
|
+
try {
|
|
447
|
+
stat = statSync(srcDir);
|
|
448
|
+
} catch {
|
|
449
|
+
console.error(`Error: ${srcDir}/ directory not found`);
|
|
450
|
+
return 1;
|
|
451
|
+
}
|
|
452
|
+
if (!stat.isDirectory()) {
|
|
453
|
+
console.error(`Error: ${srcDir}/ is not a directory`);
|
|
454
|
+
return 1;
|
|
455
|
+
}
|
|
456
|
+
const graph = buildDependencyGraph(srcDir, basePackage);
|
|
457
|
+
console.log(`Scanned ${graph.size} modules`);
|
|
458
|
+
const cycles = findCycles(graph);
|
|
459
|
+
if (cycles.length > 0) {
|
|
460
|
+
console.log(`
|
|
461
|
+
Found ${cycles.length} circular import(s):
|
|
462
|
+
`);
|
|
463
|
+
for (let i = 0; i < cycles.length; i++) {
|
|
464
|
+
console.log(` Cycle ${i + 1}: ${cycles[i].join(" -> ")}`);
|
|
465
|
+
}
|
|
466
|
+
console.log();
|
|
467
|
+
return 1;
|
|
468
|
+
}
|
|
469
|
+
console.log("No circular imports detected.");
|
|
470
|
+
return 0;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/config.ts
|
|
474
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
475
|
+
import { join as join2 } from "path";
|
|
476
|
+
function loadConfig(projectDir) {
|
|
477
|
+
const dir = projectDir ?? process.cwd();
|
|
478
|
+
const pkgPath = join2(dir, "package.json");
|
|
479
|
+
let raw;
|
|
480
|
+
try {
|
|
481
|
+
raw = readFileSync3(pkgPath, "utf-8");
|
|
482
|
+
} catch {
|
|
483
|
+
return {};
|
|
484
|
+
}
|
|
485
|
+
const pkg = JSON.parse(raw);
|
|
486
|
+
const apdev = pkg["apdev"];
|
|
487
|
+
if (apdev && typeof apdev === "object" && !Array.isArray(apdev)) {
|
|
488
|
+
return apdev;
|
|
489
|
+
}
|
|
490
|
+
return {};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/cli.ts
|
|
494
|
+
function getVersion() {
|
|
495
|
+
const thisDir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath2(import.meta.url));
|
|
496
|
+
const pkgPath = join3(thisDir, "..", "package.json");
|
|
497
|
+
try {
|
|
498
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
499
|
+
return pkg.version ?? "0.0.0";
|
|
500
|
+
} catch {
|
|
501
|
+
return "0.0.0";
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function getReleaseScript() {
|
|
505
|
+
const thisDir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath2(import.meta.url));
|
|
506
|
+
return join3(thisDir, "..", "release.sh");
|
|
507
|
+
}
|
|
508
|
+
function buildProgram() {
|
|
509
|
+
const program2 = new Command();
|
|
510
|
+
program2.name("apdev").description("Shared development tools for TypeScript/JavaScript projects").version(getVersion());
|
|
511
|
+
program2.command("check-chars").description("Validate files contain only allowed characters").argument("<files...>", "Files to check").action((files) => {
|
|
512
|
+
const resolved = files.map((f) => resolve(f));
|
|
513
|
+
const code = checkPaths(resolved);
|
|
514
|
+
process.exit(code);
|
|
515
|
+
});
|
|
516
|
+
program2.command("check-imports").description("Detect circular imports in a JS/TS package").option("--package <name>", "Base package name (e.g. mylib). Reads from package.json apdev config if omitted.").option("--src-dir <dir>", "Source directory containing the package (default: src)").action((opts) => {
|
|
517
|
+
const config = loadConfig();
|
|
518
|
+
const basePackage = opts.package ?? config["base_package"];
|
|
519
|
+
const srcDir = opts.srcDir ?? config["src_dir"] ?? "src";
|
|
520
|
+
if (!basePackage) {
|
|
521
|
+
console.error(
|
|
522
|
+
'Error: --package is required (or set base_package in package.json "apdev" field)'
|
|
523
|
+
);
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
const code = checkCircularImports(resolve(srcDir), basePackage);
|
|
527
|
+
process.exit(code);
|
|
528
|
+
});
|
|
529
|
+
program2.command("release").description("Interactive release automation (build, tag, GitHub release, npm publish)").option("--yes, -y", "Auto-accept all defaults (silent mode)").argument("[version]", "Version to release (auto-detected from package.json if omitted)").action((version, opts) => {
|
|
530
|
+
const script = getReleaseScript();
|
|
531
|
+
try {
|
|
532
|
+
readFileSync4(script);
|
|
533
|
+
} catch {
|
|
534
|
+
console.error("Error: release.sh not found in package");
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
const args = ["bash", script];
|
|
538
|
+
if (opts?.yes) args.push("--yes");
|
|
539
|
+
if (version) args.push(version);
|
|
540
|
+
try {
|
|
541
|
+
execFileSync(args[0], args.slice(1), {
|
|
542
|
+
stdio: "inherit",
|
|
543
|
+
env: process.env
|
|
544
|
+
});
|
|
545
|
+
} catch (e) {
|
|
546
|
+
const err = e;
|
|
547
|
+
process.exit(err.status ?? 1);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
return program2;
|
|
551
|
+
}
|
|
552
|
+
var program = buildProgram();
|
|
553
|
+
program.parse();
|
|
554
|
+
export {
|
|
555
|
+
buildProgram
|
|
556
|
+
};
|