@symbiote-native/css-parser 0.1.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/LICENSE +21 -0
- package/README.md +126 -0
- package/build/file-scope-id.d.ts +1 -0
- package/build/file-scope-id.js +14 -0
- package/build/generate-dts-cli.d.ts +2 -0
- package/build/generate-dts-cli.js +101 -0
- package/build/generate-dts.d.ts +2 -0
- package/build/generate-dts.js +43 -0
- package/build/global-selectors.d.ts +1 -0
- package/build/global-selectors.js +22 -0
- package/build/index.d.ts +11 -0
- package/build/index.js +7 -0
- package/build/metro-css-module.d.ts +6 -0
- package/build/metro-css-module.js +70 -0
- package/build/metro-transformer.d.ts +10 -0
- package/build/metro-transformer.js +41 -0
- package/build/parser.d.ts +22 -0
- package/build/parser.js +223 -0
- package/build/preprocessors.d.ts +20 -0
- package/build/preprocessors.js +128 -0
- package/build/properties.d.ts +24 -0
- package/build/properties.js +144 -0
- package/build/values.d.ts +27 -0
- package/build/values.js +107 -0
- package/package.json +65 -0
- package/typescript-plugin.cjs +181 -0
- package/typescript-plugin.d.ts +8 -0
package/build/values.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// CSS → React Native value conversion. RN style props take plain numbers for `px` / unitless
|
|
2
|
+
// values (there is no terminal-cell or DP grid to scale onto, unlike wolf-tui's TUI target), so
|
|
3
|
+
// unlike wolf-tui's `values.ts` there is no unit-to-cell division here — `px` is identity.
|
|
4
|
+
import valueParser from 'postcss-value-parser';
|
|
5
|
+
// symbiote has no root-font-size registry (a DOM `<html>` element would own one); we pick CSS's
|
|
6
|
+
// own default of a 16px root font size as the `rem` multiplier, so `2rem` reads as `32`.
|
|
7
|
+
const REM_TO_PX = 16;
|
|
8
|
+
const NUMBER_WITH_UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(px|rem|em)?$/;
|
|
9
|
+
const PERCENT_PATTERN = /%$/;
|
|
10
|
+
/**
|
|
11
|
+
* Convert a CSS dimension to a plain number: strips `px`, scales `rem`/`em` by
|
|
12
|
+
* {@link REM_TO_PX}, and passes unitless numbers through untouched.
|
|
13
|
+
*/
|
|
14
|
+
export function parseNumeric(value) {
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
const match = trimmed.match(NUMBER_WITH_UNIT_PATTERN);
|
|
17
|
+
if (!match) {
|
|
18
|
+
const bare = parseFloat(trimmed);
|
|
19
|
+
return Number.isNaN(bare) ? 0 : bare;
|
|
20
|
+
}
|
|
21
|
+
const amount = parseFloat(match[1]);
|
|
22
|
+
const unit = match[2];
|
|
23
|
+
return unit === 'rem' || unit === 'em' ? amount * REM_TO_PX : amount;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Same as {@link parseNumeric}, except a percentage value is kept as a string
|
|
27
|
+
* (`'50%'`) — RN accepts percentage strings for most layout props.
|
|
28
|
+
*/
|
|
29
|
+
export function parseNumericOrPercent(value) {
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
return PERCENT_PATTERN.test(trimmed) ? trimmed : parseNumeric(trimmed);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Colors, font families, and CSS keyword values (`'bold'`, `'row'`, `'red'`) pass through
|
|
35
|
+
* unchanged — RN accepts the same raw strings CSS does for these props.
|
|
36
|
+
*/
|
|
37
|
+
export function parseRawValue(value) {
|
|
38
|
+
return value.trim();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Warn once per unique `key` across a {@link parseCSS} call (the caller-owned `warned` set is
|
|
42
|
+
* shared with the plain-property drop warning in properties.ts, so every "unsupported X dropped"
|
|
43
|
+
* message in this package dedupes the same way).
|
|
44
|
+
*/
|
|
45
|
+
export function warnOnce(warned, key, message) {
|
|
46
|
+
if (warned.has(key))
|
|
47
|
+
return;
|
|
48
|
+
warned.add(key);
|
|
49
|
+
console.warn(message);
|
|
50
|
+
}
|
|
51
|
+
// A length token always starts with a digit, `.`, or `-` (`0`, `2px`, `-4px`); a color token
|
|
52
|
+
// never does, whether it's a keyword (`red`), a hex (`#fff`), or a function (`rgba(0,0,0,.3)`) —
|
|
53
|
+
// so classifying by leading character separates them without needing a CSS color grammar.
|
|
54
|
+
const LENGTH_TOKEN_PATTERN = /^-?[\d.]/;
|
|
55
|
+
/** Split a `text-shadow` value on its TOP-LEVEL commas only — a comma inside a color function
|
|
56
|
+
* (`rgba(0, 0, 0, .3)`) must not split a single shadow layer in two. */
|
|
57
|
+
function splitShadowLayers(value) {
|
|
58
|
+
const layers = [];
|
|
59
|
+
let current = [];
|
|
60
|
+
for (const node of valueParser(value.trim()).nodes) {
|
|
61
|
+
if (node.type === 'div' && node.value === ',') {
|
|
62
|
+
layers.push(valueParser.stringify(current).trim());
|
|
63
|
+
current = [];
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
current.push(node);
|
|
67
|
+
}
|
|
68
|
+
layers.push(valueParser.stringify(current).trim());
|
|
69
|
+
return layers.filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
function parseShadowTokens(layer) {
|
|
72
|
+
const rawTokens = valueParser(layer)
|
|
73
|
+
.nodes.filter(node => node.type !== 'space' && node.type !== 'div')
|
|
74
|
+
.map(node => valueParser.stringify(node));
|
|
75
|
+
const lengths = rawTokens.filter(token => LENGTH_TOKEN_PATTERN.test(token));
|
|
76
|
+
const colorToken = rawTokens.find(token => !LENGTH_TOKEN_PATTERN.test(token));
|
|
77
|
+
if (lengths.length < 2)
|
|
78
|
+
return null;
|
|
79
|
+
return {
|
|
80
|
+
offsetX: parseNumeric(lengths[0]),
|
|
81
|
+
offsetY: parseNumeric(lengths[1]),
|
|
82
|
+
blurRadius: lengths[2] !== undefined ? parseNumeric(lengths[2]) : 0,
|
|
83
|
+
color: colorToken ?? '#000000',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** `text-shadow` → RN's `textShadowColor`/`textShadowOffset`/`textShadowRadius` (no Android
|
|
87
|
+
* elevation equivalent — RN has no elevation concept for text, and no engine-level processor
|
|
88
|
+
* to defer to the way `box-shadow` does — see the PROPERTY_TABLE comment in properties.ts). */
|
|
89
|
+
export function parseTextShadow(value, warned) {
|
|
90
|
+
const layers = splitShadowLayers(value);
|
|
91
|
+
if (layers.length > 1) {
|
|
92
|
+
warnOnce(warned, 'text-shadow:multiple', '[@symbiote-native/css-parser] multiple text-shadow layers are not supported, only the first is applied');
|
|
93
|
+
}
|
|
94
|
+
const first = layers[0];
|
|
95
|
+
if (!first)
|
|
96
|
+
return null;
|
|
97
|
+
const tokens = parseShadowTokens(first);
|
|
98
|
+
if (!tokens)
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
textShadowColor: tokens.color,
|
|
102
|
+
textShadowOffset: { width: tokens.offsetX, height: tokens.offsetY },
|
|
103
|
+
textShadowRadius: tokens.blurRadius,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
//#endregion text-shadow
|
|
107
|
+
export { REM_TO_PX };
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@symbiote-native/css-parser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Build-time CSS/SCSS/Less/Stylus compiler for SymbioteJS — compiles stylesheets to React Native style objects, resolved at runtime via a cross-adapter class-name registry.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/OneEyed1366/symbiote-native.git",
|
|
8
|
+
"directory": "core/css-parser"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/core/css-parser#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/OneEyed1366/symbiote-native/issues"
|
|
13
|
+
},
|
|
14
|
+
"author": "Andrey Prokopenko <psevdoproger@gmail.com>",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./build/index.js",
|
|
17
|
+
"module": "./build/index.js",
|
|
18
|
+
"types": "./build/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./build/index.d.ts",
|
|
22
|
+
"default": "./build/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./typescript-plugin": {
|
|
25
|
+
"types": "./typescript-plugin.d.ts",
|
|
26
|
+
"default": "./typescript-plugin.cjs"
|
|
27
|
+
},
|
|
28
|
+
"./generate-dts-cli": "./build/generate-dts-cli.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"build",
|
|
32
|
+
"typescript-plugin.cjs",
|
|
33
|
+
"typescript-plugin.d.ts"
|
|
34
|
+
],
|
|
35
|
+
"bin": {
|
|
36
|
+
"css-dts": "./build/generate-dts-cli.js"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"postcss": "^8.4.49",
|
|
43
|
+
"postcss-value-parser": "^4.2.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"sass": "^1.101.0",
|
|
47
|
+
"less": "^4.6.7",
|
|
48
|
+
"stylus": "^0.64.0",
|
|
49
|
+
"@types/less": "^3.0.8",
|
|
50
|
+
"@types/stylus": "^0.48.43",
|
|
51
|
+
"typescript": "~6.0.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"typescript": ">=5.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"typescript": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"typecheck": "tsc --build",
|
|
63
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\""
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// @symbiote-native/css-parser/typescript-plugin — a TypeScript language-service plugin that gives live,
|
|
2
|
+
// in-editor autocomplete AND typo-catching for a standalone `.module.css` import
|
|
3
|
+
// (`import styles from './Card.module.css'`). No terminal, no watch process, no generation step
|
|
4
|
+
// a developer has to remember to run: it hooks into the IDE's OWN TS server (VS Code/WebStorm),
|
|
5
|
+
// so it recomputes on every keystroke the same way the rest of tsserver already does. This is
|
|
6
|
+
// the piece that makes "does the user need to keep something running" a non-question for the
|
|
7
|
+
// live-editing case; generate-dts-cli.ts (`css-dts`, wired to `pretypecheck`) remains the
|
|
8
|
+
// separate, on-disk source of truth for `tsc`/`vue-tsc` CLI runs — plugins never load there.
|
|
9
|
+
//
|
|
10
|
+
// Ported from wolf-tui's `@wolf-tui/typescript-plugin` (wolf-tui/packages/typescript-plugin/
|
|
11
|
+
// src/index.ts) — same core mechanism (override getScriptSnapshot + resolveModuleNameLiterals to
|
|
12
|
+
// synthesize a virtual .d.ts for the import), fixing two real bugs found by reading that source
|
|
13
|
+
// directly: (1) its class extractor never converts kebab-case to camelCase, so a suggested key
|
|
14
|
+
// like `section-tight` does NOT match the ACTUAL exported key our runtime produces
|
|
15
|
+
// (@symbiote-native/css-parser's parseCSS always camelCases — see src/generate-dts.ts's
|
|
16
|
+
// classNamesToDtsSource, which this plugin's dts shape mirrors); (2) its dts cache never
|
|
17
|
+
// invalidates, so autocomplete goes stale after editing the CSS file until the IDE restarts
|
|
18
|
+
// tsserver — this version keys the cache on the file's mtime instead. wolf-tui's package.json
|
|
19
|
+
// also lists a real dependency on `@wolf-tui/css-parser` that its index.ts never actually
|
|
20
|
+
// imports — a leftover of an abandoned attempt to reuse it directly, for the same reason
|
|
21
|
+
// explained below.
|
|
22
|
+
//
|
|
23
|
+
// SCOPE: plain `.module.css` only, not `.module.scss`/`.module.less`/`.module.styl` —
|
|
24
|
+
// getScriptSnapshot must be fully SYNCHRONOUS (tsserver's plugin protocol has no async hook), and
|
|
25
|
+
// while Sass has a genuine sync compile API, Less and Stylus do not (see src/preprocessors.ts) —
|
|
26
|
+
// a correct, non-approximated preprocessor pipeline can't run here today. Those files still get
|
|
27
|
+
// basic (non-literal) type coverage from the project's ambient `.css` fallback declaration and
|
|
28
|
+
// from `css-dts`'s on-disk generation at pretypecheck time — just without live per-class
|
|
29
|
+
// completion in the plugin. A real follow-up, not a silent gap: recorded here, not hidden.
|
|
30
|
+
//
|
|
31
|
+
// SCOPE, second cut: only a SIMPLE `.foo { ... }` class selector is recognized correctly — a
|
|
32
|
+
// compound (`.btn.primary`) or descendant (`.card .title`) selector, which the real
|
|
33
|
+
// src/parser.ts's extractClassName merges into ONE key (`btnPrimary`/`cardTitle`), gets
|
|
34
|
+
// extracted here as TWO separate (wrong, non-existent) keys instead. Same accepted limitation
|
|
35
|
+
// wolf-tui's own README documents for its regex approach ("complex selectors may not be
|
|
36
|
+
// detected").
|
|
37
|
+
//
|
|
38
|
+
// Hand-written plain CommonJS, NOT compiled from a `.ts`/`.cts` source — same convention already
|
|
39
|
+
// used for each adapter's metro-css-parser.cjs shim (see the symbiote-sfc-style-compiler skill's
|
|
40
|
+
// "Distribution" section). tsserver loads a plugin via a synchronous `require()`, which cannot
|
|
41
|
+
// load this package's own ESM build output; a `.cts` source was tried first and rejected because
|
|
42
|
+
// this package's shared tsconfig (`moduleResolution: "Bundler"`, needed for the rest of the
|
|
43
|
+
// package) doesn't apply the classic .cts→CJS format-forcing TypeScript otherwise gives Node16/
|
|
44
|
+
// NodeNext projects — carving out a second tsconfig/project reference just for one file was more
|
|
45
|
+
// machinery than a ~150-line, dependency-free plugin warrants.
|
|
46
|
+
'use strict';
|
|
47
|
+
|
|
48
|
+
const fs = require('node:fs');
|
|
49
|
+
|
|
50
|
+
const CSS_MODULE_RE = /\.module\.css$/;
|
|
51
|
+
const IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
52
|
+
|
|
53
|
+
function isCssModuleFile(fileName) {
|
|
54
|
+
return CSS_MODULE_RE.test(fileName);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function kebabToCamel(value) {
|
|
58
|
+
return value.replace(/-([a-z0-9])/gi, (_match, char) => char.toUpperCase());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractClassNames(css) {
|
|
62
|
+
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
63
|
+
const names = new Set();
|
|
64
|
+
const classRe = /\.([a-zA-Z_][\w-]*)/g;
|
|
65
|
+
let match;
|
|
66
|
+
while ((match = classRe.exec(withoutComments))) {
|
|
67
|
+
names.add(kebabToCamel(match[1]));
|
|
68
|
+
}
|
|
69
|
+
return [...names];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function generateDts(classNames) {
|
|
73
|
+
if (classNames.length === 0) {
|
|
74
|
+
return 'declare const styles: Record<string, string>;\nexport default styles;\n';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const fields = [...classNames]
|
|
78
|
+
.sort()
|
|
79
|
+
.map((name) => {
|
|
80
|
+
const key = IDENTIFIER_RE.test(name) ? name : JSON.stringify(name);
|
|
81
|
+
return ` readonly ${key}: string;`;
|
|
82
|
+
})
|
|
83
|
+
.join('\n');
|
|
84
|
+
|
|
85
|
+
return `declare const styles: {\n${fields}\n};\nexport default styles;\n`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveRelativePath(moduleName, containingFile) {
|
|
89
|
+
const dir = containingFile.slice(0, containingFile.lastIndexOf('/'));
|
|
90
|
+
const parts = dir.split('/');
|
|
91
|
+
for (const part of moduleName.split('/')) {
|
|
92
|
+
if (part === '.') continue;
|
|
93
|
+
if (part === '..') parts.pop();
|
|
94
|
+
else parts.push(part);
|
|
95
|
+
}
|
|
96
|
+
return parts.join('/');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function init(modules) {
|
|
100
|
+
const typescript = modules.typescript;
|
|
101
|
+
|
|
102
|
+
function create(info) {
|
|
103
|
+
const host = info.languageServiceHost;
|
|
104
|
+
const dtsCache = new Map();
|
|
105
|
+
|
|
106
|
+
function getDtsForCssFile(cssPath) {
|
|
107
|
+
let mtimeMs = 0;
|
|
108
|
+
try {
|
|
109
|
+
mtimeMs = fs.statSync(cssPath).mtimeMs;
|
|
110
|
+
} catch {
|
|
111
|
+
// missing file — falls through to a fresh (empty) read below, no cache to hit anyway
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const cached = dtsCache.get(cssPath);
|
|
115
|
+
if (cached && cached.mtimeMs === mtimeMs) return cached.dts;
|
|
116
|
+
|
|
117
|
+
const content = host.readFile ? (host.readFile(cssPath) ?? '') : '';
|
|
118
|
+
const dts = generateDts(extractClassNames(content));
|
|
119
|
+
dtsCache.set(cssPath, { mtimeMs, dts });
|
|
120
|
+
return dts;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const originalGetScriptKind = host.getScriptKind ? host.getScriptKind.bind(host) : undefined;
|
|
124
|
+
const originalGetScriptSnapshot = host.getScriptSnapshot.bind(host);
|
|
125
|
+
const originalResolveModuleNameLiterals = host.resolveModuleNameLiterals;
|
|
126
|
+
|
|
127
|
+
host.getScriptKind = (fileName) =>
|
|
128
|
+
isCssModuleFile(fileName)
|
|
129
|
+
? typescript.ScriptKind.TS
|
|
130
|
+
: (originalGetScriptKind ? originalGetScriptKind(fileName) : typescript.ScriptKind.Unknown);
|
|
131
|
+
|
|
132
|
+
host.getScriptSnapshot = (fileName) =>
|
|
133
|
+
isCssModuleFile(fileName)
|
|
134
|
+
? typescript.ScriptSnapshot.fromString(getDtsForCssFile(fileName))
|
|
135
|
+
: originalGetScriptSnapshot(fileName);
|
|
136
|
+
|
|
137
|
+
if (originalResolveModuleNameLiterals) {
|
|
138
|
+
host.resolveModuleNameLiterals = (
|
|
139
|
+
literals,
|
|
140
|
+
containingFile,
|
|
141
|
+
redirectedReference,
|
|
142
|
+
options,
|
|
143
|
+
sourceFile,
|
|
144
|
+
reusedNames,
|
|
145
|
+
) => {
|
|
146
|
+
const resolved = originalResolveModuleNameLiterals.call(
|
|
147
|
+
host,
|
|
148
|
+
literals,
|
|
149
|
+
containingFile,
|
|
150
|
+
redirectedReference,
|
|
151
|
+
options,
|
|
152
|
+
sourceFile,
|
|
153
|
+
reusedNames,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return literals.map((literal, index) => {
|
|
157
|
+
const moduleName = literal.text;
|
|
158
|
+
if (isCssModuleFile(moduleName) && moduleName.startsWith('.')) {
|
|
159
|
+
const resolvedPath = resolveRelativePath(moduleName, containingFile);
|
|
160
|
+
if (host.fileExists && host.fileExists(resolvedPath)) {
|
|
161
|
+
return {
|
|
162
|
+
resolvedModule: {
|
|
163
|
+
resolvedFileName: resolvedPath,
|
|
164
|
+
extension: typescript.Extension.Dts,
|
|
165
|
+
isExternalLibraryImport: false,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return resolved[index];
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return info.languageService;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { create };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = init;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Ambient type for the hand-written CommonJS plugin (typescript-plugin.cjs) — deliberately loose
|
|
2
|
+
// (the tsserver plugin-init shape isn't worth pulling `typescript/lib/tsserverlibrary`'s types
|
|
3
|
+
// into every consumer just to describe a function no one calls directly; tsserver itself invokes
|
|
4
|
+
// it by convention, matched by shape, not by this declaration).
|
|
5
|
+
declare function initTypeScriptPlugin(modules: { typescript: unknown }): {
|
|
6
|
+
create: (info: unknown) => unknown;
|
|
7
|
+
};
|
|
8
|
+
export = initTypeScriptPlugin;
|