├── .gitignore
├── .nxignore
├── hash
├── README.md
└── hash.js
├── socket
├── README.md
├── package.json
├── start.js
├── daemon.log
├── client.js
└── server.js
├── test
├── README.md
├── child
│ ├── index.js
│ ├── package.json
│ └── project.json
├── package.json
├── nx.js
├── run-script.js
└── cli.js
├── .gitattributes
├── workspace.json
├── screenshots
├── pipelines.png
└── forkProcessPipeOutputCapture.png
├── core
├── package.json
├── run-one.js
├── type.js
├── path.js
├── implicit-project-dependencies.js
├── npm-packages.js
├── app-root.js
├── project.json
├── project-graph.js
├── devkit.js
├── static-run-one-terminal-output-life-cycle.js
├── life-cycle.js
├── nx-deps-cache.js
├── default-tasks-runner.js
├── typescript.js
├── workspace.js
├── run-command.js
├── file-map-utils.js
├── strip-source-code.js
├── git-hash.js
├── workspace-projects.js
├── typescript-import-locator.js
├── output.js
├── project-graph-builder.js
├── target-project-locator.js
└── build-project-graph.js
├── README.md
├── tsconfig.base.json
├── package.json
├── demo
├── args.html
├── args2.html
└── index.html
├── nx.json
├── .cache
└── nxdeps.json
└── nx.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.nxignore:
--------------------------------------------------------------------------------
1 | workspace.json
--------------------------------------------------------------------------------
/hash/README.md:
--------------------------------------------------------------------------------
1 | # nx使用git命令扫描文件hash
--------------------------------------------------------------------------------
/socket/README.md:
--------------------------------------------------------------------------------
1 | # nx的sockcet服务器
--------------------------------------------------------------------------------
/test/README.md:
--------------------------------------------------------------------------------
1 | # nx的使用子进程运行子项目
--------------------------------------------------------------------------------
/test/child/index.js:
--------------------------------------------------------------------------------
1 | console.log('child')
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/workspace.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "projects": {
4 | "core": "core"
5 | }
6 | }
--------------------------------------------------------------------------------
/screenshots/pipelines.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wscats/monorepo-tutorial/HEAD/screenshots/pipelines.png
--------------------------------------------------------------------------------
/screenshots/forkProcessPipeOutputCapture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wscats/monorepo-tutorial/HEAD/screenshots/forkProcessPipeOutputCapture.png
--------------------------------------------------------------------------------
/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "core",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "start": "echo 123"
7 | }
8 | }
--------------------------------------------------------------------------------
/test/child/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "child",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "test": "node index"
7 | }
8 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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/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/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 }
--------------------------------------------------------------------------------
/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();
--------------------------------------------------------------------------------
/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 };
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/demo/args.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Document
9 |
10 |
11 |
12 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/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 }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 }
--------------------------------------------------------------------------------
/demo/args2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Document
9 |
10 |
11 |
12 |
13 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/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/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/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/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;
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Document
9 |
10 |
11 |
12 |
13 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/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 }
--------------------------------------------------------------------------------
/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 }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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 }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------