@norrix/cli 0.0.24 → 0.0.26
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/CHANGELOG.md +42 -0
- package/dist/cli.js +118 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/amplify-config.js +0 -1
- package/dist/lib/cli-settings.d.ts +15 -0
- package/dist/lib/cli-settings.js +50 -0
- package/dist/lib/cli-settings.js.map +1 -0
- package/dist/lib/commands.d.ts +104 -6
- package/dist/lib/commands.js +1339 -155
- package/dist/lib/commands.js.map +1 -1
- package/dist/lib/config-helpers.spec.js +8 -0
- package/dist/lib/config.d.ts +197 -0
- package/dist/lib/config.js +246 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/defaults.js +0 -1
- package/dist/lib/dev-defaults.js +0 -1
- package/dist/lib/fingerprinting.d.ts +8 -0
- package/dist/lib/fingerprinting.js +4 -2
- package/dist/lib/fingerprinting.js.map +1 -1
- package/dist/lib/norrix-cli.js +0 -1
- package/dist/lib/prod-defaults.js +0 -1
- package/dist/lib/workspace.d.ts +138 -0
- package/dist/lib/workspace.js +826 -0
- package/dist/lib/workspace.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nx Workspace Detection and Integration
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for detecting Nx workspaces, querying project dependencies,
|
|
5
|
+
* and managing workspace-aware builds for monorepo setups.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
/**
|
|
11
|
+
* Detect the workspace context from the current working directory.
|
|
12
|
+
* Walks up the directory tree looking for nx.json to identify Nx workspaces.
|
|
13
|
+
*/
|
|
14
|
+
export function detectWorkspaceContext(appRoot) {
|
|
15
|
+
const currentAppRoot = appRoot || process.cwd();
|
|
16
|
+
// Walk up the directory tree looking for workspace markers
|
|
17
|
+
let dir = currentAppRoot;
|
|
18
|
+
while (dir !== path.dirname(dir)) {
|
|
19
|
+
const nxJsonPath = path.join(dir, 'nx.json');
|
|
20
|
+
if (fs.existsSync(nxJsonPath)) {
|
|
21
|
+
const relativeAppPath = path.relative(dir, currentAppRoot);
|
|
22
|
+
// Only treat as Nx workspace if app is nested (not at root)
|
|
23
|
+
if (relativeAppPath && relativeAppPath !== '.') {
|
|
24
|
+
const projectName = detectProjectName(currentAppRoot);
|
|
25
|
+
const pathMappings = loadPathMappings(dir);
|
|
26
|
+
return {
|
|
27
|
+
type: 'nx',
|
|
28
|
+
workspaceRoot: dir,
|
|
29
|
+
appRoot: currentAppRoot,
|
|
30
|
+
relativeAppPath,
|
|
31
|
+
projectName,
|
|
32
|
+
pathMappings,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
dir = path.dirname(dir);
|
|
37
|
+
}
|
|
38
|
+
// Standalone project (no Nx workspace found or app is at workspace root)
|
|
39
|
+
return {
|
|
40
|
+
type: 'standalone',
|
|
41
|
+
workspaceRoot: currentAppRoot,
|
|
42
|
+
appRoot: currentAppRoot,
|
|
43
|
+
relativeAppPath: '.',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Detect the Nx project name from project.json
|
|
48
|
+
*/
|
|
49
|
+
function detectProjectName(appRoot) {
|
|
50
|
+
const projectJsonPath = path.join(appRoot, 'project.json');
|
|
51
|
+
if (fs.existsSync(projectJsonPath)) {
|
|
52
|
+
try {
|
|
53
|
+
const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf8'));
|
|
54
|
+
return projectJson.name;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Ignore parse errors
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Detect available Nx build configurations from project.json.
|
|
64
|
+
* Reads the 'build' target's configurations to find options like 'prod', 'stg', 'dev'.
|
|
65
|
+
*/
|
|
66
|
+
export function detectNxBuildConfigurations(appRoot) {
|
|
67
|
+
const projectJsonPath = path.join(appRoot, 'project.json');
|
|
68
|
+
if (!fs.existsSync(projectJsonPath)) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf8'));
|
|
73
|
+
const buildTarget = projectJson.targets?.build;
|
|
74
|
+
if (!buildTarget || !buildTarget.configurations) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const configurations = Object.keys(buildTarget.configurations);
|
|
78
|
+
if (configurations.length === 0) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
configurations,
|
|
83
|
+
defaultConfiguration: buildTarget.defaultConfiguration,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Load TypeScript path mappings from tsconfig.base.json
|
|
92
|
+
*/
|
|
93
|
+
function loadPathMappings(workspaceRoot) {
|
|
94
|
+
const tsconfigPath = path.join(workspaceRoot, 'tsconfig.base.json');
|
|
95
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const content = fs.readFileSync(tsconfigPath, 'utf8');
|
|
100
|
+
// Remove comments for JSON parsing
|
|
101
|
+
const jsonContent = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
|
|
102
|
+
const tsconfig = JSON.parse(jsonContent);
|
|
103
|
+
return tsconfig.compilerOptions?.paths;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Query Nx for the project dependency graph.
|
|
111
|
+
* Returns the list of lib paths that the specified project depends on.
|
|
112
|
+
* Also supplements with webpack alias detection for SCSS and other non-TS dependencies.
|
|
113
|
+
*/
|
|
114
|
+
export function getNxProjectDependencies(projectName, workspaceRoot, verbose = false, appRoot) {
|
|
115
|
+
try {
|
|
116
|
+
// Check if nx is available
|
|
117
|
+
execSync('npx nx --version', {
|
|
118
|
+
cwd: workspaceRoot,
|
|
119
|
+
encoding: 'utf8',
|
|
120
|
+
stdio: 'pipe',
|
|
121
|
+
});
|
|
122
|
+
// Get the project graph as JSON
|
|
123
|
+
const graphOutput = execSync('npx nx graph --file=stdout', {
|
|
124
|
+
cwd: workspaceRoot,
|
|
125
|
+
encoding: 'utf8',
|
|
126
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
127
|
+
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large workspaces
|
|
128
|
+
});
|
|
129
|
+
const graph = JSON.parse(graphOutput);
|
|
130
|
+
// Extract all dependencies for the project (recursive)
|
|
131
|
+
const allDeps = collectAllDependencies(projectName, graph);
|
|
132
|
+
if (verbose) {
|
|
133
|
+
console.log(`[workspace] Nx graph found ${allDeps.size} dependencies for ${projectName}`);
|
|
134
|
+
}
|
|
135
|
+
// Convert to paths
|
|
136
|
+
const libPaths = new Set();
|
|
137
|
+
for (const depName of allDeps) {
|
|
138
|
+
const node = graph.graph.nodes[depName];
|
|
139
|
+
if (node && node.data.root) {
|
|
140
|
+
libPaths.add(node.data.root);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Supplement with webpack alias detection (catches SCSS and other non-TS dependencies)
|
|
144
|
+
// The Nx graph may miss webpack-only aliases that aren't in tsconfig
|
|
145
|
+
if (appRoot) {
|
|
146
|
+
const webpackRefs = parseWebpackReferences(appRoot, workspaceRoot, verbose);
|
|
147
|
+
for (const ref of webpackRefs) {
|
|
148
|
+
if (!libPaths.has(ref)) {
|
|
149
|
+
libPaths.add(ref);
|
|
150
|
+
if (verbose) {
|
|
151
|
+
console.log(`[workspace] Added webpack alias dependency: ${ref}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Scan package.json for local file dependencies
|
|
157
|
+
const localFileDeps = scanLocalFileDependencies(workspaceRoot, verbose);
|
|
158
|
+
return {
|
|
159
|
+
libPaths: Array.from(libPaths),
|
|
160
|
+
rootConfigs: [
|
|
161
|
+
'nx.json',
|
|
162
|
+
'tsconfig.base.json',
|
|
163
|
+
'package.json',
|
|
164
|
+
'package-lock.json',
|
|
165
|
+
'.npmrc',
|
|
166
|
+
'references.d.ts', // NativeScript global type definitions
|
|
167
|
+
],
|
|
168
|
+
toolPaths: ['tools/'],
|
|
169
|
+
assetPaths: ['libs/assets/', 'packages/assets/'],
|
|
170
|
+
localFileDeps,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (verbose) {
|
|
175
|
+
console.log(`[workspace] Failed to query Nx graph: ${error?.message}`);
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Recursively collect all dependencies for a project
|
|
182
|
+
*/
|
|
183
|
+
function collectAllDependencies(projectName, graph, visited = new Set()) {
|
|
184
|
+
if (visited.has(projectName)) {
|
|
185
|
+
return visited;
|
|
186
|
+
}
|
|
187
|
+
const deps = graph.graph.dependencies[projectName] || [];
|
|
188
|
+
for (const dep of deps) {
|
|
189
|
+
if (!visited.has(dep.target) && dep.target !== projectName) {
|
|
190
|
+
visited.add(dep.target);
|
|
191
|
+
collectAllDependencies(dep.target, graph, visited);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return visited;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get project info using nx show project command
|
|
198
|
+
*/
|
|
199
|
+
export function getNxProjectInfo(projectName, workspaceRoot, verbose = false) {
|
|
200
|
+
try {
|
|
201
|
+
const output = execSync(`npx nx show project ${projectName} --json`, {
|
|
202
|
+
cwd: workspaceRoot,
|
|
203
|
+
encoding: 'utf8',
|
|
204
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
205
|
+
});
|
|
206
|
+
return JSON.parse(output);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (verbose) {
|
|
210
|
+
console.log(`[workspace] Failed to get project info: ${error?.message}`);
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Fallback: Parse tsconfig.base.json paths and scan source files
|
|
217
|
+
* to determine which libs are actually used.
|
|
218
|
+
*/
|
|
219
|
+
export function getWorkspaceDependenciesFallback(workspaceCtx, verbose = false) {
|
|
220
|
+
const libPaths = new Set();
|
|
221
|
+
// 1. Parse path mappings and find which ones are used
|
|
222
|
+
if (workspaceCtx.pathMappings) {
|
|
223
|
+
const usedAliases = scanForUsedPathAliases(workspaceCtx.appRoot, Object.keys(workspaceCtx.pathMappings), verbose);
|
|
224
|
+
for (const alias of usedAliases) {
|
|
225
|
+
const paths = workspaceCtx.pathMappings[alias];
|
|
226
|
+
if (paths && paths.length > 0) {
|
|
227
|
+
// Convert path like "libs/xplat/core/src/index.ts" to "libs/xplat/core"
|
|
228
|
+
const libPath = extractLibPath(paths[0]);
|
|
229
|
+
if (libPath) {
|
|
230
|
+
libPaths.add(libPath);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// 2. Parse webpack.config.js for additional references (includes SCSS aliases)
|
|
236
|
+
const webpackRefs = parseWebpackReferences(workspaceCtx.appRoot, workspaceCtx.workspaceRoot, verbose);
|
|
237
|
+
for (const ref of webpackRefs) {
|
|
238
|
+
libPaths.add(ref);
|
|
239
|
+
}
|
|
240
|
+
// 3. Parse nativescript.config.ts for hook references
|
|
241
|
+
const hookRefs = parseNativeScriptHooks(workspaceCtx.appRoot, workspaceCtx.workspaceRoot);
|
|
242
|
+
for (const ref of hookRefs) {
|
|
243
|
+
libPaths.add(ref);
|
|
244
|
+
}
|
|
245
|
+
// 4. Parse project.json for implicit dependencies
|
|
246
|
+
const implicitDeps = parseImplicitDependencies(workspaceCtx.appRoot, workspaceCtx.workspaceRoot, verbose);
|
|
247
|
+
for (const dep of implicitDeps) {
|
|
248
|
+
libPaths.add(dep);
|
|
249
|
+
}
|
|
250
|
+
// 5. Recursively scan lib dependencies to find transitive deps
|
|
251
|
+
if (workspaceCtx.pathMappings) {
|
|
252
|
+
const initialLibCount = libPaths.size;
|
|
253
|
+
scanTransitiveLibDependencies(Array.from(libPaths), workspaceCtx.workspaceRoot, workspaceCtx.pathMappings, libPaths, verbose);
|
|
254
|
+
if (verbose && libPaths.size > initialLibCount) {
|
|
255
|
+
console.log(`[workspace] Found ${libPaths.size - initialLibCount} transitive lib dependencies`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (verbose) {
|
|
259
|
+
console.log(`[workspace] Fallback found ${libPaths.size} total lib dependencies`);
|
|
260
|
+
}
|
|
261
|
+
// 6. Scan package.json for local file dependencies
|
|
262
|
+
const allLocalFileDeps = scanLocalFileDependencies(workspaceCtx.workspaceRoot, verbose);
|
|
263
|
+
// Filter out local file deps that are already covered by libPaths to avoid duplicates
|
|
264
|
+
const libPathsArray = Array.from(libPaths);
|
|
265
|
+
const localFileDeps = allLocalFileDeps.filter(localDep => {
|
|
266
|
+
for (const libPath of libPathsArray) {
|
|
267
|
+
// Skip if local dep is inside a lib path or vice versa
|
|
268
|
+
if (localDep.startsWith(libPath + '/') || localDep === libPath ||
|
|
269
|
+
libPath.startsWith(localDep + '/')) {
|
|
270
|
+
if (verbose) {
|
|
271
|
+
console.log(`[workspace] Filtering redundant local dep: ${localDep} (covered by ${libPath})`);
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
});
|
|
278
|
+
return {
|
|
279
|
+
libPaths: libPathsArray,
|
|
280
|
+
rootConfigs: [
|
|
281
|
+
'nx.json',
|
|
282
|
+
'tsconfig.base.json',
|
|
283
|
+
'package.json',
|
|
284
|
+
'package-lock.json',
|
|
285
|
+
'.npmrc',
|
|
286
|
+
'references.d.ts', // NativeScript global type definitions
|
|
287
|
+
],
|
|
288
|
+
toolPaths: ['tools/'],
|
|
289
|
+
assetPaths: ['libs/assets/', 'packages/assets/'],
|
|
290
|
+
localFileDeps,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Parse project.json for implicit dependencies.
|
|
295
|
+
* These are libs explicitly declared as dependencies in the Nx project config.
|
|
296
|
+
*/
|
|
297
|
+
function parseImplicitDependencies(appRoot, workspaceRoot, verbose = false) {
|
|
298
|
+
const deps = [];
|
|
299
|
+
const projectJsonPath = path.join(appRoot, 'project.json');
|
|
300
|
+
if (!fs.existsSync(projectJsonPath)) {
|
|
301
|
+
return deps;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const content = fs.readFileSync(projectJsonPath, 'utf8');
|
|
305
|
+
const projectJson = JSON.parse(content);
|
|
306
|
+
const implicitDeps = projectJson.implicitDependencies;
|
|
307
|
+
if (Array.isArray(implicitDeps)) {
|
|
308
|
+
for (const depName of implicitDeps) {
|
|
309
|
+
// Try to find the lib path by scanning for project.json with this name
|
|
310
|
+
const libPath = findLibPathByProjectName(depName, workspaceRoot);
|
|
311
|
+
if (libPath) {
|
|
312
|
+
deps.push(libPath);
|
|
313
|
+
if (verbose) {
|
|
314
|
+
console.log(`[workspace] Found implicit dependency: ${depName} -> ${libPath}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Ignore parse errors
|
|
322
|
+
}
|
|
323
|
+
return deps;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Find a lib's path by its Nx project name.
|
|
327
|
+
* Scans libs/ and packages/ directories for project.json files with matching name.
|
|
328
|
+
*/
|
|
329
|
+
function findLibPathByProjectName(projectName, workspaceRoot) {
|
|
330
|
+
// Cache of project name -> lib path mappings (built on first call)
|
|
331
|
+
const projectNameCache = buildProjectNameCache(workspaceRoot);
|
|
332
|
+
return projectNameCache.get(projectName);
|
|
333
|
+
}
|
|
334
|
+
// Module-level cache for project name mappings
|
|
335
|
+
let _projectNameCache;
|
|
336
|
+
let _projectNameCacheRoot;
|
|
337
|
+
function buildProjectNameCache(workspaceRoot) {
|
|
338
|
+
// Return cached version if same workspace
|
|
339
|
+
if (_projectNameCache && _projectNameCacheRoot === workspaceRoot) {
|
|
340
|
+
return _projectNameCache;
|
|
341
|
+
}
|
|
342
|
+
const cache = new Map();
|
|
343
|
+
// Scan both libs/ and packages/ directories (common Nx workspace layouts)
|
|
344
|
+
const sharedDirs = ['libs', 'packages'];
|
|
345
|
+
for (const dir of sharedDirs) {
|
|
346
|
+
const sharedPath = path.join(workspaceRoot, dir);
|
|
347
|
+
if (fs.existsSync(sharedPath)) {
|
|
348
|
+
scanForProjectJsonFiles(sharedPath, workspaceRoot, cache);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
_projectNameCache = cache;
|
|
352
|
+
_projectNameCacheRoot = workspaceRoot;
|
|
353
|
+
return cache;
|
|
354
|
+
}
|
|
355
|
+
function scanForProjectJsonFiles(dir, workspaceRoot, cache) {
|
|
356
|
+
try {
|
|
357
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
358
|
+
for (const entry of entries) {
|
|
359
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const fullPath = path.join(dir, entry.name);
|
|
363
|
+
if (entry.isDirectory()) {
|
|
364
|
+
// Check for project.json in this directory
|
|
365
|
+
const projectJsonPath = path.join(fullPath, 'project.json');
|
|
366
|
+
if (fs.existsSync(projectJsonPath)) {
|
|
367
|
+
try {
|
|
368
|
+
const content = fs.readFileSync(projectJsonPath, 'utf8');
|
|
369
|
+
const projectJson = JSON.parse(content);
|
|
370
|
+
if (projectJson.name) {
|
|
371
|
+
const relativePath = path.relative(workspaceRoot, fullPath);
|
|
372
|
+
cache.set(projectJson.name, relativePath);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// Ignore parse errors
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Continue scanning subdirectories
|
|
380
|
+
scanForProjectJsonFiles(fullPath, workspaceRoot, cache);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// Ignore directory read errors
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Recursively scan libs for their dependencies on other libs.
|
|
390
|
+
* This finds transitive dependencies that the app doesn't directly import.
|
|
391
|
+
*/
|
|
392
|
+
function scanTransitiveLibDependencies(initialLibPaths, workspaceRoot, pathMappings, allLibPaths, verbose = false) {
|
|
393
|
+
const toScan = [...initialLibPaths];
|
|
394
|
+
const scanned = new Set();
|
|
395
|
+
while (toScan.length > 0) {
|
|
396
|
+
const libPath = toScan.pop();
|
|
397
|
+
if (scanned.has(libPath)) {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
scanned.add(libPath);
|
|
401
|
+
const absoluteLibPath = path.join(workspaceRoot, libPath);
|
|
402
|
+
if (!fs.existsSync(absoluteLibPath)) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
// Scan this lib's source files for imports of other path aliases
|
|
406
|
+
const usedAliases = scanForUsedPathAliases(absoluteLibPath, Object.keys(pathMappings), false // Don't verbose for recursive scans
|
|
407
|
+
);
|
|
408
|
+
for (const alias of usedAliases) {
|
|
409
|
+
const paths = pathMappings[alias];
|
|
410
|
+
if (paths && paths.length > 0) {
|
|
411
|
+
const depLibPath = extractLibPath(paths[0]);
|
|
412
|
+
if (depLibPath && !allLibPaths.has(depLibPath)) {
|
|
413
|
+
allLibPaths.add(depLibPath);
|
|
414
|
+
toScan.push(depLibPath);
|
|
415
|
+
if (verbose) {
|
|
416
|
+
console.log(`[workspace] Found transitive dependency: ${libPath} -> ${depLibPath}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Also check the lib's project.json for implicit dependencies
|
|
422
|
+
const implicitDeps = parseImplicitDependencies(absoluteLibPath, workspaceRoot, false);
|
|
423
|
+
for (const dep of implicitDeps) {
|
|
424
|
+
if (!allLibPaths.has(dep)) {
|
|
425
|
+
allLibPaths.add(dep);
|
|
426
|
+
toScan.push(dep);
|
|
427
|
+
if (verbose) {
|
|
428
|
+
console.log(`[workspace] Found transitive implicit dependency: ${libPath} -> ${dep}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Scan source files for path alias imports
|
|
436
|
+
*/
|
|
437
|
+
function scanForUsedPathAliases(appRoot, aliases, verbose = false) {
|
|
438
|
+
const usedAliases = new Set();
|
|
439
|
+
// Build regex to match import statements with these aliases
|
|
440
|
+
const aliasPatterns = aliases.map((a) => a.replace(/\*/g, '').replace(/\//g, '\\/'));
|
|
441
|
+
const walk = (dir) => {
|
|
442
|
+
try {
|
|
443
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
444
|
+
for (const entry of entries) {
|
|
445
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const fullPath = path.join(dir, entry.name);
|
|
449
|
+
if (entry.isDirectory()) {
|
|
450
|
+
walk(fullPath);
|
|
451
|
+
}
|
|
452
|
+
else if (entry.isFile() && /\.(ts|js|tsx|jsx|scss|css)$/.test(entry.name)) {
|
|
453
|
+
try {
|
|
454
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
455
|
+
for (const alias of aliases) {
|
|
456
|
+
const baseAlias = alias.replace(/\/\*$/, '');
|
|
457
|
+
// Match imports like: from '@ups/xplat/core' or import('@ups/xplat/features')
|
|
458
|
+
const importRegex = new RegExp(`['"\`]${baseAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[/'"\`]`, 'g');
|
|
459
|
+
if (importRegex.test(content)) {
|
|
460
|
+
usedAliases.add(alias);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// Ignore file read errors
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// Ignore directory read errors
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
walk(appRoot);
|
|
475
|
+
if (verbose) {
|
|
476
|
+
console.log(`[workspace] Found ${usedAliases.size} used path aliases`);
|
|
477
|
+
}
|
|
478
|
+
return usedAliases;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Extract lib directory path from tsconfig path mapping
|
|
482
|
+
*/
|
|
483
|
+
function extractLibPath(mappingPath) {
|
|
484
|
+
// e.g., "libs/xplat/core/src/index.ts" -> "libs/xplat/core"
|
|
485
|
+
// e.g., "packages/shared/utils/src/index.ts" -> "packages/shared/utils"
|
|
486
|
+
// e.g., "libs/xplat/core/src/lib/*" -> "libs/xplat/core"
|
|
487
|
+
const match = mappingPath.match(/^((?:libs|packages)\/[^/]+(?:\/[^/]+)*?)\/src/);
|
|
488
|
+
if (match) {
|
|
489
|
+
return match[1];
|
|
490
|
+
}
|
|
491
|
+
// Try simpler pattern
|
|
492
|
+
const simpleMatch = mappingPath.match(/^((?:libs|packages)\/[^/]+(?:\/[^/]+)*)/);
|
|
493
|
+
if (simpleMatch) {
|
|
494
|
+
return simpleMatch[1];
|
|
495
|
+
}
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Parse webpack.config.js for references to workspace paths.
|
|
500
|
+
* Handles both relative path references and resolve.alias configurations.
|
|
501
|
+
*/
|
|
502
|
+
function parseWebpackReferences(appRoot, workspaceRoot, verbose = false) {
|
|
503
|
+
const refs = [];
|
|
504
|
+
const webpackPath = path.join(appRoot, 'webpack.config.js');
|
|
505
|
+
if (!fs.existsSync(webpackPath)) {
|
|
506
|
+
return refs;
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
const content = fs.readFileSync(webpackPath, 'utf8');
|
|
510
|
+
// Look for patterns like '../../../libs/...' or '../../../packages/...' or 'tools/...'
|
|
511
|
+
const relativePathRegex = /['"`](\.\.\/)+([^'"`]+)['"`]/g;
|
|
512
|
+
let match;
|
|
513
|
+
while ((match = relativePathRegex.exec(content)) !== null) {
|
|
514
|
+
const relativePath = match[0].replace(/['"`]/g, '');
|
|
515
|
+
const absolutePath = path.resolve(appRoot, relativePath);
|
|
516
|
+
const relativeToWorkspace = path.relative(workspaceRoot, absolutePath);
|
|
517
|
+
if (relativeToWorkspace.startsWith('libs/') ||
|
|
518
|
+
relativeToWorkspace.startsWith('packages/') ||
|
|
519
|
+
relativeToWorkspace.startsWith('tools/')) {
|
|
520
|
+
// Extract the lib directory - need to handle deep paths like libs/xplat/nativescript/scss/src
|
|
521
|
+
const libPath = extractLibPathFromRelative(relativeToWorkspace);
|
|
522
|
+
if (libPath) {
|
|
523
|
+
refs.push(libPath);
|
|
524
|
+
if (verbose) {
|
|
525
|
+
console.log(`[webpack] Found lib reference: ${libPath}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Look for CopyRule patterns
|
|
531
|
+
const copyRuleRegex = /from:\s*['"`]([^'"`]+)['"`]/g;
|
|
532
|
+
while ((match = copyRuleRegex.exec(content)) !== null) {
|
|
533
|
+
const fromPath = match[1];
|
|
534
|
+
if (fromPath.includes('libs/') || fromPath.includes('packages/')) {
|
|
535
|
+
const libMatch = fromPath.match(/((?:libs|packages)\/[^/]+(?:\/[^/]+)*)/);
|
|
536
|
+
if (libMatch) {
|
|
537
|
+
const libPath = extractLibPathFromRelative(libMatch[1]);
|
|
538
|
+
if (libPath) {
|
|
539
|
+
refs.push(libPath);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Look for webpack resolve.alias patterns like:
|
|
545
|
+
// config.resolve.alias.set('@ups/xplat-scss', resolve(__dirname, '../../../libs/xplat/scss/src/'))
|
|
546
|
+
// or: alias: { '@ups/xplat-scss': path.resolve(...) }
|
|
547
|
+
const aliasSetRegex = /\.alias\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*(?:resolve|path\.resolve)\s*\([^,]+,\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
548
|
+
while ((match = aliasSetRegex.exec(content)) !== null) {
|
|
549
|
+
const aliasPath = match[2];
|
|
550
|
+
const absolutePath = path.resolve(appRoot, aliasPath);
|
|
551
|
+
const relativeToWorkspace = path.relative(workspaceRoot, absolutePath);
|
|
552
|
+
if (relativeToWorkspace.startsWith('libs/') || relativeToWorkspace.startsWith('packages/')) {
|
|
553
|
+
const libPath = extractLibPathFromRelative(relativeToWorkspace);
|
|
554
|
+
if (libPath) {
|
|
555
|
+
refs.push(libPath);
|
|
556
|
+
if (verbose) {
|
|
557
|
+
console.log(`[webpack] Found alias reference: ${match[1]} -> ${libPath}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// Ignore parse errors
|
|
565
|
+
}
|
|
566
|
+
return [...new Set(refs)];
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Extract the lib directory from a relative path.
|
|
570
|
+
* Handles paths like:
|
|
571
|
+
* - libs/xplat/scss/src -> libs/xplat/scss
|
|
572
|
+
* - packages/shared/utils/src -> packages/shared/utils
|
|
573
|
+
* - libs/xplat/nativescript/scss/src/ -> libs/xplat/nativescript/scss
|
|
574
|
+
* - libs/assets/mobile/fonts -> libs/assets
|
|
575
|
+
*/
|
|
576
|
+
function extractLibPathFromRelative(relativePath) {
|
|
577
|
+
// Remove trailing slash
|
|
578
|
+
const cleanPath = relativePath.replace(/\/+$/, '');
|
|
579
|
+
// Handle /src or /src/... suffix - find the lib root
|
|
580
|
+
const srcMatch = cleanPath.match(/^((?:libs|packages)\/(?:[^/]+\/)*[^/]+)\/src(?:\/|$)/);
|
|
581
|
+
if (srcMatch) {
|
|
582
|
+
return srcMatch[1];
|
|
583
|
+
}
|
|
584
|
+
// For paths without /src, extract based on common patterns
|
|
585
|
+
// libs/assets -> libs/assets
|
|
586
|
+
// libs/xplat/core -> libs/xplat/core
|
|
587
|
+
// packages/shared -> packages/shared
|
|
588
|
+
if (cleanPath.startsWith('libs/') || cleanPath.startsWith('packages/')) {
|
|
589
|
+
const parts = cleanPath.split('/');
|
|
590
|
+
// Check if this path has a project.json to determine lib boundary
|
|
591
|
+
// For now, use heuristics: assets are at libs/assets, others are deeper
|
|
592
|
+
if (parts[1] === 'assets') {
|
|
593
|
+
return `${parts[0]}/${parts[1]}`;
|
|
594
|
+
}
|
|
595
|
+
// For xplat and other nested structures, typically 3 levels: libs/xplat/core
|
|
596
|
+
// But can be deeper: libs/xplat/nativescript/scss
|
|
597
|
+
// Try to find project.json to determine the actual lib root
|
|
598
|
+
let testPath = '';
|
|
599
|
+
for (let i = 0; i < parts.length; i++) {
|
|
600
|
+
testPath += (i > 0 ? '/' : '') + parts[i];
|
|
601
|
+
// We'll return the deepest valid lib path that's not a 'src' dir
|
|
602
|
+
if (parts[i] === 'src') {
|
|
603
|
+
return parts.slice(0, i).join('/');
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// If no /src found, return up to 4 levels or the full path
|
|
607
|
+
return parts.slice(0, Math.min(parts.length, 4)).join('/');
|
|
608
|
+
}
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Parse nativescript.config.ts for hook script references
|
|
613
|
+
*/
|
|
614
|
+
function parseNativeScriptHooks(appRoot, workspaceRoot) {
|
|
615
|
+
const refs = [];
|
|
616
|
+
const configPath = path.join(appRoot, 'nativescript.config.ts');
|
|
617
|
+
if (!fs.existsSync(configPath)) {
|
|
618
|
+
return refs;
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
622
|
+
// Look for hook script paths
|
|
623
|
+
const scriptRegex = /script:\s*['"`](\.\.\/)+([^'"`]+)['"`]/g;
|
|
624
|
+
let match;
|
|
625
|
+
while ((match = scriptRegex.exec(content)) !== null) {
|
|
626
|
+
const scriptPath = match[0].match(/['"`]([^'"`]+)['"`]/)?.[1];
|
|
627
|
+
if (scriptPath) {
|
|
628
|
+
const absolutePath = path.resolve(appRoot, scriptPath);
|
|
629
|
+
const relativeToWorkspace = path.relative(workspaceRoot, absolutePath);
|
|
630
|
+
if (!relativeToWorkspace.startsWith('..')) {
|
|
631
|
+
// Get the directory containing the script
|
|
632
|
+
const scriptDir = path.dirname(relativeToWorkspace);
|
|
633
|
+
refs.push(scriptDir);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
// Ignore parse errors
|
|
640
|
+
}
|
|
641
|
+
return [...new Set(refs)];
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Scan package.json for local file dependencies (file: protocol).
|
|
645
|
+
* Returns an array of relative paths (files or directories) that need to be included in the bundle.
|
|
646
|
+
*/
|
|
647
|
+
function scanLocalFileDependencies(workspaceRoot, verbose = false) {
|
|
648
|
+
const localDeps = new Set();
|
|
649
|
+
const packageJsonPath = path.join(workspaceRoot, 'package.json');
|
|
650
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
651
|
+
return [];
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
655
|
+
// Scan dependencies, devDependencies, and overrides for file: protocol
|
|
656
|
+
const sections = [
|
|
657
|
+
packageJson.dependencies,
|
|
658
|
+
packageJson.devDependencies,
|
|
659
|
+
packageJson.optionalDependencies,
|
|
660
|
+
packageJson.overrides,
|
|
661
|
+
];
|
|
662
|
+
for (const section of sections) {
|
|
663
|
+
if (!section || typeof section !== 'object')
|
|
664
|
+
continue;
|
|
665
|
+
for (const [name, value] of Object.entries(section)) {
|
|
666
|
+
if (typeof value !== 'string')
|
|
667
|
+
continue;
|
|
668
|
+
// Match file: protocol (e.g., "file:libs/xplat/web/plugins/util-0.12.5.tgz")
|
|
669
|
+
if (value.startsWith('file:')) {
|
|
670
|
+
const relativePath = value.slice(5); // Remove 'file:' prefix
|
|
671
|
+
const absolutePath = path.resolve(workspaceRoot, relativePath);
|
|
672
|
+
// Verify the path exists
|
|
673
|
+
if (fs.existsSync(absolutePath)) {
|
|
674
|
+
// If it's a file (like a .tgz), include the directory containing it
|
|
675
|
+
const stat = fs.statSync(absolutePath);
|
|
676
|
+
if (stat.isFile()) {
|
|
677
|
+
// Include the specific file
|
|
678
|
+
localDeps.add(relativePath);
|
|
679
|
+
if (verbose) {
|
|
680
|
+
console.log(`[workspace] Found local file dependency: ${relativePath}`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
else if (stat.isDirectory()) {
|
|
684
|
+
// Include the directory
|
|
685
|
+
localDeps.add(relativePath);
|
|
686
|
+
if (verbose) {
|
|
687
|
+
console.log(`[workspace] Found local directory dependency: ${relativePath}`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
else if (verbose) {
|
|
692
|
+
console.log(`[workspace] Warning: Local dependency not found: ${relativePath}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
if (verbose) {
|
|
700
|
+
console.log(`[workspace] Error scanning package.json for local deps: ${error?.message}`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (verbose && localDeps.size > 0) {
|
|
704
|
+
console.log(`[workspace] Found ${localDeps.size} local file dependencies`);
|
|
705
|
+
}
|
|
706
|
+
return Array.from(localDeps);
|
|
707
|
+
}
|
|
708
|
+
export function createWorkspaceManifest(workspaceCtx, deps) {
|
|
709
|
+
return {
|
|
710
|
+
version: '1.0',
|
|
711
|
+
createdAt: new Date().toISOString(),
|
|
712
|
+
workspaceType: workspaceCtx.type,
|
|
713
|
+
appPath: workspaceCtx.relativeAppPath,
|
|
714
|
+
projectName: workspaceCtx.projectName,
|
|
715
|
+
dependencies: deps,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Log workspace context information
|
|
720
|
+
*/
|
|
721
|
+
export function logWorkspaceContext(ctx, verbose = false) {
|
|
722
|
+
if (ctx.type === 'nx') {
|
|
723
|
+
console.log(`Detected Nx workspace at: ${ctx.workspaceRoot}`);
|
|
724
|
+
console.log(`App path: ${ctx.relativeAppPath}`);
|
|
725
|
+
if (ctx.projectName) {
|
|
726
|
+
console.log(`Project name: ${ctx.projectName}`);
|
|
727
|
+
}
|
|
728
|
+
if (verbose && ctx.pathMappings) {
|
|
729
|
+
console.log(`Path mappings: ${Object.keys(ctx.pathMappings).length} aliases configured`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
console.log('Standalone NativeScript project detected');
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Check if the current directory is at the root of an Nx workspace
|
|
738
|
+
* (has nx.json but no nativescript.config.ts)
|
|
739
|
+
*/
|
|
740
|
+
export function isAtWorkspaceRoot() {
|
|
741
|
+
const cwd = process.cwd();
|
|
742
|
+
const hasNxJson = fs.existsSync(path.join(cwd, 'nx.json'));
|
|
743
|
+
const hasNsConfig = fs.existsSync(path.join(cwd, 'nativescript.config.ts')) ||
|
|
744
|
+
fs.existsSync(path.join(cwd, 'nativescript.config.js'));
|
|
745
|
+
return hasNxJson && !hasNsConfig;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Discover all NativeScript apps within an Nx workspace.
|
|
749
|
+
* Looks for directories containing nativescript.config.ts files.
|
|
750
|
+
*/
|
|
751
|
+
export function discoverNativeScriptApps(workspaceRoot) {
|
|
752
|
+
const root = workspaceRoot || process.cwd();
|
|
753
|
+
const apps = [];
|
|
754
|
+
// Common locations to search for apps in Nx workspaces
|
|
755
|
+
const searchDirs = ['apps', 'packages'];
|
|
756
|
+
for (const searchDir of searchDirs) {
|
|
757
|
+
const searchPath = path.join(root, searchDir);
|
|
758
|
+
if (!fs.existsSync(searchPath))
|
|
759
|
+
continue;
|
|
760
|
+
// Recursively find nativescript.config.ts files
|
|
761
|
+
findNativeScriptApps(searchPath, root, apps);
|
|
762
|
+
}
|
|
763
|
+
return apps;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Recursively search for NativeScript apps
|
|
767
|
+
*/
|
|
768
|
+
function findNativeScriptApps(dir, workspaceRoot, apps) {
|
|
769
|
+
let entries;
|
|
770
|
+
try {
|
|
771
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
// Check if this directory is a NativeScript app
|
|
777
|
+
const hasNsConfig = entries.some((e) => e.isFile() && (e.name === 'nativescript.config.ts' || e.name === 'nativescript.config.js'));
|
|
778
|
+
if (hasNsConfig) {
|
|
779
|
+
const relativePath = path.relative(workspaceRoot, dir);
|
|
780
|
+
const projectName = detectProjectName(dir) || path.basename(dir);
|
|
781
|
+
apps.push({
|
|
782
|
+
name: projectName,
|
|
783
|
+
path: relativePath,
|
|
784
|
+
absolutePath: dir,
|
|
785
|
+
});
|
|
786
|
+
// Don't recurse further into this directory
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
// Recurse into subdirectories
|
|
790
|
+
for (const entry of entries) {
|
|
791
|
+
if (!entry.isDirectory())
|
|
792
|
+
continue;
|
|
793
|
+
// Skip common non-app directories
|
|
794
|
+
if (['node_modules', '.git', 'dist', 'platforms', '.nx', '.cache'].includes(entry.name)) {
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
findNativeScriptApps(path.join(dir, entry.name), workspaceRoot, apps);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Get workspace context for a specific app by project name or path.
|
|
802
|
+
* Used when user specifies --project from workspace root.
|
|
803
|
+
*/
|
|
804
|
+
export function getWorkspaceContextForApp(projectNameOrPath, workspaceRoot) {
|
|
805
|
+
const root = workspaceRoot || process.cwd();
|
|
806
|
+
const apps = discoverNativeScriptApps(root);
|
|
807
|
+
// Try to find by project name first
|
|
808
|
+
let app = apps.find((a) => a.name === projectNameOrPath);
|
|
809
|
+
// If not found, try by path
|
|
810
|
+
if (!app) {
|
|
811
|
+
app = apps.find((a) => a.path === projectNameOrPath || a.path.endsWith(projectNameOrPath));
|
|
812
|
+
}
|
|
813
|
+
if (!app) {
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
const pathMappings = loadPathMappings(root);
|
|
817
|
+
return {
|
|
818
|
+
type: 'nx',
|
|
819
|
+
workspaceRoot: root,
|
|
820
|
+
appRoot: app.absolutePath,
|
|
821
|
+
relativeAppPath: app.path,
|
|
822
|
+
projectName: app.name,
|
|
823
|
+
pathMappings,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
//# sourceMappingURL=workspace.js.map
|