@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 CHANGED
@@ -32,32 +32,35 @@ Node build machine — never shipped in the app's native JS bundle. Each adapter
32
32
  as a regular dependency and re-exports it via its own `./metro-css-parser` subpath, so a consuming
33
33
  app's `metro.config.js` wires:
34
34
 
35
- ```js
36
- // metro-css-transformer.js, in the app
37
- const { createCssMetroTransformer } = require('@symbiote-native/react/metro-css-parser');
38
- module.exports = createCssMetroTransformer(require('@react-native/metro-babel-transformer'));
39
- ```
40
-
41
35
  ```js
42
36
  // metro.config.js
43
37
  resolver: { sourceExts: [...defaultSourceExts, 'css', 'scss', 'sass', 'less', 'styl'] },
44
- transformer: { babelTransformerPath: require.resolve('./metro-css-transformer.js') },
38
+ transformer: { babelTransformerPath: require.resolve('@symbiote-native/react/metro-css-parser') },
45
39
  ```
46
40
 
41
+ The subpath already calls `createCssMetroTransformer` and exports the finished transformer, so an
42
+ app writes no transformer file of its own — point `babelTransformerPath` straight at it. Reach for
43
+ `createCssMetroTransformer` only when building the subpath for a NEW adapter.
44
+
47
45
  From there, a plain stylesheet import just works, from any adapter's own source file:
48
46
 
49
47
  ```ts
50
- import styles from './Card.module.css'; // CSS Modules — default export is a name→scopedName map
51
- import './theme.css'; // plain CSS — registers classes globally, no export
48
+ import styles from './Card.module.css'; // CSS Modules — default export is a name→scopedName map
49
+ import './theme.css'; // plain CSS — registers classes globally, no export
52
50
  ```
53
51
 
54
52
  ```tsx
55
- <View className="card" style={styles.highlight} /> // React
53
+ <View className="card" style={styles.highlight} /> // React
56
54
  ```
55
+
57
56
  ```html
58
57
  <!-- Vue SFC -->
59
58
  <view :class="['card', { active: isActive }]" />
60
- <style scoped>.card { padding: 10px; }</style>
59
+ <style scoped>
60
+ .card {
61
+ padding: 10px;
62
+ }
63
+ </style>
61
64
  ```
62
65
 
63
66
  ## The pipeline
@@ -67,31 +70,48 @@ import './theme.css'; // plain CSS — registers classes glo
67
70
  │ (build time, Metro) │ (runtime, all adapters)
68
71
  ▼ ▼
69
72
  @symbiote-native/css-parser @symbiote-native/engine's style-registry
70
- preprocessors.ts → parser.ts (parseCSS) registerStyles() / resolveClassName()
73
+ preprocessors → lightning (compileCssToRules) registerRules() / resolveClassName()
71
74
  ```
72
75
 
73
76
  A preprocessor source is reduced to plain CSS text first (`compileScss`/`compileSass`/
74
- `compileLess`/`compileStylus`); `parseCSS()` is the single downstream consumer either way, so every
75
- mechanism below runs identically regardless of source language.
77
+ `compileLess`/`compileStylus`); `compileCssToRules()` is the single downstream consumer either way,
78
+ so every mechanism below runs identically regardless of source language.
76
79
 
77
80
  ## API surface
78
81
 
79
82
  ```ts
80
83
  import {
81
- parseCSS, extractClassName, kebabToCamel, // core compiler
82
- compileCssFile, isCssModuleFile, // standalone .css/.module.css files
83
- createCssMetroTransformer, // Metro babelTransformerPath factory
84
- compileScss, compileSass, compileLess, compileStylus, compile, detectLanguage, isStyleFile,
85
- classNamesToDtsSource, generateModuleDts, // .d.ts generation for CSS Modules typing
86
- globalClassNamesIn, hashFilePath,
84
+ compileCssToRules, // core compiler
85
+ compileScopedCss, // a scoped <style> block, rules + name map
86
+ compileCssFile,
87
+ isCssModuleFile, // standalone .css/.module.css files
88
+ createCssMetroTransformer, // Metro babelTransformerPath factory
89
+ compileScss,
90
+ compileSass,
91
+ compileLess,
92
+ compileStylus,
93
+ compile,
94
+ detectLanguage,
95
+ isStyleFile,
96
+ classNamesToDtsSource,
97
+ generateModuleDts, // .d.ts generation for CSS Modules typing
98
+ hashFilePath,
87
99
  } from '@symbiote-native/css-parser';
88
100
  ```
89
101
 
90
- - **`parseCSS(css, { filename? })`** — the compiler core: postcss AST walk, `var()`/`calc()`
91
- resolution, selector → camelCase key (`.card` → `card`, `.btn.primary` → `btnPrimary` compound,
92
- `.card .title` → `cardTitle` descendant). A selector containing a pseudo-class (`:hover`, …) is
93
- dropped whole — RN has no pseudo-class concept, so there is no partial-application semantics to
94
- preserve.
102
+ - **`compileCssToRules(css, { filename, pattern?, remToPx? })`** — the compiler core: one
103
+ lightningcss pass over the typed AST, resolving `var()`/`calc()` and emitting a `rules` array.
104
+ Each rule carries the class TOKENS its selector was written with — as authored, never camelCased
105
+ or collapsed into a single key — plus specificity and source order, so the registry matches by
106
+ token subset instead of reversing a guess. With a CSS-Modules `pattern` it also returns the
107
+ authored→renamed `exports` map and `globals`, the tokens lightningcss did NOT rename, which is
108
+ exactly the set the author put inside `:global(...)`. A selector containing a pseudo-class
109
+ (`:hover`, …) is dropped whole — RN has no pseudo-class concept, so there is no
110
+ partial-application semantics to preserve.
111
+ - **`compileScopedCss(css, { filename, pattern })`** — the scoped-block form (a Svelte `<style>`, a
112
+ Vue `<style scoped>`): the same rules plus the authored→scoped name map its markup rewriter
113
+ resolves every class token through. Both halves come out of ONE compile, so the style side and
114
+ the markup side cannot disagree on a name.
95
115
  - **`compileCssFile` / `isCssModuleFile`** — the standalone-file form: `Card.module.css`'s classes
96
116
  are always scoped to a per-file hash and its default export is the name→scopedName map; a plain
97
117
  `.css` file registers globally via a side-effect import.
@@ -121,7 +141,7 @@ import {
121
141
 
122
142
  ## Related packages
123
143
 
124
- - [`@symbiote-native/engine`](../engine) — owns the runtime `style-registry` (`registerStyles` /
144
+ - [`@symbiote-native/engine`](../engine) — owns the runtime `style-registry` (`registerRules` /
125
145
  `resolveClassName`) this package's compiled output resolves against, and the class+style merge
126
146
  used by every adapter.
127
147
  - [`@symbiote-native/react`](../../adapters/react) / [`@symbiote-native/vue`](../../adapters/vue) /
@@ -8,9 +8,7 @@
8
8
  // appended rather than replaced — e.g. `Card.module.css.d.ts`) so TypeScript's own module
9
9
  // resolution picks it up for a default import of `Card.module.css` without a separate
10
10
  // registration step, the same convention typed-css-modules uses.
11
- import { parseCSS } from "../parser/index.js";
12
- import { compile, detectLanguage } from "../preprocessors/index.js";
13
- import { isCssModuleFile } from "../metro-css-module/index.js";
11
+ import { isCssModuleFile, moduleClassNames, } from "../metro-css-module/index.js";
14
12
  const IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
15
13
  function formatKey(className) {
16
14
  return IDENTIFIER_RE.test(className) ? className : JSON.stringify(className);
@@ -31,13 +29,10 @@ export function classNamesToDtsSource(classNames) {
31
29
  ].join('\n');
32
30
  }
33
31
  // Mirrors compileCssFile's own module/non-module branch: a plain (non-`.module.*`) style file
34
- // has no default export to type (registerStyles() runs as a side effect only), so it gets no
32
+ // has no default export to type (registerRules() runs as a side effect only), so it gets no
35
33
  // `.d.ts` at all.
36
34
  export async function generateModuleDts(source, filename) {
37
35
  if (!isCssModuleFile(filename))
38
36
  return null;
39
- const lang = detectLanguage(filename);
40
- const css = lang === 'css' ? source : await compile(source, lang, filename);
41
- const parsed = parseCSS(css, { filename });
42
- return classNamesToDtsSource(Object.keys(parsed));
37
+ return classNamesToDtsSource(await moduleClassNames(source, filename));
43
38
  }
@@ -17,7 +17,11 @@ import * as path from 'node:path';
17
17
  import { isStyleFile } from "./preprocessors/index.js";
18
18
  import { isCssModuleFile } from "./metro-css-module/index.js";
19
19
  import { generateModuleDts } from "./generate-dts/index.js";
20
- const SKIPPED_DIR_NAMES = new Set(['node_modules', 'build', '.git']);
20
+ const SKIPPED_DIR_NAMES = new Set([
21
+ 'node_modules',
22
+ 'build',
23
+ '.git',
24
+ ]);
21
25
  async function collectModuleStyleFiles(root) {
22
26
  const stat = await fs.stat(root);
23
27
  if (!stat.isDirectory()) {
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ // Fixture, not a running component. examples/svelte has exactly one styled component and it
3
+ // carries no :global(), so the escape hatch had no golden coverage on this adapter either.
4
+ let isLoud = $state(false);
5
+ </script>
6
+
7
+ <View class="panel">
8
+ <View class="panel wide" />
9
+ <View class={['panel', isLoud && 'wide']} />
10
+ <View class="untouched" />
11
+ </View>
12
+
13
+ <style>
14
+ .panel {
15
+ padding: 9px;
16
+ }
17
+
18
+ .panel.wide {
19
+ padding: 19px;
20
+ }
21
+
22
+ :global(.untouched) {
23
+ margin: 0;
24
+ }
25
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const ScopedGlobalDemo: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ScopedGlobalDemo = ReturnType<typeof ScopedGlobalDemo>;
3
+ export default ScopedGlobalDemo;
package/build/index.d.ts CHANGED
@@ -1,11 +1,12 @@
1
- export { parseCSS, extractClassName, kebabToCamel } from './parser/index.ts';
2
- export type { ICssParserOptions } from './parser/index.ts';
3
- export { globalClassNamesIn } from './global-selectors.ts';
1
+ export { compileCssToRules } from './lightning/rules.ts';
2
+ export type { ICompiledCss, ICompileRulesOptions, IStyleRule, } from './lightning/rules.ts';
4
3
  export { hashFilePath } from './file-scope-id.ts';
5
- export { compileCssFile, isCssModuleFile } from './metro-css-module/index.ts';
6
- export type { ICompiledCssFile } from './metro-css-module/index.ts';
7
- export { classNamesToDtsSource, generateModuleDts } from './generate-dts/index.ts';
4
+ export { compileScopedCss } from './scoped-classes.ts';
5
+ export type { IScopedCss, IScopedCssOptions } from './scoped-classes.ts';
6
+ export { compileCssFile, compileCssModule, isCssModuleFile, } from './metro-css-module/index.ts';
7
+ export type { ICompiledCssFile, ICompiledCssModule, } from './metro-css-module/index.ts';
8
+ export { classNamesToDtsSource, generateModuleDts, } from './generate-dts/index.ts';
8
9
  export { createCssMetroTransformer, resolveUpstreamTransformer, } from './metro-transformer/index.ts';
9
- export type { IMetroTransformer, IMetroTransformParams } from './metro-transformer/index.ts';
10
+ export type { IMetroTransformer, IMetroTransformParams, } from './metro-transformer/index.ts';
10
11
  export { compileScss, compileSass, compileLess, compileStylus, compile, detectLanguage, isStyleFile, } from './preprocessors/index.ts';
11
12
  export type { IPreprocessorLanguage } from './preprocessors/index.ts';
package/build/index.js CHANGED
@@ -1,7 +1,7 @@
1
- export { parseCSS, extractClassName, kebabToCamel } from "./parser/index.js";
2
- export { globalClassNamesIn } from "./global-selectors.js";
1
+ export { compileCssToRules } from "./lightning/rules.js";
3
2
  export { hashFilePath } from "./file-scope-id.js";
4
- export { compileCssFile, isCssModuleFile } from "./metro-css-module/index.js";
5
- export { classNamesToDtsSource, generateModuleDts } from "./generate-dts/index.js";
3
+ export { compileScopedCss } from "./scoped-classes.js";
4
+ export { compileCssFile, compileCssModule, isCssModuleFile, } from "./metro-css-module/index.js";
5
+ export { classNamesToDtsSource, generateModuleDts, } from "./generate-dts/index.js";
6
6
  export { createCssMetroTransformer, resolveUpstreamTransformer, } from "./metro-transformer/index.js";
7
7
  export { compileScss, compileSass, compileLess, compileStylus, compile, detectLanguage, isStyleFile, } from "./preprocessors/index.js";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * An OBJECT-shaped RN style value. `core/engine/src/styles.ts` has exactly one such field,
3
+ * `shadowOffset?: { width: number; height: number }`; RN's `textShadowOffset` has the same shape
4
+ * (it is missing from `ITextStyle` there, but `textShadowToStyle` below emits it and RN reads
5
+ * it). Both are all-number, so one record type covers the family.
6
+ */
7
+ export type IStyleValueObject = Readonly<Record<string, number>>;
8
+ /**
9
+ * One React Native style value.
10
+ *
11
+ * Most are scalars. Two are objects ({@link IStyleValueObject}). Six are ARRAY-capable in RN —
12
+ * `transform: ITransformProp[]`, `boxShadow`, `filter`, `experimental_backgroundImage`,
13
+ * `transformOrigin`, `fontVariant` — but every one of them is `raw` in PROPERTY_TABLE, i.e. this
14
+ * pipeline hands the engine the CSS TEXT and lets its own processors (`process-transform`,
15
+ * `process-box-shadow`, …) build the array at commit time. So the array branch exists so the
16
+ * integration step cannot hit a second widening, not because a declaration produces one today.
17
+ */
18
+ export type IStyleValue = string | number | IStyleValueObject | ReadonlyArray<string | number | IStyleValueObject>;
19
+ export type IStyleObject = Record<string, IStyleValue>;
20
+ export interface IDeclarationContext {
21
+ readonly filename: string;
22
+ /** Custom properties (`--x`) collected from the whole file, name -> raw value text. */
23
+ readonly variables: ReadonlyMap<string, string>;
24
+ /** Root font size for rem, default 16. */
25
+ readonly remToPx?: number;
26
+ }
27
+ /**
28
+ * One lightningcss `Declaration` (the object handed to a visitor / found in
29
+ * `rule.value.declarations.declarations`) mapped to zero or more RN style entries.
30
+ */
31
+ export declare function declarationToStyle(declaration: unknown, context: IDeclarationContext): IStyleObject;
32
+ /**
33
+ * Every custom property (`--x`) declared anywhere in the file, name -> its raw value text, in the
34
+ * shape {@link IDeclarationContext.variables} expects.
35
+ *
36
+ * A pass of its OWN, ahead of mapping declarations, because nothing orders `:root` first — a
37
+ * component stylesheet may declare a token in a class rule, inside `@media`, or below its first
38
+ * use, and a single forward walk would miss it. Later declaration of the same name wins, which is
39
+ * what the cascade does within one file.
40
+ *
41
+ * The value is stored VERBATIM: a token list that is itself a `var()` chain is printed back as
42
+ * `var(--other)`, and resolution stays in {@link declarationToStyle}, which is the only place that
43
+ * knows whether the chain terminates. Serialization goes through the same token printer the
44
+ * `unparsed` path uses — this package keeps ONE printer.
45
+ */
46
+ export declare function variablesIn(css: string, filename: string): ReadonlyMap<string, string>;