@symbiote-native/css-parser 0.3.0 → 0.5.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 +33 -31
- package/build/generate-dts/index.js +3 -8
- package/build/generate-dts-cli.js +5 -1
- package/build/golden-corpus/fixtures/ScopedGlobalDemo.svelte +25 -0
- package/build/golden-corpus/fixtures/ScopedGlobalDemo.svelte.d.ts +3 -0
- package/build/index.d.ts +8 -7
- package/build/index.js +4 -4
- package/build/lightning/declarations.d.ts +46 -0
- package/build/lightning/declarations.js +885 -0
- package/build/lightning/rules.d.ts +52 -0
- package/build/lightning/rules.js +157 -0
- package/build/lightning/selectors.d.ts +23 -0
- package/build/lightning/selectors.js +468 -0
- package/build/metro-css-module/index.d.ts +30 -2
- package/build/metro-css-module/index.js +142 -36
- package/build/preprocessors/index.js +4 -3
- package/build/properties.d.ts +5 -6
- package/build/properties.js +26 -14
- package/build/scoped-classes.d.ts +23 -0
- package/build/scoped-classes.js +35 -0
- package/build/values.d.ts +3 -7
- package/build/values.js +3 -60
- package/package.json +13 -3
- package/typescript-plugin.cjs +21 -14
- package/build/global-selectors.d.ts +0 -25
- package/build/global-selectors.js +0 -98
- package/build/parser/index.d.ts +0 -59
- package/build/parser/index.js +0 -338
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
// Compiles a standalone .css/.scss/.sass/.less/.styl (and their .module.* twins) file into a
|
|
2
2
|
// plain JS module — the framework-agnostic twin of a Vue SFC's inline <style>/<style module>
|
|
3
|
-
// block (
|
|
3
|
+
// block (adapters/vue/metro-vue-transformer.cjs), usable from ANY adapter's own source file:
|
|
4
4
|
// a default import of `Card.module.scss` works the same from a React .tsx, a Vue <script>, or an
|
|
5
5
|
// Angular .ts.
|
|
6
6
|
//
|
|
7
7
|
// A plain style file registers its classes globally, exactly like an unscoped Vue <style>
|
|
8
8
|
// block — side-effect import only (`import './theme.scss'`), no default export. A `.module.*`
|
|
9
|
-
// file is ALWAYS scoped (that's the entire point of the extension):
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
9
|
+
// file is ALWAYS scoped (that's the entire point of the extension): **lightningcss** owns the
|
|
10
|
+
// renaming, `composes`, and `:global()` handling, and hands back both the renamed CSS and the
|
|
11
|
+
// authored-name -> scoped-name `exports` map. We keep only what is genuinely ours — turning CSS
|
|
12
|
+
// declarations into RN style objects (`compileCssToRules`).
|
|
13
|
+
//
|
|
14
|
+
// lightningcss is MPL-2.0 and this package is MIT. Depending on it is fine (file-level copyleft
|
|
15
|
+
// does not reach our files); NEVER vendor, copy, or patch its sources — a copied file would
|
|
16
|
+
// carry MPL.
|
|
17
17
|
//
|
|
18
18
|
// A preprocessor source (SCSS/Sass/Less/Stylus) is reduced to plain CSS text via
|
|
19
|
-
// preprocessors.ts's `compile()` BEFORE
|
|
20
|
-
// logic is entirely language-agnostic, it only ever sees `parseCSS`'s plain-CSS output, same as
|
|
21
|
-
// it always did for a `.css` file.
|
|
19
|
+
// preprocessors.ts's `compile()` BEFORE lightningcss runs — it reads plain CSS only.
|
|
22
20
|
import * as path from 'node:path';
|
|
23
|
-
import {
|
|
24
|
-
import { globalClassNamesIn } from "../global-selectors.js";
|
|
21
|
+
import { compileCssToRules } from "../lightning/rules.js";
|
|
25
22
|
import { hashFilePath } from "../file-scope-id.js";
|
|
26
23
|
import { compile, detectLanguage } from "../preprocessors/index.js";
|
|
27
24
|
export function isCssModuleFile(filename) {
|
|
@@ -30,41 +27,150 @@ export function isCssModuleFile(filename) {
|
|
|
30
27
|
return false;
|
|
31
28
|
return filename.slice(0, -ext.length).endsWith('.module');
|
|
32
29
|
}
|
|
30
|
+
// A `composes` entry names the SCOPED class, so walking a chain (`.a composes .b`, `.b composes
|
|
31
|
+
// .c`) needs the way back to the composed class's own export — lightningcss reports one hop, not
|
|
32
|
+
// the flattened set.
|
|
33
|
+
function localsByScopedName(exports) {
|
|
34
|
+
const locals = new Map();
|
|
35
|
+
for (const [local, entry] of Object.entries(exports)) {
|
|
36
|
+
locals.set(entry.name, local);
|
|
37
|
+
}
|
|
38
|
+
return locals;
|
|
39
|
+
}
|
|
40
|
+
function referenceTokens(reference, exports, locals, visited, filename) {
|
|
41
|
+
// Resolving one would mean compiling the other file here, which this per-file transform cannot
|
|
42
|
+
// do. Warn-and-drop is what the declaration mapper already does with a property it cannot map.
|
|
43
|
+
if (reference.type === 'dependency') {
|
|
44
|
+
console.warn(`[@symbiote-native/css-parser] ${filename}: \`composes: ${reference.name} from ` +
|
|
45
|
+
`"${reference.specifier}"\` ` +
|
|
46
|
+
'crosses files and is not supported — dropped.');
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
if (reference.type === 'global')
|
|
50
|
+
return [reference.name];
|
|
51
|
+
const local = locals.get(reference.name);
|
|
52
|
+
return local === undefined
|
|
53
|
+
? [reference.name]
|
|
54
|
+
: tokensFor(local, exports, locals, visited, filename);
|
|
55
|
+
}
|
|
56
|
+
// Composed names come FIRST, the class's own name LAST. The runtime registry merges a
|
|
57
|
+
// space-separated class string left to right (core/engine/src/style-registry), so this is what
|
|
58
|
+
// makes the composing class override what it composes — the cascade order `composes` means.
|
|
59
|
+
function tokensFor(local, exports, locals, visited, filename) {
|
|
60
|
+
if (visited.has(local))
|
|
61
|
+
return [];
|
|
62
|
+
visited.add(local);
|
|
63
|
+
const entry = exports[local];
|
|
64
|
+
if (entry === undefined)
|
|
65
|
+
return [];
|
|
66
|
+
const tokens = [];
|
|
67
|
+
for (const reference of entry.composes) {
|
|
68
|
+
tokens.push(...referenceTokens(reference, exports, locals, visited, filename));
|
|
69
|
+
}
|
|
70
|
+
tokens.push(entry.name);
|
|
71
|
+
return tokens;
|
|
72
|
+
}
|
|
73
|
+
// `globals` is deliberately a SUPERSET of what CSS Modules specifies. lightningcss (like
|
|
74
|
+
// css-loader and every other CSS Modules implementation) exports only the names it renamed, and
|
|
75
|
+
// a `:global(...)` name is
|
|
76
|
+
// by definition not renamed — so upstream has nothing to map and omits it. For us that omission
|
|
77
|
+
// would force the author back to a bare string literal (`class="legacy-reset"`), throwing away the
|
|
78
|
+
// typed, discoverable `styles.x` access that is the entire reason this map exists. So a global
|
|
79
|
+
// name is exported as ITSELF: same key, no scope suffix.
|
|
80
|
+
function classMapFrom(exports, globals, filename) {
|
|
81
|
+
const locals = localsByScopedName(exports);
|
|
82
|
+
const classMap = {};
|
|
83
|
+
// Sorted, because `exports` is a Rust HashMap whose iteration order is randomized PER PROCESS
|
|
84
|
+
// — emitting it verbatim makes the generated module bytes differ between builds and churns
|
|
85
|
+
// Metro's content cache. Measured, not assumed: three runs over one file gave three orders.
|
|
86
|
+
const names = [...new Set([...Object.keys(exports), ...globals])].sort();
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
// A name that is BOTH locally declared and `:global()`-wrapped elsewhere in the file resolves
|
|
89
|
+
// as local — the rename actually happened, so the scoped spelling is the one that matches.
|
|
90
|
+
const value = exports[name] === undefined
|
|
91
|
+
? name
|
|
92
|
+
: tokensFor(name, exports, locals, new Set(), filename).join(' ');
|
|
93
|
+
// Keyed AS AUTHORED — `.legacy-reset` keys as `legacy-reset`, read as
|
|
94
|
+
// `styles['legacy-reset']`. This reverses an earlier camelCase decision (css-loader's
|
|
95
|
+
// `exportLocalsConvention`), which existed only to keep dot access legal and paid for it by
|
|
96
|
+
// merging `.legacy-reset` and `.legacyReset` into one key — a collision that needed a warning
|
|
97
|
+
// to be survivable. Nothing downstream normalizes any more: the registry key is the authored
|
|
98
|
+
// name too, and `classNamesToDtsSource`'s `formatKey` already quotes a non-identifier key, so
|
|
99
|
+
// `styles['legacy-reset']` type-checks.
|
|
100
|
+
classMap[name] = value;
|
|
101
|
+
}
|
|
102
|
+
return classMap;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* One CSS source compiled as a CSS MODULE — a standalone `.module.*` file and a Vue
|
|
106
|
+
* `<style module>` block are the same thing and go through this, so neither can register a class
|
|
107
|
+
* under a name the other would not.
|
|
108
|
+
*
|
|
109
|
+
* The scope tail is our own `hashFilePath`, not lightningcss's `[hash]`: the runtime registry
|
|
110
|
+
* parses this tail to factor a scope back out (SCOPE_TAIL_PATTERN in
|
|
111
|
+
* core/engine/src/style-registry) and its alphabet is lowercase base36, which lightningcss's
|
|
112
|
+
* mixed-case hash does not fit — that would silently kill scoped-token base layering. Same hash
|
|
113
|
+
* the `<style scoped>` and Svelte scopers use, so all three scoping shapes stay one algorithm.
|
|
114
|
+
*/
|
|
115
|
+
export function compileCssModule(css, filename) {
|
|
116
|
+
const compiled = compileCssToRules(css, {
|
|
117
|
+
filename,
|
|
118
|
+
pattern: `[local]__module__${hashFilePath(filename)}`,
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
rules: compiled.rules,
|
|
122
|
+
classMap: classMapFrom(compiled.moduleExports, compiled.globals, filename),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async function toPlainCss(source, filename) {
|
|
126
|
+
const lang = detectLanguage(filename);
|
|
127
|
+
return lang === 'css' ? source : await compile(source, lang, filename);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The names a `.module.*` file's default export actually carries.
|
|
131
|
+
*
|
|
132
|
+
* The `.d.ts` generator MUST read them from here rather than re-deriving names off the raw
|
|
133
|
+
* source: what a rule MATCHES on and what the module EXPORTS are different sets. A compound
|
|
134
|
+
* rule `.card.big` matches on both its tokens, but only the names the author wrote as classes
|
|
135
|
+
* are exports. Typing off a re-derived set invents members that are `undefined` at runtime and
|
|
136
|
+
* hides valid ones behind a TS2339.
|
|
137
|
+
*/
|
|
138
|
+
export async function moduleClassNames(source, filename) {
|
|
139
|
+
const css = await toPlainCss(source, filename);
|
|
140
|
+
return Object.keys(compileCssModule(css, filename).classMap);
|
|
141
|
+
}
|
|
33
142
|
// Preprocessing (SCSS/Less/Stylus → plain CSS text) is inherently async in Node — Less has no
|
|
34
143
|
// sync render API at all, and Stylus's callback-based render must be Promise-wrapped — so
|
|
35
144
|
// compileCssFile is async uniformly, even for a plain `.css` file that needs no preprocessing.
|
|
36
145
|
// See metro-transformer.ts for the fuller sync-vs-async writeup; the short version is that a
|
|
37
146
|
// sync fast-path for `.css` would fork this function into two shapes for a build-time-only,
|
|
38
147
|
// content-hash-cached call that is never a runtime hot path.
|
|
39
|
-
export async function compileCssFile(source, filename
|
|
40
|
-
const
|
|
41
|
-
const css = lang === 'css' ? source : await compile(source, lang, filename);
|
|
42
|
-
const parsed = parseCSS(css, { filename, ...options });
|
|
148
|
+
export async function compileCssFile(source, filename) {
|
|
149
|
+
const css = await toPlainCss(source, filename);
|
|
43
150
|
if (!isCssModuleFile(filename)) {
|
|
151
|
+
const { rules } = compileCssToRules(css, { filename });
|
|
44
152
|
return {
|
|
45
|
-
code: `import {
|
|
153
|
+
code: `import { registerRules } from '@symbiote-native/engine';\n` +
|
|
154
|
+
`registerRules(${serializeRules(rules)});\n`,
|
|
46
155
|
};
|
|
47
156
|
}
|
|
48
|
-
const
|
|
49
|
-
// Scanned against the COMPILED css text, not the raw source: :global(...) isn't native SCSS/
|
|
50
|
-
// Less/Stylus syntax (each preprocessor just passes an unrecognized selector through
|
|
51
|
-
// unchanged), but scanning the compiler's actual output — rather than assuming source and
|
|
52
|
-
// output stay textually identical for this token — is the one that can't drift under nesting/
|
|
53
|
-
// interpolation.
|
|
54
|
-
const exemptFromScope = globalClassNamesIn(css);
|
|
55
|
-
const styles = {};
|
|
56
|
-
const classMap = {};
|
|
57
|
-
for (const [className, props] of Object.entries(parsed)) {
|
|
58
|
-
const isExempt = exemptFromScope.has(className);
|
|
59
|
-
const scopedName = isExempt ? className : `${className}__module__${scopeId}`;
|
|
60
|
-
classMap[className] = scopedName;
|
|
61
|
-
styles[scopedName] = props;
|
|
62
|
-
}
|
|
157
|
+
const { rules, classMap } = compileCssModule(css, filename);
|
|
63
158
|
return {
|
|
64
159
|
code: [
|
|
65
|
-
`import {
|
|
66
|
-
`
|
|
160
|
+
`import { registerRules } from '@symbiote-native/engine';`,
|
|
161
|
+
`registerRules(${serializeRules(rules)});`,
|
|
67
162
|
`export default ${JSON.stringify(classMap)};`,
|
|
68
163
|
].join('\n') + '\n',
|
|
69
164
|
};
|
|
70
165
|
}
|
|
166
|
+
// `combinators` is compile-time-only — the registry matches by token subset and never reads it —
|
|
167
|
+
// so it is stripped rather than shipped in every app bundle. Stage 4 is where it starts to mean
|
|
168
|
+
// something at runtime, and that is when it earns its bytes.
|
|
169
|
+
function serializeRules(rules) {
|
|
170
|
+
return JSON.stringify(rules.map(({ tokens, specificity, order, style }) => ({
|
|
171
|
+
tokens,
|
|
172
|
+
specificity,
|
|
173
|
+
order,
|
|
174
|
+
style,
|
|
175
|
+
})));
|
|
176
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Optional SCSS/Sass, Less, and Stylus preprocessor support. Each compiler here only ever reduces
|
|
2
|
-
// its own syntax down to plain CSS text —
|
|
3
|
-
// downstream consumer of that text, exactly as it
|
|
2
|
+
// its own syntax down to plain CSS text — `lightning/rules.ts`'s `compileCssToRules()` is the
|
|
3
|
+
// single, UNCHANGED downstream consumer of that text, exactly as it is for a plain `.css`
|
|
4
|
+
// file. Tailwind
|
|
4
5
|
// is a separate, out-of-scope concern and has no branch here.
|
|
5
6
|
//
|
|
6
7
|
// `sass`/`less`/`stylus` are lazy `import()`ed, never a top-level import, and are
|
|
@@ -29,7 +30,7 @@ export function isStyleFile(filename) {
|
|
|
29
30
|
* (`compileScss` itself picks the concrete syntax off the extension); anything unrecognized is
|
|
30
31
|
* treated as plain CSS, same as today. */
|
|
31
32
|
export function detectLanguage(filename) {
|
|
32
|
-
return RECOGNIZED_EXTENSIONS.get(path.extname(filename).toLowerCase()) ?? 'css';
|
|
33
|
+
return (RECOGNIZED_EXTENSIONS.get(path.extname(filename).toLowerCase()) ?? 'css');
|
|
33
34
|
}
|
|
34
35
|
let sassModule;
|
|
35
36
|
let lessModule;
|
package/build/properties.d.ts
CHANGED
|
@@ -12,13 +12,12 @@ type IPropertyMapping = {
|
|
|
12
12
|
export declare const PROPERTY_TABLE: Record<string, IPropertyMapping>;
|
|
13
13
|
/**
|
|
14
14
|
* Map one CSS declaration to its RN style entry. Returns `null` and warns once per unique
|
|
15
|
-
* unsupported property name
|
|
16
|
-
* warning fires once per {@link parseCSS} call, not per occurrence).
|
|
15
|
+
* unsupported property name, deduped via the caller-owned `warnedProperties` set.
|
|
17
16
|
*
|
|
18
|
-
* `text-shadow`
|
|
19
|
-
* (unlike `transform`/`box-shadow` above)
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* `text-shadow` is absent from {@link PROPERTY_TABLE} and is NOT handled here: RN has no unified
|
|
18
|
+
* CSS-string `textShadow` prop (unlike `transform`/`box-shadow` above), only three decomposed
|
|
19
|
+
* legacy props, so it needs the typed AST rather than a value string —
|
|
20
|
+
* `lightning/declarations.ts`'s `textShadowToStyle` owns it.
|
|
22
21
|
*/
|
|
23
22
|
export declare function mapCSSProperty(prop: string, value: string, warnedProperties: Set<string>): Record<string, unknown> | null;
|
|
24
23
|
export {};
|
package/build/properties.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// CSS property → React Native ViewStyle/TextStyle prop mapping. RN's own style props already
|
|
2
2
|
// mirror CSS's shorthand model 1:1 (`margin`, `borderRadius`, `borderTopLeftRadius`, …), so this
|
|
3
3
|
// table is a flat kebab→camel rename plus a value-conversion kind — no shorthand expansion is needed.
|
|
4
|
-
import { parseNumeric, parseNumericOrPercent, parseRawValue,
|
|
4
|
+
import { parseNumeric, parseNumericOrPercent, parseRawValue, warnOnce, } from "./values.js";
|
|
5
5
|
/**
|
|
6
6
|
* kebab-case CSS property → { RN camelCase prop, value-conversion kind }.
|
|
7
7
|
* `dimension` = number-or-percent (`parseNumericOrPercent`); `number` = always a plain
|
|
@@ -67,7 +67,10 @@ export const PROPERTY_TABLE = {
|
|
|
67
67
|
// from RN's own `processBackgroundImage.js`. RN's own style prop is itself named with an
|
|
68
68
|
// `experimental_` prefix (still evolving upstream), which is why the RN key doesn't just
|
|
69
69
|
// match a kebab→camel rename of the CSS property the way every other entry here does.
|
|
70
|
-
'background-image': {
|
|
70
|
+
'background-image': {
|
|
71
|
+
rnProperty: 'experimental_backgroundImage',
|
|
72
|
+
kind: 'raw',
|
|
73
|
+
},
|
|
71
74
|
// Spacing
|
|
72
75
|
margin: { rnProperty: 'margin', kind: 'dimension' },
|
|
73
76
|
'margin-top': { rnProperty: 'marginTop', kind: 'dimension' },
|
|
@@ -91,10 +94,22 @@ export const PROPERTY_TABLE = {
|
|
|
91
94
|
'border-bottom-color': { rnProperty: 'borderBottomColor', kind: 'raw' },
|
|
92
95
|
'border-left-color': { rnProperty: 'borderLeftColor', kind: 'raw' },
|
|
93
96
|
'border-radius': { rnProperty: 'borderRadius', kind: 'dimension' },
|
|
94
|
-
'border-top-left-radius': {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
'border-top-left-radius': {
|
|
98
|
+
rnProperty: 'borderTopLeftRadius',
|
|
99
|
+
kind: 'dimension',
|
|
100
|
+
},
|
|
101
|
+
'border-top-right-radius': {
|
|
102
|
+
rnProperty: 'borderTopRightRadius',
|
|
103
|
+
kind: 'dimension',
|
|
104
|
+
},
|
|
105
|
+
'border-bottom-left-radius': {
|
|
106
|
+
rnProperty: 'borderBottomLeftRadius',
|
|
107
|
+
kind: 'dimension',
|
|
108
|
+
},
|
|
109
|
+
'border-bottom-right-radius': {
|
|
110
|
+
rnProperty: 'borderBottomRightRadius',
|
|
111
|
+
kind: 'dimension',
|
|
112
|
+
},
|
|
98
113
|
'border-style': { rnProperty: 'borderStyle', kind: 'raw' },
|
|
99
114
|
// Visual
|
|
100
115
|
'background-color': { rnProperty: 'backgroundColor', kind: 'raw' },
|
|
@@ -122,17 +137,14 @@ function convertValue(kind, value) {
|
|
|
122
137
|
}
|
|
123
138
|
/**
|
|
124
139
|
* Map one CSS declaration to its RN style entry. Returns `null` and warns once per unique
|
|
125
|
-
* unsupported property name
|
|
126
|
-
* warning fires once per {@link parseCSS} call, not per occurrence).
|
|
140
|
+
* unsupported property name, deduped via the caller-owned `warnedProperties` set.
|
|
127
141
|
*
|
|
128
|
-
* `text-shadow`
|
|
129
|
-
* (unlike `transform`/`box-shadow` above)
|
|
130
|
-
*
|
|
131
|
-
*
|
|
142
|
+
* `text-shadow` is absent from {@link PROPERTY_TABLE} and is NOT handled here: RN has no unified
|
|
143
|
+
* CSS-string `textShadow` prop (unlike `transform`/`box-shadow` above), only three decomposed
|
|
144
|
+
* legacy props, so it needs the typed AST rather than a value string —
|
|
145
|
+
* `lightning/declarations.ts`'s `textShadowToStyle` owns it.
|
|
132
146
|
*/
|
|
133
147
|
export function mapCSSProperty(prop, value, warnedProperties) {
|
|
134
|
-
if (prop === 'text-shadow')
|
|
135
|
-
return parseTextShadow(value, warnedProperties);
|
|
136
148
|
const mapping = PROPERTY_TABLE[prop];
|
|
137
149
|
if (!mapping) {
|
|
138
150
|
warnOnce(warnedProperties, prop, `[@symbiote-native/css-parser] unsupported CSS property "${prop}" dropped`);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type IStyleRule } from './lightning/rules.ts';
|
|
2
|
+
export type IScopedCss = {
|
|
3
|
+
readonly rules: readonly IStyleRule[];
|
|
4
|
+
/** Authored class name -> the name it registers under. */
|
|
5
|
+
readonly names: ReadonlyMap<string, string>;
|
|
6
|
+
};
|
|
7
|
+
export type IScopedCssOptions = {
|
|
8
|
+
readonly filename: string;
|
|
9
|
+
/** A lightningcss CSS-Modules pattern, `[local]` first — see the table above. */
|
|
10
|
+
readonly pattern: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A SCOPED style block (a Svelte `<style>`, a Vue `<style scoped>`) compiled for the registry:
|
|
14
|
+
* the rules keyed under their scoped names, and the name map its markup rewriter resolves every
|
|
15
|
+
* class token through.
|
|
16
|
+
*
|
|
17
|
+
* Both halves come out of ONE `compileCssToRules` call. The two-pass shape this used to have —
|
|
18
|
+
* rename, then re-parse the ORIGINAL text and re-key it per token — existed only because the old
|
|
19
|
+
* parser derived a key by camelCasing the selector, which mangled a scope tail whose base36 hash
|
|
20
|
+
* started with a letter (`card__svelte-p4np8c` registered as `card__svelteP4np8c`). Nothing
|
|
21
|
+
* camelCases any more, so there is nothing to work around.
|
|
22
|
+
*/
|
|
23
|
+
export declare function compileScopedCss(css: string, options: IScopedCssOptions): IScopedCss;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The ONE class-renaming call in this repo. Every scoping shape we have is the same lightningcss
|
|
2
|
+
// CSS-Modules rename with a different suffix, so none of them owns a loop of its own:
|
|
3
|
+
//
|
|
4
|
+
// standalone .module.* [local]__module__<hash> metro-css-module/index.ts
|
|
5
|
+
// Vue <style module> [local]__module__<hash>
|
|
6
|
+
// Vue <style scoped> [local]__data-v-<hash>
|
|
7
|
+
// Svelte <style> [local]__svelte-<hash> adapters/svelte/src/preprocessor
|
|
8
|
+
//
|
|
9
|
+
// The rename is not the point — the name MAP is. A scoping pass has two halves (the registered
|
|
10
|
+
// style rules and the markup rewriter) and they must name a class identically; each half deriving
|
|
11
|
+
// the name for itself is the bug class this file exists to close. Both halves read `names` below.
|
|
12
|
+
// lightningcss also decides `:global()` for free: a name it did NOT rename is global, surfaced as
|
|
13
|
+
// `ICompiledCss.globals`, so nothing has to re-walk selectors hunting for the escape hatch.
|
|
14
|
+
//
|
|
15
|
+
// lightningcss is MPL-2.0 and this package is MIT. Depending on it is fine (file-level copyleft
|
|
16
|
+
// does not reach our files); NEVER vendor, copy, or patch its sources.
|
|
17
|
+
import { compileCssToRules } from "./lightning/rules.js";
|
|
18
|
+
/**
|
|
19
|
+
* A SCOPED style block (a Svelte `<style>`, a Vue `<style scoped>`) compiled for the registry:
|
|
20
|
+
* the rules keyed under their scoped names, and the name map its markup rewriter resolves every
|
|
21
|
+
* class token through.
|
|
22
|
+
*
|
|
23
|
+
* Both halves come out of ONE `compileCssToRules` call. The two-pass shape this used to have —
|
|
24
|
+
* rename, then re-parse the ORIGINAL text and re-key it per token — existed only because the old
|
|
25
|
+
* parser derived a key by camelCasing the selector, which mangled a scope tail whose base36 hash
|
|
26
|
+
* started with a letter (`card__svelte-p4np8c` registered as `card__svelteP4np8c`). Nothing
|
|
27
|
+
* camelCases any more, so there is nothing to work around.
|
|
28
|
+
*/
|
|
29
|
+
export function compileScopedCss(css, options) {
|
|
30
|
+
const compiled = compileCssToRules(css, options);
|
|
31
|
+
return {
|
|
32
|
+
rules: compiled.rules,
|
|
33
|
+
names: new Map(Object.entries(compiled.exports)),
|
|
34
|
+
};
|
|
35
|
+
}
|
package/build/values.d.ts
CHANGED
|
@@ -15,13 +15,9 @@ export declare function parseNumericOrPercent(value: string): number | string;
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function parseRawValue(value: string): string;
|
|
17
17
|
/**
|
|
18
|
-
* Warn once per unique `key`
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Warn once per unique `key` against a caller-owned `warned` set. Every "unsupported X dropped"
|
|
19
|
+
* message in this package dedupes through here, so the set's lifetime is what picks the
|
|
20
|
+
* granularity: `compileCssToRules` keeps one per call, `lightning/declarations.ts` one per file.
|
|
21
21
|
*/
|
|
22
22
|
export declare function warnOnce(warned: Set<string>, key: string, message: string): void;
|
|
23
|
-
/** `text-shadow` → RN's `textShadowColor`/`textShadowOffset`/`textShadowRadius` (no Android
|
|
24
|
-
* elevation equivalent — RN has no elevation concept for text, and no engine-level processor
|
|
25
|
-
* to defer to the way `box-shadow` does — see the PROPERTY_TABLE comment in properties.ts). */
|
|
26
|
-
export declare function parseTextShadow(value: string, warned: Set<string>): Record<string, unknown> | null;
|
|
27
23
|
export { REM_TO_PX };
|
package/build/values.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// CSS → React Native value conversion. RN style props take plain numbers for `px` / unitless
|
|
2
2
|
// values, so there is no unit conversion needed here beyond scaling `rem`/`em` — `px` is identity.
|
|
3
|
-
import valueParser from 'postcss-value-parser';
|
|
4
3
|
// symbiote has no root-font-size registry (a DOM `<html>` element would own one); we pick CSS's
|
|
5
4
|
// own default of a 16px root font size as the `rem` multiplier, so `2rem` reads as `32`.
|
|
6
5
|
const REM_TO_PX = 16;
|
|
@@ -37,9 +36,9 @@ export function parseRawValue(value) {
|
|
|
37
36
|
return value.trim();
|
|
38
37
|
}
|
|
39
38
|
/**
|
|
40
|
-
* Warn once per unique `key`
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* Warn once per unique `key` against a caller-owned `warned` set. Every "unsupported X dropped"
|
|
40
|
+
* message in this package dedupes through here, so the set's lifetime is what picks the
|
|
41
|
+
* granularity: `compileCssToRules` keeps one per call, `lightning/declarations.ts` one per file.
|
|
43
42
|
*/
|
|
44
43
|
export function warnOnce(warned, key, message) {
|
|
45
44
|
if (warned.has(key))
|
|
@@ -47,60 +46,4 @@ export function warnOnce(warned, key, message) {
|
|
|
47
46
|
warned.add(key);
|
|
48
47
|
console.warn(message);
|
|
49
48
|
}
|
|
50
|
-
// A length token always starts with a digit, `.`, or `-` (`0`, `2px`, `-4px`); a color token
|
|
51
|
-
// never does, whether it's a keyword (`red`), a hex (`#fff`), or a function (`rgba(0,0,0,.3)`) —
|
|
52
|
-
// so classifying by leading character separates them without needing a CSS color grammar.
|
|
53
|
-
const LENGTH_TOKEN_PATTERN = /^-?[\d.]/;
|
|
54
|
-
/** Split a `text-shadow` value on its TOP-LEVEL commas only — a comma inside a color function
|
|
55
|
-
* (`rgba(0, 0, 0, .3)`) must not split a single shadow layer in two. */
|
|
56
|
-
function splitShadowLayers(value) {
|
|
57
|
-
const layers = [];
|
|
58
|
-
let current = [];
|
|
59
|
-
for (const node of valueParser(value.trim()).nodes) {
|
|
60
|
-
if (node.type === 'div' && node.value === ',') {
|
|
61
|
-
layers.push(valueParser.stringify(current).trim());
|
|
62
|
-
current = [];
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
current.push(node);
|
|
66
|
-
}
|
|
67
|
-
layers.push(valueParser.stringify(current).trim());
|
|
68
|
-
return layers.filter(Boolean);
|
|
69
|
-
}
|
|
70
|
-
function parseShadowTokens(layer) {
|
|
71
|
-
const rawTokens = valueParser(layer)
|
|
72
|
-
.nodes.filter(node => node.type !== 'space' && node.type !== 'div')
|
|
73
|
-
.map(node => valueParser.stringify(node));
|
|
74
|
-
const lengths = rawTokens.filter(token => LENGTH_TOKEN_PATTERN.test(token));
|
|
75
|
-
const colorToken = rawTokens.find(token => !LENGTH_TOKEN_PATTERN.test(token));
|
|
76
|
-
if (lengths.length < 2)
|
|
77
|
-
return null;
|
|
78
|
-
return {
|
|
79
|
-
offsetX: parseNumeric(lengths[0]),
|
|
80
|
-
offsetY: parseNumeric(lengths[1]),
|
|
81
|
-
blurRadius: lengths[2] !== undefined ? parseNumeric(lengths[2]) : 0,
|
|
82
|
-
color: colorToken ?? '#000000',
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
/** `text-shadow` → RN's `textShadowColor`/`textShadowOffset`/`textShadowRadius` (no Android
|
|
86
|
-
* elevation equivalent — RN has no elevation concept for text, and no engine-level processor
|
|
87
|
-
* to defer to the way `box-shadow` does — see the PROPERTY_TABLE comment in properties.ts). */
|
|
88
|
-
export function parseTextShadow(value, warned) {
|
|
89
|
-
const layers = splitShadowLayers(value);
|
|
90
|
-
if (layers.length > 1) {
|
|
91
|
-
warnOnce(warned, 'text-shadow:multiple', '[@symbiote-native/css-parser] multiple text-shadow layers are not supported, only the first is applied');
|
|
92
|
-
}
|
|
93
|
-
const first = layers[0];
|
|
94
|
-
if (!first)
|
|
95
|
-
return null;
|
|
96
|
-
const tokens = parseShadowTokens(first);
|
|
97
|
-
if (!tokens)
|
|
98
|
-
return null;
|
|
99
|
-
return {
|
|
100
|
-
textShadowColor: tokens.color,
|
|
101
|
-
textShadowOffset: { width: tokens.offsetX, height: tokens.offsetY },
|
|
102
|
-
textShadowRadius: tokens.blurRadius,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
//#endregion text-shadow
|
|
106
49
|
export { REM_TO_PX };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/css-parser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Build-time CSS/SCSS/Less/Stylus compiler for SymbioteNative — compiles stylesheets to React Native style objects, resolved at runtime via a cross-adapter class-name registry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,6 +17,17 @@
|
|
|
17
17
|
"main": "./build/index.js",
|
|
18
18
|
"module": "./build/index.js",
|
|
19
19
|
"types": "./build/index.d.ts",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"react-native",
|
|
22
|
+
"symbiote-native",
|
|
23
|
+
"css",
|
|
24
|
+
"scss",
|
|
25
|
+
"css-modules",
|
|
26
|
+
"postcss",
|
|
27
|
+
"lightningcss",
|
|
28
|
+
"metro",
|
|
29
|
+
"build-tool"
|
|
30
|
+
],
|
|
20
31
|
"exports": {
|
|
21
32
|
".": {
|
|
22
33
|
"types": "./build/index.d.ts",
|
|
@@ -41,8 +52,7 @@
|
|
|
41
52
|
},
|
|
42
53
|
"dependencies": {
|
|
43
54
|
"@react-native/metro-babel-transformer": "0.86.0",
|
|
44
|
-
"
|
|
45
|
-
"postcss-value-parser": "^4.2.0"
|
|
55
|
+
"lightningcss": "^1.32.0"
|
|
46
56
|
},
|
|
47
57
|
"devDependencies": {
|
|
48
58
|
"sass": "^1.101.0",
|
package/typescript-plugin.cjs
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
// separate, on-disk source of truth for `tsc`/`vue-tsc` CLI runs — plugins never load there.
|
|
9
9
|
//
|
|
10
10
|
// Core mechanism: override getScriptSnapshot + resolveModuleNameLiterals to synthesize a virtual
|
|
11
|
-
// .d.ts for the import. Two things worth calling out: (1) the class extractor
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// .d.ts for the import. Two things worth calling out: (1) the class extractor keeps the AUTHORED
|
|
12
|
+
// spelling, because that is the key the runtime export map carries — see
|
|
13
|
+
// src/generate-dts/index.ts's classNamesToDtsSource, which this plugin's dts shape mirrors, and
|
|
14
|
+
// which quotes a non-identifier key for the same reason; (2) the dts
|
|
15
15
|
// cache is keyed on the file's mtime, so autocomplete doesn't go stale after editing the CSS file
|
|
16
16
|
// and wait for the IDE to restart tsserver.
|
|
17
17
|
//
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
// completion in the plugin.
|
|
25
25
|
//
|
|
26
26
|
// SCOPE, second cut: only a SIMPLE `.foo { ... }` class selector is recognized correctly — a
|
|
27
|
-
// compound (`.btn.primary`) or descendant (`.card .title`) selector
|
|
28
|
-
//
|
|
29
|
-
//
|
|
27
|
+
// compound (`.btn.primary`) or descendant (`.card .title`) selector is extracted here as TWO
|
|
28
|
+
// separate keys, where the real compiler keeps the tokens together on one rule and exports only
|
|
29
|
+
// the names the author actually wrote.
|
|
30
30
|
//
|
|
31
31
|
// Hand-written plain CommonJS, NOT compiled from a `.ts`/`.cts` source — same convention already
|
|
32
32
|
// used for each adapter's metro-css-parser.cjs shim. tsserver loads a plugin via a synchronous
|
|
@@ -46,17 +46,19 @@ function isCssModuleFile(fileName) {
|
|
|
46
46
|
return CSS_MODULE_RE.test(fileName);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
// Names come out AS AUTHORED — `.section-tight` stays `section-tight`, read as
|
|
50
|
+
// `styles['section-tight']`. This must track `compileCssModule`'s export map exactly: the editor
|
|
51
|
+
// offering a key the runtime does not carry is the one failure this plugin can produce, and it
|
|
52
|
+
// fails in both directions at once (a suggested `sectionTight` is `undefined` at runtime, while
|
|
53
|
+
// the real `styles['section-tight']` reads as a TS2339). `generateDts` quotes a key that is not a
|
|
54
|
+
// valid identifier, which is what makes the kebab spelling usable.
|
|
53
55
|
function extractClassNames(css) {
|
|
54
56
|
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
55
57
|
const names = new Set();
|
|
56
58
|
const classRe = /\.([a-zA-Z_][\w-]*)/g;
|
|
57
59
|
let match;
|
|
58
60
|
while ((match = classRe.exec(withoutComments))) {
|
|
59
|
-
names.add(
|
|
61
|
+
names.add(match[1]);
|
|
60
62
|
}
|
|
61
63
|
return [...names];
|
|
62
64
|
}
|
|
@@ -112,7 +114,9 @@ function init(modules) {
|
|
|
112
114
|
return dts;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
const originalGetScriptKind = host.getScriptKind
|
|
117
|
+
const originalGetScriptKind = host.getScriptKind
|
|
118
|
+
? host.getScriptKind.bind(host)
|
|
119
|
+
: undefined;
|
|
116
120
|
const originalGetScriptSnapshot = host.getScriptSnapshot.bind(host);
|
|
117
121
|
const originalResolveModuleNameLiterals = host.resolveModuleNameLiterals;
|
|
118
122
|
|
|
@@ -150,7 +154,10 @@ function init(modules) {
|
|
|
150
154
|
return literals.map((literal, index) => {
|
|
151
155
|
const moduleName = literal.text;
|
|
152
156
|
if (isCssModuleFile(moduleName) && moduleName.startsWith('.')) {
|
|
153
|
-
const resolvedPath = resolveRelativePath(
|
|
157
|
+
const resolvedPath = resolveRelativePath(
|
|
158
|
+
moduleName,
|
|
159
|
+
containingFile,
|
|
160
|
+
);
|
|
154
161
|
if (host.fileExists && host.fileExists(resolvedPath)) {
|
|
155
162
|
return {
|
|
156
163
|
resolvedModule: {
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { type ICssParserOptions } from './parser/index.ts';
|
|
2
|
-
/**
|
|
3
|
-
* The registered keys whose ENTIRE selector lived outside the file's scope — `:global(.reset)` →
|
|
4
|
-
* `reset`, `:global(.btn.primary)` → `btnPrimary`. These register under their plain name and get
|
|
5
|
-
* no scope suffix.
|
|
6
|
-
*
|
|
7
|
-
* A selector with a scoped part (`.card :global(.reset)` → `cardReset`) is deliberately absent:
|
|
8
|
-
* the rule still only applies where the file's own `.card` does, so its collapsed key belongs to
|
|
9
|
-
* this file. Only its `:global()` half escapes, which is {@link globalClassTokensIn}'s answer.
|
|
10
|
-
*/
|
|
11
|
-
export declare function globalClassNamesIn(css: string, options?: ICssParserOptions): Set<string>;
|
|
12
|
-
/**
|
|
13
|
-
* Every class token that came out of a `:global(...)` payload, wherever in a selector it sat —
|
|
14
|
-
* `.card :global(.legacy-widget) span` → `{ legacyWidget }`.
|
|
15
|
-
*
|
|
16
|
-
* This is the set a scope-suffixing caller subtracts from the tokens it owns. A token is in it
|
|
17
|
-
* whether or not the selector around it was scoped, which is the whole point: the author reached
|
|
18
|
-
* for `:global()` precisely because that name is spelled the same way in markup this file does
|
|
19
|
-
* not own, and a suffix would break the match it was reaching for.
|
|
20
|
-
*
|
|
21
|
-
* A selector the parser rejects contributes nothing — it registers no key, so it names nothing to
|
|
22
|
-
* exempt, and letting its payload leak in here would unscope a token some other rule legitimately
|
|
23
|
-
* owns.
|
|
24
|
-
*/
|
|
25
|
-
export declare function globalClassTokensIn(css: string, options?: ICssParserOptions): Set<string>;
|