@leyman/nx-plugin 0.0.0-stage → 0.0.1

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/package.json CHANGED
@@ -1,6 +1,89 @@
1
1
  {
2
2
  "name": "@leyman/nx-plugin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.1",
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.0.1",
70
+ "@leyman/configs": "^0.0.1",
71
+ "@leyman/eslint-config": "^0.0.1",
72
+ "@leyman/expect": "^0.0.1",
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
+ }