@animus-ui/layergate 0.0.0-stage → 0.1.36
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/README.md +239 -2
- package/dist/compile-C4GyCiuY.cjs +323 -0
- package/dist/compile-Cu_GFS3l.mjs +295 -0
- package/dist/compile-DZwVBjTM.d.cts +126 -0
- package/dist/compile-DZwVBjTM.d.cts.map +1 -0
- package/dist/compile-DZwVBjTM.d.mts +126 -0
- package/dist/compile-DZwVBjTM.d.mts.map +1 -0
- package/dist/host-BGcb1rMJ.mjs +169 -0
- package/dist/host-C4ZaMdkx.cjs +210 -0
- package/dist/index.cjs +6 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +3 -0
- package/dist/plan-Bsn_yxS9.mjs +29 -0
- package/dist/plan-hvk04qzh.cjs +40 -0
- package/dist/plan-w-y0limy.d.cts +35 -0
- package/dist/plan-w-y0limy.d.cts.map +1 -0
- package/dist/plan-w-y0limy.d.mts +35 -0
- package/dist/plan-w-y0limy.d.mts.map +1 -0
- package/dist/sheet-BUnyt5Jt.cjs +576 -0
- package/dist/sheet-DJGhbeYI.mjs +571 -0
- package/dist/vite.cjs +295 -0
- package/dist/vite.d.cts +59 -0
- package/dist/vite.d.cts.map +1 -0
- package/dist/vite.d.mts +59 -0
- package/dist/vite.d.mts.map +1 -0
- package/dist/vite.mjs +290 -0
- package/dist/webpack-loader.cjs +104 -0
- package/dist/webpack-loader.d.cts +35 -0
- package/dist/webpack-loader.d.cts.map +1 -0
- package/dist/webpack-loader.d.mts +36 -0
- package/dist/webpack-loader.d.mts.map +1 -0
- package/dist/webpack-loader.mjs +104 -0
- package/dist/webpack-state-BpF2PPjA.d.cts +15 -0
- package/dist/webpack-state-BpF2PPjA.d.cts.map +1 -0
- package/dist/webpack-state-BtmtzsWZ.cjs +61 -0
- package/dist/webpack-state-CXoQQ7XR.d.mts +15 -0
- package/dist/webpack-state-CXoQQ7XR.d.mts.map +1 -0
- package/dist/webpack-state-DwltSkQq.mjs +44 -0
- package/dist/webpack.cjs +161 -0
- package/dist/webpack.d.cts +98 -0
- package/dist/webpack.d.cts.map +1 -0
- package/dist/webpack.d.mts +98 -0
- package/dist/webpack.d.mts.map +1 -0
- package/dist/webpack.mjs +155 -0
- package/package.json +90 -4
package/README.md
CHANGED
|
@@ -1,3 +1,240 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @animus-ui/layergate
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
For the vendor stylesheet you have to import: a date picker's, a grid
|
|
4
|
+
library's, a package's `styles.css`. layergate loads it into a cascade
|
|
5
|
+
layer below yours, so its normal rules can't fight your design system,
|
|
6
|
+
whatever their specificity or load order.
|
|
7
|
+
|
|
8
|
+
A stylesheet outside any `@layer` beats every layered rule, so vendor CSS
|
|
9
|
+
imported as usual overrides a design system or utility framework that uses
|
|
10
|
+
layers. layergate loads that CSS itself, inlines its `@import`s, and puts
|
|
11
|
+
each file's own rules in one layer below yours:
|
|
12
|
+
|
|
13
|
+
```css
|
|
14
|
+
@layer vendor, app;
|
|
15
|
+
@layer vendor {
|
|
16
|
+
/* react-grid-layout/css/styles.css, as written */
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Your own CSS is out of scope. App stylesheets and CSS Modules stay as you
|
|
21
|
+
configure them: layergate layers only the vendor files you declare
|
|
22
|
+
(`sheets`) or opt into (`dependencies`), including where your CSS imports
|
|
23
|
+
one.
|
|
24
|
+
|
|
25
|
+
## Vite
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { layergate } from '@animus-ui/layergate/vite';
|
|
29
|
+
// vite.config.ts
|
|
30
|
+
import { defineConfig } from 'vite';
|
|
31
|
+
|
|
32
|
+
export default defineConfig({
|
|
33
|
+
plugins: [
|
|
34
|
+
layergate({
|
|
35
|
+
layer: 'vendor',
|
|
36
|
+
order: ['vendor', 'base', 'components', 'utilities'],
|
|
37
|
+
sheets: ['react-grid-layout/css/styles.css', './styles/legacy.css'],
|
|
38
|
+
dependencies: ['react-datepicker'],
|
|
39
|
+
optOut: ['./styles/legacy/keep-unlayered.css'],
|
|
40
|
+
}),
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// your entry module, once, before other CSS
|
|
47
|
+
import 'virtual:layergate';
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Option | Meaning |
|
|
51
|
+
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| `layer` | The layer owned stylesheets go into. Default `vendor`. |
|
|
53
|
+
| `order` | The full layer order every owned stylesheet declares first. It must include `layer`. Default `[layer]`. |
|
|
54
|
+
| `sheets` | Stylesheets `virtual:layergate` delivers, in this order, whether or not anything else imports them: package specifiers or paths relative to the Vite root. |
|
|
55
|
+
| `dependencies` | Also own the `.css` a JS module imports from a package other than your app: `true` for every package, or package names. Default: none. |
|
|
56
|
+
| `optOut` | Files inside an owned stylesheet whose own rules stay outside the layer. Exact specifiers or paths, never globs. |
|
|
57
|
+
|
|
58
|
+
A JS module's import of an owned stylesheet, by any spelling, gets the same
|
|
59
|
+
single copy, and the declared sheets always come first, in their declared
|
|
60
|
+
order, whichever owned stylesheet the app imports first. Vite's own CSS
|
|
61
|
+
pipeline then processes each one once: PostCSS, minification, injection and
|
|
62
|
+
extraction. A nested `@import` of a package resolves as a CSS import does,
|
|
63
|
+
through its `style` entry. Fonts and images keep resolving, because
|
|
64
|
+
layergate rewrites a relative `url()` to resolve from the delivered file.
|
|
65
|
+
Edits to an owned file, nested imports and files under `node_modules`
|
|
66
|
+
included, hot-update in place.
|
|
67
|
+
|
|
68
|
+
## webpack
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
// webpack.config.js
|
|
72
|
+
const { LayergatePlugin } = require('@animus-ui/layergate/webpack');
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
plugins: [
|
|
76
|
+
new LayergatePlugin({
|
|
77
|
+
layer: 'vendor',
|
|
78
|
+
order: ['vendor', 'base', 'components', 'utilities'],
|
|
79
|
+
sheets: ['react-grid-layout/css/styles.css'],
|
|
80
|
+
dependencies: ['react-datepicker'],
|
|
81
|
+
declaredBy: require.resolve('./src/styles/app.css'),
|
|
82
|
+
}),
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The plugin adds no rule. It joins every loader chain of yours whose
|
|
88
|
+
css-loader is explicitly global (`modules: false`, as Next's global chains
|
|
89
|
+
are), just before PostCSS runs, and raises that css-loader's
|
|
90
|
+
`importLoaders` by one, so every stylesheet passes through it, imported
|
|
91
|
+
ones included. An owned stylesheet arrives finished, its `@import`s
|
|
92
|
+
resolved as css-loader resolves them and inlined, so css-loader never sees
|
|
93
|
+
a `layer()` import. PostCSS, Sass and css-loader keep running as before.
|
|
94
|
+
|
|
95
|
+
Any other chain is left untouched. One whose css-loader may treat a file
|
|
96
|
+
as a CSS Module (`modules` unset, or `auto`) is reported in one line
|
|
97
|
+
(`layergate.coverage`); a chain that only builds CSS Modules, such as
|
|
98
|
+
Next's, is left alone silently.
|
|
99
|
+
|
|
100
|
+
### CSS Modules alongside layergate
|
|
101
|
+
|
|
102
|
+
layergate does not decide which stylesheets are CSS Modules. If one rule
|
|
103
|
+
handles both, split it: global CSS in a rule whose css-loader has
|
|
104
|
+
`modules: false`, which layergate joins, and CSS Modules in a rule of
|
|
105
|
+
their own, which it leaves alone.
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
module.exports = {
|
|
109
|
+
module: {
|
|
110
|
+
rules: [
|
|
111
|
+
{
|
|
112
|
+
test: /\.css$/,
|
|
113
|
+
exclude: /\.module\.css$/,
|
|
114
|
+
use: [
|
|
115
|
+
'style-loader',
|
|
116
|
+
{ loader: 'css-loader', options: { modules: false } },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
test: /\.module\.css$/,
|
|
121
|
+
use: [
|
|
122
|
+
'style-loader',
|
|
123
|
+
{ loader: 'css-loader', options: { modules: true } },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Next already builds CSS this way.
|
|
132
|
+
|
|
133
|
+
What layergate returns is never cached by webpack, so a warm start with a
|
|
134
|
+
persistent cache finishes owned stylesheets again: about 118 ms per 200
|
|
135
|
+
vendor sheets in a synthetic measurement. Caching them safely needs their
|
|
136
|
+
provenance (the files and plan they came from) kept with the cache, a
|
|
137
|
+
possible later change. Ownership, opt-outs and
|
|
138
|
+
the declared sheets go by realpath; `url()`s are rebased from where webpack
|
|
139
|
+
reads each file, so `resolve.symlinks: false` resolves them as css-loader
|
|
140
|
+
does. The `declaredBy` stylesheet is never layered, even when an owned
|
|
141
|
+
stylesheet imports it.
|
|
142
|
+
|
|
143
|
+
webpack has no `virtual:layergate`. Pass `declaredBy`, the path of a
|
|
144
|
+
stylesheet of yours: the declared sheets are delivered at its head, and a
|
|
145
|
+
module's own import of one then loads nothing. Without it, import each
|
|
146
|
+
declared sheet yourself; it is owned wherever it is imported.
|
|
147
|
+
|
|
148
|
+
- A remote `@import` in an owned stylesheet is an error
|
|
149
|
+
(`layergate.remote`): webpack cannot load it inside the layer, and an
|
|
150
|
+
unlayered copy would escape it. Vendor the sheet locally, or opt the file
|
|
151
|
+
out if unlayered is acceptable.
|
|
152
|
+
- A chain that uses Lightning CSS instead of css-loader is not covered,
|
|
153
|
+
and is reported (`layergate.coverage`).
|
|
154
|
+
- In watch mode, an edit to any file an owned stylesheet read, files under
|
|
155
|
+
`node_modules` included, rebuilds it. The plugin makes a RegExp or
|
|
156
|
+
function `watchOptions.ignored` watch those files; a host that owns its
|
|
157
|
+
watch options passes `watchIgnored: false` and asks `plugin.watches(path)`.
|
|
158
|
+
- Turbopack runs no loader on the stylesheets a stylesheet imports, so
|
|
159
|
+
layergate cannot work there.
|
|
160
|
+
|
|
161
|
+
With Next and Animus, use the `quarantine` option of
|
|
162
|
+
`@animus-ui/next-plugin`, which sets this up for Next's client compiler.
|
|
163
|
+
|
|
164
|
+
## What it does to a stylesheet
|
|
165
|
+
|
|
166
|
+
- **Each file is decided on its own.** A file's own rules go into the
|
|
167
|
+
layer. An opted-out file's own rules stay outside it, under the
|
|
168
|
+
conditions of the `@import` that loads it, and the files it imports keep
|
|
169
|
+
their own decisions.
|
|
170
|
+
- **Imports keep their conditions.** `@import "x.css" layer(a) supports(…)
|
|
171
|
+
screen` becomes `@media screen { @supports (…) { @layer a { … } } }`
|
|
172
|
+
inside the layer, so its rules land in `vendor.a`. An anonymous `layer`
|
|
173
|
+
import gets a private layer of its own, never the shared one.
|
|
174
|
+
- **Text is kept as written.** Selectors, declarations, `!important`,
|
|
175
|
+
keyframes and the file's own layers reach the browser unchanged. Only a
|
|
176
|
+
relative `url()` in a file outside the delivered file's directory changes.
|
|
177
|
+
- **Remote imports stay remote.** Under Vite, an `http(s):` import is
|
|
178
|
+
hoisted to the top, in its layer, and reported, since its contents are
|
|
179
|
+
not inspected. Under webpack it is an error (see above).
|
|
180
|
+
|
|
181
|
+
## What a layer does not change
|
|
182
|
+
|
|
183
|
+
A layer bounds normal declarations only. These are kept and reported as
|
|
184
|
+
warnings, never stripped:
|
|
185
|
+
|
|
186
|
+
- `!important` declarations, which beat your normal declarations from any
|
|
187
|
+
layer (`layergate.important`);
|
|
188
|
+
- animations and keyframes, whose values override normal declarations
|
|
189
|
+
while they run (`layergate.animation`).
|
|
190
|
+
|
|
191
|
+
Font, keyframe and property names are global, whatever their layer.
|
|
192
|
+
|
|
193
|
+
## Not yet
|
|
194
|
+
|
|
195
|
+
- CSS Modules, Sass, Less and Stylus are left to Vite. Under webpack, Sass
|
|
196
|
+
output is finished like any CSS, and CSS Modules are left alone.
|
|
197
|
+
- An owned file can arrive more than once. A stylesheet Vite loads
|
|
198
|
+
itself, such as an app sheet whose `@import` reaches an owned file, is
|
|
199
|
+
finished by layergate too: its own rules stay unlayered and the owned
|
|
200
|
+
file is inlined layered, so no unlayered copy escapes. But each finished
|
|
201
|
+
stylesheet stands on its own, so the same owned file reached from two of
|
|
202
|
+
them is delivered in both, each copy layered. One delivery per file
|
|
203
|
+
across the document would need a document-wide registry, a possible
|
|
204
|
+
later increment.
|
|
205
|
+
- App CSS is left alone unless you list it in `sheets`. CSS that loads
|
|
206
|
+
before `virtual:layergate`, or another tool's layer order statement that
|
|
207
|
+
comes first, can still place your layers after `vendor`.
|
|
208
|
+
- A `.css` request with a query (`?inline`, `?url`, `?raw`) is left to Vite.
|
|
209
|
+
- A stylesheet with `@namespace` is an error (`layergate.namespace`): it
|
|
210
|
+
applies to a whole stylesheet, which a file inlined into another is not.
|
|
211
|
+
- A host's PostCSS runs once, over each finished stylesheet, not per file.
|
|
212
|
+
- Turbopack: not supported (see webpack above).
|
|
213
|
+
|
|
214
|
+
## Delivering from a host stylesheet
|
|
215
|
+
|
|
216
|
+
A tool with its own stylesheet module can deliver the declared sheets at
|
|
217
|
+
its head instead of through `virtual:layergate`: pass `declaredBy` (the
|
|
218
|
+
module's specifier, which an owned stylesheet a JS module imports then
|
|
219
|
+
loads first), and prepend `await plugin.stylesheetHead.call(this, id)` to
|
|
220
|
+
that module's CSS in its `load` hook. The head uses root-relative URLs, so
|
|
221
|
+
assets resolve from a virtual module too; a file outside the Vite root is
|
|
222
|
+
reached through `/@fs/`: in development a declared sheet's directory is
|
|
223
|
+
added to `server.fs.allow` when the server starts (beside your own list, or
|
|
224
|
+
Vite's default workspace root), and an asset it references in another
|
|
225
|
+
directory is allowed when it is found; in a build it is emitted as an
|
|
226
|
+
asset. Animus's `quarantine`
|
|
227
|
+
option works this way.
|
|
228
|
+
|
|
229
|
+
## Core API
|
|
230
|
+
|
|
231
|
+
`compileUnit(roots, { layer, order, resolve, read, quarantined, owns, url, base, cache })`
|
|
232
|
+
compiles stylesheet graphs, in order, into one finished stylesheet with the
|
|
233
|
+
host's resolver and file reader, and returns the CSS, a source map, the files
|
|
234
|
+
read, whether anything was layered, and the diagnostics. `owns` says which
|
|
235
|
+
files start an owned graph (default: all), and `url` maps a referenced file
|
|
236
|
+
to its URL text (default: relative to the first root).
|
|
237
|
+
`base` is the directory generated layer names are relative to (the project
|
|
238
|
+
root), so they are the same on every machine. `cache` keeps parsed files
|
|
239
|
+
between compiles; drop a file's entry when it changes. The Vite and webpack
|
|
240
|
+
adapters are built on it.
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
//#endregion
|
|
23
|
+
const require_sheet = require("./sheet-BUnyt5Jt.cjs");
|
|
24
|
+
let magic_string = require("magic-string");
|
|
25
|
+
magic_string = __toESM(magic_string, 1);
|
|
26
|
+
let node_crypto = require("node:crypto");
|
|
27
|
+
let node_path = require("node:path");
|
|
28
|
+
//#region src/compile.ts
|
|
29
|
+
/**
|
|
30
|
+
* Compiles one owned stylesheet graph into one finished stylesheet: every
|
|
31
|
+
* local `@import` inlined at its position, under its media, supports and
|
|
32
|
+
* layer conditions, and each file's own text placed inside the quarantine
|
|
33
|
+
* layer, or outside it for a file opted out. Each file is decided on its
|
|
34
|
+
* own, before assembly, so a parent's decision never reaches a child's
|
|
35
|
+
* text. File text is kept as written; only relative URLs are rewritten, to
|
|
36
|
+
* resolve from the finished stylesheet's location.
|
|
37
|
+
*/
|
|
38
|
+
const open = (frame) => frame.kind === "media" ? `@media ${frame.text} {\n` : frame.kind === "supports" ? `@supports (${frame.text}) {\n` : `@layer ${frame.text} {\n`;
|
|
39
|
+
/** A URL the finished stylesheet leaves to the browser. */
|
|
40
|
+
const isRemote = (url) => /^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(url);
|
|
41
|
+
/** A URL that resolves the same from any stylesheet location. */
|
|
42
|
+
const isLocationFree = (url) => url === "" || /^(?:[a-z][a-z0-9+.-]*:|\/|#|%23)/i.test(url);
|
|
43
|
+
const quoted = (url) => `"${url.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\a ")}"`;
|
|
44
|
+
/** `@import "x"` names a stylesheet relative to its own; a bare name that
|
|
45
|
+
* is no relative file is the package a bundler resolves. */
|
|
46
|
+
function candidates(url) {
|
|
47
|
+
if (url.startsWith("~")) return [url.slice(1)];
|
|
48
|
+
if (url.startsWith(".") || url.startsWith("/")) return [url];
|
|
49
|
+
return [`./${url}`, url];
|
|
50
|
+
}
|
|
51
|
+
/** Line and column (1-based) of each offset in `text`. */
|
|
52
|
+
function locator(text) {
|
|
53
|
+
const starts = [0];
|
|
54
|
+
for (let at = 0; at < text.length; at++) if (text[at] === "\n") starts.push(at + 1);
|
|
55
|
+
return (offset) => {
|
|
56
|
+
let low = 0;
|
|
57
|
+
let high = starts.length - 1;
|
|
58
|
+
while (low < high) {
|
|
59
|
+
const mid = low + high + 1 >> 1;
|
|
60
|
+
if (starts[mid] <= offset) low = mid;
|
|
61
|
+
else high = mid - 1;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
line: low + 1,
|
|
65
|
+
column: offset - starts[low] + 1
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** A URL's path and its `?query#fragment`. */
|
|
70
|
+
function splitSuffix(url) {
|
|
71
|
+
const at = url.search(/[?#]/);
|
|
72
|
+
return at === -1 ? [url, ""] : [url.slice(0, at), url.slice(at)];
|
|
73
|
+
}
|
|
74
|
+
/** The frames an import adds, media, then supports, then layer; an
|
|
75
|
+
* anonymous layer as `anonymous`. */
|
|
76
|
+
function importFrames(statement, anonymous) {
|
|
77
|
+
const frames = [];
|
|
78
|
+
if (statement.media) frames.push({
|
|
79
|
+
kind: "media",
|
|
80
|
+
text: statement.media
|
|
81
|
+
});
|
|
82
|
+
if (statement.supports !== null) frames.push({
|
|
83
|
+
kind: "supports",
|
|
84
|
+
text: statement.supports
|
|
85
|
+
});
|
|
86
|
+
if (statement.layer.kind === "named") frames.push({
|
|
87
|
+
kind: "layer",
|
|
88
|
+
text: statement.layer.name
|
|
89
|
+
});
|
|
90
|
+
else if (statement.layer.kind === "anonymous") frames.push({
|
|
91
|
+
kind: "layer",
|
|
92
|
+
text: anonymous()
|
|
93
|
+
});
|
|
94
|
+
return frames;
|
|
95
|
+
}
|
|
96
|
+
/** A remote `@import` hoisted to the top of the finished stylesheet: its
|
|
97
|
+
* layer is every layer on its way, its supports every supports, and its
|
|
98
|
+
* media the innermost media. */
|
|
99
|
+
function remoteImport(url, frames) {
|
|
100
|
+
const of = (kind) => frames.filter((frame) => frame.kind === kind).map((frame) => frame.text);
|
|
101
|
+
const [layers, supports, media] = [
|
|
102
|
+
of("layer"),
|
|
103
|
+
of("supports"),
|
|
104
|
+
of("media")
|
|
105
|
+
];
|
|
106
|
+
return [
|
|
107
|
+
`@import url(${quoted(url)})`,
|
|
108
|
+
layers.length > 0 ? ` layer(${layers.join(".")})` : "",
|
|
109
|
+
supports.length > 0 ? ` supports(${supports.map((text) => `(${text})`).join(" and ")})` : "",
|
|
110
|
+
media.length > 0 ? ` ${combinedMedia(media)}` : "",
|
|
111
|
+
";"
|
|
112
|
+
].join("");
|
|
113
|
+
}
|
|
114
|
+
/** Media conditions on the way to an import, as one: joined when each is a
|
|
115
|
+
* bare condition (`(min-width: 1px)`), otherwise the innermost. */
|
|
116
|
+
function combinedMedia(media) {
|
|
117
|
+
return media.every((text) => text.startsWith("(") && !text.includes(",")) ? media.join(" and ") : media.at(-1);
|
|
118
|
+
}
|
|
119
|
+
/** The layer names `frames` nest, outermost first. */
|
|
120
|
+
function layerPath(frames) {
|
|
121
|
+
return frames.filter((frame) => frame.kind === "layer").flatMap((frame) => frame.text.split("."));
|
|
122
|
+
}
|
|
123
|
+
/** Compiles `roots`, in order, into one finished stylesheet. */
|
|
124
|
+
async function compileUnit(roots, options) {
|
|
125
|
+
const deliveryDir = (0, node_path.dirname)(options.delivery ?? roots[0]);
|
|
126
|
+
/** The URL text reaching `target`; by default relative to the
|
|
127
|
+
* delivery's directory, so a file there keeps its URLs as written. */
|
|
128
|
+
const url = options.url ?? ((target) => (0, node_path.relative)(deliveryDir, target).split(node_path.sep).join("/"));
|
|
129
|
+
/** Files whose own text went into the layer, and owned files opted out. */
|
|
130
|
+
const layered = /* @__PURE__ */ new Set();
|
|
131
|
+
const optedOut = /* @__PURE__ */ new Set();
|
|
132
|
+
const bundle = new magic_string.Bundle({ separator: "" });
|
|
133
|
+
const diagnostics = [];
|
|
134
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
135
|
+
const remote = [];
|
|
136
|
+
/** Every layer path, in the order the original cascade first creates it. */
|
|
137
|
+
const created = /* @__PURE__ */ new Set();
|
|
138
|
+
const create = (path) => {
|
|
139
|
+
for (let depth = 1; depth <= path.length; depth++) {
|
|
140
|
+
const name = path.slice(0, depth).join(".");
|
|
141
|
+
created.add(name);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const report = (code, severity, message, file, at) => {
|
|
145
|
+
diagnostics.push({
|
|
146
|
+
code,
|
|
147
|
+
severity,
|
|
148
|
+
message,
|
|
149
|
+
file,
|
|
150
|
+
...loaded.get(file).locate(at)
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
/** A stable private name for the anonymous layer an import occurrence
|
|
154
|
+
* creates: from the unit and the chain of imports that reaches it from
|
|
155
|
+
* the root, so each occurrence, of the same file too, gets its own, and
|
|
156
|
+
* the same on every machine. */
|
|
157
|
+
const anonymousName = (occurrence) => () => `layergate-anonymous-${(0, node_crypto.createHash)("sha256").update(occurrence).digest("hex").slice(0, 10)}`;
|
|
158
|
+
/** What a file holds that the layer cannot hold back, or that layergate
|
|
159
|
+
* cannot place, reported once per file. */
|
|
160
|
+
const reportEffects = (file, sheet) => {
|
|
161
|
+
const effect = (offsets, code, what) => {
|
|
162
|
+
if (offsets.length === 0) return;
|
|
163
|
+
const them = offsets.length === 1 ? "it" : "them";
|
|
164
|
+
report(code, "warning", `${offsets.length} ${what} kept: the quarantine layer does not hold ${them} back`, file, offsets[0]);
|
|
165
|
+
};
|
|
166
|
+
effect(sheet.important, "layergate.important", "!important declaration(s)");
|
|
167
|
+
effect(sheet.keyframes, "layergate.animation", "@keyframes rule(s)");
|
|
168
|
+
effect(sheet.animations, "layergate.animation", "animation declaration(s)");
|
|
169
|
+
};
|
|
170
|
+
/** What layergate leaves as written or cannot place, reported once per
|
|
171
|
+
* file; `placed` says the file's text sits inside a block, where
|
|
172
|
+
* `@namespace` cannot. */
|
|
173
|
+
const reportFile = (file, sheet, placed) => {
|
|
174
|
+
for (const at of sheet.ignoredImports) report("layergate.ignored-import", "warning", "@import after other rules is ignored by browsers, and layergate leaves it as written", file, at);
|
|
175
|
+
if (placed && sheet.namespaces.length > 0) report("layergate.namespace", "error", "@namespace applies to a whole stylesheet, which layergate cannot place yet", file, sheet.namespaces[0]);
|
|
176
|
+
};
|
|
177
|
+
const load = async (file) => {
|
|
178
|
+
if (loaded.has(file)) return loaded.get(file);
|
|
179
|
+
let entry = null;
|
|
180
|
+
try {
|
|
181
|
+
let parsed = options.cache?.get(file);
|
|
182
|
+
if (!parsed) {
|
|
183
|
+
const text = String(await options.read(file));
|
|
184
|
+
parsed = {
|
|
185
|
+
text,
|
|
186
|
+
sheet: require_sheet.readSheet(text)
|
|
187
|
+
};
|
|
188
|
+
options.cache?.set(file, parsed);
|
|
189
|
+
}
|
|
190
|
+
const source = new magic_string.default(parsed.text);
|
|
191
|
+
const kept = !options.url && (0, node_path.dirname)(file) === deliveryDir;
|
|
192
|
+
for (const reference of kept ? [] : parsed.sheet.urls) {
|
|
193
|
+
if (isLocationFree(reference.url) || isRemote(reference.url)) continue;
|
|
194
|
+
const [path, suffix] = splitSuffix(reference.url);
|
|
195
|
+
const rebased = url((0, node_path.join)((0, node_path.dirname)(file), path)) + suffix;
|
|
196
|
+
if (rebased === reference.url) continue;
|
|
197
|
+
source.overwrite(reference.start, reference.end, reference.kind === "token" ? `url(${quoted(rebased)})` : quoted(rebased));
|
|
198
|
+
}
|
|
199
|
+
for (const [from, to] of parsed.sheet.removed) source.remove(from, to);
|
|
200
|
+
entry = {
|
|
201
|
+
...parsed,
|
|
202
|
+
source,
|
|
203
|
+
locate: locator(parsed.text)
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
diagnostics.push({
|
|
207
|
+
code: "layergate.unreadable",
|
|
208
|
+
severity: "error",
|
|
209
|
+
message: `cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
210
|
+
file
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
loaded.set(file, entry);
|
|
214
|
+
return entry;
|
|
215
|
+
};
|
|
216
|
+
/** The file's text from `from` to `to` inside `frames`, unless it holds
|
|
217
|
+
* only whitespace. */
|
|
218
|
+
const emit = (file, frames, from, to) => {
|
|
219
|
+
const entry = loaded.get(file);
|
|
220
|
+
let content = false;
|
|
221
|
+
let at = from;
|
|
222
|
+
for (const [start, end] of entry.sheet.removed) {
|
|
223
|
+
if (end <= at || start >= to) continue;
|
|
224
|
+
content ||= /\S/.test(entry.text.slice(at, start));
|
|
225
|
+
at = end;
|
|
226
|
+
}
|
|
227
|
+
if (!content && !/\S/.test(entry.text.slice(at, to))) return;
|
|
228
|
+
if (frames.length > 0) bundle.addSource({ content: new magic_string.default(frames.map(open).join("")) });
|
|
229
|
+
bundle.addSource({
|
|
230
|
+
filename: file,
|
|
231
|
+
content: entry.source.snip(from, to)
|
|
232
|
+
});
|
|
233
|
+
bundle.addSource({ content: new magic_string.default(`\n${"}\n".repeat(frames.length)}`) });
|
|
234
|
+
};
|
|
235
|
+
/** Emits `file` as one occurrence: `frames` are the conditions its import
|
|
236
|
+
* chain puts it under, `stack` the files on that chain, `occurrence` the
|
|
237
|
+
* chain's import indexes. A file imported twice is emitted at each
|
|
238
|
+
* import, as a browser applies it at each. */
|
|
239
|
+
const visit = async (file, frames, stack, occurrence, insideOwned) => {
|
|
240
|
+
const first = !loaded.has(file);
|
|
241
|
+
const entry = await load(file);
|
|
242
|
+
if (!entry) return;
|
|
243
|
+
const owned = insideOwned || (options.owns?.(file) ?? true);
|
|
244
|
+
const quarantined = owned && (options.quarantined?.(file) ?? true);
|
|
245
|
+
if (owned && !quarantined) optedOut.add(file);
|
|
246
|
+
if (first) reportFile(file, entry.sheet, quarantined || stack.length > 1);
|
|
247
|
+
if (quarantined && !layered.has(file)) {
|
|
248
|
+
layered.add(file);
|
|
249
|
+
reportEffects(file, entry.sheet);
|
|
250
|
+
}
|
|
251
|
+
const own = quarantined ? [{
|
|
252
|
+
kind: "layer",
|
|
253
|
+
text: options.layer
|
|
254
|
+
}, ...frames] : frames;
|
|
255
|
+
const ownPath = layerPath(own);
|
|
256
|
+
const createLayers = (from, to) => {
|
|
257
|
+
for (const { at, names } of entry.sheet.layers) if (at >= from && at < to) for (const name of names) create([...ownPath, ...name.split(".")]);
|
|
258
|
+
};
|
|
259
|
+
create(ownPath);
|
|
260
|
+
let cursor = 0;
|
|
261
|
+
for (const [index, statement] of entry.sheet.imports.entries()) {
|
|
262
|
+
createLayers(cursor, statement.start);
|
|
263
|
+
emit(file, own, cursor, statement.start);
|
|
264
|
+
cursor = statement.end;
|
|
265
|
+
const added = importFrames(statement, anonymousName(`${occurrence}/${index}`));
|
|
266
|
+
if (isRemote(statement.url)) {
|
|
267
|
+
const chain = [...own, ...added];
|
|
268
|
+
if (options.remote === "refuse" && chain.some((frame) => frame.kind !== "media")) {
|
|
269
|
+
report("layergate.remote", "error", `@import "${statement.url}" is remote, and this host cannot load it inside a layer: vendor the sheet locally, or opt this file out explicitly if unlayered is acceptable`, file, statement.start);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const media = chain.filter((frame) => frame.kind === "media").length;
|
|
273
|
+
const kept = media > 1 ? `; its ${media} media conditions are joined where each is a bare condition, else only the innermost is kept` : "";
|
|
274
|
+
report("layergate.remote", "warning", `@import "${statement.url}" stays remote, so its contents are not inspected${kept}`, file, statement.start);
|
|
275
|
+
create(layerPath(chain));
|
|
276
|
+
remote.push(remoteImport(statement.url, chain));
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
let child = null;
|
|
280
|
+
for (const candidate of candidates(statement.url)) {
|
|
281
|
+
child = await options.resolve(candidate, file);
|
|
282
|
+
if (child) break;
|
|
283
|
+
}
|
|
284
|
+
if (!child) report("layergate.unresolved", "error", `cannot resolve @import "${statement.url}"`, file, statement.start);
|
|
285
|
+
else if (stack.includes(child)) report("layergate.cycle", "warning", `@import "${statement.url}" imports a stylesheet already being imported; skipped`, file, statement.start);
|
|
286
|
+
else await visit(child, [...frames, ...added], [...stack, child], `${occurrence}/${index}`, owned);
|
|
287
|
+
}
|
|
288
|
+
createLayers(cursor, entry.text.length);
|
|
289
|
+
emit(file, own, cursor, entry.text.length);
|
|
290
|
+
};
|
|
291
|
+
for (const root of roots) {
|
|
292
|
+
const name = (0, node_path.relative)(options.base ?? deliveryDir, root).split(node_path.sep).join("/");
|
|
293
|
+
await visit(root, [], [root], name, false);
|
|
294
|
+
}
|
|
295
|
+
bundle.prepend([
|
|
296
|
+
`@layer ${options.order.join(", ")};\n`,
|
|
297
|
+
...remote.length > 0 ? [`@layer ${[...created].join(", ")};\n`] : [],
|
|
298
|
+
...remote.map((line) => `${line}\n`)
|
|
299
|
+
].join(""));
|
|
300
|
+
for (const file of optedOut) diagnostics.push({
|
|
301
|
+
code: "layergate.opt-out",
|
|
302
|
+
severity: "info",
|
|
303
|
+
message: `opted out: its own rules stay outside @layer ${options.layer}`,
|
|
304
|
+
file
|
|
305
|
+
});
|
|
306
|
+
return {
|
|
307
|
+
css: bundle.toString(),
|
|
308
|
+
map: options.map === false ? null : bundle.generateMap({
|
|
309
|
+
hires: "boundary",
|
|
310
|
+
includeContent: true
|
|
311
|
+
}),
|
|
312
|
+
files: [...loaded.keys()],
|
|
313
|
+
layered: layered.size > 0,
|
|
314
|
+
diagnostics
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
//#endregion
|
|
318
|
+
Object.defineProperty(exports, "compileUnit", {
|
|
319
|
+
enumerable: true,
|
|
320
|
+
get: function() {
|
|
321
|
+
return compileUnit;
|
|
322
|
+
}
|
|
323
|
+
});
|