@kbach/react-native 1.0.0-beta.31 → 1.0.0-beta.32

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/babel-plugin.js +65 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react-native",
3
- "version": "1.0.0-beta.31",
3
+ "version": "1.0.0-beta.32",
4
4
  "description": "Kbach's utility-class styling engine for React Native — the same class names as @kbach/react, resolved to a StyleSheet-ready style object via a Kotlin/JNI TurboModule (native), the same WASM engine @kbach/react uses (Expo Web / react-native-web), or a pure-JS fallback engine (Expo Go). Ships its own native Android module — one npm install, no separate native package. Beta. Native is Android-only (no iOS bridge yet); works with the React Native CLI, an Expo dev client, AND Expo Go (a reduced-coverage pure-JS fallback, since Expo Go can't load any custom native module).",
5
5
  "keywords": [
6
6
  "css",
@@ -25,7 +25,7 @@ const template = require('@babel/template').default;
25
25
  * synchronous) native-bridge call inside the jsx-runtime instead (see
26
26
  * nativeBridge.ts), so this plugin's only job is the pragma injection —
27
27
  * PLUS one more, added later: auto-applying `kbach.config.js` if one
28
- * exists (see `getConfigStatement`'s own doc comment), so a project never
28
+ * exists (see `buildConfigStatement`'s own doc comment), so a project never
29
29
  * needs its own explicit `applyKbachConfig(kbachConfig)` call at all. That
30
30
  * addition deliberately still stops short of old-kbach's own version of
31
31
  * this (an mtime-polling live-reload of the config, re-checked on every
@@ -37,18 +37,59 @@ const template = require('@babel/template').default;
37
37
  // Keyed by the root directory checked — undefined = not checked yet for
38
38
  // that root, null = checked, no kbach.config.js found there. A Babel/Metro
39
39
  // worker process transforms many files over its lifetime — this makes the
40
- // existsSync + template-build work happen ONCE per root, not on every
41
- // single file `pre()` runs for.
42
- const _configStatementCache = new Map();
40
+ // existsSync check happen ONCE per root, not on every single file `pre()`
41
+ // runs for. Caches only the resolved PATH (or null) — never the built AST
42
+ // statement itself; see `buildConfigStatement`'s own doc comment for why
43
+ // that distinction is load-bearing, not a style preference.
44
+ const _configPathCache = new Map();
45
+
46
+ function resolveConfigPath(root) {
47
+ if (_configPathCache.has(root)) return _configPathCache.get(root);
48
+ const configPath = path.join(root, 'kbach.config.js');
49
+ const resolved = fs.existsSync(configPath) ? configPath : null;
50
+ _configPathCache.set(root, resolved);
51
+ return resolved;
52
+ }
53
+
54
+ // Windows' filesystem is case-insensitive but case-preserving, and Metro's
55
+ // own `file.opts.filename` isn't guaranteed to share the exact casing/
56
+ // separator normalization `path.join` produces — comparing case-sensitively
57
+ // risked a false NEGATIVE (kbach.config.js not recognized as itself,
58
+ // re-introducing the self-require bug `buildConfigStatement`'s doc comment
59
+ // describes) far more than a false positive risks colliding two genuinely
60
+ // different files, which would require two paths differing ONLY by case in
61
+ // the same project — effectively never happens in practice.
62
+ function samePath(a, b) {
63
+ return !!a && !!b && path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
64
+ }
43
65
 
44
66
  /**
45
- * Builds the (uncompiled) AST statement that applies `kbach.config.js`,
46
- * cached per `root` after the first call — `null` if no such file exists
47
- * there. `root` defaults to `process.cwd()` (how `metro start`/`expo
48
- * start` are conventionally invoked) at the plugin's own call site below;
49
- * pass `{ configRoot: '...' }` as this plugin's Babel options if your
50
- * `kbach.config.js` lives somewhere else (a monorepo where Metro's actual
51
- * working directory isn't the app's own root, for instance).
67
+ * Builds a FRESH (uncompiled) AST statement that applies `kbach.config.js`
68
+ * — a NEW node on every single call, deliberately never cached or reused
69
+ * across files. `root` defaults to `process.cwd()` (how `metro start`/
70
+ * `expo start` are conventionally invoked) at the plugin's own call site
71
+ * below; pass `{ configRoot: '...' }` as this plugin's Babel options if
72
+ * your `kbach.config.js` lives somewhere else (a monorepo where Metro's
73
+ * actual working directory isn't the app's own root, for instance).
74
+ *
75
+ * `null` if no such file exists at `root` (checked via the cached
76
+ * `resolveConfigPath` above — `fs.existsSync` itself is cheap enough to
77
+ * memoize per root without any of the risk rebuilding the AST fresh here
78
+ * guards against).
79
+ *
80
+ * An EARLIER version of this function built the statement once per root
81
+ * and cached/reused that SAME AST node object across every file's `pre()`
82
+ * call. That's unsafe with Babel: a node spliced into one file's AST gets
83
+ * mutated in place as THAT file's own transform pipeline runs (location
84
+ * info, scope bindings, hoisting markers, ...) — sharing one mutable node
85
+ * instance across many independently-processed file ASTs corrupts it after
86
+ * the first file. Confirmed as the actual cause of a real bundling failure
87
+ * (not a hypothetical): Metro's own dependency collector needs a require()
88
+ * call's location info to validate/instrument it, and once the shared
89
+ * node's location got corrupted by an earlier file's pass, Metro rejected
90
+ * a LATER file's copy with "Invalid call at line <unknown>". Rebuilding
91
+ * fresh per call fixes this at negligible cost — `@babel/template`'s own
92
+ * parse is cheap, and this runs once per file Metro transforms regardless.
52
93
  *
53
94
  * The injected statement is a plain `require(...).applyKbachConfig(require(...))`
54
95
  * — no try/catch around it. `kbach.config.js`'s own path is only ever
@@ -64,21 +105,14 @@ const _configStatementCache = new Map();
64
105
  * all) — simpler than building the equivalent `@babel/types` node graph by
65
106
  * hand for one straightforward statement.
66
107
  */
67
- function getConfigStatement(root) {
68
- if (_configStatementCache.has(root)) return _configStatementCache.get(root);
69
-
70
- const configPath = path.join(root, 'kbach.config.js');
71
- if (!fs.existsSync(configPath)) {
72
- _configStatementCache.set(root, null);
73
- return null;
74
- }
108
+ function buildConfigStatement(root) {
109
+ const configPath = resolveConfigPath(root);
110
+ if (configPath === null) return null;
75
111
 
76
112
  const buildStatement = template(
77
113
  `require('@kbach/react-native').applyKbachConfig(require(${JSON.stringify(configPath)}));`,
78
114
  );
79
- const statement = buildStatement();
80
- _configStatementCache.set(root, statement);
81
- return statement;
115
+ return buildStatement();
82
116
  }
83
117
 
84
118
  module.exports = function kbachReactNativeBabelPlugin(_babel, options) {
@@ -96,7 +130,13 @@ module.exports = function kbachReactNativeBabelPlugin(_babel, options) {
96
130
  comments.unshift({ type: 'CommentLine', value: ' @jsxImportSource @kbach/react-native' });
97
131
  }
98
132
 
99
- const configStatement = getConfigStatement(configRoot);
133
+ // Never inject the auto-apply statement into kbach.config.js itself —
134
+ // it would require() its own absolute path, a real self-reference
135
+ // cycle in Metro's dependency graph for a statement that makes no
136
+ // sense there anyway (a config file applying itself to itself).
137
+ if (samePath(filename, resolveConfigPath(configRoot))) return;
138
+
139
+ const configStatement = buildConfigStatement(configRoot);
100
140
  if (configStatement) {
101
141
  file.ast.program.body.unshift(configStatement);
102
142
  }
@@ -105,7 +145,7 @@ module.exports = function kbachReactNativeBabelPlugin(_babel, options) {
105
145
  };
106
146
  };
107
147
 
108
- /** Exported for tests only — clears the per-root config-file cache between test cases. */
148
+ /** Exported for tests only — clears the per-root config-path cache between test cases. */
109
149
  module.exports._resetForTests = function resetForTests() {
110
- _configStatementCache.clear();
150
+ _configPathCache.clear();
111
151
  };