@leyman/nx-plugin 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -2
- package/dist/executors/targets.d.ts +30 -0
- package/dist/executors/targets.js +31 -0
- package/dist/generators/init.d.ts +82 -0
- package/dist/generators/init.js +190 -0
- package/dist/generators/package.d.ts +79 -0
- package/dist/generators/package.js +208 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +12 -0
- package/dist/lib/infer.d.ts +45 -0
- package/dist/lib/infer.js +66 -0
- package/dist/lib/options.d.ts +154 -0
- package/dist/lib/options.js +75 -0
- package/dist/lib/plugin.d.ts +12 -0
- package/dist/lib/plugin.js +38 -0
- package/dist/lib/projects.d.ts +27 -0
- package/dist/lib/projects.js +44 -0
- package/dist/lib/schema.d.ts +83 -0
- package/dist/lib/schema.js +255 -0
- package/dist/lib/sync.d.ts +31 -0
- package/dist/lib/sync.js +109 -0
- package/dist/lib/targets.d.ts +17 -0
- package/dist/lib/targets.js +266 -0
- package/dist/lib/versions.d.ts +42 -0
- package/dist/lib/versions.js +65 -0
- package/executors.json +9 -0
- package/generators.json +14 -0
- package/out/executors/targets/schema.json +22 -0
- package/out/generators/init/schema.json +25 -0
- package/out/generators/package/schema.json +79 -0
- package/out/schema.json +624 -0
- package/package.json +86 -3
- package/scripts/biome-format.sh +10 -0
- package/scripts/clean-dist.sh +26 -0
- package/scripts/coverage-report.sh +22 -0
- package/scripts/swc-compile.sh +51 -0
- package/scripts/sync-injected.mjs +143 -0
package/package.json
CHANGED
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leyman/nx-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Nx plugin inferring the build, test, lint and package-check targets of TypeScript ESM packages: SWC and tsc builds, Mocha or Vitest under c8, ESLint, Biome and check-package.",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.13.0"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"author": "JacobLey",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/JacobLey/leyman.git",
|
|
14
|
+
"directory": "leyman/nx-plugin"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/JacobLey/leyman/tree/main/leyman/nx-plugin#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/JacobLey/leyman/issues"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"nx",
|
|
22
|
+
"nx-plugin",
|
|
23
|
+
"monorepo",
|
|
24
|
+
"typescript",
|
|
25
|
+
"swc",
|
|
26
|
+
"mocha",
|
|
27
|
+
"vitest",
|
|
28
|
+
"c8",
|
|
29
|
+
"eslint",
|
|
30
|
+
"biome",
|
|
31
|
+
"targets"
|
|
32
|
+
],
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"executors": "./executors.json",
|
|
35
|
+
"generators": "./generators.json",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": "./dist/index.js",
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"bin": {
|
|
41
|
+
"leyman-biome-format": "./scripts/biome-format.sh",
|
|
42
|
+
"leyman-clean-dist": "./scripts/clean-dist.sh",
|
|
43
|
+
"leyman-coverage-report": "./scripts/coverage-report.sh",
|
|
44
|
+
"leyman-swc-compile": "./scripts/swc-compile.sh",
|
|
45
|
+
"leyman-sync-injected": "./scripts/sync-injected.mjs"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"!dist/**/*.map",
|
|
50
|
+
"!dist/tsconfig.tsbuildinfo",
|
|
51
|
+
"!dist/file-content.*",
|
|
52
|
+
"executors.json",
|
|
53
|
+
"generators.json",
|
|
54
|
+
"out",
|
|
55
|
+
"!out/.generated.json",
|
|
56
|
+
"scripts"
|
|
57
|
+
],
|
|
58
|
+
"imports": {
|
|
59
|
+
"#internal/*": "./dist/*"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@nx/devkit": "^23.3.1",
|
|
63
|
+
"juniper": "^2.7.0",
|
|
64
|
+
"juniper-validator": "^0.6.0",
|
|
65
|
+
"yaml": "^2.9.1"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@biomejs/biome": "^2.6.4",
|
|
69
|
+
"@leyman/check-package": "^0.1.0",
|
|
70
|
+
"@leyman/configs": "^0.1.0",
|
|
71
|
+
"@leyman/eslint-config": "^0.1.0",
|
|
72
|
+
"@leyman/expect": "^0.1.0",
|
|
73
|
+
"@nx/js": "^23.3.1",
|
|
74
|
+
"@swc/cli": "^0.8.1",
|
|
75
|
+
"@swc/core": "^1.16.20",
|
|
76
|
+
"@types/mocha": "^10.0.10",
|
|
77
|
+
"@types/node": "^22.20.5",
|
|
78
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
79
|
+
"c8": "^12.0.0",
|
|
80
|
+
"eslint": "^10.13.0",
|
|
81
|
+
"load-populate-files": "^1.1.5",
|
|
82
|
+
"mocha": "^12.0.3",
|
|
83
|
+
"mocha-chain": "^1.1.0",
|
|
84
|
+
"nx": "^23.3.1",
|
|
85
|
+
"nx-lifecycle": "^1.2.2",
|
|
86
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
87
|
+
"vitest": "^5.0.3"
|
|
88
|
+
}
|
|
6
89
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
# Formatting is part of `prepare`, so it rewrites source by default.
|
|
6
|
+
# In CI it only checks, failing if anything was committed unformatted.
|
|
7
|
+
case "${CI:-false}" in
|
|
8
|
+
false | 0 | "") biome format --write . ;;
|
|
9
|
+
*) biome format . ;;
|
|
10
|
+
esac
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Clears ./dist before a build, but keeps what lets `tsc` skip unchanged files: its build info and the declarations.
|
|
4
|
+
# `tsc` trusts the build info without checking its outputs, so a declaration deleted beside a kept build info is
|
|
5
|
+
# never written again, and the build still passes. So they stay together, only while the declarations are exactly
|
|
6
|
+
# those of the current sources. Any difference, such as a deleted source's leftover, clears everything.
|
|
7
|
+
|
|
8
|
+
set -e
|
|
9
|
+
|
|
10
|
+
if [ ! -d dist ]; then
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
expected=$(find src -type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' \) \
|
|
15
|
+
! -name '*.d.ts' ! -name '*.d.mts' ! -name '*.d.cts' |
|
|
16
|
+
sed -E 's#^src/##; s#\.tsx?$#.d.ts#; s#\.mts$#.d.mts#; s#\.cts$#.d.cts#' | LC_ALL=C sort)
|
|
17
|
+
actual=$(find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' \) |
|
|
18
|
+
sed -E 's#^dist/##' | LC_ALL=C sort)
|
|
19
|
+
|
|
20
|
+
if [ -f dist/tsconfig.tsbuildinfo ] && [ "$expected" = "$actual" ]; then
|
|
21
|
+
find dist -type f ! -name tsconfig.tsbuildinfo \
|
|
22
|
+
! -name '*.d.ts' ! -name '*.d.mts' ! -name '*.d.cts' \
|
|
23
|
+
! -name '*.d.ts.map' ! -name '*.d.mts.map' ! -name '*.d.cts.map' -delete
|
|
24
|
+
else
|
|
25
|
+
rm -rf dist
|
|
26
|
+
fi
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Checks a project's coverage from its test targets: leyman-coverage-report <c8rc> [c8 options...].
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
config_file=$1
|
|
8
|
+
shift
|
|
9
|
+
|
|
10
|
+
COVERAGE_DIR="$NX_WORKSPACE_ROOT/.coverage/project/$NX_TASK_TARGET_PROJECT"
|
|
11
|
+
COVERAGE_TMP="$COVERAGE_DIR/report-tmp"
|
|
12
|
+
|
|
13
|
+
# Each test target writes to its own sub-directory (so they can be cached independently),
|
|
14
|
+
# but c8 only reads the top level of its temp directory, so flatten into a single directory.
|
|
15
|
+
rm -rf "$COVERAGE_TMP"
|
|
16
|
+
mkdir -p "$COVERAGE_TMP"
|
|
17
|
+
find "$COVERAGE_DIR/tmp" -maxdepth 2 -name "*.json" -exec cp {} "$COVERAGE_TMP/" \;
|
|
18
|
+
|
|
19
|
+
# lcov paths are from the workspace root: Coverage Gutters matches a file by the end of its path, so
|
|
20
|
+
# `src/index.ts`, relative to the package, would show this package's coverage on every package's index.ts.
|
|
21
|
+
c8 --temp-directory="$COVERAGE_TMP" -o "$COVERAGE_DIR/report" -c "$config_file" \
|
|
22
|
+
--reporter-options.lcovonly.projectRoot="$NX_WORKSPACE_ROOT" report --all --check-coverage --merge-async "$@"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Compiles a directory of TypeScript with SWC: leyman-swc-compile <source> <output> <swcrc>.
|
|
4
|
+
# `.mts` and `.cts` need a pass each, for their extension and module type. Starting SWC costs over 100ms,
|
|
5
|
+
# and few packages have either, so those passes only run when there is a file for them.
|
|
6
|
+
#
|
|
7
|
+
# The target follows the oldest Node.js the package supports: the major version of `engines.node` in the current
|
|
8
|
+
# directory's package.json. A floor older than the running Node.js compiles to that version's ES year, so the output
|
|
9
|
+
# runs there; otherwise, or without `engines`, the target is `esnext`.
|
|
10
|
+
|
|
11
|
+
set -e
|
|
12
|
+
|
|
13
|
+
source_dir=$1
|
|
14
|
+
output_dir=$2
|
|
15
|
+
config_file=$3
|
|
16
|
+
|
|
17
|
+
target=$(node -e '
|
|
18
|
+
const { readFileSync } = require("node:fs");
|
|
19
|
+
const range = JSON.parse(readFileSync("package.json", "utf8")).engines?.node;
|
|
20
|
+
const floor = Number(/^>=\s*(\d+)/v.exec(range ?? "")?.[1]);
|
|
21
|
+
// SWC has no target past es2024 yet
|
|
22
|
+
const targets = { 22: "es2024" };
|
|
23
|
+
if (range === undefined || floor >= Number(process.versions.node.split(".")[0])) {
|
|
24
|
+
console.log("esnext");
|
|
25
|
+
} else if (floor in targets) {
|
|
26
|
+
console.log(targets[floor]);
|
|
27
|
+
} else {
|
|
28
|
+
console.error(`leyman-swc-compile: no target for engines.node "${range}"; add its floor to the table`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
')
|
|
32
|
+
|
|
33
|
+
compile() {
|
|
34
|
+
swc "$source_dir" -d "$output_dir" --config-file "$config_file" \
|
|
35
|
+
-C "jsc.target=$target" --strip-leading-paths "$@"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
has_files() {
|
|
39
|
+
[ -n "$(find "$source_dir" \( -name "*.$1" -o -name "*.${1}x" \) -print -quit)" ]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
compile --only '**/*.ts?(x)'
|
|
43
|
+
|
|
44
|
+
if has_files mts; then
|
|
45
|
+
compile --only '**/*.mts?(x)' --out-file-extension mjs
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
if has_files cts; then
|
|
49
|
+
compile -C module.type=commonjs -C module.ignoreDynamic=true -C module.exportInteropAnnotation=true \
|
|
50
|
+
--only '**/*.cts?(x)' --out-file-extension cjs
|
|
51
|
+
fi
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Workspace dependencies are injected (`injectWorkspacePackages`): each dependent gets a copy of the
|
|
4
|
+
// package's published files in the virtual store, made at install time. Those copies don't follow
|
|
5
|
+
// later builds, or Nx restoring `dist/` from cache, so mirror the package into them after every build.
|
|
6
|
+
//
|
|
7
|
+
// Declaration maps aren't published, but each copy gets one pointing back at this package's source,
|
|
8
|
+
// so editors' go-to-definition opens the workspace source instead of the copied `.d.ts`.
|
|
9
|
+
//
|
|
10
|
+
// Never cached: dependents' cache keys hash this package's build output, so they must never run against a stale copy.
|
|
11
|
+
//
|
|
12
|
+
// Also writes the fields of package.json that can affect a dependent, which dependents hash in place of
|
|
13
|
+
// the file, so a version bump or a new description doesn't rerun them. Inert fields are listed rather than
|
|
14
|
+
// consumed ones, so a field nobody thought about still invalidates.
|
|
15
|
+
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import {
|
|
18
|
+
copyFileSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
rmdirSync,
|
|
23
|
+
rmSync,
|
|
24
|
+
statSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
|
|
29
|
+
const workspaceRoot = process.env.NX_WORKSPACE_ROOT;
|
|
30
|
+
const projectRoot = process.cwd();
|
|
31
|
+
|
|
32
|
+
const inertFields = new Set([
|
|
33
|
+
'author',
|
|
34
|
+
'bugs',
|
|
35
|
+
'description',
|
|
36
|
+
'devDependencies',
|
|
37
|
+
'homepage',
|
|
38
|
+
'keywords',
|
|
39
|
+
'license',
|
|
40
|
+
'repository',
|
|
41
|
+
'scripts',
|
|
42
|
+
'version',
|
|
43
|
+
]);
|
|
44
|
+
const packageJson = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
|
|
45
|
+
const surface = Object.fromEntries(
|
|
46
|
+
Object.entries(packageJson).filter(([field]) => !inertFields.has(field))
|
|
47
|
+
);
|
|
48
|
+
const surfacePath = path.join(
|
|
49
|
+
workspaceRoot,
|
|
50
|
+
'.package-surface',
|
|
51
|
+
process.env.NX_TASK_TARGET_PROJECT,
|
|
52
|
+
'package-surface.json'
|
|
53
|
+
);
|
|
54
|
+
mkdirSync(path.dirname(surfacePath), { recursive: true });
|
|
55
|
+
writeFileSync(surfacePath, `${JSON.stringify(surface, null, 2)}\n`);
|
|
56
|
+
|
|
57
|
+
// Exactly the files pnpm would publish, and so inject
|
|
58
|
+
const { name, files } = JSON.parse(
|
|
59
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path -- Run the workspace's pnpm, like Nx itself
|
|
60
|
+
execFileSync('pnpm', ['pack', '--dry-run', '--json'], { encoding: 'utf8' })
|
|
61
|
+
);
|
|
62
|
+
const packed = new Set(files.map(file => file.path));
|
|
63
|
+
|
|
64
|
+
// Each copy is named `<name>@file+<path>` by pnpm, with `/` replaced by `+`, plus a suffix for its resolved peers
|
|
65
|
+
const prefix = `${name.replace('/', '+')}@file+${path.relative(workspaceRoot, projectRoot).replaceAll(path.sep, '+')}`;
|
|
66
|
+
const virtualStore = path.join(workspaceRoot, 'node_modules', '.pnpm');
|
|
67
|
+
const copies = readdirSync(virtualStore)
|
|
68
|
+
.filter(dir => dir === prefix || dir.startsWith(`${prefix}_`))
|
|
69
|
+
.map(dir => path.join(virtualStore, dir, 'node_modules', name));
|
|
70
|
+
|
|
71
|
+
const isSameFile = (source, target) => {
|
|
72
|
+
try {
|
|
73
|
+
return (
|
|
74
|
+
statSync(source).size === statSync(target).size &&
|
|
75
|
+
readFileSync(source).equals(readFileSync(target))
|
|
76
|
+
);
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const declarationMaps = [...packed]
|
|
83
|
+
.filter(file => /\.d\.[cm]?ts$/v.test(file))
|
|
84
|
+
.map(file => `${file}.map`)
|
|
85
|
+
.filter(file => {
|
|
86
|
+
try {
|
|
87
|
+
return statSync(path.join(projectRoot, file)).isFile();
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
const expected = new Set([...packed, ...declarationMaps]);
|
|
93
|
+
|
|
94
|
+
// Map sources are relative to the map, so re-resolve them from the copy back to this package
|
|
95
|
+
const redirectDeclarationMap = (source, target) => {
|
|
96
|
+
const map = JSON.parse(readFileSync(source, 'utf8'));
|
|
97
|
+
const sourceDir = path.resolve(path.dirname(source), map.sourceRoot ?? '');
|
|
98
|
+
map.sourceRoot = '';
|
|
99
|
+
map.sources = map.sources.map(file =>
|
|
100
|
+
path.relative(path.dirname(target), path.resolve(sourceDir, file)).replaceAll(path.sep, '/')
|
|
101
|
+
);
|
|
102
|
+
const content = JSON.stringify(map);
|
|
103
|
+
try {
|
|
104
|
+
if (readFileSync(target, 'utf8') === content) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// No map there yet: write it
|
|
109
|
+
}
|
|
110
|
+
writeFileSync(target, content);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
for (const copy of copies) {
|
|
114
|
+
const entries = readdirSync(copy, { recursive: true, withFileTypes: true });
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
const file = path.relative(copy, path.join(entry.parentPath, entry.name));
|
|
117
|
+
if (!entry.isDirectory() && !expected.has(file)) {
|
|
118
|
+
rmSync(path.join(copy, file));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Deepest first, so a directory emptied by removing its children is removed too
|
|
122
|
+
const dirs = readdirSync(copy, { recursive: true, withFileTypes: true })
|
|
123
|
+
.filter(entry => entry.isDirectory())
|
|
124
|
+
.map(entry => path.join(entry.parentPath, entry.name))
|
|
125
|
+
.toSorted((a, b) => b.length - a.length);
|
|
126
|
+
for (const dir of dirs) {
|
|
127
|
+
if (readdirSync(dir).length === 0) {
|
|
128
|
+
rmdirSync(dir);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const file of packed) {
|
|
132
|
+
const source = path.join(projectRoot, file);
|
|
133
|
+
const target = path.join(copy, file);
|
|
134
|
+
if (isSameFile(source, target)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
138
|
+
copyFileSync(source, target);
|
|
139
|
+
}
|
|
140
|
+
for (const file of declarationMaps) {
|
|
141
|
+
redirectDeclarationMap(path.join(projectRoot, file), path.join(copy, file));
|
|
142
|
+
}
|
|
143
|
+
}
|