@k8slens/package-build 3.2.0 → 3.4.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.
@@ -0,0 +1,137 @@
1
+ import type { Plugin } from "esbuild";
2
+ import { builtinModules as rawBuiltinModules } from "node:module";
3
+ import path from "node:path";
4
+ import { partition } from "lodash";
5
+
6
+ export interface DeclaredImportsPluginParams {
7
+ readonly packageDirectory: string;
8
+ readonly packageJson: {
9
+ readonly name: string;
10
+ readonly dependencies?: Record<string, string>;
11
+ readonly peerDependencies?: Record<string, string>;
12
+ readonly devDependencies?: Record<string, string>;
13
+ readonly lens?: { readonly inlinedDependencies?: readonly string[] };
14
+ };
15
+ }
16
+
17
+ const isExperimentalModule = (module: string) => module.startsWith("_");
18
+ const alsoAddNodeRefs = (module: string) => [module, `node:${module}`];
19
+
20
+ const { builtinModules, disallowedBuiltinModules } = (() => {
21
+ const [disallowed, allowed] = partition(rawBuiltinModules, isExperimentalModule);
22
+
23
+ return {
24
+ disallowedBuiltinModules: new Set(disallowed.flatMap(alsoAddNodeRefs)),
25
+ builtinModules: new Set(allowed.flatMap(alsoAddNodeRefs)),
26
+ };
27
+ })();
28
+
29
+ const getPackageName = (specifier: string) => {
30
+ const [firstPart, secondPart] = specifier.split("/");
31
+
32
+ return specifier.startsWith("@") ? [firstPart, secondPart].join("/") : firstPart;
33
+ };
34
+
35
+ const bundleIt = null;
36
+
37
+ /**
38
+ * Keeps a package's dist to the package's own code: every bare import of the package's own
39
+ * sources must name a package declared in its dependencies or peerDependencies, and stays a
40
+ * `require()` of it. Esbuild would otherwise inline the imported package and everything it
41
+ * imports in turn, shipping a private copy of e.g. MobX or the DI container in the dist, with
42
+ * module state of its own. A package that inlines something on purpose names it in
43
+ * `lens.inlinedDependencies` (and declares it as a devDependency).
44
+ *
45
+ * A package of the Lens monorepo, which has a `lens` section in its package.json, fails to build
46
+ * on such an import. Any other package, such as an extension built with this tool, is warned
47
+ * instead, as its build inlined undeclared imports before and keeps doing so.
48
+ */
49
+ export const declaredImportsPlugin = ({ packageDirectory, packageJson }: DeclaredImportsPluginParams): Plugin => {
50
+ const externals = new Set([
51
+ ...Object.keys(packageJson.dependencies ?? {}),
52
+ ...Object.keys(packageJson.peerDependencies ?? {}),
53
+ ]);
54
+
55
+ const devDependencies = new Set(Object.keys(packageJson.devDependencies ?? {}));
56
+ const inlinedDependencies = new Set(packageJson.lens?.inlinedDependencies ?? []);
57
+
58
+ const fail = (text: string) => ({ errors: [{ text }] });
59
+ const isPackageOfMonorepo = packageJson.lens !== undefined;
60
+ const refuse = (text: string) => (isPackageOfMonorepo ? fail(text) : { warnings: [{ text }] });
61
+
62
+ return {
63
+ name: "declared-imports",
64
+ setup: (build) => {
65
+ build.onResolve({ filter: /^\./ }, (args) => {
66
+ const relativePathToImporter = path.relative(packageDirectory, args.importer);
67
+ const relativePath = path.relative(packageDirectory, path.resolve(args.resolveDir, args.path));
68
+
69
+ if (relativePath.startsWith("..") && !relativePathToImporter.startsWith("..")) {
70
+ return fail(
71
+ `Tried to import "${args.path}" from "${relativePathToImporter}" but it is outside of the package.`,
72
+ );
73
+ }
74
+
75
+ return bundleIt;
76
+ });
77
+
78
+ build.onResolve({ filter: /^[^.]/ }, (args) => {
79
+ // Only the package's own sources are policed: whatever an inlined dependency imports in
80
+ // turn is part of that deliberate exception.
81
+ const relativePathToImporter = path.relative(packageDirectory, args.importer);
82
+ const importerIsOwnFile =
83
+ !relativePathToImporter.startsWith("..") &&
84
+ !path.isAbsolute(relativePathToImporter) &&
85
+ !relativePathToImporter.split(path.sep).includes("node_modules");
86
+
87
+ if (!importerIsOwnFile || !/\.(t|j)sx?$/.test(args.importer)) {
88
+ return bundleIt;
89
+ }
90
+
91
+ if (disallowedBuiltinModules.has(args.path)) {
92
+ return fail(
93
+ `Tried to import "${args.path}" from "${relativePathToImporter}" but it is a disallowed builtin module.`,
94
+ );
95
+ }
96
+
97
+ if (builtinModules.has(args.path)) {
98
+ return { external: true, path: args.path };
99
+ }
100
+
101
+ const dependencyName = getPackageName(args.path);
102
+
103
+ if (externals.has(dependencyName)) {
104
+ // The full specifier is kept, as it names an "exports" entry of the dependency.
105
+ return { external: true, path: args.path };
106
+ }
107
+
108
+ if (inlinedDependencies.has(dependencyName) && devDependencies.has(dependencyName)) {
109
+ return bundleIt;
110
+ }
111
+
112
+ const where = `"${packageJson.name}" imports "${args.path}" from "${relativePathToImporter}"`;
113
+
114
+ if (devDependencies.has(dependencyName)) {
115
+ return refuse(
116
+ [
117
+ `${where}, but "${dependencyName}" is only a devDependency, so the build would inline it, and everything it imports, into dist.`,
118
+ `Fix: move "${dependencyName}" to "dependencies" (or "peerDependencies") if production code needs it.`,
119
+ "If it is test machinery, keep the import out of files the entry point reaches.",
120
+ `If inlining it is deliberate, list "${dependencyName}" in "lens.inlinedDependencies" of package.json.`,
121
+ ].join("\n"),
122
+ );
123
+ }
124
+
125
+ if (inlinedDependencies.has(dependencyName)) {
126
+ return refuse(
127
+ `${where}, and "lens.inlinedDependencies" lists "${dependencyName}", but it is not a devDependency. Fix: declare it in "devDependencies".`,
128
+ );
129
+ }
130
+
131
+ return refuse(
132
+ `${where}, but "${dependencyName}" is not a dependency of the package. Fix: declare it in "dependencies" (or "peerDependencies").`,
133
+ );
134
+ });
135
+ },
136
+ };
137
+ };
@@ -9,7 +9,7 @@ export const extensionDependencyVersionsFileEnvironmentVariable = "LENS_EXTENSIO
9
9
  // Lens writes this file beside its extension-development README every time it loads. The name is
10
10
  // repeated from `extensionDependencyVersionsFileName` of Lens's dynamic-features contracts, which a
11
11
  // published build tool cannot depend on.
12
- const defaultVersionsFilePath = () =>
12
+ export const defaultVersionsFilePath = () =>
13
13
  path.join(
14
14
  homedir(),
15
15
  ".k8slens",
@@ -29,6 +29,8 @@ export interface CheckExtensionDependenciesParams {
29
29
  readonly versionsFilePath: string;
30
30
  readonly fix: boolean;
31
31
  readonly log: (line: string) => void;
32
+ /** Fails, instead of passing, when Lens has written no versions to check against: what publishing needs. */
33
+ readonly requireVersions?: boolean;
32
34
  }
33
35
 
34
36
  interface Mismatch {
@@ -77,16 +79,25 @@ const indentationOf = (json: string) => /^([ \t]+)"/m.exec(json)?.[1] ?? 2;
77
79
  * fails the check; one in `devDependencies` only warns, as Lens never reads those. With `fix`,
78
80
  * rewrites every such range, in all three, to the caret range of the version Lens serves.
79
81
  *
80
- * @returns the exit code: 0 when Lens would load the extension, or when there is nothing to check against.
82
+ * @returns the exit code: 0 when Lens would load the extension, or when there is nothing to check against and `requireVersions` is not set.
81
83
  */
82
84
  export const checkExtensionDependencies = ({
83
85
  packageJsonPath,
84
86
  versionsFilePath,
85
87
  fix,
86
88
  log,
89
+ requireVersions = false,
87
90
  }: CheckExtensionDependenciesParams): number => {
88
91
  const versions = readVersions(versionsFilePath);
89
92
 
93
+ if (!versions && requireVersions) {
94
+ log(
95
+ `Cannot check the dependency ranges against Lens: no "${versionsFilePath}", which Lens writes when it runs on this machine. Start Lens once, or name the versions of another Lens in ${extensionDependencyVersionsFileEnvironmentVariable}.`,
96
+ );
97
+
98
+ return 1;
99
+ }
100
+
90
101
  if (!versions) {
91
102
  log(
92
103
  `Skipped checking dependency ranges against Lens: no "${versionsFilePath}", which Lens writes when it runs on this machine.`,
@@ -0,0 +1,286 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { checkExtensionPublishing, extensionPublishCheck } from "./extension-publish-check";
5
+
6
+ describe("checking an extension is fit to publish, before npm publishes it", () => {
7
+ let directory: string;
8
+ let packageJsonPath: string;
9
+ let versionsFilePath: string;
10
+ let packedFiles: string[];
11
+ let logged: string[];
12
+
13
+ beforeEach(() => {
14
+ directory = mkdtempSync(path.join(tmpdir(), "extension-publish-check-"));
15
+ packageJsonPath = path.join(directory, "package.json");
16
+ versionsFilePath = path.join(directory, "extension-dependency-versions.json");
17
+ packedFiles = ["package.json", "dist/index.js"];
18
+ logged = [];
19
+
20
+ givenLensServes({ "some-package": "1.2.0" });
21
+ });
22
+
23
+ afterEach(() => {
24
+ rmSync(directory, { recursive: true, force: true });
25
+ });
26
+
27
+ it("given an extension fit to publish, passes", () => {
28
+ givenPackageJson(fitToPublish);
29
+
30
+ expect(check()).toBe(0);
31
+ });
32
+
33
+ describe("given npm would pack the extension without its built entrypoint, when checked", () => {
34
+ let exitCode: number;
35
+
36
+ beforeEach(() => {
37
+ givenPackageJson(fitToPublish);
38
+ packedFiles = ["package.json", "src/index.ts"];
39
+
40
+ exitCode = check();
41
+ });
42
+
43
+ it("fails", () => {
44
+ expect(exitCode).toBe(1);
45
+ });
46
+
47
+ it("names the entrypoint, and how to have npm pack it", () => {
48
+ expect(logged).toContain(
49
+ 'npm would publish the extension without its entrypoint "./dist/index.js", and Lens could not install it. Build first, and list the built directory in "files" of package.json, such as "files": ["dist"].',
50
+ );
51
+ });
52
+ });
53
+
54
+ describe("given the entrypoint is named in exports, finds it packed by that name", () => {
55
+ it.each([
56
+ ["as the exports themselves", { exports: "./dist/index.js" }],
57
+ ['as the "." of exports', { exports: { ".": "./dist/index.js" } }],
58
+ ['as the default condition of "."', { exports: { ".": { default: "./dist/index.js" } } }],
59
+ ['as the require condition of "."', { exports: { ".": { require: "./dist/index.js" } } }],
60
+ ])("%s", (_, entrypoint) => {
61
+ givenPackageJson({ ...fitToPublish, main: "./irrelevant-main.js", ...entrypoint });
62
+
63
+ expect(check()).toBe(0);
64
+ });
65
+ });
66
+
67
+ it('given exports without "." of their own, finds the entrypoint named in main packed', () => {
68
+ givenPackageJson({ ...fitToPublish, exports: { "./package.json": "./package.json" } });
69
+
70
+ expect(check()).toBe(0);
71
+ });
72
+
73
+ it("given npm lists the packed files with Windows separators, finds the entrypoint packed", () => {
74
+ givenPackageJson(fitToPublish);
75
+ packedFiles = ["package.json", "dist\\index.js"];
76
+
77
+ expect(check()).toBe(0);
78
+ });
79
+
80
+ describe("given package.json names no entrypoint, when checked", () => {
81
+ let exitCode: number;
82
+
83
+ beforeEach(() => {
84
+ givenPackageJson({ ...fitToPublish, main: undefined });
85
+
86
+ exitCode = check();
87
+ });
88
+
89
+ it("fails", () => {
90
+ expect(exitCode).toBe(1);
91
+ });
92
+
93
+ it("says what to name", () => {
94
+ expect(logged).toContain(
95
+ 'package.json names no entrypoint: set "main" to the built file Lens loads, such as "./dist/index.js".',
96
+ );
97
+ });
98
+ });
99
+
100
+ it("given main is not a path, says the extension names no entrypoint", () => {
101
+ givenPackageJson({ ...fitToPublish, main: 42 });
102
+
103
+ check();
104
+
105
+ expect(logged).toContain(
106
+ 'package.json names no entrypoint: set "main" to the built file Lens loads, such as "./dist/index.js".',
107
+ );
108
+ });
109
+
110
+ describe("given a range Lens would refuse, when checked", () => {
111
+ let exitCode: number;
112
+
113
+ beforeEach(() => {
114
+ givenPackageJson({ ...fitToPublish, dependencies: { "some-package": "^1.2.1" } });
115
+
116
+ exitCode = check();
117
+ });
118
+
119
+ it("fails", () => {
120
+ expect(exitCode).toBe(1);
121
+ });
122
+
123
+ it("names the range and the one to declare instead", () => {
124
+ expect(logged).toContainEqual(
125
+ expect.stringContaining(
126
+ ' in dependencies, "some-package": "^1.2.1" does not include Lens\'s 1.2.0, declare "^1.2.0"',
127
+ ),
128
+ );
129
+ });
130
+ });
131
+
132
+ describe("given Lens on this machine has written no versions, when checked", () => {
133
+ let exitCode: number;
134
+
135
+ beforeEach(() => {
136
+ givenPackageJson(fitToPublish);
137
+ rmSync(versionsFilePath);
138
+
139
+ exitCode = check();
140
+ });
141
+
142
+ it("fails, as publishing unchecked ranges is how an extension no Lens loads reaches users", () => {
143
+ expect(exitCode).toBe(1);
144
+ });
145
+
146
+ it("says how to give it versions to check against", () => {
147
+ expect(logged).toContain(
148
+ `Cannot check the dependency ranges against Lens: no "${versionsFilePath}", which Lens writes when it runs on this machine. Start Lens once, or name the versions of another Lens in LENS_EXTENSION_DEPENDENCY_VERSIONS_FILE.`,
149
+ );
150
+ });
151
+ });
152
+
153
+ describe("given a prerelease version", () => {
154
+ beforeEach(() => {
155
+ givenPackageJson({ ...fitToPublish, version: "0.0.0-stage" });
156
+ });
157
+
158
+ describe("when checked for publishing as the latest version", () => {
159
+ let exitCode: number;
160
+
161
+ beforeEach(() => {
162
+ exitCode = check();
163
+ });
164
+
165
+ it("fails", () => {
166
+ expect(exitCode).toBe(1);
167
+ });
168
+
169
+ it("says to publish it under a tag of its own, or as a release", () => {
170
+ expect(logged).toContain(
171
+ '0.0.0-stage is a prerelease, which would become the version users see and install by default. Publish it with "--tag next", or raise "version" in package.json to a release.',
172
+ );
173
+ });
174
+ });
175
+
176
+ it("when checked for publishing explicitly as latest, fails", () => {
177
+ expect(check({ distTag: "latest" })).toBe(1);
178
+ });
179
+
180
+ it("when checked for publishing under a tag of its own, passes", () => {
181
+ expect(check({ distTag: "next" })).toBe(0);
182
+ });
183
+ });
184
+
185
+ it("given every kind of problem at once, says each of them, then that it will not publish", () => {
186
+ givenPackageJson({ ...fitToPublish, version: "0.0.0-stage", dependencies: { "some-package": "^2.0.0" } });
187
+ packedFiles = ["package.json"];
188
+
189
+ check();
190
+
191
+ expect(logged).toEqual([
192
+ [
193
+ `Lens would refuse to load this extension. These ranges in package.json do not include the version Lens serves (read from "${versionsFilePath}"):`,
194
+ ' in dependencies, "some-package": "^2.0.0" does not include Lens\'s 1.2.0, declare "^1.2.0"',
195
+ 'Run "npx lens-extension-dependencies-check --fix" to rewrite them, then "npm install".',
196
+ ].join("\n"),
197
+ 'npm would publish the extension without its entrypoint "./dist/index.js", and Lens could not install it. Build first, and list the built directory in "files" of package.json, such as "files": ["dist"].',
198
+ '0.0.0-stage is a prerelease, which would become the version users see and install by default. Publish it with "--tag next", or raise "version" in package.json to a release.',
199
+ "Not publishing: fix the above, and publish again.",
200
+ ]);
201
+ });
202
+
203
+ describe("as the command npm runs before publishing", () => {
204
+ let commandsRun: { command: string; directory: string }[];
205
+ let npmPrints: string;
206
+
207
+ beforeEach(() => {
208
+ commandsRun = [];
209
+ npmPrints = JSON.stringify([{ files: [{ path: "package.json" }, { path: "dist/index.js" }] }]);
210
+ givenPackageJson(fitToPublish);
211
+ });
212
+
213
+ const runCommand = ({ environment = {} }: { environment?: Record<string, string> } = {}) =>
214
+ extensionPublishCheck({
215
+ directory,
216
+ environment: { LENS_EXTENSION_DEPENDENCY_VERSIONS_FILE: versionsFilePath, ...environment },
217
+ runCommand: (command, inDirectory) => {
218
+ commandsRun.push({ command, directory: inDirectory });
219
+
220
+ return npmPrints;
221
+ },
222
+ log: (line) => logged.push(line),
223
+ });
224
+
225
+ it("asks npm, in the extension's directory and running none of its scripts, what it would pack", () => {
226
+ runCommand();
227
+
228
+ expect(commandsRun).toEqual([{ command: "npm pack --dry-run --json --ignore-scripts", directory }]);
229
+ });
230
+
231
+ it("reads what npm would pack past whatever npm prints before it", () => {
232
+ npmPrints = `npm warn some-warning\n${npmPrints}`;
233
+
234
+ expect(runCommand()).toBe(0);
235
+ });
236
+
237
+ it("given npm prints no listing, finds nothing packed", () => {
238
+ npmPrints = "";
239
+
240
+ runCommand();
241
+
242
+ expect(logged).toContainEqual(expect.stringContaining("without its entrypoint"));
243
+ });
244
+
245
+ it("checks the ranges against the versions the environment names", () => {
246
+ writeFileSync(path.join(directory, "other-lens-versions.json"), JSON.stringify({ "some-package": "9.0.0" }));
247
+ givenPackageJson({ ...fitToPublish, dependencies: { "some-package": "^1.2.0" } });
248
+
249
+ expect(
250
+ runCommand({
251
+ environment: { LENS_EXTENSION_DEPENDENCY_VERSIONS_FILE: path.join(directory, "other-lens-versions.json") },
252
+ }),
253
+ ).toBe(1);
254
+ });
255
+
256
+ it("given npm publishes a prerelease under a tag of its own, passes", () => {
257
+ givenPackageJson({ ...fitToPublish, version: "0.0.0-stage" });
258
+
259
+ expect(runCommand({ environment: { npm_config_tag: "next" } })).toBe(0);
260
+ });
261
+ });
262
+
263
+ const fitToPublish = {
264
+ version: "0.1.0",
265
+ main: "./dist/index.js",
266
+ dependencies: { "some-package": "^1.2.0" },
267
+ };
268
+
269
+ function check({ distTag }: { distTag?: string } = {}) {
270
+ return checkExtensionPublishing({
271
+ packageJsonPath,
272
+ versionsFilePath,
273
+ packedFiles: () => packedFiles,
274
+ distTag,
275
+ log: (line) => logged.push(line),
276
+ });
277
+ }
278
+
279
+ function givenPackageJson(packageJson: object) {
280
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
281
+ }
282
+
283
+ function givenLensServes(versions: Record<string, string>) {
284
+ writeFileSync(versionsFilePath, JSON.stringify(versions));
285
+ }
286
+ });
@@ -0,0 +1,177 @@
1
+ import { execSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import semver from "semver";
5
+ import {
6
+ checkExtensionDependencies,
7
+ defaultVersionsFilePath,
8
+ extensionDependencyVersionsFileEnvironmentVariable,
9
+ } from "./extension-dependencies-check";
10
+
11
+ /** The dist-tag a prerelease is published under, so it never becomes the version users install by default. */
12
+ export const prereleaseDistTag = "next";
13
+
14
+ export interface CheckExtensionPublishingParams {
15
+ readonly packageJsonPath: string;
16
+ readonly versionsFilePath: string;
17
+ /** The paths npm would pack, relative to the extension's directory, as `npm pack --dry-run` lists them. */
18
+ readonly packedFiles: () => readonly string[];
19
+ /** The dist-tag npm is publishing under, when one was given. */
20
+ readonly distTag: string | undefined;
21
+ readonly log: (line: string) => void;
22
+ }
23
+
24
+ // The same reading of a manifest as Lens's own when it installs an extension, so what is checked
25
+ // here is what Lens will load. Lens's own Publish repeats it, as neither can depend on the other.
26
+ const relativeEntrypointOf = (packageJson: Record<string, unknown>): string | undefined => {
27
+ const { exports, main } = packageJson;
28
+
29
+ if (exports === undefined || exports === null) {
30
+ return typeof main === "string" ? main : undefined;
31
+ }
32
+
33
+ if (typeof exports === "string") {
34
+ return exports;
35
+ }
36
+
37
+ const defaultInExports = (exports as Record<string, unknown>)["."];
38
+
39
+ if (typeof defaultInExports === "string") {
40
+ return defaultInExports;
41
+ }
42
+
43
+ if (typeof defaultInExports === "object" && defaultInExports !== null) {
44
+ const conditions = defaultInExports as Record<string, unknown>;
45
+ const conditional = conditions.default ?? conditions.require;
46
+
47
+ if (typeof conditional === "string") {
48
+ return conditional;
49
+ }
50
+ }
51
+
52
+ return typeof main === "string" ? main : undefined;
53
+ };
54
+
55
+ // The same reading of a path as Lens's own Publish, which checks what npm would pack too: "./dist\\index.js" is "dist/index.js".
56
+ const normalized = (relativePath: string) =>
57
+ relativePath
58
+ .replaceAll("\\", "/")
59
+ .split("/")
60
+ .filter((segment) => segment !== "" && segment !== ".")
61
+ .join("/");
62
+
63
+ const checkEntrypointIsPacked = (packageJson: Record<string, unknown>, packedFiles: () => readonly string[]) => {
64
+ const entrypoint = relativeEntrypointOf(packageJson);
65
+
66
+ if (entrypoint === undefined) {
67
+ return 'package.json names no entrypoint: set "main" to the built file Lens loads, such as "./dist/index.js".';
68
+ }
69
+
70
+ if (!packedFiles().map(normalized).includes(normalized(entrypoint))) {
71
+ return `npm would publish the extension without its entrypoint "${entrypoint}", and Lens could not install it. Build first, and list the built directory in "files" of package.json, such as "files": ["dist"].`;
72
+ }
73
+
74
+ return undefined;
75
+ };
76
+
77
+ const checkPrereleaseIsTagged = (version: unknown, distTag: string | undefined) => {
78
+ const isPrerelease = typeof version === "string" && (semver.prerelease(version) ?? []).length > 0;
79
+
80
+ if (!isPrerelease || (distTag !== undefined && distTag !== "latest")) {
81
+ return undefined;
82
+ }
83
+
84
+ return `${version} is a prerelease, which would become the version users see and install by default. Publish it with "--tag ${prereleaseDistTag}", or raise "version" in package.json to a release.`;
85
+ };
86
+
87
+ /**
88
+ * Checks an extension is fit to publish, before npm publishes it: that what npm packs has the
89
+ * entrypoint Lens loads, that Lens would accept every range the extension declares for a package
90
+ * Lens serves, and that a prerelease is not published as the latest version. Fails when Lens has
91
+ * written no versions to check the ranges against, as publishing unchecked is how an extension
92
+ * reaches users that no Lens loads.
93
+ *
94
+ * @returns the exit code: 0 when the extension is fit to publish.
95
+ */
96
+ export const checkExtensionPublishing = ({
97
+ packageJsonPath,
98
+ versionsFilePath,
99
+ packedFiles,
100
+ distTag,
101
+ log,
102
+ }: CheckExtensionPublishingParams): number => {
103
+ const packageJson: Record<string, unknown> = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
104
+
105
+ const problems = [
106
+ checkEntrypointIsPacked(packageJson, packedFiles),
107
+ checkPrereleaseIsTagged(packageJson.version, distTag),
108
+ ].filter((problem) => problem !== undefined);
109
+
110
+ const dependenciesExitCode = checkExtensionDependencies({
111
+ packageJsonPath,
112
+ versionsFilePath,
113
+ fix: false,
114
+ log,
115
+ requireVersions: true,
116
+ });
117
+
118
+ for (const problem of problems) {
119
+ log(problem);
120
+ }
121
+
122
+ if (problems.length > 0 || dependenciesExitCode !== 0) {
123
+ log("Not publishing: fix the above, and publish again.");
124
+
125
+ return 1;
126
+ }
127
+
128
+ return 0;
129
+ };
130
+
131
+ /**
132
+ * The paths `npm pack --dry-run --json` says it would pack, from what it printed: one entry per
133
+ * package packed, listing its files, after whatever lines npm writes before it. None when npm
134
+ * printed no listing.
135
+ */
136
+ export const packedFilesOf = (output: string): string[] => {
137
+ const listingStart = output.indexOf("[");
138
+
139
+ if (listingStart === -1) {
140
+ return [];
141
+ }
142
+
143
+ const [packed] = JSON.parse(output.slice(listingStart)) as { files?: { path: string }[] }[];
144
+
145
+ return packed?.files?.map((file) => file.path) ?? [];
146
+ };
147
+
148
+ export interface ExtensionPublishCheckParams {
149
+ /** The extension's directory, where npm runs its scripts. */
150
+ readonly directory: string;
151
+ /** The environment npm runs the script in. */
152
+ readonly environment: Readonly<Record<string, string | undefined>>;
153
+ /** Runs a command in a directory, giving back what it printed. */
154
+ readonly runCommand: (command: string, directory: string) => string;
155
+ readonly log: (line: string) => void;
156
+ }
157
+
158
+ /**
159
+ * The `lens-extension-publish-check` command, run in the extension's directory, as its
160
+ * `prepublishOnly` script: npm runs it before every publish, and gives up publishing when it fails.
161
+ */
162
+ export const extensionPublishCheck = (
163
+ { directory, environment, runCommand, log }: ExtensionPublishCheckParams = {
164
+ directory: process.cwd(),
165
+ environment: process.env,
166
+ runCommand: (command, cwd) => execSync(command, { cwd, encoding: "utf-8" }),
167
+ log: (line) => console.log(line),
168
+ },
169
+ ): number =>
170
+ checkExtensionPublishing({
171
+ packageJsonPath: path.resolve(directory, "package.json"),
172
+ versionsFilePath: environment[extensionDependencyVersionsFileEnvironmentVariable] || defaultVersionsFilePath(),
173
+ packedFiles: () => packedFilesOf(runCommand("npm pack --dry-run --json --ignore-scripts", directory)),
174
+ // What npm tells the scripts it runs of `npm publish --tag <tag>`.
175
+ distTag: environment.npm_config_tag || undefined,
176
+ log,
177
+ });