@keepsnappy/react-native 0.0.0-stage → 0.1.0-alpha.11

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.
@@ -0,0 +1,31 @@
1
+ // zustand/vanilla in a PerfLab bundle. withPerfLab (metro.js) resolves every
2
+ // `zustand/vanilla` request to this module (zustand 5's index, react and
3
+ // traditional entries all load it by that name), and the name below to the real
4
+ // one. Each store's setState goes through the cause tracker's store writer
5
+ // (causes.ts storeWriter), also for the `set` its initializer and middleware
6
+ // get. In a process that does not measure, stores are left as they are.
7
+ 'use strict';
8
+
9
+ var real = require('@keepsnappy/react-native/zustand/vanilla');
10
+
11
+ var wrap = function (createState) {
12
+ return function (set, get, api) {
13
+ var stores = globalThis.__PERFLAB_STORES__;
14
+ if (!stores) {
15
+ return createState(set, get, api);
16
+ }
17
+ var write = stores.writer(set);
18
+ api.setState = write;
19
+ return createState(write, get, api);
20
+ };
21
+ };
22
+
23
+ var createStore = function (createState) {
24
+ return createState
25
+ ? real.createStore(wrap(createState))
26
+ : function (later) {
27
+ return real.createStore(wrap(later));
28
+ };
29
+ };
30
+
31
+ module.exports = Object.assign({}, real, { createStore: createStore });
package/metro.js ADDED
@@ -0,0 +1,238 @@
1
+ // withPerfLab(metroConfig): the only line an app adds to metro.config.js.
2
+ //
3
+ // With EXPO_PUBLIC_PERF_LAB=1 it
4
+ // 1. appends the React DevTools hook as a Metro polyfill, so the hook exists
5
+ // before any module (React included) evaluates;
6
+ // 2. wraps the Babel transformer so the app's entry module imports the SDK boot
7
+ // module (Metro only runs pre-main modules that are already in the graph);
8
+ // 3. lists the boot module in getModulesRunBeforeMainModule, so it runs right
9
+ // after InitializeCore and before the entry's own imports.
10
+ // 4. resolves `zustand/vanilla` to js/zustand-vanilla.js, which routes every
11
+ // zustand store's setState through the cause tracker (REQ-MEAS-32 timers).
12
+ // Without the flag it returns the config object untouched.
13
+ //
14
+ // PERF_LAB_REACT_PROFILING=1 (independent of the flag) resolves React Native's
15
+ // production renderer to its profiling build (fiber actualDuration /
16
+ // selfBaseDuration, root.memoizedUpdaters), the same rewrite as
17
+ // @callstack/inspector's withInspector. Nothing else in the bundle changes.
18
+ //
19
+ // PERF_LAB_REACT_PROFILING=switch (flag on only) bundles both Fabric renderers
20
+ // behind js/renderer-switch.js, which loads the profiling one only in a process
21
+ // whose activation asked for it (plan 40 §2.1). Ignored with the flag off.
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+
26
+ const POLYFILL_PATH = path.join(__dirname, 'js', 'hook.polyfill.js');
27
+ const BOOT_PATH = path.join(__dirname, 'js', 'sdk', 'boot.ts');
28
+ const TRANSFORMER_PATH = path.join(__dirname, 'js', 'babel-transformer.js');
29
+
30
+ const ENV_UPSTREAM = 'KEEPSNAPPY_UPSTREAM_BABEL_TRANSFORMER';
31
+ const ENV_ENTRY = 'KEEPSNAPPY_ENTRY_FILE';
32
+ const ENV_BOOT = 'KEEPSNAPPY_BOOT_FILE';
33
+
34
+ const isEnabled = () => (process.env.EXPO_PUBLIC_KEEPSNAPPY ?? process.env.EXPO_PUBLIC_PERF_LAB) === '1';
35
+ const isProfiling = () => (process.env.KEEPSNAPPY_REACT_PROFILING ?? process.env.PERF_LAB_REACT_PROFILING) === '1';
36
+ const isSwitch = () => (process.env.KEEPSNAPPY_REACT_PROFILING ?? process.env.PERF_LAB_REACT_PROFILING) === 'switch';
37
+
38
+ const SWITCH_PATH = path.join(__dirname, 'js', 'renderer-switch.js');
39
+ // The names renderer-switch.js requires; they resolve next to RN's shim.
40
+ const SDK_NAMES = ['@keepsnappy/react-native', '@perf-labs/react-native'];
41
+ const SWITCH_TARGET_PREFIXES = SDK_NAMES.map(name => `${name}/renderer/`);
42
+ const FABRIC_RENDERERS = new Set([
43
+ '../implementations/ReactFabric-prod',
44
+ '../implementations/ReactFabric-prod.js',
45
+ '../implementations/ReactFabric-profiling',
46
+ '../implementations/ReactFabric-profiling.js',
47
+ ]);
48
+
49
+ const PROD_RENDERERS = {
50
+ '../implementations/ReactFabric-prod': '../implementations/ReactFabric-profiling',
51
+ '../implementations/ReactFabric-prod.js': '../implementations/ReactFabric-profiling.js',
52
+ '../implementations/ReactNativeRenderer-prod': '../implementations/ReactNativeRenderer-profiling',
53
+ '../implementations/ReactNativeRenderer-prod.js': '../implementations/ReactNativeRenderer-profiling.js',
54
+ };
55
+
56
+ const profilingModuleName = moduleName => PROD_RENDERERS[moduleName] ?? moduleName;
57
+
58
+ function withReactProfiling(config) {
59
+ const resolver = config.resolver ?? {};
60
+ const upstream = resolver.resolveRequest;
61
+ return {
62
+ ...config,
63
+ resolver: {
64
+ ...resolver,
65
+ resolveRequest: (context, moduleName, platform) => {
66
+ const rewritten = profilingModuleName(moduleName);
67
+ return upstream
68
+ ? upstream(context, rewritten, platform)
69
+ : context.resolveRequest(context, rewritten, platform);
70
+ },
71
+ },
72
+ };
73
+ }
74
+
75
+ function withRendererSwitch(config) {
76
+ const resolver = config.resolver ?? {};
77
+ const upstream = resolver.resolveRequest;
78
+ const resolve = (context, moduleName, platform) =>
79
+ upstream
80
+ ? upstream(context, moduleName, platform)
81
+ : context.resolveRequest(context, moduleName, platform);
82
+ // RN's shim is always resolved before the switch module's own requires.
83
+ let shim = null;
84
+ return {
85
+ ...config,
86
+ resolver: {
87
+ ...resolver,
88
+ resolveRequest: (context, moduleName, platform) => {
89
+ const switchPrefix = SWITCH_TARGET_PREFIXES.find(prefix => moduleName.startsWith(prefix));
90
+ if (switchPrefix) {
91
+ if (!shim) {
92
+ throw new Error(`KeepSnappy: ${moduleName} required before React Native's renderer shim`);
93
+ }
94
+ const file = moduleName.slice(switchPrefix.length);
95
+ return resolve({ ...context, originModulePath: shim }, `../implementations/${file}`, platform);
96
+ }
97
+ if (FABRIC_RENDERERS.has(moduleName)) {
98
+ shim = context.originModulePath;
99
+ return { type: 'sourceFile', filePath: SWITCH_PATH };
100
+ }
101
+ return resolve(context, moduleName, platform);
102
+ },
103
+ },
104
+ };
105
+ }
106
+
107
+ const STORE_PATH = path.join(__dirname, 'js', 'zustand-vanilla.js');
108
+ const STORE_MODULE = 'zustand/vanilla';
109
+ // The name js/zustand-vanilla.js requires; it resolves to the app's own zustand.
110
+ const STORE_TARGETS = SDK_NAMES.map(name => `${name}/${STORE_MODULE}`);
111
+
112
+ // zustand 5 loads its vanilla module by package name from every entry, so one
113
+ // name covers them. There is one wrapper module, so the app must have one
114
+ // zustand copy: a request that resolves to another copy fails the build rather
115
+ // than give that copy's entries a vanilla module of another version.
116
+ function withStoreWrap(config) {
117
+ const resolver = config.resolver ?? {};
118
+ const upstream = resolver.resolveRequest;
119
+ const resolve = (context, moduleName, platform) =>
120
+ upstream
121
+ ? upstream(context, moduleName, platform)
122
+ : context.resolveRequest(context, moduleName, platform);
123
+ let real = null;
124
+ return {
125
+ ...config,
126
+ resolver: {
127
+ ...resolver,
128
+ resolveRequest: (context, moduleName, platform) => {
129
+ if (moduleName === STORE_MODULE) {
130
+ const found = resolve(context, STORE_MODULE, platform);
131
+ if (!real) {
132
+ real = found;
133
+ } else if (found.filePath !== real.filePath) {
134
+ throw new Error(
135
+ `KeepSnappy: two zustand copies (${real.filePath}, ${found.filePath}); the KeepSnappy store wrap needs one`,
136
+ );
137
+ }
138
+ return { type: 'sourceFile', filePath: STORE_PATH };
139
+ }
140
+ if (STORE_TARGETS.includes(moduleName)) {
141
+ if (!real) {
142
+ throw new Error(`KeepSnappy: ${moduleName} required before ${STORE_MODULE}`);
143
+ }
144
+ return real;
145
+ }
146
+ return resolve(context, moduleName, platform);
147
+ },
148
+ },
149
+ };
150
+ }
151
+
152
+ const SOURCE_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
153
+
154
+ const resolveEntry = projectRoot => {
155
+ let main = 'index';
156
+ try {
157
+ const pkg = JSON.parse(
158
+ fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'),
159
+ );
160
+ if (typeof pkg.main === 'string') {
161
+ main = pkg.main;
162
+ }
163
+ } catch {
164
+ // no package.json main: Metro's own default is index
165
+ }
166
+ const base = path.resolve(projectRoot, main);
167
+ if (fs.existsSync(base) && fs.statSync(base).isFile()) {
168
+ return base;
169
+ }
170
+ for (const ext of SOURCE_EXTS) {
171
+ if (fs.existsSync(base + ext)) {
172
+ return base + ext;
173
+ }
174
+ }
175
+ return null;
176
+ };
177
+
178
+ function withPerfLab(input) {
179
+ if (!isEnabled()) {
180
+ return isProfiling() ? withReactProfiling(input) : input;
181
+ }
182
+ let config = input;
183
+ if (isProfiling()) {
184
+ config = withReactProfiling(input);
185
+ } else if (isSwitch()) {
186
+ config = withRendererSwitch(input);
187
+ }
188
+ config = withStoreWrap(config);
189
+ const projectRoot = config.projectRoot ?? process.cwd();
190
+ const entry = resolveEntry(projectRoot);
191
+ const upstream = config.transformer?.babelTransformerPath;
192
+ if (!entry || !upstream) {
193
+ throw new Error(
194
+ `KeepSnappy: cannot ${entry ? 'find transformer.babelTransformerPath' : 'resolve the entry file from package.json main'}`,
195
+ );
196
+ }
197
+
198
+ // Transform workers are forked after the config loads and inherit this env.
199
+ process.env[ENV_UPSTREAM] = upstream;
200
+ process.env[ENV_ENTRY] = entry;
201
+ process.env[ENV_BOOT] = BOOT_PATH;
202
+
203
+ const serializer = config.serializer ?? {};
204
+ const upstreamPolyfills = serializer.getPolyfills;
205
+ const upstreamPreMain = serializer.getModulesRunBeforeMainModule;
206
+
207
+ return {
208
+ ...config,
209
+ serializer: {
210
+ ...serializer,
211
+ getPolyfills: options => [
212
+ ...(upstreamPolyfills ? upstreamPolyfills(options) : []),
213
+ POLYFILL_PATH,
214
+ ],
215
+ getModulesRunBeforeMainModule: entryFilePath => [
216
+ ...(upstreamPreMain ? upstreamPreMain(entryFilePath) : []),
217
+ BOOT_PATH,
218
+ ],
219
+ },
220
+ transformer: {
221
+ ...config.transformer,
222
+ babelTransformerPath: TRANSFORMER_PATH,
223
+ },
224
+ };
225
+ }
226
+
227
+ module.exports = {
228
+ withPerfLab,
229
+ withKeepSnappy: withPerfLab,
230
+ withReactProfiling,
231
+ withRendererSwitch,
232
+ withStoreWrap,
233
+ profilingModuleName,
234
+ STORE_PATH,
235
+ SWITCH_PATH,
236
+ POLYFILL_PATH,
237
+ BOOT_PATH,
238
+ };
package/package.json CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
2
  "name": "@keepsnappy/react-native",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-alpha.11",
4
+ "description": "KeepSnappy in-app SDK for React Native / Expo: zero-code measurement windows, React commit hook, native signposts and log file",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/arekkubaczkowski/keepsnappy.git",
9
+ "directory": "packages/react-native"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "files": [
15
+ "app.plugin.js",
16
+ "metro.js",
17
+ "expo-module.config.json",
18
+ "js",
19
+ "ios",
20
+ "android/build.gradle",
21
+ "android/src",
22
+ "README.md"
23
+ ],
24
+ "peerDependencies": {
25
+ "expo": "*",
26
+ "react-native": "*"
27
+ }
28
+ }