@symbiote-native/css-parser 0.2.3 → 0.4.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 +46 -26
- 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 +21 -0
- package/build/lightning/selectors.js +372 -0
- package/build/metro-css-module/index.d.ts +30 -2
- package/build/metro-css-module/index.js +142 -36
- package/build/metro-transformer/index.js +14 -22
- package/build/preprocessors/index.js +4 -3
- package/build/properties.d.ts +5 -6
- package/build/properties.js +29 -17
- 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 +2 -3
- package/typescript-plugin.cjs +32 -25
- package/build/global-selectors.d.ts +0 -1
- package/build/global-selectors.js +0 -22
- package/build/parser/index.d.ts +0 -22
- package/build/parser/index.js +0 -221
|
@@ -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
|
+
}
|
|
@@ -8,31 +8,23 @@
|
|
|
8
8
|
// shamefully-hoist pnpm config (.npmrc) makes that resolvable without the app adding
|
|
9
9
|
// @symbiote-native/css-parser to its own package.json.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// `
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// either way — see preprocessors.ts). A sync fast-path could still be kept for plain `.css`, but
|
|
20
|
-
// that forks this function into two shapes to save a single microtask on a call that only ever
|
|
21
|
-
// runs at Metro build time, content-hash-cached, never a runtime hot path — not worth the
|
|
22
|
-
// duplication. `return upstreamTransformer.transform(...)` as the last line of an async function
|
|
23
|
-
// forwards whatever it returns (Promise or not) as this function's own resolved value with no
|
|
24
|
-
// extra `await` needed; Metro awaits the whole chain regardless.
|
|
11
|
+
// `transform()` is async uniformly, even for plain `.css`: Metro's `metro-transform-worker`
|
|
12
|
+
// already awaits `transformer.transform(...)` before using the result (`transformJSWithBabel` in
|
|
13
|
+
// its `index.js`), so a babelTransformerPath module returning a Promise is a supported shape.
|
|
14
|
+
// SCSS/Less/Stylus compilation is inherently async in Node (Less ships no sync render API;
|
|
15
|
+
// Stylus's render is callback-based; Sass's sync `compileString` still needs an async
|
|
16
|
+
// `import('sass')` — see preprocessors.ts). No separate sync path for plain `.css`: this only
|
|
17
|
+
// runs at Metro build time, content-hash-cached, never a runtime hot path, so forking the
|
|
18
|
+
// function to save one microtask isn't worth the duplication.
|
|
25
19
|
import { createRequire } from 'node:module';
|
|
26
20
|
import { compileCssFile } from "../metro-css-module/index.js";
|
|
27
21
|
import { isStyleFile } from "../preprocessors/index.js";
|
|
28
|
-
// @react-native/metro-babel-transformer is a real `dependency` of this package
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// branch) can reuse this instead of its own fragile direct `require('@react-native/metro-babel-
|
|
35
|
-
// transformer')`, which would only resolve for an external install by accident of hoisting.
|
|
22
|
+
// @react-native/metro-babel-transformer is a real `dependency` of this package, so it resolves
|
|
23
|
+
// via css-parser's own node_modules under pnpm — no hoisting/`paths` trick needed, unlike the
|
|
24
|
+
// app-local workaround this replaces (formerly duplicated in every adapter's example
|
|
25
|
+
// metro-css-transformer.js). Exported so a per-framework transformer that also needs the
|
|
26
|
+
// upstream RN transformer (e.g. the Vue SFC transformer's non-.vue passthrough branch) can reuse
|
|
27
|
+
// this instead of its own fragile direct `require('@react-native/metro-babel-transformer')`.
|
|
36
28
|
export function resolveUpstreamTransformer() {
|
|
37
29
|
const require = createRequire(import.meta.url);
|
|
38
30
|
return require('@react-native/metro-babel-transformer');
|
|
@@ -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
|
|
@@ -35,9 +35,9 @@ export const PROPERTY_TABLE = {
|
|
|
35
35
|
overflow: { rnProperty: 'overflow', kind: 'raw' },
|
|
36
36
|
// Only `flex`/`none` are valid RN values; passed through unvalidated per spec.
|
|
37
37
|
display: { rnProperty: 'display', kind: 'raw' },
|
|
38
|
-
// A genuine 1:1 CSS property (unlike transform/shadow
|
|
39
|
-
//
|
|
40
|
-
//
|
|
38
|
+
// A genuine 1:1 CSS property (unlike transform/shadow, no shape mismatch). `2 / 3` string
|
|
39
|
+
// ratios are not accepted here (`parseNumeric` requires a plain number) — CSS
|
|
40
|
+
// `aspect-ratio: 0.667` works, `aspect-ratio: 2/3` doesn't yet.
|
|
41
41
|
'aspect-ratio': { rnProperty: 'aspectRatio', kind: 'number' },
|
|
42
42
|
gap: { rnProperty: 'gap', kind: 'dimension' },
|
|
43
43
|
'row-gap': { rnProperty: 'rowGap', kind: 'dimension' },
|
|
@@ -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.4.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": {
|
|
@@ -41,8 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@react-native/metro-babel-transformer": "0.86.0",
|
|
44
|
-
"
|
|
45
|
-
"postcss-value-parser": "^4.2.0"
|
|
44
|
+
"lightningcss": "^1.32.0"
|
|
46
45
|
},
|
|
47
46
|
"devDependencies": {
|
|
48
47
|
"sass": "^1.101.0",
|