@appartmint/tsm-scripts 0.0.2 → 0.0.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@appartmint/tsm-scripts",
3
3
  "author": "App Art Mint LLC",
4
- "version": "0.0.2",
4
+ "version": "0.0.3",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "description": "TypeScript Node Scripts from App Art Mint LLC",
@@ -1,13 +1,17 @@
1
- import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
- import * as ts from 'typescript';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import ts from 'typescript';
4
4
 
5
5
  const rootArg = process.argv[2];
6
6
  const ROOT = rootArg ? path.resolve(rootArg) : process.cwd();
7
- const SRC = path.join(ROOT, 'src');
8
7
 
9
8
  type ImportGroup = 'external' | 'workspace' | 'local' | 'environment';
10
9
 
10
+ interface TsConfigAliasEntry {
11
+ fileNames: Set<string>;
12
+ aliasPrefixes: string[];
13
+ }
14
+
11
15
  interface ParsedImport {
12
16
  spec: string;
13
17
  group: ImportGroup;
@@ -24,17 +28,66 @@ function walkTsFiles(dir: string, out: string[] = []): string[] {
24
28
  return out;
25
29
  }
26
30
 
27
- const FROM_RE = /from\s+(['"])([^'"]+)\1/;
28
-
29
31
  function getModuleSpecifier(text: string): string {
30
- const m = FROM_RE.exec(text);
32
+ const m = /from\s+(['"])([^'"]+)\1/.exec(text);
31
33
  return m ? m[2] : '';
32
34
  }
33
35
 
34
- function classify(spec: string): ImportGroup {
36
+ function normalizePathForCompare(p: string): string {
37
+ return path.resolve(p).replace(/\\/g, '/').toLowerCase();
38
+ }
39
+
40
+ function normalizeAliasPrefix(key: string): string {
41
+ if (key === '*' || !key) return '';
42
+ if (key.endsWith('/*')) return key.slice(0, -1);
43
+ return key;
44
+ }
45
+
46
+ function discoverRootTsconfigs(rootDir: string): string[] {
47
+ return fs
48
+ .readdirSync(rootDir, { withFileTypes: true })
49
+ .filter((entry) => entry.isFile() && /^tsconfig.*\.json$/i.test(entry.name))
50
+ .map((entry) => path.join(rootDir, entry.name));
51
+ }
52
+
53
+ function loadTsConfigAliasEntries(rootDir: string): TsConfigAliasEntry[] {
54
+ const entries: TsConfigAliasEntry[] = [];
55
+ for (const configPath of discoverRootTsconfigs(rootDir)) {
56
+ const read = ts.readConfigFile(configPath, (fileName) => ts.sys.readFile(fileName));
57
+ if (read.error) continue;
58
+
59
+ const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(configPath), undefined, configPath);
60
+ const paths = parsed.options.paths ?? {};
61
+ const aliasPrefixes = Object.keys(paths)
62
+ .map((key) => normalizeAliasPrefix(key))
63
+ .filter((key) => key.length > 0);
64
+ if (aliasPrefixes.length === 0) continue;
65
+
66
+ entries.push({
67
+ fileNames: new Set(parsed.fileNames.map((name) => normalizePathForCompare(name))),
68
+ aliasPrefixes
69
+ });
70
+ }
71
+ return entries;
72
+ }
73
+
74
+ function resolveLocalAliasPrefixes(filePath: string, tsconfigEntries: TsConfigAliasEntry[]): string[] {
75
+ const normalizedFilePath = normalizePathForCompare(filePath);
76
+ const prefixes = new Set<string>();
77
+ for (const entry of tsconfigEntries) {
78
+ if (!entry.fileNames.has(normalizedFilePath)) continue;
79
+ for (const prefix of entry.aliasPrefixes) {
80
+ prefixes.add(prefix);
81
+ }
82
+ }
83
+ return [...prefixes];
84
+ }
85
+
86
+ function classify(spec: string, localAliasPrefixes: string[]): ImportGroup {
35
87
  if (!spec) return 'external';
36
- if (spec.includes('/environments/') || spec.includes('environments/environment')) return 'environment';
88
+ if (spec.includes('/environments/') || spec.includes('environments/environment') || spec.includes('.env')) return 'environment';
37
89
  if (spec.startsWith('@app-art-mint/') || spec.startsWith('@appartmint/')) return 'workspace';
90
+ if (localAliasPrefixes.some((prefix) => spec === prefix || spec.startsWith(prefix))) return 'local';
38
91
  if (spec.startsWith('.') || spec.startsWith('..')) return 'local';
39
92
  return 'external';
40
93
  }
@@ -101,53 +154,15 @@ function compareSpec(a: string, b: string): number {
101
154
  return sortKey(a).localeCompare(sortKey(b), 'en');
102
155
  }
103
156
 
104
- function stripKnownSectionComments(text: string): string {
105
- let s = text;
106
- s = s.replace(/\n\s*\/\*\*\s*\n\s*\*\s*Routes\s*\n\s*\*\/\s*\n/g, '\n');
107
- s = s.replace(/\n\s*\/\*\*\s*\n\s*\*\s*Module\s*\n\s*\*\/\s*\n/g, '\n');
108
- return s;
109
- }
110
-
111
- function removeLeadingSectionJSDocOnStatements(text: string): string {
112
- const sf = ts.createSourceFile('x.ts', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
113
- const toRemove: [number, number][] = [];
114
-
115
- for (const stmt of sf.statements) {
116
- const ranges = ts.getLeadingCommentRanges(text, stmt.getFullStart());
117
- if (!ranges?.length) continue;
118
- for (const r of ranges) {
119
- const ctext = text.slice(r.pos, r.end);
120
- if (!/^\s*\/\*\*/.test(ctext)) continue;
121
- const inner = ctext
122
- .replace(/^\s*\/\*\*\s*/, '')
123
- .replace(/\s*\*\/\s*$/, '')
124
- .replace(/^\s*\*\s?/gm, '')
125
- .trim();
126
- const lines = inner.split(/\n/).map((l) => l.trim()).filter(Boolean);
127
- if (lines.length === 1 && /^(Routes|Module|Imports)$/.test(lines[0])) {
128
- toRemove.push([r.pos, r.end]);
129
- }
130
- }
131
- }
132
-
133
- toRemove.sort((a, b) => b[0] - a[0]);
134
- let out = text;
135
- for (const [pos, end] of toRemove) {
136
- out = out.slice(0, pos) + out.slice(end);
137
- }
138
- return out;
139
- }
140
157
 
141
- function organizeImports(sourceText: string, filePath: string): string {
158
+ function organizeImports(sourceText: string, filePath: string, tsconfigEntries: TsConfigAliasEntry[]): string {
142
159
  const sf = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
160
+ const localAliasPrefixes = resolveLocalAliasPrefixes(filePath, tsconfigEntries);
143
161
 
144
162
  let i = 0;
145
163
  const stmts = sf.statements;
146
164
  while (i < stmts.length && ts.isImportDeclaration(stmts[i])) i++;
147
-
148
- if (i === 0) {
149
- return stripKnownSectionComments(sourceText);
150
- }
165
+ if (i === 0) return sourceText;
151
166
 
152
167
  const importNodes = stmts.slice(0, i);
153
168
  const replaceStart = importNodes[0].getFullStart();
@@ -159,7 +174,7 @@ function organizeImports(sourceText: string, filePath: string): string {
159
174
 
160
175
  const parsed: ParsedImport[] = importTexts.map((trimmed) => {
161
176
  const spec = getModuleSpecifier(trimmed);
162
- const group = classify(spec);
177
+ const group = classify(spec, localAliasPrefixes);
163
178
  const wrapped = wrapImportIfNeeded(trimmed, 100, semicolon);
164
179
  return { spec, group, finalText: wrapped, multi: isMultiLineImport(wrapped) };
165
180
  });
@@ -177,20 +192,19 @@ function organizeImports(sourceText: string, filePath: string): string {
177
192
  const before = sourceText.slice(0, replaceStart);
178
193
  const after = sourceText.slice(replaceEnd);
179
194
 
180
- let result = before + newBlock + after;
181
- result = stripKnownSectionComments(result);
195
+ const result = before + newBlock + after;
182
196
  return result;
183
197
  }
184
198
 
185
199
  let changed = 0;
186
- const files = walkTsFiles(SRC);
200
+ const files = walkTsFiles(ROOT);
201
+ const tsconfigEntries = loadTsConfigAliasEntries(ROOT);
187
202
 
188
203
  for (const filePath of files) {
189
204
  let text = fs.readFileSync(filePath, 'utf8');
190
205
  const original = text;
191
206
 
192
- text = organizeImports(text, filePath);
193
- text = removeLeadingSectionJSDocOnStatements(text);
207
+ text = organizeImports(text, filePath, tsconfigEntries);
194
208
  text = text.replace(/\n{3,}(\/\*\*)/g, '\n\n$1');
195
209
 
196
210
  if (text !== original) {