@sprlab/wccompiler 0.12.1 → 0.14.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/README.md +998 -998
- package/adapters/angular-compiled/angular.d.ts +197 -197
- package/adapters/angular-compiled/angular.mjs +488 -488
- package/adapters/angular.js +54 -54
- package/adapters/angular.ts +630 -630
- package/adapters/react.js +114 -114
- package/adapters/vue.js +103 -103
- package/bin/wcc.js +412 -412
- package/bin/wcc.test.js +126 -126
- package/integrations/angular.js +73 -73
- package/integrations/react.js +859 -859
- package/integrations/vue.js +253 -253
- package/lib/codegen.js +2074 -2029
- package/lib/compiler-browser.js +545 -545
- package/lib/compiler.js +483 -473
- package/lib/config.js +71 -71
- package/lib/css-scoper.js +180 -180
- package/lib/dev-server.js +193 -193
- package/lib/import-resolver.js +160 -160
- package/lib/parser-extractors.js +1240 -1169
- package/lib/parser.js +273 -269
- package/lib/reactive-runtime.js +143 -143
- package/lib/sfc-parser.js +333 -333
- package/lib/template-normalizer.js +114 -109
- package/lib/tree-walker.js +1013 -923
- package/lib/types.js +262 -240
- package/lib/wcc-runtime.js +68 -68
- package/package.json +85 -85
- package/types/wcc.d.ts +28 -28
- package/types/wcc.test.js +46 -46
package/lib/config.js
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import { pathToFileURL } from 'node:url';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @typedef {Object} WccConfig
|
|
7
|
-
* @property {number} port — Dev server port (default: 4100)
|
|
8
|
-
* @property {string} input — Source directory (default: 'src')
|
|
9
|
-
* @property {string} output — Output directory (default: 'dist')
|
|
10
|
-
* @property {boolean} standalone — Inline runtime in each component (default: false)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Load wcc.config.js from the project root.
|
|
15
|
-
* Returns defaults if the file doesn't exist.
|
|
16
|
-
* Validates port (finite number), input (non-empty string), output (non-empty string).
|
|
17
|
-
*
|
|
18
|
-
* @param {string} projectRoot
|
|
19
|
-
* @returns {Promise<WccConfig>}
|
|
20
|
-
*/
|
|
21
|
-
export async function loadConfig(projectRoot) {
|
|
22
|
-
const defaults = { port: 4100, input: 'src', output: 'dist', standalone: false, minify: false, comments: false };
|
|
23
|
-
const configPath = resolve(projectRoot, 'wcc.config.js');
|
|
24
|
-
|
|
25
|
-
if (!existsSync(configPath)) return defaults;
|
|
26
|
-
|
|
27
|
-
const configUrl = pathToFileURL(configPath).href;
|
|
28
|
-
// Add cache-busting query to avoid ESM module cache issues
|
|
29
|
-
const mod = await import(`${configUrl}?t=${Date.now()}`);
|
|
30
|
-
// Unwrap ESM module namespace: handle double-nesting from dynamic import
|
|
31
|
-
let userConfig = mod.default || mod;
|
|
32
|
-
if (userConfig.__esModule && userConfig.default) {
|
|
33
|
-
userConfig = userConfig.default;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const config = { ...defaults, ...userConfig };
|
|
37
|
-
|
|
38
|
-
// Validate
|
|
39
|
-
if (typeof config.port !== 'number' || !isFinite(config.port)) {
|
|
40
|
-
const error = new Error(`Error en wcc.config.js: port debe ser un número finito`);
|
|
41
|
-
error.code = 'INVALID_CONFIG';
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
if (typeof config.input !== 'string' || !config.input.trim()) {
|
|
45
|
-
const error = new Error(`Error en wcc.config.js: input debe ser un string no vacío`);
|
|
46
|
-
error.code = 'INVALID_CONFIG';
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
if (typeof config.output !== 'string' || !config.output.trim()) {
|
|
50
|
-
const error = new Error(`Error en wcc.config.js: output debe ser un string no vacío`);
|
|
51
|
-
error.code = 'INVALID_CONFIG';
|
|
52
|
-
throw error;
|
|
53
|
-
}
|
|
54
|
-
if (typeof config.standalone !== 'boolean') {
|
|
55
|
-
const error = new Error(`Error en wcc.config.js: standalone debe ser un booleano`);
|
|
56
|
-
error.code = 'INVALID_CONFIG';
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
if (typeof config.minify !== 'boolean') {
|
|
60
|
-
const error = new Error(`Error en wcc.config.js: minify debe ser un booleano`);
|
|
61
|
-
error.code = 'INVALID_CONFIG';
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
if (typeof config.comments !== 'boolean') {
|
|
65
|
-
const error = new Error(`Error en wcc.config.js: comments debe ser un booleano`);
|
|
66
|
-
error.code = 'INVALID_CONFIG';
|
|
67
|
-
throw error;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return config;
|
|
71
|
-
}
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} WccConfig
|
|
7
|
+
* @property {number} port — Dev server port (default: 4100)
|
|
8
|
+
* @property {string} input — Source directory (default: 'src')
|
|
9
|
+
* @property {string} output — Output directory (default: 'dist')
|
|
10
|
+
* @property {boolean} standalone — Inline runtime in each component (default: false)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Load wcc.config.js from the project root.
|
|
15
|
+
* Returns defaults if the file doesn't exist.
|
|
16
|
+
* Validates port (finite number), input (non-empty string), output (non-empty string).
|
|
17
|
+
*
|
|
18
|
+
* @param {string} projectRoot
|
|
19
|
+
* @returns {Promise<WccConfig>}
|
|
20
|
+
*/
|
|
21
|
+
export async function loadConfig(projectRoot) {
|
|
22
|
+
const defaults = { port: 4100, input: 'src', output: 'dist', standalone: false, minify: false, comments: false };
|
|
23
|
+
const configPath = resolve(projectRoot, 'wcc.config.js');
|
|
24
|
+
|
|
25
|
+
if (!existsSync(configPath)) return defaults;
|
|
26
|
+
|
|
27
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
28
|
+
// Add cache-busting query to avoid ESM module cache issues
|
|
29
|
+
const mod = await import(`${configUrl}?t=${Date.now()}`);
|
|
30
|
+
// Unwrap ESM module namespace: handle double-nesting from dynamic import
|
|
31
|
+
let userConfig = mod.default || mod;
|
|
32
|
+
if (userConfig.__esModule && userConfig.default) {
|
|
33
|
+
userConfig = userConfig.default;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const config = { ...defaults, ...userConfig };
|
|
37
|
+
|
|
38
|
+
// Validate
|
|
39
|
+
if (typeof config.port !== 'number' || !isFinite(config.port)) {
|
|
40
|
+
const error = new Error(`Error en wcc.config.js: port debe ser un número finito`);
|
|
41
|
+
error.code = 'INVALID_CONFIG';
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
if (typeof config.input !== 'string' || !config.input.trim()) {
|
|
45
|
+
const error = new Error(`Error en wcc.config.js: input debe ser un string no vacío`);
|
|
46
|
+
error.code = 'INVALID_CONFIG';
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
if (typeof config.output !== 'string' || !config.output.trim()) {
|
|
50
|
+
const error = new Error(`Error en wcc.config.js: output debe ser un string no vacío`);
|
|
51
|
+
error.code = 'INVALID_CONFIG';
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
if (typeof config.standalone !== 'boolean') {
|
|
55
|
+
const error = new Error(`Error en wcc.config.js: standalone debe ser un booleano`);
|
|
56
|
+
error.code = 'INVALID_CONFIG';
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
if (typeof config.minify !== 'boolean') {
|
|
60
|
+
const error = new Error(`Error en wcc.config.js: minify debe ser un booleano`);
|
|
61
|
+
error.code = 'INVALID_CONFIG';
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
if (typeof config.comments !== 'boolean') {
|
|
65
|
+
const error = new Error(`Error en wcc.config.js: comments debe ser un booleano`);
|
|
66
|
+
error.code = 'INVALID_CONFIG';
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return config;
|
|
71
|
+
}
|
package/lib/css-scoper.js
CHANGED
|
@@ -1,180 +1,180 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CSS Scoper — prefixes CSS selectors with the component tag name.
|
|
3
|
-
*
|
|
4
|
-
* Handles:
|
|
5
|
-
* - Simple selectors (.class, #id, element)
|
|
6
|
-
* - Comma-separated selectors
|
|
7
|
-
* - At-rules (@media, @keyframes) — preserved without prefixing
|
|
8
|
-
* - Nested selectors inside at-rules — still prefixed
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Scope CSS by prefixing each selector with the component tag name.
|
|
13
|
-
*
|
|
14
|
-
* @param {string} css - Raw CSS string
|
|
15
|
-
* @param {string} tagName - Component tag name (e.g. "wcc-hi")
|
|
16
|
-
* @returns {string} Scoped CSS string
|
|
17
|
-
*/
|
|
18
|
-
export function scopeCSS(css, tagName) {
|
|
19
|
-
if (!css || !css.trim()) return '';
|
|
20
|
-
|
|
21
|
-
const result = [];
|
|
22
|
-
let i = 0;
|
|
23
|
-
|
|
24
|
-
while (i < css.length) {
|
|
25
|
-
// Skip whitespace
|
|
26
|
-
if (/\s/.test(css[i])) {
|
|
27
|
-
result.push(css[i]);
|
|
28
|
-
i++;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Detect at-rules
|
|
33
|
-
if (css[i] === '@') {
|
|
34
|
-
const atResult = consumeAtRule(css, i, tagName);
|
|
35
|
-
result.push(atResult.text);
|
|
36
|
-
i = atResult.end;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Detect closing brace (end of a block)
|
|
41
|
-
if (css[i] === '}') {
|
|
42
|
-
result.push('}');
|
|
43
|
-
i++;
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Otherwise, it's a selector — read until '{'
|
|
48
|
-
const selectorEnd = css.indexOf('{', i);
|
|
49
|
-
if (selectorEnd === -1) {
|
|
50
|
-
// No more blocks, append remaining text as-is
|
|
51
|
-
result.push(css.slice(i));
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const rawSelector = css.slice(i, selectorEnd);
|
|
56
|
-
const scopedSelector = prefixSelectors(rawSelector, tagName);
|
|
57
|
-
result.push(scopedSelector);
|
|
58
|
-
|
|
59
|
-
// Now consume the declaration block (until matching '}')
|
|
60
|
-
const blockResult = consumeBlock(css, selectorEnd);
|
|
61
|
-
result.push(blockResult.text);
|
|
62
|
-
i = blockResult.end;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return result.join('');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Prefix comma-separated selectors with the tag name.
|
|
71
|
-
* e.g. ".foo, .bar" → "tag .foo, tag .bar"
|
|
72
|
-
*
|
|
73
|
-
* @param {string} raw - Raw selector string (may be comma-separated)
|
|
74
|
-
* @param {string} tagName - Component tag name to prefix
|
|
75
|
-
* @returns {string} Prefixed selector string
|
|
76
|
-
*/
|
|
77
|
-
function prefixSelectors(raw, tagName) {
|
|
78
|
-
return raw
|
|
79
|
-
.split(',')
|
|
80
|
-
.map(s => {
|
|
81
|
-
const trimmed = s.trim();
|
|
82
|
-
if (!trimmed) return s;
|
|
83
|
-
// Preserve the leading whitespace from the original
|
|
84
|
-
const leadingWs = s.match(/^(\s*)/)[1];
|
|
85
|
-
return `${leadingWs}${tagName} ${trimmed}`;
|
|
86
|
-
})
|
|
87
|
-
.join(',');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Consume a { ... } block starting at the opening brace.
|
|
92
|
-
* Returns the text (including braces) and the position after the closing brace.
|
|
93
|
-
*
|
|
94
|
-
* @param {string} css - Full CSS string
|
|
95
|
-
* @param {number} start - Index of the opening brace
|
|
96
|
-
* @returns {{text: string, end: number}} Consumed block text and position after closing brace
|
|
97
|
-
*/
|
|
98
|
-
function consumeBlock(css, start) {
|
|
99
|
-
let depth = 0;
|
|
100
|
-
let i = start;
|
|
101
|
-
const chars = [];
|
|
102
|
-
|
|
103
|
-
while (i < css.length) {
|
|
104
|
-
chars.push(css[i]);
|
|
105
|
-
if (css[i] === '{') depth++;
|
|
106
|
-
if (css[i] === '}') {
|
|
107
|
-
depth--;
|
|
108
|
-
if (depth === 0) {
|
|
109
|
-
return { text: chars.join(''), end: i + 1 };
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
i++;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Unclosed block — return what we have
|
|
116
|
-
return { text: chars.join(''), end: i };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Consume an at-rule starting at '@'.
|
|
121
|
-
* Handles both block at-rules (@media { ... }) and statement at-rules (@import ...).
|
|
122
|
-
* For block at-rules, recursively scopes selectors inside.
|
|
123
|
-
*
|
|
124
|
-
* @param {string} css - Full CSS string
|
|
125
|
-
* @param {number} start - Index of the '@' character
|
|
126
|
-
* @param {string} tagName - Component tag name for scoping nested selectors
|
|
127
|
-
* @returns {{text: string, end: number}} Consumed at-rule text and position after it
|
|
128
|
-
*/
|
|
129
|
-
function consumeAtRule(css, start, tagName) {
|
|
130
|
-
// Read the at-rule prelude (everything up to '{' or ';')
|
|
131
|
-
let i = start;
|
|
132
|
-
const prelude = [];
|
|
133
|
-
|
|
134
|
-
while (i < css.length && css[i] !== '{' && css[i] !== ';') {
|
|
135
|
-
prelude.push(css[i]);
|
|
136
|
-
i++;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (i >= css.length) {
|
|
140
|
-
// Unterminated at-rule
|
|
141
|
-
return { text: prelude.join(''), end: i };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (css[i] === ';') {
|
|
145
|
-
// Statement at-rule (e.g. @import, @charset)
|
|
146
|
-
prelude.push(';');
|
|
147
|
-
return { text: prelude.join(''), end: i + 1 };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Block at-rule — css[i] === '{'
|
|
151
|
-
const preludeStr = prelude.join('');
|
|
152
|
-
const atName = preludeStr.trim().split(/\s/)[0]; // e.g. "@media", "@keyframes"
|
|
153
|
-
|
|
154
|
-
// @keyframes: don't scope inner selectors (they're keyframe stops, not CSS selectors)
|
|
155
|
-
if (atName === '@keyframes' || atName === '@-webkit-keyframes') {
|
|
156
|
-
const block = consumeBlock(css, i);
|
|
157
|
-
return { text: preludeStr + block.text, end: block.end };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// For @media and other nesting at-rules: scope the inner selectors
|
|
161
|
-
// We need to parse the inner content recursively
|
|
162
|
-
const innerStart = i + 1; // after '{'
|
|
163
|
-
let depth = 1;
|
|
164
|
-
let j = innerStart;
|
|
165
|
-
|
|
166
|
-
// Find the matching closing brace
|
|
167
|
-
while (j < css.length && depth > 0) {
|
|
168
|
-
if (css[j] === '{') depth++;
|
|
169
|
-
if (css[j] === '}') depth--;
|
|
170
|
-
if (depth > 0) j++;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const innerCSS = css.slice(innerStart, j);
|
|
174
|
-
const scopedInner = scopeCSS(innerCSS, tagName);
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
text: `${preludeStr}{${scopedInner}}`,
|
|
178
|
-
end: j + 1,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* CSS Scoper — prefixes CSS selectors with the component tag name.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Simple selectors (.class, #id, element)
|
|
6
|
+
* - Comma-separated selectors
|
|
7
|
+
* - At-rules (@media, @keyframes) — preserved without prefixing
|
|
8
|
+
* - Nested selectors inside at-rules — still prefixed
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Scope CSS by prefixing each selector with the component tag name.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} css - Raw CSS string
|
|
15
|
+
* @param {string} tagName - Component tag name (e.g. "wcc-hi")
|
|
16
|
+
* @returns {string} Scoped CSS string
|
|
17
|
+
*/
|
|
18
|
+
export function scopeCSS(css, tagName) {
|
|
19
|
+
if (!css || !css.trim()) return '';
|
|
20
|
+
|
|
21
|
+
const result = [];
|
|
22
|
+
let i = 0;
|
|
23
|
+
|
|
24
|
+
while (i < css.length) {
|
|
25
|
+
// Skip whitespace
|
|
26
|
+
if (/\s/.test(css[i])) {
|
|
27
|
+
result.push(css[i]);
|
|
28
|
+
i++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Detect at-rules
|
|
33
|
+
if (css[i] === '@') {
|
|
34
|
+
const atResult = consumeAtRule(css, i, tagName);
|
|
35
|
+
result.push(atResult.text);
|
|
36
|
+
i = atResult.end;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Detect closing brace (end of a block)
|
|
41
|
+
if (css[i] === '}') {
|
|
42
|
+
result.push('}');
|
|
43
|
+
i++;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Otherwise, it's a selector — read until '{'
|
|
48
|
+
const selectorEnd = css.indexOf('{', i);
|
|
49
|
+
if (selectorEnd === -1) {
|
|
50
|
+
// No more blocks, append remaining text as-is
|
|
51
|
+
result.push(css.slice(i));
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const rawSelector = css.slice(i, selectorEnd);
|
|
56
|
+
const scopedSelector = prefixSelectors(rawSelector, tagName);
|
|
57
|
+
result.push(scopedSelector);
|
|
58
|
+
|
|
59
|
+
// Now consume the declaration block (until matching '}')
|
|
60
|
+
const blockResult = consumeBlock(css, selectorEnd);
|
|
61
|
+
result.push(blockResult.text);
|
|
62
|
+
i = blockResult.end;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result.join('');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Prefix comma-separated selectors with the tag name.
|
|
71
|
+
* e.g. ".foo, .bar" → "tag .foo, tag .bar"
|
|
72
|
+
*
|
|
73
|
+
* @param {string} raw - Raw selector string (may be comma-separated)
|
|
74
|
+
* @param {string} tagName - Component tag name to prefix
|
|
75
|
+
* @returns {string} Prefixed selector string
|
|
76
|
+
*/
|
|
77
|
+
function prefixSelectors(raw, tagName) {
|
|
78
|
+
return raw
|
|
79
|
+
.split(',')
|
|
80
|
+
.map(s => {
|
|
81
|
+
const trimmed = s.trim();
|
|
82
|
+
if (!trimmed) return s;
|
|
83
|
+
// Preserve the leading whitespace from the original
|
|
84
|
+
const leadingWs = s.match(/^(\s*)/)[1];
|
|
85
|
+
return `${leadingWs}${tagName} ${trimmed}`;
|
|
86
|
+
})
|
|
87
|
+
.join(',');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Consume a { ... } block starting at the opening brace.
|
|
92
|
+
* Returns the text (including braces) and the position after the closing brace.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} css - Full CSS string
|
|
95
|
+
* @param {number} start - Index of the opening brace
|
|
96
|
+
* @returns {{text: string, end: number}} Consumed block text and position after closing brace
|
|
97
|
+
*/
|
|
98
|
+
function consumeBlock(css, start) {
|
|
99
|
+
let depth = 0;
|
|
100
|
+
let i = start;
|
|
101
|
+
const chars = [];
|
|
102
|
+
|
|
103
|
+
while (i < css.length) {
|
|
104
|
+
chars.push(css[i]);
|
|
105
|
+
if (css[i] === '{') depth++;
|
|
106
|
+
if (css[i] === '}') {
|
|
107
|
+
depth--;
|
|
108
|
+
if (depth === 0) {
|
|
109
|
+
return { text: chars.join(''), end: i + 1 };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Unclosed block — return what we have
|
|
116
|
+
return { text: chars.join(''), end: i };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Consume an at-rule starting at '@'.
|
|
121
|
+
* Handles both block at-rules (@media { ... }) and statement at-rules (@import ...).
|
|
122
|
+
* For block at-rules, recursively scopes selectors inside.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} css - Full CSS string
|
|
125
|
+
* @param {number} start - Index of the '@' character
|
|
126
|
+
* @param {string} tagName - Component tag name for scoping nested selectors
|
|
127
|
+
* @returns {{text: string, end: number}} Consumed at-rule text and position after it
|
|
128
|
+
*/
|
|
129
|
+
function consumeAtRule(css, start, tagName) {
|
|
130
|
+
// Read the at-rule prelude (everything up to '{' or ';')
|
|
131
|
+
let i = start;
|
|
132
|
+
const prelude = [];
|
|
133
|
+
|
|
134
|
+
while (i < css.length && css[i] !== '{' && css[i] !== ';') {
|
|
135
|
+
prelude.push(css[i]);
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (i >= css.length) {
|
|
140
|
+
// Unterminated at-rule
|
|
141
|
+
return { text: prelude.join(''), end: i };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (css[i] === ';') {
|
|
145
|
+
// Statement at-rule (e.g. @import, @charset)
|
|
146
|
+
prelude.push(';');
|
|
147
|
+
return { text: prelude.join(''), end: i + 1 };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Block at-rule — css[i] === '{'
|
|
151
|
+
const preludeStr = prelude.join('');
|
|
152
|
+
const atName = preludeStr.trim().split(/\s/)[0]; // e.g. "@media", "@keyframes"
|
|
153
|
+
|
|
154
|
+
// @keyframes: don't scope inner selectors (they're keyframe stops, not CSS selectors)
|
|
155
|
+
if (atName === '@keyframes' || atName === '@-webkit-keyframes') {
|
|
156
|
+
const block = consumeBlock(css, i);
|
|
157
|
+
return { text: preludeStr + block.text, end: block.end };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// For @media and other nesting at-rules: scope the inner selectors
|
|
161
|
+
// We need to parse the inner content recursively
|
|
162
|
+
const innerStart = i + 1; // after '{'
|
|
163
|
+
let depth = 1;
|
|
164
|
+
let j = innerStart;
|
|
165
|
+
|
|
166
|
+
// Find the matching closing brace
|
|
167
|
+
while (j < css.length && depth > 0) {
|
|
168
|
+
if (css[j] === '{') depth++;
|
|
169
|
+
if (css[j] === '}') depth--;
|
|
170
|
+
if (depth > 0) j++;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const innerCSS = css.slice(innerStart, j);
|
|
174
|
+
const scopedInner = scopeCSS(innerCSS, tagName);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
text: `${preludeStr}{${scopedInner}}`,
|
|
178
|
+
end: j + 1,
|
|
179
|
+
};
|
|
180
|
+
}
|