apdev-js 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # apdev
2
2
 
3
- Shared development tools for TypeScript/JavaScript projects - character validation, circular import detection, and more.
3
+ General-purpose development tools for TypeScript/JavaScript projects - character validation, circular import detection, and more.
4
4
 
5
5
  ## Installation
6
6
 
package/dist/cli.js CHANGED
@@ -9,88 +9,98 @@ var __dirname = /* @__PURE__ */ getDirname();
9
9
 
10
10
  // src/cli.ts
11
11
  import { readFileSync as readFileSync4 } from "fs";
12
- import { resolve, dirname, join as join3 } from "path";
13
- import { fileURLToPath as fileURLToPath2 } from "url";
12
+ import { resolve, dirname as dirname2, join as join4, sep as sep3 } from "path";
13
+ import { fileURLToPath as fileURLToPath3 } from "url";
14
14
  import { execFileSync } from "child_process";
15
15
  import { Command } from "commander";
16
16
 
17
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;
18
+ import { readFileSync, existsSync, readdirSync, statSync } from "fs";
19
+ import { extname, dirname, join, sep } from "path";
20
+ import { fileURLToPath as fileURLToPath2 } from "url";
21
+ function getCharsetsDir() {
22
+ const thisDir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath2(import.meta.url));
23
+ const devPath = join(thisDir, "charsets");
24
+ if (existsSync(devPath)) {
25
+ return devPath;
26
+ }
27
+ return join(thisDir, "..", "src", "charsets");
28
+ }
29
+ function loadCharset(nameOrPath) {
30
+ if (nameOrPath.includes(sep) || nameOrPath.includes("/") || nameOrPath.endsWith(".json")) {
31
+ if (!existsSync(nameOrPath)) {
32
+ throw new Error(`Charset file not found: ${nameOrPath}`);
33
+ }
34
+ return JSON.parse(readFileSync(nameOrPath, "utf-8"));
35
+ }
36
+ const filePath = join(getCharsetsDir(), `${nameOrPath}.json`);
37
+ if (!existsSync(filePath)) {
38
+ throw new Error(`Unknown charset: ${nameOrPath}`);
39
+ }
40
+ return JSON.parse(readFileSync(filePath, "utf-8"));
41
+ }
42
+ function parseRanges(entries) {
43
+ return entries.map((e) => [parseInt(e.start, 16), parseInt(e.end, 16)]);
44
+ }
45
+ function parseDangerous(entries) {
46
+ const map = /* @__PURE__ */ new Map();
47
+ for (const e of entries) {
48
+ map.set(parseInt(e.code, 16), e.name);
49
+ }
50
+ return map;
51
+ }
52
+ function resolveCharsets(charsetNames, charsetFiles) {
53
+ const base = loadCharset("base");
54
+ const rangesSet = /* @__PURE__ */ new Map();
55
+ const dangerous = parseDangerous(base.dangerous ?? []);
56
+ function addRanges(entries) {
57
+ for (const [s, e] of parseRanges(entries)) {
58
+ rangesSet.set(`${s}-${e}`, [s, e]);
59
+ }
84
60
  }
85
- for (const [start, end] of ALL_RANGES) {
86
- if (code >= start && code <= end) {
87
- return true;
61
+ addRanges(base.emoji_ranges ?? []);
62
+ addRanges(base.extra_ranges ?? []);
63
+ for (const name of charsetNames) {
64
+ const data = loadCharset(name);
65
+ addRanges(data.emoji_ranges ?? []);
66
+ addRanges(data.extra_ranges ?? []);
67
+ if (data.dangerous) {
68
+ for (const [code, dname] of parseDangerous(data.dangerous)) {
69
+ dangerous.set(code, dname);
70
+ }
71
+ }
72
+ }
73
+ for (const path2 of charsetFiles) {
74
+ const data = loadCharset(path2);
75
+ addRanges(data.emoji_ranges ?? []);
76
+ addRanges(data.extra_ranges ?? []);
77
+ if (data.dangerous) {
78
+ for (const [code, dname] of parseDangerous(data.dangerous)) {
79
+ dangerous.set(code, dname);
80
+ }
88
81
  }
89
82
  }
83
+ const ranges = [...rangesSet.values()].sort((a, b) => a[0] - b[0]);
84
+ return { ranges, dangerous };
85
+ }
86
+ var PYTHON_SUFFIXES = /* @__PURE__ */ new Set([".py"]);
87
+ var JS_SUFFIXES = /* @__PURE__ */ new Set([".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"]);
88
+ function isInRanges(code, ranges) {
89
+ if (code <= 127) return true;
90
+ for (const [start, end] of ranges) {
91
+ if (code >= start && code <= end) return true;
92
+ }
90
93
  return false;
91
94
  }
92
- function isDangerousChar(c) {
93
- return DANGEROUS_CODEPOINTS.has(c.codePointAt(0));
95
+ var _baseRanges = null;
96
+ var _baseDangerous = null;
97
+ function getBaseDefaults() {
98
+ if (!_baseRanges || !_baseDangerous) {
99
+ const defaults = resolveCharsets([], []);
100
+ _baseRanges = defaults.ranges;
101
+ _baseDangerous = defaults.dangerous;
102
+ }
103
+ return { ranges: _baseRanges, dangerous: _baseDangerous };
94
104
  }
95
105
  function computeCommentMask(content, suffix) {
96
106
  if (PYTHON_SUFFIXES.has(suffix)) {
@@ -209,8 +219,13 @@ function computeCommentMaskJs(content) {
209
219
  }
210
220
  return mask;
211
221
  }
212
- function checkFile(filePath, maxProblems = 5) {
222
+ function checkFile(filePath, maxProblems = 5, extraRanges, dangerousMap) {
213
223
  const problems = [];
224
+ if (!extraRanges || !dangerousMap) {
225
+ const defaults = getBaseDefaults();
226
+ extraRanges ??= defaults.ranges;
227
+ dangerousMap ??= defaults.dangerous;
228
+ }
214
229
  try {
215
230
  const content = readFileSync(filePath, "utf-8");
216
231
  const suffix = extname(filePath).toLowerCase();
@@ -220,15 +235,15 @@ function checkFile(filePath, maxProblems = 5) {
220
235
  for (const char of content) {
221
236
  position++;
222
237
  const code = char.codePointAt(0);
223
- if (isDangerousChar(char)) {
238
+ if (dangerousMap.has(code)) {
224
239
  if (!commentMask.has(offset)) {
225
- const name = DANGEROUS_CODEPOINTS.get(code);
240
+ const name = dangerousMap.get(code);
226
241
  const hex = code.toString(16).toUpperCase().padStart(4, "0");
227
242
  problems.push(
228
243
  `Dangerous character in code at position ${position}: U+${hex} (${name})`
229
244
  );
230
245
  }
231
- } else if (!isAllowedChar(char)) {
246
+ } else if (!isInRanges(code, extraRanges)) {
232
247
  const hex = code.toString(16).toUpperCase().padStart(4, "0");
233
248
  problems.push(
234
249
  `Illegal character at position ${position}: ${JSON.stringify(char)} (U+${hex})`
@@ -239,15 +254,169 @@ function checkFile(filePath, maxProblems = 5) {
239
254
  }
240
255
  offset += char.length;
241
256
  }
242
- } catch {
243
- problems.push(`Failed to read file: ${filePath}`);
257
+ } catch (e) {
258
+ problems.push(`Failed to read file: ${filePath} (${e})`);
244
259
  }
245
260
  return problems;
246
261
  }
247
- function checkPaths(paths) {
262
+ var SKIP_SUFFIXES = /* @__PURE__ */ new Set([
263
+ // Bytecode
264
+ ".pyc",
265
+ ".pyo",
266
+ // Images
267
+ ".png",
268
+ ".jpg",
269
+ ".jpeg",
270
+ ".gif",
271
+ ".bmp",
272
+ ".ico",
273
+ ".svg",
274
+ ".webp",
275
+ // Fonts
276
+ ".ttf",
277
+ ".otf",
278
+ ".woff",
279
+ ".woff2",
280
+ ".eot",
281
+ // Archives
282
+ ".zip",
283
+ ".tar",
284
+ ".gz",
285
+ ".bz2",
286
+ ".xz",
287
+ ".7z",
288
+ // Compiled / binary
289
+ ".so",
290
+ ".dylib",
291
+ ".dll",
292
+ ".exe",
293
+ ".o",
294
+ ".a",
295
+ ".whl",
296
+ ".egg",
297
+ // Media
298
+ ".mp3",
299
+ ".mp4",
300
+ ".wav",
301
+ ".avi",
302
+ ".mov",
303
+ ".flac",
304
+ ".ogg",
305
+ // Documents
306
+ ".pdf",
307
+ ".doc",
308
+ ".docx",
309
+ ".xls",
310
+ ".xlsx",
311
+ ".ppt",
312
+ ".pptx",
313
+ // Data
314
+ ".db",
315
+ ".sqlite",
316
+ ".sqlite3",
317
+ ".pickle",
318
+ ".pkl"
319
+ ]);
320
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
321
+ "__pycache__",
322
+ "node_modules",
323
+ ".git",
324
+ ".venv",
325
+ "venv",
326
+ ".tox",
327
+ ".mypy_cache",
328
+ ".pytest_cache",
329
+ ".ruff_cache",
330
+ "dist",
331
+ "build"
332
+ ]);
333
+ var DEFAULT_DIRS = ["src", "tests", "examples"];
334
+ var DEFAULT_GLOBS = ["*.md", "*.yml", "*.yaml", "*.json", ".gitignore"];
335
+ function walkDir(directory) {
336
+ const files = [];
337
+ let entries;
338
+ try {
339
+ entries = readdirSync(directory).sort();
340
+ } catch {
341
+ return files;
342
+ }
343
+ for (const name of entries) {
344
+ if (name.startsWith(".")) continue;
345
+ const fullPath = join(directory, name);
346
+ let stat;
347
+ try {
348
+ stat = statSync(fullPath);
349
+ } catch {
350
+ continue;
351
+ }
352
+ if (stat.isDirectory()) {
353
+ if (SKIP_DIRS.has(name) || name.endsWith(".egg-info")) continue;
354
+ files.push(...walkDir(fullPath));
355
+ } else if (stat.isFile()) {
356
+ if (SKIP_SUFFIXES.has(extname(name).toLowerCase())) continue;
357
+ files.push(fullPath);
358
+ }
359
+ }
360
+ return files;
361
+ }
362
+ function defaultProjectFiles() {
363
+ const cwd = process.cwd();
364
+ const files = [];
365
+ for (const dirname3 of DEFAULT_DIRS) {
366
+ const d = join(cwd, dirname3);
367
+ if (existsSync(d) && statSync(d).isDirectory()) {
368
+ files.push(...walkDir(d));
369
+ }
370
+ }
371
+ for (const pattern of DEFAULT_GLOBS) {
372
+ if (pattern.startsWith("*.")) {
373
+ const suffix = pattern.slice(1);
374
+ try {
375
+ for (const name of readdirSync(cwd).sort()) {
376
+ if (name.endsWith(suffix) && statSync(join(cwd, name)).isFile()) {
377
+ files.push(join(cwd, name));
378
+ }
379
+ }
380
+ } catch {
381
+ }
382
+ } else {
383
+ const fullPath = join(cwd, pattern);
384
+ if (existsSync(fullPath) && statSync(fullPath).isFile()) {
385
+ files.push(fullPath);
386
+ }
387
+ }
388
+ }
389
+ return files;
390
+ }
391
+ function resolvePaths(paths) {
392
+ if (paths.length === 0) {
393
+ return defaultProjectFiles();
394
+ }
395
+ const result = [];
396
+ for (const p of paths) {
397
+ try {
398
+ if (statSync(p).isDirectory()) {
399
+ result.push(...walkDir(p));
400
+ } else {
401
+ result.push(p);
402
+ }
403
+ } catch {
404
+ result.push(p);
405
+ }
406
+ }
407
+ return result;
408
+ }
409
+ function checkPaths(paths, extraRanges, dangerousMap) {
410
+ const resolved = resolvePaths(paths);
411
+ if (resolved.length === 0) {
412
+ console.log("No files to check.");
413
+ return 0;
414
+ }
248
415
  let hasError = false;
249
- for (const path2 of paths) {
250
- const problems = checkFile(path2);
416
+ let checked = 0;
417
+ for (const path2 of resolved) {
418
+ const problems = checkFile(path2, 5, extraRanges, dangerousMap);
419
+ checked++;
251
420
  if (problems.length > 0) {
252
421
  hasError = true;
253
422
  console.log(`
@@ -257,12 +426,15 @@ ${path2} contains illegal characters:`);
257
426
  }
258
427
  }
259
428
  }
429
+ if (!hasError) {
430
+ console.log(`All ${checked} files passed.`);
431
+ }
260
432
  return hasError ? 1 : 0;
261
433
  }
262
434
 
263
435
  // src/check-imports.ts
264
- import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
265
- import { join, relative, sep, extname as extname2, basename } from "path";
436
+ import { readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
437
+ import { join as join2, relative, sep as sep2, extname as extname2, basename } from "path";
266
438
  import ts from "typescript";
267
439
  var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
268
440
  ".ts",
@@ -282,7 +454,7 @@ var INDEX_BASENAMES = /* @__PURE__ */ new Set([
282
454
  ]);
283
455
  function fileToModule(filePath, srcDir) {
284
456
  const rel = relative(srcDir, filePath);
285
- const parts = rel.split(sep);
457
+ const parts = rel.split(sep2);
286
458
  const last = parts[parts.length - 1];
287
459
  if (last === "index.ts" || last === "index.js" || last === "index.tsx" || last === "index.jsx") {
288
460
  parts.pop();
@@ -350,13 +522,13 @@ function resolveImports(rawImports, basePackage, currentModule, isPackage) {
350
522
  function findSourceFiles(dir) {
351
523
  const results = [];
352
524
  function walk(d) {
353
- const entries = readdirSync(d);
525
+ const entries = readdirSync2(d);
354
526
  for (const entry of entries) {
355
527
  if (entry === "node_modules" || entry === "dist" || entry === ".git") {
356
528
  continue;
357
529
  }
358
- const full = join(d, entry);
359
- const stat = statSync(full);
530
+ const full = join2(d, entry);
531
+ const stat = statSync2(full);
360
532
  if (stat.isDirectory()) {
361
533
  walk(full);
362
534
  } else if (SUPPORTED_EXTENSIONS.has(extname2(entry))) {
@@ -444,7 +616,7 @@ function findCycles(graph) {
444
616
  function checkCircularImports(srcDir, basePackage) {
445
617
  let stat;
446
618
  try {
447
- stat = statSync(srcDir);
619
+ stat = statSync2(srcDir);
448
620
  } catch {
449
621
  console.error(`Error: ${srcDir}/ directory not found`);
450
622
  return 1;
@@ -472,10 +644,10 @@ Found ${cycles.length} circular import(s):
472
644
 
473
645
  // src/config.ts
474
646
  import { readFileSync as readFileSync3 } from "fs";
475
- import { join as join2 } from "path";
647
+ import { join as join3 } from "path";
476
648
  function loadConfig(projectDir) {
477
649
  const dir = projectDir ?? process.cwd();
478
- const pkgPath = join2(dir, "package.json");
650
+ const pkgPath = join3(dir, "package.json");
479
651
  let raw;
480
652
  try {
481
653
  raw = readFileSync3(pkgPath, "utf-8");
@@ -491,9 +663,12 @@ function loadConfig(projectDir) {
491
663
  }
492
664
 
493
665
  // src/cli.ts
666
+ function collect(value, previous) {
667
+ return [...previous, value];
668
+ }
494
669
  function getVersion() {
495
- const thisDir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath2(import.meta.url));
496
- const pkgPath = join3(thisDir, "..", "package.json");
670
+ const thisDir = typeof __dirname !== "undefined" ? __dirname : dirname2(fileURLToPath3(import.meta.url));
671
+ const pkgPath = join4(thisDir, "..", "package.json");
497
672
  try {
498
673
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
499
674
  return pkg.version ?? "0.0.0";
@@ -502,15 +677,32 @@ function getVersion() {
502
677
  }
503
678
  }
504
679
  function getReleaseScript() {
505
- const thisDir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath2(import.meta.url));
506
- return join3(thisDir, "..", "release.sh");
680
+ const thisDir = typeof __dirname !== "undefined" ? __dirname : dirname2(fileURLToPath3(import.meta.url));
681
+ return join4(thisDir, "..", "release.sh");
507
682
  }
508
683
  function buildProgram() {
509
684
  const program2 = new Command();
510
685
  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) => {
686
+ program2.command("check-chars").description("Validate files contain only allowed characters").argument("[files...]", "Files or directories to check (defaults to src/, tests/, examples/ and config files)").option("--charset <name>", "Extra charset preset (repeatable)", collect, []).option("--charset-file <path>", "Custom charset JSON file (repeatable)", collect, []).action((files, opts) => {
687
+ let charsetNames = opts.charset;
688
+ let charsetFiles = opts.charsetFile;
689
+ if (charsetNames.length === 0 && charsetFiles.length === 0) {
690
+ const envVal = process.env.APDEV_EXTRA_CHARS ?? "";
691
+ if (envVal) {
692
+ for (const item of envVal.split(",")) {
693
+ const trimmed = item.trim();
694
+ if (!trimmed) continue;
695
+ if (trimmed.includes("/") || trimmed.includes(sep3) || trimmed.endsWith(".json")) {
696
+ charsetFiles.push(trimmed);
697
+ } else {
698
+ charsetNames.push(trimmed);
699
+ }
700
+ }
701
+ }
702
+ }
703
+ const { ranges, dangerous } = resolveCharsets(charsetNames, charsetFiles);
512
704
  const resolved = files.map((f) => resolve(f));
513
- const code = checkPaths(resolved);
705
+ const code = checkPaths(resolved, ranges, dangerous);
514
706
  process.exit(code);
515
707
  });
516
708
  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) => {
@@ -526,7 +718,7 @@ function buildProgram() {
526
718
  const code = checkCircularImports(resolve(srcDir), basePackage);
527
719
  process.exit(code);
528
720
  });
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) => {
721
+ program2.command("release").description("Interactive release automation (build, tag, GitHub release, npm publish)").option("-y, --yes", "Auto-accept all defaults (silent mode)").argument("[version]", "Version to release (auto-detected from package.json if omitted)").action((version, opts) => {
530
722
  const script = getReleaseScript();
531
723
  try {
532
724
  readFileSync4(script);