@leyman/nx-plugin 0.0.1 → 0.2.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/README.md CHANGED
@@ -136,10 +136,12 @@ nx g @leyman/nx-plugin:init
136
136
 
137
137
  Sets the workspace up for the plugin, leaving alone anything already set:
138
138
 
139
- - `nx.json`: the plugin, and [nx-lifecycle](https://www.npmjs.com/package/nx-lifecycle)'s plugin with the stages install, prepare (generate, format), check (lint), build (run, post, sync), test (compile, run, report) and verify, and each target bound to one, under the name the options give it. `--lifecycle=false` leaves nx-lifecycle out.
140
- - `devDependencies` of the root `package.json`: the plugin, `@leyman/configs`, `@biomejs/biome`, `@leyman/check-package`, `@nx/js` and `nx-lifecycle`.
139
+ - `nx.json`: the plugin, with `checkPackage.profile` `esm-only` for the packages the `package` generator writes, and [nx-lifecycle](https://www.npmjs.com/package/nx-lifecycle)'s plugin with the stages install, prepare (generate, format), check (lint), build (run, post, sync), test (compile, run, report) and verify, and each target bound to one, under the name the options give it. As every target is bound before any project has it, nx-lifecycle's `warnUnusedBindings` is off. `--lifecycle=false` leaves nx-lifecycle out.
140
+ - `devDependencies` of the root `package.json`: the plugin, `@leyman/configs`, `@biomejs/biome`, `@leyman/check-package`, `@nx/js`, `@typescript/native` (TypeScript 7, the `tsc` the targets run) and `nx-lifecycle`.
141
141
  - `biome.json`, extending `@leyman/configs/biome.base.json`.
142
- - `pnpm-workspace.yaml`, if there is one: `injectWorkspacePackages: true` and `dedupeInjectedDeps: false`, so dependents get workspace packages as published, which `sync-injected` keeps current.
142
+ - `tsconfig.json`, with no files, for Nx's TypeScript sync generator to keep a reference to each package in.
143
+ - `.gitignore`: `node_modules`, `dist`, `dist-test`, and what the targets and Nx write at the root (`.coverage`, `.package-surface`, `.nx/cache` and `.nx/workspace-data`). check-package reads it for the files a package doesn't publish.
144
+ - `pnpm-workspace.yaml`, if there is one: `injectWorkspacePackages: true` and `dedupeInjectedDeps: false`, so dependents get workspace packages as published, which `sync-injected` keeps current, and `allowBuilds` `false` for `nx` and `@swc/core`, whose install scripts pnpm otherwise stops on.
143
145
  - `.claude/skills/principles/SKILL.md`, pointing coding agents at the [principles](https://jacobley.github.io/leyman/framework/principles) of the setup. `--skill=false` leaves it out.
144
146
 
145
147
  Then it installs the dependencies, unless `--skipInstall`.
@@ -179,7 +181,7 @@ Set the ones a workspace always passes as defaults in `nx.json`:
179
181
  }
180
182
  ```
181
183
 
182
- Each dependency's version comes from the workspace if it can: `workspace:^` for one of its own packages, `catalog:` for one in the pnpm catalog (`catalog:node-{floor}` for a published package's `@types/node`), or the range another package already uses. Otherwise it is the version this plugin was published with, and a package with none of those fails.
184
+ Each dependency's version comes from the workspace if it can: `workspace:^` for one of its own packages, `catalog:` for one in the pnpm catalog (`catalog:node-{floor}` for a published package's `@types/node`), or the range another package (or the root `package.json`) already uses. Nx's own plugins, such as `@nx/js`, otherwise take the range of `nx`. Otherwise it is the version this plugin was published with, and a package with none of those fails.
183
185
 
184
186
  ## The targets executor
185
187
 
@@ -68,8 +68,9 @@ export declare const lifecycleBindings: {
68
68
  };
69
69
  /**
70
70
  * Nx generator setting an Nx workspace up for the plugin: adds it (and nx-lifecycle, with stages and bindings for its
71
- * targets) to `nx.json`, installs it and the tools its targets run at the root, extends the shared Biome config, has
72
- * pnpm inject workspace packages, and points coding agents at the principles. Run by `nx add @leyman/nx-plugin`.
71
+ * targets) to `nx.json`, installs it and the tools its targets run at the root, extends the shared Biome config, writes
72
+ * a root `tsconfig.json`, has pnpm inject workspace packages and skip install scripts the targets don't need, ignores
73
+ * what the targets write in `.gitignore`, and points coding agents at the principles. Run by `nx add @leyman/nx-plugin`.
73
74
  * Leaves alone what is already set.
74
75
  *
75
76
  * @param tree - Nx's view of the files
@@ -1,5 +1,5 @@
1
1
  import { installPackagesTask, readJson, readNxJson, writeJson } from '@nx/devkit';
2
- import { parse } from 'yaml';
2
+ import { parseDocument, stringify } from 'yaml';
3
3
  import { resolveOptions } from '../lib/options.js';
4
4
  import { name as pluginName } from '../lib/plugin.js';
5
5
  import { assertOptions } from '../lib/schema.js';
@@ -76,7 +76,12 @@ const rootDependencies = [
76
76
  '@biomejs/biome',
77
77
  '@leyman/check-package',
78
78
  '@leyman/configs',
79
- '@nx/js'
79
+ '@nx/js',
80
+ "@typescript/native"
81
+ ];
82
+ const skippedBuilds = [
83
+ '@swc/core',
84
+ 'nx'
80
85
  ];
81
86
  const principlesSkill = `---
82
87
  name: principles
@@ -95,25 +100,73 @@ injectWorkspacePackages: true
95
100
  const dedupeSettings = `# Otherwise pnpm symlinks to the source again whenever peers allow it
96
101
  dedupeInjectedDeps: false
97
102
  `;
98
- const injectWorkspacePackages = (tree)=>{
103
+ const ignoredPaths = [
104
+ 'node_modules',
105
+ 'dist',
106
+ 'dist-test',
107
+ '/.coverage',
108
+ '/.package-surface',
109
+ '/.nx/cache',
110
+ '/.nx/workspace-data'
111
+ ];
112
+ const ignoreOutputs = (tree)=>{
113
+ const gitignore = tree.read('.gitignore', 'utf8') ?? '';
114
+ const lines = new Set(gitignore.split('\n').map((line)=>line.trim()));
115
+ const missing = ignoredPaths.filter((path)=>!lines.has(path));
116
+ if (missing.length === 0) {
117
+ return;
118
+ }
119
+ const separator = gitignore === '' || gitignore.endsWith('\n') ? '' : '\n';
120
+ const added = missing.map((path)=>`${path}\n`).join('');
121
+ tree.write('.gitignore', `${gitignore}${separator}${added}`);
122
+ };
123
+ const configurePnpm = (tree)=>{
99
124
  const yaml = tree.read('pnpm-workspace.yaml', 'utf8');
100
125
  if (yaml === null) {
101
126
  return;
102
127
  }
103
- const settings = parse(yaml) ?? {};
128
+ const document = parseDocument(yaml);
129
+ const missingBuilds = skippedBuilds.filter((name)=>!document.hasIn([
130
+ 'allowBuilds',
131
+ name
132
+ ]));
133
+ const hasAllowBuilds = document.has('allowBuilds');
134
+ let updated = yaml;
135
+ if (hasAllowBuilds && missingBuilds.length > 0) {
136
+ for (const name of missingBuilds){
137
+ document.setIn([
138
+ 'allowBuilds',
139
+ name
140
+ ], false);
141
+ }
142
+ updated = document.toString({
143
+ singleQuote: true
144
+ });
145
+ }
146
+ const allowBuilds = Object.fromEntries(missingBuilds.map((name)=>[
147
+ name,
148
+ false
149
+ ]));
104
150
  const added = [
105
- ...Object.hasOwn(settings, 'injectWorkspacePackages') ? [] : [
151
+ ...document.has('injectWorkspacePackages') ? [] : [
106
152
  injectSettings
107
153
  ],
108
- ...Object.hasOwn(settings, 'dedupeInjectedDeps') ? [] : [
154
+ ...document.has('dedupeInjectedDeps') ? [] : [
109
155
  dedupeSettings
156
+ ],
157
+ ...hasAllowBuilds ? [] : [
158
+ `# Install scripts of the packages the targets run, which they don't need\n${stringify({
159
+ allowBuilds
160
+ }, {
161
+ singleQuote: true
162
+ })}`
110
163
  ]
111
164
  ];
112
- if (added.length === 0) {
165
+ if (updated === yaml && added.length === 0) {
113
166
  return;
114
167
  }
115
- const separator = yaml === '' || yaml.endsWith('\n') ? '' : '\n';
116
- tree.write('pnpm-workspace.yaml', `${yaml}${separator}${added.join('')}`);
168
+ const separator = updated === '' || updated.endsWith('\n') ? '' : '\n';
169
+ tree.write('pnpm-workspace.yaml', `${updated}${separator}${added.join('')}`);
117
170
  };
118
171
  const initGenerator = async (tree, options = {})=>{
119
172
  const { lifecycle = true, skill = true } = options;
@@ -125,7 +178,11 @@ const initGenerator = async (tree, options = {})=>{
125
178
  if (existing === undefined) {
126
179
  plugins.push({
127
180
  plugin: pluginName,
128
- options: {}
181
+ options: {
182
+ checkPackage: {
183
+ profile: 'esm-only'
184
+ }
185
+ }
129
186
  });
130
187
  }
131
188
  const pluginOptions = typeof existing === 'object' ? existing.options ?? {} : {};
@@ -145,7 +202,8 @@ const initGenerator = async (tree, options = {})=>{
145
202
  plugin: lifecyclePlugin,
146
203
  options: {
147
204
  stages: lifecycleStages,
148
- bindings: Object.fromEntries(bindings)
205
+ bindings: Object.fromEntries(bindings),
206
+ warnUnusedBindings: false
149
207
  }
150
208
  });
151
209
  }
@@ -178,7 +236,14 @@ const initGenerator = async (tree, options = {})=>{
178
236
  ]
179
237
  });
180
238
  }
181
- injectWorkspacePackages(tree);
239
+ if (!tree.exists('tsconfig.json')) {
240
+ writeJson(tree, 'tsconfig.json', {
241
+ files: [],
242
+ references: []
243
+ });
244
+ }
245
+ configurePnpm(tree);
246
+ ignoreOutputs(tree);
182
247
  const skillPath = '.claude/skills/principles/SKILL.md';
183
248
  if (skill && !tree.exists(skillPath)) {
184
249
  tree.write(skillPath, principlesSkill);
@@ -30,7 +30,8 @@ export interface WorkspaceVersions {
30
30
  export declare const readWorkspaceVersions: (tree: Tree) => Promise<WorkspaceVersions>;
31
31
  /**
32
32
  * The range to depend on a package by: `workspace:^` for one of the workspace's own, else `catalog:` from the default
33
- * catalog, else a range another workspace package uses, else the version this plugin was published with.
33
+ * catalog, else a range a workspace package uses, else, for one of Nx's own plugins (`@nx/*`), the range of `nx`,
34
+ * else the version this plugin was published with.
34
35
  *
35
36
  * @param name - the package
36
37
  * @param versions - from {@link readWorkspaceVersions}
@@ -13,13 +13,15 @@ const ownVersions = async ()=>{
13
13
  export const readWorkspaceVersions = async (tree)=>{
14
14
  const packages = new Set();
15
15
  const ranges = new Map();
16
- for (const { root } of getProjects(tree).values()){
16
+ const roots = new Set(getProjects(tree).values().map(({ root })=>root));
17
+ roots.add('.');
18
+ for (const root of roots){
17
19
  const path = joinPathFragments(root, 'package.json');
18
20
  if (!tree.exists(path)) {
19
21
  continue;
20
22
  }
21
23
  const packageJson = readJson(tree, path);
22
- if (packageJson.name !== undefined) {
24
+ if (root !== '.' && packageJson.name !== undefined) {
23
25
  packages.add(packageJson.name);
24
26
  }
25
27
  const entries = [
@@ -29,7 +31,7 @@ export const readWorkspaceVersions = async (tree)=>{
29
31
  packageJson.peerDependencies
30
32
  ].flatMap((field)=>field === undefined ? [] : Object.entries(field));
31
33
  for (const [name, range] of entries){
32
- if (!ranges.has(name) && !range.startsWith('workspace:')) {
34
+ if (!ranges.has(name) && !isProtocol(range)) {
33
35
  ranges.set(name, range);
34
36
  }
35
37
  }
@@ -55,7 +57,7 @@ export const resolveVersion = (name, versions)=>{
55
57
  if (versions.catalogs.default?.[name] !== undefined) {
56
58
  return 'catalog:';
57
59
  }
58
- const range = versions.ranges.get(name) ?? versions.fallback[name];
60
+ const range = versions.ranges.get(name) ?? (name.startsWith('@nx/') ? versions.ranges.get('nx') ?? versions.catalogs.default?.nx : undefined) ?? versions.fallback[name];
59
61
  if (range === undefined) {
60
62
  throw new Error(`No version of ${name} in the workspace: add it to the pnpm catalog or to a package first`);
61
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leyman/nx-plugin",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
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
5
  "engines": {
6
6
  "node": ">=22.13.0"
@@ -66,10 +66,10 @@
66
66
  },
67
67
  "devDependencies": {
68
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",
69
+ "@leyman/check-package": "^0.1.0",
70
+ "@leyman/configs": "^0.1.0",
71
+ "@leyman/eslint-config": "^0.1.1",
72
+ "@leyman/expect": "^0.1.0",
73
73
  "@nx/js": "^23.3.1",
74
74
  "@swc/cli": "^0.8.1",
75
75
  "@swc/core": "^1.16.20",
@@ -82,7 +82,7 @@
82
82
  "mocha": "^12.0.3",
83
83
  "mocha-chain": "^1.1.0",
84
84
  "nx": "^23.3.1",
85
- "nx-lifecycle": "^1.2.2",
85
+ "nx-lifecycle": "^1.3.0",
86
86
  "typescript": "npm:@typescript/typescript6@^6.0.2",
87
87
  "vitest": "^5.0.3"
88
88
  }