@k8slens/package-build 2.2.3 → 3.0.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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +488 -319
- 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 +7 -5
- 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/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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k8slens/package-build",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"lens": {
|
|
5
5
|
"env": "agnostic",
|
|
6
6
|
"layer": "infrastructure",
|
|
@@ -50,26 +50,28 @@
|
|
|
50
50
|
"dist"
|
|
51
51
|
],
|
|
52
52
|
"scripts": {
|
|
53
|
-
"build:infrastructure:code": "esbuild --bundle index.ts --outfile=dist/index.js --external:node:* --external:postcss --external:esbuild --external:fast-glob --external:
|
|
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
54
|
"build:infrastructure:types": "tsc --project .",
|
|
55
55
|
"build:infrastructure": "npm run build:infrastructure:code; npm run build:infrastructure:types",
|
|
56
56
|
"lint": "lens-verify-code-style",
|
|
57
57
|
"clean": "exit 0",
|
|
58
|
-
"check-types:tests": "lens-types-build --checkTypesTests"
|
|
58
|
+
"check-types:tests": "lens-types-build --checkTypesTests",
|
|
59
|
+
"test:unit": "jest"
|
|
59
60
|
},
|
|
60
61
|
"dependencies": {
|
|
61
62
|
"esbuild": "^0.25.4",
|
|
62
|
-
"esbuild-sass-plugin": "^3.3.1",
|
|
63
63
|
"fast-glob": "^3.3.3",
|
|
64
64
|
"lodash": "^4.17.21",
|
|
65
65
|
"postcss": "^8.5.18",
|
|
66
66
|
"postcss-modules": "^6.0.1",
|
|
67
|
+
"sass": "^1.89.0",
|
|
67
68
|
"tailwindcss": "^3.4.17",
|
|
68
69
|
"yargs": "^17.7.2"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
72
|
"@async-fn/jest": "^1.6.4",
|
|
72
|
-
"@k8slens/typescript": "^
|
|
73
|
+
"@k8slens/typescript": "^8.0.0",
|
|
74
|
+
"@lensapp/jest": "^7.2.0"
|
|
73
75
|
},
|
|
74
76
|
"publishConfig": {
|
|
75
77
|
"access": "public",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "overlayscrollbars/overlayscrollbars.css";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./some.scss";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$partialColor: purple;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$unused: 1;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
not really a font
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * as ofDependency from "overlayscrollbars/overlayscrollbars.css";
|
|
2
|
+
|
|
3
|
+
export * as global from "./global.scss";
|
|
4
|
+
export * as someModule from "./some.module.scss";
|
|
5
|
+
export * as importer from "./importer.scss";
|
|
6
|
+
export * as empty from "./empty.scss";
|
|
7
|
+
export * as plain from "./plain.css";
|
|
8
|
+
export * as plainModule from "./plain.module.css";
|
|
9
|
+
export * as tilde from "./tilde.scss";
|
|
10
|
+
export * as fonts from "./fonts.scss";
|
package/src/package-build.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { type Plugin, build } from "esbuild";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { sassPlugin, postcssModules } from "esbuild-sass-plugin";
|
|
4
3
|
import { readFileSync } from "node:fs";
|
|
5
4
|
import { builtinModules as rawBuiltinModules } from "node:module";
|
|
6
5
|
import yargs from "yargs";
|
|
7
6
|
import { globImportPlugin } from "./glob-import-plugin";
|
|
8
|
-
import
|
|
7
|
+
import { stylesheetPlugin } from "./stylesheet-plugin";
|
|
9
8
|
import { partition } from "lodash";
|
|
10
9
|
|
|
11
10
|
const CWD = process.cwd();
|
|
@@ -199,22 +198,15 @@ export const packageBuild = async () => {
|
|
|
199
198
|
target: "node16",
|
|
200
199
|
sourcemap: "inline",
|
|
201
200
|
plugins: [
|
|
201
|
+
// First: a dependency's stylesheet is compiled into the bundle, not left external.
|
|
202
|
+
stylesheetPlugin({
|
|
203
|
+
packageName: packageJson.name,
|
|
204
|
+
packageDirectory: CWD,
|
|
205
|
+
declaredDependencies: new Set(Object.keys(packageJson.dependencies ?? {})),
|
|
206
|
+
}),
|
|
202
207
|
ensureCorrectImportsPlugin,
|
|
203
208
|
externalImagesPlugin,
|
|
204
209
|
globImportPlugin(),
|
|
205
|
-
sassPlugin({
|
|
206
|
-
filter: /\.module\.scss$/,
|
|
207
|
-
transform: postcssModules(
|
|
208
|
-
{
|
|
209
|
-
basedir: __dirname,
|
|
210
|
-
generateScopedName: "[name]__[local]___[hash:base64:5]",
|
|
211
|
-
},
|
|
212
|
-
[tailwindcss],
|
|
213
|
-
),
|
|
214
|
-
}),
|
|
215
|
-
sassPlugin({
|
|
216
|
-
filter: /\.scss$/,
|
|
217
|
-
}),
|
|
218
210
|
nativeNodeModulesPlugin,
|
|
219
211
|
],
|
|
220
212
|
loader: {
|
|
@@ -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
|
-
};
|