@k8slens/package-build 2.2.3 → 3.1.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/bin/extension-dependencies-check +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +581 -319
- package/dist/src/extension-dependencies-check.d.ts +21 -0
- package/dist/src/extension-dependencies-check.d.ts.map +1 -0
- package/dist/src/package-build.d.ts.map +1 -1
- package/dist/src/stylesheet-plugin.d.ts +33 -0
- package/dist/src/stylesheet-plugin.d.ts.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -6
- package/src/__fixtures__/package-with-undeclared-dependency/index.js +1 -0
- package/src/__fixtures__/package-with-undeclared-dependency/manifest.json +6 -0
- package/src/__fixtures__/package-without-dependency/index.js +1 -0
- package/src/__fixtures__/package-without-dependency/manifest.json +3 -0
- package/src/__fixtures__/package-without-dependency/some.scss +3 -0
- package/src/__fixtures__/styled-package/_partial.scss +1 -0
- package/src/__fixtures__/styled-package/empty.scss +1 -0
- package/src/__fixtures__/styled-package/fonts/some-font.ttf +1 -0
- package/src/__fixtures__/styled-package/fonts.scss +8 -0
- package/src/__fixtures__/styled-package/global.scss +3 -0
- package/src/__fixtures__/styled-package/importer.scss +5 -0
- package/src/__fixtures__/styled-package/index.js +10 -0
- package/src/__fixtures__/styled-package/manifest.json +7 -0
- package/src/__fixtures__/styled-package/plain.css +3 -0
- package/src/__fixtures__/styled-package/plain.module.css +3 -0
- package/src/__fixtures__/styled-package/some.module.scss +7 -0
- package/src/__fixtures__/styled-package/tilde.scss +7 -0
- package/src/extension-dependencies-check.test.ts +259 -0
- package/src/extension-dependencies-check.ts +158 -0
- package/src/package-build.ts +7 -15
- package/src/stylesheet-plugin.test.ts +161 -0
- package/src/stylesheet-plugin.ts +272 -0
- package/src/glob-import-plugin.js +0 -46
- package/src/package-build.js +0 -193
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { build } from "esbuild";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { stylesheetPlugin } from "./stylesheet-plugin";
|
|
5
|
+
|
|
6
|
+
describe("stylesheets in a package build", () => {
|
|
7
|
+
describe("given a package importing stylesheets, when built", () => {
|
|
8
|
+
let bundle: string;
|
|
9
|
+
let exportsOfBundle: Record<string, Record<string, unknown>>;
|
|
10
|
+
let outputFileCount: number;
|
|
11
|
+
let warnings: string[];
|
|
12
|
+
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
const result = await buildFixture("styled-package");
|
|
15
|
+
|
|
16
|
+
warnings = result.warnings.map((warning) => warning.text);
|
|
17
|
+
outputFileCount = result.outputFiles.length;
|
|
18
|
+
bundle = result.outputFiles[0].text;
|
|
19
|
+
exportsOfBundle = evaluate(bundle);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("emits no css file beside the bundle", () => {
|
|
23
|
+
expect(outputFileCount).toBe(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("declares a global stylesheet as a stylesheet of the package, identified by package name and file", () => {
|
|
27
|
+
expect(exportsOfBundle.global.stylesheet).toEqual({
|
|
28
|
+
id: "@some-scope/styled-package/global.scss",
|
|
29
|
+
css: ".global {\n color: red;\n}",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("exports the scoped class names of a css module as its default", () => {
|
|
34
|
+
expect(exportsOfBundle.someModule.default).toEqual({
|
|
35
|
+
some: expect.stringMatching(/^some-module__some___.{5}$/),
|
|
36
|
+
nested: expect.stringMatching(/^some-module__nested___.{5}$/),
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("declares the scoped css of a css module as a stylesheet", () => {
|
|
41
|
+
const classNames = exportsOfBundle.someModule.default as Record<string, string>;
|
|
42
|
+
|
|
43
|
+
expect(exportsOfBundle.someModule.stylesheet).toEqual({
|
|
44
|
+
id: "@some-scope/styled-package/some.module.scss",
|
|
45
|
+
css: `.${classNames.some} {\n color: blue;\n}\n.${classNames.some} .${classNames.nested} {\n color: green;\n}`,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("includes the rules of an imported partial in the importing stylesheet", () => {
|
|
50
|
+
expect(exportsOfBundle.importer.stylesheet).toEqual({
|
|
51
|
+
id: "@some-scope/styled-package/importer.scss",
|
|
52
|
+
css: ".importer {\n color: purple;\n}",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("declares no stylesheet for a stylesheet that compiles to nothing", () => {
|
|
57
|
+
expect(exportsOfBundle.empty.stylesheet).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("declares plain css as a stylesheet too", () => {
|
|
61
|
+
expect(exportsOfBundle.plain.stylesheet).toEqual({
|
|
62
|
+
id: "@some-scope/styled-package/plain.css",
|
|
63
|
+
css: ".plain {\n color: black;\n}",
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("scopes the class names of a plain css module", () => {
|
|
68
|
+
expect(exportsOfBundle.plainModule.default).toEqual({
|
|
69
|
+
plainLocal: expect.stringMatching(/^plain-module__plainLocal___.{5}$/),
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// include-media is a dependency of the monorepo, hoisted to its root node_modules.
|
|
74
|
+
it("resolves a ~ import from a dependency the way node would", () => {
|
|
75
|
+
expect(exportsOfBundle.tilde.stylesheet).toEqual({
|
|
76
|
+
id: "@some-scope/styled-package/tilde.scss",
|
|
77
|
+
css: expect.stringContaining("@media (min-width: 100px)"),
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("inlines a file a stylesheet refers to by url as a data url, since the text travels without its neighbours", () => {
|
|
82
|
+
const css = (exportsOfBundle.fonts.stylesheet as { css: string }).css;
|
|
83
|
+
|
|
84
|
+
expect(css).toContain(
|
|
85
|
+
`src: url("data:font/ttf;base64,${Buffer.from("not really a font").toString("base64")}") format("truetype")`,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("leaves a url it cannot resolve as it is", () => {
|
|
90
|
+
const css = (exportsOfBundle.fonts.stylesheet as { css: string }).css;
|
|
91
|
+
|
|
92
|
+
expect(css).toContain('background: url("./missing.png")');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("warns about a url it cannot resolve", () => {
|
|
96
|
+
expect(warnings).toEqual([expect.stringContaining('"fonts.scss" refers to "./missing.png"')]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("keeps @k8slens/stylesheet-contracts external so that the token identity is shared", () => {
|
|
100
|
+
expect(bundle).toContain('require("@k8slens/stylesheet-contracts")');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// overlayscrollbars is a dependency of the monorepo, hoisted to its root node_modules.
|
|
104
|
+
it("compiles the stylesheet of a declared dependency into a stylesheet identified by the import specifier", () => {
|
|
105
|
+
expect(exportsOfBundle.ofDependency.stylesheet).toEqual({
|
|
106
|
+
id: "@some-scope/styled-package/overlayscrollbars/overlayscrollbars.css",
|
|
107
|
+
css: expect.stringContaining(".os-scrollbar"),
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("given a package importing the stylesheet of a dependency it does not declare, when built", () => {
|
|
113
|
+
it("fails, naming the dependency", async () => {
|
|
114
|
+
await expect(buildFixture("package-with-undeclared-dependency")).rejects.toThrow(
|
|
115
|
+
'"@some-scope/package-with-undeclared-dependency" imports the stylesheet "overlayscrollbars/overlayscrollbars.css" of "overlayscrollbars" without declaring "overlayscrollbars" in its dependencies',
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("given a package importing a stylesheet without declaring @k8slens/stylesheet-contracts, when built", () => {
|
|
121
|
+
it("fails, naming the missing dependency", async () => {
|
|
122
|
+
await expect(buildFixture("package-without-dependency")).rejects.toThrow(
|
|
123
|
+
'"@some-scope/package-without-dependency" imports a stylesheet but does not declare "@k8slens/stylesheet-contracts" in its dependencies',
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const buildFixture = (fixture: string) => {
|
|
130
|
+
const packageDirectory = path.join(__dirname, "__fixtures__", fixture);
|
|
131
|
+
// Named manifest.json rather than package.json so that nothing takes the fixture for a workspace package.
|
|
132
|
+
const packageJson = JSON.parse(readFileSync(path.join(packageDirectory, "manifest.json"), "utf-8"));
|
|
133
|
+
|
|
134
|
+
return build({
|
|
135
|
+
entryPoints: [path.join(packageDirectory, "index.js")],
|
|
136
|
+
bundle: true,
|
|
137
|
+
write: false,
|
|
138
|
+
format: "cjs",
|
|
139
|
+
platform: "node",
|
|
140
|
+
logLevel: "silent",
|
|
141
|
+
plugins: [
|
|
142
|
+
stylesheetPlugin({
|
|
143
|
+
packageName: packageJson.name,
|
|
144
|
+
packageDirectory,
|
|
145
|
+
declaredDependencies: new Set(Object.keys(packageJson.dependencies ?? {})),
|
|
146
|
+
}),
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Runs the bundle with the contracts stubbed to hand back what they were given,
|
|
152
|
+
// so that the tests see the stylesheets the bundle declares.
|
|
153
|
+
const evaluate = (bundle: string) => {
|
|
154
|
+
const module = { exports: {} as Record<string, Record<string, unknown>> };
|
|
155
|
+
const requireStub = (specifier: string) =>
|
|
156
|
+
specifier === "@k8slens/stylesheet-contracts" ? { asStylesheet: (stylesheet: unknown) => stylesheet } : {};
|
|
157
|
+
|
|
158
|
+
new Function("require", "module", "exports", bundle)(requireStub, module, module.exports);
|
|
159
|
+
|
|
160
|
+
return module.exports;
|
|
161
|
+
};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { Plugin } from "esbuild";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { type FileImporter, Logger, compile } from "sass";
|
|
6
|
+
import postcss, { type AcceptedPlugin } from "postcss";
|
|
7
|
+
import postcssModules from "postcss-modules";
|
|
8
|
+
import tailwindcss from "tailwindcss";
|
|
9
|
+
|
|
10
|
+
export const stylesheetContractsPackageName = "@k8slens/stylesheet-contracts";
|
|
11
|
+
|
|
12
|
+
const namespace = "lens-stylesheet";
|
|
13
|
+
const isStylesheetPath = /\.(scss|css)$/;
|
|
14
|
+
const isModulePath = /\.module\.(scss|css)$/;
|
|
15
|
+
const generateScopedName = "[name]__[local]___[hash:base64:5]";
|
|
16
|
+
|
|
17
|
+
export interface StylesheetPluginParams {
|
|
18
|
+
/** The name of the package being built; the first half of every stylesheet id. */
|
|
19
|
+
readonly packageName: string;
|
|
20
|
+
/** The directory of the package being built; stylesheet ids are relative to it. */
|
|
21
|
+
readonly packageDirectory: string;
|
|
22
|
+
/** The `dependencies` of the package being built, by name. */
|
|
23
|
+
readonly declaredDependencies: ReadonlySet<string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getPackageNameOf = (specifier: string) => {
|
|
27
|
+
const [first, second] = specifier.split("/");
|
|
28
|
+
|
|
29
|
+
return specifier.startsWith("@") ? `${first}/${second}` : first;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compiles every stylesheet the package imports into a module that declares
|
|
34
|
+
* it as a stylesheet injectable of the package's Feature:
|
|
35
|
+
*
|
|
36
|
+
* import { asStylesheet } from "@k8slens/stylesheet-contracts";
|
|
37
|
+
* export const stylesheet = asStylesheet({ id: "<package>/<file>", css: "…" });
|
|
38
|
+
* export default { local: "scoped" }; // css modules only
|
|
39
|
+
*
|
|
40
|
+
* The Feature picks the `stylesheet` export up through its stylesheet glob,
|
|
41
|
+
* and the renderer application applies it while the Feature is registered.
|
|
42
|
+
* A css module keeps its scoped class names as the default export, and its
|
|
43
|
+
* `@apply` rules go through tailwind as before. Nothing is written beside the
|
|
44
|
+
* bundle any more: the css travels inside it.
|
|
45
|
+
*
|
|
46
|
+
* A stylesheet of a dependency, imported by a bare specifier from the
|
|
47
|
+
* package's own sources (`import "xterm/css/xterm.css"`), is compiled the same
|
|
48
|
+
* way and identified by that specifier. Nothing registers it by itself, since
|
|
49
|
+
* it is not under the Feature's stylesheet glob: the package re-exports its
|
|
50
|
+
* `stylesheet` from an `*.injectable.ts` file, and the Feature registers it
|
|
51
|
+
* like any injectable.
|
|
52
|
+
*/
|
|
53
|
+
export const stylesheetPlugin = ({
|
|
54
|
+
packageName,
|
|
55
|
+
packageDirectory,
|
|
56
|
+
declaredDependencies,
|
|
57
|
+
}: StylesheetPluginParams): Plugin => ({
|
|
58
|
+
name: "lens-stylesheet",
|
|
59
|
+
|
|
60
|
+
setup: (build) => {
|
|
61
|
+
const isOwnSourceFile = (file: string) => {
|
|
62
|
+
const relative = path.relative(packageDirectory, file);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
!relative.startsWith("..") && !relative.split(path.sep).includes("node_modules") && /\.(t|j)sx?$/.test(file)
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
build.onResolve({ filter: isStylesheetPath }, (args) => {
|
|
70
|
+
if (args.path.startsWith(".")) {
|
|
71
|
+
const absolutePath = path.resolve(args.resolveDir, args.path);
|
|
72
|
+
|
|
73
|
+
if (path.relative(packageDirectory, absolutePath).startsWith("..")) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { path: absolutePath, namespace };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!isOwnSourceFile(args.importer)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const dependency = getPackageNameOf(args.path);
|
|
85
|
+
|
|
86
|
+
if (!declaredDependencies.has(dependency)) {
|
|
87
|
+
return {
|
|
88
|
+
errors: [
|
|
89
|
+
{
|
|
90
|
+
text: `"${packageName}" imports the stylesheet "${args.path}" of "${dependency}" without declaring "${dependency}" in its dependencies.`,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
return {
|
|
98
|
+
path: require.resolve(args.path, { paths: [packageDirectory] }),
|
|
99
|
+
namespace,
|
|
100
|
+
pluginData: { id: `${packageName}/${args.path}` },
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
return {
|
|
104
|
+
errors: [
|
|
105
|
+
{ text: `"${packageName}" imports the stylesheet "${args.path}", which "${dependency}" does not have.` },
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
build.onResolve({ filter: /.*/, namespace }, (args) => {
|
|
112
|
+
if (args.path !== stylesheetContractsPackageName) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!declaredDependencies.has(stylesheetContractsPackageName)) {
|
|
117
|
+
return {
|
|
118
|
+
errors: [
|
|
119
|
+
{
|
|
120
|
+
text: `"${packageName}" imports a stylesheet but does not declare "${stylesheetContractsPackageName}" in its dependencies. Every stylesheet becomes an injectable bound to its token.`,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Shared with the rest of the application, so that the token identity is: never bundled in.
|
|
127
|
+
return { path: args.path, external: true };
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
build.onLoad({ filter: /.*/, namespace }, async (args) => {
|
|
131
|
+
const isModule = isModulePath.test(args.path);
|
|
132
|
+
// Quiet: the legacy stylesheets trip sass warnings by the hundred, and a build
|
|
133
|
+
// that prints them drowns what matters. Errors still fail the build.
|
|
134
|
+
const compiled = compile(args.path, { logger: Logger.silent, importers: [tildeImporter(packageDirectory)] });
|
|
135
|
+
const processed = isModule
|
|
136
|
+
? await toCssModule(compiled.css, args.path)
|
|
137
|
+
: { css: compiled.css, classNames: undefined };
|
|
138
|
+
const loadedDirectories = compiled.loadedUrls.map((url) => path.dirname(fileURLToPath(url)));
|
|
139
|
+
const { css, unresolved } = inlineUrls(processed.css, [path.dirname(args.path), ...loadedDirectories]);
|
|
140
|
+
const classNames = processed.classNames;
|
|
141
|
+
const id: string =
|
|
142
|
+
args.pluginData?.id ?? `${packageName}/${path.relative(packageDirectory, args.path).split(path.sep).join("/")}`;
|
|
143
|
+
|
|
144
|
+
const contents = [
|
|
145
|
+
...(css.trim() === ""
|
|
146
|
+
? []
|
|
147
|
+
: [
|
|
148
|
+
`import { asStylesheet } from "${stylesheetContractsPackageName}";`,
|
|
149
|
+
`export const stylesheet = asStylesheet({ id: ${JSON.stringify(id)}, css: ${JSON.stringify(css)} });`,
|
|
150
|
+
]),
|
|
151
|
+
...(isModule ? [`export default ${JSON.stringify(classNames ?? {})};`] : []),
|
|
152
|
+
].join("\n");
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
contents,
|
|
156
|
+
loader: "js",
|
|
157
|
+
resolveDir: path.dirname(args.path),
|
|
158
|
+
watchFiles: compiled.loadedUrls.map((url) => fileURLToPath(url)),
|
|
159
|
+
warnings: unresolved.map((url) => ({
|
|
160
|
+
text: `"${path.relative(packageDirectory, args.path)}" refers to "${url}", which was not found next to the stylesheet or its imports; the browser will not find it either.`,
|
|
161
|
+
})),
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* `@import "~some-package/some/file"` reaches into a dependency, the way the
|
|
169
|
+
* webpack-era stylesheets were written. The package is found the way node
|
|
170
|
+
* would find it from the package being built, and sass takes it from there:
|
|
171
|
+
* the extension, the `_partial` prefix and the index file are its business.
|
|
172
|
+
*/
|
|
173
|
+
const tildeImporter = (packageDirectory: string): FileImporter<"sync"> => ({
|
|
174
|
+
findFileUrl: (url) => {
|
|
175
|
+
if (!url.startsWith("~")) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const specifier = url.slice(1);
|
|
180
|
+
const segments = specifier.split("/");
|
|
181
|
+
const packageNameLength = specifier.startsWith("@") ? 2 : 1;
|
|
182
|
+
const packageName = segments.slice(0, packageNameLength).join("/");
|
|
183
|
+
const packageRoot = findInNodeModules(packageName, packageDirectory);
|
|
184
|
+
|
|
185
|
+
return packageRoot ? pathToFileURL(path.join(packageRoot, ...segments.slice(packageNameLength))) : null;
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const findInNodeModules = (packageName: string, from: string): string | undefined => {
|
|
190
|
+
for (let directory = from; ; directory = path.dirname(directory)) {
|
|
191
|
+
const candidate = path.join(directory, "node_modules", packageName);
|
|
192
|
+
|
|
193
|
+
if (fs.existsSync(candidate)) {
|
|
194
|
+
return candidate;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (path.dirname(directory) === directory) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const mimeTypes: Record<string, string> = {
|
|
204
|
+
".ttf": "font/ttf",
|
|
205
|
+
".otf": "font/otf",
|
|
206
|
+
".woff": "font/woff",
|
|
207
|
+
".woff2": "font/woff2",
|
|
208
|
+
".eot": "application/vnd.ms-fontobject",
|
|
209
|
+
".svg": "image/svg+xml",
|
|
210
|
+
".png": "image/png",
|
|
211
|
+
".jpg": "image/jpeg",
|
|
212
|
+
".jpeg": "image/jpeg",
|
|
213
|
+
".gif": "image/gif",
|
|
214
|
+
".webp": "image/webp",
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const isExternalUrl = (url: string) => /^(data:|https?:|\/\/|#)/.test(url);
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A stylesheet travels inside the bundle as text, so a `url("../fonts/x.ttf")`
|
|
221
|
+
* it refers to has no file next to it any more when the browser reads it. The
|
|
222
|
+
* referenced file goes into the text as a data url instead — resolved the way
|
|
223
|
+
* the old css pipeline did, relative to the stylesheet being compiled, with
|
|
224
|
+
* the directories of its imports as fallbacks for a partial that refers to a
|
|
225
|
+
* neighbour of its own.
|
|
226
|
+
*/
|
|
227
|
+
const inlineUrls = (css: string, directories: string[]) => {
|
|
228
|
+
const unresolved: string[] = [];
|
|
229
|
+
|
|
230
|
+
const inlined = css.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/g, (match, quote: string, url: string) => {
|
|
231
|
+
if (isExternalUrl(url)) {
|
|
232
|
+
return match;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const filePath = url.replace(/[?#].*$/, "");
|
|
236
|
+
const found = directories
|
|
237
|
+
.map((directory) => path.resolve(directory, filePath))
|
|
238
|
+
.find((candidate) => fs.existsSync(candidate));
|
|
239
|
+
|
|
240
|
+
if (!found) {
|
|
241
|
+
unresolved.push(url);
|
|
242
|
+
|
|
243
|
+
return match;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const mimeType = mimeTypes[path.extname(found).toLowerCase()] ?? "application/octet-stream";
|
|
247
|
+
|
|
248
|
+
return `url(${quote}data:${mimeType};base64,${fs.readFileSync(found).toString("base64")}${quote})`;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return { css: inlined, unresolved };
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const toCssModule = async (css: string, filePath: string) => {
|
|
255
|
+
let classNames: Record<string, string> = {};
|
|
256
|
+
|
|
257
|
+
// postcss-modules and tailwind resolve their own copy of postcss, whose plugin
|
|
258
|
+
// type the compiler will not compare to ours; at runtime it is the same shape.
|
|
259
|
+
const plugins = [
|
|
260
|
+
postcssModules({
|
|
261
|
+
generateScopedName,
|
|
262
|
+
getJSON: (_cssFileName: string, json: Record<string, string>) => {
|
|
263
|
+
classNames = json;
|
|
264
|
+
},
|
|
265
|
+
}),
|
|
266
|
+
tailwindcss,
|
|
267
|
+
] as unknown as AcceptedPlugin[];
|
|
268
|
+
|
|
269
|
+
const result = await postcss(plugins).process(css, { from: filePath, map: false });
|
|
270
|
+
|
|
271
|
+
return { css: result.css, classNames };
|
|
272
|
+
};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import glob from "fast-glob";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { resolve as resolvePath } from "node:path";
|
|
4
|
-
export const globImportPlugin = () => ({
|
|
5
|
-
name: "glob-import-plugin",
|
|
6
|
-
setup: (build) => {
|
|
7
|
-
const uniquenessCache = new Map();
|
|
8
|
-
build.onResolve({ filter: /\*/ }, async (args) => {
|
|
9
|
-
const { resolveDir, path } = args;
|
|
10
|
-
if (resolveDir === "") {
|
|
11
|
-
return; // Ignore unresolvable paths
|
|
12
|
-
}
|
|
13
|
-
const cacheKey = resolvePath(resolveDir, path);
|
|
14
|
-
const uniqueId = getOrInsertWith(uniquenessCache, cacheKey, getRandomUniqueId);
|
|
15
|
-
return {
|
|
16
|
-
path: `${path}/proof-of-uniqueness/${uniqueId}`,
|
|
17
|
-
namespace: "import-glob",
|
|
18
|
-
pluginData: { resolveDir },
|
|
19
|
-
};
|
|
20
|
-
});
|
|
21
|
-
build.onLoad({ filter: /.*/, namespace: "import-glob" }, async (args) => {
|
|
22
|
-
const { path, pluginData: { resolveDir }, } = args;
|
|
23
|
-
const [globImportString] = path.split("/proof-of-uniqueness/");
|
|
24
|
-
const files = (await glob(globImportString, { cwd: resolveDir })).sort();
|
|
25
|
-
const contents = [
|
|
26
|
-
files.map((module, index) => `import * as module${index} from './${module}'`).join(";"),
|
|
27
|
-
"",
|
|
28
|
-
`const modules = [${files.map((_, index) => `module${index}`).join(",")}];`,
|
|
29
|
-
"",
|
|
30
|
-
"export default modules;",
|
|
31
|
-
`export const filenames = [${files.map((module) => `'${module}'`).join(",")}]`,
|
|
32
|
-
"",
|
|
33
|
-
].join("\n");
|
|
34
|
-
return { contents, resolveDir };
|
|
35
|
-
});
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
const getRandomUniqueId = () => randomUUID();
|
|
39
|
-
const getOrInsertWith = (map, key, builder) => {
|
|
40
|
-
if (map.has(key)) {
|
|
41
|
-
return map.get(key);
|
|
42
|
-
}
|
|
43
|
-
const value = builder();
|
|
44
|
-
map.set(key, value);
|
|
45
|
-
return value;
|
|
46
|
-
};
|
package/src/package-build.js
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { build } from "esbuild";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { sassPlugin, postcssModules } from "esbuild-sass-plugin";
|
|
4
|
-
import { readFileSync } from "node:fs";
|
|
5
|
-
import { builtinModules as rawBuiltinModules } from "node:module";
|
|
6
|
-
import yargs from "yargs";
|
|
7
|
-
import { globImportPlugin } from "./glob-import-plugin";
|
|
8
|
-
import tailwindcss from "tailwindcss";
|
|
9
|
-
import { partition } from "lodash";
|
|
10
|
-
const CWD = process.cwd();
|
|
11
|
-
const packageJson = JSON.parse(readFileSync(path.resolve(CWD, "package.json"), "utf-8"));
|
|
12
|
-
const isExperimentalModule = (module) => module.startsWith("_");
|
|
13
|
-
const alsoAddNodeRefs = (module) => [module, `node:${module}`];
|
|
14
|
-
const { builtinModules, disallowedBuiltinModules } = (() => {
|
|
15
|
-
const [disallowed, allowed] = partition(rawBuiltinModules, isExperimentalModule);
|
|
16
|
-
const disallowedBuiltinModules = new Set(disallowed.flatMap(alsoAddNodeRefs));
|
|
17
|
-
const builtinModules = new Set(allowed.flatMap(alsoAddNodeRefs));
|
|
18
|
-
return { disallowedBuiltinModules, builtinModules };
|
|
19
|
-
})();
|
|
20
|
-
const getPackageName = (path) => {
|
|
21
|
-
const [firstPart, secondPart] = path.split("/");
|
|
22
|
-
if (path.startsWith("@")) {
|
|
23
|
-
return [firstPart, secondPart].join("/");
|
|
24
|
-
}
|
|
25
|
-
return firstPart;
|
|
26
|
-
};
|
|
27
|
-
const bundleDependency = null;
|
|
28
|
-
const ensureCorrectImportsPlugin = {
|
|
29
|
-
name: "ensure-correct-imports",
|
|
30
|
-
setup: (build) => {
|
|
31
|
-
build.onResolve({ filter: /^\./ }, (args) => {
|
|
32
|
-
const fullPath = path.resolve(args.resolveDir, args.path);
|
|
33
|
-
const relativePathToImporter = path.relative(CWD, args.importer);
|
|
34
|
-
const relativePath = path.relative(CWD, fullPath);
|
|
35
|
-
if (relativePath.startsWith("..") && !relativePathToImporter.startsWith("..")) {
|
|
36
|
-
throw new Error(`Tried to import "${args.path}" from "${relativePathToImporter}" but it is outside of the package.`);
|
|
37
|
-
}
|
|
38
|
-
return bundleDependency;
|
|
39
|
-
});
|
|
40
|
-
build.onResolve({ filter: /^[^.]/ }, async (args) => {
|
|
41
|
-
// Enforce declared imports only for the package's own sources (.ts/.tsx/.js/.jsx).
|
|
42
|
-
// Anything imported from outside the package directory — node_modules files or
|
|
43
|
-
// other workspace packages' dists (whose realpaths live outside node_modules) —
|
|
44
|
-
// belongs to an already-bundled dependency and must keep bundling freely.
|
|
45
|
-
const relativePathToImporter = path.relative(CWD, args.importer);
|
|
46
|
-
const importerIsOwnFile = !relativePathToImporter.startsWith("..") &&
|
|
47
|
-
!relativePathToImporter.split(path.sep).includes("node_modules");
|
|
48
|
-
const importerIsSourceFile = /\.(t|j)sx?$/.test(args.importer);
|
|
49
|
-
if (!importerIsOwnFile || !importerIsSourceFile) {
|
|
50
|
-
return bundleDependency;
|
|
51
|
-
}
|
|
52
|
-
if (disallowedBuiltinModules.has(args.path)) {
|
|
53
|
-
throw new Error(`Tried to import "${args.path}" from "${args.importer}" but it is a disallowed builtin module.`);
|
|
54
|
-
}
|
|
55
|
-
if (builtinModules.has(args.path)) {
|
|
56
|
-
return {
|
|
57
|
-
external: true,
|
|
58
|
-
path: args.path,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
const packageName = getPackageName(args.path);
|
|
62
|
-
const isExplicitDependency = packageName in (packageJson.dependencies ?? {});
|
|
63
|
-
const isExplicitDevDependency = packageName in (packageJson.devDependencies ?? {});
|
|
64
|
-
if (isExplicitDependency) {
|
|
65
|
-
// Note: Return the full path since we assume that it is a "exports" entry of the dependency found in package.json
|
|
66
|
-
return {
|
|
67
|
-
external: true,
|
|
68
|
-
path: args.path,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
if (isExplicitDevDependency) {
|
|
72
|
-
return bundleDependency;
|
|
73
|
-
}
|
|
74
|
-
throw new Error(`Tried to import "${args.path}" from "${relativePathToImporter}" but it is not a dependency of the package.`);
|
|
75
|
-
});
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
const parsedArgs = yargs()
|
|
79
|
-
.options({
|
|
80
|
-
tsconfig: {
|
|
81
|
-
type: "string",
|
|
82
|
-
default: "tsconfig.json",
|
|
83
|
-
description: 'Custom tsconfig.json -file, defaults to "tsconfig.json"',
|
|
84
|
-
},
|
|
85
|
-
"entry-point": {
|
|
86
|
-
type: "string",
|
|
87
|
-
default: "index.ts",
|
|
88
|
-
description: 'Custom entry-point, defaults to "index.ts"',
|
|
89
|
-
},
|
|
90
|
-
outfile: {
|
|
91
|
-
type: "string",
|
|
92
|
-
default: path.resolve(CWD, "dist/index.js"),
|
|
93
|
-
description: `Custom outfile, defaults to "dist/index.js"`,
|
|
94
|
-
},
|
|
95
|
-
format: {
|
|
96
|
-
choices: ["cjs", "esm"],
|
|
97
|
-
default: "cjs",
|
|
98
|
-
description: `Transpilation output format, defaults to "cjs"`,
|
|
99
|
-
},
|
|
100
|
-
})
|
|
101
|
-
.parseSync(process.argv);
|
|
102
|
-
// Copied from https://github.com/evanw/esbuild/issues/1051
|
|
103
|
-
const nativeNodeModulesPlugin = {
|
|
104
|
-
name: "native-node-modules",
|
|
105
|
-
setup(build) {
|
|
106
|
-
// If a ".node" file is imported within a module in the "file" namespace, resolve
|
|
107
|
-
// it to an absolute path and put it into the "node-file" virtual namespace.
|
|
108
|
-
build.onResolve({ filter: /\.node$/, namespace: "file" }, (args) => ({
|
|
109
|
-
path: require.resolve(args.path, { paths: [args.resolveDir] }),
|
|
110
|
-
namespace: "node-file",
|
|
111
|
-
}));
|
|
112
|
-
// Files in the "node-file" virtual namespace call "require()" on the
|
|
113
|
-
// path from esbuild of the ".node" file in the output directory.
|
|
114
|
-
build.onLoad({ filter: /.*/, namespace: "node-file" }, (args) => ({
|
|
115
|
-
contents: `
|
|
116
|
-
import path from ${JSON.stringify(args.path)}
|
|
117
|
-
try { module.exports = require(path) }
|
|
118
|
-
catch {}
|
|
119
|
-
`,
|
|
120
|
-
}));
|
|
121
|
-
// If a ".node" file is imported within a module in the "node-file" namespace, put
|
|
122
|
-
// it in the "file" namespace where esbuild's default loading behavior will handle
|
|
123
|
-
// it. It is already an absolute path since we resolved it to one above.
|
|
124
|
-
build.onResolve({ filter: /\.node$/, namespace: "node-file" }, (args) => ({
|
|
125
|
-
path: args.path,
|
|
126
|
-
namespace: "file",
|
|
127
|
-
}));
|
|
128
|
-
// Tell esbuild's default loading behavior to use the "file" loader for
|
|
129
|
-
// these ".node" files.
|
|
130
|
-
const opts = build.initialOptions;
|
|
131
|
-
opts.loader = opts.loader || {};
|
|
132
|
-
opts.loader[".node"] = "file";
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
// Keep .png imports as require() calls in the dist bundle instead of inlining
|
|
136
|
-
// them as base64 data urls, so images ship as files loaded on demand rather
|
|
137
|
-
// than megabytes of string literals in every consuming bundle. The import
|
|
138
|
-
// path is rewritten to stay correct relative to the output file, since the
|
|
139
|
-
// application build resolves it from there with its "file" loader.
|
|
140
|
-
const externalImagesPlugin = {
|
|
141
|
-
name: "external-images",
|
|
142
|
-
setup(build) {
|
|
143
|
-
build.onResolve({ filter: /\.png$/ }, (args) => {
|
|
144
|
-
const absolutePath = path.resolve(args.resolveDir, args.path);
|
|
145
|
-
const outputDirectory = path.dirname(path.resolve(CWD, parsedArgs.outfile));
|
|
146
|
-
const relativePath = path.relative(outputDirectory, absolutePath).split(path.sep).join("/");
|
|
147
|
-
return {
|
|
148
|
-
path: relativePath.startsWith(".") ? relativePath : `./${relativePath}`,
|
|
149
|
-
external: true,
|
|
150
|
-
};
|
|
151
|
-
});
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
export const packageBuild = async () => {
|
|
155
|
-
try {
|
|
156
|
-
await build({
|
|
157
|
-
entryPoints: [parsedArgs.entryPoint],
|
|
158
|
-
tsconfig: parsedArgs.tsconfig,
|
|
159
|
-
outfile: parsedArgs.outfile,
|
|
160
|
-
format: parsedArgs.format,
|
|
161
|
-
bundle: true,
|
|
162
|
-
platform: "node",
|
|
163
|
-
target: "node16",
|
|
164
|
-
sourcemap: "inline",
|
|
165
|
-
plugins: [
|
|
166
|
-
ensureCorrectImportsPlugin,
|
|
167
|
-
externalImagesPlugin,
|
|
168
|
-
globImportPlugin(),
|
|
169
|
-
sassPlugin({
|
|
170
|
-
filter: /\.module\.scss$/,
|
|
171
|
-
transform: postcssModules({
|
|
172
|
-
basedir: __dirname,
|
|
173
|
-
generateScopedName: "[name]__[local]___[hash:base64:5]",
|
|
174
|
-
}, [tailwindcss]),
|
|
175
|
-
}),
|
|
176
|
-
sassPlugin({
|
|
177
|
-
filter: /\.scss$/,
|
|
178
|
-
}),
|
|
179
|
-
nativeNodeModulesPlugin,
|
|
180
|
-
],
|
|
181
|
-
loader: {
|
|
182
|
-
".ttf": "dataurl",
|
|
183
|
-
".svg": "text",
|
|
184
|
-
".yaml": "text",
|
|
185
|
-
".yml": "text",
|
|
186
|
-
".yml.hb": "text",
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
process.exit(1);
|
|
192
|
-
}
|
|
193
|
-
};
|