@k8slens/package-build 3.0.0 → 3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k8slens/package-build",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "lens": {
5
5
  "env": "agnostic",
6
6
  "layer": "infrastructure",
@@ -41,7 +41,8 @@
41
41
  }
42
42
  },
43
43
  "bin": {
44
- "lens-package-build": "bin/package-build"
44
+ "lens-package-build": "bin/package-build",
45
+ "lens-extension-dependencies-check": "bin/extension-dependencies-check"
45
46
  },
46
47
  "files": [
47
48
  "index.js",
@@ -50,7 +51,7 @@
50
51
  "dist"
51
52
  ],
52
53
  "scripts": {
53
- "build:infrastructure:code": "esbuild --bundle index.ts --outfile=dist/index.js --external:node:* --external:postcss --external:postcss-modules --external:esbuild --external:fast-glob --external:sass --external:yargs --external:tailwindcss --format=cjs",
54
+ "build:infrastructure:code": "esbuild --bundle index.ts --outfile=dist/index.js --external:node:* --external:postcss --external:postcss-modules --external:esbuild --external:fast-glob --external:sass --external:yargs --external:semver --external:tailwindcss --format=cjs",
54
55
  "build:infrastructure:types": "tsc --project .",
55
56
  "build:infrastructure": "npm run build:infrastructure:code; npm run build:infrastructure:types",
56
57
  "lint": "lens-verify-code-style",
@@ -65,13 +66,15 @@
65
66
  "postcss": "^8.5.18",
66
67
  "postcss-modules": "^6.0.1",
67
68
  "sass": "^1.89.0",
69
+ "semver": "^7.7.2",
68
70
  "tailwindcss": "^3.4.17",
69
71
  "yargs": "^17.7.2"
70
72
  },
71
73
  "devDependencies": {
72
74
  "@async-fn/jest": "^1.6.4",
73
- "@k8slens/typescript": "^8.0.0",
74
- "@lensapp/jest": "^7.2.0"
75
+ "@k8slens/typescript": "^8.1.0",
76
+ "@lensapp/jest": "^7.2.0",
77
+ "@types/semver": "^7.7.0"
75
78
  },
76
79
  "publishConfig": {
77
80
  "access": "public",
@@ -82,6 +85,7 @@
82
85
  "build:infrastructure": {
83
86
  "inputs": [
84
87
  "{projectRoot}/bin/package-build",
88
+ "{projectRoot}/bin/extension-dependencies-check",
85
89
  "{projectRoot}/index.ts",
86
90
  "{projectRoot}/src/**/*.ts"
87
91
  ],
@@ -0,0 +1,259 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { checkExtensionDependencies, extensionDependenciesCheck } from "./extension-dependencies-check";
5
+
6
+ const originalArgv = process.argv;
7
+
8
+ describe("checking an extension's dependency ranges against the versions Lens serves", () => {
9
+ let directory: string;
10
+ let packageJsonPath: string;
11
+ let versionsFilePath: string;
12
+ let logged: string[];
13
+
14
+ beforeEach(() => {
15
+ directory = mkdtempSync(path.join(tmpdir(), "extension-dependencies-check-"));
16
+ packageJsonPath = path.join(directory, "package.json");
17
+ versionsFilePath = path.join(directory, "extension-dependency-versions.json");
18
+ logged = [];
19
+ });
20
+
21
+ afterEach(() => {
22
+ rmSync(directory, { recursive: true, force: true });
23
+ });
24
+
25
+ const check = ({ fix = false } = {}) =>
26
+ checkExtensionDependencies({ packageJsonPath, versionsFilePath, fix, log: (line) => logged.push(line) });
27
+
28
+ const givenPackageJson = (packageJson: object) =>
29
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
30
+
31
+ const givenLensServes = (versions: Record<string, string>) =>
32
+ writeFileSync(versionsFilePath, JSON.stringify(versions));
33
+
34
+ describe("given Lens on this machine has written no versions, when checked", () => {
35
+ let exitCode: number;
36
+
37
+ beforeEach(() => {
38
+ givenPackageJson({ dependencies: { "some-package": "^999.0.0" } });
39
+
40
+ exitCode = check();
41
+ });
42
+
43
+ it("passes", () => {
44
+ expect(exitCode).toBe(0);
45
+ });
46
+
47
+ it("says in one line that the check was skipped, naming the file it looked for", () => {
48
+ expect(logged).toEqual([
49
+ `Skipped checking dependency ranges against Lens: no "${versionsFilePath}", which Lens writes when it runs on this machine.`,
50
+ ]);
51
+ });
52
+ });
53
+
54
+ describe("given ranges that include every version Lens serves, when checked", () => {
55
+ let exitCode: number;
56
+
57
+ beforeEach(() => {
58
+ givenLensServes({ "some-package": "1.2.0", "@some-scope/some-other-package": "2.0.0-beta.1" });
59
+
60
+ givenPackageJson({
61
+ dependencies: { "some-package": "^1.0.0", "some-package-lens-does-not-serve": "^999.0.0" },
62
+ peerDependencies: { "@some-scope/some-other-package": "^2.0.0-beta.0" },
63
+ });
64
+
65
+ exitCode = check();
66
+ });
67
+
68
+ it("passes", () => {
69
+ expect(exitCode).toBe(0);
70
+ });
71
+
72
+ it("says every range includes the version Lens serves", () => {
73
+ expect(logged).toEqual([
74
+ `Every dependency range includes the version Lens serves (read from "${versionsFilePath}").`,
75
+ ]);
76
+ });
77
+ });
78
+
79
+ describe("given a range in dependencies that excludes the version Lens serves, when checked", () => {
80
+ let exitCode: number;
81
+
82
+ beforeEach(() => {
83
+ givenLensServes({ react: "19.2.0" });
84
+ givenPackageJson({ dependencies: { react: "^19.3.1" } });
85
+
86
+ exitCode = check();
87
+ });
88
+
89
+ it("fails", () => {
90
+ expect(exitCode).toBe(1);
91
+ });
92
+
93
+ it("names the range, the version Lens serves, the range to declare instead and how to fix it", () => {
94
+ expect(logged).toEqual([
95
+ [
96
+ `Lens would refuse to load this extension. These ranges in package.json do not include the version Lens serves (read from "${versionsFilePath}"):`,
97
+ ` in dependencies, "react": "^19.3.1" does not include Lens's 19.2.0, declare "^19.2.0"`,
98
+ `Run "npx lens-extension-dependencies-check --fix" to rewrite them, then "npm install".`,
99
+ ].join("\n"),
100
+ ]);
101
+ });
102
+ });
103
+
104
+ describe("given several ranges that exclude the versions Lens serves, when checked", () => {
105
+ let exitCode: number;
106
+
107
+ beforeEach(() => {
108
+ givenLensServes({ react: "19.2.0", mobx: "6.13.0", "@k8slens/injectable": "26.0.0" });
109
+
110
+ givenPackageJson({
111
+ dependencies: { react: "^19.3.1", "@k8slens/injectable": "^26.0.0" },
112
+ peerDependencies: { mobx: "^7.0.0" },
113
+ });
114
+
115
+ exitCode = check();
116
+ });
117
+
118
+ it("fails", () => {
119
+ expect(exitCode).toBe(1);
120
+ });
121
+
122
+ it("lists every one of them", () => {
123
+ expect(logged.join("\n")).toContain(
124
+ [
125
+ ` in dependencies, "react": "^19.3.1" does not include Lens's 19.2.0, declare "^19.2.0"`,
126
+ ` in peerDependencies, "mobx": "^7.0.0" does not include Lens's 6.13.0, declare "^6.13.0"`,
127
+ ].join("\n"),
128
+ );
129
+ });
130
+ });
131
+
132
+ describe("given a range in dependencies that Lens does not read, as peerDependencies declares one Lens accepts, when checked", () => {
133
+ let exitCode: number;
134
+
135
+ beforeEach(() => {
136
+ givenLensServes({ react: "19.2.0" });
137
+
138
+ givenPackageJson({
139
+ dependencies: { react: "^19.3.1" },
140
+ peerDependencies: { react: "^19.0.0" },
141
+ });
142
+
143
+ exitCode = check();
144
+ });
145
+
146
+ it("passes, as Lens would load the extension", () => {
147
+ expect(exitCode).toBe(0);
148
+ });
149
+ });
150
+
151
+ describe("given a range in devDependencies only that excludes the version Lens serves, when checked", () => {
152
+ let exitCode: number;
153
+
154
+ beforeEach(() => {
155
+ givenLensServes({ react: "19.2.0" });
156
+ givenPackageJson({ devDependencies: { react: "^19.3.1" } });
157
+
158
+ exitCode = check();
159
+ });
160
+
161
+ it("passes, as Lens does not read devDependencies", () => {
162
+ expect(exitCode).toBe(0);
163
+ });
164
+
165
+ it("warns that the extension builds against another version than it runs on", () => {
166
+ expect(logged).toContain(
167
+ `Warning: in devDependencies, "react": "^19.3.1" does not include Lens's 19.2.0, declare "^19.2.0". The extension builds against another version than it runs on.`,
168
+ );
169
+ });
170
+ });
171
+
172
+ describe("given ranges that exclude the versions Lens serves, when fixed", () => {
173
+ let exitCode: number;
174
+
175
+ beforeEach(() => {
176
+ givenLensServes({ react: "19.2.0", mobx: "6.13.0", "@k8slens/injectable": "26.0.0" });
177
+
178
+ givenPackageJson({
179
+ name: "some-extension",
180
+ dependencies: { react: "^19.3.1", "@k8slens/injectable": "^26.0.0", "some-package": "^1.0.0" },
181
+ peerDependencies: { mobx: "^7.0.0" },
182
+ devDependencies: { react: "^19.3.1" },
183
+ });
184
+
185
+ exitCode = check({ fix: true });
186
+ });
187
+
188
+ it("passes", () => {
189
+ expect(exitCode).toBe(0);
190
+ });
191
+
192
+ it("rewrites each of them to the caret range of the version Lens serves, leaving everything else as it was", () => {
193
+ expect(readFileSync(packageJsonPath, "utf-8")).toBe(
194
+ `${JSON.stringify(
195
+ {
196
+ name: "some-extension",
197
+ dependencies: { react: "^19.2.0", "@k8slens/injectable": "^26.0.0", "some-package": "^1.0.0" },
198
+ peerDependencies: { mobx: "^6.13.0" },
199
+ devDependencies: { react: "^19.2.0" },
200
+ },
201
+ null,
202
+ 2,
203
+ )}\n`,
204
+ );
205
+ });
206
+
207
+ it("says what it rewrote and to reinstall", () => {
208
+ expect(logged).toEqual([
209
+ `Rewrote "react" in dependencies from "^19.3.1" to "^19.2.0".`,
210
+ `Rewrote "mobx" in peerDependencies from "^7.0.0" to "^6.13.0".`,
211
+ `Rewrote "react" in devDependencies from "^19.3.1" to "^19.2.0".`,
212
+ `Run "npm install" so node_modules has the versions Lens serves.`,
213
+ ]);
214
+ });
215
+
216
+ it("when checked again, passes", () => {
217
+ expect(check()).toBe(0);
218
+ });
219
+ });
220
+
221
+ it("given every range includes the version Lens serves, when fixed, leaves package.json untouched", () => {
222
+ givenLensServes({ react: "19.2.0" });
223
+ writeFileSync(packageJsonPath, '{\n "dependencies": { "react": "^19.0.0" }\n}\n');
224
+
225
+ check({ fix: true });
226
+
227
+ expect(readFileSync(packageJsonPath, "utf-8")).toBe('{\n "dependencies": { "react": "^19.0.0" }\n}\n');
228
+ });
229
+
230
+ describe("given the environment names the versions file, when the command runs with --fix in the extension's directory", () => {
231
+ let exitCode: number;
232
+
233
+ beforeEach(() => {
234
+ givenLensServes({ react: "19.2.0" });
235
+ givenPackageJson({ dependencies: { react: "^19.3.1" } });
236
+
237
+ jest.spyOn(process, "cwd").mockReturnValue(directory);
238
+ jest.spyOn(console, "log").mockImplementation(() => {});
239
+ process.env.LENS_EXTENSION_DEPENDENCY_VERSIONS_FILE = versionsFilePath;
240
+ process.argv = ["some-node", "lens-extension-dependencies-check", "--fix"];
241
+
242
+ exitCode = extensionDependenciesCheck();
243
+ });
244
+
245
+ afterEach(() => {
246
+ jest.restoreAllMocks();
247
+ delete process.env.LENS_EXTENSION_DEPENDENCY_VERSIONS_FILE;
248
+ process.argv = originalArgv;
249
+ });
250
+
251
+ it("passes", () => {
252
+ expect(exitCode).toBe(0);
253
+ });
254
+
255
+ it("rewrites the range in the extension's package.json to the version in that file", () => {
256
+ expect(JSON.parse(readFileSync(packageJsonPath, "utf-8"))).toEqual({ dependencies: { react: "^19.2.0" } });
257
+ });
258
+ });
259
+ });
@@ -0,0 +1,158 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import semver from "semver";
5
+
6
+ /** Overrides where the check reads the versions Lens serves, e.g. to check against another Lens than the one on this machine. */
7
+ export const extensionDependencyVersionsFileEnvironmentVariable = "LENS_EXTENSION_DEPENDENCY_VERSIONS_FILE";
8
+
9
+ // Lens writes this file beside its extension-development README every time it loads. The name is
10
+ // repeated from `extensionDependencyVersionsFileName` of Lens's dynamic-features contracts, which a
11
+ // published build tool cannot depend on.
12
+ const defaultVersionsFilePath = () =>
13
+ path.join(
14
+ homedir(),
15
+ ".k8slens",
16
+ "extensions",
17
+ "extension-development-instructions",
18
+ "extension-dependency-versions.json",
19
+ );
20
+
21
+ const commandName = "lens-extension-dependencies-check";
22
+
23
+ type Section = "dependencies" | "peerDependencies" | "devDependencies";
24
+
25
+ type PackageJson = Partial<Record<Section, Record<string, string>>>;
26
+
27
+ export interface CheckExtensionDependenciesParams {
28
+ readonly packageJsonPath: string;
29
+ readonly versionsFilePath: string;
30
+ readonly fix: boolean;
31
+ readonly log: (line: string) => void;
32
+ }
33
+
34
+ interface Mismatch {
35
+ readonly section: Section;
36
+ readonly name: string;
37
+ readonly declaredRange: string;
38
+ readonly hostVersion: string;
39
+ }
40
+
41
+ // The rule Lens applies before it lets an extension require a package it serves.
42
+ const isSatisfied = (hostVersion: string, declaredRange: string) =>
43
+ semver.satisfies(hostVersion, declaredRange, { includePrerelease: true });
44
+
45
+ const describeMismatch = ({ name, declaredRange, hostVersion }: Mismatch) =>
46
+ `"${name}": "${declaredRange}" does not include Lens's ${hostVersion}, declare "^${hostVersion}"`;
47
+
48
+ const readVersions = (versionsFilePath: string): Record<string, string> | undefined => {
49
+ try {
50
+ return JSON.parse(readFileSync(versionsFilePath, "utf-8"));
51
+ } catch (error) {
52
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
53
+ return undefined;
54
+ }
55
+
56
+ throw error;
57
+ }
58
+ };
59
+
60
+ const mismatchesOf = (packageJson: PackageJson, versions: Record<string, string>) =>
61
+ (["dependencies", "peerDependencies", "devDependencies"] as const).flatMap((section) =>
62
+ Object.entries(packageJson[section] ?? {}).flatMap(([name, declaredRange]): Mismatch[] => {
63
+ const hostVersion = versions[name];
64
+
65
+ return hostVersion !== undefined && !isSatisfied(hostVersion, declaredRange)
66
+ ? [{ section, name, declaredRange, hostVersion }]
67
+ : [];
68
+ }),
69
+ );
70
+
71
+ const indentationOf = (json: string) => /^([ \t]+)"/m.exec(json)?.[1] ?? 2;
72
+
73
+ /**
74
+ * Checks the ranges an extension's package.json declares for the packages Lens serves to
75
+ * extensions against the versions the Lens on this machine serves, with the rule Lens applies
76
+ * before loading the extension. A range Lens would refuse in `dependencies` or `peerDependencies`
77
+ * fails the check; one in `devDependencies` only warns, as Lens never reads those. With `fix`,
78
+ * rewrites every such range, in all three, to the caret range of the version Lens serves.
79
+ *
80
+ * @returns the exit code: 0 when Lens would load the extension, or when there is nothing to check against.
81
+ */
82
+ export const checkExtensionDependencies = ({
83
+ packageJsonPath,
84
+ versionsFilePath,
85
+ fix,
86
+ log,
87
+ }: CheckExtensionDependenciesParams): number => {
88
+ const versions = readVersions(versionsFilePath);
89
+
90
+ if (!versions) {
91
+ log(
92
+ `Skipped checking dependency ranges against Lens: no "${versionsFilePath}", which Lens writes when it runs on this machine.`,
93
+ );
94
+
95
+ return 0;
96
+ }
97
+
98
+ const packageJsonText = readFileSync(packageJsonPath, "utf-8");
99
+ const packageJson: PackageJson = JSON.parse(packageJsonText);
100
+ const mismatches = mismatchesOf(packageJson, versions);
101
+
102
+ if (fix) {
103
+ if (mismatches.length === 0) {
104
+ log("Nothing to fix: every dependency range includes the version Lens serves.");
105
+
106
+ return 0;
107
+ }
108
+
109
+ for (const { section, name, declaredRange, hostVersion } of mismatches) {
110
+ packageJson[section] = { ...packageJson[section], [name]: `^${hostVersion}` };
111
+ log(`Rewrote "${name}" in ${section} from "${declaredRange}" to "^${hostVersion}".`);
112
+ }
113
+
114
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, indentationOf(packageJsonText))}\n`);
115
+ log('Run "npm install" so node_modules has the versions Lens serves.');
116
+
117
+ return 0;
118
+ }
119
+
120
+ // Lens reads the range from peerDependencies over dependencies, as the extension declares it there.
121
+ const refusedByLens = mismatches.filter(
122
+ ({ section, name }) =>
123
+ section === "peerDependencies" || (section === "dependencies" && !packageJson.peerDependencies?.[name]),
124
+ );
125
+
126
+ const devOnly = mismatches.filter(({ section }) => section === "devDependencies");
127
+
128
+ for (const mismatch of devOnly) {
129
+ log(
130
+ `Warning: in devDependencies, ${describeMismatch(mismatch)}. The extension builds against another version than it runs on.`,
131
+ );
132
+ }
133
+
134
+ if (refusedByLens.length === 0) {
135
+ log(`Every dependency range includes the version Lens serves (read from "${versionsFilePath}").`);
136
+
137
+ return 0;
138
+ }
139
+
140
+ log(
141
+ [
142
+ `Lens would refuse to load this extension. These ranges in package.json do not include the version Lens serves (read from "${versionsFilePath}"):`,
143
+ ...refusedByLens.map((mismatch) => ` in ${mismatch.section}, ${describeMismatch(mismatch)}`),
144
+ `Run "npx ${commandName} --fix" to rewrite them, then "npm install".`,
145
+ ].join("\n"),
146
+ );
147
+
148
+ return 1;
149
+ };
150
+
151
+ /** The `lens-extension-dependencies-check [--fix]` command, run in the extension's directory. */
152
+ export const extensionDependenciesCheck = (): number =>
153
+ checkExtensionDependencies({
154
+ packageJsonPath: path.resolve(process.cwd(), "package.json"),
155
+ versionsFilePath: process.env[extensionDependencyVersionsFileEnvironmentVariable] || defaultVersionsFilePath(),
156
+ fix: process.argv.slice(2).includes("--fix"),
157
+ log: (line) => console.log(line),
158
+ });
@@ -165,15 +165,15 @@ const nativeNodeModulesPlugin: Plugin = {
165
165
  },
166
166
  };
167
167
 
168
- // Keep .png imports as require() calls in the dist bundle instead of inlining
169
- // them as base64 data urls, so images ship as files loaded on demand rather
170
- // than megabytes of string literals in every consuming bundle. The import
171
- // path is rewritten to stay correct relative to the output file, since the
172
- // application build resolves it from there with its "file" loader.
168
+ // Keep .png and .webm imports as require() calls in the dist bundle instead of
169
+ // inlining them as base64 data urls, so images and videos ship as files loaded
170
+ // on demand rather than megabytes of string literals in every consuming bundle.
171
+ // The import path is rewritten to stay correct relative to the output file,
172
+ // since the application build resolves it from there with its "file" loader.
173
173
  const externalImagesPlugin: Plugin = {
174
174
  name: "external-images",
175
175
  setup(build) {
176
- build.onResolve({ filter: /\.png$/ }, (args) => {
176
+ build.onResolve({ filter: /\.(png|webm)$/ }, (args) => {
177
177
  const absolutePath = path.resolve(args.resolveDir, args.path);
178
178
  const outputDirectory = path.dirname(path.resolve(CWD, parsedArgs.outfile));
179
179
  const relativePath = path.relative(outputDirectory, absolutePath).split(path.sep).join("/");