@gusnips/native 0.0.0-stage → 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 +310 -2
- package/dist/device-locale.d.ts +15 -0
- package/dist/device-locale.d.ts.map +1 -0
- package/dist/device-locale.js +27 -0
- package/dist/device-locale.js.map +1 -0
- package/dist/i18n.d.ts +34 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +63 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/network.d.ts +18 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +35 -0
- package/dist/network.js.map +1 -0
- package/dist/node/bin/gusnips-native-css.d.ts +3 -0
- package/dist/node/bin/gusnips-native-css.d.ts.map +1 -0
- package/dist/node/bin/gusnips-native-css.js +75 -0
- package/dist/node/bin/gusnips-native-css.js.map +1 -0
- package/dist/node/css.d.ts +44 -0
- package/dist/node/css.d.ts.map +1 -0
- package/dist/node/css.js +181 -0
- package/dist/node/css.js.map +1 -0
- package/dist/node/metro.d.ts +51 -0
- package/dist/node/metro.d.ts.map +1 -0
- package/dist/node/metro.js +100 -0
- package/dist/node/metro.js.map +1 -0
- package/dist/query.d.ts +13 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +37 -0
- package/dist/query.js.map +1 -0
- package/dist/sse.d.ts +47 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +135 -0
- package/dist/sse.js.map +1 -0
- package/dist/supabase.d.ts +18 -0
- package/dist/supabase.d.ts.map +1 -0
- package/dist/supabase.js +29 -0
- package/dist/supabase.js.map +1 -0
- package/dist/theme.d.ts +25 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +99 -0
- package/dist/theme.js.map +1 -0
- package/package.json +150 -4
- package/src/device-locale.test.ts +33 -0
- package/src/device-locale.ts +28 -0
- package/src/i18n.ts +91 -0
- package/src/index.ts +18 -0
- package/src/network.ts +37 -0
- package/src/node/bin/gusnips-native-css.ts +84 -0
- package/src/node/css.ts +246 -0
- package/src/node/metro.test.ts +66 -0
- package/src/node/metro.ts +143 -0
- package/src/query.ts +41 -0
- package/src/sse.test.ts +195 -0
- package/src/sse.ts +174 -0
- package/src/supabase.ts +29 -0
- package/src/theme.test.ts +92 -0
- package/src/theme.ts +132 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gustavo Salomé
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,311 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gusnips/native
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
What a bare React Native app needs beside `@gusnips/react`: the Metro config for a bun monorepo,
|
|
4
|
+
the Tailwind build for react-native-css, streams from your API, React Query that knows when the app
|
|
5
|
+
is in the background, and a theme and a language that remember what the person picked. No Expo.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @gusnips/native
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
// metro.config.js
|
|
13
|
+
const { getDefaultConfig } = require("@react-native/metro-config");
|
|
14
|
+
const { nativeMetroConfig } = require("@gusnips/native/metro");
|
|
15
|
+
|
|
16
|
+
module.exports = nativeMetroConfig(getDefaultConfig(__dirname));
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
That one line makes every `react` import in the bundle the app's own copy, and maps
|
|
20
|
+
react-native-css's override module so styled components load. Both are crashes you would otherwise
|
|
21
|
+
meet at launch.
|
|
22
|
+
|
|
23
|
+
## What is in it
|
|
24
|
+
|
|
25
|
+
| Import | What it does | Also needs |
|
|
26
|
+
| -------------------------- | -------------------------------------------------------------- | ----------------------------------------------------- |
|
|
27
|
+
| `@gusnips/native` | `openSseStream`: server-sent events over XHR | nothing more |
|
|
28
|
+
| `@gusnips/native/theme` | `createNativeTheme`: light, dark or system, remembered | `@react-native-async-storage/async-storage` |
|
|
29
|
+
| `@gusnips/native/network` | `useIsOffline`, `isDeviceOffline` | `@react-native-community/netinfo` |
|
|
30
|
+
| `@gusnips/native/query` | `installQueryManagers`: React Query on app focus and network | `@tanstack/react-query`, netinfo |
|
|
31
|
+
| `@gusnips/native/i18n` | `createNativeLanguage`: the phone's language, or the saved one | `react-native-localize`, async-storage, `i18next` |
|
|
32
|
+
| `@gusnips/native/supabase` | `supabaseAuthStorage`: where supabase-js keeps the session | async-storage, `@supabase/supabase-js` |
|
|
33
|
+
| `@gusnips/native/metro` | `nativeMetroConfig`, for `metro.config.js` | nothing more |
|
|
34
|
+
| `@gusnips/native/css` | `buildNativeCss` and the `gusnips-native-css` command | `@tailwindcss/cli`, `tailwindcss`, `react-native-css` |
|
|
35
|
+
|
|
36
|
+
Every app needs `react`, `react-native`, `@gusnips/react` and `@gusnips/http`. The rest are
|
|
37
|
+
optional: you install one only when you import the line that uses it. Most of them carry native
|
|
38
|
+
code that has to be linked into the app, so an app that does not use NetInfo should not have to
|
|
39
|
+
ship it.
|
|
40
|
+
|
|
41
|
+
## Metro
|
|
42
|
+
|
|
43
|
+
In a monorepo, tell it where the root is:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
module.exports = nativeMetroConfig(getDefaultConfig(__dirname), {
|
|
47
|
+
monorepoRoot: require("node:path").resolve(__dirname, "../.."),
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
What it adds to the config:
|
|
52
|
+
|
|
53
|
+
- **The monorepo.** `watchFolders` and `nodeModulesPaths` point at the root, so a workspace package
|
|
54
|
+
that ships TypeScript source resolves like any other. Metro does not find workspaces on its own.
|
|
55
|
+
- **One React.** bun's isolated store keeps a spare `react` at its root. A package that forgets to
|
|
56
|
+
declare react as a peer finds that one instead, and the bundle has two: your screens render, and
|
|
57
|
+
that package's hooks crash with "Cannot read property 'useMemo' of null". Every `react` and
|
|
58
|
+
`react/*` import now goes to the copy your app resolves. `react-native` and `react-dom` are left
|
|
59
|
+
alone.
|
|
60
|
+
- **react-native-css's override module.** `react-native-css/native` imports a module that throws
|
|
61
|
+
unless Metro maps it to the package's empty `override.js`. Without the mapping every styled
|
|
62
|
+
component fails with "Cannot read property 'useCssElement' of undefined". Skipped when the app
|
|
63
|
+
does not use react-native-css.
|
|
64
|
+
|
|
65
|
+
A resolver already on the config still runs for everything else.
|
|
66
|
+
|
|
67
|
+
**`metro.config.js` stays CommonJS, as the React Native template writes it.** This package is ES
|
|
68
|
+
modules only, and `require()` of an ES module works without a flag on every Node version React
|
|
69
|
+
Native 0.86 supports: 20.19.4 and later, 22.13 and later, 24.3 and later. Measured by loading the
|
|
70
|
+
config through Metro's own `loadConfig` on Node 22.22 and 24.21.
|
|
71
|
+
|
|
72
|
+
Never add `withReactNativeCSS` or NativeWind's `withNativewind` on top. Both also replace Metro's
|
|
73
|
+
transformer with one that requires `@expo/metro-config`, and Metro crashes on a bare app. The CSS
|
|
74
|
+
is built ahead of time instead, below.
|
|
75
|
+
|
|
76
|
+
## Styles: Tailwind on React Native
|
|
77
|
+
|
|
78
|
+
NativeWind v5 and react-native-css read `className` at runtime against a compiled stylesheet.
|
|
79
|
+
`gusnips-native-css` builds that stylesheet from your `global.css`, with the Tailwind v4 CLI and
|
|
80
|
+
then react-native-css's compiler:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
gusnips-native-css # global.css → src/generated/stylesheet.ts
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Run it before Metro, in the same script, or a class you just added resolves to nothing:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"scripts": {
|
|
91
|
+
"start": "gusnips-native-css && react-native start",
|
|
92
|
+
"ios": "gusnips-native-css && react-native run-ios",
|
|
93
|
+
"android": "gusnips-native-css && react-native run-android"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Then load the stylesheet once, at the top of `App.tsx`:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { StyleCollection } from "react-native-css/native-internal";
|
|
102
|
+
import { stylesheet } from "./generated/stylesheet";
|
|
103
|
+
|
|
104
|
+
StyleCollection.inject(stylesheet);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Never import `global.css` from `index.js`: NativeWind warns that it breaks Fast Refresh, and the
|
|
108
|
+
compiled file is what the app reads. Add `src/generated/` to `.gitignore`.
|
|
109
|
+
|
|
110
|
+
The tools come from your app, not from this package, because the stylesheet must match the
|
|
111
|
+
react-native-css your bundle runs:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
bun add react-native-css && bun add -d @tailwindcss/cli tailwindcss
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Five things worth knowing:
|
|
118
|
+
|
|
119
|
+
- **One `rem` is 16 pixels, as on the web.** react-native-css's own default is 14, React Native's
|
|
120
|
+
root font size, which shrinks every rem-based size by an eighth: `h-12` comes out 42 px instead
|
|
121
|
+
of 48. `--rem` changes it if you really want another value.
|
|
122
|
+
- **Tailwind finds your classes by itself** in the app's folder. Tailwind v4 has no `--content`
|
|
123
|
+
flag. For a file outside the folder, add an `@source` line to `global.css`.
|
|
124
|
+
- **Pin lightningcss to 1.30.1, or use a version after 1.30.2.** 1.30.2 breaks react-native-css's
|
|
125
|
+
compiler, and react-native-css's own check for it is swallowed by an empty `catch`. The build
|
|
126
|
+
refuses 1.30.2 instead. Pin it with `"overrides": { "lightningcss": "1.30.1" }` in the root
|
|
127
|
+
`package.json`: a devDependency is not enough, because react-native-css loads the copy under
|
|
128
|
+
`@expo/metro-config` first.
|
|
129
|
+
- **A `className` change hot-reloads.** Only a change to `global.css` or your tokens needs the
|
|
130
|
+
build to run again.
|
|
131
|
+
- **There is no global font.** react-native-css drops element and `*` selectors, so a text without
|
|
132
|
+
a `font-*` class renders in the system font.
|
|
133
|
+
|
|
134
|
+
### Colours for code
|
|
135
|
+
|
|
136
|
+
An icon's `color`, the navigation theme and the status bar take a hex string, not a class. Name
|
|
137
|
+
the colours in a JSON file, each as a class per scheme:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"text": { "light": "text-ink", "dark": "dark:text-ink-dark" },
|
|
142
|
+
"canvas": { "light": "bg-surface", "dark": "dark:bg-surface-dark" }
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`gusnips-native-css --palette palette.json` writes `colors.ts` beside the stylesheet, with the hex
|
|
147
|
+
each class compiles to. The colours come from the stylesheet itself, so a change of tokens moves the
|
|
148
|
+
icons with the text. Tailwind writes only classes it finds in your files, so keep each palette
|
|
149
|
+
class in your source; the build stops and names any it cannot find.
|
|
150
|
+
|
|
151
|
+
From a script, the same build is `buildNativeCss({ palette })` from `@gusnips/native/css`.
|
|
152
|
+
|
|
153
|
+
## Streams
|
|
154
|
+
|
|
155
|
+
React Native's `fetch` cannot read a response as it arrives, so `readSseStream` from
|
|
156
|
+
`@gusnips/react` has nothing to read. `openSseStream` uses `XMLHttpRequest`, which can, and the
|
|
157
|
+
same frame parser:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { openSseStream } from "@gusnips/native";
|
|
161
|
+
|
|
162
|
+
const stream = openSseStream({
|
|
163
|
+
url: `${API_URL}/chat/stream`,
|
|
164
|
+
body: { message },
|
|
165
|
+
session, // the SessionAdapter your createApiClient uses
|
|
166
|
+
onFrame: ({ event, data }) => handle(event, JSON.parse(data)),
|
|
167
|
+
onError: showError,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
stream.abort(); // when the screen closes
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- A refused stream fails with the same `ApiError` your API client throws, with the code, the
|
|
174
|
+
request id and the `Retry-After` wait. So a 402 on a stream gets the same sentence and button as
|
|
175
|
+
any other 402. A lost connection is a plain `Error`, which the describer reads as "never
|
|
176
|
+
landed".
|
|
177
|
+
- It sends the session's token, and on a 401 it calls the session's `refresh()` once and opens
|
|
178
|
+
again. Pass the same adapter your API client uses: its refresh runs once for everyone, so a
|
|
179
|
+
stream and a page load do not refresh at the same time (the auth server rotates the refresh
|
|
180
|
+
token, and the loser of that race signs the person out). No token back, and the stream fails
|
|
181
|
+
with the 401; ending the session stays the API client's job. `headers` adds anything else, like
|
|
182
|
+
the client's own option.
|
|
183
|
+
- `data` is raw text. Parse it the way your server writes it.
|
|
184
|
+
- No reconnect. Reopen the stream from where your screen knows how to recover.
|
|
185
|
+
- The whole response stays in memory until the stream ends. Fine for a chat answer; not for a feed
|
|
186
|
+
left open all day.
|
|
187
|
+
|
|
188
|
+
## React Query
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { queryDefaults } from "@gusnips/react";
|
|
192
|
+
import { installQueryManagers } from "@gusnips/native/query";
|
|
193
|
+
|
|
194
|
+
installQueryManagers();
|
|
195
|
+
export const queryClient = new QueryClient({ defaultOptions: queryDefaults() });
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
React Query's own defaults watch the browser's window and `navigator.onLine`, which a phone does
|
|
199
|
+
not have. After this, retries wait while the app is in the background or offline, and stale
|
|
200
|
+
queries run again when the connection returns.
|
|
201
|
+
|
|
202
|
+
It does **not** refetch when the app comes back to the foreground: `queryDefaults` leaves
|
|
203
|
+
`refetchOnWindowFocus` off. For an app people leave open for days, you probably want it on:
|
|
204
|
+
`queryDefaults({ refetchOnWindowFocus: true })`.
|
|
205
|
+
|
|
206
|
+
## Offline
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { useIsOffline } from "@gusnips/native/network";
|
|
210
|
+
|
|
211
|
+
const offline = useIsOffline(); // true while the phone has no connection
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`isDeviceOffline()` answers the same question once. React Native's `fetch` fails the same way
|
|
215
|
+
whether the server is down or the phone has no signal, so ask it before you tell someone the
|
|
216
|
+
service is having trouble. While NetInfo is still working it out, both say "online", so no
|
|
217
|
+
offline banner flashes at launch.
|
|
218
|
+
|
|
219
|
+
## Theme
|
|
220
|
+
|
|
221
|
+
The same options and the same `useTheme` as `@gusnips/react/theme`, applied with
|
|
222
|
+
`Appearance.setColorScheme`, which is what `useColorScheme()` and the `dark:` classes read:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { createNativeTheme } from "@gusnips/native/theme";
|
|
226
|
+
|
|
227
|
+
export const theme = createNativeTheme({ key: "app:theme" });
|
|
228
|
+
|
|
229
|
+
// In the root component:
|
|
230
|
+
useEffect(() => theme.start(), []);
|
|
231
|
+
|
|
232
|
+
// Anywhere:
|
|
233
|
+
const { mode, resolved, setMode } = theme.useTheme();
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`start()` loads the saved choice and puts it back each time the app returns to the foreground,
|
|
237
|
+
because Android drops the choice when it recreates the screen. A choice made while the saved one is
|
|
238
|
+
still loading wins over it.
|
|
239
|
+
|
|
240
|
+
## Language
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import i18n from "i18next";
|
|
244
|
+
import { initReactI18next } from "react-i18next";
|
|
245
|
+
import { createNativeLanguage } from "@gusnips/native/i18n";
|
|
246
|
+
|
|
247
|
+
export const language = createNativeLanguage(i18n, {
|
|
248
|
+
supported: ["pt-BR", "en"],
|
|
249
|
+
storageKey: "app:language",
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
i18n.use(language.detector).use(initReactI18next).init({ resources, fallbackLng: "pt-BR" });
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
At launch, call `language.restore()`. From your language picker, call `language.choose("en")`, or
|
|
256
|
+
`language.choose(null)` to follow the phone again.
|
|
257
|
+
|
|
258
|
+
- The first render is already in the phone's language. The saved choice is read after, and wins.
|
|
259
|
+
- The phone's languages are read in its order: on `pt-PT, en-US` the app picks `pt-BR`, and on
|
|
260
|
+
`fr-FR, en-US` it picks `en` instead of the fallback.
|
|
261
|
+
- Only `choose` saves a language. i18next hands every language change to the detector, including
|
|
262
|
+
the one it detects at launch, so a detector that saved there would treat the phone's language as
|
|
263
|
+
the person's choice, and the app would ignore the phone from then on.
|
|
264
|
+
|
|
265
|
+
## Supabase
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { supabaseAuthStorage } from "@gusnips/native/supabase";
|
|
269
|
+
|
|
270
|
+
export const authStorage = supabaseAuthStorage(SUPABASE_URL);
|
|
271
|
+
export const supabase = createClient(SUPABASE_URL, SUPABASE_KEY, {
|
|
272
|
+
auth: { ...authStorage, detectSessionInUrl: false, flowType: "pkce" },
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await signOutEvenOffline(supabase.auth, authStorage); // from @gusnips/react/supabase
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The session goes in AsyncStorage, not the keychain: a session is two tokens and the user's data in
|
|
279
|
+
one value, and it often passes the about 2 KB a keystore holds per value. The key is the one
|
|
280
|
+
supabase-js picks itself, so moving onto this signs nobody out. Pass the same object to
|
|
281
|
+
`signOutEvenOffline`, so the sign-out clears the key the client reads.
|
|
282
|
+
|
|
283
|
+
Use `flowType: "pkce"`. A sign-in returns through your app's URL scheme, which any app can claim,
|
|
284
|
+
and under the default flow that URL carries the refresh token.
|
|
285
|
+
|
|
286
|
+
## Also worth knowing
|
|
287
|
+
|
|
288
|
+
- **Do not use `react-native-css/components/react-native-safe-area-context`.** In 3.0.7 everything
|
|
289
|
+
under it loses its `className` styles. Use `SafeAreaView` from `react-native-safe-area-context`
|
|
290
|
+
with `style={{ flex: 1 }}`, and put the colours on a styled `View` inside it.
|
|
291
|
+
- **`nativewind/babel` is a preset, not a plugin.** Put it in `presets` in `babel.config.js`.
|
|
292
|
+
- **Keep React at one version in the whole repo**, with the root `workspaces.catalog`. The Metro
|
|
293
|
+
resolver protects the bundle from bun's store layout; the catalog stops the versions drifting in
|
|
294
|
+
the first place.
|
|
295
|
+
|
|
296
|
+
## Why the dependencies are peers
|
|
297
|
+
|
|
298
|
+
There are no `dependencies`. Each package here is one your app already has, or one that carries
|
|
299
|
+
native code, and an app must have exactly one copy of each:
|
|
300
|
+
|
|
301
|
+
- `react`, `react-native`: two copies of either breaks the app.
|
|
302
|
+
- `@gusnips/react` is a peer, not a dependency, on purpose. `@gusnips/vite` depends on it, so its
|
|
303
|
+
release pins one exact version, and an app on another version gets a second copy nested inside
|
|
304
|
+
vite. A peer uses yours. Its range is `>=0.10.0 <1`, so a new minor of `@gusnips/react` does not
|
|
305
|
+
need a release of this package.
|
|
306
|
+
- `@gusnips/http`: `openSseStream` reads the error envelope and `Retry-After` with it, the same
|
|
307
|
+
functions the API client uses. Same reasoning, same kind of range.
|
|
308
|
+
- AsyncStorage, NetInfo and react-native-localize hold native code that autolinking compiles into
|
|
309
|
+
the app, so each is optional and behind its own import.
|
|
310
|
+
- `tailwindcss`, `@tailwindcss/cli` and `react-native-css` are read from your app's folder at build
|
|
311
|
+
time, so the stylesheet matches what your bundle runs.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The language we ship that the phone's list asks for first, or `null` when none of them is one.
|
|
3
|
+
*
|
|
4
|
+
* Walk the phone's list in ITS order, and for each tag take an exact match (any case), then a
|
|
5
|
+
* language with the same base subtag. So a phone set to `["pt-PT", "en-US"]` gets our `pt-BR`: the
|
|
6
|
+
* person put Portuguese first. The donor looked at the first tag only, so a phone on
|
|
7
|
+
* `["fr-FR", "en-US"]` got the fallback instead of the English the person also reads.
|
|
8
|
+
*
|
|
9
|
+
* This is `matchLocale` from `@gusnips/locale`, written again because that package is a leaf
|
|
10
|
+
* nothing in this repo depends on (AGENTS.md says why). `@gusnips/react`'s `i18nInitOptions` keeps
|
|
11
|
+
* a third copy. The test holds this one to the same table, so a reader lands on one language
|
|
12
|
+
* whichever road they come in by.
|
|
13
|
+
*/
|
|
14
|
+
export declare function pickDeviceLocale(deviceTags: readonly string[], supported: readonly string[]): string | null;
|
|
15
|
+
//# sourceMappingURL=device-locale.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-locale.d.ts","sourceRoot":"","sources":["../src/device-locale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,SAAS,EAAE,SAAS,MAAM,EAAE,GAC3B,MAAM,GAAG,IAAI,CAWf"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The language we ship that the phone's list asks for first, or `null` when none of them is one.
|
|
3
|
+
*
|
|
4
|
+
* Walk the phone's list in ITS order, and for each tag take an exact match (any case), then a
|
|
5
|
+
* language with the same base subtag. So a phone set to `["pt-PT", "en-US"]` gets our `pt-BR`: the
|
|
6
|
+
* person put Portuguese first. The donor looked at the first tag only, so a phone on
|
|
7
|
+
* `["fr-FR", "en-US"]` got the fallback instead of the English the person also reads.
|
|
8
|
+
*
|
|
9
|
+
* This is `matchLocale` from `@gusnips/locale`, written again because that package is a leaf
|
|
10
|
+
* nothing in this repo depends on (AGENTS.md says why). `@gusnips/react`'s `i18nInitOptions` keeps
|
|
11
|
+
* a third copy. The test holds this one to the same table, so a reader lands on one language
|
|
12
|
+
* whichever road they come in by.
|
|
13
|
+
*/
|
|
14
|
+
export function pickDeviceLocale(deviceTags, supported) {
|
|
15
|
+
for (const tag of deviceTags) {
|
|
16
|
+
const lower = tag.trim().toLowerCase();
|
|
17
|
+
if (lower === "")
|
|
18
|
+
continue;
|
|
19
|
+
const base = lower.split("-")[0];
|
|
20
|
+
const found = supported.find((lng) => lng.toLowerCase() === lower) ??
|
|
21
|
+
supported.find((lng) => lng.split("-")[0]?.toLowerCase() === base);
|
|
22
|
+
if (found !== undefined)
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=device-locale.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-locale.js","sourceRoot":"","sources":["../src/device-locale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAA6B,EAC7B,SAA4B;IAE5B,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACvC,IAAI,KAAK,KAAK,EAAE;YAAE,SAAS;QAC3B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,KAAK,GACT,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC;YACpD,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC,CAAC;QACrE,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;IACxC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/i18n.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { i18n as I18n, LanguageDetectorModule } from "i18next";
|
|
2
|
+
export interface NativeLanguageOptions {
|
|
3
|
+
/** Every language this app ships, as the exact tags its catalogs use. */
|
|
4
|
+
supported: readonly string[];
|
|
5
|
+
/** The AsyncStorage key holding the person's choice. Keep the key your app already uses. */
|
|
6
|
+
storageKey: string;
|
|
7
|
+
}
|
|
8
|
+
export interface NativeLanguage {
|
|
9
|
+
/** Hand to `i18n.use()` before `init`. */
|
|
10
|
+
detector: LanguageDetectorModule;
|
|
11
|
+
/** Apply the saved choice, if there is one. Call once at launch, after `init`. */
|
|
12
|
+
restore(): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* The person picked a language: switch to it and remember it. `null` means "follow the phone":
|
|
15
|
+
* forget the choice and switch to what the phone asks for.
|
|
16
|
+
*/
|
|
17
|
+
choose(language: string | null): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* ```ts
|
|
21
|
+
* import i18n from "i18next";
|
|
22
|
+
*
|
|
23
|
+
* export const language = createNativeLanguage(i18n, {
|
|
24
|
+
* supported: ["pt-BR", "en"],
|
|
25
|
+
* storageKey: "app:language",
|
|
26
|
+
* });
|
|
27
|
+
* i18n.use(language.detector).use(initReactI18next).init({ resources, fallbackLng: "pt-BR" });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* A phone whose languages are none of `supported` gets `fallbackLng`: the detector answers nothing
|
|
31
|
+
* and i18next falls back on its own.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createNativeLanguage(i18n: I18n, options: NativeLanguageOptions): NativeLanguage;
|
|
34
|
+
//# sourceMappingURL=i18n.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,IAAI,IAAI,IAAI,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAIpE,MAAM,WAAW,qBAAqB;IACpC,yEAAyE;IACzE,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,4FAA4F;IAC5F,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,QAAQ,EAAE,sBAAsB,CAAC;IACjC,kFAAkF;IAClF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB;;;OAGG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,qBAAqB,GAAG,cAAc,CAqC/F"}
|
package/dist/i18n.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app's language on a phone: the one the person picked, else the phone's own, else your
|
|
3
|
+
* fallback.
|
|
4
|
+
*
|
|
5
|
+
* The detector reads the phone's languages with react-native-localize, synchronously, so the first
|
|
6
|
+
* render is already in a real language. A choice saved earlier is read from AsyncStorage after
|
|
7
|
+
* that (`restore`), and wins.
|
|
8
|
+
*
|
|
9
|
+
* **Only `choose` writes the saved choice.** i18next calls a detector's `cacheUserLanguage` on
|
|
10
|
+
* EVERY language change, the one it detected at launch included. The donor's detector saved there,
|
|
11
|
+
* so the first launch saved the phone's language as though the person had picked it, and when they
|
|
12
|
+
* later switched the phone to another language the app stayed in the old one for good. That is
|
|
13
|
+
* `storageWriter: "app"` in `@gusnips/react`'s `i18nInitOptions`, the same lesson from the web: one
|
|
14
|
+
* writer per key, and here the writer is your language control.
|
|
15
|
+
*/
|
|
16
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
17
|
+
import { getLocales } from "react-native-localize";
|
|
18
|
+
import { pickDeviceLocale } from "./device-locale.js";
|
|
19
|
+
/**
|
|
20
|
+
* ```ts
|
|
21
|
+
* import i18n from "i18next";
|
|
22
|
+
*
|
|
23
|
+
* export const language = createNativeLanguage(i18n, {
|
|
24
|
+
* supported: ["pt-BR", "en"],
|
|
25
|
+
* storageKey: "app:language",
|
|
26
|
+
* });
|
|
27
|
+
* i18n.use(language.detector).use(initReactI18next).init({ resources, fallbackLng: "pt-BR" });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* A phone whose languages are none of `supported` gets `fallbackLng`: the detector answers nothing
|
|
31
|
+
* and i18next falls back on its own.
|
|
32
|
+
*/
|
|
33
|
+
export function createNativeLanguage(i18n, options) {
|
|
34
|
+
const { supported, storageKey } = options;
|
|
35
|
+
// A choice made while the saved one is still loading wins over it.
|
|
36
|
+
let chosen = false;
|
|
37
|
+
const fromDevice = () => pickDeviceLocale(getLocales().map((locale) => locale.languageTag), supported) ?? undefined;
|
|
38
|
+
const detector = {
|
|
39
|
+
type: "languageDetector",
|
|
40
|
+
init: () => { },
|
|
41
|
+
detect: fromDevice,
|
|
42
|
+
// Deliberately empty. See the top of this file.
|
|
43
|
+
cacheUserLanguage: () => { },
|
|
44
|
+
};
|
|
45
|
+
async function restore() {
|
|
46
|
+
const stored = await AsyncStorage.getItem(storageKey).catch(() => null);
|
|
47
|
+
if (chosen || stored === null || !supported.includes(stored))
|
|
48
|
+
return;
|
|
49
|
+
if (i18n.language !== stored)
|
|
50
|
+
await i18n.changeLanguage(stored);
|
|
51
|
+
}
|
|
52
|
+
async function choose(language) {
|
|
53
|
+
chosen = true;
|
|
54
|
+
const write = language === null
|
|
55
|
+
? AsyncStorage.removeItem(storageKey)
|
|
56
|
+
: AsyncStorage.setItem(storageKey, language);
|
|
57
|
+
// Not remembered across launches when storage fails, and still applied for this one.
|
|
58
|
+
await write.catch(() => { });
|
|
59
|
+
await i18n.changeLanguage(language ?? fromDevice());
|
|
60
|
+
}
|
|
61
|
+
return { detector, restore, choose };
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=i18n.js.map
|
package/dist/i18n.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,YAAY,MAAM,2CAA2C,CAAC;AAErE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAqBtD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAU,EAAE,OAA8B;IAC7E,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IAC1C,mEAAmE;IACnE,IAAI,MAAM,GAAG,KAAK,CAAC;IAEnB,MAAM,UAAU,GAAG,GAAG,EAAE,CACtB,gBAAgB,CACd,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,EAChD,SAAS,CACV,IAAI,SAAS,CAAC;IAEjB,MAAM,QAAQ,GAA2B;QACvC,IAAI,EAAE,kBAAkB;QACxB,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;QACd,MAAM,EAAE,UAAU;QAClB,gDAAgD;QAChD,iBAAiB,EAAE,GAAG,EAAE,GAAE,CAAC;KAC5B,CAAC;IAEF,KAAK,UAAU,OAAO;QACpB,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACxE,IAAI,MAAM,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO;QACrE,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM;YAAE,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IAClE,CAAC;IAED,KAAK,UAAU,MAAM,CAAC,QAAuB;QAC3C,MAAM,GAAG,IAAI,CAAC;QACd,MAAM,KAAK,GACT,QAAQ,KAAK,IAAI;YACf,CAAC,CAAC,YAAY,CAAC,UAAU,CAAC,UAAU,CAAC;YACrC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QACjD,qFAAqF;QACrF,MAAM,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC5B,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,IAAI,UAAU,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AACvC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,aAAa,EAAE,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @gusnips/native: what a bare React Native app needs beside @gusnips/react.
|
|
2
|
+
//
|
|
3
|
+
// The rule that picks the subpaths is the kit's invariant 15, checked by `bun run exports`: a peer
|
|
4
|
+
// marked optional must not be reachable from this entry. Every subpath below needs one more package
|
|
5
|
+
// with native code or a build tool in it, and an app installs that package only by importing the
|
|
6
|
+
// thing that uses it:
|
|
7
|
+
//
|
|
8
|
+
// @gusnips/native/theme createNativeTheme → @react-native-async-storage/async-storage
|
|
9
|
+
// @gusnips/native/network useIsOffline → @react-native-community/netinfo
|
|
10
|
+
// @gusnips/native/query installQueryManagers → @tanstack/react-query, netinfo
|
|
11
|
+
// @gusnips/native/i18n createNativeLanguage → react-native-localize, async-storage
|
|
12
|
+
// @gusnips/native/supabase supabaseAuthStorage → async-storage, @supabase/supabase-js
|
|
13
|
+
// @gusnips/native/metro nativeMetroConfig → Node, at bundle time
|
|
14
|
+
// @gusnips/native/css buildNativeCss → Node, tailwindcss, react-native-css
|
|
15
|
+
//
|
|
16
|
+
// What is left here needs only the required peers.
|
|
17
|
+
export { openSseStream } from "./sse.js";
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,EAAE;AACF,mGAAmG;AACnG,oGAAoG;AACpG,iGAAiG;AACjG,sBAAsB;AACtB,EAAE;AACF,mGAAmG;AACnG,yFAAyF;AACzF,wFAAwF;AACxF,8FAA8F;AAC9F,8FAA8F;AAC9F,8EAA8E;AAC9E,6FAA6F;AAC7F,EAAE;AACF,mDAAmD;AAEnD,OAAO,EAAE,aAAa,EAAyC,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True while the phone has no connection.
|
|
3
|
+
*
|
|
4
|
+
* React Query already pauses on its own once `installQueryManagers` from `@gusnips/native/query`
|
|
5
|
+
* is running. This is for a screen that should SAY it is offline instead of showing old data as
|
|
6
|
+
* if it were current.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useIsOffline(): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Was that failure the phone, or the server?
|
|
11
|
+
*
|
|
12
|
+
* React Native's `fetch` rejects with the same bare `TypeError` when the server is down and when
|
|
13
|
+
* the phone has no signal. Ask this before telling somebody the service is having trouble: only a
|
|
14
|
+
* `false` here means the service is to blame. A NetInfo that cannot answer counts as online, so
|
|
15
|
+
* the doubt goes to the message that has a retry beside it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isDeviceOffline(): Promise<boolean>;
|
|
18
|
+
//# sourceMappingURL=network.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"AAYA;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAItC;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CAGxD"}
|
package/dist/network.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the phone offline? NetInfo answers, with one rule both helpers here share.
|
|
3
|
+
*
|
|
4
|
+
* `isConnected === false` is offline, and nothing else is. NetInfo reports `null` while it has not
|
|
5
|
+
* worked it out yet, which happens at every launch; reading that as offline puts an offline banner
|
|
6
|
+
* on screen for the first moment of every session.
|
|
7
|
+
*/
|
|
8
|
+
import NetInfo, {} from "@react-native-community/netinfo";
|
|
9
|
+
import { useEffect, useState } from "react";
|
|
10
|
+
const offline = (state) => state.isConnected === false;
|
|
11
|
+
/**
|
|
12
|
+
* True while the phone has no connection.
|
|
13
|
+
*
|
|
14
|
+
* React Query already pauses on its own once `installQueryManagers` from `@gusnips/native/query`
|
|
15
|
+
* is running. This is for a screen that should SAY it is offline instead of showing old data as
|
|
16
|
+
* if it were current.
|
|
17
|
+
*/
|
|
18
|
+
export function useIsOffline() {
|
|
19
|
+
const [isOffline, setIsOffline] = useState(false);
|
|
20
|
+
useEffect(() => NetInfo.addEventListener((state) => setIsOffline(offline(state))), []);
|
|
21
|
+
return isOffline;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Was that failure the phone, or the server?
|
|
25
|
+
*
|
|
26
|
+
* React Native's `fetch` rejects with the same bare `TypeError` when the server is down and when
|
|
27
|
+
* the phone has no signal. Ask this before telling somebody the service is having trouble: only a
|
|
28
|
+
* `false` here means the service is to blame. A NetInfo that cannot answer counts as online, so
|
|
29
|
+
* the doubt goes to the message that has a retry beside it.
|
|
30
|
+
*/
|
|
31
|
+
export async function isDeviceOffline() {
|
|
32
|
+
const state = await NetInfo.fetch().catch(() => null);
|
|
33
|
+
return state !== null && offline(state);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=network.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.js","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,OAAO,EAAE,EAAqB,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE5C,MAAM,OAAO,GAAG,CAAC,KAAmB,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,KAAK,KAAK,CAAC;AAErE;;;;;;GAMG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClD,SAAS,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACvF,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IACtD,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC;AAC1C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gusnips-native-css.d.ts","sourceRoot":"","sources":["../../../src/node/bin/gusnips-native-css.ts"],"names":[],"mappings":""}
|