roadmapsmith 0.9.0 → 0.9.2
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/bin/cli.js +254 -254
- package/package.json +56 -56
- package/src/config.js +219 -219
- package/src/generator/index.js +614 -614
- package/src/index.js +11 -11
- package/src/io.js +264 -264
- package/src/match.js +86 -86
- package/src/model.js +33 -33
- package/src/parser/index.js +107 -107
- package/src/renderer/professional.js +544 -544
- package/src/sync/index.js +1 -1
- package/src/templates/index.js +1 -1
- package/src/utils.js +142 -142
- package/src/validator/index.js +730 -730
- package/templates/roadmap.template.md +1 -1
package/src/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
module.exports = {
|
|
4
|
-
generator: require('./generator'),
|
|
5
|
-
parser: require('./parser'),
|
|
6
|
-
sync: require('./sync'),
|
|
7
|
-
validator: require('./validator'),
|
|
8
|
-
config: require('./config'),
|
|
9
|
-
model: require('./model'),
|
|
10
|
-
renderer: require('./renderer')
|
|
11
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
generator: require('./generator'),
|
|
5
|
+
parser: require('./parser'),
|
|
6
|
+
sync: require('./sync'),
|
|
7
|
+
validator: require('./validator'),
|
|
8
|
+
config: require('./config'),
|
|
9
|
+
model: require('./model'),
|
|
10
|
+
renderer: require('./renderer')
|
|
11
|
+
};
|
package/src/io.js
CHANGED
|
@@ -1,264 +1,264 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { ensureTrailingNewline, toPosix } = require('./utils');
|
|
6
|
-
|
|
7
|
-
const DEFAULT_IGNORED_DIRS = new Set([
|
|
8
|
-
'.git',
|
|
9
|
-
'.idea',
|
|
10
|
-
'.vscode',
|
|
11
|
-
'.next',
|
|
12
|
-
'.nuxt',
|
|
13
|
-
'.turbo',
|
|
14
|
-
'.cache',
|
|
15
|
-
'dist',
|
|
16
|
-
'build',
|
|
17
|
-
'coverage',
|
|
18
|
-
'target',
|
|
19
|
-
'node_modules',
|
|
20
|
-
'.venv',
|
|
21
|
-
'venv',
|
|
22
|
-
'__pycache__',
|
|
23
|
-
'.pytest_cache',
|
|
24
|
-
'.mypy_cache',
|
|
25
|
-
'.ruff_cache'
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
function readTextIfExists(filePath) {
|
|
29
|
-
try {
|
|
30
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
31
|
-
} catch {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function ensureDirForFile(filePath) {
|
|
37
|
-
const dir = path.dirname(filePath);
|
|
38
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function writeText(filePath, content, options = {}) {
|
|
42
|
-
const next = ensureTrailingNewline(content);
|
|
43
|
-
const before = readTextIfExists(filePath);
|
|
44
|
-
const changed = before == null || before !== next;
|
|
45
|
-
|
|
46
|
-
if (!changed) {
|
|
47
|
-
return {
|
|
48
|
-
changed: false,
|
|
49
|
-
before,
|
|
50
|
-
after: next,
|
|
51
|
-
path: filePath
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!options.dryRun) {
|
|
56
|
-
ensureDirForFile(filePath);
|
|
57
|
-
fs.writeFileSync(filePath, next, 'utf8');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
changed: true,
|
|
62
|
-
before,
|
|
63
|
-
after: next,
|
|
64
|
-
path: filePath
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function walkFiles(rootPath, options = {}) {
|
|
69
|
-
const ignoredDirs = options.ignoredDirs || DEFAULT_IGNORED_DIRS;
|
|
70
|
-
const result = [];
|
|
71
|
-
|
|
72
|
-
function walk(current) {
|
|
73
|
-
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
74
|
-
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
75
|
-
|
|
76
|
-
for (const entry of entries) {
|
|
77
|
-
const absolutePath = path.join(current, entry.name);
|
|
78
|
-
const relativePath = toPosix(path.relative(rootPath, absolutePath));
|
|
79
|
-
if (entry.isDirectory()) {
|
|
80
|
-
if (ignoredDirs.has(entry.name)) {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
walk(absolutePath);
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
result.push(relativePath);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
walk(rootPath);
|
|
91
|
-
return result;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function parseJsonIfExists(filePath) {
|
|
95
|
-
const content = readTextIfExists(filePath);
|
|
96
|
-
if (!content) {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
try {
|
|
100
|
-
return JSON.parse(content);
|
|
101
|
-
} catch {
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function detectLanguages(files) {
|
|
107
|
-
const languageByExtension = {
|
|
108
|
-
'.js': 'JavaScript',
|
|
109
|
-
'.cjs': 'JavaScript',
|
|
110
|
-
'.mjs': 'JavaScript',
|
|
111
|
-
'.ts': 'TypeScript',
|
|
112
|
-
'.tsx': 'TypeScript',
|
|
113
|
-
'.jsx': 'JavaScript',
|
|
114
|
-
'.py': 'Python',
|
|
115
|
-
'.go': 'Go',
|
|
116
|
-
'.rs': 'Rust',
|
|
117
|
-
'.java': 'Java',
|
|
118
|
-
'.kt': 'Kotlin',
|
|
119
|
-
'.swift': 'Swift',
|
|
120
|
-
'.rb': 'Ruby',
|
|
121
|
-
'.php': 'PHP',
|
|
122
|
-
'.cs': 'C#',
|
|
123
|
-
'.cpp': 'C++',
|
|
124
|
-
'.c': 'C',
|
|
125
|
-
'.h': 'C',
|
|
126
|
-
'.sh': 'Shell'
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const languages = new Set();
|
|
130
|
-
for (const file of files) {
|
|
131
|
-
const ext = path.extname(file).toLowerCase();
|
|
132
|
-
if (languageByExtension[ext]) {
|
|
133
|
-
languages.add(languageByExtension[ext]);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return Array.from(languages).sort((left, right) => left.localeCompare(right));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function detectTestFrameworks(projectRoot, files) {
|
|
140
|
-
const frameworks = new Set();
|
|
141
|
-
|
|
142
|
-
const packageJson = parseJsonIfExists(path.join(projectRoot, 'package.json'));
|
|
143
|
-
if (packageJson) {
|
|
144
|
-
const scripts = packageJson.scripts || {};
|
|
145
|
-
if (scripts.test) {
|
|
146
|
-
frameworks.add('node-test-script');
|
|
147
|
-
}
|
|
148
|
-
const deps = {
|
|
149
|
-
...(packageJson.dependencies || {}),
|
|
150
|
-
...(packageJson.devDependencies || {})
|
|
151
|
-
};
|
|
152
|
-
if (deps.jest) frameworks.add('jest');
|
|
153
|
-
if (deps.vitest) frameworks.add('vitest');
|
|
154
|
-
if (deps.mocha) frameworks.add('mocha');
|
|
155
|
-
if (deps.ava) frameworks.add('ava');
|
|
156
|
-
if (deps.tap) frameworks.add('tap');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (files.some((file) => file.endsWith('pyproject.toml')) || files.some((file) => file.endsWith('pytest.ini'))) {
|
|
160
|
-
frameworks.add('pytest');
|
|
161
|
-
}
|
|
162
|
-
if (files.some((file) => /(^|\/)test_.*\.py$/.test(file)) || files.some((file) => /(^|\/)tests\//.test(file))) {
|
|
163
|
-
frameworks.add('python-tests');
|
|
164
|
-
}
|
|
165
|
-
if (files.some((file) => file.endsWith('go.mod'))) {
|
|
166
|
-
frameworks.add('go');
|
|
167
|
-
}
|
|
168
|
-
if (files.some((file) => file.endsWith('_test.go'))) {
|
|
169
|
-
frameworks.add('go-test');
|
|
170
|
-
}
|
|
171
|
-
if (files.some((file) => file.endsWith('Cargo.toml'))) {
|
|
172
|
-
frameworks.add('rust');
|
|
173
|
-
}
|
|
174
|
-
if (files.some((file) => /(^|\/)tests\//.test(file) && file.endsWith('.rs'))) {
|
|
175
|
-
frameworks.add('cargo-test');
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (files.some((file) => /(^|\/)(__tests__|tests)\//.test(file) || /\.test\.|\.spec\./.test(file))) {
|
|
179
|
-
frameworks.add('generic-tests');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return Array.from(frameworks).sort((left, right) => left.localeCompare(right));
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Detects workspace package roots (one level deep) from package.json workspaces field
|
|
186
|
-
// and physical scan of packages/, apps/, tools/. Supports prefix/* and prefix/** globs.
|
|
187
|
-
function detectWorkspaces(projectRoot, files) {
|
|
188
|
-
const packages = new Set();
|
|
189
|
-
const workspacePrefixes = ['packages/', 'apps/', 'tools/'];
|
|
190
|
-
|
|
191
|
-
for (const file of files) {
|
|
192
|
-
if (!file.endsWith('/package.json')) continue;
|
|
193
|
-
for (const prefix of workspacePrefixes) {
|
|
194
|
-
if (!file.startsWith(prefix)) continue;
|
|
195
|
-
const rest = file.slice(prefix.length);
|
|
196
|
-
const segments = rest.split('/');
|
|
197
|
-
if (segments.length === 2 && segments[1] === 'package.json') {
|
|
198
|
-
packages.add(prefix + segments[0]);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const pkg = parseJsonIfExists(path.join(projectRoot, 'package.json'));
|
|
204
|
-
const globs = Array.isArray(pkg && pkg.workspaces) ? pkg.workspaces
|
|
205
|
-
: (pkg && pkg.workspaces && Array.isArray(pkg.workspaces.packages)) ? pkg.workspaces.packages
|
|
206
|
-
: [];
|
|
207
|
-
for (const glob of globs) {
|
|
208
|
-
const match = glob.match(/^([A-Za-z0-9_.-]+)\/\*{1,2}$/);
|
|
209
|
-
if (!match) continue;
|
|
210
|
-
const prefix = match[1] + '/';
|
|
211
|
-
for (const file of files) {
|
|
212
|
-
if (!file.startsWith(prefix) || !file.endsWith('/package.json')) continue;
|
|
213
|
-
const rest = file.slice(prefix.length);
|
|
214
|
-
const segments = rest.split('/');
|
|
215
|
-
if (segments.length === 2) packages.add(prefix + segments[0]);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return Array.from(packages).sort((a, b) => a.localeCompare(b));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function lineDiff(before, after) {
|
|
223
|
-
const left = (before || '').split(/\r?\n/);
|
|
224
|
-
const right = (after || '').split(/\r?\n/);
|
|
225
|
-
const max = Math.max(left.length, right.length);
|
|
226
|
-
const changes = [];
|
|
227
|
-
|
|
228
|
-
for (let i = 0; i < max; i += 1) {
|
|
229
|
-
const oldLine = left[i] == null ? '' : left[i];
|
|
230
|
-
const newLine = right[i] == null ? '' : right[i];
|
|
231
|
-
if (oldLine !== newLine) {
|
|
232
|
-
changes.push({ index: i + 1, oldLine, newLine });
|
|
233
|
-
}
|
|
234
|
-
if (changes.length >= 20) {
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return changes;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function printDryRunDiff(filePath, before, after) {
|
|
243
|
-
const changes = lineDiff(before, after);
|
|
244
|
-
console.log(`Dry run: ${filePath}`);
|
|
245
|
-
if (changes.length === 0) {
|
|
246
|
-
console.log('- no line changes');
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
for (const change of changes) {
|
|
251
|
-
console.log(`L${change.index} - ${change.oldLine}`);
|
|
252
|
-
console.log(`L${change.index} + ${change.newLine}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
module.exports = {
|
|
257
|
-
detectLanguages,
|
|
258
|
-
detectTestFrameworks,
|
|
259
|
-
detectWorkspaces,
|
|
260
|
-
printDryRunDiff,
|
|
261
|
-
readTextIfExists,
|
|
262
|
-
walkFiles,
|
|
263
|
-
writeText
|
|
264
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { ensureTrailingNewline, toPosix } = require('./utils');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_IGNORED_DIRS = new Set([
|
|
8
|
+
'.git',
|
|
9
|
+
'.idea',
|
|
10
|
+
'.vscode',
|
|
11
|
+
'.next',
|
|
12
|
+
'.nuxt',
|
|
13
|
+
'.turbo',
|
|
14
|
+
'.cache',
|
|
15
|
+
'dist',
|
|
16
|
+
'build',
|
|
17
|
+
'coverage',
|
|
18
|
+
'target',
|
|
19
|
+
'node_modules',
|
|
20
|
+
'.venv',
|
|
21
|
+
'venv',
|
|
22
|
+
'__pycache__',
|
|
23
|
+
'.pytest_cache',
|
|
24
|
+
'.mypy_cache',
|
|
25
|
+
'.ruff_cache'
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function readTextIfExists(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureDirForFile(filePath) {
|
|
37
|
+
const dir = path.dirname(filePath);
|
|
38
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeText(filePath, content, options = {}) {
|
|
42
|
+
const next = ensureTrailingNewline(content);
|
|
43
|
+
const before = readTextIfExists(filePath);
|
|
44
|
+
const changed = before == null || before !== next;
|
|
45
|
+
|
|
46
|
+
if (!changed) {
|
|
47
|
+
return {
|
|
48
|
+
changed: false,
|
|
49
|
+
before,
|
|
50
|
+
after: next,
|
|
51
|
+
path: filePath
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!options.dryRun) {
|
|
56
|
+
ensureDirForFile(filePath);
|
|
57
|
+
fs.writeFileSync(filePath, next, 'utf8');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
changed: true,
|
|
62
|
+
before,
|
|
63
|
+
after: next,
|
|
64
|
+
path: filePath
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function walkFiles(rootPath, options = {}) {
|
|
69
|
+
const ignoredDirs = options.ignoredDirs || DEFAULT_IGNORED_DIRS;
|
|
70
|
+
const result = [];
|
|
71
|
+
|
|
72
|
+
function walk(current) {
|
|
73
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
74
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
75
|
+
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const absolutePath = path.join(current, entry.name);
|
|
78
|
+
const relativePath = toPosix(path.relative(rootPath, absolutePath));
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
if (ignoredDirs.has(entry.name)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
walk(absolutePath);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
result.push(relativePath);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
walk(rootPath);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseJsonIfExists(filePath) {
|
|
95
|
+
const content = readTextIfExists(filePath);
|
|
96
|
+
if (!content) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(content);
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function detectLanguages(files) {
|
|
107
|
+
const languageByExtension = {
|
|
108
|
+
'.js': 'JavaScript',
|
|
109
|
+
'.cjs': 'JavaScript',
|
|
110
|
+
'.mjs': 'JavaScript',
|
|
111
|
+
'.ts': 'TypeScript',
|
|
112
|
+
'.tsx': 'TypeScript',
|
|
113
|
+
'.jsx': 'JavaScript',
|
|
114
|
+
'.py': 'Python',
|
|
115
|
+
'.go': 'Go',
|
|
116
|
+
'.rs': 'Rust',
|
|
117
|
+
'.java': 'Java',
|
|
118
|
+
'.kt': 'Kotlin',
|
|
119
|
+
'.swift': 'Swift',
|
|
120
|
+
'.rb': 'Ruby',
|
|
121
|
+
'.php': 'PHP',
|
|
122
|
+
'.cs': 'C#',
|
|
123
|
+
'.cpp': 'C++',
|
|
124
|
+
'.c': 'C',
|
|
125
|
+
'.h': 'C',
|
|
126
|
+
'.sh': 'Shell'
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const languages = new Set();
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
const ext = path.extname(file).toLowerCase();
|
|
132
|
+
if (languageByExtension[ext]) {
|
|
133
|
+
languages.add(languageByExtension[ext]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return Array.from(languages).sort((left, right) => left.localeCompare(right));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function detectTestFrameworks(projectRoot, files) {
|
|
140
|
+
const frameworks = new Set();
|
|
141
|
+
|
|
142
|
+
const packageJson = parseJsonIfExists(path.join(projectRoot, 'package.json'));
|
|
143
|
+
if (packageJson) {
|
|
144
|
+
const scripts = packageJson.scripts || {};
|
|
145
|
+
if (scripts.test) {
|
|
146
|
+
frameworks.add('node-test-script');
|
|
147
|
+
}
|
|
148
|
+
const deps = {
|
|
149
|
+
...(packageJson.dependencies || {}),
|
|
150
|
+
...(packageJson.devDependencies || {})
|
|
151
|
+
};
|
|
152
|
+
if (deps.jest) frameworks.add('jest');
|
|
153
|
+
if (deps.vitest) frameworks.add('vitest');
|
|
154
|
+
if (deps.mocha) frameworks.add('mocha');
|
|
155
|
+
if (deps.ava) frameworks.add('ava');
|
|
156
|
+
if (deps.tap) frameworks.add('tap');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (files.some((file) => file.endsWith('pyproject.toml')) || files.some((file) => file.endsWith('pytest.ini'))) {
|
|
160
|
+
frameworks.add('pytest');
|
|
161
|
+
}
|
|
162
|
+
if (files.some((file) => /(^|\/)test_.*\.py$/.test(file)) || files.some((file) => /(^|\/)tests\//.test(file))) {
|
|
163
|
+
frameworks.add('python-tests');
|
|
164
|
+
}
|
|
165
|
+
if (files.some((file) => file.endsWith('go.mod'))) {
|
|
166
|
+
frameworks.add('go');
|
|
167
|
+
}
|
|
168
|
+
if (files.some((file) => file.endsWith('_test.go'))) {
|
|
169
|
+
frameworks.add('go-test');
|
|
170
|
+
}
|
|
171
|
+
if (files.some((file) => file.endsWith('Cargo.toml'))) {
|
|
172
|
+
frameworks.add('rust');
|
|
173
|
+
}
|
|
174
|
+
if (files.some((file) => /(^|\/)tests\//.test(file) && file.endsWith('.rs'))) {
|
|
175
|
+
frameworks.add('cargo-test');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (files.some((file) => /(^|\/)(__tests__|tests)\//.test(file) || /\.test\.|\.spec\./.test(file))) {
|
|
179
|
+
frameworks.add('generic-tests');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return Array.from(frameworks).sort((left, right) => left.localeCompare(right));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Detects workspace package roots (one level deep) from package.json workspaces field
|
|
186
|
+
// and physical scan of packages/, apps/, tools/. Supports prefix/* and prefix/** globs.
|
|
187
|
+
function detectWorkspaces(projectRoot, files) {
|
|
188
|
+
const packages = new Set();
|
|
189
|
+
const workspacePrefixes = ['packages/', 'apps/', 'tools/'];
|
|
190
|
+
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
if (!file.endsWith('/package.json')) continue;
|
|
193
|
+
for (const prefix of workspacePrefixes) {
|
|
194
|
+
if (!file.startsWith(prefix)) continue;
|
|
195
|
+
const rest = file.slice(prefix.length);
|
|
196
|
+
const segments = rest.split('/');
|
|
197
|
+
if (segments.length === 2 && segments[1] === 'package.json') {
|
|
198
|
+
packages.add(prefix + segments[0]);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const pkg = parseJsonIfExists(path.join(projectRoot, 'package.json'));
|
|
204
|
+
const globs = Array.isArray(pkg && pkg.workspaces) ? pkg.workspaces
|
|
205
|
+
: (pkg && pkg.workspaces && Array.isArray(pkg.workspaces.packages)) ? pkg.workspaces.packages
|
|
206
|
+
: [];
|
|
207
|
+
for (const glob of globs) {
|
|
208
|
+
const match = glob.match(/^([A-Za-z0-9_.-]+)\/\*{1,2}$/);
|
|
209
|
+
if (!match) continue;
|
|
210
|
+
const prefix = match[1] + '/';
|
|
211
|
+
for (const file of files) {
|
|
212
|
+
if (!file.startsWith(prefix) || !file.endsWith('/package.json')) continue;
|
|
213
|
+
const rest = file.slice(prefix.length);
|
|
214
|
+
const segments = rest.split('/');
|
|
215
|
+
if (segments.length === 2) packages.add(prefix + segments[0]);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return Array.from(packages).sort((a, b) => a.localeCompare(b));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function lineDiff(before, after) {
|
|
223
|
+
const left = (before || '').split(/\r?\n/);
|
|
224
|
+
const right = (after || '').split(/\r?\n/);
|
|
225
|
+
const max = Math.max(left.length, right.length);
|
|
226
|
+
const changes = [];
|
|
227
|
+
|
|
228
|
+
for (let i = 0; i < max; i += 1) {
|
|
229
|
+
const oldLine = left[i] == null ? '' : left[i];
|
|
230
|
+
const newLine = right[i] == null ? '' : right[i];
|
|
231
|
+
if (oldLine !== newLine) {
|
|
232
|
+
changes.push({ index: i + 1, oldLine, newLine });
|
|
233
|
+
}
|
|
234
|
+
if (changes.length >= 20) {
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return changes;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function printDryRunDiff(filePath, before, after) {
|
|
243
|
+
const changes = lineDiff(before, after);
|
|
244
|
+
console.log(`Dry run: ${filePath}`);
|
|
245
|
+
if (changes.length === 0) {
|
|
246
|
+
console.log('- no line changes');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for (const change of changes) {
|
|
251
|
+
console.log(`L${change.index} - ${change.oldLine}`);
|
|
252
|
+
console.log(`L${change.index} + ${change.newLine}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
detectLanguages,
|
|
258
|
+
detectTestFrameworks,
|
|
259
|
+
detectWorkspaces,
|
|
260
|
+
printDryRunDiff,
|
|
261
|
+
readTextIfExists,
|
|
262
|
+
walkFiles,
|
|
263
|
+
writeText
|
|
264
|
+
};
|