@reliverse/dler 1.6.3 → 1.6.4

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.
@@ -1,44 +1,496 @@
1
+ import { getFileImportsExports, extname, join } from "@reliverse/pathkit";
2
+ import fs from "@reliverse/relifso";
1
3
  import { relinka } from "@reliverse/relinka";
2
4
  import {
3
5
  defineCommand,
4
- defineArgs,
5
6
  selectPrompt,
6
- multiselectPrompt
7
+ multiselectPrompt,
8
+ confirmPrompt,
9
+ defineArgs
7
10
  } from "@reliverse/rempts";
11
+ import { readTSConfig } from "pkg-types";
12
+ import { checkMissingDependencies } from "../deps/impl/wrapper.js";
13
+ import { IGNORE_PATTERNS } from "../../libs/sdk/sdk-impl/constants.js";
14
+ const ALLOWED_FILE_EXTENSIONS = {
15
+ src: ["", ".ts", ".css", ".json"],
16
+ // ✅ .ts files allowed in src
17
+ "dist-npm": ["", ".js", ".css", ".json"],
18
+ // ❌ no .ts files in npm dist
19
+ "dist-jsr": ["", ".ts", ".css", ".json"],
20
+ // ✅ .ts files allowed in jsr dist
21
+ "dist-libs/npm": ["", ".js", ".css", ".json"],
22
+ // ❌ no .ts files in npm libs
23
+ "dist-libs/jsr": ["", ".ts", ".css", ".json"]
24
+ // ✅ .ts files allowed in jsr libs
25
+ };
26
+ const STRICT_FILE_EXTENSIONS = {
27
+ src: [".ts", ".css", ".json"],
28
+ // ✅ .ts files required in src
29
+ "dist-npm": [".js", ".css", ".json"],
30
+ // ❌ no .ts files in npm dist
31
+ "dist-jsr": [".ts", ".css", ".json"],
32
+ // ✅ .ts files required in jsr dist
33
+ "dist-libs/npm": [".js", ".css", ".json"],
34
+ // ❌ no .ts files in npm libs
35
+ "dist-libs/jsr": [".ts", ".css", ".json"]
36
+ // ✅ .ts files required in jsr libs
37
+ };
38
+ const ALLOWED_IMPORT_EXTENSIONS = {
39
+ src: ["", ".js", ".css", ".json"],
40
+ // ❌ no .ts imports (use .js)
41
+ "dist-npm": ["", ".js", ".css", ".json"],
42
+ // ❌ no .ts imports
43
+ "dist-jsr": ["", ".ts", ".css", ".json"],
44
+ // ✅ .ts imports allowed
45
+ "dist-libs/npm": ["", ".js", ".css", ".json"],
46
+ // ❌ no .ts imports
47
+ "dist-libs/jsr": ["", ".ts", ".css", ".json"]
48
+ // ✅ .ts imports allowed
49
+ };
50
+ const STRICT_IMPORT_EXTENSIONS = {
51
+ src: [".js", ".css", ".json"],
52
+ // ❌ no .ts imports, no empty
53
+ "dist-npm": [".js", ".css", ".json"],
54
+ // ❌ no .ts imports
55
+ "dist-jsr": [".ts", ".css", ".json"],
56
+ // ✅ .ts imports required
57
+ "dist-libs/npm": [".js", ".css", ".json"],
58
+ // ❌ no .ts imports
59
+ "dist-libs/jsr": [".ts", ".css", ".json"]
60
+ // ✅ .ts imports required
61
+ };
62
+ async function validateDirectory(dir) {
63
+ try {
64
+ const stat = await fs.stat(dir);
65
+ return stat.isDirectory();
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+ function shouldIgnoreFile(filePath) {
71
+ const pathSegments = filePath.split("/");
72
+ return IGNORE_PATTERNS.some(
73
+ (pattern) => pathSegments.some((segment) => segment.includes(pattern))
74
+ );
75
+ }
76
+ async function getAllFiles(dir, onProgress) {
77
+ const results = [];
78
+ let fileCount = 0;
79
+ async function searchDirectory(directory) {
80
+ try {
81
+ const files = await fs.readdir(directory);
82
+ for (const file of files) {
83
+ const fullPath = join(directory, file);
84
+ if (shouldIgnoreFile(fullPath)) {
85
+ continue;
86
+ }
87
+ try {
88
+ const stat = await fs.stat(fullPath);
89
+ if (stat.isDirectory()) {
90
+ if (file === "templates") continue;
91
+ await searchDirectory(fullPath);
92
+ } else {
93
+ results.push(fullPath);
94
+ fileCount++;
95
+ onProgress?.(fileCount, fileCount, fullPath);
96
+ }
97
+ } catch {
98
+ relinka("warn", `skipping inaccessible file: ${fullPath}`);
99
+ }
100
+ }
101
+ } catch {
102
+ relinka("warn", `skipping inaccessible directory: ${directory}`);
103
+ }
104
+ }
105
+ if (!await validateDirectory(dir)) {
106
+ throw new Error(`directory "${dir}" does not exist or is not accessible`);
107
+ }
108
+ await searchDirectory(dir);
109
+ return results;
110
+ }
111
+ async function validateModuleResolution() {
112
+ try {
113
+ const tsconfig = await readTSConfig();
114
+ const moduleResolution = tsconfig?.compilerOptions?.moduleResolution;
115
+ if (!moduleResolution) {
116
+ throw new Error("moduleResolution is not specified in tsconfig.json");
117
+ }
118
+ if (moduleResolution !== "bundler" && moduleResolution !== "nodenext") {
119
+ throw new Error(
120
+ `unsupported moduleResolution: ${moduleResolution}. Only "bundler" and "nodenext" are supported`
121
+ );
122
+ }
123
+ return moduleResolution;
124
+ } catch (error) {
125
+ throw new Error(
126
+ `failed to read tsconfig.json: ${error instanceof Error ? error.message : "unknown error"}`
127
+ );
128
+ }
129
+ }
130
+ function getAllowedFileExtensions(directory, strict, moduleResolution) {
131
+ if (!strict) {
132
+ return ALLOWED_FILE_EXTENSIONS[directory];
133
+ }
134
+ if (moduleResolution === "bundler") {
135
+ return STRICT_FILE_EXTENSIONS[directory];
136
+ }
137
+ return STRICT_FILE_EXTENSIONS[directory];
138
+ }
139
+ function getAllowedImportExtensions(directory, strict) {
140
+ if (strict) {
141
+ return STRICT_IMPORT_EXTENSIONS[directory];
142
+ }
143
+ return ALLOWED_IMPORT_EXTENSIONS[directory];
144
+ }
145
+ export async function checkFileExtensions(options) {
146
+ const startTime = Date.now();
147
+ const issues = [];
148
+ const { directory, strict, moduleResolution, onProgress } = options;
149
+ const allowedExts = getAllowedFileExtensions(
150
+ directory,
151
+ strict,
152
+ moduleResolution
153
+ );
154
+ try {
155
+ const files = await getAllFiles(directory, onProgress);
156
+ const batchSize = 50;
157
+ const batches = [];
158
+ for (let i = 0; i < files.length; i += batchSize) {
159
+ batches.push(files.slice(i, i + batchSize));
160
+ }
161
+ for (const [batchIndex, batch] of batches.entries()) {
162
+ const batchPromises = batch.map(async (file, fileIndex) => {
163
+ const globalIndex = batchIndex * batchSize + fileIndex;
164
+ onProgress?.(globalIndex + 1, files.length, file);
165
+ const ext = extname(file);
166
+ if (!allowedExts.includes(ext)) {
167
+ let message = `file has disallowed extension "${ext}" (allowed: ${allowedExts.join(", ")})`;
168
+ if (ext === ".ts" && (directory === "dist-npm" || directory === "dist-libs/npm")) {
169
+ message = `typescript file found in javascript environment: ${file} (should be compiled to .js)`;
170
+ } else if (ext === ".js" && (directory === "src" || directory === "dist-jsr" || directory === "dist-libs/jsr")) {
171
+ message = `javascript file found in typescript environment: ${file} (should be .ts)`;
172
+ }
173
+ return {
174
+ file,
175
+ message,
176
+ type: "file-extension"
177
+ };
178
+ }
179
+ return null;
180
+ });
181
+ const batchResults = await Promise.all(batchPromises);
182
+ issues.push(...batchResults.filter(Boolean));
183
+ }
184
+ return {
185
+ success: issues.length === 0,
186
+ issues,
187
+ stats: {
188
+ filesChecked: files.length,
189
+ importsChecked: 0,
190
+ timeElapsed: Date.now() - startTime
191
+ }
192
+ };
193
+ } catch (error) {
194
+ throw new Error(
195
+ `failed to check file extensions: ${error instanceof Error ? error.message : "unknown error"}`
196
+ );
197
+ }
198
+ }
199
+ export async function checkPathExtensions(options) {
200
+ const startTime = Date.now();
201
+ const issues = [];
202
+ const { directory, strict, onProgress } = options;
203
+ const allowedExts = getAllowedImportExtensions(directory, strict);
204
+ try {
205
+ const files = await getAllFiles(directory);
206
+ let totalImports = 0;
207
+ const importableFiles = files.filter((file) => {
208
+ const ext = extname(file);
209
+ return [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext);
210
+ });
211
+ const batchSize = 20;
212
+ const batches = [];
213
+ for (let i = 0; i < importableFiles.length; i += batchSize) {
214
+ batches.push(importableFiles.slice(i, i + batchSize));
215
+ }
216
+ for (const [batchIndex, batch] of batches.entries()) {
217
+ const batchPromises = batch.map(async (file, fileIndex) => {
218
+ const globalIndex = batchIndex * batchSize + fileIndex;
219
+ onProgress?.(globalIndex + 1, importableFiles.length, file);
220
+ try {
221
+ const content = await fs.readFile(file, "utf-8");
222
+ const imports = getFileImportsExports(content, {
223
+ kind: "import",
224
+ pathTypes: ["relative", "alias"]
225
+ });
226
+ totalImports += imports.length;
227
+ const fileIssues = [];
228
+ for (const imp of imports) {
229
+ if (!imp.source) continue;
230
+ const ext = extname(imp.source);
231
+ if (!allowedExts.includes(ext)) {
232
+ const isTypeScriptImport = ext === ".ts";
233
+ const isJsEnvironment = directory === "src" || directory === "dist-npm" || directory === "dist-libs/npm";
234
+ let message;
235
+ if (isTypeScriptImport && isJsEnvironment) {
236
+ message = `import uses .ts extension in javascript environment: ${imp.source} (use .js extension instead)`;
237
+ } else {
238
+ message = `import has disallowed extension "${ext}": ${imp.source} (allowed: ${allowedExts.join(", ")})`;
239
+ }
240
+ fileIssues.push({
241
+ file,
242
+ message,
243
+ type: "path-extension",
244
+ line: getLineNumber(content, imp.start),
245
+ source: imp.source
246
+ });
247
+ }
248
+ }
249
+ return { issues: fileIssues, importCount: imports.length };
250
+ } catch {
251
+ relinka("warn", `skipping unreadable file: ${file}`);
252
+ return { issues: [], importCount: 0 };
253
+ }
254
+ });
255
+ const batchResults = await Promise.all(batchPromises);
256
+ for (const result of batchResults) {
257
+ issues.push(...result.issues);
258
+ totalImports += result.importCount;
259
+ }
260
+ }
261
+ return {
262
+ success: issues.length === 0,
263
+ issues,
264
+ stats: {
265
+ filesChecked: importableFiles.length,
266
+ importsChecked: totalImports,
267
+ timeElapsed: Date.now() - startTime
268
+ }
269
+ };
270
+ } catch (error) {
271
+ throw new Error(
272
+ `failed to check path extensions: ${error instanceof Error ? error.message : "unknown error"}`
273
+ );
274
+ }
275
+ }
276
+ function getLineNumber(content, position) {
277
+ return content.slice(0, position).split("\n").length;
278
+ }
279
+ function displayResults(checkType, directory, result) {
280
+ const { success, issues, stats } = result;
281
+ if (success) {
282
+ relinka("success", `\u2713 ${checkType} check passed for ${directory}`);
283
+ relinka(
284
+ "info",
285
+ ` files checked: ${stats.filesChecked}, imports: ${stats.importsChecked}, time: ${stats.timeElapsed}ms`
286
+ );
287
+ } else {
288
+ relinka(
289
+ "error",
290
+ `\u2717 ${checkType} check failed for ${directory} (${issues.length} issues)`
291
+ );
292
+ const fileIssues = issues.filter((i) => i.type === "file-extension");
293
+ const pathIssues = issues.filter((i) => i.type === "path-extension");
294
+ const missingDepIssues = issues.filter(
295
+ (i) => i.type === "missing-dependency"
296
+ );
297
+ const builtinIssues = issues.filter((i) => i.type === "builtin-module");
298
+ if (fileIssues.length > 0) {
299
+ relinka("error", ` file extension issues (${fileIssues.length}):`);
300
+ for (const issue of fileIssues.slice(0, 10)) {
301
+ relinka("error", ` ${issue.file}: ${issue.message}`);
302
+ }
303
+ if (fileIssues.length > 10) {
304
+ relinka("error", ` ... and ${fileIssues.length - 10} more`);
305
+ }
306
+ }
307
+ if (pathIssues.length > 0) {
308
+ relinka("error", ` import extension issues (${pathIssues.length}):`);
309
+ for (const issue of pathIssues.slice(0, 10)) {
310
+ relinka("error", ` ${issue.file}:${issue.line}: ${issue.message}`);
311
+ }
312
+ if (pathIssues.length > 10) {
313
+ relinka("error", ` ... and ${pathIssues.length - 10} more`);
314
+ }
315
+ }
316
+ if (missingDepIssues.length > 0) {
317
+ relinka("error", ` missing dependencies (${missingDepIssues.length}):`);
318
+ for (const issue of missingDepIssues.slice(0, 10)) {
319
+ relinka("error", ` ${issue.message}`);
320
+ }
321
+ if (missingDepIssues.length > 10) {
322
+ relinka("error", ` ... and ${missingDepIssues.length - 10} more`);
323
+ }
324
+ }
325
+ if (builtinIssues.length > 0) {
326
+ relinka("warn", ` builtin modules used (${builtinIssues.length}):`);
327
+ for (const issue of builtinIssues.slice(0, 10)) {
328
+ relinka("warn", ` ${issue.message}`);
329
+ }
330
+ if (builtinIssues.length > 10) {
331
+ relinka("warn", ` ... and ${builtinIssues.length - 10} more`);
332
+ }
333
+ }
334
+ relinka(
335
+ "info",
336
+ ` stats: ${stats.filesChecked} files, ${stats.importsChecked} imports, ${stats.timeElapsed}ms`
337
+ );
338
+ }
339
+ }
8
340
  export default defineCommand({
9
341
  meta: {
10
342
  name: "check",
11
343
  version: "1.0.0",
12
- description: "Describe what check command does."
344
+ description: "check your codebase source and dists for any issues."
13
345
  },
14
346
  args: defineArgs({
15
- exampleArg: {
347
+ directory: {
348
+ type: "string",
349
+ description: "directory to check (src, dist-npm, dist-jsr, dist-libs/npm, dist-libs/jsr, or all)"
350
+ },
351
+ checks: {
16
352
  type: "string",
17
- default: "defaultValue",
18
- description: "An example argument"
353
+ description: "comma-separated list of checks to run (missing-dependencies,file-extensions,path-extensions)"
354
+ },
355
+ strict: {
356
+ type: "boolean",
357
+ description: "enable strict mode (requires explicit extensions)"
358
+ },
359
+ json: {
360
+ type: "boolean",
361
+ description: "output results in JSON format"
19
362
  }
20
363
  }),
21
364
  async run({ args }) {
22
365
  relinka(
23
366
  "info",
24
- "This command allows you to check your codebase source and dists for any issues."
25
- );
26
- const dir = await selectPrompt({
27
- title: "Select a directory to check",
28
- options: [
29
- { label: "all", value: "all" },
30
- { label: "src", value: "src" },
31
- { label: "dist-npm", value: "dist-npm" },
32
- { label: "dist-jsr", value: "dist-jsr" },
33
- { label: "dist-libs-npm", value: "dist-libs-npm" },
34
- { label: "dist-libs-jsr", value: "dist-libs-jsr" }
35
- ]
36
- });
37
- const checks = await multiselectPrompt({
38
- title: "Select checks to run",
39
- options: [
40
- { label: "path-extensions", value: "path-extensions" }
41
- ]
42
- });
367
+ "this command checks your codebase for extension and dependency issues."
368
+ );
369
+ relinka(
370
+ "info",
371
+ "\u{1F4C1} file rules: .ts files allowed in src/jsr dirs, .js files in npm dirs"
372
+ );
373
+ relinka(
374
+ "info",
375
+ "\u{1F4E6} import rules: use .js imports in src/npm dirs, .ts imports in jsr dirs"
376
+ );
377
+ let moduleResolution;
378
+ try {
379
+ moduleResolution = await validateModuleResolution();
380
+ relinka("info", `using moduleResolution: ${moduleResolution}`);
381
+ } catch (error) {
382
+ relinka(
383
+ "error",
384
+ error instanceof Error ? error.message : "unknown error"
385
+ );
386
+ return;
387
+ }
388
+ let dir;
389
+ let checks;
390
+ let strict;
391
+ if (args.directory) {
392
+ dir = args.directory;
393
+ } else {
394
+ dir = await selectPrompt({
395
+ title: "select a directory to check",
396
+ options: [
397
+ { label: "all directories", value: "all" },
398
+ { label: "src (typescript source)", value: "src" },
399
+ { label: "dist-npm (compiled js)", value: "dist-npm" },
400
+ { label: "dist-jsr (typescript)", value: "dist-jsr" },
401
+ { label: "dist-libs/npm (compiled js)", value: "dist-libs/npm" },
402
+ { label: "dist-libs/jsr (typescript)", value: "dist-libs/jsr" }
403
+ ]
404
+ });
405
+ }
406
+ if (args.checks) {
407
+ checks = args.checks.split(",");
408
+ } else {
409
+ checks = await multiselectPrompt({
410
+ title: "select checks to run",
411
+ options: [
412
+ { label: "missing dependencies", value: "missing-dependencies" },
413
+ {
414
+ label: "file extensions (.ts/.js files)",
415
+ value: "file-extensions"
416
+ },
417
+ {
418
+ label: "import path extensions (.ts/.js imports)",
419
+ value: "path-extensions"
420
+ }
421
+ ]
422
+ });
423
+ }
424
+ if (args.strict !== void 0) {
425
+ strict = args.strict;
426
+ } else {
427
+ strict = await confirmPrompt({
428
+ title: "activate strict mode?",
429
+ content: "strict mode requires explicit extensions (no empty extensions). files: .ts in src/jsr dirs, .js in npm dirs. imports: .js in src/npm dirs, .ts in jsr dirs. templates folder is always exempt."
430
+ });
431
+ }
432
+ if (checks.length === 0) {
433
+ relinka("warn", "no checks selected, exiting...");
434
+ return;
435
+ }
436
+ const directories = dir === "all" ? [
437
+ "src",
438
+ "dist-npm",
439
+ "dist-jsr",
440
+ "dist-libs/npm",
441
+ "dist-libs/jsr"
442
+ ] : [dir];
443
+ for (const directory of directories) {
444
+ relinka("info", `
445
+ checking directory: ${directory}`);
446
+ const onProgress = (current, total) => {
447
+ if (current % 10 === 0 || current === total) {
448
+ process.stdout.write(`\r progress: ${current}/${total} files...`);
449
+ }
450
+ };
451
+ try {
452
+ if (checks.includes("missing-dependencies")) {
453
+ process.stdout.write(" checking missing dependencies...\n");
454
+ const result = await checkMissingDependencies({
455
+ directory,
456
+ strict: false,
457
+ // not used for deps check
458
+ moduleResolution,
459
+ // not used for deps check
460
+ onProgress
461
+ });
462
+ process.stdout.write("\r");
463
+ displayResults("missing dependencies", directory, result);
464
+ }
465
+ if (checks.includes("file-extensions")) {
466
+ process.stdout.write(" checking file extensions...\n");
467
+ const result = await checkFileExtensions({
468
+ directory,
469
+ strict,
470
+ moduleResolution,
471
+ onProgress
472
+ });
473
+ process.stdout.write("\r");
474
+ displayResults("file extensions", directory, result);
475
+ }
476
+ if (checks.includes("path-extensions")) {
477
+ process.stdout.write(" checking import path extensions...\n");
478
+ const result = await checkPathExtensions({
479
+ directory,
480
+ strict,
481
+ moduleResolution,
482
+ onProgress
483
+ });
484
+ process.stdout.write("\r");
485
+ displayResults("path extensions", directory, result);
486
+ }
487
+ } catch (error) {
488
+ relinka(
489
+ "error",
490
+ `failed to check ${directory}: ${error instanceof Error ? error.message : "unknown error"}`
491
+ );
492
+ }
493
+ }
494
+ relinka("success", "all checks completed!");
43
495
  }
44
496
  });