@ultimat3/render 20.1.2 → 20.1.3
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/package.json +5 -5
- package/src/css-modules.ts +46 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/render",
|
|
3
|
-
"version": "20.1.
|
|
3
|
+
"version": "20.1.3",
|
|
4
4
|
"description": "The route primitive and the five render modes: static, isr, ssr, stream, spa.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"test": "bun test"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@ultimat3/cache": "20.1.
|
|
40
|
-
"@ultimat3/core": "20.1.
|
|
41
|
-
"@ultimat3/i18n": "20.1.
|
|
42
|
-
"@ultimat3/seo": "20.1.
|
|
39
|
+
"@ultimat3/cache": "20.1.3",
|
|
40
|
+
"@ultimat3/core": "20.1.3",
|
|
41
|
+
"@ultimat3/i18n": "20.1.3",
|
|
42
|
+
"@ultimat3/seo": "20.1.3",
|
|
43
43
|
"sass": "1.102.0"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/css-modules.ts
CHANGED
|
@@ -81,6 +81,47 @@ const CLASS_SELECTOR = /\.(-?[A-Za-z_][\w-]*)/g;
|
|
|
81
81
|
const NUL = '\u0000';
|
|
82
82
|
const MASKED = new RegExp(`${NUL}(\\d+)${NUL}`, 'g');
|
|
83
83
|
|
|
84
|
+
const GLOBAL_OPEN = ':global(';
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Unwrap every `:global(<selector>)` to `<selector>`, handing each payload to `keep` so the class
|
|
88
|
+
* rewrite that follows cannot touch it. `:global()` is CSS-MODULES syntax, not CSS: emitted as
|
|
89
|
+
* written it is an unknown pseudo-class, and a browser drops the whole rule. `Table.module.scss`
|
|
90
|
+
* shipped twelve such rules, so the catalog table reached every app with no cell padding, no row
|
|
91
|
+
* borders and no header ground — invisible to every test, because the CSS was never parsed by
|
|
92
|
+
* anything but a browser.
|
|
93
|
+
*
|
|
94
|
+
* A scanner rather than a regex: the payload is a selector and may hold its own parentheses
|
|
95
|
+
* (`tr:nth-child(even)`), so the wrapper closes on ITS parenthesis, found by depth. An unclosed
|
|
96
|
+
* wrapper is left exactly as written — swallowing the rest of the sheet to "repair" a typo would
|
|
97
|
+
* turn one broken rule into every rule after it.
|
|
98
|
+
*/
|
|
99
|
+
function unwrapGlobal(css: string, keep: (selector: string) => string): string {
|
|
100
|
+
let out = '';
|
|
101
|
+
let cursor = 0;
|
|
102
|
+
for (;;) {
|
|
103
|
+
const open = css.indexOf(GLOBAL_OPEN, cursor);
|
|
104
|
+
if (open === -1) return out + css.slice(cursor);
|
|
105
|
+
const start = open + GLOBAL_OPEN.length;
|
|
106
|
+
let depth = 1;
|
|
107
|
+
let end = start;
|
|
108
|
+
while (end < css.length && depth > 0) {
|
|
109
|
+
const char = css[end];
|
|
110
|
+
// A `{` means the selector list ended with the wrapper still open: not ours to repair.
|
|
111
|
+
if (char === '{') break;
|
|
112
|
+
if (char === '(') depth += 1;
|
|
113
|
+
else if (char === ')') depth -= 1;
|
|
114
|
+
end += 1;
|
|
115
|
+
}
|
|
116
|
+
if (depth !== 0) return out + css.slice(cursor);
|
|
117
|
+
// The payload is unwrapped FIRST, so `:global(:global(.x))` yields `.x` — a wrapper left inside
|
|
118
|
+
// the mask would be restored verbatim, and the browser would drop the rule after all.
|
|
119
|
+
out +=
|
|
120
|
+
css.slice(cursor, open) + keep(unwrapGlobal(css.slice(start, end - 1), (inner) => inner));
|
|
121
|
+
cursor = end;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
84
125
|
/**
|
|
85
126
|
* Rewrite every class selector to its scoped name and report the map. Done on the compiled CSS
|
|
86
127
|
* rather than the SCSS source so mixins, `@extend` and interpolation have already produced their
|
|
@@ -92,10 +133,13 @@ export function scopeClasses(
|
|
|
92
133
|
): { readonly css: string; readonly classes: Record<string, string> } {
|
|
93
134
|
const classes: Record<string, string> = {};
|
|
94
135
|
const literals: string[] = [];
|
|
95
|
-
const
|
|
136
|
+
const mask = (match: string): string => {
|
|
96
137
|
literals.push(match);
|
|
97
138
|
return `${NUL}${literals.length - 1}${NUL}`;
|
|
98
|
-
}
|
|
139
|
+
};
|
|
140
|
+
// Strings and `url()` first, so a `:global(` inside a `content:` string is never unwrapped;
|
|
141
|
+
// then each `:global()` payload joins the same mask and comes back unscoped.
|
|
142
|
+
const masked = unwrapGlobal(css.replace(PROTECTED, mask), mask);
|
|
99
143
|
const scoped = masked.replace(CLASS_SELECTOR, (_match, name: string) => {
|
|
100
144
|
const local = `${name}_${suffix}`;
|
|
101
145
|
classes[name] = local;
|