├── .cache └── nxdeps.json ├── .gitattributes ├── .gitignore ├── .nxignore ├── README.md ├── core ├── app-root.js ├── build-project-graph.js ├── default-tasks-runner.js ├── devkit.js ├── file-map-utils.js ├── git-hash.js ├── implicit-project-dependencies.js ├── life-cycle.js ├── npm-packages.js ├── nx-deps-cache.js ├── output.js ├── package.json ├── path.js ├── project-graph-builder.js ├── project-graph.js ├── project.json ├── run-command.js ├── run-one.js ├── static-run-one-terminal-output-life-cycle.js ├── strip-source-code.js ├── target-project-locator.js ├── type.js ├── typescript-import-locator.js ├── typescript.js ├── workspace-projects.js └── workspace.js ├── demo ├── args.html ├── args2.html ├── index.html └── lodash.js ├── hash ├── README.md └── hash.js ├── nx.js ├── nx.json ├── package-lock.json ├── package.json ├── screenshots ├── forkProcessPipeOutputCapture.png └── pipelines.png ├── socket ├── README.md ├── client.js ├── daemon.log ├── package.json ├── server.js └── start.js ├── test ├── README.md ├── child │ ├── index.js │ ├── package.json │ └── project.json ├── cli.js ├── nx.js ├── package.json └── run-script.js ├── tsconfig.base.json └── workspace.json /.cache/nxdeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5.0", 3 | "deps": { 4 | "chalk": "^5.0.0", 5 | "fast-glob": "^3.2.11", 6 | "fs-extra": "^10.0.0", 7 | "ignore": "^5.2.0", 8 | "jsonc-parser": "^3.0.0", 9 | "typescript": "^4.5.5", 10 | "yargs-parser": "^21.0.0" 11 | }, 12 | "pathMappings": { 13 | "@stores/demoapp2": [ 14 | "libs/demoapp2/src/index.ts" 15 | ] 16 | }, 17 | "nxJsonPlugins": [], 18 | "nodes": { 19 | "core": { 20 | "name": "core", 21 | "type": "lib", 22 | "data": { 23 | "root": "core", 24 | "targets": { 25 | "start": { 26 | "executor": "@nrwl/workspace:run-script", 27 | "options": { 28 | "script": "start" 29 | } 30 | }, 31 | "build": { 32 | "executor": "@nrwl/js:tsc", 33 | "outputs": [ 34 | "{options.outputPath}" 35 | ], 36 | "options": { 37 | "outputPath": "dist/apps/demoapp", 38 | "main": "apps/demoapp/src/index.ts", 39 | "tsConfig": "apps/demoapp/tsconfig.app.json", 40 | "assets": [ 41 | "apps/demoapp/*.md" 42 | ] 43 | } 44 | }, 45 | "serve": { 46 | "executor": "@nrwl/js:node", 47 | "options": { 48 | "buildTarget": "simple:test" 49 | } 50 | }, 51 | "make": { 52 | "executor": "@nrwl/workspace:run-commands", 53 | "options": { 54 | "commands": [ 55 | { 56 | "command": "node ./index.js" 57 | } 58 | ] 59 | } 60 | } 61 | }, 62 | "tags": [], 63 | "files": [ 64 | { 65 | "file": "core/app-root.js", 66 | "hash": "1be820df9be13a7179255eb294b74f722823b7f4" 67 | }, 68 | { 69 | "file": "core/build-project-graph.js", 70 | "hash": "eb62d2ec5acce1df107b51f30f651bade96b339e" 71 | }, 72 | { 73 | "file": "core/default-tasks-runner.js", 74 | "hash": "998ef3aa23f7febb2c2646710d81dcfeaecb5c76" 75 | }, 76 | { 77 | "file": "core/devkit.js", 78 | "hash": "ae007cb6bd1a576f7be636421fa94bd9a3b10bee", 79 | "deps": [ 80 | "npm:jsonc-parser" 81 | ] 82 | }, 83 | { 84 | "file": "core/file-map-utils.js", 85 | "hash": "1b818887244da60821731491b2032f1a15bb1df3" 86 | }, 87 | { 88 | "file": "core/git-hash.js", 89 | "hash": "482ab0d9fcee3b2aee1368dbd7ad48985b55cef3", 90 | "deps": [ 91 | "npm:fs-extra", 92 | "npm:ignore" 93 | ] 94 | }, 95 | { 96 | "file": "core/implicit-project-dependencies.js", 97 | "hash": "8870f764818de8c7876b150fad88060fe42166fd" 98 | }, 99 | { 100 | "file": "core/life-cycle.js", 101 | "hash": "9240bcff4ba7d7b128b2779d33e452774ae86eb9" 102 | }, 103 | { 104 | "file": "core/npm-packages.js", 105 | "hash": "824f31fb0a8f09cf8799e56a59d8203a0875621b" 106 | }, 107 | { 108 | "file": "core/nx-deps-cache.js", 109 | "hash": "9c8dd0fb92d8aa82fa506653a362a401fb09989a" 110 | }, 111 | { 112 | "file": "core/output.js", 113 | "hash": "2b76962aa3ba7dbcbc740d0138a4318372f1ebe6", 114 | "deps": [ 115 | "npm:chalk" 116 | ] 117 | }, 118 | { 119 | "file": "core/package.json", 120 | "hash": "07975e4f25e084cb1873a85f932409555ff0978f" 121 | }, 122 | { 123 | "file": "core/path.js", 124 | "hash": "cd684ee874efb21223f60e3472c29c515cb3764a" 125 | }, 126 | { 127 | "file": "core/project-graph-builder.js", 128 | "hash": "47a3bbaa03a0c7fc9cc3bed8332683ca2802888b" 129 | }, 130 | { 131 | "file": "core/project-graph.js", 132 | "hash": "b60550af287192cb6b711e869de03e643395b308" 133 | }, 134 | { 135 | "file": "core/project.json", 136 | "hash": "1922a42ef51e68e8d4e64dee09e05607866ade38" 137 | }, 138 | { 139 | "file": "core/run-command.js", 140 | "hash": "40e225c97aa0ab3440b3f2f5357fcfdb5a57cc38" 141 | }, 142 | { 143 | "file": "core/run-one.js", 144 | "hash": "7ed7a4dfeee5a2513d2eb5fb981b2ec003e01558" 145 | }, 146 | { 147 | "file": "core/static-run-one-terminal-output-life-cycle.js", 148 | "hash": "909c770aa0fd22cab87b042a7b32d08d84ca3708" 149 | }, 150 | { 151 | "file": "core/strip-source-code.js", 152 | "hash": "69d10edf8801a03f0ae84bb275694fd4e57a1b4e", 153 | "deps": [ 154 | "npm:typescript" 155 | ] 156 | }, 157 | { 158 | "file": "core/target-project-locator.js", 159 | "hash": "891bef6a0e695b6526166a926023c2997d4bf210" 160 | }, 161 | { 162 | "file": "core/type.js", 163 | "hash": "cc5beafb4f85afc0ef43e318fe129c83b56bbd59" 164 | }, 165 | { 166 | "file": "core/typescript-import-locator.js", 167 | "hash": "5ab83d3d05c716cd32ce98619bf4daab82f301e5", 168 | "deps": [ 169 | "npm:typescript" 170 | ] 171 | }, 172 | { 173 | "file": "core/typescript.js", 174 | "hash": "538ff7139a4f23d9204268dc9bfbfe28b505e325", 175 | "deps": [ 176 | "npm:typescript" 177 | ] 178 | }, 179 | { 180 | "file": "core/workspace-projects.js", 181 | "hash": "6af9de87be7524bb367d0e645a70b39a29905743", 182 | "deps": [ 183 | "npm:fs-extra", 184 | "npm:fast-glob" 185 | ] 186 | }, 187 | { 188 | "file": "core/workspace.js", 189 | "hash": "57985926e32391f7a26afa7a7cad8e26df5ec081" 190 | } 191 | ] 192 | } 193 | } 194 | }, 195 | "externalNodes": { 196 | "npm:chalk": { 197 | "type": "npm", 198 | "name": "npm:chalk", 199 | "data": { 200 | "version": "^5.0.0", 201 | "packageName": "chalk" 202 | } 203 | }, 204 | "npm:fast-glob": { 205 | "type": "npm", 206 | "name": "npm:fast-glob", 207 | "data": { 208 | "version": "^3.2.11", 209 | "packageName": "fast-glob" 210 | } 211 | }, 212 | "npm:fs-extra": { 213 | "type": "npm", 214 | "name": "npm:fs-extra", 215 | "data": { 216 | "version": "^10.0.0", 217 | "packageName": "fs-extra" 218 | } 219 | }, 220 | "npm:ignore": { 221 | "type": "npm", 222 | "name": "npm:ignore", 223 | "data": { 224 | "version": "^5.2.0", 225 | "packageName": "ignore" 226 | } 227 | }, 228 | "npm:jsonc-parser": { 229 | "type": "npm", 230 | "name": "npm:jsonc-parser", 231 | "data": { 232 | "version": "^3.0.0", 233 | "packageName": "jsonc-parser" 234 | } 235 | }, 236 | "npm:typescript": { 237 | "type": "npm", 238 | "name": "npm:typescript", 239 | "data": { 240 | "version": "^4.5.5", 241 | "packageName": "typescript" 242 | } 243 | }, 244 | "npm:yargs-parser": { 245 | "type": "npm", 246 | "name": "npm:yargs-parser", 247 | "data": { 248 | "version": "^21.0.0", 249 | "packageName": "yargs-parser" 250 | } 251 | } 252 | }, 253 | "dependencies": { 254 | "core": [ 255 | { 256 | "source": "core", 257 | "target": "npm:jsonc-parser", 258 | "type": "static" 259 | }, 260 | { 261 | "source": "core", 262 | "target": "npm:fs-extra", 263 | "type": "static" 264 | }, 265 | { 266 | "source": "core", 267 | "target": "npm:ignore", 268 | "type": "static" 269 | }, 270 | { 271 | "source": "core", 272 | "target": "npm:chalk", 273 | "type": "static" 274 | }, 275 | { 276 | "source": "core", 277 | "target": "npm:typescript", 278 | "type": "static" 279 | }, 280 | { 281 | "source": "core", 282 | "target": "npm:fast-glob", 283 | "type": "static" 284 | } 285 | ] 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.nxignore: -------------------------------------------------------------------------------- 1 | workspace.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nx架构图 2 | 3 | https://app.diagrams.net/#G1kgAmZj_n_qzBESU8OR43jjQ7ls1ORkbW 4 | 5 | # 源码解读 6 | https://github.com/Wscats/nx/issues/1 7 | 8 | # 源码实现 9 | https://github.com/Wscats/monorepo-tutorial/tree/main/test 10 | -------------------------------------------------------------------------------- /core/app-root.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | function pathInner(dir) { 4 | if (process.env.NX_WORKSPACE_ROOT_PATH) 5 | return process.env.NX_WORKSPACE_ROOT_PATH; 6 | if (path.dirname(dir) === dir) 7 | return process.cwd(); 8 | if (fileExists(path.join(dir, 'workspace.json')) || 9 | fileExists(path.join(dir, 'nx.json')) || 10 | fileExists(path.join(dir, 'angular.json'))) { 11 | return dir; 12 | } 13 | else { 14 | return pathInner(path.dirname(dir)); 15 | } 16 | } 17 | function fileExists(filePath) { 18 | try { 19 | return (0, fs.statSync)(filePath).isFile(); 20 | } 21 | catch (err) { 22 | return false; 23 | } 24 | } 25 | 26 | module.exports = { 27 | appRootPath: pathInner(__dirname), 28 | fileExists: fileExists 29 | } 30 | -------------------------------------------------------------------------------- /core/build-project-graph.js: -------------------------------------------------------------------------------- 1 | const { TypeScriptImportLocator } = require('./typescript-import-locator'); 2 | const { TargetProjectLocator } = require('./target-project-locator'); 3 | const { joinPathFragments } = require('./path') 4 | const { parseJson } = require("./devkit"); 5 | 6 | function buildExplicitTypeScriptDependencies( 7 | workspace, 8 | graph, 9 | filesToProcess 10 | ) { 11 | const importLocator = new TypeScriptImportLocator(); 12 | const targetProjectLocator = new TargetProjectLocator( 13 | graph.nodes, 14 | graph.externalNodes 15 | ); 16 | const res = []; 17 | Object.keys(filesToProcess).forEach((source) => { 18 | Object.values(filesToProcess[source]).forEach((f) => { 19 | importLocator.fromFile( 20 | f.file, 21 | (importExpr, filePath, type) => { 22 | const target = targetProjectLocator.findProjectWithImport( 23 | importExpr, 24 | f.file, 25 | workspace.npmScope 26 | ); 27 | if (target) { 28 | res.push({ 29 | sourceProjectName: source, 30 | targetProjectName: target, 31 | sourceProjectFile: f.file, 32 | }); 33 | } 34 | } 35 | ); 36 | }); 37 | }); 38 | return res; 39 | } 40 | 41 | function processPackageJson( 42 | sourceProject, 43 | fileName, 44 | graph, 45 | collectedDeps, 46 | packageNameMap 47 | ) { 48 | try { 49 | const deps = readDeps(parseJson(defaultFileRead(fileName))); 50 | // the name matches the import path 51 | deps.forEach((d) => { 52 | // package.json refers to another project in the monorepo 53 | if (packageNameMap[d]) { 54 | collectedDeps.push({ 55 | sourceProjectName: sourceProject, 56 | targetProjectName: packageNameMap[d], 57 | sourceProjectFile: fileName, 58 | }); 59 | } else if (graph.externalNodes[`npm:${d}`]) { 60 | collectedDeps.push({ 61 | sourceProjectName: sourceProject, 62 | targetProjectName: `npm:${d}`, 63 | sourceProjectFile: fileName, 64 | }); 65 | } 66 | }); 67 | } catch (e) { 68 | if (process.env.NX_VERBOSE_LOGGING === 'true') { 69 | console.log(e); 70 | } 71 | } 72 | } 73 | 74 | function readDeps(packageJsonDeps) { 75 | // ?? 76 | return [ 77 | ...Object.keys(packageJsonDeps && packageJsonDeps.dependencies || {}), 78 | ...Object.keys(packageJsonDeps && packageJsonDeps.devDependencies || {}), 79 | ...Object.keys(packageJsonDeps && packageJsonDeps.peerDependencies || {}), 80 | ]; 81 | } 82 | 83 | function buildExplicitPackageJsonDependencies( 84 | workspace, 85 | graph, 86 | filesToProcess 87 | ) { 88 | const res = []; 89 | let packageNameMap = undefined; 90 | Object.keys(filesToProcess).forEach((source) => { 91 | Object.values(filesToProcess[source]).forEach((f) => { 92 | if (isPackageJsonAtProjectRoot(graph.nodes, f.file)) { 93 | // we only create the package name map once and only if a package.json file changes 94 | packageNameMap = packageNameMap || createPackageNameMap(workspace); 95 | processPackageJson(source, f.file, graph, res, packageNameMap); 96 | } 97 | }); 98 | }); 99 | return res; 100 | } 101 | 102 | function defaultFileRead(filePath) { 103 | return fs.readFileSync(path.join(appRootPath, filePath), 'utf-8'); 104 | } 105 | 106 | function createPackageNameMap(w) { 107 | const res = {}; 108 | for (let projectName of Object.keys(w.projects)) { 109 | try { 110 | const packageJson = parseJson( 111 | defaultFileRead(path.join(w.projects[projectName].root, 'package.json')) 112 | ); 113 | res[packageJson.name || `@${w.npmScope}/${projectName}`] = projectName; 114 | } catch (e) { } 115 | } 116 | return res; 117 | } 118 | 119 | function isPackageJsonAtProjectRoot( 120 | nodes, 121 | fileName 122 | ) { 123 | return Object.values(nodes).find( 124 | (projectNode) => 125 | (projectNode.type === 'lib' || projectNode.type === 'app') && 126 | joinPathFragments(projectNode.data.root, 'package.json') === fileName 127 | ); 128 | } 129 | 130 | function buildExplicitTypescriptAndPackageJsonDependencies( 131 | jsPluginConfig, 132 | workspace, 133 | projectGraph, 134 | filesToProcess 135 | ) { 136 | let res = []; 137 | if ( 138 | jsPluginConfig.analyzeSourceFiles === undefined || 139 | jsPluginConfig.analyzeSourceFiles === true 140 | ) { 141 | res = res.concat( 142 | buildExplicitTypeScriptDependencies( 143 | workspace, 144 | projectGraph, 145 | filesToProcess 146 | ) 147 | ); 148 | } 149 | if ( 150 | jsPluginConfig.analyzePackageJson === undefined || 151 | jsPluginConfig.analyzePackageJson === true 152 | ) { 153 | res = res.concat( 154 | buildExplicitPackageJsonDependencies( 155 | workspace, 156 | projectGraph, 157 | filesToProcess 158 | ) 159 | ); 160 | } 161 | return res; 162 | } 163 | 164 | function jsPluginConfig(nxJson) { 165 | if ( 166 | nxJson && 167 | nxJson && 168 | nxJson.pluginsConfig && 169 | nxJson.pluginsConfig['@nrwl/js'] 170 | ) { 171 | return nxJson && nxJson.pluginsConfig['@nrwl/js']; 172 | } else { 173 | return {}; 174 | } 175 | } 176 | 177 | function buildExplicitDependencies( 178 | jsPluginConfig, 179 | ctx, 180 | builder 181 | ) { 182 | let totalNumOfFilesToProcess = totalNumberOfFilesToProcess(ctx); 183 | // using workers has an overhead, so we only do it when the number of 184 | // files we need to process is >= 100 and there are more than 2 CPUs 185 | // to be able to use at least 2 workers (1 worker per CPU and 186 | // 1 CPU for the main thread) 187 | if (totalNumOfFilesToProcess < 100 || getNumberOfWorkers() <= 2) { 188 | return buildExplicitDependenciesWithoutWorkers( 189 | jsPluginConfig, 190 | ctx, 191 | builder 192 | ); 193 | } else { 194 | // return buildExplicitDependenciesUsingWorkers( 195 | // jsPluginConfig, 196 | // ctx, 197 | // totalNumOfFilesToProcess, 198 | // builder 199 | // ); 200 | } 201 | } 202 | 203 | function buildExplicitDependenciesWithoutWorkers( 204 | jsPluginConfig, 205 | ctx, 206 | builder 207 | ) { 208 | buildExplicitTypescriptAndPackageJsonDependencies( 209 | jsPluginConfig, 210 | ctx.workspace, 211 | builder.graph, 212 | ctx.filesToProcess 213 | ).forEach((r) => { 214 | builder.addExplicitDependency( 215 | r.sourceProjectName, 216 | r.sourceProjectFile, 217 | r.targetProjectName 218 | ); 219 | }); 220 | } 221 | 222 | function getNumberOfWorkers() { 223 | return process.env.NX_PROJECT_GRAPH_MAX_WORKERS 224 | ? +process.env.NX_PROJECT_GRAPH_MAX_WORKERS 225 | : os.cpus().length - 1; 226 | } 227 | 228 | function totalNumberOfFilesToProcess(ctx) { 229 | let totalNumOfFilesToProcess = 0; 230 | Object.values(ctx.filesToProcess).forEach( 231 | (t) => (totalNumOfFilesToProcess += t.length) 232 | ); 233 | return totalNumOfFilesToProcess; 234 | } 235 | 236 | module.exports = { buildExplicitDependencies, jsPluginConfig } -------------------------------------------------------------------------------- /core/default-tasks-runner.js: -------------------------------------------------------------------------------- 1 | // const task_orchestrator_1 = require("./task-orchestrator"); 2 | // const task_graph_creator_1 = require("./task-graph-creator"); 3 | // const hasher_1 = require("../core/hasher/hasher"); 4 | const defaultTasksRunner = async (tasks, options, context) => { 5 | if (options['parallel'] === 'false' || 6 | options['parallel'] === false) { 7 | options['parallel'] = 1; 8 | } 9 | else if (options['parallel'] === 'true' || 10 | options['parallel'] === true) { 11 | options['parallel'] = Number(options['maxParallel'] || 3); 12 | } 13 | else if (options.parallel === undefined) { 14 | options.parallel = Number(options['maxParallel'] || 3); 15 | } 16 | options.lifeCycle.startCommand(); 17 | try { 18 | // return await runAllTasks(tasks, options, context); 19 | } 20 | catch (e) { 21 | console.error('Unexpected error:'); 22 | console.error(e); 23 | process.exit(1); 24 | } 25 | finally { 26 | options.lifeCycle.endCommand(); 27 | } 28 | }; 29 | async function runAllTasks(tasks, options, context) { 30 | var _a; 31 | return () => { 32 | // const defaultTargetDependencies = (_a = context.nxJson.targetDependencies) !== null && _a !== void 0 ? _a : {}; 33 | // const taskGraphCreator = new task_graph_creator_1.TaskGraphCreator(context.projectGraph, defaultTargetDependencies); 34 | // const taskGraph = taskGraphCreator.createTaskGraph(tasks); 35 | // perf_hooks_1.performance.mark('task-graph-created'); 36 | // perf_hooks_1.performance.measure('nx-prep-work', 'init-local', 'task-graph-created'); 37 | // perf_hooks_1.performance.measure('graph-creation', 'command-execution-begins', 'task-graph-created'); 38 | // const hasher = new hasher_1.Hasher(context.projectGraph, context.nxJson, options); 39 | // const orchestrator = new task_orchestrator_1.TaskOrchestrator(hasher, context.initiatingProject, context.projectGraph, taskGraph, options); 40 | // return orchestrator.run(); 41 | }; 42 | } 43 | 44 | module.exports = defaultTasksRunner; -------------------------------------------------------------------------------- /core/devkit.js: -------------------------------------------------------------------------------- 1 | const jsonc_parser = require("jsonc-parser"); 2 | /** 3 | * Parses the given JSON string and returns the object the JSON content represents. 4 | * By default javascript-style comments are allowed. 5 | * 6 | * @param input JSON content as string 7 | * @param options JSON parse options 8 | * @returns Object the JSON content represents 9 | */ 10 | function parseJson(input, options) { 11 | try { 12 | if ((options === null || options === void 0 ? void 0 : options.disallowComments) === true || 13 | (options === null || options === void 0 ? void 0 : options.expectComments) !== true) { 14 | return JSON.parse(input); 15 | } 16 | } 17 | catch (error) { 18 | if ((options === null || options === void 0 ? void 0 : options.disallowComments) === true) { 19 | throw error; 20 | } 21 | } 22 | const errors = []; 23 | const result = jsonc_parser.parse(input, errors); 24 | if (errors.length > 0) { 25 | const { error, offset } = errors[0]; 26 | throw new Error(`${jsonc_parser.printParseErrorCode(error)} in JSON at position ${offset}`); 27 | } 28 | return result; 29 | } 30 | 31 | module.exports = { parseJson } -------------------------------------------------------------------------------- /core/file-map-utils.js: -------------------------------------------------------------------------------- 1 | const { dirname } = require("path"); 2 | 3 | function createProjectRootMappings( 4 | workspaceJson, 5 | projectFileMap 6 | ) { 7 | const projectRootMappings = new Map(); 8 | for (const projectName of Object.keys(workspaceJson.projects)) { 9 | if (!projectFileMap[projectName]) { 10 | projectFileMap[projectName] = []; 11 | } 12 | const root = workspaceJson.projects[projectName].root; 13 | projectRootMappings.set( 14 | root.endsWith('/') ? root.substring(0, root.length - 1) : root, 15 | projectFileMap[projectName] 16 | ); 17 | } 18 | return projectRootMappings; 19 | } 20 | 21 | function findMatchingProjectFiles( 22 | projectRootMappings, 23 | file 24 | ) { 25 | for ( 26 | let currentPath = dirname(file); 27 | currentPath != dirname(currentPath); 28 | currentPath = dirname(currentPath) 29 | ) { 30 | const p = projectRootMappings.get(currentPath); 31 | if (p) { 32 | return p; 33 | } 34 | } 35 | return null; 36 | } 37 | 38 | function createProjectFileMap( 39 | workspaceJson, 40 | allWorkspaceFiles 41 | ) { 42 | const projectFileMap = {}; 43 | const projectRootMappings = createProjectRootMappings( 44 | workspaceJson, 45 | projectFileMap 46 | ); 47 | for (const f of allWorkspaceFiles) { 48 | const matchingProjectFiles = findMatchingProjectFiles( 49 | projectRootMappings, 50 | f.file 51 | ); 52 | if (matchingProjectFiles) { 53 | matchingProjectFiles.push(f); 54 | } 55 | } 56 | return { projectFileMap, allWorkspaceFiles }; 57 | } 58 | 59 | function updateProjectFileMap( 60 | workspaceJson, 61 | projectFileMap, 62 | allWorkspaceFiles, 63 | updatedFiles, 64 | deletedFiles 65 | ) { 66 | const projectRootMappings = createProjectRootMappings( 67 | workspaceJson, 68 | projectFileMap 69 | ); 70 | 71 | for (const f of updatedFiles.keys()) { 72 | const matchingProjectFiles = findMatchingProjectFiles( 73 | projectRootMappings, 74 | f 75 | ); 76 | if (matchingProjectFiles) { 77 | const fileData = matchingProjectFiles.find((t) => t.file === f); 78 | if (fileData) { 79 | fileData.hash = updatedFiles.get(f); 80 | } else { 81 | matchingProjectFiles.push({ 82 | file: f, 83 | hash: updatedFiles.get(f), 84 | }); 85 | } 86 | } 87 | 88 | const fileData = allWorkspaceFiles.find((t) => t.file === f); 89 | if (fileData) { 90 | fileData.hash = updatedFiles.get(f); 91 | } else { 92 | allWorkspaceFiles.push({ 93 | file: f, 94 | hash: updatedFiles.get(f), 95 | }); 96 | } 97 | } 98 | 99 | for (const f of deletedFiles) { 100 | const matchingProjectFiles = findMatchingProjectFiles( 101 | projectRootMappings, 102 | f 103 | ); 104 | if (matchingProjectFiles) { 105 | const index = matchingProjectFiles.findIndex((t) => t.file === f); 106 | if (index > -1) { 107 | matchingProjectFiles.splice(index, 1); 108 | } 109 | } 110 | const index = allWorkspaceFiles.findIndex((t) => t.file === f); 111 | if (index > -1) { 112 | allWorkspaceFiles.splice(index, 1); 113 | } 114 | } 115 | return { projectFileMap, allWorkspaceFiles }; 116 | } 117 | 118 | module.exports = { createProjectFileMap, updateProjectFileMap } -------------------------------------------------------------------------------- /core/git-hash.js: -------------------------------------------------------------------------------- 1 | const { statSync, existsSync, readFileSync } = require('fs-extra'); 2 | const { spawn } = require('child_process'); 3 | const path = require("path"); 4 | const ignore = require("ignore"); 5 | function removeWindowsDriveLetter(osSpecificPath) { 6 | return osSpecificPath.replace(/^[A-Z]:/, ''); 7 | } 8 | 9 | /** 10 | * Coverts an os specific path to a unix style path 11 | */ 12 | function normalizePath(osSpecificPath) { 13 | return removeWindowsDriveLetter(osSpecificPath).split('\\').join('/'); 14 | } 15 | 16 | /** 17 | * Normalized path fragments and joins them 18 | */ 19 | function joinPathFragments(...fragments) { 20 | return normalizePath(path.join(...fragments)); 21 | } 22 | 23 | function fileExists(filePath) { 24 | try { 25 | return statSync(filePath).isFile(); 26 | } catch (err) { 27 | return false; 28 | } 29 | } 30 | 31 | async function getGitHashForFiles( 32 | potentialFilesToHash, 33 | path 34 | ) { 35 | const filesToHash = []; 36 | const deleted = []; 37 | for (let f of potentialFilesToHash) { 38 | if (fileExists(joinPathFragments(path, f))) { 39 | filesToHash.push(f); 40 | } else { 41 | deleted.push(f); 42 | } 43 | } 44 | 45 | const res = new Map(); 46 | if (filesToHash.length) { 47 | const hashStdout = await spawnProcess( 48 | 'git', 49 | ['hash-object', ...filesToHash], 50 | path 51 | ); 52 | const hashes = hashStdout.split('\n').filter((s) => !!s); 53 | if (hashes.length !== filesToHash.length) { 54 | throw new Error( 55 | `Passed ${filesToHash.length} file paths to Git to hash, but received ${hashes.length} hashes.` 56 | ); 57 | } 58 | for (let i = 0; i < hashes.length; i++) { 59 | const hash = hashes[i]; 60 | const filePath = filesToHash[i]; 61 | res.set(filePath, hash); 62 | } 63 | } 64 | return { hashes: res, deleted }; 65 | } 66 | 67 | async function spawnProcess(command, args, cwd) { 68 | const cp = spawn(command, args, { 69 | windowsHide: true, 70 | shell: false, 71 | cwd, 72 | }); 73 | let s = ''; 74 | for await (const data of cp.stdout) { 75 | s += data; 76 | } 77 | return s; 78 | } 79 | 80 | async function getStagedFiles(path) { 81 | const staged = await spawnProcess( 82 | 'git', 83 | ['ls-files', '-s', '-z', '--exclude-standard', '.'], 84 | path 85 | ); 86 | const res = new Map(); 87 | for (let line of staged.split('\0')) { 88 | if (!line) continue; 89 | const [_, hash, __, ...fileParts] = line.split(/\s/); 90 | const fileName = fileParts.join(' '); 91 | res.set(fileName, hash); 92 | } 93 | return res; 94 | } 95 | 96 | async function getUnstagedFiles(path) { 97 | const unstaged = await spawnProcess( 98 | 'git', 99 | ['ls-files', '-m', '-z', '--exclude-standard', '.'], 100 | path 101 | ); 102 | const lines = unstaged.split('\0').filter((f) => !!f); 103 | return getGitHashForFiles(lines, path); 104 | } 105 | 106 | async function getUntrackedFiles(path) { 107 | const untracked = await spawnProcess( 108 | 'git', 109 | ['ls-files', '--other', '-z', '--exclude-standard', '.'], 110 | path 111 | ); 112 | const lines = untracked.split('\0').filter((f) => !!f); 113 | return getGitHashForFiles(lines, path); 114 | } 115 | 116 | async function getFileHashes(path) { 117 | const [staged, unstaged, untracked] = await Promise.all([ 118 | getStagedFiles(path), 119 | getUnstagedFiles(path), 120 | getUntrackedFiles(path), 121 | ]); 122 | 123 | unstaged.hashes.forEach((hash, filename) => { 124 | staged.set(filename, hash); 125 | }); 126 | 127 | unstaged.deleted.forEach((filename) => { 128 | staged.delete(filename); 129 | }); 130 | 131 | untracked.hashes.forEach((hash, filename) => { 132 | staged.set(filename, hash); 133 | }); 134 | 135 | return { allFiles: staged }; 136 | } 137 | 138 | function getIgnoredGlobs(appRootPath) { 139 | if (existsSync(`${appRootPath}/.nxignore`)) { 140 | const ig = ignore(); 141 | ig.add(readFileSync(`${appRootPath}/.nxignore`, 'utf-8')); 142 | return ig; 143 | } else { 144 | return { ignores: (file) => false }; 145 | } 146 | } 147 | 148 | module.exports = { getFileHashes, getIgnoredGlobs } -------------------------------------------------------------------------------- /core/implicit-project-dependencies.js: -------------------------------------------------------------------------------- 1 | function buildImplicitProjectDependencies(ctx, builder) { 2 | Object.keys(ctx.workspace.projects).forEach((source) => { 3 | const p = ctx.workspace.projects[source]; 4 | if (p.implicitDependencies && p.implicitDependencies.length > 0) { 5 | p.implicitDependencies.forEach((target) => { 6 | if (target.startsWith('!')) { 7 | builder.removeDependency(source, target.substr(1)); 8 | } 9 | else { 10 | builder.addImplicitDependency(source, target); 11 | } 12 | }); 13 | } 14 | }); 15 | } 16 | 17 | module.exports = { buildImplicitProjectDependencies } -------------------------------------------------------------------------------- /core/life-cycle.js: -------------------------------------------------------------------------------- 1 | class CompositeLifeCycle { 2 | constructor(lifeCycles) { 3 | this.lifeCycles = lifeCycles; 4 | } 5 | startCommand() { 6 | for (let l of this.lifeCycles) { 7 | if (l.startCommand) { 8 | l.startCommand(); 9 | } 10 | } 11 | } 12 | endCommand() { 13 | for (let l of this.lifeCycles) { 14 | if (l.endCommand) { 15 | l.endCommand(); 16 | } 17 | } 18 | } 19 | scheduleTask(task) { 20 | for (let l of this.lifeCycles) { 21 | if (l.scheduleTask) { 22 | l.scheduleTask(task); 23 | } 24 | } 25 | } 26 | startTask(task) { 27 | for (let l of this.lifeCycles) { 28 | if (l.startTask) { 29 | l.startTask(task); 30 | } 31 | } 32 | } 33 | endTask(task, code) { 34 | for (let l of this.lifeCycles) { 35 | if (l.endTask) { 36 | l.endTask(task, code); 37 | } 38 | } 39 | } 40 | startTasks(tasks, metadata) { 41 | for (let l of this.lifeCycles) { 42 | if (l.startTasks) { 43 | l.startTasks(tasks, metadata); 44 | } 45 | else if (l.startTask) { 46 | tasks.forEach((t) => l.startTask(t)); 47 | } 48 | } 49 | } 50 | endTasks(taskResults, metadata) { 51 | for (let l of this.lifeCycles) { 52 | if (l.endTasks) { 53 | l.endTasks(taskResults, metadata); 54 | } 55 | else if (l.endTask) { 56 | taskResults.forEach((t) => l.endTask(t.task, t.code)); 57 | } 58 | } 59 | } 60 | printTaskTerminalOutput(task, status, output) { 61 | for (let l of this.lifeCycles) { 62 | if (l.printTaskTerminalOutput) { 63 | l.printTaskTerminalOutput(task, status, output); 64 | } 65 | } 66 | } 67 | } 68 | 69 | module.exports = { CompositeLifeCycle } -------------------------------------------------------------------------------- /core/npm-packages.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | function readJsonFile(path) { 4 | const content = fs.readFileSync(path, 'utf-8'); 5 | return JSON.parse(content); 6 | } 7 | 8 | function buildNpmPackageNodes(builder, appRootPath) { 9 | const packageJson = readJsonFile(path.join(appRootPath, 'package.json')); 10 | const deps = { 11 | ...packageJson.dependencies, 12 | ...packageJson.devDependencies, 13 | }; 14 | Object.keys(deps).forEach((d) => { 15 | builder.addExternalNode({ 16 | type: 'npm', 17 | name: `npm:${d}`, 18 | data: { 19 | version: deps[d], 20 | packageName: d, 21 | }, 22 | }); 23 | }); 24 | } 25 | 26 | module.exports = { buildNpmPackageNodes } -------------------------------------------------------------------------------- /core/nx-deps-cache.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync } = require('fs'); 2 | function createCache( 3 | nxJson, 4 | packageJsonDeps, 5 | projectGraph, 6 | tsConfig 7 | ) { 8 | const nxJsonPlugins = (nxJson.plugins || []).map((p) => ({ 9 | name: p, 10 | version: packageJsonDeps[p], 11 | })); 12 | const newValue = { 13 | version: projectGraph.version || '5.0', 14 | deps: packageJsonDeps, 15 | // compilerOptions may not exist, especially for repos converted through add-nx-to-monorepo 16 | pathMappings: tsConfig && tsConfig.compilerOptions && tsConfig.compilerOptions.paths || {}, 17 | nxJsonPlugins, 18 | nodes: projectGraph.nodes, 19 | externalNodes: projectGraph.externalNodes, 20 | dependencies: projectGraph.dependencies, 21 | }; 22 | return newValue; 23 | } 24 | 25 | /** 26 | * Serializes the given data to a JSON string. 27 | * By default the JSON string is formatted with a 2 space intendation to be easy readable. 28 | * 29 | * @param input Object which should be serialized to JSON 30 | * @param options JSON serialize options 31 | * @returns the formatted JSON representation of the object 32 | */ 33 | function serializeJson( 34 | input, 35 | options 36 | ) { 37 | return JSON.stringify(input, null, options && options.spaces || 2) + '\n'; 38 | } 39 | 40 | /** 41 | * Serializes the given data to JSON and writes it to a file. 42 | * 43 | * @param path A path to a file. 44 | * @param data data which should be serialized to JSON and written to the file 45 | * @param options JSON serialize options 46 | */ 47 | function writeJsonFile( 48 | path, 49 | data, 50 | options 51 | ) { 52 | const serializedJson = serializeJson(data, options); 53 | const content = options && options.appendNewLine 54 | ? `${serializedJson}\n` 55 | : serializedJson; 56 | writeFileSync(path, content, { encoding: 'utf-8' }); 57 | } 58 | 59 | function writeCache(cache) { 60 | writeJsonFile('.cache/nxdeps.json', cache); 61 | } 62 | 63 | module.exports = { createCache, writeCache } 64 | -------------------------------------------------------------------------------- /core/output.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | const os = require("os"); 3 | 4 | class CLIOutput { 5 | constructor() { 6 | this.X_PADDING = ' '; 7 | /** 8 | * Expose some color and other utility functions so that other parts of the codebase that need 9 | * more fine-grained control of message bodies are still using a centralized 10 | * implementation. 11 | */ 12 | this.colors = { 13 | gray: chalk.gray, 14 | green: chalk.green, 15 | red: chalk.red, 16 | cyan: chalk.cyan, 17 | white: chalk.white, 18 | }; 19 | this.bold = chalk.bold; 20 | this.underline = chalk.underline; 21 | this.dim = chalk.dim; 22 | } 23 | /** 24 | * Longer dash character which forms more of a continuous line when place side to side 25 | * with itself, unlike the standard dash character 26 | */ 27 | get VERTICAL_SEPARATOR() { 28 | let divider = ''; 29 | for (let i = 0; i < process.stdout.columns - this.X_PADDING.length * 2; i++) { 30 | divider += '\u2014'; 31 | } 32 | return divider; 33 | } 34 | writeToStdOut(str) { 35 | process.stdout.write(str); 36 | } 37 | writeOutputTitle({ color, title, }) { 38 | this.writeToStdOut(` ${this.applyNxPrefix(color, title)}${os.EOL}`); 39 | } 40 | writeOptionalOutputBody(bodyLines) { 41 | if (!bodyLines) { 42 | return; 43 | } 44 | this.addNewline(); 45 | bodyLines.forEach((bodyLine) => this.writeToStdOut(` ${bodyLine}${os.EOL}`)); 46 | } 47 | applyNxPrefix(color = 'cyan', text) { 48 | let nxPrefix = ''; 49 | if (chalk[color]) { 50 | nxPrefix = `${chalk[color]('>')} ${chalk.reset.inverse.bold[color](' NX ')}`; 51 | } 52 | else { 53 | nxPrefix = `${chalk.keyword(color)('>')} ${chalk.reset.inverse.bold.keyword(color)(' NX ')}`; 54 | } 55 | return `${nxPrefix} ${text}`; 56 | } 57 | addNewline() { 58 | this.writeToStdOut(os.EOL); 59 | } 60 | addVerticalSeparator(color = 'gray') { 61 | this.addNewline(); 62 | this.addVerticalSeparatorWithoutNewLines(color); 63 | this.addNewline(); 64 | } 65 | addVerticalSeparatorWithoutNewLines(color = 'gray') { 66 | this.writeToStdOut(`${this.X_PADDING}${chalk.dim[color](this.VERTICAL_SEPARATOR)}${os.EOL}`); 67 | } 68 | error({ title, slug, bodyLines }) { 69 | this.addNewline(); 70 | this.writeOutputTitle({ 71 | color: 'red', 72 | title: chalk.red(title), 73 | }); 74 | this.writeOptionalOutputBody(bodyLines); 75 | /** 76 | * Optional slug to be used in an Nx error message redirect URL 77 | */ 78 | if (slug && typeof slug === 'string') { 79 | this.addNewline(); 80 | this.writeToStdOut(`${chalk.grey(' Learn more about this error: ')}https://errors.nx.dev/${slug}${os.EOL}`); 81 | } 82 | this.addNewline(); 83 | } 84 | warn({ title, slug, bodyLines }) { 85 | this.addNewline(); 86 | this.writeOutputTitle({ 87 | color: 'yellow', 88 | title: chalk.yellow(title), 89 | }); 90 | this.writeOptionalOutputBody(bodyLines); 91 | /** 92 | * Optional slug to be used in an Nx warning message redirect URL 93 | */ 94 | if (slug && typeof slug === 'string') { 95 | this.addNewline(); 96 | this.writeToStdOut(`${chalk.grey(' Learn more about this warning: ')}https://errors.nx.dev/${slug}\n`); 97 | } 98 | this.addNewline(); 99 | } 100 | note({ title, bodyLines }) { 101 | this.addNewline(); 102 | this.writeOutputTitle({ 103 | color: 'orange', 104 | title: chalk.keyword('orange')(title), 105 | }); 106 | this.writeOptionalOutputBody(bodyLines); 107 | this.addNewline(); 108 | } 109 | success({ title, bodyLines }) { 110 | this.addNewline(); 111 | this.writeOutputTitle({ 112 | color: 'green', 113 | title: chalk.green(title), 114 | }); 115 | this.writeOptionalOutputBody(bodyLines); 116 | this.addNewline(); 117 | } 118 | logSingleLine(message) { 119 | this.addNewline(); 120 | this.writeOutputTitle({ 121 | color: 'gray', 122 | title: message, 123 | }); 124 | this.addNewline(); 125 | } 126 | logCommand(message, taskStatus) { 127 | // normalize the message 128 | if (message.startsWith('nx run ')) { 129 | message = message.substring('nx run '.length); 130 | } 131 | else if (message.startsWith('run ')) { 132 | message = message.substring('run '.length); 133 | } 134 | this.addNewline(); 135 | let commandOutput = `${chalk.dim('> nx run')} ${message}`; 136 | if (taskStatus === 'local-cache') { 137 | commandOutput += ` ${chalk.grey('[local cache]')}`; 138 | } 139 | else if (taskStatus === 'remote-cache') { 140 | commandOutput += ` ${chalk.grey('[remote cache]')}`; 141 | } 142 | else if (taskStatus === 'local-cache-kept-existing') { 143 | commandOutput += ` ${chalk.grey('[existing outputs match the cache, left as is]')}`; 144 | } 145 | this.writeToStdOut(commandOutput); 146 | this.addNewline(); 147 | } 148 | log({ title, bodyLines, color }) { 149 | this.addNewline(); 150 | color = color || 'white'; 151 | this.writeOutputTitle({ 152 | color: 'cyan', 153 | title: chalk[color](title), 154 | }); 155 | this.writeOptionalOutputBody(bodyLines); 156 | this.addNewline(); 157 | } 158 | } 159 | 160 | module.exports = { output: new CLIOutput() } 161 | -------------------------------------------------------------------------------- /core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "core", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "echo 123" 7 | } 8 | } -------------------------------------------------------------------------------- /core/path.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | function removeWindowsDriveLetter(osSpecificPath) { 3 | return osSpecificPath.replace(/^[A-Z]:/, ''); 4 | } 5 | 6 | /** 7 | * Coverts an os specific path to a unix style path 8 | */ 9 | function normalizePath(osSpecificPath) { 10 | return removeWindowsDriveLetter(osSpecificPath).split('\\').join('/'); 11 | } 12 | 13 | /** 14 | * Normalized path fragments and joins them 15 | */ 16 | function joinPathFragments(...fragments) { 17 | return normalizePath(path.join(...fragments)); 18 | } 19 | 20 | module.exports = { normalizePath, joinPathFragments }; -------------------------------------------------------------------------------- /core/project-graph-builder.js: -------------------------------------------------------------------------------- 1 | const { DependencyType } = require("./type"); 2 | 3 | /** 4 | * Builder for adding nodes and dependencies to a {@link ProjectGraph} 5 | */ 6 | class ProjectGraphBuilder { 7 | graph; 8 | removedEdges; 9 | 10 | constructor(g) { 11 | this.removedEdges = {}; 12 | if (g) { 13 | this.graph = g; 14 | } else { 15 | this.graph = { 16 | nodes: {}, 17 | externalNodes: {}, 18 | dependencies: {}, 19 | }; 20 | } 21 | } 22 | 23 | /** 24 | * Adds a project node to the project graph 25 | */ 26 | addNode(node) { 27 | // Check if project with the same name already exists 28 | if (this.graph.nodes[node.name]) { 29 | // Throw if existing project is of a different type 30 | if (this.graph.nodes[node.name].type !== node.type) { 31 | throw new Error( 32 | `Multiple projects are named "${node.name}". One is of type "${node.type 33 | }" and the other is of type "${this.graph.nodes[node.name].type 34 | }". Please resolve the conflicting project names.` 35 | ); 36 | } 37 | } 38 | this.graph.nodes[node.name] = node; 39 | this.graph.dependencies[node.name] = []; 40 | } 41 | 42 | /** 43 | * Adds a external node to the project graph 44 | */ 45 | addExternalNode(node) { 46 | this.graph.externalNodes[node.name] = node; 47 | } 48 | 49 | /** 50 | * Adds a dependency from source project to target project 51 | */ 52 | addImplicitDependency( 53 | sourceProjectName, 54 | targetProjectName 55 | ) { 56 | if (sourceProjectName === targetProjectName) { 57 | return; 58 | } 59 | if (!this.graph.nodes[sourceProjectName]) { 60 | throw new Error(`Source project does not exist: ${sourceProjectName}`); 61 | } 62 | if ( 63 | !this.graph.nodes[targetProjectName] && 64 | !this.graph.externalNodes[targetProjectName] 65 | ) { 66 | throw new Error(`Target project does not exist: ${targetProjectName}`); 67 | } 68 | this.graph.dependencies[sourceProjectName].push({ 69 | source: sourceProjectName, 70 | target: targetProjectName, 71 | type: DependencyType.implicit, 72 | }); 73 | } 74 | 75 | /** 76 | * Removes a dependency from source project to target project 77 | */ 78 | removeDependency(sourceProjectName, targetProjectName) { 79 | if (sourceProjectName === targetProjectName) { 80 | return; 81 | } 82 | if (!this.graph.nodes[sourceProjectName]) { 83 | throw new Error(`Source project does not exist: ${sourceProjectName}`); 84 | } 85 | if ( 86 | !this.graph.nodes[targetProjectName] && 87 | !this.graph.externalNodes[targetProjectName] 88 | ) { 89 | throw new Error(`Target project does not exist: ${targetProjectName}`); 90 | } 91 | // this.graph.dependencies[sourceProjectName] = this.graph.dependencies[ 92 | // sourceProjectName 93 | // ].filter((d) => d.target !== targetProjectName); 94 | if (!this.removedEdges[sourceProjectName]) { 95 | this.removedEdges[sourceProjectName] = new Set(); 96 | } 97 | this.removedEdges[sourceProjectName].add(targetProjectName); 98 | } 99 | 100 | /** 101 | * Add an explicit dependency from a file in source project to target project 102 | */ 103 | addExplicitDependency( 104 | sourceProjectName, 105 | sourceProjectFile, 106 | targetProjectName 107 | ) { 108 | if (sourceProjectName === targetProjectName) { 109 | return; 110 | } 111 | const source = this.graph.nodes[sourceProjectName]; 112 | if (!source) { 113 | throw new Error(`Source project does not exist: ${sourceProjectName}`); 114 | } 115 | 116 | if ( 117 | !this.graph.nodes[targetProjectName] && 118 | !this.graph.externalNodes[targetProjectName] 119 | ) { 120 | throw new Error(`Target project does not exist: ${targetProjectName}`); 121 | } 122 | 123 | const fileData = source.data.files.find( 124 | (f) => f.file === sourceProjectFile 125 | ); 126 | if (!fileData) { 127 | throw new Error( 128 | `Source project ${sourceProjectName} does not have a file: ${sourceProjectFile}` 129 | ); 130 | } 131 | 132 | if (!fileData.deps) { 133 | fileData.deps = []; 134 | } 135 | 136 | if (!fileData.deps.find((t) => t === targetProjectName)) { 137 | fileData.deps.push(targetProjectName); 138 | } 139 | } 140 | 141 | /** 142 | * Set version of the project graph 143 | */ 144 | setVersion(version) { 145 | this.graph.version = version; 146 | } 147 | 148 | getUpdatedProjectGraph() { 149 | for (const sourceProject of Object.keys(this.graph.nodes)) { 150 | const alreadySetTargetProjects = 151 | this.calculateAlreadySetTargetDeps(sourceProject); 152 | this.graph.dependencies[sourceProject] = [ 153 | ...alreadySetTargetProjects.values(), 154 | ]; 155 | 156 | const fileDeps = this.calculateTargetDepsFromFiles(sourceProject); 157 | for (const targetProject of fileDeps) { 158 | if (!alreadySetTargetProjects.has(targetProject)) { 159 | if ( 160 | !this.removedEdges[sourceProject] || 161 | !this.removedEdges[sourceProject].has(targetProject) 162 | ) { 163 | this.graph.dependencies[sourceProject].push({ 164 | source: sourceProject, 165 | target: targetProject, 166 | type: DependencyType.static, 167 | }); 168 | } 169 | } 170 | } 171 | } 172 | return this.graph; 173 | } 174 | 175 | calculateTargetDepsFromFiles(sourceProject) { 176 | const fileDeps = new Set(); 177 | const files = this.graph.nodes[sourceProject].data.files; 178 | if (!files) return fileDeps; 179 | for (let f of files) { 180 | if (f.deps) { 181 | for (let p of f.deps) { 182 | fileDeps.add(p); 183 | } 184 | } 185 | } 186 | return fileDeps; 187 | } 188 | 189 | calculateAlreadySetTargetDeps(sourceProject) { 190 | const alreadySetTargetProjects = new Map(); 191 | const removed = this.removedEdges[sourceProject]; 192 | for (const d of this.graph.dependencies[sourceProject]) { 193 | if (!removed || !removed.has(d.target)) { 194 | alreadySetTargetProjects.set(d.target, d); 195 | } 196 | } 197 | return alreadySetTargetProjects; 198 | } 199 | } 200 | 201 | module.exports = { ProjectGraphBuilder } -------------------------------------------------------------------------------- /core/project-graph.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Backwards compatibility adapter for project graph 3 | * @param {string} sourceVersion 4 | * @param {string} targetVersion 5 | * @param projectGraph 6 | * @param {ProjectGraph} projectGraph 7 | * @returns {ProjectGraph} 8 | */ 9 | function projectGraphAdapter( 10 | sourceVersion, 11 | targetVersion, 12 | projectGraph 13 | ) { 14 | if (sourceVersion === targetVersion) { 15 | return projectGraph; 16 | } 17 | if (sourceVersion === '5.0' && targetVersion === '4.0') { 18 | return projectGraphCompat5to4(projectGraph); 19 | } 20 | throw new Error( 21 | `Invalid source or target versions. Source: ${sourceVersion}, Target: ${targetVersion}. 22 | Only backwards compatibility between "5.0" and "4.0" is supported. 23 | This error can be caused by "@nrwl/..." packages getting out of sync or outdated project graph cache. 24 | Check the versions running "nx report" and/or remove your "nxdeps.json" file (in node_modules/.cache/nx folder). 25 | ` 26 | ); 27 | } 28 | 29 | module.exports = { projectGraphAdapter } -------------------------------------------------------------------------------- /core/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "core", 3 | "targets": { 4 | "build": { 5 | "executor": "@nrwl/js:tsc", 6 | "outputs": [ 7 | "{options.outputPath}" 8 | ], 9 | "options": { 10 | "outputPath": "dist/apps/demoapp", 11 | "main": "apps/demoapp/src/index.ts", 12 | "tsConfig": "apps/demoapp/tsconfig.app.json", 13 | "assets": [ 14 | "apps/demoapp/*.md" 15 | ] 16 | } 17 | }, 18 | "serve": { 19 | "executor": "@nrwl/js:node", 20 | "options": { 21 | "buildTarget": "simple:test" 22 | } 23 | }, 24 | "make": { 25 | "executor": "@nrwl/workspace:run-commands", 26 | "options": { 27 | "commands": [ 28 | { 29 | "command": "node ./index.js" 30 | } 31 | ] 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /core/run-command.js: -------------------------------------------------------------------------------- 1 | const { StaticRunOneTerminalOutputLifeCycle } = require('./static-run-one-terminal-output-life-cycle'); 2 | const { CompositeLifeCycle } = require('./life-cycle'); 3 | async function getTerminalOutputLifeCycle( 4 | initiatingProject, 5 | terminalOutputStrategy, 6 | projectNames, 7 | tasks, 8 | nxArgs, 9 | overrides, 10 | runnerOptions 11 | ) { 12 | if (terminalOutputStrategy === 'run-one') { 13 | return { 14 | lifeCycle: new StaticRunOneTerminalOutputLifeCycle( 15 | initiatingProject, 16 | projectNames, 17 | tasks, 18 | nxArgs 19 | ), 20 | renderIsDone: Promise.resolve(), 21 | }; 22 | } 23 | } 24 | 25 | async function runCommand(projectsToRun, projectGraph, { nxJson }, nxArgs, overrides, terminalOutputStrategy, initiatingProject) { 26 | const { tasksRunner, runnerOptions } = getRunner(nxArgs, nxJson); 27 | console.log('runCommand'); 28 | const tasksMap = []; 29 | // 组合命令信息 30 | // createTasksForProjectToRun 31 | for (const project of projectsToRun) { 32 | tasksMap.push({ 33 | id: `${project.name}:${nxArgs.target}`, 34 | overrides, 35 | target: { 36 | project: project.name, 37 | // 先不演示有命令带参数的情况 38 | configuration: undefined, 39 | target: nxArgs.target 40 | }, 41 | projectRoot: project.data.root, 42 | 43 | }) 44 | } 45 | const projectNames = projectsToRun.map((t) => t.name); 46 | const { lifeCycle, renderIsDone } = await getTerminalOutputLifeCycle(initiatingProject, terminalOutputStrategy, projectNames, tasksMap, nxArgs, overrides, runnerOptions); 47 | const lifeCycles = [lifeCycle]; 48 | // 使用 default-tasks-runner 来执行代码 49 | const promiseOrObservable = tasksRunner( 50 | tasksMap, 51 | Object.assign( 52 | Object.assign({}, runnerOptions), { 53 | lifeCycle: new CompositeLifeCycle(lifeCycles) 54 | }), 55 | { 56 | initiatingProject, 57 | target: nxArgs.target, 58 | projectGraph, 59 | nxJson, 60 | } 61 | ); 62 | 63 | 64 | console.log(tasksMap); 65 | } 66 | 67 | function getRunner(nxArgs, nxJson) { 68 | let runner = nxArgs.runner; 69 | runner = runner || 'default'; 70 | if (nxJson.tasksRunnerOptions[runner]) { 71 | // 这里取的是 nx.json 里面默认的 tasksRunnerOptions 的 default runner 72 | let modulePath = nxJson.tasksRunnerOptions[runner].runner; 73 | let tasksRunner; 74 | if (modulePath) { 75 | tasksRunner = require(modulePath); 76 | // to support both babel and ts formats 77 | if (tasksRunner.default) { 78 | tasksRunner = tasksRunner.default; 79 | } 80 | } 81 | return { 82 | tasksRunner, 83 | runnerOptions: Object.assign(Object.assign({}, nxJson.tasksRunnerOptions[runner].options), nxArgs), 84 | }; 85 | } 86 | } 87 | 88 | module.exports = { runCommand } -------------------------------------------------------------------------------- /core/run-one.js: -------------------------------------------------------------------------------- 1 | function getProjects(projectGraph, project) { 2 | let projects = [projectGraph.nodes[project]]; 3 | let projectsMap = { 4 | [project]: projectGraph.nodes[project], 5 | }; 6 | 7 | return { projects, projectsMap }; 8 | } 9 | 10 | module.exports = { getProjects }; 11 | -------------------------------------------------------------------------------- /core/static-run-one-terminal-output-life-cycle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The following life cycle's outputs are static, meaning no previous content 3 | * is rewritten or modified as new outputs are added. It is therefore intended 4 | * for use in CI environments. 5 | * 6 | * For the common case of a user executing a command on their local machine, 7 | * the dynamic equivalent of this life cycle is usually preferable. 8 | */ 9 | class StaticRunOneTerminalOutputLifeCycle { 10 | constructor(initiatingProject, projectNames, tasks, args) { 11 | this.initiatingProject = initiatingProject; 12 | this.projectNames = projectNames; 13 | this.tasks = tasks; 14 | this.args = args; 15 | this.failedTasks = []; 16 | this.cachedTasks = []; 17 | } 18 | startCommand() { 19 | if (process.env.NX_INVOKED_BY_RUNNER) { 20 | return; 21 | } 22 | const numberOfDeps = this.tasks.length - 1; 23 | if (numberOfDeps > 0) { 24 | console.log(`Running target ${output.output.bold(this.args.target)} for project ${output.output.bold(this.initiatingProject)} and ${output.output.bold(numberOfDeps)} task(s) it depends on`); 25 | } 26 | } 27 | endCommand() { 28 | // Silent for a single task 29 | if (process.env.NX_INVOKED_BY_RUNNER) { 30 | return; 31 | } 32 | } 33 | endTasks(taskResults) { 34 | for (let t of taskResults) { 35 | if (t.status === 'failure') { 36 | this.failedTasks.push(t.task); 37 | } 38 | else if (t.status === 'local-cache') { 39 | this.cachedTasks.push(t.task); 40 | } 41 | else if (t.status === 'local-cache-kept-existing') { 42 | this.cachedTasks.push(t.task); 43 | } 44 | else if (t.status === 'remote-cache') { 45 | this.cachedTasks.push(t.task); 46 | } 47 | } 48 | } 49 | } 50 | 51 | module.exports = { StaticRunOneTerminalOutputLifeCycle } -------------------------------------------------------------------------------- /core/strip-source-code.js: -------------------------------------------------------------------------------- 1 | let SyntaxKind; 2 | function stripSourceCode(scanner, contents) { 3 | if (!SyntaxKind) { 4 | SyntaxKind = require('typescript').SyntaxKind; 5 | } 6 | 7 | if (contents.indexOf('loadChildren') > -1) { 8 | return contents; 9 | } 10 | 11 | scanner.setText(contents); 12 | let token = scanner.scan(); 13 | const statements = []; 14 | let start = null; 15 | while (token !== SyntaxKind.EndOfFileToken) { 16 | const potentialStart = scanner.getStartPos(); 17 | switch (token) { 18 | case SyntaxKind.MultiLineCommentTrivia: 19 | case SyntaxKind.SingleLineCommentTrivia: { 20 | const isMultiLineCommentTrivia = 21 | token === SyntaxKind.MultiLineCommentTrivia; 22 | const start = potentialStart + 2; 23 | token = scanner.scan(); 24 | const end = scanner.getStartPos() - (isMultiLineCommentTrivia ? 2 : 0); 25 | const comment = contents.substring(start, end).trim(); 26 | if (comment === 'nx-ignore-next-line') { 27 | // reading till the end of the line 28 | while ( 29 | token === SyntaxKind.WhitespaceTrivia || 30 | token === SyntaxKind.NewLineTrivia 31 | ) { 32 | token = scanner.scan(); 33 | } 34 | 35 | // ignore next line 36 | while ( 37 | token !== SyntaxKind.NewLineTrivia && 38 | token !== SyntaxKind.EndOfFileToken 39 | ) { 40 | token = scanner.scan(); 41 | } 42 | } 43 | break; 44 | } 45 | 46 | case SyntaxKind.RequireKeyword: 47 | case SyntaxKind.ImportKeyword: { 48 | token = scanner.scan(); 49 | while ( 50 | token === SyntaxKind.WhitespaceTrivia || 51 | token === SyntaxKind.NewLineTrivia 52 | ) { 53 | token = scanner.scan(); 54 | } 55 | start = potentialStart; 56 | break; 57 | } 58 | 59 | case SyntaxKind.ExportKeyword: { 60 | token = scanner.scan(); 61 | while ( 62 | token === SyntaxKind.WhitespaceTrivia || 63 | token === SyntaxKind.NewLineTrivia 64 | ) { 65 | token = scanner.scan(); 66 | } 67 | if ( 68 | token === SyntaxKind.OpenBraceToken || 69 | token === SyntaxKind.AsteriskToken 70 | ) { 71 | start = potentialStart; 72 | } 73 | break; 74 | } 75 | 76 | case SyntaxKind.StringLiteral: { 77 | if (start !== null) { 78 | token = scanner.scan(); 79 | if (token === SyntaxKind.CloseParenToken) { 80 | token = scanner.scan(); 81 | } 82 | const end = scanner.getStartPos(); 83 | statements.push(contents.substring(start, end)); 84 | start = null; 85 | } else { 86 | token = scanner.scan(); 87 | } 88 | break; 89 | } 90 | 91 | default: { 92 | token = scanner.scan(); 93 | } 94 | } 95 | } 96 | 97 | return statements.join('\n'); 98 | } 99 | 100 | module.exports = { stripSourceCode } -------------------------------------------------------------------------------- /core/target-project-locator.js: -------------------------------------------------------------------------------- 1 | const typescript = require("./typescript"); 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | const app_root = require("./app-root"); 5 | const { parseJson } = require("./devkit"); 6 | 7 | function isRelativePath(path) { 8 | return (path === '.' || 9 | path === '..' || 10 | path.startsWith('./') || 11 | path.startsWith('../')); 12 | } 13 | 14 | 15 | 16 | function readFileIfExisting(path) { 17 | return fs.existsSync(path) ? fs.readFileSync(path, 'utf-8') : ''; 18 | } 19 | 20 | class TargetProjectLocator { 21 | constructor(nodes, externalNodes) { 22 | var _a, _b; 23 | this.nodes = nodes; 24 | this.externalNodes = externalNodes; 25 | this.projectRootMappings = createProjectRootMappings(this.nodes); 26 | this.npmProjects = this.externalNodes 27 | ? Object.values(this.externalNodes) 28 | : []; 29 | this.tsConfig = this.getRootTsConfig(); 30 | this.paths = (_b = (_a = this.tsConfig.config) === null || _a === void 0 ? void 0 : _a.compilerOptions) === null || _b === void 0 ? void 0 : _b.paths; 31 | this.typescriptResolutionCache = new Map(); 32 | this.npmResolutionCache = new Map(); 33 | } 34 | /** 35 | * Find a project based on its import 36 | * 37 | * @param importExpr 38 | * @param filePath 39 | * @param npmScope 40 | * Npm scope shouldn't be used finding a project, but, to improve backward 41 | * compatibility, we fallback to checking the scope. 42 | * This happens in cases where someone has the dist output in their tsconfigs 43 | * and typescript will find the dist before the src. 44 | */ 45 | findProjectWithImport(importExpr, filePath, npmScope) { 46 | const normalizedImportExpr = importExpr.split('#')[0]; 47 | if (isRelativePath(normalizedImportExpr)) { 48 | const resolvedModule = path.posix.join((0, path.dirname)(filePath), normalizedImportExpr); 49 | return this.findProjectOfResolvedModule(resolvedModule); 50 | } 51 | const paths = this.findPaths(normalizedImportExpr); 52 | if (paths) { 53 | for (let p of paths) { 54 | const maybeResolvedProject = this.findProjectOfResolvedModule(p); 55 | if (maybeResolvedProject) { 56 | return maybeResolvedProject; 57 | } 58 | } 59 | } 60 | // try to find npm package before using expensive typescript resolution 61 | const npmProject = this.findNpmPackage(normalizedImportExpr); 62 | if (npmProject) { 63 | return npmProject; 64 | } 65 | if (this.tsConfig.config) { 66 | // TODO(meeroslav): this block is probably obsolete 67 | // and existed only because of the incomplete `paths` matching 68 | // if import cannot be matched using tsconfig `paths` the compilation would fail anyway 69 | const resolvedProject = this.resolveImportWithTypescript(normalizedImportExpr, filePath); 70 | if (resolvedProject) { 71 | return resolvedProject; 72 | } 73 | } 74 | // nothing found, cache for later 75 | this.npmResolutionCache.set(normalizedImportExpr, undefined); 76 | return null; 77 | } 78 | findPaths(normalizedImportExpr) { 79 | if (!this.paths) { 80 | return undefined; 81 | } 82 | if (this.paths[normalizedImportExpr]) { 83 | return this.paths[normalizedImportExpr]; 84 | } 85 | const wildcardPath = Object.keys(this.paths).find((path) => path.endsWith('/*') && 86 | (normalizedImportExpr.startsWith(path.replace(/\*$/, '')) || 87 | normalizedImportExpr === path.replace(/\/\*$/, ''))); 88 | if (wildcardPath) { 89 | return this.paths[wildcardPath]; 90 | } 91 | return undefined; 92 | } 93 | resolveImportWithTypescript(normalizedImportExpr, filePath) { 94 | let resolvedModule; 95 | if (this.typescriptResolutionCache.has(normalizedImportExpr)) { 96 | resolvedModule = this.typescriptResolutionCache.get(normalizedImportExpr); 97 | } 98 | else { 99 | resolvedModule = (0, typescript.resolveModuleByImport)(normalizedImportExpr, filePath, this.tsConfig.absolutePath); 100 | this.typescriptResolutionCache.set(normalizedImportExpr, resolvedModule ? resolvedModule : null); 101 | } 102 | // TODO: vsavkin temporary workaround. Remove it once we reworking handling of npm packages. 103 | if (resolvedModule && resolvedModule.indexOf('node_modules/') === -1) { 104 | const resolvedProject = this.findProjectOfResolvedModule(resolvedModule); 105 | if (resolvedProject) { 106 | return resolvedProject; 107 | } 108 | } 109 | return; 110 | } 111 | findNpmPackage(npmImport) { 112 | if (this.npmResolutionCache.has(npmImport)) { 113 | return this.npmResolutionCache.get(npmImport); 114 | } 115 | else { 116 | const pkg = this.npmProjects.find((pkg) => npmImport === pkg.data.packageName || 117 | npmImport.startsWith(`${pkg.data.packageName}/`)); 118 | if (pkg) { 119 | this.npmResolutionCache.set(npmImport, pkg.name); 120 | return pkg.name; 121 | } 122 | } 123 | } 124 | findProjectOfResolvedModule(resolvedModule) { 125 | const normalizedResolvedModule = resolvedModule.startsWith('./') 126 | ? resolvedModule.substring(2) 127 | : resolvedModule; 128 | const importedProject = this.findMatchingProjectFiles(normalizedResolvedModule); 129 | return importedProject ? importedProject.name : void 0; 130 | } 131 | getAbsolutePath(path2) { 132 | return path.join(app_root.appRootPath, path2); 133 | } 134 | getRootTsConfig() { 135 | let path = 'tsconfig.base.json'; 136 | let absolutePath = this.getAbsolutePath(path); 137 | let content = readFileIfExisting(absolutePath); 138 | if (!content) { 139 | path = 'tsconfig.json'; 140 | absolutePath = this.getAbsolutePath(path); 141 | content = readFileIfExisting(absolutePath); 142 | } 143 | if (!content) { 144 | return { 145 | path: null, 146 | absolutePath: null, 147 | config: null, 148 | }; 149 | } 150 | return { path, absolutePath, config: parseJson(content) }; 151 | } 152 | findMatchingProjectFiles(file) { 153 | for (let currentPath = file; currentPath != (0, path.dirname)(currentPath); currentPath = (0, path.dirname)(currentPath)) { 154 | const p = this.projectRootMappings.get(currentPath); 155 | if (p) { 156 | return p; 157 | } 158 | } 159 | return null; 160 | } 161 | } 162 | function createProjectRootMappings(nodes) { 163 | const projectRootMappings = new Map(); 164 | for (const projectName of Object.keys(nodes)) { 165 | const root = nodes[projectName].data.root; 166 | projectRootMappings.set(root && root.endsWith('/') ? root.substring(0, root.length - 1) : root, nodes[projectName]); 167 | } 168 | return projectRootMappings; 169 | } 170 | 171 | module.exports = { TargetProjectLocator } -------------------------------------------------------------------------------- /core/type.js: -------------------------------------------------------------------------------- 1 | const DependencyType = { 2 | /** 3 | * Static dependencies are tied to the loading of the module 4 | */ 5 | static: 'static', 6 | /** 7 | * Dynamic dependencies are brought in by the module at run time 8 | */ 9 | dynamic: 'dynamic', 10 | /** 11 | * Implicit dependencies are inferred 12 | */ 13 | implicit: 'implicit', 14 | } 15 | 16 | module.exports = { DependencyType } -------------------------------------------------------------------------------- /core/typescript-import-locator.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { stripSourceCode } = require("./strip-source-code"); 3 | const app_root = require("./app-root"); 4 | const { existsSync, readFileSync } = require("fs"); 5 | const { DependencyType } = require("./type"); 6 | 7 | function defaultFileRead(filePath) { 8 | return readFileSync(path.join(app_root.appRootPath, filePath), 'utf-8'); 9 | } 10 | 11 | let tsModule; 12 | class TypeScriptImportLocator { 13 | constructor() { 14 | tsModule = require('typescript'); 15 | this.scanner = tsModule.createScanner(tsModule.ScriptTarget.Latest, false); 16 | } 17 | fromFile(filePath, visitor) { 18 | const extension = path.extname(filePath); 19 | if (extension !== '.ts' && 20 | extension !== '.tsx' && 21 | extension !== '.js' && 22 | extension !== '.jsx') { 23 | return; 24 | } 25 | const content = defaultFileRead(filePath); 26 | const strippedContent = stripSourceCode(this.scanner, content); 27 | if (strippedContent !== '') { 28 | const tsFile = tsModule.createSourceFile(filePath, strippedContent, tsModule.ScriptTarget.Latest, true); 29 | this.fromNode(filePath, tsFile, visitor); 30 | } 31 | } 32 | fromNode(filePath, node, visitor) { 33 | if (tsModule.isImportDeclaration(node) || 34 | (tsModule.isExportDeclaration(node) && node.moduleSpecifier)) { 35 | if (!this.ignoreStatement(node)) { 36 | const imp = this.getStringLiteralValue(node.moduleSpecifier); 37 | visitor(imp, filePath, DependencyType.static); 38 | } 39 | return; // stop traversing downwards 40 | } 41 | if (tsModule.isCallExpression(node) && 42 | node.expression.kind === tsModule.SyntaxKind.ImportKeyword && 43 | node.arguments.length === 1 && 44 | tsModule.isStringLiteral(node.arguments[0])) { 45 | if (!this.ignoreStatement(node)) { 46 | const imp = this.getStringLiteralValue(node.arguments[0]); 47 | visitor(imp, filePath, DependencyType.dynamic); 48 | } 49 | return; 50 | } 51 | if (tsModule.isCallExpression(node) && 52 | node.expression.getText() === 'require' && 53 | node.arguments.length === 1 && 54 | tsModule.isStringLiteral(node.arguments[0])) { 55 | if (!this.ignoreStatement(node)) { 56 | const imp = this.getStringLiteralValue(node.arguments[0]); 57 | visitor(imp, filePath, DependencyType.static); 58 | } 59 | return; 60 | } 61 | if (node.kind === tsModule.SyntaxKind.PropertyAssignment) { 62 | const name = this.getPropertyAssignmentName(node.name); 63 | if (name === 'loadChildren') { 64 | const init = node.initializer; 65 | if (init.kind === tsModule.SyntaxKind.StringLiteral && 66 | !this.ignoreLoadChildrenDependency(node.getFullText())) { 67 | const childrenExpr = this.getStringLiteralValue(init); 68 | visitor(childrenExpr, filePath, DependencyType.dynamic); 69 | return; // stop traversing downwards 70 | } 71 | } 72 | } 73 | /** 74 | * Continue traversing down the AST from the current node 75 | */ 76 | tsModule.forEachChild(node, (child) => this.fromNode(filePath, child, visitor)); 77 | } 78 | ignoreStatement(node) { 79 | return stripSourceCode(this.scanner, node.getFullText()) === ''; 80 | } 81 | ignoreLoadChildrenDependency(contents) { 82 | this.scanner.setText(contents); 83 | let token = this.scanner.scan(); 84 | while (token !== tsModule.SyntaxKind.EndOfFileToken) { 85 | if (token === tsModule.SyntaxKind.SingleLineCommentTrivia || 86 | token === tsModule.SyntaxKind.MultiLineCommentTrivia) { 87 | const start = this.scanner.getStartPos() + 2; 88 | token = this.scanner.scan(); 89 | const isMultiLineCommentTrivia = token === tsModule.SyntaxKind.MultiLineCommentTrivia; 90 | const end = this.scanner.getStartPos() - (isMultiLineCommentTrivia ? 2 : 0); 91 | const comment = contents.substring(start, end).trim(); 92 | if (comment === 'nx-ignore-next-line') { 93 | return true; 94 | } 95 | } 96 | else { 97 | token = this.scanner.scan(); 98 | } 99 | } 100 | return false; 101 | } 102 | getPropertyAssignmentName(nameNode) { 103 | switch (nameNode.kind) { 104 | case tsModule.SyntaxKind.Identifier: 105 | return nameNode.getText(); 106 | case tsModule.SyntaxKind.StringLiteral: 107 | return nameNode.text; 108 | default: 109 | return null; 110 | } 111 | } 112 | getStringLiteralValue(node) { 113 | return node.getText().substr(1, node.getText().length - 2); 114 | } 115 | } 116 | 117 | module.exports = { TypeScriptImportLocator } 118 | -------------------------------------------------------------------------------- /core/typescript.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const app_root = require("./app-root"); 3 | const normalizedAppRoot = app_root.appRootPath.replace(/\\/g, '/'); 4 | let tsModule; 5 | function readTsConfig(tsConfigPath) { 6 | if (!tsModule) { 7 | tsModule = require('typescript'); 8 | } 9 | const readResult = tsModule.readConfigFile(tsConfigPath, tsModule.sys.readFile); 10 | return tsModule.parseJsonConfigFileContent(readResult.config, tsModule.sys, (0, path.dirname)(tsConfigPath)); 11 | } 12 | exports.readTsConfig = readTsConfig; 13 | function readTsConfigOptions(tsConfigPath) { 14 | if (!tsModule) { 15 | tsModule = require('typescript'); 16 | } 17 | const readResult = tsModule.readConfigFile(tsConfigPath, tsModule.sys.readFile); 18 | // we don't need to scan the files, we only care about options 19 | const host = { 20 | readDirectory: () => [], 21 | fileExists: tsModule.sys.fileExists, 22 | }; 23 | return tsModule.parseJsonConfigFileContent(readResult.config, host, (0, path.dirname)(tsConfigPath)).options; 24 | } 25 | let compilerHost; 26 | /** 27 | * Find a module based on it's import 28 | * 29 | * @param importExpr Import used to resolve to a module 30 | * @param filePath 31 | * @param tsConfigPath 32 | */ 33 | function resolveModuleByImport(importExpr, filePath, tsConfigPath) { 34 | compilerHost = compilerHost || getCompilerHost(tsConfigPath); 35 | const { options, host, moduleResolutionCache } = compilerHost; 36 | const { resolvedModule } = tsModule.resolveModuleName(importExpr, filePath, options, host, moduleResolutionCache); 37 | if (!resolvedModule) { 38 | return; 39 | } 40 | else { 41 | return resolvedModule.resolvedFileName.replace(`${normalizedAppRoot}/`, ''); 42 | } 43 | } 44 | function getCompilerHost(tsConfigPath) { 45 | const options = readTsConfigOptions(tsConfigPath); 46 | const host = tsModule.createCompilerHost(options, true); 47 | const moduleResolutionCache = tsModule.createModuleResolutionCache(app_root.appRootPath, host.getCanonicalFileName); 48 | return { options, host, moduleResolutionCache }; 49 | } 50 | 51 | module.exports = { resolveModuleByImport } -------------------------------------------------------------------------------- /core/workspace-projects.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const { statSync, existsSync, readFileSync } = require("fs-extra"); 4 | const { sync } = require("fast-glob"); 5 | 6 | 7 | function findPluginPackageJson(path, plugin) { 8 | while (true) { 9 | if (!path.startsWith(appRootPath)) { 10 | throw new Error("Couldn't find a package.json for Nx plugin:" + plugin); 11 | } 12 | if (existsSync(join(path, 'package.json'))) { 13 | return join(path, 'package.json'); 14 | } 15 | path = path.dirname(path); 16 | } 17 | } 18 | 19 | let nxPluginCache = null; 20 | function loadNxPlugins(plugins) { 21 | return plugins && plugins.length 22 | ? nxPluginCache || 23 | (nxPluginCache = plugins.map((path) => { 24 | const pluginPath = require.resolve(path, { 25 | paths: [appRootPath], 26 | }); 27 | 28 | const { name } = readJsonFile( 29 | findPluginPackageJson(pluginPath, path) 30 | ); 31 | const plugin = require(pluginPath); 32 | plugin.name = name; 33 | 34 | return plugin; 35 | })) 36 | : []; 37 | } 38 | 39 | function mergePluginTargetsWithNxTargets( 40 | projectRoot, 41 | targets, 42 | plugins 43 | ) { 44 | let newTargets = {}; 45 | for (const plugin of plugins) { 46 | if (!plugin.projectFilePatterns && plugin.projectFilePatterns.length || !plugin.registerProjectTargets) { 47 | continue; 48 | } 49 | 50 | const projectFiles = sync(`+(${plugin.projectFilePatterns.join('|')})`, { 51 | cwd: path.join(appRootPath, projectRoot), 52 | }); 53 | for (const projectFile of projectFiles) { 54 | newTargets = { 55 | ...newTargets, 56 | ...plugin.registerProjectTargets(path.join(projectRoot, projectFile)), 57 | }; 58 | } 59 | } 60 | return { ...newTargets, ...targets }; 61 | } 62 | 63 | function readJsonFile(path) { 64 | const content = fs.readFileSync(path, 'utf-8'); 65 | return JSON.parse(content); 66 | } 67 | 68 | function buildTargetFromScript( 69 | script, 70 | nx 71 | ) { 72 | const nxTargetConfiguration = nx && nx.targets && nx.targets[script] || {}; 73 | 74 | return { 75 | ...nxTargetConfiguration, 76 | executor: '@nrwl/workspace:run-script', 77 | options: { 78 | ...(nxTargetConfiguration.options || {}), 79 | script, 80 | }, 81 | }; 82 | } 83 | 84 | function mergeNpmScriptsWithTargets( 85 | projectRoot, 86 | targets 87 | ) { 88 | try { 89 | const { scripts, nx } = readJsonFile( 90 | `${projectRoot}/package.json` 91 | ); 92 | const res = {}; 93 | // handle no scripts 94 | Object.keys(scripts || {}).forEach((script) => { 95 | res[script] = buildTargetFromScript(script, nx); 96 | }); 97 | return { ...res, ...(targets || {}) }; 98 | } catch (e) { 99 | return undefined; 100 | } 101 | } 102 | 103 | function buildWorkspaceProjectNodes( 104 | ctx, 105 | builder, 106 | appRootPath, 107 | ) { 108 | const toAdd = []; 109 | Object.keys(ctx.workspace.projects).forEach((key) => { 110 | const p = ctx.workspace.projects[key]; 111 | const projectRoot = path.join(appRootPath, p.root); 112 | if (existsSync(path.join(projectRoot, 'package.json'))) { 113 | p.targets = mergeNpmScriptsWithTargets(projectRoot, p.targets); 114 | } 115 | p.targets = mergePluginTargetsWithNxTargets( 116 | p.root, 117 | p.targets, 118 | loadNxPlugins(ctx.workspace.plugins) 119 | ); 120 | const projectType = 121 | p.projectType === 'application' 122 | ? key.endsWith('-e2e') 123 | ? 'e2e' 124 | : 'app' 125 | : 'lib'; 126 | const tags = 127 | ctx.workspace.projects && ctx.workspace.projects[key] 128 | ? ctx.workspace.projects[key].tags || [] 129 | : []; 130 | 131 | toAdd.push({ 132 | name: key, 133 | type: projectType, 134 | data: { 135 | ...p, 136 | tags, 137 | files: ctx.fileMap[key], 138 | }, 139 | }); 140 | }); 141 | 142 | // Sort by root directory length (do we need this?) 143 | toAdd.sort((a, b) => { 144 | if (!a.data.root) return -1; 145 | if (!b.data.root) return -1; 146 | return a.data.root.length > b.data.root.length ? -1 : 1; 147 | }); 148 | 149 | toAdd.forEach((n) => { 150 | builder.addNode({ 151 | name: n.name, 152 | type: n.type, 153 | data: n.data, 154 | }); 155 | }); 156 | } 157 | 158 | module.exports = { buildWorkspaceProjectNodes } -------------------------------------------------------------------------------- /core/workspace.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | function readJsonFile(path) { 4 | const content = fs.readFileSync(path, 'utf-8'); 5 | return JSON.parse(content); 6 | } 7 | function toNewFormatOrNull(w) { 8 | let formatted = false; 9 | Object.values(w.projects || {}).forEach((projectConfig) => { 10 | if (projectConfig.architect) { 11 | renamePropertyWithStableKeys(projectConfig, 'architect', 'targets'); 12 | formatted = true; 13 | } 14 | if (projectConfig.schematics) { 15 | renamePropertyWithStableKeys(projectConfig, 'schematics', 'generators'); 16 | formatted = true; 17 | } 18 | Object.values(projectConfig.targets || {}).forEach((target) => { 19 | if (target.builder !== undefined) { 20 | renamePropertyWithStableKeys(target, 'builder', 'executor'); 21 | formatted = true; 22 | } 23 | }); 24 | }); 25 | if (w.schematics) { 26 | renamePropertyWithStableKeys(w, 'schematics', 'generators'); 27 | formatted = true; 28 | } 29 | if (w.version !== 2) { 30 | w.version = 2; 31 | formatted = true; 32 | } 33 | return formatted ? w : null; 34 | } 35 | 36 | // we have to do it this way to preserve the order of properties 37 | // not to screw up the formatting 38 | function renamePropertyWithStableKeys( 39 | obj, 40 | from, 41 | to 42 | ) { 43 | const copy = { ...obj }; 44 | Object.keys(obj).forEach((k) => { 45 | delete obj[k]; 46 | }); 47 | Object.keys(copy).forEach((k) => { 48 | if (k === from) { 49 | obj[to] = copy[k]; 50 | } else { 51 | obj[k] = copy[k]; 52 | } 53 | }); 54 | } 55 | 56 | function inlineProjectConfigurations(w, root) { 57 | Object.entries(w.projects || {}).forEach( 58 | ([project, config]) => { 59 | if (typeof config === 'string') { 60 | const configFilePath = path.join(root, config, 'project.json'); 61 | const fileConfig = readJsonFile(configFilePath); 62 | w.projects[project] = fileConfig; 63 | } 64 | } 65 | ); 66 | return w; 67 | } 68 | 69 | function toNewFormat(w) { 70 | const f = toNewFormatOrNull(w); 71 | // 相当于 return f ?? w; 72 | return f !== null && f !== void 0 ? f : w; 73 | } 74 | 75 | function resolveNewFormatWithInlineProjects( 76 | w, 77 | root, 78 | ) { 79 | return toNewFormat(inlineProjectConfigurations(w, root)); 80 | } 81 | 82 | module.exports = { resolveNewFormatWithInlineProjects } -------------------------------------------------------------------------------- /demo/args.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /demo/args2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 13 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 13 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /hash/README.md: -------------------------------------------------------------------------------- 1 | # nx使用git命令扫描文件hash -------------------------------------------------------------------------------- /hash/hash.js: -------------------------------------------------------------------------------- 1 | const child_process = require("child_process"); 2 | const fs = require("fs"); 3 | const nodePath = require("path"); 4 | 5 | function fileExists(filePath) { 6 | try { 7 | return fs.statSync(filePath).isFile(); 8 | } 9 | catch (err) { 10 | return false; 11 | } 12 | } 13 | 14 | async function getGitHashForFiles(potentialFilesToHash, path) { 15 | const filesToHash = []; 16 | const deleted = []; 17 | for (let file of potentialFilesToHash) { 18 | if (fileExists(nodePath.join(path, file))) { 19 | filesToHash.push(file); 20 | } 21 | else { 22 | deleted.push(file); 23 | } 24 | } 25 | const res = new Map(); 26 | if (filesToHash.length) { 27 | const hashStdout = await spawnProcess('git', ['hash-object', ...filesToHash], path); 28 | const hashes = hashStdout.split('\n').filter((s) => !!s); 29 | if (hashes.length !== filesToHash.length) { 30 | throw new Error(`Passed ${filesToHash.length} file paths to Git to hash, but received ${hashes.length} hashes.`); 31 | } 32 | for (let i = 0; i < hashes.length; i++) { 33 | const hash = hashes[i]; 34 | const filePath = filesToHash[i]; 35 | res.set(filePath, hash); 36 | } 37 | } 38 | return { hashes: res, deleted }; 39 | } 40 | 41 | async function spawnProcess(command, args, cwd) { 42 | const cp = child_process.spawn(command, args, { 43 | windowsHide: true, 44 | shell: false, 45 | cwd, 46 | }); 47 | let stdout = ''; 48 | for await (const data of cp.stdout) { 49 | stdout += data; 50 | } 51 | return stdout; 52 | } 53 | 54 | async function getStagedFiles(path) { 55 | const staged = await spawnProcess('git', ['ls-files', '-s', '-z', '--exclude-standard', '.'], path); 56 | const res = new Map(); 57 | for (let line of staged.split('\0')) { 58 | if (!line) 59 | continue; 60 | const [_, hash, __, ...fileParts] = line.split(/\s/); 61 | const fileName = fileParts.join(' '); 62 | res.set(fileName, hash); 63 | } 64 | return res; 65 | } 66 | async function getUnstagedFiles(path) { 67 | const unstaged = await spawnProcess('git', ['ls-files', '-m', '-z', '--exclude-standard', '.'], path); 68 | const lines = unstaged.split('\0').filter((f) => !!f); 69 | return getGitHashForFiles(lines, path); 70 | } 71 | async function getUntrackedFiles(path) { 72 | const untracked = await spawnProcess('git', ['ls-files', '--other', '-z', '--exclude-standard', '.'], path); 73 | const lines = untracked.split('\0').filter((f) => !!f); 74 | return getGitHashForFiles(lines, path); 75 | } 76 | async function getFileHashes(path) { 77 | const [staged, unstaged, untracked] = await Promise.all([ 78 | getStagedFiles(path), 79 | getUnstagedFiles(path), 80 | getUntrackedFiles(path), 81 | ]); 82 | unstaged.hashes.forEach((hash, filename) => { 83 | staged.set(filename, hash); 84 | }); 85 | unstaged.deleted.forEach((filename) => { 86 | staged.delete(filename); 87 | }); 88 | untracked.hashes.forEach((hash, filename) => { 89 | staged.set(filename, hash); 90 | }); 91 | return { allFiles: staged }; 92 | } 93 | 94 | (async () => { 95 | const fileHashes = await getFileHashes(__dirname); 96 | console.log(fileHashes); 97 | 98 | const res = []; 99 | fileHashes.allFiles.forEach((hash, file) => { 100 | res.push({ 101 | file, 102 | hash, 103 | }); 104 | }); 105 | console.log(res); 106 | })(); 107 | -------------------------------------------------------------------------------- /nx.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const yargsParser = require("yargs-parser"); 4 | const net = require("net"); 5 | const { existsSync } = require('fs-extra'); 6 | const { getFileHashes, getIgnoredGlobs } = require("./core/git-hash"); 7 | const { createProjectFileMap, updateProjectFileMap } = require("./core/file-map-utils"); 8 | const { ProjectGraphBuilder } = require("./core/project-graph-builder"); 9 | const { buildWorkspaceProjectNodes } = require("./core/workspace-projects"); 10 | const { resolveNewFormatWithInlineProjects } = require("./core/workspace"); 11 | const { buildNpmPackageNodes } = require("./core/npm-packages"); 12 | const { buildExplicitDependencies, jsPluginConfig } = require("./core/build-project-graph"); 13 | const { buildImplicitProjectDependencies } = require("./core/implicit-project-dependencies"); 14 | const { createCache, writeCache } = require("./core/nx-deps-cache"); 15 | const { projectGraphAdapter } = require('./core/project-graph'); 16 | const { getProjects } = require('./core/run-one'); 17 | const { runCommand } = require('./core/run-command'); 18 | 19 | function findWorkspaceRoot(dir) { 20 | if (fs.existsSync(path.join(dir, 'angular.json'))) { 21 | return { type: 'angular', dir }; 22 | } 23 | if (fs.existsSync(path.join(dir, 'nx.json'))) { 24 | return { type: 'nx', dir }; 25 | } 26 | if (path.dirname(dir) === dir) { 27 | return null; 28 | } 29 | return findWorkspaceRoot(path.dirname(dir)); 30 | } 31 | 32 | function readJsonFile(path) { 33 | const content = fs.readFileSync(path, 'utf-8'); 34 | return JSON.parse(content); 35 | } 36 | 37 | function normalizeNxJson(nxJson, projects) { 38 | return nxJson.implicitDependencies 39 | ? Object.assign(Object.assign({}, nxJson), { 40 | implicitDependencies: Object.entries(nxJson.implicitDependencies).reduce((acc, [key, val]) => { 41 | acc[key] = recur(projects, val); 42 | return acc; 43 | }, {}) 44 | }) : nxJson; 45 | } 46 | 47 | function recur(projects, v) { 48 | if (v === '*') { 49 | return projects; 50 | } 51 | else if (Array.isArray(v)) { 52 | return v; 53 | } 54 | else { 55 | return Object.keys(v).reduce((acc, key) => { 56 | acc[key] = recur(projects, v[key]); 57 | return acc; 58 | }, {}); 59 | } 60 | } 61 | 62 | function readCombinedDeps(appRootPath) { 63 | const json = readJsonFile(path.join(appRootPath, 'package.json')); 64 | return { ...json.dependencies, ...json.devDependencies }; 65 | } 66 | 67 | function readRootTsConfig(appRootPath) { 68 | for (const tsConfigName of ['tsconfig.base.json', 'tsconfig.json']) { 69 | const tsConfigPath = path.join(appRootPath, tsConfigName); 70 | if (existsSync(tsConfigPath)) { 71 | return readJsonFile(tsConfigPath); 72 | } 73 | } 74 | } 75 | 76 | function createContext( 77 | workspaceJson, 78 | nxJson, 79 | fileMap, 80 | filesToProcess 81 | ) { 82 | const projects = Object.keys(workspaceJson.projects).reduce((map, projectName) => { 83 | map[projectName] = { 84 | ...workspaceJson.projects[projectName], 85 | }; 86 | return map; 87 | }, {}); 88 | return { 89 | workspace: { 90 | ...workspaceJson, 91 | ...nxJson, 92 | projects, 93 | }, 94 | fileMap, 95 | filesToProcess, 96 | }; 97 | } 98 | 99 | const runOne = [ 100 | 'target', 101 | 'configuration', 102 | 'prod', 103 | 'runner', 104 | 'parallel', 105 | 'max-parallel', 106 | 'exclude', 107 | 'only-failed', 108 | 'help', 109 | 'with-deps', 110 | 'skip-nx-cache', 111 | 'scan', 112 | 'hide-cached-output', 113 | ]; 114 | const runMany = [...runOne, 'projects', 'all']; 115 | const runAffected = [ 116 | ...runOne, 117 | 'untracked', 118 | 'uncommitted', 119 | 'all', 120 | 'base', 121 | 'head', 122 | 'files', 123 | 'plain', 124 | 'select', 125 | ]; 126 | 127 | const ignoreArgs = ['$0', '_']; 128 | 129 | function splitArgsIntoNxArgsAndOverrides(args, mode, options = { printWarnings: true }) { 130 | console.log(args, mode, options); 131 | const nxSpecific = mode === 'run-one' ? runOne : mode === 'run-many' ? runMany : runAffected; 132 | const nxArgs = {}; 133 | const overrides = yargsParser(args._, { 134 | configuration: { 135 | 'strip-dashed': true, 136 | 'dot-notation': false, 137 | }, 138 | }); 139 | args._ = overrides._; 140 | delete overrides._; 141 | Object.entries(args).forEach(([key, value]) => { 142 | if (nxSpecific.includes(key) || key.startsWith('nx-')) { 143 | if (value !== undefined) 144 | nxArgs[key] = value; 145 | } 146 | else if (!ignoreArgs.includes(key)) { 147 | overrides[key] = value; 148 | } 149 | }); 150 | if (!nxArgs.skipNxCache) { 151 | nxArgs.skipNxCache = process.env.NX_SKIP_NX_CACHE === 'true'; 152 | } 153 | return { nxArgs, overrides }; 154 | } 155 | 156 | 157 | // 读取本地 nx.json 和 workspace.json 配置文件 158 | const workspace = findWorkspaceRoot(process.cwd()); 159 | const nxJsonPath = path.join(workspace.dir, 'nx.json'); 160 | const workspaceJsonPath = path.join(workspace.dir, 'workspace.json'); 161 | const nxJson = readJsonFile(nxJsonPath); 162 | const nxDepsJsonPath = path.join(workspace.dir, 'nx-deps.json'); 163 | const nxDepsJson = fs.existsSync(nxDepsJsonPath) ? readJsonFile(nxDepsJsonPath) : null; 164 | // 转换 workspaceJson 的数据 165 | let workspaceJson = readJsonFile(workspaceJsonPath); 166 | // 本质 workspaceJson + project.json 167 | // 读取 workspaceJson 里所有子项目的 project.json 配置,并合并到 workspaceJson 的 projects 属性里面 168 | workspaceJson = resolveNewFormatWithInlineProjects(workspaceJson, workspace.dir); 169 | // 本质 workspaceJson + nx.json 170 | // readWorkspaceConfiguration,将 workspaceJson 和 nx.json 合并 171 | // 这里省略检测 nx.json 配置的合法性 172 | workspaceJson = Object.assign(Object.assign({}, workspaceJson), nxJson); 173 | 174 | 175 | // 这里会有个 parseRunOneOptions 来过滤参数 176 | const args = process.argv.slice(2); 177 | 178 | const parsedArgs = yargsParser(args, { 179 | boolean: ['prod', 'help'], 180 | string: ['configuration', 'project'], 181 | alias: { 182 | c: 'configuration', 183 | }, 184 | configuration: { 185 | 'strip-dashed': true, 186 | }, 187 | }); 188 | 189 | let project; 190 | let target; 191 | let configuration; 192 | if (parsedArgs._[0] === 'run') { 193 | [project, target, configuration] = parsedArgs._[1].split(':'); 194 | parsedArgs._ = parsedArgs._.slice(2); 195 | } 196 | const runOpts = { project, target, configuration, parsedArgs }; 197 | delete parsedArgs['c']; 198 | delete parsedArgs['configuration']; 199 | delete parsedArgs['prod']; 200 | delete parsedArgs['project']; 201 | 202 | // nx 支持的命令 203 | const supportedNxCommands = [ 204 | 'affected', 205 | 'affected:apps', 206 | 'affected:libs', 207 | 'affected:build', 208 | 'affected:test', 209 | 'affected:e2e', 210 | 'affected:dep-graph', 211 | 'affected:graph', 212 | 'affected:lint', 213 | 'print-affected', 214 | 'daemon', 215 | 'dep-graph', 216 | 'graph', 217 | 'format', 218 | 'format:check', 219 | 'format:write', 220 | 'workspace-schematic', 221 | 'workspace-generator', 222 | 'workspace-lint', 223 | 'migrate', 224 | 'report', 225 | 'run-many', 226 | 'connect-to-nx-cloud', 227 | 'clear-cache', 228 | 'reset', 229 | 'list', 230 | 'help', 231 | '--help', 232 | '--version', 233 | ]; 234 | 235 | // 判断是否用内置命令 236 | const running = runOpts !== false; 237 | if (supportedNxCommands.includes(process.argv[2])) { 238 | 239 | } 240 | // 判断是否运行一条命令,例如 nx run xxx 241 | else if (running) { 242 | (async () => { 243 | // 将 parsedArgs (执行命令参数)拆分为 Nx 参数并覆盖,最终所有参数组合成 nxArgs 244 | const { nxArgs, overrides } = splitArgsIntoNxArgsAndOverrides(Object.assign(Object.assign({}, runOpts.parsedArgs), { configuration: runOpts.configuration, target: runOpts.target }), 'run-one'); 245 | 246 | // 使用 git 命令查看变更的文件 247 | const gitResult = await getFileHashes(workspace.dir); 248 | const ignore = getIgnoredGlobs(workspace.dir); 249 | const fileHashes = new Map(); 250 | gitResult.allFiles.forEach((hash, filename) => { 251 | if (!ignore.ignores(filename)) { 252 | fileHashes.set(filename, hash); 253 | } 254 | }); 255 | 256 | const allFileData = (() => { 257 | const res = []; 258 | fileHashes.forEach((hash, file) => { 259 | res.push({ 260 | file, 261 | hash, 262 | }); 263 | }); 264 | res.sort((x, y) => x.file.localeCompare(y.file)); 265 | return res; 266 | })() 267 | 268 | // createProjectFileMap 269 | const projectGraphVersion = '5.0'; 270 | const { projectFileMap, allWorkspaceFiles } = createProjectFileMap(workspaceJson, allFileData); 271 | // 缓存是否启用 272 | const cacheEnabled = process.env.NX_CACHE_PROJECT_GRAPH !== 'false'; 273 | // 判断是否存在 nx_deps.json,如果层级执行过这个文件会被存储起来当缓存,并读取 nx_deps.json 缓存文件 274 | let cache = nxDepsJson; 275 | const normalizedNxJson = normalizeNxJson(nxJson, Object.keys(workspaceJson.projects)); 276 | const packageJsonDeps = readCombinedDeps(workspace.dir); 277 | const rootTsConfig = readRootTsConfig(workspace.dir); 278 | let filesToProcess; 279 | let cachedFileData; 280 | if (cache) { 281 | 282 | } else { 283 | filesToProcess = projectFileMap; 284 | cachedFileData = {}; 285 | } 286 | const context = createContext(workspaceJson, normalizedNxJson, projectFileMap, filesToProcess); 287 | // buildProjectGraphUsingContext 使用作用域构建 projectGraph 288 | const builder = new ProjectGraphBuilder(); 289 | // 创建完 builder 之后,其实最关键是 getUpdatedProjectGraph 290 | // 这里会把 project.json 和 package.json 的命令做合并 291 | // package.json 会默认使用 @nrwl/workspace:run-script 作为 executor 执行器 292 | buildWorkspaceProjectNodes(context, builder, workspace.dir); 293 | buildNpmPackageNodes(builder, workspace.dir); 294 | for (const proj of Object.keys(cachedFileData)) { 295 | for (const f of builder.graph.nodes[proj].data.files) { 296 | const cached = cachedFileData[proj][f.file]; 297 | if (cached && cached.deps) { 298 | f.deps = [...cached.deps]; 299 | } 300 | } 301 | } 302 | // 分析 ts 代码的 import 依赖 303 | await buildExplicitDependencies(jsPluginConfig(nxJson), context, builder); 304 | buildImplicitProjectDependencies(context, builder); 305 | builder.setVersion(projectGraphVersion); 306 | // 创建 projectGraph 307 | let projectGraph = builder.getUpdatedProjectGraph(); 308 | // 缓存 projectGraph 309 | const projectGraphCache = createCache(nxJson, packageJsonDeps, projectGraph, rootTsConfig); 310 | // 写入 nxdeps.json 缓存文件到 .cache 文件夹 311 | if (cacheEnabled) { 312 | writeCache(projectGraphCache); 313 | } 314 | // projectGraph 项目图的向后兼容性适配器 315 | // 这里如果是使用 5 的 nx 版本,但是是 4 版本的写法,会使用 projectGraphCompat5to4 函数转换一次 316 | projectGraph = projectGraphAdapter('5.0', projectGraphVersion, projectGraph); 317 | // 根据项目图配合命令 npm run xxx:xxx 参数寻找出 project.json 中对应的命令 318 | const { projects, projectsMap } = getProjects(projectGraph, runOpts.project); 319 | // 读取 nxJson 和 workspaceJson 作为环境 320 | const env = { nxJson, workspaceJson, workspaceResults: null }; 321 | await runCommand( 322 | projects, 323 | projectGraph, 324 | env, 325 | nxArgs, 326 | overrides, 327 | 'run-one', 328 | runOpts.project 329 | ); 330 | 331 | // const socket = net.connect('./d.sock'); 332 | // socket.on('error', (err) => { 333 | // console.log('socekt error', err); 334 | // }); 335 | 336 | // socket.on('connect', () => { 337 | // socket.write('REQUEST_PROJECT_GRAPH_PAYLOAD'); 338 | // let serializedProjectGraphResult = ''; 339 | // socket.on('data', (data) => { 340 | // serializedProjectGraphResult += data.toString(); 341 | // }); 342 | // socket.on('end', () => { 343 | // try { 344 | // const projectGraphResult = JSON.parse(serializedProjectGraphResult); 345 | // console.log('projectGraphResult', projectGraphResult); 346 | // } 347 | // catch (e) { 348 | // console.log('connect error', e); 349 | // } 350 | // }); 351 | // }); 352 | console.log('end'); 353 | })() 354 | } 355 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "stores", 3 | "affected": { 4 | "defaultBase": "main" 5 | }, 6 | "cli": { 7 | "defaultCollection": "@nrwl/react" 8 | }, 9 | "implicitDependencies": { 10 | "package.json": { 11 | "dependencies": "*", 12 | "devDependencies": "*" 13 | }, 14 | ".eslintrc.json": "*" 15 | }, 16 | "tasksRunnerOptions": { 17 | "default": { 18 | "runner": "./default-tasks-runner.js", 19 | "options": { 20 | "cacheableOperations": ["build", "lint", "test", "e2e"] 21 | } 22 | } 23 | }, 24 | "targetDependencies": { 25 | "build": [ 26 | { 27 | "target": "build", 28 | "projects": "dependencies" 29 | } 30 | ] 31 | }, 32 | "generators": { 33 | "@nrwl/react": { 34 | "application": { 35 | "style": "less", 36 | "linter": "eslint", 37 | "babel": true 38 | }, 39 | "component": { 40 | "style": "less" 41 | }, 42 | "library": { 43 | "style": "less", 44 | "linter": "eslint" 45 | } 46 | } 47 | }, 48 | "defaultProject": "eno" 49 | } 50 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-tutorial", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@nodelib/fs.scandir": { 8 | "version": "2.1.5", 9 | "resolved": "https://mirrors.tencent.com/npm/@nodelib%2ffs.scandir/-/fs.scandir-2.1.5.tgz", 10 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 11 | "requires": { 12 | "@nodelib/fs.stat": "2.0.5", 13 | "run-parallel": "^1.1.9" 14 | } 15 | }, 16 | "@nodelib/fs.stat": { 17 | "version": "2.0.5", 18 | "resolved": "https://mirrors.tencent.com/npm/@nodelib%2ffs.stat/-/fs.stat-2.0.5.tgz", 19 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" 20 | }, 21 | "@nodelib/fs.walk": { 22 | "version": "1.2.8", 23 | "resolved": "https://mirrors.tencent.com/npm/@nodelib%2ffs.walk/-/fs.walk-1.2.8.tgz", 24 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 25 | "requires": { 26 | "@nodelib/fs.scandir": "2.1.5", 27 | "fastq": "^1.6.0" 28 | } 29 | }, 30 | "braces": { 31 | "version": "3.0.2", 32 | "resolved": "https://mirrors.tencent.com/npm/braces/-/braces-3.0.2.tgz", 33 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 34 | "requires": { 35 | "fill-range": "^7.0.1" 36 | } 37 | }, 38 | "chalk": { 39 | "version": "5.0.0", 40 | "resolved": "https://mirrors.tencent.com/npm/chalk/-/chalk-5.0.0.tgz", 41 | "integrity": "sha512-/duVOqst+luxCQRKEo4bNxinsOQtMP80ZYm7mMqzuh5PociNL0PvmHFvREJ9ueYL2TxlHjBcmLCdmocx9Vg+IQ==" 42 | }, 43 | "fast-glob": { 44 | "version": "3.2.11", 45 | "resolved": "https://mirrors.tencent.com/npm/fast-glob/-/fast-glob-3.2.11.tgz", 46 | "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", 47 | "requires": { 48 | "@nodelib/fs.stat": "^2.0.2", 49 | "@nodelib/fs.walk": "^1.2.3", 50 | "glob-parent": "^5.1.2", 51 | "merge2": "^1.3.0", 52 | "micromatch": "^4.0.4" 53 | } 54 | }, 55 | "fastq": { 56 | "version": "1.13.0", 57 | "resolved": "https://mirrors.tencent.com/npm/fastq/-/fastq-1.13.0.tgz", 58 | "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", 59 | "requires": { 60 | "reusify": "^1.0.4" 61 | } 62 | }, 63 | "fill-range": { 64 | "version": "7.0.1", 65 | "resolved": "https://mirrors.tencent.com/npm/fill-range/-/fill-range-7.0.1.tgz", 66 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 67 | "requires": { 68 | "to-regex-range": "^5.0.1" 69 | } 70 | }, 71 | "fs-extra": { 72 | "version": "10.0.0", 73 | "resolved": "https://mirrors.tencent.com/npm/fs-extra/-/fs-extra-10.0.0.tgz", 74 | "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", 75 | "requires": { 76 | "graceful-fs": "^4.2.0", 77 | "jsonfile": "^6.0.1", 78 | "universalify": "^2.0.0" 79 | } 80 | }, 81 | "glob-parent": { 82 | "version": "5.1.2", 83 | "resolved": "https://mirrors.tencent.com/npm/glob-parent/-/glob-parent-5.1.2.tgz", 84 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 85 | "requires": { 86 | "is-glob": "^4.0.1" 87 | } 88 | }, 89 | "graceful-fs": { 90 | "version": "4.2.9", 91 | "resolved": "https://mirrors.tencent.com/npm/graceful-fs/-/graceful-fs-4.2.9.tgz", 92 | "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" 93 | }, 94 | "ignore": { 95 | "version": "5.2.0", 96 | "resolved": "https://mirrors.tencent.com/npm/ignore/-/ignore-5.2.0.tgz", 97 | "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" 98 | }, 99 | "is-extglob": { 100 | "version": "2.1.1", 101 | "resolved": "https://mirrors.tencent.com/npm/is-extglob/-/is-extglob-2.1.1.tgz", 102 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" 103 | }, 104 | "is-glob": { 105 | "version": "4.0.3", 106 | "resolved": "https://mirrors.tencent.com/npm/is-glob/-/is-glob-4.0.3.tgz", 107 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 108 | "requires": { 109 | "is-extglob": "^2.1.1" 110 | } 111 | }, 112 | "is-number": { 113 | "version": "7.0.0", 114 | "resolved": "https://mirrors.tencent.com/npm/is-number/-/is-number-7.0.0.tgz", 115 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" 116 | }, 117 | "jsonc-parser": { 118 | "version": "3.0.0", 119 | "resolved": "https://mirrors.tencent.com/npm/jsonc-parser/-/jsonc-parser-3.0.0.tgz", 120 | "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==" 121 | }, 122 | "jsonfile": { 123 | "version": "6.1.0", 124 | "resolved": "https://mirrors.tencent.com/npm/jsonfile/-/jsonfile-6.1.0.tgz", 125 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", 126 | "requires": { 127 | "graceful-fs": "^4.1.6", 128 | "universalify": "^2.0.0" 129 | } 130 | }, 131 | "merge2": { 132 | "version": "1.4.1", 133 | "resolved": "https://mirrors.tencent.com/npm/merge2/-/merge2-1.4.1.tgz", 134 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" 135 | }, 136 | "micromatch": { 137 | "version": "4.0.4", 138 | "resolved": "https://mirrors.tencent.com/npm/micromatch/-/micromatch-4.0.4.tgz", 139 | "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", 140 | "requires": { 141 | "braces": "^3.0.1", 142 | "picomatch": "^2.2.3" 143 | } 144 | }, 145 | "picomatch": { 146 | "version": "2.3.1", 147 | "resolved": "https://mirrors.tencent.com/npm/picomatch/-/picomatch-2.3.1.tgz", 148 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" 149 | }, 150 | "queue-microtask": { 151 | "version": "1.2.3", 152 | "resolved": "https://mirrors.tencent.com/npm/queue-microtask/-/queue-microtask-1.2.3.tgz", 153 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" 154 | }, 155 | "reusify": { 156 | "version": "1.0.4", 157 | "resolved": "https://mirrors.tencent.com/npm/reusify/-/reusify-1.0.4.tgz", 158 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" 159 | }, 160 | "run-parallel": { 161 | "version": "1.2.0", 162 | "resolved": "https://mirrors.tencent.com/npm/run-parallel/-/run-parallel-1.2.0.tgz", 163 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 164 | "requires": { 165 | "queue-microtask": "^1.2.2" 166 | } 167 | }, 168 | "to-regex-range": { 169 | "version": "5.0.1", 170 | "resolved": "https://mirrors.tencent.com/npm/to-regex-range/-/to-regex-range-5.0.1.tgz", 171 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 172 | "requires": { 173 | "is-number": "^7.0.0" 174 | } 175 | }, 176 | "typescript": { 177 | "version": "4.5.5", 178 | "resolved": "https://mirrors.tencent.com/npm/typescript/-/typescript-4.5.5.tgz", 179 | "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==" 180 | }, 181 | "universalify": { 182 | "version": "2.0.0", 183 | "resolved": "https://mirrors.tencent.com/npm/universalify/-/universalify-2.0.0.tgz", 184 | "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" 185 | }, 186 | "yargs-parser": { 187 | "version": "21.0.0", 188 | "resolved": "https://mirrors.tencent.com/npm/yargs-parser/-/yargs-parser-21.0.0.tgz", 189 | "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==" 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "nx.js", 6 | "scripts": { 7 | "start": "node ./nx.js run core:test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Wscats/monorepo-tutorial.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/Wscats/monorepo-tutorial/issues" 17 | }, 18 | "homepage": "https://github.com/Wscats/monorepo-tutorial#readme", 19 | "dependencies": { 20 | "chalk": "^5.0.0", 21 | "fast-glob": "^3.2.11", 22 | "fs-extra": "^10.0.0", 23 | "ignore": "^5.2.0", 24 | "jsonc-parser": "^3.0.0", 25 | "typescript": "^4.5.5", 26 | "yargs-parser": "^21.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /screenshots/forkProcessPipeOutputCapture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/monorepo-tutorial/7485cee56110fb2d24d01bd500fe6f5706460004/screenshots/forkProcessPipeOutputCapture.png -------------------------------------------------------------------------------- /screenshots/pipelines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/monorepo-tutorial/7485cee56110fb2d24d01bd500fe6f5706460004/screenshots/pipelines.png -------------------------------------------------------------------------------- /socket/README.md: -------------------------------------------------------------------------------- 1 | # nx的sockcet服务器 -------------------------------------------------------------------------------- /socket/client.js: -------------------------------------------------------------------------------- 1 | const net = require("net"); 2 | 3 | const socket = net.connect('./d.sock'); 4 | 5 | socket.on('error', (err) => { 6 | console.log('socekt error', err); 7 | }); 8 | 9 | socket.on('connect', () => { 10 | socket.write('REQUEST_PROJECT_GRAPH_PAYLOAD'); 11 | let serializedProjectGraphResult = ''; 12 | socket.on('data', (data) => { 13 | serializedProjectGraphResult += data.toString(); 14 | }); 15 | socket.on('end', () => { 16 | try { 17 | const projectGraphResult = JSON.parse(serializedProjectGraphResult); 18 | console.log('projectGraphResult', projectGraphResult); 19 | } 20 | catch (e) { 21 | console.log('connect error', e); 22 | } 23 | }); 24 | }); 25 | 26 | 27 | -------------------------------------------------------------------------------- /socket/daemon.log: -------------------------------------------------------------------------------- 1 | Debugger attached. 2 | Waiting for the debugger to disconnect... 3 | internal/modules/cjs/loader.js:905 4 | throw err; 5 | ^ 6 | 7 | Error: Cannot find module '/Users/eno/Documents/GitHub/monorepo-tutorial/server.js' 8 | at Function.Module._resolveFilename (internal/modules/cjs/loader.js:902:15) 9 | at Function.Module._load (internal/modules/cjs/loader.js:746:27) 10 | at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:75:12) 11 | at internal/main/run_main_module.js:17:47 { 12 | code: 'MODULE_NOT_FOUND', 13 | requireStack: [] 14 | } 15 | Debugger attached. 16 | Started listening 17 | -------------------------------------------------------------------------------- /socket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "nx.js", 6 | "scripts": { 7 | "start": "node start" 8 | } 9 | } -------------------------------------------------------------------------------- /socket/server.js: -------------------------------------------------------------------------------- 1 | const { createServer, Server, Socket } = require("net"); 2 | const { unlinkSync } = require('fs'); 3 | 4 | const FULL_OS_SOCKET_PATH = './d.sock'; 5 | const server = createServer(async (socket) => { 6 | socket.on('data', async (data) => { 7 | // const payload = data.toString(); 8 | // const result = await getCachedSerializedProjectGraphPromise(); 9 | // const serializedResult = serializeResult( 10 | // result.error, 11 | // result.serializedProjectGraph 12 | // ); 13 | serializedResult = JSON.stringify({ test: 'eno' }); 14 | socket.write(serializedResult, () => { 15 | socket.end(); 16 | }); 17 | }); 18 | }); 19 | 20 | async function startServer() { 21 | return new Promise((resolve) => { 22 | // 要监听套接字 d.sock 23 | server.listen(FULL_OS_SOCKET_PATH, async () => { 24 | console.log(`Started listening`); 25 | return resolve(server); 26 | }); 27 | 28 | }); 29 | }; 30 | 31 | async function stopServer() { 32 | return new Promise((resolve, reject) => { 33 | server.close((err) => { 34 | if (err) { 35 | if (!err.message.startsWith('Server is not running')) { 36 | return reject(err); 37 | } 38 | } 39 | unlinkSync(FULL_OS_SOCKET_PATH); 40 | return resolve(); 41 | }); 42 | }); 43 | } 44 | 45 | function isServerAvailable() { 46 | return new Promise((resolve) => { 47 | try { 48 | const socket = socket('./d.sock', () => { 49 | socket.destroy(); 50 | resolve(true); 51 | }); 52 | socket.once('error', () => { 53 | resolve(false); 54 | }); 55 | } 56 | catch (err) { 57 | resolve(false); 58 | } 59 | }); 60 | } 61 | 62 | (async () => { 63 | stopServer(); 64 | try { 65 | const isEnable = await isServerAvailable(); 66 | await !isEnable && startServer(); 67 | } catch (err) { 68 | console.error('Something unexpected went wrong when starting the server'); 69 | process.exit(1); 70 | } 71 | })(); 72 | -------------------------------------------------------------------------------- /socket/start.js: -------------------------------------------------------------------------------- 1 | const child_process = require("child_process"); 2 | const fs = require("fs"); 3 | 4 | // 记录终端输出日志 5 | const out = fs.openSync('./daemon.log', 'a'); 6 | const err = fs.openSync('./daemon.log', 'a'); 7 | 8 | const backgroundProcess = child_process.spawn(process.execPath, ['./server.js'], { 9 | cwd: __dirname, 10 | stdio: ['ignore', out, err], 11 | detached: true, 12 | windowsHide: true, 13 | shell: false, 14 | }); 15 | 16 | backgroundProcess.unref(); -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # nx的使用子进程运行子项目 -------------------------------------------------------------------------------- /test/child/index.js: -------------------------------------------------------------------------------- 1 | console.log('child') -------------------------------------------------------------------------------- /test/child/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "child", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "node index" 7 | } 8 | } -------------------------------------------------------------------------------- /test/child/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "test/child", 3 | "targets": { 4 | "start": { 5 | "executor": "./run-script", 6 | "options": { 7 | "script": "test" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const args = process.argv.slice(2); 4 | const [command, ...commandArgs] = args; 5 | 6 | switch (command) { 7 | case 'help': 8 | case '--help': 9 | break; 10 | case 'run': 11 | case 'r': 12 | // child: test ↓ 13 | // project = child; target = test 14 | const [project, target] = commandArgs[0].split(':'); 15 | // 根据 project 参数知道要去 child 目录下寻找 package.json 和 /project.json 16 | // 读取 package.json 17 | const packageJson = fs.readFileSync(path.join(__dirname, `${project}/package.json`), 'utf-8').toString(); 18 | // 读取 project.json 19 | const projectJson = fs.readFileSync(path.join(__dirname, `${project}/project.json`), 'utf-8').toString(); 20 | // 解析 package.json 21 | const { scripts } = JSON.parse(packageJson); 22 | // 解析 project.json 23 | const { targets } = JSON.parse(projectJson); 24 | let mergePackageAndProjectScripts = {}; 25 | // 遍历所有的命令,如果自定义了执行器则 require 对应的执行器执行命令 26 | // 否则默认都是使用 run-script 执行器,还有 package.json 的命令默认都是使用 run-script 执行器 27 | Object.keys(scripts || {}).forEach((script) => { 28 | mergePackageAndProjectScripts[script] = { 29 | // 默认的执行器 30 | executor: './run-script', 31 | // 命令参数 32 | options: { 33 | script, 34 | }, 35 | }; 36 | }); 37 | // 合并 project.json(nx) 和 package.json(npm) 的 scripts 命令 38 | mergePackageAndProjectScripts = { ...mergePackageAndProjectScripts, ...(targets || {}) }; 39 | // 获取 run-script 执行器 40 | const module = require(mergePackageAndProjectScripts[target]['executor']); 41 | // 使用 run-script 执行器运行命令 42 | // option: 命令信息和带有的参数信息,告诉 run-script 具体执行那个命令,并且使用了那些具体参数 43 | // context: 作用域信息,可用于传递文件夹位置,项目目录,cli版本信息等 44 | module.default(mergePackageAndProjectScripts[target]['options'], { 45 | // 执行命令的项目位置 46 | project 47 | // 如果命令足够复杂,还可以传递更多的信息 48 | }); 49 | break; 50 | } 51 | -------------------------------------------------------------------------------- /test/nx.js: -------------------------------------------------------------------------------- 1 | // node nx run child:test 2 | // process.argv[0] => node 3 | // process.argv[1] => nx 4 | // process.argv[2] => run 5 | // process.argv[3] => child:test 6 | const child_process = require("child_process"); 7 | const path = require("path"); 8 | // 用于衍生新的 Node.js 子进程 9 | // 注意:子进程独立于父进程 10 | // 除了两者之间建立的 IPC 通信通道 11 | // 每个进程都有自己的内存,具有自己的 V8 实例 12 | // 由于需要额外的资源分配,不建议衍生大量子 Node.js 进程 13 | const command = process.argv[2]; 14 | const commandArgs = process.argv[3]; 15 | const child = child_process.fork( 16 | path.join(__dirname, 'cli'), 17 | // 添加参数给子进程 18 | // 将命令和参数传递到 cli.js 处理 19 | // 相当于运行 node cli run child:test 20 | [command, commandArgs] 21 | ); 22 | child.on('error', (err) => { 23 | // 如果控制器中止,则这将在 err 为 AbortError 的情况下被调用 24 | }); 25 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "nx.js", 6 | "scripts": { 7 | "start": "node nx run child:test" 8 | } 9 | } -------------------------------------------------------------------------------- /test/run-script.js: -------------------------------------------------------------------------------- 1 | const child_process = require("child_process"); 2 | const path = require("path"); 3 | module.exports = { 4 | default: (options, context) => { 5 | const script = options.script; 6 | // 解析出命令之后,删除 script 属性,方便后面解析其参数信息 7 | delete options.script; 8 | // 解析命令参数 9 | const args = []; 10 | Object.keys(options).forEach((r) => { 11 | args.push(`--${r}=${options[r]}`); 12 | }); 13 | // 执行命令 14 | child_process.execSync({ 15 | install: 'npm install', 16 | add: 'npm install', 17 | addDev: 'npm install -D', 18 | rm: 'npm rm', 19 | exec: 'npx', 20 | run: (script, args) => `npm run ${script} -- ${args}`, 21 | list: 'npm ls', 22 | }.run(script, args), { 23 | stdio: ['inherit', 'inherit', 'inherit'], 24 | // 在 child 目录,运行 npm run test 25 | cwd: path.join(__dirname, context.project), 26 | }); 27 | } 28 | } -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@stores/demoapp2": ["libs/demoapp2/src/index.ts"] 19 | } 20 | }, 21 | "exclude": ["node_modules", "tmp"] 22 | } 23 | -------------------------------------------------------------------------------- /workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "projects": { 4 | "core": "core" 5 | } 6 | } --------------------------------------------------------------------------------