apdev-js 0.1.1 → 0.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.
- package/dist/cli.js +128 -93
- package/dist/index.cjs +109 -78
- package/dist/index.d.cts +41 -4
- package/dist/index.d.ts +41 -4
- package/dist/index.js +115 -87
- package/package.json +13 -4
- package/src/charsets/base.json +44 -0
- package/src/charsets/chinese.json +9 -0
- package/src/charsets/japanese.json +11 -0
- package/src/charsets/korean.json +11 -0
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
|
|
13
|
-
import { fileURLToPath as
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 } 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"));
|
|
84
35
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
}
|
|
60
|
+
}
|
|
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
|
+
}
|
|
88
71
|
}
|
|
89
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
|
+
}
|
|
81
|
+
}
|
|
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
|
-
|
|
93
|
-
|
|
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 (
|
|
238
|
+
if (dangerousMap.has(code)) {
|
|
224
239
|
if (!commentMask.has(offset)) {
|
|
225
|
-
const name =
|
|
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 (!
|
|
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,15 @@ 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
|
+
function checkPaths(paths, extraRanges, dangerousMap) {
|
|
248
263
|
let hasError = false;
|
|
249
264
|
for (const path2 of paths) {
|
|
250
|
-
const problems = checkFile(path2);
|
|
265
|
+
const problems = checkFile(path2, 5, extraRanges, dangerousMap);
|
|
251
266
|
if (problems.length > 0) {
|
|
252
267
|
hasError = true;
|
|
253
268
|
console.log(`
|
|
@@ -262,7 +277,7 @@ ${path2} contains illegal characters:`);
|
|
|
262
277
|
|
|
263
278
|
// src/check-imports.ts
|
|
264
279
|
import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
265
|
-
import { join, relative, sep, extname as extname2, basename } from "path";
|
|
280
|
+
import { join as join2, relative, sep as sep2, extname as extname2, basename } from "path";
|
|
266
281
|
import ts from "typescript";
|
|
267
282
|
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
268
283
|
".ts",
|
|
@@ -282,7 +297,7 @@ var INDEX_BASENAMES = /* @__PURE__ */ new Set([
|
|
|
282
297
|
]);
|
|
283
298
|
function fileToModule(filePath, srcDir) {
|
|
284
299
|
const rel = relative(srcDir, filePath);
|
|
285
|
-
const parts = rel.split(
|
|
300
|
+
const parts = rel.split(sep2);
|
|
286
301
|
const last = parts[parts.length - 1];
|
|
287
302
|
if (last === "index.ts" || last === "index.js" || last === "index.tsx" || last === "index.jsx") {
|
|
288
303
|
parts.pop();
|
|
@@ -355,7 +370,7 @@ function findSourceFiles(dir) {
|
|
|
355
370
|
if (entry === "node_modules" || entry === "dist" || entry === ".git") {
|
|
356
371
|
continue;
|
|
357
372
|
}
|
|
358
|
-
const full =
|
|
373
|
+
const full = join2(d, entry);
|
|
359
374
|
const stat = statSync(full);
|
|
360
375
|
if (stat.isDirectory()) {
|
|
361
376
|
walk(full);
|
|
@@ -472,10 +487,10 @@ Found ${cycles.length} circular import(s):
|
|
|
472
487
|
|
|
473
488
|
// src/config.ts
|
|
474
489
|
import { readFileSync as readFileSync3 } from "fs";
|
|
475
|
-
import { join as
|
|
490
|
+
import { join as join3 } from "path";
|
|
476
491
|
function loadConfig(projectDir) {
|
|
477
492
|
const dir = projectDir ?? process.cwd();
|
|
478
|
-
const pkgPath =
|
|
493
|
+
const pkgPath = join3(dir, "package.json");
|
|
479
494
|
let raw;
|
|
480
495
|
try {
|
|
481
496
|
raw = readFileSync3(pkgPath, "utf-8");
|
|
@@ -491,9 +506,12 @@ function loadConfig(projectDir) {
|
|
|
491
506
|
}
|
|
492
507
|
|
|
493
508
|
// src/cli.ts
|
|
509
|
+
function collect(value, previous) {
|
|
510
|
+
return [...previous, value];
|
|
511
|
+
}
|
|
494
512
|
function getVersion() {
|
|
495
|
-
const thisDir = typeof __dirname !== "undefined" ? __dirname :
|
|
496
|
-
const pkgPath =
|
|
513
|
+
const thisDir = typeof __dirname !== "undefined" ? __dirname : dirname2(fileURLToPath3(import.meta.url));
|
|
514
|
+
const pkgPath = join4(thisDir, "..", "package.json");
|
|
497
515
|
try {
|
|
498
516
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
499
517
|
return pkg.version ?? "0.0.0";
|
|
@@ -502,15 +520,32 @@ function getVersion() {
|
|
|
502
520
|
}
|
|
503
521
|
}
|
|
504
522
|
function getReleaseScript() {
|
|
505
|
-
const thisDir = typeof __dirname !== "undefined" ? __dirname :
|
|
506
|
-
return
|
|
523
|
+
const thisDir = typeof __dirname !== "undefined" ? __dirname : dirname2(fileURLToPath3(import.meta.url));
|
|
524
|
+
return join4(thisDir, "..", "release.sh");
|
|
507
525
|
}
|
|
508
526
|
function buildProgram() {
|
|
509
527
|
const program2 = new Command();
|
|
510
528
|
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) => {
|
|
529
|
+
program2.command("check-chars").description("Validate files contain only allowed characters").argument("<files...>", "Files to check").option("--charset <name>", "Extra charset preset (repeatable)", collect, []).option("--charset-file <path>", "Custom charset JSON file (repeatable)", collect, []).action((files, opts) => {
|
|
530
|
+
let charsetNames = opts.charset;
|
|
531
|
+
let charsetFiles = opts.charsetFile;
|
|
532
|
+
if (charsetNames.length === 0 && charsetFiles.length === 0) {
|
|
533
|
+
const envVal = process.env.APDEV_EXTRA_CHARS ?? "";
|
|
534
|
+
if (envVal) {
|
|
535
|
+
for (const item of envVal.split(",")) {
|
|
536
|
+
const trimmed = item.trim();
|
|
537
|
+
if (!trimmed) continue;
|
|
538
|
+
if (trimmed.includes("/") || trimmed.includes(sep3) || trimmed.endsWith(".json")) {
|
|
539
|
+
charsetFiles.push(trimmed);
|
|
540
|
+
} else {
|
|
541
|
+
charsetNames.push(trimmed);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const { ranges, dangerous } = resolveCharsets(charsetNames, charsetFiles);
|
|
512
547
|
const resolved = files.map((f) => resolve(f));
|
|
513
|
-
const code = checkPaths(resolved);
|
|
548
|
+
const code = checkPaths(resolved, ranges, dangerous);
|
|
514
549
|
process.exit(code);
|
|
515
550
|
});
|
|
516
551
|
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 +561,7 @@ function buildProgram() {
|
|
|
526
561
|
const code = checkCircularImports(resolve(srcDir), basePackage);
|
|
527
562
|
process.exit(code);
|
|
528
563
|
});
|
|
529
|
-
program2.command("release").description("Interactive release automation (build, tag, GitHub release, npm publish)").option("--yes
|
|
564
|
+
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
565
|
const script = getReleaseScript();
|
|
531
566
|
try {
|
|
532
567
|
readFileSync4(script);
|
package/dist/index.cjs
CHANGED
|
@@ -37,7 +37,10 @@ __export(src_exports, {
|
|
|
37
37
|
fileToModule: () => fileToModule,
|
|
38
38
|
findCycles: () => findCycles,
|
|
39
39
|
isAllowedChar: () => isAllowedChar,
|
|
40
|
+
isDangerousChar: () => isDangerousChar,
|
|
41
|
+
loadCharset: () => loadCharset,
|
|
40
42
|
loadConfig: () => loadConfig,
|
|
43
|
+
resolveCharsets: () => resolveCharsets,
|
|
41
44
|
version: () => version
|
|
42
45
|
});
|
|
43
46
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -49,80 +52,100 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
|
49
52
|
// src/check-chars.ts
|
|
50
53
|
var import_node_fs = require("fs");
|
|
51
54
|
var import_node_path = require("path");
|
|
52
|
-
var
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
[
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
[
|
|
88
|
-
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Bidi control characters (Trojan Source - CVE-2021-42574)
|
|
93
|
-
[8234, "LEFT-TO-RIGHT EMBEDDING"],
|
|
94
|
-
[8235, "RIGHT-TO-LEFT EMBEDDING"],
|
|
95
|
-
[8236, "POP DIRECTIONAL FORMATTING"],
|
|
96
|
-
[8237, "LEFT-TO-RIGHT OVERRIDE"],
|
|
97
|
-
[8238, "RIGHT-TO-LEFT OVERRIDE"],
|
|
98
|
-
[8294, "LEFT-TO-RIGHT ISOLATE"],
|
|
99
|
-
[8295, "RIGHT-TO-LEFT ISOLATE"],
|
|
100
|
-
[8296, "FIRST STRONG ISOLATE"],
|
|
101
|
-
[8297, "POP DIRECTIONAL ISOLATE"],
|
|
102
|
-
// Zero-width characters
|
|
103
|
-
[8203, "ZERO WIDTH SPACE"],
|
|
104
|
-
[8204, "ZERO WIDTH NON-JOINER"],
|
|
105
|
-
[8205, "ZERO WIDTH JOINER"],
|
|
106
|
-
[8206, "LEFT-TO-RIGHT MARK"],
|
|
107
|
-
[8207, "RIGHT-TO-LEFT MARK"],
|
|
108
|
-
[8288, "WORD JOINER"]
|
|
109
|
-
]);
|
|
110
|
-
var PYTHON_SUFFIXES = /* @__PURE__ */ new Set([".py"]);
|
|
111
|
-
var JS_SUFFIXES = /* @__PURE__ */ new Set([".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"]);
|
|
112
|
-
function isAllowedChar(c) {
|
|
113
|
-
const code = c.codePointAt(0);
|
|
114
|
-
if (code <= 127) {
|
|
115
|
-
return true;
|
|
55
|
+
var import_node_url = require("url");
|
|
56
|
+
function getCharsetsDir() {
|
|
57
|
+
const thisDir = typeof __dirname !== "undefined" ? __dirname : (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
58
|
+
const devPath = (0, import_node_path.join)(thisDir, "charsets");
|
|
59
|
+
if ((0, import_node_fs.existsSync)(devPath)) {
|
|
60
|
+
return devPath;
|
|
61
|
+
}
|
|
62
|
+
return (0, import_node_path.join)(thisDir, "..", "src", "charsets");
|
|
63
|
+
}
|
|
64
|
+
function loadCharset(nameOrPath) {
|
|
65
|
+
if (nameOrPath.includes(import_node_path.sep) || nameOrPath.includes("/") || nameOrPath.endsWith(".json")) {
|
|
66
|
+
if (!(0, import_node_fs.existsSync)(nameOrPath)) {
|
|
67
|
+
throw new Error(`Charset file not found: ${nameOrPath}`);
|
|
68
|
+
}
|
|
69
|
+
return JSON.parse((0, import_node_fs.readFileSync)(nameOrPath, "utf-8"));
|
|
70
|
+
}
|
|
71
|
+
const filePath = (0, import_node_path.join)(getCharsetsDir(), `${nameOrPath}.json`);
|
|
72
|
+
if (!(0, import_node_fs.existsSync)(filePath)) {
|
|
73
|
+
throw new Error(`Unknown charset: ${nameOrPath}`);
|
|
74
|
+
}
|
|
75
|
+
return JSON.parse((0, import_node_fs.readFileSync)(filePath, "utf-8"));
|
|
76
|
+
}
|
|
77
|
+
function parseRanges(entries) {
|
|
78
|
+
return entries.map((e) => [parseInt(e.start, 16), parseInt(e.end, 16)]);
|
|
79
|
+
}
|
|
80
|
+
function parseDangerous(entries) {
|
|
81
|
+
const map = /* @__PURE__ */ new Map();
|
|
82
|
+
for (const e of entries) {
|
|
83
|
+
map.set(parseInt(e.code, 16), e.name);
|
|
84
|
+
}
|
|
85
|
+
return map;
|
|
86
|
+
}
|
|
87
|
+
function resolveCharsets(charsetNames, charsetFiles) {
|
|
88
|
+
const base = loadCharset("base");
|
|
89
|
+
const rangesSet = /* @__PURE__ */ new Map();
|
|
90
|
+
const dangerous = parseDangerous(base.dangerous ?? []);
|
|
91
|
+
function addRanges(entries) {
|
|
92
|
+
for (const [s, e] of parseRanges(entries)) {
|
|
93
|
+
rangesSet.set(`${s}-${e}`, [s, e]);
|
|
94
|
+
}
|
|
116
95
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
96
|
+
addRanges(base.emoji_ranges ?? []);
|
|
97
|
+
addRanges(base.extra_ranges ?? []);
|
|
98
|
+
for (const name of charsetNames) {
|
|
99
|
+
const data = loadCharset(name);
|
|
100
|
+
addRanges(data.emoji_ranges ?? []);
|
|
101
|
+
addRanges(data.extra_ranges ?? []);
|
|
102
|
+
if (data.dangerous) {
|
|
103
|
+
for (const [code, dname] of parseDangerous(data.dangerous)) {
|
|
104
|
+
dangerous.set(code, dname);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const path of charsetFiles) {
|
|
109
|
+
const data = loadCharset(path);
|
|
110
|
+
addRanges(data.emoji_ranges ?? []);
|
|
111
|
+
addRanges(data.extra_ranges ?? []);
|
|
112
|
+
if (data.dangerous) {
|
|
113
|
+
for (const [code, dname] of parseDangerous(data.dangerous)) {
|
|
114
|
+
dangerous.set(code, dname);
|
|
115
|
+
}
|
|
120
116
|
}
|
|
121
117
|
}
|
|
118
|
+
const ranges = [...rangesSet.values()].sort((a, b) => a[0] - b[0]);
|
|
119
|
+
return { ranges, dangerous };
|
|
120
|
+
}
|
|
121
|
+
var PYTHON_SUFFIXES = /* @__PURE__ */ new Set([".py"]);
|
|
122
|
+
var JS_SUFFIXES = /* @__PURE__ */ new Set([".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"]);
|
|
123
|
+
function isInRanges(code, ranges) {
|
|
124
|
+
if (code <= 127) return true;
|
|
125
|
+
for (const [start, end] of ranges) {
|
|
126
|
+
if (code >= start && code <= end) return true;
|
|
127
|
+
}
|
|
122
128
|
return false;
|
|
123
129
|
}
|
|
130
|
+
var _baseRanges = null;
|
|
131
|
+
var _baseDangerous = null;
|
|
132
|
+
function getBaseDefaults() {
|
|
133
|
+
if (!_baseRanges || !_baseDangerous) {
|
|
134
|
+
const defaults = resolveCharsets([], []);
|
|
135
|
+
_baseRanges = defaults.ranges;
|
|
136
|
+
_baseDangerous = defaults.dangerous;
|
|
137
|
+
}
|
|
138
|
+
return { ranges: _baseRanges, dangerous: _baseDangerous };
|
|
139
|
+
}
|
|
140
|
+
function isAllowedChar(c) {
|
|
141
|
+
const { ranges, dangerous } = getBaseDefaults();
|
|
142
|
+
const code = c.codePointAt(0);
|
|
143
|
+
if (dangerous.has(code)) return false;
|
|
144
|
+
return isInRanges(code, ranges);
|
|
145
|
+
}
|
|
124
146
|
function isDangerousChar(c) {
|
|
125
|
-
|
|
147
|
+
const { dangerous } = getBaseDefaults();
|
|
148
|
+
return dangerous.has(c.codePointAt(0));
|
|
126
149
|
}
|
|
127
150
|
function computeCommentMask(content, suffix) {
|
|
128
151
|
if (PYTHON_SUFFIXES.has(suffix)) {
|
|
@@ -241,8 +264,13 @@ function computeCommentMaskJs(content) {
|
|
|
241
264
|
}
|
|
242
265
|
return mask;
|
|
243
266
|
}
|
|
244
|
-
function checkFile(filePath, maxProblems = 5) {
|
|
267
|
+
function checkFile(filePath, maxProblems = 5, extraRanges, dangerousMap) {
|
|
245
268
|
const problems = [];
|
|
269
|
+
if (!extraRanges || !dangerousMap) {
|
|
270
|
+
const defaults = getBaseDefaults();
|
|
271
|
+
extraRanges ??= defaults.ranges;
|
|
272
|
+
dangerousMap ??= defaults.dangerous;
|
|
273
|
+
}
|
|
246
274
|
try {
|
|
247
275
|
const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
|
|
248
276
|
const suffix = (0, import_node_path.extname)(filePath).toLowerCase();
|
|
@@ -252,15 +280,15 @@ function checkFile(filePath, maxProblems = 5) {
|
|
|
252
280
|
for (const char of content) {
|
|
253
281
|
position++;
|
|
254
282
|
const code = char.codePointAt(0);
|
|
255
|
-
if (
|
|
283
|
+
if (dangerousMap.has(code)) {
|
|
256
284
|
if (!commentMask.has(offset)) {
|
|
257
|
-
const name =
|
|
285
|
+
const name = dangerousMap.get(code);
|
|
258
286
|
const hex = code.toString(16).toUpperCase().padStart(4, "0");
|
|
259
287
|
problems.push(
|
|
260
288
|
`Dangerous character in code at position ${position}: U+${hex} (${name})`
|
|
261
289
|
);
|
|
262
290
|
}
|
|
263
|
-
} else if (!
|
|
291
|
+
} else if (!isInRanges(code, extraRanges)) {
|
|
264
292
|
const hex = code.toString(16).toUpperCase().padStart(4, "0");
|
|
265
293
|
problems.push(
|
|
266
294
|
`Illegal character at position ${position}: ${JSON.stringify(char)} (U+${hex})`
|
|
@@ -271,15 +299,15 @@ function checkFile(filePath, maxProblems = 5) {
|
|
|
271
299
|
}
|
|
272
300
|
offset += char.length;
|
|
273
301
|
}
|
|
274
|
-
} catch {
|
|
275
|
-
problems.push(`Failed to read file: ${filePath}`);
|
|
302
|
+
} catch (e) {
|
|
303
|
+
problems.push(`Failed to read file: ${filePath} (${e})`);
|
|
276
304
|
}
|
|
277
305
|
return problems;
|
|
278
306
|
}
|
|
279
|
-
function checkPaths(paths) {
|
|
307
|
+
function checkPaths(paths, extraRanges, dangerousMap) {
|
|
280
308
|
let hasError = false;
|
|
281
309
|
for (const path of paths) {
|
|
282
|
-
const problems = checkFile(path);
|
|
310
|
+
const problems = checkFile(path, 5, extraRanges, dangerousMap);
|
|
283
311
|
if (problems.length > 0) {
|
|
284
312
|
hasError = true;
|
|
285
313
|
console.log(`
|
|
@@ -525,9 +553,9 @@ function loadConfig(projectDir) {
|
|
|
525
553
|
// src/index.ts
|
|
526
554
|
var import_node_fs4 = require("fs");
|
|
527
555
|
var import_node_path4 = require("path");
|
|
528
|
-
var
|
|
556
|
+
var import_node_url2 = require("url");
|
|
529
557
|
function readVersion() {
|
|
530
|
-
const thisDir = typeof __dirname !== "undefined" ? __dirname : (0, import_node_path4.dirname)((0,
|
|
558
|
+
const thisDir = typeof __dirname !== "undefined" ? __dirname : (0, import_node_path4.dirname)((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
531
559
|
try {
|
|
532
560
|
const pkg = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path4.join)(thisDir, "..", "package.json"), "utf-8"));
|
|
533
561
|
return pkg.version ?? "0.0.0";
|
|
@@ -545,6 +573,9 @@ var version = readVersion();
|
|
|
545
573
|
fileToModule,
|
|
546
574
|
findCycles,
|
|
547
575
|
isAllowedChar,
|
|
576
|
+
isDangerousChar,
|
|
577
|
+
loadCharset,
|
|
548
578
|
loadConfig,
|
|
579
|
+
resolveCharsets,
|
|
549
580
|
version
|
|
550
581
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,14 +1,51 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Character validation tool.
|
|
3
|
+
*
|
|
4
|
+
* Checks that files contain only allowed characters: ASCII, common emoji,
|
|
5
|
+
* and standard technical symbols (arrows, box-drawing, math operators, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Additionally flags dangerous invisible/bidi characters in code regions
|
|
8
|
+
* (Trojan Source - CVE-2021-42574) while allowing them in comments.
|
|
9
|
+
*/
|
|
10
|
+
interface RangeEntry {
|
|
11
|
+
start: string;
|
|
12
|
+
end: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
interface DangerousEntry {
|
|
16
|
+
code: string;
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
interface CharsetData {
|
|
20
|
+
name: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
emoji_ranges?: RangeEntry[];
|
|
23
|
+
extra_ranges?: RangeEntry[];
|
|
24
|
+
dangerous?: DangerousEntry[];
|
|
25
|
+
}
|
|
26
|
+
declare function loadCharset(nameOrPath: string): CharsetData;
|
|
27
|
+
declare function resolveCharsets(charsetNames: string[], charsetFiles: string[]): {
|
|
28
|
+
ranges: [number, number][];
|
|
29
|
+
dangerous: Map<number, string>;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Return true if the character is in the base allowed set.
|
|
33
|
+
*
|
|
34
|
+
* Dangerous codepoints (Trojan Source vectors) are excluded even though
|
|
35
|
+
* they fall within allowed Unicode ranges.
|
|
36
|
+
*/
|
|
2
37
|
declare function isAllowedChar(c: string): boolean;
|
|
38
|
+
/** Return true if the character is a dangerous invisible/bidi codepoint. */
|
|
39
|
+
declare function isDangerousChar(c: string): boolean;
|
|
3
40
|
/**
|
|
4
41
|
* Check a single file for illegal characters.
|
|
5
42
|
* Returns a list of problem descriptions (empty if the file is clean).
|
|
6
43
|
*/
|
|
7
|
-
declare function checkFile(filePath: string, maxProblems?: number): string[];
|
|
44
|
+
declare function checkFile(filePath: string, maxProblems?: number, extraRanges?: [number, number][], dangerousMap?: Map<number, string>): string[];
|
|
8
45
|
/**
|
|
9
46
|
* Check multiple files. Returns 0 if all clean, 1 if any have problems.
|
|
10
47
|
*/
|
|
11
|
-
declare function checkPaths(paths: string[]): number;
|
|
48
|
+
declare function checkPaths(paths: string[], extraRanges?: [number, number][], dangerousMap?: Map<number, string>): number;
|
|
12
49
|
|
|
13
50
|
/**
|
|
14
51
|
* Circular import detection tool.
|
|
@@ -38,4 +75,4 @@ declare function loadConfig(projectDir?: string): Record<string, unknown>;
|
|
|
38
75
|
|
|
39
76
|
declare const version: string;
|
|
40
77
|
|
|
41
|
-
export { buildDependencyGraph, checkCircularImports, checkFile, checkPaths, fileToModule, findCycles, isAllowedChar, loadConfig, version };
|
|
78
|
+
export { buildDependencyGraph, checkCircularImports, checkFile, checkPaths, fileToModule, findCycles, isAllowedChar, isDangerousChar, loadCharset, loadConfig, resolveCharsets, version };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,51 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Character validation tool.
|
|
3
|
+
*
|
|
4
|
+
* Checks that files contain only allowed characters: ASCII, common emoji,
|
|
5
|
+
* and standard technical symbols (arrows, box-drawing, math operators, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Additionally flags dangerous invisible/bidi characters in code regions
|
|
8
|
+
* (Trojan Source - CVE-2021-42574) while allowing them in comments.
|
|
9
|
+
*/
|
|
10
|
+
interface RangeEntry {
|
|
11
|
+
start: string;
|
|
12
|
+
end: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
interface DangerousEntry {
|
|
16
|
+
code: string;
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
interface CharsetData {
|
|
20
|
+
name: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
emoji_ranges?: RangeEntry[];
|
|
23
|
+
extra_ranges?: RangeEntry[];
|
|
24
|
+
dangerous?: DangerousEntry[];
|
|
25
|
+
}
|
|
26
|
+
declare function loadCharset(nameOrPath: string): CharsetData;
|
|
27
|
+
declare function resolveCharsets(charsetNames: string[], charsetFiles: string[]): {
|
|
28
|
+
ranges: [number, number][];
|
|
29
|
+
dangerous: Map<number, string>;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Return true if the character is in the base allowed set.
|
|
33
|
+
*
|
|
34
|
+
* Dangerous codepoints (Trojan Source vectors) are excluded even though
|
|
35
|
+
* they fall within allowed Unicode ranges.
|
|
36
|
+
*/
|
|
2
37
|
declare function isAllowedChar(c: string): boolean;
|
|
38
|
+
/** Return true if the character is a dangerous invisible/bidi codepoint. */
|
|
39
|
+
declare function isDangerousChar(c: string): boolean;
|
|
3
40
|
/**
|
|
4
41
|
* Check a single file for illegal characters.
|
|
5
42
|
* Returns a list of problem descriptions (empty if the file is clean).
|
|
6
43
|
*/
|
|
7
|
-
declare function checkFile(filePath: string, maxProblems?: number): string[];
|
|
44
|
+
declare function checkFile(filePath: string, maxProblems?: number, extraRanges?: [number, number][], dangerousMap?: Map<number, string>): string[];
|
|
8
45
|
/**
|
|
9
46
|
* Check multiple files. Returns 0 if all clean, 1 if any have problems.
|
|
10
47
|
*/
|
|
11
|
-
declare function checkPaths(paths: string[]): number;
|
|
48
|
+
declare function checkPaths(paths: string[], extraRanges?: [number, number][], dangerousMap?: Map<number, string>): number;
|
|
12
49
|
|
|
13
50
|
/**
|
|
14
51
|
* Circular import detection tool.
|
|
@@ -38,4 +75,4 @@ declare function loadConfig(projectDir?: string): Record<string, unknown>;
|
|
|
38
75
|
|
|
39
76
|
declare const version: string;
|
|
40
77
|
|
|
41
|
-
export { buildDependencyGraph, checkCircularImports, checkFile, checkPaths, fileToModule, findCycles, isAllowedChar, loadConfig, version };
|
|
78
|
+
export { buildDependencyGraph, checkCircularImports, checkFile, checkPaths, fileToModule, findCycles, isAllowedChar, isDangerousChar, loadCharset, loadConfig, resolveCharsets, version };
|
package/dist/index.js
CHANGED
|
@@ -6,82 +6,102 @@ var getDirname = () => path.dirname(getFilename());
|
|
|
6
6
|
var __dirname = /* @__PURE__ */ getDirname();
|
|
7
7
|
|
|
8
8
|
// src/check-chars.ts
|
|
9
|
-
import { readFileSync } from "fs";
|
|
10
|
-
import { extname } from "path";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
[
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
[
|
|
47
|
-
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Bidi control characters (Trojan Source - CVE-2021-42574)
|
|
52
|
-
[8234, "LEFT-TO-RIGHT EMBEDDING"],
|
|
53
|
-
[8235, "RIGHT-TO-LEFT EMBEDDING"],
|
|
54
|
-
[8236, "POP DIRECTIONAL FORMATTING"],
|
|
55
|
-
[8237, "LEFT-TO-RIGHT OVERRIDE"],
|
|
56
|
-
[8238, "RIGHT-TO-LEFT OVERRIDE"],
|
|
57
|
-
[8294, "LEFT-TO-RIGHT ISOLATE"],
|
|
58
|
-
[8295, "RIGHT-TO-LEFT ISOLATE"],
|
|
59
|
-
[8296, "FIRST STRONG ISOLATE"],
|
|
60
|
-
[8297, "POP DIRECTIONAL ISOLATE"],
|
|
61
|
-
// Zero-width characters
|
|
62
|
-
[8203, "ZERO WIDTH SPACE"],
|
|
63
|
-
[8204, "ZERO WIDTH NON-JOINER"],
|
|
64
|
-
[8205, "ZERO WIDTH JOINER"],
|
|
65
|
-
[8206, "LEFT-TO-RIGHT MARK"],
|
|
66
|
-
[8207, "RIGHT-TO-LEFT MARK"],
|
|
67
|
-
[8288, "WORD JOINER"]
|
|
68
|
-
]);
|
|
69
|
-
var PYTHON_SUFFIXES = /* @__PURE__ */ new Set([".py"]);
|
|
70
|
-
var JS_SUFFIXES = /* @__PURE__ */ new Set([".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"]);
|
|
71
|
-
function isAllowedChar(c) {
|
|
72
|
-
const code = c.codePointAt(0);
|
|
73
|
-
if (code <= 127) {
|
|
74
|
-
return true;
|
|
9
|
+
import { readFileSync, existsSync } from "fs";
|
|
10
|
+
import { extname, dirname, join, sep } from "path";
|
|
11
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12
|
+
function getCharsetsDir() {
|
|
13
|
+
const thisDir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath2(import.meta.url));
|
|
14
|
+
const devPath = join(thisDir, "charsets");
|
|
15
|
+
if (existsSync(devPath)) {
|
|
16
|
+
return devPath;
|
|
17
|
+
}
|
|
18
|
+
return join(thisDir, "..", "src", "charsets");
|
|
19
|
+
}
|
|
20
|
+
function loadCharset(nameOrPath) {
|
|
21
|
+
if (nameOrPath.includes(sep) || nameOrPath.includes("/") || nameOrPath.endsWith(".json")) {
|
|
22
|
+
if (!existsSync(nameOrPath)) {
|
|
23
|
+
throw new Error(`Charset file not found: ${nameOrPath}`);
|
|
24
|
+
}
|
|
25
|
+
return JSON.parse(readFileSync(nameOrPath, "utf-8"));
|
|
26
|
+
}
|
|
27
|
+
const filePath = join(getCharsetsDir(), `${nameOrPath}.json`);
|
|
28
|
+
if (!existsSync(filePath)) {
|
|
29
|
+
throw new Error(`Unknown charset: ${nameOrPath}`);
|
|
30
|
+
}
|
|
31
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
32
|
+
}
|
|
33
|
+
function parseRanges(entries) {
|
|
34
|
+
return entries.map((e) => [parseInt(e.start, 16), parseInt(e.end, 16)]);
|
|
35
|
+
}
|
|
36
|
+
function parseDangerous(entries) {
|
|
37
|
+
const map = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
map.set(parseInt(e.code, 16), e.name);
|
|
40
|
+
}
|
|
41
|
+
return map;
|
|
42
|
+
}
|
|
43
|
+
function resolveCharsets(charsetNames, charsetFiles) {
|
|
44
|
+
const base = loadCharset("base");
|
|
45
|
+
const rangesSet = /* @__PURE__ */ new Map();
|
|
46
|
+
const dangerous = parseDangerous(base.dangerous ?? []);
|
|
47
|
+
function addRanges(entries) {
|
|
48
|
+
for (const [s, e] of parseRanges(entries)) {
|
|
49
|
+
rangesSet.set(`${s}-${e}`, [s, e]);
|
|
50
|
+
}
|
|
75
51
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
52
|
+
addRanges(base.emoji_ranges ?? []);
|
|
53
|
+
addRanges(base.extra_ranges ?? []);
|
|
54
|
+
for (const name of charsetNames) {
|
|
55
|
+
const data = loadCharset(name);
|
|
56
|
+
addRanges(data.emoji_ranges ?? []);
|
|
57
|
+
addRanges(data.extra_ranges ?? []);
|
|
58
|
+
if (data.dangerous) {
|
|
59
|
+
for (const [code, dname] of parseDangerous(data.dangerous)) {
|
|
60
|
+
dangerous.set(code, dname);
|
|
61
|
+
}
|
|
79
62
|
}
|
|
80
63
|
}
|
|
64
|
+
for (const path2 of charsetFiles) {
|
|
65
|
+
const data = loadCharset(path2);
|
|
66
|
+
addRanges(data.emoji_ranges ?? []);
|
|
67
|
+
addRanges(data.extra_ranges ?? []);
|
|
68
|
+
if (data.dangerous) {
|
|
69
|
+
for (const [code, dname] of parseDangerous(data.dangerous)) {
|
|
70
|
+
dangerous.set(code, dname);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const ranges = [...rangesSet.values()].sort((a, b) => a[0] - b[0]);
|
|
75
|
+
return { ranges, dangerous };
|
|
76
|
+
}
|
|
77
|
+
var PYTHON_SUFFIXES = /* @__PURE__ */ new Set([".py"]);
|
|
78
|
+
var JS_SUFFIXES = /* @__PURE__ */ new Set([".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"]);
|
|
79
|
+
function isInRanges(code, ranges) {
|
|
80
|
+
if (code <= 127) return true;
|
|
81
|
+
for (const [start, end] of ranges) {
|
|
82
|
+
if (code >= start && code <= end) return true;
|
|
83
|
+
}
|
|
81
84
|
return false;
|
|
82
85
|
}
|
|
86
|
+
var _baseRanges = null;
|
|
87
|
+
var _baseDangerous = null;
|
|
88
|
+
function getBaseDefaults() {
|
|
89
|
+
if (!_baseRanges || !_baseDangerous) {
|
|
90
|
+
const defaults = resolveCharsets([], []);
|
|
91
|
+
_baseRanges = defaults.ranges;
|
|
92
|
+
_baseDangerous = defaults.dangerous;
|
|
93
|
+
}
|
|
94
|
+
return { ranges: _baseRanges, dangerous: _baseDangerous };
|
|
95
|
+
}
|
|
96
|
+
function isAllowedChar(c) {
|
|
97
|
+
const { ranges, dangerous } = getBaseDefaults();
|
|
98
|
+
const code = c.codePointAt(0);
|
|
99
|
+
if (dangerous.has(code)) return false;
|
|
100
|
+
return isInRanges(code, ranges);
|
|
101
|
+
}
|
|
83
102
|
function isDangerousChar(c) {
|
|
84
|
-
|
|
103
|
+
const { dangerous } = getBaseDefaults();
|
|
104
|
+
return dangerous.has(c.codePointAt(0));
|
|
85
105
|
}
|
|
86
106
|
function computeCommentMask(content, suffix) {
|
|
87
107
|
if (PYTHON_SUFFIXES.has(suffix)) {
|
|
@@ -200,8 +220,13 @@ function computeCommentMaskJs(content) {
|
|
|
200
220
|
}
|
|
201
221
|
return mask;
|
|
202
222
|
}
|
|
203
|
-
function checkFile(filePath, maxProblems = 5) {
|
|
223
|
+
function checkFile(filePath, maxProblems = 5, extraRanges, dangerousMap) {
|
|
204
224
|
const problems = [];
|
|
225
|
+
if (!extraRanges || !dangerousMap) {
|
|
226
|
+
const defaults = getBaseDefaults();
|
|
227
|
+
extraRanges ??= defaults.ranges;
|
|
228
|
+
dangerousMap ??= defaults.dangerous;
|
|
229
|
+
}
|
|
205
230
|
try {
|
|
206
231
|
const content = readFileSync(filePath, "utf-8");
|
|
207
232
|
const suffix = extname(filePath).toLowerCase();
|
|
@@ -211,15 +236,15 @@ function checkFile(filePath, maxProblems = 5) {
|
|
|
211
236
|
for (const char of content) {
|
|
212
237
|
position++;
|
|
213
238
|
const code = char.codePointAt(0);
|
|
214
|
-
if (
|
|
239
|
+
if (dangerousMap.has(code)) {
|
|
215
240
|
if (!commentMask.has(offset)) {
|
|
216
|
-
const name =
|
|
241
|
+
const name = dangerousMap.get(code);
|
|
217
242
|
const hex = code.toString(16).toUpperCase().padStart(4, "0");
|
|
218
243
|
problems.push(
|
|
219
244
|
`Dangerous character in code at position ${position}: U+${hex} (${name})`
|
|
220
245
|
);
|
|
221
246
|
}
|
|
222
|
-
} else if (!
|
|
247
|
+
} else if (!isInRanges(code, extraRanges)) {
|
|
223
248
|
const hex = code.toString(16).toUpperCase().padStart(4, "0");
|
|
224
249
|
problems.push(
|
|
225
250
|
`Illegal character at position ${position}: ${JSON.stringify(char)} (U+${hex})`
|
|
@@ -230,15 +255,15 @@ function checkFile(filePath, maxProblems = 5) {
|
|
|
230
255
|
}
|
|
231
256
|
offset += char.length;
|
|
232
257
|
}
|
|
233
|
-
} catch {
|
|
234
|
-
problems.push(`Failed to read file: ${filePath}`);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
problems.push(`Failed to read file: ${filePath} (${e})`);
|
|
235
260
|
}
|
|
236
261
|
return problems;
|
|
237
262
|
}
|
|
238
|
-
function checkPaths(paths) {
|
|
263
|
+
function checkPaths(paths, extraRanges, dangerousMap) {
|
|
239
264
|
let hasError = false;
|
|
240
265
|
for (const path2 of paths) {
|
|
241
|
-
const problems = checkFile(path2);
|
|
266
|
+
const problems = checkFile(path2, 5, extraRanges, dangerousMap);
|
|
242
267
|
if (problems.length > 0) {
|
|
243
268
|
hasError = true;
|
|
244
269
|
console.log(`
|
|
@@ -253,7 +278,7 @@ ${path2} contains illegal characters:`);
|
|
|
253
278
|
|
|
254
279
|
// src/check-imports.ts
|
|
255
280
|
import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
256
|
-
import { join, relative, sep, extname as extname2, basename } from "path";
|
|
281
|
+
import { join as join2, relative, sep as sep2, extname as extname2, basename } from "path";
|
|
257
282
|
import ts from "typescript";
|
|
258
283
|
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
259
284
|
".ts",
|
|
@@ -273,7 +298,7 @@ var INDEX_BASENAMES = /* @__PURE__ */ new Set([
|
|
|
273
298
|
]);
|
|
274
299
|
function fileToModule(filePath, srcDir) {
|
|
275
300
|
const rel = relative(srcDir, filePath);
|
|
276
|
-
const parts = rel.split(
|
|
301
|
+
const parts = rel.split(sep2);
|
|
277
302
|
const last = parts[parts.length - 1];
|
|
278
303
|
if (last === "index.ts" || last === "index.js" || last === "index.tsx" || last === "index.jsx") {
|
|
279
304
|
parts.pop();
|
|
@@ -346,7 +371,7 @@ function findSourceFiles(dir) {
|
|
|
346
371
|
if (entry === "node_modules" || entry === "dist" || entry === ".git") {
|
|
347
372
|
continue;
|
|
348
373
|
}
|
|
349
|
-
const full =
|
|
374
|
+
const full = join2(d, entry);
|
|
350
375
|
const stat = statSync(full);
|
|
351
376
|
if (stat.isDirectory()) {
|
|
352
377
|
walk(full);
|
|
@@ -463,10 +488,10 @@ Found ${cycles.length} circular import(s):
|
|
|
463
488
|
|
|
464
489
|
// src/config.ts
|
|
465
490
|
import { readFileSync as readFileSync3 } from "fs";
|
|
466
|
-
import { join as
|
|
491
|
+
import { join as join3 } from "path";
|
|
467
492
|
function loadConfig(projectDir) {
|
|
468
493
|
const dir = projectDir ?? process.cwd();
|
|
469
|
-
const pkgPath =
|
|
494
|
+
const pkgPath = join3(dir, "package.json");
|
|
470
495
|
let raw;
|
|
471
496
|
try {
|
|
472
497
|
raw = readFileSync3(pkgPath, "utf-8");
|
|
@@ -483,12 +508,12 @@ function loadConfig(projectDir) {
|
|
|
483
508
|
|
|
484
509
|
// src/index.ts
|
|
485
510
|
import { readFileSync as readFileSync4 } from "fs";
|
|
486
|
-
import { dirname, join as
|
|
487
|
-
import { fileURLToPath as
|
|
511
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
512
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
488
513
|
function readVersion() {
|
|
489
|
-
const thisDir = typeof __dirname !== "undefined" ? __dirname :
|
|
514
|
+
const thisDir = typeof __dirname !== "undefined" ? __dirname : dirname2(fileURLToPath3(import.meta.url));
|
|
490
515
|
try {
|
|
491
|
-
const pkg = JSON.parse(readFileSync4(
|
|
516
|
+
const pkg = JSON.parse(readFileSync4(join4(thisDir, "..", "package.json"), "utf-8"));
|
|
492
517
|
return pkg.version ?? "0.0.0";
|
|
493
518
|
} catch {
|
|
494
519
|
return "0.0.0";
|
|
@@ -503,6 +528,9 @@ export {
|
|
|
503
528
|
fileToModule,
|
|
504
529
|
findCycles,
|
|
505
530
|
isAllowedChar,
|
|
531
|
+
isDangerousChar,
|
|
532
|
+
loadCharset,
|
|
506
533
|
loadConfig,
|
|
534
|
+
resolveCharsets,
|
|
507
535
|
version
|
|
508
536
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apdev-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Shared development tools for TypeScript/JavaScript projects - character validation, circular import detection, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
|
-
"release.sh"
|
|
21
|
+
"release.sh",
|
|
22
|
+
"src/charsets"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
24
25
|
"build": "tsup",
|
|
@@ -46,12 +47,20 @@
|
|
|
46
47
|
"url": "https://github.com/aipartnerup/apdev/issues"
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
|
-
"commander": "^13.0.0"
|
|
50
|
-
|
|
50
|
+
"commander": "^13.0.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"typescript": "^5.0.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"typescript": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
51
59
|
},
|
|
52
60
|
"devDependencies": {
|
|
53
61
|
"@types/node": "^22.0.0",
|
|
54
62
|
"tsup": "^8.0.0",
|
|
63
|
+
"typescript": "^5.7.0",
|
|
55
64
|
"vitest": "^3.0.0"
|
|
56
65
|
}
|
|
57
66
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "base",
|
|
3
|
+
"description": "Default: ASCII + emoji + technical symbols",
|
|
4
|
+
"emoji_ranges": [
|
|
5
|
+
{ "start": "0x1F300", "end": "0x1F5FF", "name": "Symbols and Pictographs" },
|
|
6
|
+
{ "start": "0x1F600", "end": "0x1F64F", "name": "Emoticons" },
|
|
7
|
+
{ "start": "0x1F680", "end": "0x1F6FF", "name": "Transport and Map Symbols" },
|
|
8
|
+
{ "start": "0x1F780", "end": "0x1F7FF", "name": "Geometric Shapes Extended" },
|
|
9
|
+
{ "start": "0x1F900", "end": "0x1F9FF", "name": "Supplemental Symbols and Pictographs" },
|
|
10
|
+
{ "start": "0x2600", "end": "0x26FF", "name": "Miscellaneous Symbols" },
|
|
11
|
+
{ "start": "0x2700", "end": "0x27BF", "name": "Dingbats" }
|
|
12
|
+
],
|
|
13
|
+
"extra_ranges": [
|
|
14
|
+
{ "start": "0x0080", "end": "0x00FF", "name": "Latin-1 Supplement" },
|
|
15
|
+
{ "start": "0x2000", "end": "0x206F", "name": "General Punctuation" },
|
|
16
|
+
{ "start": "0x2100", "end": "0x214F", "name": "Letterlike Symbols" },
|
|
17
|
+
{ "start": "0x2190", "end": "0x21FF", "name": "Arrows" },
|
|
18
|
+
{ "start": "0x2200", "end": "0x22FF", "name": "Mathematical Operators" },
|
|
19
|
+
{ "start": "0x2300", "end": "0x23FF", "name": "Miscellaneous Technical" },
|
|
20
|
+
{ "start": "0x2500", "end": "0x257F", "name": "Box Drawing" },
|
|
21
|
+
{ "start": "0x2580", "end": "0x259F", "name": "Block Elements" },
|
|
22
|
+
{ "start": "0x25A0", "end": "0x25FF", "name": "Geometric Shapes" },
|
|
23
|
+
{ "start": "0x2800", "end": "0x28FF", "name": "Braille Patterns" },
|
|
24
|
+
{ "start": "0x2B00", "end": "0x2BFF", "name": "Miscellaneous Symbols and Arrows" },
|
|
25
|
+
{ "start": "0xFE00", "end": "0xFE0F", "name": "Variation Selectors" }
|
|
26
|
+
],
|
|
27
|
+
"dangerous": [
|
|
28
|
+
{ "code": "0x202A", "name": "LEFT-TO-RIGHT EMBEDDING" },
|
|
29
|
+
{ "code": "0x202B", "name": "RIGHT-TO-LEFT EMBEDDING" },
|
|
30
|
+
{ "code": "0x202C", "name": "POP DIRECTIONAL FORMATTING" },
|
|
31
|
+
{ "code": "0x202D", "name": "LEFT-TO-RIGHT OVERRIDE" },
|
|
32
|
+
{ "code": "0x202E", "name": "RIGHT-TO-LEFT OVERRIDE" },
|
|
33
|
+
{ "code": "0x2066", "name": "LEFT-TO-RIGHT ISOLATE" },
|
|
34
|
+
{ "code": "0x2067", "name": "RIGHT-TO-LEFT ISOLATE" },
|
|
35
|
+
{ "code": "0x2068", "name": "FIRST STRONG ISOLATE" },
|
|
36
|
+
{ "code": "0x2069", "name": "POP DIRECTIONAL ISOLATE" },
|
|
37
|
+
{ "code": "0x200B", "name": "ZERO WIDTH SPACE" },
|
|
38
|
+
{ "code": "0x200C", "name": "ZERO WIDTH NON-JOINER" },
|
|
39
|
+
{ "code": "0x200D", "name": "ZERO WIDTH JOINER" },
|
|
40
|
+
{ "code": "0x200E", "name": "LEFT-TO-RIGHT MARK" },
|
|
41
|
+
{ "code": "0x200F", "name": "RIGHT-TO-LEFT MARK" },
|
|
42
|
+
{ "code": "0x2060", "name": "WORD JOINER" }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chinese",
|
|
3
|
+
"description": "Chinese: CJK Unified Ideographs, CJK Punctuation, Fullwidth Forms",
|
|
4
|
+
"extra_ranges": [
|
|
5
|
+
{ "start": "0x3000", "end": "0x303F", "name": "CJK Symbols and Punctuation" },
|
|
6
|
+
{ "start": "0x4E00", "end": "0x9FFF", "name": "CJK Unified Ideographs" },
|
|
7
|
+
{ "start": "0xFF00", "end": "0xFFEF", "name": "Fullwidth Forms" }
|
|
8
|
+
]
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "japanese",
|
|
3
|
+
"description": "Japanese: Hiragana, Katakana, CJK Unified Ideographs, CJK Punctuation, Fullwidth Forms",
|
|
4
|
+
"extra_ranges": [
|
|
5
|
+
{ "start": "0x3000", "end": "0x303F", "name": "CJK Symbols and Punctuation" },
|
|
6
|
+
{ "start": "0x3040", "end": "0x309F", "name": "Hiragana" },
|
|
7
|
+
{ "start": "0x30A0", "end": "0x30FF", "name": "Katakana" },
|
|
8
|
+
{ "start": "0x4E00", "end": "0x9FFF", "name": "CJK Unified Ideographs" },
|
|
9
|
+
{ "start": "0xFF00", "end": "0xFFEF", "name": "Fullwidth Forms" }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "korean",
|
|
3
|
+
"description": "Korean: Hangul Jamo, Hangul Syllables, CJK Unified Ideographs, CJK Punctuation, Fullwidth Forms",
|
|
4
|
+
"extra_ranges": [
|
|
5
|
+
{ "start": "0x1100", "end": "0x11FF", "name": "Hangul Jamo" },
|
|
6
|
+
{ "start": "0x3000", "end": "0x303F", "name": "CJK Symbols and Punctuation" },
|
|
7
|
+
{ "start": "0x4E00", "end": "0x9FFF", "name": "CJK Unified Ideographs" },
|
|
8
|
+
{ "start": "0xAC00", "end": "0xD7AF", "name": "Hangul Syllables" },
|
|
9
|
+
{ "start": "0xFF00", "end": "0xFFEF", "name": "Fullwidth Forms" }
|
|
10
|
+
]
|
|
11
|
+
}
|