@symbiote-native/engine 0.1.7 → 0.3.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 +17 -10
- package/build/action-sheet-ios/index.js +4 -1
- package/build/alert/index.ios.js +10 -0
- package/build/animated/animations/composition.js +3 -1
- package/build/animated/animations/spring-config.js +4 -1
- package/build/animated/animations/spring.js +23 -9
- package/build/animated/animations/timing.js +3 -2
- package/build/animated/bezier.js +2 -1
- package/build/animated/color.d.ts +2 -8
- package/build/animated/color.js +8 -47
- package/build/animated/easing.js +0 -8
- package/build/animated/event.js +7 -3
- package/build/animated/graph.d.ts +13 -5
- package/build/animated/graph.js +92 -13
- package/build/animated/index.d.ts +3 -3
- package/build/animated/index.js +3 -3
- package/build/animated/interpolation.js +8 -3
- package/build/animated/leaf-lifecycle.d.ts +7 -0
- package/build/animated/leaf-lifecycle.js +185 -0
- package/build/animated/mock.js +1 -1
- package/build/animated/native/native-animated.js +2 -1
- package/build/animated/operators.js +27 -6
- package/build/animated/props.js +1 -1
- package/build/animated/rgba.d.ts +8 -0
- package/build/animated/rgba.js +70 -0
- package/build/animated/style.js +13 -3
- package/build/animated/value-xy.d.ts +8 -5
- package/build/animated/value-xy.js +4 -1
- package/build/animated/value.js +12 -7
- package/build/appearance/index.js +4 -1
- package/build/commit.d.ts +13 -0
- package/build/commit.js +146 -14
- package/build/debug.d.ts +1 -1
- package/build/debug.js +10 -3
- package/build/events/index.js +19 -10
- package/build/fabric-props.js +8 -2
- package/build/image-loader.js +3 -2
- package/build/index.d.ts +23 -19
- package/build/index.js +19 -11
- package/build/interaction-manager/index.js +2 -1
- package/build/layout-animation/index.js +12 -5
- package/build/native-events.js +2 -1
- package/build/native-modules/index.js +3 -1
- package/build/node.d.ts +25 -0
- package/build/node.js +168 -2
- package/build/pan-responder/index.js +20 -9
- package/build/permissions-android/index.js +3 -1
- package/build/platform/index.android.d.ts +1 -1
- package/build/platform/index.android.js +1 -1
- package/build/platform/index.ios.d.ts +1 -1
- package/build/platform-color/index.js +3 -1
- package/build/post-commit.d.ts +1 -0
- package/build/post-commit.js +6 -0
- package/build/process-background-image/index.js +30 -9
- package/build/process-filter.js +11 -4
- package/build/process-font-variant.js +0 -1
- package/build/process-transform/index.js +13 -19
- package/build/registry.js +10 -2
- package/build/report-error.d.ts +5 -0
- package/build/report-error.js +63 -0
- package/build/share/index.android.js +1 -1
- package/build/share/index.ios.js +1 -1
- package/build/status-bar/index.android.js +8 -10
- package/build/status-bar/index.ios.js +1 -1
- package/build/style-registry/index.d.ts +9 -2
- package/build/style-registry/index.js +149 -95
- package/build/style-registry/scope.d.ts +15 -1
- package/build/style-registry/scope.js +29 -27
- package/build/style-sheet/index.js +4 -2
- package/build/styles.d.ts +11 -1
- package/build/surface.d.ts +1 -1
- package/build/surface.js +7 -1
- package/build/tags.d.ts +1 -0
- package/build/tags.js +31 -1
- package/build/touch-history.js +9 -2
- package/build/vibration/index.android.js +2 -3
- package/build/vibration/index.ios.js +1 -1
- package/build/view-config.js +14 -2
- package/package.json +2 -2
- package/build/animated/interpolation-node.d.ts +0 -15
- package/build/animated/interpolation-node.js +0 -58
- package/build/style-registry/scope/index.d.ts +0 -3
- package/build/style-registry/scope/index.js +0 -41
- package/build/touch-history/index.d.ts +0 -23
- package/build/touch-history/index.js +0 -150
- package/build/type-guards/index.d.ts +0 -4
- package/build/type-guards/index.js +0 -15
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
// JS-side port of RN's processTransform (Libraries/StyleSheet/processTransform.js).
|
|
2
|
-
// Same root cause as transformOrigin/boxShadow:
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// Android native cast to ReadableArray and crashed with
|
|
8
|
-
// `java.lang.String cannot be cast to com.facebook.react.bridge.ReadableArray`. This
|
|
9
|
-
// restores the missing JS parse so native always receives the entry array.
|
|
2
|
+
// Same root cause as transformOrigin/boxShadow: RN only parses `transform` in JS when
|
|
3
|
+
// nativeCSSParsing is off (the default), and only for a STRING input - an ARRAY is
|
|
4
|
+
// returned unchanged. symbiote forwarded a raw string once, which Android native cast
|
|
5
|
+
// to ReadableArray and crashed (`String cannot be cast to ReadableArray`). This
|
|
6
|
+
// restores the missing JS parse.
|
|
10
7
|
//
|
|
11
|
-
// CRITICAL: the animated / sticky-header hot path produces transform ARRAYS
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// array unchanged (matching the boxShadow/transformOrigin idiom).
|
|
8
|
+
// CRITICAL: the animated / sticky-header hot path produces transform ARRAYS on every
|
|
9
|
+
// commit flush, so the array branch MUST return the byte-identical reference (no
|
|
10
|
+
// decompose, no clone). RN's only array-path check, _validateTransforms, is __DEV__-only
|
|
11
|
+
// and throws via invariant; we never throw in commit, so we dlog instead and still
|
|
12
|
+
// return the array unchanged.
|
|
17
13
|
import { dlog } from '../debug.js';
|
|
18
14
|
// RN processTransform.js:28: matches each `name(args)` in the CSS string.
|
|
19
15
|
const TRANSFORM_REGEX = /(\w+)\(([^)]+)\)/g;
|
|
@@ -78,16 +74,14 @@ function getKeyAndValueFromCSSTransform(key, args) {
|
|
|
78
74
|
return { key, value: isNaN(Number(args)) ? args : Number(args) };
|
|
79
75
|
}
|
|
80
76
|
}
|
|
81
|
-
// RN processTransform.js:24-50.
|
|
82
|
-
// is returned
|
|
83
|
-
// throws; we never throw, so we skip it and run a non-throwing dlog sanity check).
|
|
77
|
+
// RN processTransform.js:24-50. String input is parsed into the entry array; array
|
|
78
|
+
// input is returned unchanged (see file header for why).
|
|
84
79
|
export function processTransform(transform) {
|
|
85
80
|
if (transform == null) {
|
|
86
81
|
return [];
|
|
87
82
|
}
|
|
88
83
|
if (typeof transform !== 'string') {
|
|
89
|
-
// Hot path:
|
|
90
|
-
// same reference (no decompose, no clone) so the flush is a no-op.
|
|
84
|
+
// Hot path: return the same reference, no clone (see file header).
|
|
91
85
|
warnInvalidTransforms(transform);
|
|
92
86
|
return transform;
|
|
93
87
|
}
|
package/build/registry.js
CHANGED
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
// NOT in that set derives. The list never grows with the community; it grows only
|
|
20
20
|
// when we add a core primitive of our own.
|
|
21
21
|
import { isRecord } from './type-guards.js';
|
|
22
|
-
const EMPTY = {
|
|
22
|
+
const EMPTY = {
|
|
23
|
+
listeners: new Set(),
|
|
24
|
+
byRaw: new Map(),
|
|
25
|
+
processors: new Map(),
|
|
26
|
+
};
|
|
23
27
|
// OUR own primitives: the finite set shared hand-tunes (view-config events,
|
|
24
28
|
// commit COLOR_PROPS). The source is never consulted for these, so they can't
|
|
25
29
|
// drift. Everything else derives. This list grows only when WE add a core
|
|
@@ -81,7 +85,11 @@ function deriveFromConfig(config, into) {
|
|
|
81
85
|
for (const raw in directEventTypes) {
|
|
82
86
|
const registrationName = directEventTypes[raw]?.registrationName;
|
|
83
87
|
if (typeof registrationName === 'string') {
|
|
84
|
-
addEvent(into, {
|
|
88
|
+
addEvent(into, {
|
|
89
|
+
raw,
|
|
90
|
+
listener: splitListener(registrationName),
|
|
91
|
+
direct: true,
|
|
92
|
+
});
|
|
85
93
|
}
|
|
86
94
|
}
|
|
87
95
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Hand an error to the host the way React Native itself does, for the seams where an adapter
|
|
2
|
+
// catches something its framework would otherwise have surfaced on its own.
|
|
3
|
+
//
|
|
4
|
+
// RN routes an uncaught render error through ReactFiberErrorDialog -> ExceptionsManager
|
|
5
|
+
// (Libraries/Core/ReactFiberErrorDialog.js), which is reachable only by a deep path into RN's
|
|
6
|
+
// Libraries. `global.ErrorUtils` is the same surface through the documented global that
|
|
7
|
+
// @react-native/js-polyfills installs before any module evaluates - RN's own
|
|
8
|
+
// Libraries/vendor/core/ErrorUtils.js is nothing but `export default global.ErrorUtils`. Off a
|
|
9
|
+
// native host (vitest, node smokes) the global is absent and console.error is the whole channel.
|
|
10
|
+
//
|
|
11
|
+
// This is NOT the dlog channel. dlog is DEBUG-gated and therefore the developer's; an error that
|
|
12
|
+
// blanked the screen has to reach the app whether or not anyone turned diagnostics on.
|
|
13
|
+
// Read off globalThis through Reflect rather than named as an identifier, and deliberately NOT
|
|
14
|
+
// declared in an ambient block: react-native ships its own `const ErrorUtils: ErrorUtils` in
|
|
15
|
+
// src/types/globals.d.ts, so a `declare global { var ErrorUtils }` here redeclares it (TS2451) for
|
|
16
|
+
// every package whose tsconfig pulls in RN's types - the engine's own does not, so it typechecks
|
|
17
|
+
// clean here and breaks ngc downstream. RN's declared shape also lists only the two
|
|
18
|
+
// get/setGlobalHandler members, not reportError, which the polyfill does install.
|
|
19
|
+
function nativeReporter() {
|
|
20
|
+
const utils = Reflect.get(globalThis, 'ErrorUtils');
|
|
21
|
+
if (typeof utils !== 'object' || utils === null)
|
|
22
|
+
return null;
|
|
23
|
+
const report = Reflect.get(utils, 'reportError');
|
|
24
|
+
if (typeof report !== 'function')
|
|
25
|
+
return null;
|
|
26
|
+
return error => {
|
|
27
|
+
Reflect.apply(report, utils, [error]);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// A throw is not required to be an Error - a string, or null, reaches here just as easily, and
|
|
31
|
+
// the host's reporter wants something with a message and a stack. RN wraps the same way, via
|
|
32
|
+
// SyntheticError.
|
|
33
|
+
function toError(value, origin) {
|
|
34
|
+
if (value instanceof Error)
|
|
35
|
+
return value;
|
|
36
|
+
return new Error(`${origin}: ${String(value)}`);
|
|
37
|
+
}
|
|
38
|
+
export function reportUncaughtError(value, info) {
|
|
39
|
+
const error = toError(value, info.origin);
|
|
40
|
+
if (info.componentStack) {
|
|
41
|
+
// LogBox reads these two off the error to render the component frames; RN's own
|
|
42
|
+
// ReactFiberErrorDialog sets exactly the same pair, in a try/catch because a frozen or
|
|
43
|
+
// sealed error still deserves to be reported, just without the frames.
|
|
44
|
+
try {
|
|
45
|
+
Object.assign(error, {
|
|
46
|
+
componentStack: info.componentStack,
|
|
47
|
+
isComponentError: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Reported below regardless.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const report = nativeReporter();
|
|
55
|
+
if (report !== null) {
|
|
56
|
+
// Exactly one channel, never both: RN routes console.error into LogBox as well, so logging
|
|
57
|
+
// here too would double-report every error. Upstream returns false from showErrorDialog for
|
|
58
|
+
// this same reason (Libraries/Core/ReactFiberErrorDialog.js:57).
|
|
59
|
+
report(error);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.error(`[symbiote] ${info.origin}:`, error);
|
|
63
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// name, so it is only proven on a real Android host (a bridgeless resolution log there).
|
|
10
10
|
import { dlog } from '../debug.js';
|
|
11
11
|
import { getNativeModule } from '../native-modules/index.js';
|
|
12
|
-
import { validateContent, shareActions, SHARED_ACTION, DISMISSED_ACTION } from './shared.js';
|
|
12
|
+
import { validateContent, shareActions, SHARED_ACTION, DISMISSED_ACTION, } from './shared.js';
|
|
13
13
|
const SHARE_MODULE = 'ShareModule';
|
|
14
14
|
// ShareModule.share resolves an untyped value at the native boundary; narrow it before
|
|
15
15
|
// reading `action` (no `as`).
|
package/build/share/index.ios.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { dlog } from '../debug.js';
|
|
8
8
|
import { getNativeModule } from '../native-modules/index.js';
|
|
9
9
|
import { ACTION_SHEET_MANAGER, } from '../action-sheet-ios/index.js';
|
|
10
|
-
import { validateContent, shareActions, SHARED_ACTION, DISMISSED_ACTION } from './shared.js';
|
|
10
|
+
import { validateContent, shareActions, SHARED_ACTION, DISMISSED_ACTION, } from './shared.js';
|
|
11
11
|
export const Share = {
|
|
12
12
|
...shareActions,
|
|
13
13
|
// Open the iOS share sheet for `content`. Resolves with the user's action
|
|
@@ -4,16 +4,15 @@
|
|
|
4
4
|
// setColor/setTranslucent, same module name ('StatusBarManager'). Metro picks this on an
|
|
5
5
|
// Android host; iOS keeps its own shape. Each adapter wraps these with its declarative component.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// light/dark text). See render.ts's installStopSurfaceGlobal.
|
|
7
|
+
// Driving window flags from our bridgeless surface used to blank the app: a status-bar
|
|
8
|
+
// relayout triggered stopSurface, which threw "Global was not installed" because RN's
|
|
9
|
+
// own renderer installs global.RN$stopSurface and we replace that renderer. Fixed by
|
|
10
|
+
// render.ts installing RN$stopSurface itself (see installStopSurfaceGlobal) - verified
|
|
11
|
+
// on device: show/hide + light/dark text survive the relayout without blanking.
|
|
13
12
|
import { getNativeModule } from '../native-modules/index.js';
|
|
14
13
|
import { dlog } from '../debug.js';
|
|
15
14
|
import { processColor } from '../platform-color/index.js';
|
|
16
|
-
import { STATUS_BAR_MANAGER } from './shared.js';
|
|
15
|
+
import { STATUS_BAR_MANAGER, } from './shared.js';
|
|
17
16
|
// processColor returns `unknown` (its result is platform-dependent); narrow to the
|
|
18
17
|
// number Fabric/native expects, like RN's invariant before setColor. A non-number
|
|
19
18
|
// (null for an unparseable color, headless identity passthrough of a string) is dropped.
|
|
@@ -32,7 +31,7 @@ function applyBackgroundColor(manager, color, animated) {
|
|
|
32
31
|
// single-arg Android setters. The adapter calls this from its declarative component's effect,
|
|
33
32
|
// on mount and on every prop change.
|
|
34
33
|
export function applyStatusBarProps(props) {
|
|
35
|
-
const { barStyle, hidden, animated = false, backgroundColor, translucent } = props;
|
|
34
|
+
const { barStyle, hidden, animated = false, backgroundColor, translucent, } = props;
|
|
36
35
|
const manager = getNativeModule(STATUS_BAR_MANAGER);
|
|
37
36
|
if (manager === null) {
|
|
38
37
|
dlog('StatusBar android: StatusBarManager not resolvable — skipping');
|
|
@@ -78,6 +77,5 @@ export const statusBarImperative = {
|
|
|
78
77
|
// constant is absent (older RN, or a fake that doesn't define getConstants). Read lazily
|
|
79
78
|
// so nothing touches native at import time.
|
|
80
79
|
export function statusBarCurrentHeight() {
|
|
81
|
-
return getNativeModule(STATUS_BAR_MANAGER)?.getConstants?.()
|
|
82
|
-
.HEIGHT;
|
|
80
|
+
return getNativeModule(STATUS_BAR_MANAGER)?.getConstants?.().HEIGHT;
|
|
83
81
|
}
|
|
@@ -21,7 +21,7 @@ import { STATUS_BAR_MANAGER, STATIC_HIDE_TRANSITION, hideTransition, } from './s
|
|
|
21
21
|
// (deepest/last wins); we direct-apply a single component's props, which is correct for one
|
|
22
22
|
// StatusBar and a fine first cut.
|
|
23
23
|
export function applyStatusBarProps(props) {
|
|
24
|
-
const { barStyle, hidden, animated = false, networkActivityIndicatorVisible } = props;
|
|
24
|
+
const { barStyle, hidden, animated = false, networkActivityIndicatorVisible, } = props;
|
|
25
25
|
const manager = getNativeModule(STATUS_BAR_MANAGER);
|
|
26
26
|
if (manager === null) {
|
|
27
27
|
dlog('StatusBar: StatusBarManager not resolvable via __turboModuleProxy — skipping');
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { IViewStyle, ITextStyle } from '../styles';
|
|
2
2
|
type IResolvedStyle = Partial<IViewStyle & ITextStyle>;
|
|
3
3
|
export type IClassNameValue = string | IResolvedStyle | Array<string | IResolvedStyle> | undefined | null;
|
|
4
|
-
export
|
|
4
|
+
export interface IStyleRule {
|
|
5
|
+
/** Class names AS AUTHORED / as renamed by the scoping pass. No normalization. */
|
|
6
|
+
readonly tokens: readonly string[];
|
|
7
|
+
readonly specificity: readonly [number, number, number];
|
|
8
|
+
/** Source order within its file; the registry adds a per-registration epoch on top. */
|
|
9
|
+
readonly order: number;
|
|
10
|
+
readonly style: IResolvedStyle;
|
|
11
|
+
}
|
|
12
|
+
export declare function registerRules(rules: readonly IStyleRule[]): void;
|
|
5
13
|
export declare function clearGlobalStyles(): void;
|
|
6
14
|
export declare function isClassNameValue(value: unknown): value is IClassNameValue;
|
|
7
15
|
export declare function resolveClassName(className: IClassNameValue): IResolvedStyle;
|
|
8
|
-
export declare function kebabToCamel(value: string): string;
|
|
9
16
|
export {};
|
|
@@ -1,39 +1,101 @@
|
|
|
1
1
|
// Runtime style registry. Side-effect CSS imports (compiled by the sibling CSS-to-style
|
|
2
|
-
// build package, not this module) call
|
|
3
|
-
// look them up via resolveClassName(). No CSS parsing here - just
|
|
2
|
+
// build package, not this module) call registerRules() with the token SET each selector
|
|
3
|
+
// names; components look them up via resolveClassName(). No CSS parsing here - just an
|
|
4
|
+
// inverted Map<token, rules> and a subset test.
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
6
|
+
// A rule matches when its tokens are a SUBSET of the element's class list, in any order and
|
|
7
|
+
// at any length, and the matches cascade by (derived-scope, specificity, epoch, order).
|
|
8
|
+
// That replaced an earlier flat Map<key, style>, where the compiler COLLAPSED a selector's
|
|
9
|
+
// tokens into one camelCase name (`.a.b` -> `aB`) and the runtime guessed the split back
|
|
10
|
+
// apart by permuting the class string: 60 built keys on a 4-token miss, a camelCase
|
|
11
|
+
// round-trip that could not survive a scope suffix, no real cascade, and a silent cliff at
|
|
12
|
+
// 5 tokens where the compound branch stopped running at all. Tokens now arrive AS AUTHORED,
|
|
13
|
+
// so there is nothing left to guess.
|
|
14
|
+
// The scope tail as OUR OWN compilers emit it — all three shapes, the hash being css-parser's
|
|
15
|
+
// base36 hashFilePath:
|
|
16
|
+
// `card__data-v-<h>` Vue `<style scoped>`
|
|
17
|
+
// `card__svelte-<h>` Svelte `<style>`
|
|
18
|
+
// `card__module__<h>` CSS Modules — BOTH a standalone `.module.css` and Vue's `<style module>`
|
|
19
|
+
// (A preprocessor source — SCSS/Less/Stylus — reduces to plain CSS before any of this, so it
|
|
20
|
+
// carries whichever of the three its host block uses; there is no fourth shape.)
|
|
8
21
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
// Matched by SHAPE, never by "there is a `__` somewhere in the name" — a BEM class
|
|
23
|
+
// (`card__title`) must never be read as a scoped `card`, which would silently merge the block's
|
|
24
|
+
// styles into the element's.
|
|
25
|
+
//
|
|
26
|
+
// Matched as ONE TAIL UNIT anchored at the end, rather than as "split on the last `__`, then
|
|
27
|
+
// shape-test what follows": the module form carries its own `__`, so a `lastIndexOf` split cuts
|
|
28
|
+
// `badge__module__<h>` into base `badge__module` + scope `<h>` and the base it hands back names
|
|
29
|
+
// no class. Widening the shape test alone would NOT fix that — the split POINT is wrong, not the
|
|
30
|
+
// alphabet.
|
|
31
|
+
//
|
|
32
|
+
// Note this suffix scheme is ours, not the frameworks'. Svelte appends a SEPARATE token
|
|
33
|
+
// (`class="card svelte-<h>"`) and Vue `<style scoped>` renames nothing at all (it adds a
|
|
34
|
+
// `data-v-<h>` attribute). We have no DOM and no attribute matching, so a scope is expressed by
|
|
35
|
+
// renaming the token; the names only borrow their vocabulary.
|
|
36
|
+
const SCOPE_TAIL_PATTERN = /__((?:data-v|svelte)-[0-9a-z]+|module__[0-9a-z]+)$/;
|
|
37
|
+
function splitScopedToken(token) {
|
|
38
|
+
const match = SCOPE_TAIL_PATTERN.exec(token);
|
|
39
|
+
// index 0 would mean the token is nothing BUT a scope tail, which names no class.
|
|
40
|
+
if (match === null || match.index <= 0)
|
|
41
|
+
return null;
|
|
42
|
+
return { base: token.slice(0, match.index), scope: match[1] };
|
|
43
|
+
}
|
|
44
|
+
// Inverted index, ONE bucket per rule rather than one per token: a rule can only match when the
|
|
45
|
+
// element carries every one of its tokens, so any single token of it is a sufficient hook. The
|
|
46
|
+
// alternative (indexing under all of them) surfaces the same rule once per shared token and buys
|
|
47
|
+
// nothing but a dedupe pass.
|
|
48
|
+
const ruleIndex = new Map();
|
|
49
|
+
let ruleEpoch = 0;
|
|
50
|
+
// resolveClassName runs from routeProp on every class-prop WRITE, per node, and a screen only
|
|
51
|
+
// ever uses a few dozen distinct class strings — so after warm-up the whole resolution collapses
|
|
52
|
+
// to one Map.get. Only the STRING branch is memoized: an object/array argument has no stable key
|
|
53
|
+
// (a fresh literal every render) and would just fill the map.
|
|
54
|
+
//
|
|
55
|
+
// Safe to hand the SAME object out repeatedly because no caller mutates it — every consumer
|
|
56
|
+
// (routeProp's commitClassStyle, and the ScrollView / VirtualizedList / FlatList / ImageBackground
|
|
57
|
+
// class-to-style props in all five adapters) spreads it or drops it into a style array, and
|
|
58
|
+
// flattenStyle shallow-copies.
|
|
59
|
+
const resolvedCache = new Map();
|
|
60
|
+
// Bounded so a screen generating unique class strings at runtime cannot grow it without limit.
|
|
61
|
+
// Overflow drops everything rather than evicting one entry: the cache is a warm-up optimization,
|
|
62
|
+
// not a working set, and an LRU's bookkeeping costs more than the rebuild it saves.
|
|
63
|
+
const RESOLVED_CACHE_LIMIT = 512;
|
|
64
|
+
// Every registration is a cascade change, so nothing resolved before it can be trusted. Clearing
|
|
65
|
+
// wholesale beats versioning each entry — registration happens at import time, resolution happens
|
|
66
|
+
// per commit, and only the second one is hot.
|
|
67
|
+
function invalidateResolved() {
|
|
68
|
+
resolvedCache.clear();
|
|
69
|
+
}
|
|
70
|
+
export function registerRules(rules) {
|
|
71
|
+
invalidateResolved();
|
|
72
|
+
const epoch = ruleEpoch++;
|
|
73
|
+
for (const rule of rules) {
|
|
74
|
+
const hook = rule.tokens[0];
|
|
75
|
+
// The empty set is a subset of every element's tokens, so a token-less rule would paint
|
|
76
|
+
// everything. It names no class and can only come from a broken compile.
|
|
77
|
+
if (hook === undefined)
|
|
78
|
+
continue;
|
|
79
|
+
const bucket = ruleIndex.get(hook);
|
|
80
|
+
if (bucket === undefined) {
|
|
81
|
+
ruleIndex.set(hook, [{ rule, epoch }]);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
bucket.push({ rule, epoch });
|
|
25
85
|
}
|
|
26
86
|
}
|
|
27
87
|
// Used between tests to reset registry state.
|
|
28
88
|
export function clearGlobalStyles() {
|
|
29
|
-
|
|
89
|
+
ruleIndex.clear();
|
|
90
|
+
ruleEpoch = 0;
|
|
91
|
+
invalidateResolved();
|
|
30
92
|
}
|
|
31
93
|
// A `class`/`className` prop arrives as `unknown` at the routeProp boundary (any adapter can
|
|
32
94
|
// hand over anything); this narrows before resolveClassName without an `as` cast. Shared with
|
|
33
95
|
// adapters/vue/src/components/scroll-view/shared.ts's identical need, rather than each keeping
|
|
34
96
|
// its own copy - that file imports this one instead of redeclaring it.
|
|
35
97
|
export function isClassNameValue(value) {
|
|
36
|
-
return typeof value === 'string' || (typeof value === 'object' && value !== null);
|
|
98
|
+
return (typeof value === 'string' || (typeof value === 'object' && value !== null));
|
|
37
99
|
}
|
|
38
100
|
export function resolveClassName(className) {
|
|
39
101
|
if (!className)
|
|
@@ -46,83 +108,75 @@ export function resolveClassName(className) {
|
|
|
46
108
|
return { ...acc, ...resolveClassName(item) };
|
|
47
109
|
}, {});
|
|
48
110
|
}
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
return
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const compound = tryCompoundLookup(parts);
|
|
58
|
-
if (compound)
|
|
59
|
-
return compound;
|
|
60
|
-
}
|
|
61
|
-
return parts.reduce((acc, cls) => {
|
|
62
|
-
return { ...acc, ...resolveOne(cls) };
|
|
63
|
-
}, {});
|
|
111
|
+
const cached = resolvedCache.get(className);
|
|
112
|
+
if (cached !== undefined)
|
|
113
|
+
return cached;
|
|
114
|
+
const resolved = resolveClassString(className);
|
|
115
|
+
if (resolvedCache.size >= RESOLVED_CACHE_LIMIT)
|
|
116
|
+
resolvedCache.clear();
|
|
117
|
+
resolvedCache.set(className, resolved);
|
|
118
|
+
return resolved;
|
|
64
119
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
return [];
|
|
68
|
-
const compounds = [];
|
|
69
|
-
for (let size = COMPOUND_MIN_PARTS; size <= parts.length; size++) {
|
|
70
|
-
compounds.push(...generateKPermutations(parts, size));
|
|
71
|
-
}
|
|
72
|
-
return compounds;
|
|
73
|
-
}
|
|
74
|
-
function generateKPermutations(parts, size) {
|
|
75
|
-
if (size === 0)
|
|
76
|
-
return [''];
|
|
120
|
+
function resolveClassString(className) {
|
|
121
|
+
const parts = className.trim().split(/\s+/).filter(Boolean);
|
|
77
122
|
if (parts.length === 0)
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
123
|
+
return {};
|
|
124
|
+
return matchRules(parts) ?? {};
|
|
125
|
+
}
|
|
126
|
+
// `null` rather than `{}` for "nothing matched", so the caller can skip the empty-object churn on
|
|
127
|
+
// every class-prop set that has no rules.
|
|
128
|
+
function matchRules(parts) {
|
|
129
|
+
const literal = new Set(parts);
|
|
130
|
+
const tokens = elementTokens(parts);
|
|
131
|
+
let matched = null;
|
|
132
|
+
for (const token of tokens) {
|
|
133
|
+
const bucket = ruleIndex.get(token);
|
|
134
|
+
if (bucket === undefined)
|
|
135
|
+
continue;
|
|
136
|
+
for (const indexed of bucket) {
|
|
137
|
+
if (!indexed.rule.tokens.every(needed => tokens.has(needed)))
|
|
138
|
+
continue;
|
|
139
|
+
matched ??= [];
|
|
140
|
+
matched.push({
|
|
141
|
+
indexed,
|
|
142
|
+
derivedTokens: indexed.rule.tokens.filter(token => !literal.has(token))
|
|
143
|
+
.length,
|
|
144
|
+
});
|
|
87
145
|
}
|
|
88
146
|
}
|
|
89
|
-
|
|
90
|
-
return result;
|
|
91
|
-
}
|
|
92
|
-
// Compound permutations join as camelCase ("btn primary" -> "btnPrimary") because this
|
|
93
|
-
// repo's CSS-to-style compiler emits plain camelCase keys for every class, single or
|
|
94
|
-
// compound, so "btn primary" must resolve against a registered "btnPrimary".
|
|
95
|
-
function toCompoundKey(parts) {
|
|
96
|
-
return parts.reduce((key, part, index) => (index === 0 ? part : key + capitalize(part)), '');
|
|
97
|
-
}
|
|
98
|
-
function capitalize(value) {
|
|
99
|
-
return value.length === 0 ? value : value[0].toUpperCase() + value.slice(1);
|
|
100
|
-
}
|
|
101
|
-
// Duplicated from @symbiote-native/css-parser's identical helper rather than imported: css-parser
|
|
102
|
-
// pulls in postcss and is build-time only (never shipped in the app bundle), and this registry
|
|
103
|
-
// is the opposite - pure runtime, in every app bundle - so importing it here would leak a
|
|
104
|
-
// build-time dependency into the shipped app. The conversion itself is two lines; keeping both
|
|
105
|
-
// copies in sync is a smaller cost than the alternative.
|
|
106
|
-
//
|
|
107
|
-
// Exported (not just local to this file) because ./scope.ts's Vue `<style scoped>` name
|
|
108
|
-
// rewriter needs the same kebab->camel normalization and is a different responsibility living
|
|
109
|
-
// in a sibling module - see that file's own doc comment for why it's split out of this one.
|
|
110
|
-
export function kebabToCamel(value) {
|
|
111
|
-
return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
112
|
-
}
|
|
113
|
-
function tryCompoundLookup(parts) {
|
|
114
|
-
if (parts.length < COMPOUND_MIN_PARTS)
|
|
147
|
+
if (matched === null)
|
|
115
148
|
return null;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
149
|
+
matched.sort(byCascadeOrder);
|
|
150
|
+
return matched.reduce((acc, match) => ({ ...acc, ...match.indexed.rule.style }), {});
|
|
151
|
+
}
|
|
152
|
+
// A scoped token contributes its BASE as well: the scope is expressed by RENAMING the token
|
|
153
|
+
// rather than adding a second one, so a global `.card` in App.css only survives a component
|
|
154
|
+
// defining its own `.card` if the base is put back into the set here. On the web the element
|
|
155
|
+
// would carry both names and no such step would exist.
|
|
156
|
+
function elementTokens(parts) {
|
|
157
|
+
const tokens = new Set();
|
|
158
|
+
for (const part of parts) {
|
|
159
|
+
tokens.add(part);
|
|
160
|
+
const split = splitScopedToken(part);
|
|
161
|
+
if (split !== null)
|
|
162
|
+
tokens.add(split.base);
|
|
120
163
|
}
|
|
121
|
-
return
|
|
164
|
+
return tokens;
|
|
122
165
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
166
|
+
// Ascending, because the caller merges left to right: the winner has to be spread LAST. Ties
|
|
167
|
+
// break the way the cascade does — later import first, then later line in the file.
|
|
168
|
+
function byCascadeOrder(left, right) {
|
|
169
|
+
if (left.derivedTokens !== right.derivedTokens) {
|
|
170
|
+
return right.derivedTokens - left.derivedTokens;
|
|
171
|
+
}
|
|
172
|
+
const leftRule = left.indexed.rule;
|
|
173
|
+
const rightRule = right.indexed.rule;
|
|
174
|
+
for (let index = 0; index < leftRule.specificity.length; index++) {
|
|
175
|
+
const diff = leftRule.specificity[index] - rightRule.specificity[index];
|
|
176
|
+
if (diff !== 0)
|
|
177
|
+
return diff;
|
|
178
|
+
}
|
|
179
|
+
return left.indexed.epoch === right.indexed.epoch
|
|
180
|
+
? leftRule.order - rightRule.order
|
|
181
|
+
: left.indexed.epoch - right.indexed.epoch;
|
|
128
182
|
}
|
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
export type IClassToggleMap = Record<string, boolean | undefined>;
|
|
2
2
|
export type IScopableClassValue = string | IClassToggleMap | Array<string | IClassToggleMap> | undefined | null;
|
|
3
|
-
export
|
|
3
|
+
export type IClassNameRenames = Readonly<Record<string, string>>;
|
|
4
|
+
/**
|
|
5
|
+
* Rewrites every class token the style compiler renamed, leaving the rest alone.
|
|
6
|
+
*
|
|
7
|
+
* The names come from the compiler's own rename table, emitted verbatim, rather than being
|
|
8
|
+
* RECOMPUTED here as `token + '__' + scopeId`. Recomputing made the runtime a second,
|
|
9
|
+
* independent implementation of "what is this class called now" beside the style compiler —
|
|
10
|
+
* they agreed by construction until a rename mechanism changed under one of them. lightningcss
|
|
11
|
+
* renames the class; this reads the result.
|
|
12
|
+
*
|
|
13
|
+
* A token absent from the table is left alone: it belongs to another file (a `:global()` escape
|
|
14
|
+
* hatch, App.css, a class handed down by a parent), and the registry resolves it under its own
|
|
15
|
+
* authored name.
|
|
16
|
+
*/
|
|
17
|
+
export declare function renameClassTokens(value: IScopableClassValue, renames: IClassNameRenames): IScopableClassValue;
|
|
@@ -1,41 +1,43 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// no
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
// Class-name rewriter for scoped styles. Distinct responsibility from the sibling ./index.ts
|
|
2
|
+
// (the CSS class -> style registry): this module does pure NAME rewriting, no registry lookup,
|
|
3
|
+
// no CSS parsing. It runs at the compiled call site of a scoped-style template -
|
|
4
|
+
// `adapters/vue/metro-vue-transformer.cjs` emits calls to renameClassTokens (imported there as
|
|
5
|
+
// `__scopeClass`) - BEFORE Vue's own normalizeClass() collapses string/object/array `class`
|
|
6
|
+
// values to a final string, so it must pre-process all three shapes normalizeClass understands.
|
|
7
|
+
// resolveClassName in ./index.ts still does the actual style lookup, unchanged, against the
|
|
8
|
+
// rewritten name.
|
|
9
|
+
/**
|
|
10
|
+
* Rewrites every class token the style compiler renamed, leaving the rest alone.
|
|
11
|
+
*
|
|
12
|
+
* The names come from the compiler's own rename table, emitted verbatim, rather than being
|
|
13
|
+
* RECOMPUTED here as `token + '__' + scopeId`. Recomputing made the runtime a second,
|
|
14
|
+
* independent implementation of "what is this class called now" beside the style compiler —
|
|
15
|
+
* they agreed by construction until a rename mechanism changed under one of them. lightningcss
|
|
16
|
+
* renames the class; this reads the result.
|
|
17
|
+
*
|
|
18
|
+
* A token absent from the table is left alone: it belongs to another file (a `:global()` escape
|
|
19
|
+
* hatch, App.css, a class handed down by a parent), and the registry resolves it under its own
|
|
20
|
+
* authored name.
|
|
21
|
+
*/
|
|
22
|
+
export function renameClassTokens(value, renames) {
|
|
13
23
|
if (value === undefined || value === null)
|
|
14
24
|
return value;
|
|
15
25
|
if (Array.isArray(value)) {
|
|
16
|
-
return value.map(item =>
|
|
26
|
+
return value.map(item => renameClassEntry(item, renames));
|
|
17
27
|
}
|
|
18
|
-
return
|
|
28
|
+
return renameClassEntry(value, renames);
|
|
19
29
|
}
|
|
20
|
-
|
|
21
|
-
// authoring form (`section-label`) - normalize to camelCase FIRST, then decide scoping, so
|
|
22
|
-
// `localNames` (always camelCase, built from the css-parser's registered keys) recognizes a
|
|
23
|
-
// kebab-written token. The emitted (possibly suffixed) name is always the camelCase form.
|
|
24
|
-
function scopeToken(token, localNames, scopeId) {
|
|
25
|
-
const camelToken = kebabToCamel(token);
|
|
26
|
-
return localNames.has(camelToken) ? `${camelToken}__${scopeId}` : camelToken;
|
|
27
|
-
}
|
|
28
|
-
function scopeClassEntry(value, localNames, scopeId) {
|
|
30
|
+
function renameClassEntry(value, renames) {
|
|
29
31
|
if (typeof value === 'object') {
|
|
30
|
-
const
|
|
32
|
+
const renamed = {};
|
|
31
33
|
for (const [name, enabled] of Object.entries(value)) {
|
|
32
|
-
|
|
34
|
+
renamed[renames[name] ?? name] = enabled;
|
|
33
35
|
}
|
|
34
|
-
return
|
|
36
|
+
return renamed;
|
|
35
37
|
}
|
|
36
38
|
return value
|
|
37
39
|
.split(/\s+/)
|
|
38
40
|
.filter(Boolean)
|
|
39
|
-
.map(token =>
|
|
41
|
+
.map(token => renames[token] ?? token)
|
|
40
42
|
.join(' ');
|
|
41
43
|
}
|
|
@@ -49,8 +49,10 @@ const absoluteFill = Object.freeze({
|
|
|
49
49
|
top: 0,
|
|
50
50
|
bottom: 0,
|
|
51
51
|
});
|
|
52
|
-
// RN's composeStyles:
|
|
53
|
-
// (which flatten later collapses, later keys winning).
|
|
52
|
+
// RN's composeStyles: a null/undefined side yields the other, else the pair [a, b]
|
|
53
|
+
// (which flatten later collapses, later keys winning). The test is null/undefined, NOT
|
|
54
|
+
// falsy - compose(0, y) returns [0, y], matching react-native's own
|
|
55
|
+
// src/private/styles/composeStyles.js, which branches on `== null`.
|
|
54
56
|
function compose(style1, style2) {
|
|
55
57
|
if (style1 === null || style1 === undefined)
|
|
56
58
|
return style2;
|