@sigil-dev/compiler 0.7.6 → 0.8.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/package.json +2 -1
- package/src/babel/handlers/ssr/state.ts +10 -10
- package/src/babel/index.ts +480 -430
- package/src/babel/jsx/anchor-mount.ts +28 -5
- package/src/babel/jsx/children.ts +124 -124
- package/src/babel/jsx/element.ts +830 -803
- package/src/babel/jsx/ssr.ts +319 -319
- package/src/babel/jsx/utils.ts +47 -3
- package/src/babel/util/bind.ts +139 -126
- package/src/babel/util/css.ts +151 -151
- package/src/babel/util/dead-code.ts +28 -28
- package/src/babel/util/magic.ts +1 -0
- package/src/bun-plugin.ts +89 -86
- package/test/hydration.test.ts +1 -1
- package/test/jsx.test.ts +197 -195
- package/test/tree-shaking.test.ts +78 -79
package/src/babel/jsx/utils.ts
CHANGED
|
@@ -6,12 +6,16 @@ export const ATTR_MAP: Record<string, string> = {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export function containsSignal(
|
|
9
|
-
expr: t.Expression,
|
|
9
|
+
expr: t.Expression | t.Node,
|
|
10
10
|
signals: Set<string>,
|
|
11
11
|
): boolean {
|
|
12
|
+
if (!expr) return false;
|
|
12
13
|
if (t.isIdentifier(expr)) return signals.has(expr.name);
|
|
13
14
|
if (t.isCallExpression(expr)) {
|
|
14
|
-
return
|
|
15
|
+
return (
|
|
16
|
+
containsSignal(expr.callee as t.Expression, signals) ||
|
|
17
|
+
expr.arguments.some((a) => containsSignal(a as t.Expression, signals))
|
|
18
|
+
);
|
|
15
19
|
}
|
|
16
20
|
if (t.isBinaryExpression(expr)) {
|
|
17
21
|
return (
|
|
@@ -39,7 +43,47 @@ export function containsSignal(
|
|
|
39
43
|
return containsSignal(expr.argument as t.Expression, signals);
|
|
40
44
|
}
|
|
41
45
|
if (t.isTemplateLiteral(expr)) {
|
|
42
|
-
return expr.expressions.some(e =>
|
|
46
|
+
return expr.expressions.some((e) =>
|
|
47
|
+
containsSignal(e as t.Expression, signals),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
// ── additions ──
|
|
51
|
+
if (t.isArrayExpression(expr)) {
|
|
52
|
+
return expr.elements.some(
|
|
53
|
+
(e) => e !== null && containsSignal(e as t.Expression, signals),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (t.isObjectExpression(expr)) {
|
|
57
|
+
return expr.properties.some((p) => {
|
|
58
|
+
if (t.isObjectProperty(p)) return containsSignal(p.value as t.Expression, signals);
|
|
59
|
+
if (t.isSpreadElement(p)) return containsSignal(p.argument, signals);
|
|
60
|
+
return false;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (t.isSequenceExpression(expr)) {
|
|
64
|
+
return expr.expressions.some((e) => containsSignal(e, signals));
|
|
65
|
+
}
|
|
66
|
+
if (t.isAssignmentExpression(expr)) {
|
|
67
|
+
return containsSignal(expr.right, signals);
|
|
68
|
+
}
|
|
69
|
+
if (t.isSpreadElement(expr)) {
|
|
70
|
+
return containsSignal(expr.argument, signals);
|
|
71
|
+
}
|
|
72
|
+
// ArrowFunction/Function — don't traverse, signals inside are captured
|
|
73
|
+
// not reactive at the call site (onclick={() => count()} is fine as-is)
|
|
74
|
+
if (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
// Babel represents ?. as distinct node types not in t.is* guards
|
|
78
|
+
if ((expr as any).type === "OptionalMemberExpression") {
|
|
79
|
+
return containsSignal((expr as any).object, signals);
|
|
80
|
+
}
|
|
81
|
+
if ((expr as any).type === "OptionalCallExpression") {
|
|
82
|
+
const n = expr as any;
|
|
83
|
+
return (
|
|
84
|
+
containsSignal(n.callee, signals) ||
|
|
85
|
+
n.arguments.some((a: t.Node) => containsSignal(a, signals))
|
|
86
|
+
);
|
|
43
87
|
}
|
|
44
88
|
return false;
|
|
45
89
|
}
|
package/src/babel/util/bind.ts
CHANGED
|
@@ -1,126 +1,139 @@
|
|
|
1
|
-
import { types as t } from "@babel/core";
|
|
2
|
-
|
|
3
|
-
const BIND_EVENT: Record<string, string> = {
|
|
4
|
-
value: "input",
|
|
5
|
-
checked: "change",
|
|
6
|
-
innerHTML: "input",
|
|
7
|
-
textContent: "input",
|
|
8
|
-
group: "change",
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function buildBind(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
): void {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
1
|
+
import { types as t } from "@babel/core";
|
|
2
|
+
|
|
3
|
+
const BIND_EVENT: Record<string, string> = {
|
|
4
|
+
value: "input",
|
|
5
|
+
checked: "change",
|
|
6
|
+
innerHTML: "input",
|
|
7
|
+
textContent: "input",
|
|
8
|
+
group: "change",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function buildBind(
|
|
12
|
+
varName: string,
|
|
13
|
+
bindProp: string,
|
|
14
|
+
attr: t.JSXAttribute,
|
|
15
|
+
statements: t.Statement[],
|
|
16
|
+
): void {
|
|
17
|
+
const signal = (attr.value as t.JSXExpressionContainer)
|
|
18
|
+
.expression as t.CallExpression; // already text()
|
|
19
|
+
|
|
20
|
+
const signalId = signal.callee as t.Identifier; // text — for .set()
|
|
21
|
+
|
|
22
|
+
const event = BIND_EVENT[bindProp] ?? "input";
|
|
23
|
+
|
|
24
|
+
// createEffect(() => _el.prop = text())
|
|
25
|
+
statements.push(
|
|
26
|
+
t.expressionStatement(
|
|
27
|
+
t.callExpression(t.identifier("createEffect"), [
|
|
28
|
+
t.arrowFunctionExpression(
|
|
29
|
+
[],
|
|
30
|
+
t.blockStatement([
|
|
31
|
+
t.expressionStatement(
|
|
32
|
+
t.assignmentExpression(
|
|
33
|
+
"=",
|
|
34
|
+
t.memberExpression(
|
|
35
|
+
t.identifier(varName),
|
|
36
|
+
t.identifier(bindProp),
|
|
37
|
+
),
|
|
38
|
+
signal,
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
]),
|
|
42
|
+
),
|
|
43
|
+
]),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// addEventListener(event, e => text.set(e.target.prop))
|
|
48
|
+
statements.push(
|
|
49
|
+
t.expressionStatement(
|
|
50
|
+
t.callExpression(
|
|
51
|
+
t.memberExpression(
|
|
52
|
+
t.identifier(varName),
|
|
53
|
+
t.identifier("addEventListener"),
|
|
54
|
+
),
|
|
55
|
+
[
|
|
56
|
+
t.stringLiteral(event),
|
|
57
|
+
t.arrowFunctionExpression(
|
|
58
|
+
[t.identifier("e")],
|
|
59
|
+
t.callExpression(
|
|
60
|
+
t.memberExpression(signalId, t.identifier("set")),
|
|
61
|
+
[
|
|
62
|
+
t.memberExpression(
|
|
63
|
+
t.memberExpression(t.identifier("e"), t.identifier("target")),
|
|
64
|
+
t.identifier(bindProp),
|
|
65
|
+
),
|
|
66
|
+
],
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
],
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildProxyBind(
|
|
76
|
+
varName: string,
|
|
77
|
+
bindProp: string,
|
|
78
|
+
attr: t.JSXAttribute,
|
|
79
|
+
statements: t.Statement[],
|
|
80
|
+
signal: t.MemberExpression, // already form().name
|
|
81
|
+
): void {
|
|
82
|
+
const event = BIND_EVENT[bindProp] ?? "input";
|
|
83
|
+
|
|
84
|
+
// createEffect(() => el.prop = form().name)
|
|
85
|
+
statements.push(
|
|
86
|
+
t.expressionStatement(
|
|
87
|
+
t.callExpression(t.identifier("createEffect"), [
|
|
88
|
+
t.arrowFunctionExpression(
|
|
89
|
+
[],
|
|
90
|
+
t.blockStatement([
|
|
91
|
+
t.expressionStatement(
|
|
92
|
+
t.assignmentExpression(
|
|
93
|
+
"=",
|
|
94
|
+
t.memberExpression(
|
|
95
|
+
t.identifier(varName),
|
|
96
|
+
t.identifier(bindProp),
|
|
97
|
+
),
|
|
98
|
+
signal,
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
]),
|
|
102
|
+
),
|
|
103
|
+
]),
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// addEventListener(event, e => { form().name = e.target.prop })
|
|
108
|
+
statements.push(
|
|
109
|
+
t.expressionStatement(
|
|
110
|
+
t.callExpression(
|
|
111
|
+
t.memberExpression(
|
|
112
|
+
t.identifier(varName),
|
|
113
|
+
t.identifier("addEventListener"),
|
|
114
|
+
),
|
|
115
|
+
[
|
|
116
|
+
t.stringLiteral(event),
|
|
117
|
+
t.arrowFunctionExpression(
|
|
118
|
+
[t.identifier("e")],
|
|
119
|
+
t.blockStatement([
|
|
120
|
+
t.expressionStatement(
|
|
121
|
+
t.assignmentExpression(
|
|
122
|
+
"=",
|
|
123
|
+
signal as unknown as t.LVal, // form().name — valid LVal
|
|
124
|
+
t.memberExpression(
|
|
125
|
+
t.memberExpression(
|
|
126
|
+
t.identifier("e"),
|
|
127
|
+
t.identifier("target"),
|
|
128
|
+
),
|
|
129
|
+
t.identifier(bindProp),
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
]),
|
|
134
|
+
),
|
|
135
|
+
],
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
}
|
package/src/babel/util/css.ts
CHANGED
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
import { createHash } from "crypto";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Compute a deterministic hash from a file path.
|
|
5
|
-
* Same file always gets same hash. Different files always get different hashes.
|
|
6
|
-
* 's' prefix for Sigil.
|
|
7
|
-
*/
|
|
8
|
-
export function computeHash(filePath: string): string {
|
|
9
|
-
return "s" + createHash("md5").update(filePath).digest("hex").slice(0, 8);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Scope CSS by rewriting selectors with hash.
|
|
14
|
-
* :global(.selector) escapes scoping (passed through unchanged).
|
|
15
|
-
*/
|
|
16
|
-
export function scopeCSS(css: string, hash: string): string {
|
|
17
|
-
let result = "";
|
|
18
|
-
let i = 0;
|
|
19
|
-
|
|
20
|
-
while (i < css.length) {
|
|
21
|
-
// Skip comments
|
|
22
|
-
if (css[i] === "/" && css[i + 1] === "*") {
|
|
23
|
-
const end = css.indexOf("*/", i + 2);
|
|
24
|
-
if (end === -1) {
|
|
25
|
-
result += css.slice(i);
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
result += css.slice(i, end + 2);
|
|
29
|
-
i = end + 2;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Skip string literals
|
|
34
|
-
if (css[i] === '"' || css[i] === "'") {
|
|
35
|
-
const quote = css[i];
|
|
36
|
-
let j = i + 1;
|
|
37
|
-
while (j < css.length && css[j] !== quote) {
|
|
38
|
-
if (css[j] === "\\") j++;
|
|
39
|
-
j++;
|
|
40
|
-
}
|
|
41
|
-
result += css.slice(i, j + 1);
|
|
42
|
-
i = j + 1;
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Find next { or end of string
|
|
47
|
-
if (css[i] === "{") {
|
|
48
|
-
// Find the matching }
|
|
49
|
-
let depth = 1;
|
|
50
|
-
let j = i + 1;
|
|
51
|
-
while (j < css.length && depth > 0) {
|
|
52
|
-
if (css[j] === "{") depth++;
|
|
53
|
-
else if (css[j] === "}") depth--;
|
|
54
|
-
j++;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// The block content (inside { }) - recurse into it
|
|
58
|
-
const blockContent = css.slice(i + 1, j - 1);
|
|
59
|
-
const scopedBlock = scopeCSS(blockContent, hash);
|
|
60
|
-
|
|
61
|
-
// Find where the selector starts (look back from {)
|
|
62
|
-
let selectorEnd = i;
|
|
63
|
-
let k = i - 1;
|
|
64
|
-
// Skip whitespace before {
|
|
65
|
-
while (k >= 0 && css[k] === " ") k--;
|
|
66
|
-
selectorEnd = k + 1;
|
|
67
|
-
|
|
68
|
-
// Find start of selector (look back for } or ; or start of string)
|
|
69
|
-
let selectorStart = selectorEnd;
|
|
70
|
-
k = selectorEnd - 1;
|
|
71
|
-
while (k >= 0) {
|
|
72
|
-
if (css[k] === "}" || css[k] === ";") {
|
|
73
|
-
selectorStart = k + 1;
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
if (k === 0) {
|
|
77
|
-
selectorStart = 0;
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
k--;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const selector = css.slice(selectorStart, selectorEnd).trim();
|
|
84
|
-
|
|
85
|
-
if (selector) {
|
|
86
|
-
// Rewrite selector with hash
|
|
87
|
-
result += css.slice(0, selectorStart);
|
|
88
|
-
result += rewriteSelector(selector, hash);
|
|
89
|
-
result += " {";
|
|
90
|
-
result += scopedBlock;
|
|
91
|
-
result += "}";
|
|
92
|
-
} else {
|
|
93
|
-
// No selector found, just output the block
|
|
94
|
-
result += css.slice(0, i);
|
|
95
|
-
result += " {";
|
|
96
|
-
result += scopedBlock;
|
|
97
|
-
result += "}";
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
i = j;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
result += css[i];
|
|
105
|
-
i++;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return result;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Rewrite a selector (or comma-separated list) by appending hash to each part.
|
|
113
|
-
* Handles :global() by passing through unchanged.
|
|
114
|
-
*/
|
|
115
|
-
function rewriteSelector(selector: string, hash: string): string {
|
|
116
|
-
// First, extract all :global() blocks and replace with placeholders
|
|
117
|
-
const globals: string[] = [];
|
|
118
|
-
let processed = selector;
|
|
119
|
-
|
|
120
|
-
// Find :global(...) with nested parens
|
|
121
|
-
while (processed.includes(":global(")) {
|
|
122
|
-
const start = processed.indexOf(":global(");
|
|
123
|
-
let depth = 1;
|
|
124
|
-
let j = start + 7; // Skip ':global('
|
|
125
|
-
while (j < processed.length && depth > 0) {
|
|
126
|
-
if (processed[j] === "(") depth++;
|
|
127
|
-
else if (processed[j] === ")") depth--;
|
|
128
|
-
j++;
|
|
129
|
-
}
|
|
130
|
-
const globalContent = processed.slice(start + 7, j - 1); // Content inside :global()
|
|
131
|
-
const placeholder = `__GLOBAL_${globals.length}__`;
|
|
132
|
-
globals.push(globalContent);
|
|
133
|
-
processed = processed.slice(0, start) + placeholder + processed.slice(j);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Now split by comma (safe - no commas inside :global())
|
|
137
|
-
const parts = processed.split(",").map((part) => {
|
|
138
|
-
const trimmed = part.trim();
|
|
139
|
-
if (!trimmed) return trimmed;
|
|
140
|
-
|
|
141
|
-
// Check for placeholder
|
|
142
|
-
const globalMatch = trimmed.match(/^__GLOBAL_(\d+)__$/);
|
|
143
|
-
if (globalMatch) {
|
|
144
|
-
return globals[parseInt(globalMatch[1]!)];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return trimmed + "." + hash;
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
return parts.join(", ");
|
|
151
|
-
}
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compute a deterministic hash from a file path.
|
|
5
|
+
* Same file always gets same hash. Different files always get different hashes.
|
|
6
|
+
* 's' prefix for Sigil.
|
|
7
|
+
*/
|
|
8
|
+
export function computeHash(filePath: string): string {
|
|
9
|
+
return "s" + createHash("md5").update(filePath).digest("hex").slice(0, 8);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Scope CSS by rewriting selectors with hash.
|
|
14
|
+
* :global(.selector) escapes scoping (passed through unchanged).
|
|
15
|
+
*/
|
|
16
|
+
export function scopeCSS(css: string, hash: string): string {
|
|
17
|
+
let result = "";
|
|
18
|
+
let i = 0;
|
|
19
|
+
|
|
20
|
+
while (i < css.length) {
|
|
21
|
+
// Skip comments
|
|
22
|
+
if (css[i] === "/" && css[i + 1] === "*") {
|
|
23
|
+
const end = css.indexOf("*/", i + 2);
|
|
24
|
+
if (end === -1) {
|
|
25
|
+
result += css.slice(i);
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
result += css.slice(i, end + 2);
|
|
29
|
+
i = end + 2;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Skip string literals
|
|
34
|
+
if (css[i] === '"' || css[i] === "'") {
|
|
35
|
+
const quote = css[i];
|
|
36
|
+
let j = i + 1;
|
|
37
|
+
while (j < css.length && css[j] !== quote) {
|
|
38
|
+
if (css[j] === "\\") j++;
|
|
39
|
+
j++;
|
|
40
|
+
}
|
|
41
|
+
result += css.slice(i, j + 1);
|
|
42
|
+
i = j + 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Find next { or end of string
|
|
47
|
+
if (css[i] === "{") {
|
|
48
|
+
// Find the matching }
|
|
49
|
+
let depth = 1;
|
|
50
|
+
let j = i + 1;
|
|
51
|
+
while (j < css.length && depth > 0) {
|
|
52
|
+
if (css[j] === "{") depth++;
|
|
53
|
+
else if (css[j] === "}") depth--;
|
|
54
|
+
j++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The block content (inside { }) - recurse into it
|
|
58
|
+
const blockContent = css.slice(i + 1, j - 1);
|
|
59
|
+
const scopedBlock = scopeCSS(blockContent, hash);
|
|
60
|
+
|
|
61
|
+
// Find where the selector starts (look back from {)
|
|
62
|
+
let selectorEnd = i;
|
|
63
|
+
let k = i - 1;
|
|
64
|
+
// Skip whitespace before {
|
|
65
|
+
while (k >= 0 && css[k] === " ") k--;
|
|
66
|
+
selectorEnd = k + 1;
|
|
67
|
+
|
|
68
|
+
// Find start of selector (look back for } or ; or start of string)
|
|
69
|
+
let selectorStart = selectorEnd;
|
|
70
|
+
k = selectorEnd - 1;
|
|
71
|
+
while (k >= 0) {
|
|
72
|
+
if (css[k] === "}" || css[k] === ";") {
|
|
73
|
+
selectorStart = k + 1;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
if (k === 0) {
|
|
77
|
+
selectorStart = 0;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
k--;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const selector = css.slice(selectorStart, selectorEnd).trim();
|
|
84
|
+
|
|
85
|
+
if (selector) {
|
|
86
|
+
// Rewrite selector with hash
|
|
87
|
+
result += css.slice(0, selectorStart);
|
|
88
|
+
result += rewriteSelector(selector, hash);
|
|
89
|
+
result += " {";
|
|
90
|
+
result += scopedBlock;
|
|
91
|
+
result += "}";
|
|
92
|
+
} else {
|
|
93
|
+
// No selector found, just output the block
|
|
94
|
+
result += css.slice(0, i);
|
|
95
|
+
result += " {";
|
|
96
|
+
result += scopedBlock;
|
|
97
|
+
result += "}";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
i = j;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
result += css[i];
|
|
105
|
+
i++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Rewrite a selector (or comma-separated list) by appending hash to each part.
|
|
113
|
+
* Handles :global() by passing through unchanged.
|
|
114
|
+
*/
|
|
115
|
+
function rewriteSelector(selector: string, hash: string): string {
|
|
116
|
+
// First, extract all :global() blocks and replace with placeholders
|
|
117
|
+
const globals: string[] = [];
|
|
118
|
+
let processed = selector;
|
|
119
|
+
|
|
120
|
+
// Find :global(...) with nested parens
|
|
121
|
+
while (processed.includes(":global(")) {
|
|
122
|
+
const start = processed.indexOf(":global(");
|
|
123
|
+
let depth = 1;
|
|
124
|
+
let j = start + 7; // Skip ':global('
|
|
125
|
+
while (j < processed.length && depth > 0) {
|
|
126
|
+
if (processed[j] === "(") depth++;
|
|
127
|
+
else if (processed[j] === ")") depth--;
|
|
128
|
+
j++;
|
|
129
|
+
}
|
|
130
|
+
const globalContent = processed.slice(start + 7, j - 1); // Content inside :global()
|
|
131
|
+
const placeholder = `__GLOBAL_${globals.length}__`;
|
|
132
|
+
globals.push(globalContent);
|
|
133
|
+
processed = processed.slice(0, start) + placeholder + processed.slice(j);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Now split by comma (safe - no commas inside :global())
|
|
137
|
+
const parts = processed.split(",").map((part) => {
|
|
138
|
+
const trimmed = part.trim();
|
|
139
|
+
if (!trimmed) return trimmed;
|
|
140
|
+
|
|
141
|
+
// Check for placeholder
|
|
142
|
+
const globalMatch = trimmed.match(/^__GLOBAL_(\d+)__$/);
|
|
143
|
+
if (globalMatch) {
|
|
144
|
+
return globals[parseInt(globalMatch[1]!)];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return trimmed + "." + hash;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return parts.join(", ");
|
|
151
|
+
}
|