@k8slens/package-build 3.1.0 → 3.3.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
+ };
@@ -1,105 +1,14 @@
1
1
  import { type Plugin, build } from "esbuild";
2
2
  import path from "node:path";
3
3
  import { readFileSync } from "node:fs";
4
- import { builtinModules as rawBuiltinModules } from "node:module";
5
4
  import yargs from "yargs";
5
+ import { declaredImportsPlugin } from "./declared-imports-plugin";
6
6
  import { globImportPlugin } from "./glob-import-plugin";
7
7
  import { stylesheetPlugin } from "./stylesheet-plugin";
8
- import { partition } from "lodash";
9
8
 
10
9
  const CWD = process.cwd();
11
10
  const packageJson = JSON.parse(readFileSync(path.resolve(CWD, "package.json"), "utf-8"));
12
11
 
13
- const isExperimentalModule = (module: string) => module.startsWith("_");
14
- const alsoAddNodeRefs = (module: string) => [module, `node:${module}`];
15
-
16
- const { builtinModules, disallowedBuiltinModules } = (() => {
17
- const [disallowed, allowed] = partition(rawBuiltinModules, isExperimentalModule);
18
- const disallowedBuiltinModules = new Set(disallowed.flatMap(alsoAddNodeRefs));
19
- const builtinModules = new Set(allowed.flatMap(alsoAddNodeRefs));
20
-
21
- return { disallowedBuiltinModules, builtinModules };
22
- })();
23
-
24
- const getPackageName = (path: string) => {
25
- const [firstPart, secondPart] = path.split("/");
26
-
27
- if (path.startsWith("@")) {
28
- return [firstPart, secondPart].join("/");
29
- }
30
-
31
- return firstPart;
32
- };
33
-
34
- const bundleDependency = null;
35
-
36
- const ensureCorrectImportsPlugin: Plugin = {
37
- name: "ensure-correct-imports",
38
- setup: (build) => {
39
- build.onResolve({ filter: /^\./ }, (args) => {
40
- const fullPath = path.resolve(args.resolveDir, args.path);
41
- const relativePathToImporter = path.relative(CWD, args.importer);
42
- const relativePath = path.relative(CWD, fullPath);
43
-
44
- if (relativePath.startsWith("..") && !relativePathToImporter.startsWith("..")) {
45
- throw new Error(
46
- `Tried to import "${args.path}" from "${relativePathToImporter}" but it is outside of the package.`,
47
- );
48
- }
49
-
50
- return bundleDependency;
51
- });
52
- build.onResolve({ filter: /^[^.]/ }, async (args) => {
53
- // Enforce declared imports only for the package's own sources (.ts/.tsx/.js/.jsx).
54
- // Anything imported from outside the package directory — node_modules files or
55
- // other workspace packages' dists (whose realpaths live outside node_modules) —
56
- // belongs to an already-bundled dependency and must keep bundling freely.
57
- const relativePathToImporter = path.relative(CWD, args.importer);
58
- const importerIsOwnFile =
59
- !relativePathToImporter.startsWith("..") && !relativePathToImporter.split(path.sep).includes("node_modules");
60
- const importerIsSourceFile = /\.(t|j)sx?$/.test(args.importer);
61
-
62
- if (!importerIsOwnFile || !importerIsSourceFile) {
63
- return bundleDependency;
64
- }
65
-
66
- if (disallowedBuiltinModules.has(args.path)) {
67
- throw new Error(
68
- `Tried to import "${args.path}" from "${args.importer}" but it is a disallowed builtin module.`,
69
- );
70
- }
71
-
72
- if (builtinModules.has(args.path)) {
73
- return {
74
- external: true,
75
- path: args.path,
76
- };
77
- }
78
-
79
- const packageName = getPackageName(args.path);
80
-
81
- const isExplicitDependency = packageName in (packageJson.dependencies ?? {});
82
- const isExplicitDevDependency = packageName in (packageJson.devDependencies ?? {});
83
-
84
- if (isExplicitDependency) {
85
- // Note: Return the full path since we assume that it is a "exports" entry of the dependency found in package.json
86
- return {
87
- external: true,
88
- path: args.path,
89
- };
90
- }
91
-
92
- if (isExplicitDevDependency) {
93
- return bundleDependency;
94
- }
95
-
96
- throw new Error(
97
- `Tried to import "${args.path}" from "${relativePathToImporter}" but it is not a dependency of the package.`,
98
- );
99
- });
100
- },
101
- };
102
-
103
12
  const parsedArgs = yargs()
104
13
  .options({
105
14
  tsconfig: {
@@ -165,15 +74,15 @@ const nativeNodeModulesPlugin: Plugin = {
165
74
  },
166
75
  };
167
76
 
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.
77
+ // Keep .png and .webm imports as require() calls in the dist bundle instead of
78
+ // inlining them as base64 data urls, so images and videos ship as files loaded
79
+ // on demand rather than megabytes of string literals in every consuming bundle.
80
+ // The import path is rewritten to stay correct relative to the output file,
81
+ // since the application build resolves it from there with its "file" loader.
173
82
  const externalImagesPlugin: Plugin = {
174
83
  name: "external-images",
175
84
  setup(build) {
176
- build.onResolve({ filter: /\.png$/ }, (args) => {
85
+ build.onResolve({ filter: /\.(png|webm)$/ }, (args) => {
177
86
  const absolutePath = path.resolve(args.resolveDir, args.path);
178
87
  const outputDirectory = path.dirname(path.resolve(CWD, parsedArgs.outfile));
179
88
  const relativePath = path.relative(outputDirectory, absolutePath).split(path.sep).join("/");
@@ -204,7 +113,7 @@ export const packageBuild = async () => {
204
113
  packageDirectory: CWD,
205
114
  declaredDependencies: new Set(Object.keys(packageJson.dependencies ?? {})),
206
115
  }),
207
- ensureCorrectImportsPlugin,
116
+ declaredImportsPlugin({ packageDirectory: CWD, packageJson }),
208
117
  externalImagesPlugin,
209
118
  globImportPlugin(),
210
119
  nativeNodeModulesPlugin,