@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.
@@ -0,0 +1,44 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import Path from 'node:path';
3
+ import { inferencePaths } from './infer.js';
4
+ export const readJson = async (path)=>JSON.parse(await readFile(path, 'utf8'));
5
+ const isPresent = async (path)=>{
6
+ try {
7
+ await access(path);
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ };
13
+ const readJsonIfPresent = async (path)=>await isPresent(path) ? readJson(path) : undefined;
14
+ export const loadProjects = async (workspaceRoot, roots)=>{
15
+ const loaded = await Promise.all(roots.map(async (root)=>{
16
+ const projectRoot = Path.join(workspaceRoot, root);
17
+ const [projectJson, packageJson, present] = await Promise.all([
18
+ readJsonIfPresent(Path.join(projectRoot, 'project.json')),
19
+ readJsonIfPresent(Path.join(projectRoot, 'package.json')),
20
+ Promise.all(inferencePaths.map(async (path)=>isPresent(Path.join(projectRoot, path))))
21
+ ]);
22
+ return {
23
+ root: root.replaceAll(Path.sep, '/'),
24
+ present: new Set(inferencePaths.filter((_path, i)=>present[i] === true)),
25
+ projectJson: projectJson,
26
+ packageJson: packageJson
27
+ };
28
+ }));
29
+ const dependencies = new Set(loaded.flatMap(({ packageJson })=>[
30
+ packageJson?.dependencies,
31
+ packageJson?.devDependencies,
32
+ packageJson?.optionalDependencies,
33
+ packageJson?.peerDependencies
34
+ ].flatMap((field)=>Object.keys(field ?? {}))));
35
+ return loaded.map(({ packageJson, present, projectJson, root })=>({
36
+ packageJson,
37
+ projectJson,
38
+ root,
39
+ isDependency: packageJson?.name !== undefined && dependencies.has(packageJson.name),
40
+ has: (path)=>present.has(path)
41
+ }));
42
+ };
43
+
44
+ //# sourceMappingURL=projects.js.map
@@ -0,0 +1,83 @@
1
+ import type { NxPluginOptions } from './options.js';
2
+ /**
3
+ * JSON schema of the options, for `nx.json` or the file the `targets` executor reads.
4
+ */
5
+ export declare const optionsJsonSchema: import("juniper").JsonSchema<{
6
+ targets?: {
7
+ [x: string]: false | {
8
+ name?: string;
9
+ include?: string[];
10
+ exclude?: string[];
11
+ } | undefined;
12
+ };
13
+ configs?: {
14
+ swc?: string;
15
+ c8?: string;
16
+ vitest?: string;
17
+ };
18
+ scripts?: string;
19
+ coverage?: {
20
+ branches?: number;
21
+ functions?: number;
22
+ lines?: number;
23
+ statements?: number;
24
+ };
25
+ checkPackage?: {
26
+ profile?: "esm-only" | "node16" | "strict";
27
+ floorTsconfig?: string | false;
28
+ inputs?: ((string | import("juniper").EmptyObject) & (string | import("juniper").EmptyObject | undefined))[];
29
+ };
30
+ inputs?: {
31
+ configs?: ((string | import("juniper").EmptyObject) & (string | import("juniper").EmptyObject | undefined))[];
32
+ tools?: ((string | import("juniper").EmptyObject) & (string | import("juniper").EmptyObject | undefined))[];
33
+ };
34
+ namedInputs?: {
35
+ [x: string]: ((string | import("juniper").EmptyObject) & (string | import("juniper").EmptyObject | undefined))[] | undefined;
36
+ };
37
+ $schema?: string;
38
+ }>;
39
+ /**
40
+ * JSON schema of the `targets` executor's options.
41
+ */
42
+ export declare const executorJsonSchema: import("juniper").JsonSchema<{
43
+ config: string;
44
+ check?: boolean;
45
+ }>;
46
+ /**
47
+ * Major of the oldest Node.js a package the `package` generator adds supports, unless told otherwise.
48
+ */
49
+ export declare const defaultFloor = 22;
50
+ /**
51
+ * JSON schema of the `package` generator's options.
52
+ */
53
+ export declare const packageGeneratorJsonSchema: import("juniper").JsonSchema<{
54
+ directory: string;
55
+ name?: string;
56
+ description: string;
57
+ published?: boolean;
58
+ floor?: number;
59
+ test?: string;
60
+ repository?: string;
61
+ author?: string;
62
+ license?: string;
63
+ devDependencies?: string[];
64
+ config?: string;
65
+ skipInstall?: boolean;
66
+ }>;
67
+ /**
68
+ * JSON schema of the `init` generator's options.
69
+ */
70
+ export declare const initGeneratorJsonSchema: import("juniper").JsonSchema<{
71
+ lifecycle?: boolean;
72
+ skill?: boolean;
73
+ skipInstall?: boolean;
74
+ }>;
75
+ /**
76
+ * Asserts the plugin options are valid, with each problem in the error's message.
77
+ *
78
+ * @param options - the options, from `nx.json` or a file
79
+ * @param where - where they are from, for the message
80
+ * @throws {Error} naming each invalid option
81
+ */
82
+ export declare const assertOptions: (options: unknown, where: string) => asserts options is NxPluginOptions;
83
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1,255 @@
1
+ import { arraySchema, booleanSchema, enumSchema, mergeSchema, numberSchema, objectSchema, stringSchema } from 'juniper';
2
+ import { makeValidator } from 'juniper-validator';
3
+ import { namedInputNames, targetNames } from './options.js';
4
+ const inputsSchema = arraySchema(mergeSchema().oneOf([
5
+ stringSchema({
6
+ description: 'A file pattern, or the name of a named input'
7
+ }),
8
+ objectSchema({
9
+ description: 'An input object, such as `{ "externalDependencies": ["pkg"] }`'
10
+ })
11
+ ]));
12
+ const rootsSchema = arraySchema(stringSchema({
13
+ description: 'Root of a project, e.g. `packages/a`'
14
+ }));
15
+ const targetSchema = mergeSchema().oneOf([
16
+ enumSchema({
17
+ enum: [
18
+ false
19
+ ],
20
+ description: 'Infer the target nowhere'
21
+ }),
22
+ objectSchema({
23
+ properties: {
24
+ name: stringSchema({
25
+ description: "Name of the target. Defaults to the target's own"
26
+ }),
27
+ include: rootsSchema.description('Projects that get the target whatever their files'),
28
+ exclude: rootsSchema.description('Projects that never get the target')
29
+ },
30
+ additionalProperties: false
31
+ })
32
+ ]);
33
+ const maxPercent = 100;
34
+ const percentSchema = numberSchema({
35
+ minimum: 0,
36
+ maximum: maxPercent
37
+ });
38
+ const optionsProperties = {
39
+ targets: objectSchema({
40
+ description: "Each target's options, or `false` to infer it nowhere",
41
+ properties: Object.fromEntries(targetNames.map((name)=>[
42
+ name,
43
+ targetSchema
44
+ ])),
45
+ additionalProperties: false
46
+ }),
47
+ configs: objectSchema({
48
+ description: 'Paths of the shared configs, relative to each project',
49
+ properties: {
50
+ swc: stringSchema({
51
+ default: 'node_modules/@leyman/configs/swcrc.jsonc'
52
+ }),
53
+ c8: stringSchema({
54
+ default: 'node_modules/@leyman/configs/c8rc.json'
55
+ }),
56
+ vitest: stringSchema({
57
+ default: 'node_modules/@leyman/configs/vitest.config.js'
58
+ })
59
+ },
60
+ additionalProperties: false
61
+ }),
62
+ scripts: stringSchema({
63
+ description: "Directory to run the plugin's scripts from, instead of the package's bins (`leyman-swc-compile`...)"
64
+ }),
65
+ coverage: objectSchema({
66
+ description: "Coverage thresholds `coverage-report` passes to c8, over the c8 config's",
67
+ properties: {
68
+ branches: percentSchema,
69
+ functions: percentSchema,
70
+ lines: percentSchema,
71
+ statements: percentSchema
72
+ },
73
+ additionalProperties: false
74
+ }),
75
+ checkPackage: objectSchema({
76
+ description: 'Options of `check-package`',
77
+ properties: {
78
+ profile: enumSchema({
79
+ enum: [
80
+ 'esm-only',
81
+ 'node16',
82
+ 'strict'
83
+ ],
84
+ description: 'The module resolutions every entry point must work under'
85
+ }),
86
+ floorTsconfig: mergeSchema().oneOf([
87
+ stringSchema({
88
+ description: "The config a package's `tsconfig.json` must extend, with `{major}` for its support floor",
89
+ default: '@leyman/configs/tsconfig.node-{major}.json'
90
+ }),
91
+ enumSchema({
92
+ enum: [
93
+ false
94
+ ],
95
+ description: 'Not checked'
96
+ })
97
+ ]),
98
+ inputs: inputsSchema.description('What a change of check-package itself is hashed by')
99
+ },
100
+ additionalProperties: false
101
+ }),
102
+ inputs: objectSchema({
103
+ description: 'What changes of the shared tools are hashed by',
104
+ properties: {
105
+ configs: inputsSchema.description('The shared configs, for every target that reads one'),
106
+ tools: inputsSchema.description("This plugin's scripts, for every target that runs one")
107
+ },
108
+ additionalProperties: false
109
+ }),
110
+ namedInputs: objectSchema({
111
+ description: 'Inputs added to the end of each named input, such as a file the tests read',
112
+ properties: Object.fromEntries(namedInputNames.map((name)=>[
113
+ name,
114
+ inputsSchema
115
+ ])),
116
+ additionalProperties: false
117
+ })
118
+ };
119
+ const optionsSchema = objectSchema({
120
+ title: '@leyman/nx-plugin',
121
+ description: 'Infers the targets of TypeScript ESM packages',
122
+ properties: optionsProperties,
123
+ additionalProperties: false
124
+ });
125
+ export const optionsJsonSchema = optionsSchema.properties({
126
+ $schema: stringSchema({
127
+ description: 'Path or URL of this JSON schema'
128
+ })
129
+ }).toJSON({
130
+ schema: true
131
+ });
132
+ const executorSchema = objectSchema({
133
+ title: 'targets',
134
+ description: "Writes the plugin's named inputs and target defaults into nx.json, and the targets each project gets into its project.json",
135
+ properties: {
136
+ config: stringSchema({
137
+ description: 'Path of the file holding the plugin options, relative to the workspace root'
138
+ }),
139
+ check: booleanSchema({
140
+ description: 'Fail rather than write when a file is out of date. Defaults to true in CI'
141
+ })
142
+ },
143
+ required: [
144
+ 'config'
145
+ ],
146
+ additionalProperties: false
147
+ });
148
+ export const executorJsonSchema = executorSchema.metadata({
149
+ version: 1,
150
+ outputCapture: 'direct-nodejs',
151
+ cli: 'nx'
152
+ }).toJSON();
153
+ export const defaultFloor = 22;
154
+ const packageGeneratorSchema = objectSchema({
155
+ title: 'package',
156
+ description: 'Adds a TypeScript ESM package, with a project.json declaring the targets the plugin infers',
157
+ properties: {
158
+ directory: stringSchema({
159
+ description: 'Where the package goes, relative to the workspace root'
160
+ }).metadata({
161
+ $default: {
162
+ $source: 'argv',
163
+ index: 0
164
+ },
165
+ 'x-prompt': 'Which directory should the package go in?'
166
+ }),
167
+ name: stringSchema({
168
+ description: "Name of the package. Defaults to the directory's last segment"
169
+ }),
170
+ description: stringSchema({
171
+ description: "Description, for package.json and the README"
172
+ }).metadata('x-prompt', 'What does the package do?'),
173
+ published: booleanSchema({
174
+ description: 'Published to npm, rather than private',
175
+ default: false
176
+ }),
177
+ floor: numberSchema({
178
+ type: 'integer',
179
+ description: 'Major of the oldest Node.js a published package supports',
180
+ default: defaultFloor
181
+ }),
182
+ test: enumSchema({
183
+ enum: [
184
+ 'mocha',
185
+ 'vitest',
186
+ 'none'
187
+ ],
188
+ description: 'Test runner, or none',
189
+ default: 'mocha'
190
+ }),
191
+ repository: stringSchema({
192
+ description: "URL of the workspace's GitHub repository, for a published package's repository, homepage and bugs"
193
+ }),
194
+ author: stringSchema({
195
+ description: 'author of package.json'
196
+ }),
197
+ license: stringSchema({
198
+ description: "license of package.json. A published package also gets a copy of the workspace's LICENSE"
199
+ }),
200
+ devDependencies: arraySchema(stringSchema()).description('More packages to add to devDependencies'),
201
+ config: stringSchema({
202
+ description: 'File of the plugin options, relative to the workspace root, for a workspace running the targets executor. Defaults to the options in nx.json'
203
+ }),
204
+ skipInstall: booleanSchema({
205
+ description: "Don't install dependencies afterwards",
206
+ default: false
207
+ })
208
+ },
209
+ required: [
210
+ 'directory',
211
+ "description"
212
+ ],
213
+ additionalProperties: false
214
+ });
215
+ export const packageGeneratorJsonSchema = packageGeneratorSchema.metadata('cli', 'nx').toJSON({
216
+ schema: true
217
+ });
218
+ const initGeneratorSchema = objectSchema({
219
+ title: 'init',
220
+ description: 'Sets an Nx workspace up for the plugin: nx.json, root devDependencies, the Biome config, pnpm injection and the principles skill',
221
+ properties: {
222
+ lifecycle: booleanSchema({
223
+ description: "Wire the targets' order with nx-lifecycle",
224
+ default: true
225
+ }),
226
+ skill: booleanSchema({
227
+ description: 'Write a .claude/skills/principles/SKILL.md pointing coding agents at the principles behind the setup',
228
+ default: true
229
+ }),
230
+ skipInstall: booleanSchema({
231
+ description: "Don't install dependencies afterwards",
232
+ default: false
233
+ })
234
+ },
235
+ additionalProperties: false
236
+ });
237
+ export const initGeneratorJsonSchema = initGeneratorSchema.metadata('cli', 'nx').toJSON({
238
+ schema: true
239
+ });
240
+ const optionsValidator = makeValidator(optionsJsonSchema);
241
+ export const assertOptions = (options, where)=>{
242
+ const { issues } = optionsValidator.validate(options);
243
+ if (issues === undefined) {
244
+ return;
245
+ }
246
+ throw new Error([
247
+ `Invalid @leyman/nx-plugin options in ${where}:`,
248
+ ...issues.map(({ message, path = [] })=>{
249
+ const at = path.map((key)=>`/${String(key)}`).join('');
250
+ return `- options${at} ${message}`;
251
+ })
252
+ ].join('\n'));
253
+ };
254
+
255
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A project of the workspace, as Nx knows it.
3
+ */
4
+ export interface WorkspaceProject {
5
+ readonly name: string;
6
+ /**
7
+ * Relative to the workspace root.
8
+ */
9
+ readonly root: string;
10
+ }
11
+ /**
12
+ * Writes the plugin's named inputs and target defaults into `nx.json`, and the targets each project gets into its
13
+ * `project.json`, for a workspace that runs the plugin's targets without loading it as a plugin.
14
+ *
15
+ * @param params - what to update, and how
16
+ * @param params.root - the workspace root
17
+ * @param params.projects - every project of the workspace
18
+ * @param params.options - plugin options, not yet validated
19
+ * @param params.optionsSource - where the options are from, for errors
20
+ * @param params.check - fail rather than write when a file is out of date
21
+ * @returns the path of each file written, or that would be
22
+ * @rejects {Error} when the options are invalid, or a file is out of date in `check` mode
23
+ */
24
+ export declare const syncTargets: ({ root, projects, options, optionsSource, check, }: {
25
+ root: string;
26
+ projects: readonly WorkspaceProject[];
27
+ options: unknown;
28
+ optionsSource: string;
29
+ check: boolean;
30
+ }) => Promise<string[]>;
31
+ //# sourceMappingURL=sync.d.ts.map
@@ -0,0 +1,109 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import Path from 'node:path';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import { inferTargets } from './infer.js';
5
+ import { namedInputNames, resolveOptions, targetNames } from './options.js';
6
+ import { loadProjects, readJson } from './projects.js';
7
+ import { assertOptions } from './schema.js';
8
+ import { namedInputs, targetConfigurations } from './targets.js';
9
+ const updateKeys = (object, updates, place = (_key, keys)=>keys.at(-1))=>{
10
+ const keys = Object.keys(object);
11
+ for (const [key, value] of updates){
12
+ if (value === undefined || keys.includes(key)) {
13
+ continue;
14
+ }
15
+ const after = place(key, keys);
16
+ keys.splice(after === undefined ? 0 : keys.indexOf(after) + 1, 0, key);
17
+ }
18
+ const entries = keys.map((key)=>[
19
+ key,
20
+ updates.has(key) ? updates.get(key) : object[key]
21
+ ]);
22
+ return Object.fromEntries(entries.filter(([, value])=>value !== undefined));
23
+ };
24
+ const updateNxJson = (nxJson, options)=>{
25
+ const inputs = namedInputs(options);
26
+ const configurations = targetConfigurations(options);
27
+ const targetDefaults = nxJson.targetDefaults ?? {};
28
+ const defaults = new Map();
29
+ for (const target of targetNames){
30
+ const targetOptions = options.targets[target];
31
+ if (targetOptions === false) {
32
+ continue;
33
+ }
34
+ const { dependsOn } = targetDefaults[targetOptions.name] ?? {};
35
+ defaults.set(targetOptions.name, {
36
+ ...configurations[target],
37
+ ...dependsOn !== undefined && {
38
+ dependsOn
39
+ }
40
+ });
41
+ }
42
+ const ownNamedInputs = new Map(namedInputNames.map((name)=>[
43
+ name,
44
+ inputs[name]
45
+ ]));
46
+ return updateKeys(nxJson, new Map([
47
+ [
48
+ 'namedInputs',
49
+ updateKeys(nxJson.namedInputs ?? {}, ownNamedInputs)
50
+ ],
51
+ [
52
+ 'targetDefaults',
53
+ updateKeys(targetDefaults, defaults)
54
+ ]
55
+ ]));
56
+ };
57
+ const updateProjectJson = (projectJson, inferred, options)=>{
58
+ const targets = projectJson.targets ?? {};
59
+ const names = targetNames.map((target)=>{
60
+ const targetOptions = options.targets[target];
61
+ return targetOptions === false ? target : targetOptions.name;
62
+ });
63
+ const updates = new Map();
64
+ for (const [i, target] of targetNames.entries()){
65
+ const name = names[i];
66
+ const current = targets[name];
67
+ if (inferred.includes(target)) {
68
+ updates.set(name, current ?? {});
69
+ } else if (isDeepStrictEqual(current, {})) {
70
+ updates.set(name, undefined);
71
+ }
72
+ }
73
+ return updateKeys(projectJson, new Map([
74
+ [
75
+ 'targets',
76
+ updateKeys(targets, updates, (key, keys)=>names.slice(0, names.indexOf(key)).findLast((name)=>keys.includes(name)))
77
+ ]
78
+ ]));
79
+ };
80
+ export const syncTargets = async ({ root, projects, options, optionsSource, check })=>{
81
+ assertOptions(options, optionsSource);
82
+ const resolved = resolveOptions(options);
83
+ const loaded = await loadProjects(root, projects.map((project)=>project.root));
84
+ const nxJsonPath = Path.join(root, 'nx.json');
85
+ const nxJson = await readJson(nxJsonPath);
86
+ const files = [
87
+ {
88
+ path: nxJsonPath,
89
+ current: nxJson,
90
+ updated: updateNxJson(nxJson, resolved)
91
+ },
92
+ ...loaded.filter((project)=>project.projectJson !== undefined).map((project)=>({
93
+ path: Path.join(root, project.root, 'project.json'),
94
+ current: project.projectJson,
95
+ updated: updateProjectJson(project.projectJson, inferTargets(project, resolved), resolved)
96
+ }))
97
+ ];
98
+ const outdated = files.filter(({ current, updated })=>JSON.stringify(current) !== JSON.stringify(updated));
99
+ if (check && outdated.length > 0) {
100
+ throw new Error([
101
+ 'Not up to date with @leyman/nx-plugin; run its targets executor:',
102
+ ...outdated.map(({ path })=>`- ${Path.relative(root, path)}`)
103
+ ].join('\n'));
104
+ }
105
+ await Promise.all(outdated.map(async ({ path, updated })=>writeFile(path, `${JSON.stringify(updated, null, 2)}\n`)));
106
+ return outdated.map(({ path })=>path);
107
+ };
108
+
109
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1,17 @@
1
+ import type { TargetConfiguration } from '@nx/devkit';
2
+ import type { Input, NamedInputName, ResolvedOptions, TargetName } from './options.js';
3
+ /**
4
+ * The named inputs the targets use.
5
+ *
6
+ * @param options - resolved options
7
+ * @returns each named input
8
+ */
9
+ export declare const namedInputs: (options: ResolvedOptions) => Record<NamedInputName, Input[]>;
10
+ /**
11
+ * How each target runs, without when: the lifecycle's `dependsOn` is nx-lifecycle's to add.
12
+ *
13
+ * @param options - resolved options
14
+ * @returns each target's configuration, by the target's own name
15
+ */
16
+ export declare const targetConfigurations: (options: ResolvedOptions) => Record<TargetName, TargetConfiguration>;
17
+ //# sourceMappingURL=targets.d.ts.map