├── .eslintrc.js
├── .gitignore
├── README.md
├── babel.config.js
├── build
├── index.d.ts
├── index.d.ts.map
├── index.js
├── index.js.map
├── useDrizzleStudio.d.ts
├── useDrizzleStudio.d.ts.map
├── useDrizzleStudio.js
└── useDrizzleStudio.js.map
├── expo-module.config.json
├── package-lock.json
├── package.json
├── src
├── index.ts
└── useDrizzleStudio.tsx
├── tsconfig.json
└── webui
├── App.tsx
├── app.json
├── babel.config.js
├── metro.config.js
├── package-lock.json
├── package.json
├── studio.js
└── tsconfig.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // @generated by expo-module-scripts
2 | module.exports = require('expo-module-scripts/eslintrc.base.js');
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Drizzle Studio for Expo SQLite
2 | Expo dev tools plugin for you to browse your Expo SQLite data with Drizzle Studio 🎉
3 |
4 | ### Get Started
5 | [Add Expo SQLite to your project](https://docs.expo.dev/versions/latest/sdk/sqlite/)
6 |
7 | Install `expo-drizzle-studio-plugin`
8 | ```shell
9 | npm i expo-drizzle-studio-plugin
10 | ```
11 |
12 | Setup Drizzle Studio plugin
13 | ```jsx
14 | import { useDrizzleStudio } from "expo-drizzle-studio-plugin";
15 | import * as SQLite from "expo-sqlite";
16 | import { View } from "react-native";
17 |
18 | const db = SQLite.openDatabaseSync("db");
19 |
20 | export default function App() {
21 | useDrizzleStudio(db);
22 |
23 | return ;
24 | }
25 | ```
26 |
27 | Run and Expo app on physical device, simulator or emulator which supports Expo SQLite. Web doesn't support SQLite.
28 | ```shell
29 | npx expo start
30 | ```
31 |
32 | Open devtools menu from the terminal with "start" process and choose `expo-drizzle-studio-plugin`
33 | ```
34 | shift + m
35 | ```
36 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | // @generated by expo-module-scripts
2 | module.exports = require('expo-module-scripts/babel.config.base');
3 |
--------------------------------------------------------------------------------
/build/index.d.ts:
--------------------------------------------------------------------------------
1 | import useDrizzleStudio from "./useDrizzleStudio";
2 | export { useDrizzleStudio };
3 | //# sourceMappingURL=index.d.ts.map
--------------------------------------------------------------------------------
/build/index.d.ts.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,gBAAgB,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
--------------------------------------------------------------------------------
/build/index.js:
--------------------------------------------------------------------------------
1 | import useDrizzleStudio from "./useDrizzleStudio";
2 | export { useDrizzleStudio };
3 | //# sourceMappingURL=index.js.map
--------------------------------------------------------------------------------
/build/index.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,gBAAgB,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,CAAC","sourcesContent":["import useDrizzleStudio from \"./useDrizzleStudio\";\nexport { useDrizzleStudio };\n"]}
--------------------------------------------------------------------------------
/build/useDrizzleStudio.d.ts:
--------------------------------------------------------------------------------
1 | import * as SQLite from "expo-sqlite";
2 | export default function useDrizzleStudio(db: SQLite.SQLiteDatabase | null): void;
3 | //# sourceMappingURL=useDrizzleStudio.d.ts.map
--------------------------------------------------------------------------------
/build/useDrizzleStudio.d.ts.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"useDrizzleStudio.d.ts","sourceRoot":"","sources":["../src/useDrizzleStudio.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAItC,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EAAE,EAAE,MAAM,CAAC,cAAc,GAAG,IAAI,QAqCxE"}
--------------------------------------------------------------------------------
/build/useDrizzleStudio.js:
--------------------------------------------------------------------------------
1 | import { useDevToolsPluginClient } from "expo/devtools";
2 | import { useEffect } from "react";
3 | export default function useDrizzleStudio(db) {
4 | const client = useDevToolsPluginClient("expo-drizzle-studio-plugin");
5 | const transferData = async (e) => {
6 | if (!db)
7 | return;
8 | try {
9 | const statement = await db.prepareAsync(e.sql);
10 | let executed;
11 | if (e.arrayMode) {
12 | executed = await statement.executeForRawResultAsync(e.params);
13 | }
14 | else {
15 | executed = await statement.executeAsync(e.params);
16 | }
17 | const data = await executed.getAllAsync();
18 | client?.sendMessage(`transferData-${e.id}`, { from: "app", data });
19 | }
20 | catch (error) {
21 | console.error(error);
22 | }
23 | };
24 | useEffect(() => {
25 | const subscriptions = [];
26 | subscriptions.push(client?.addMessageListener("getData", transferData));
27 | return () => {
28 | for (const subscription of subscriptions) {
29 | subscription?.remove();
30 | }
31 | };
32 | }, [client]);
33 | }
34 | //# sourceMappingURL=useDrizzleStudio.js.map
--------------------------------------------------------------------------------
/build/useDrizzleStudio.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"useDrizzleStudio.js","sourceRoot":"","sources":["../src/useDrizzleStudio.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElC,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EAAgC;IACrE,MAAM,MAAM,GAAG,uBAAuB,CAAC,4BAA4B,CAAC,CAAC;IAErE,MAAM,YAAY,GAAG,KAAK,EAAE,CAK3B,EAAE,EAAE;QACD,IAAI,CAAC,EAAE;YAAE,OAAO;QAChB,IAAI,CAAC;YACD,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YAC/C,IAAI,QAAQ,CAAC;YACb,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;gBACd,QAAQ,GAAG,MAAM,SAAS,CAAC,wBAAwB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAClE,CAAC;iBAAM,CAAC;gBACJ,QAAQ,GAAG,MAAM,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YACtD,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;YAC1C,MAAM,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACvE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC;IACL,CAAC,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACX,MAAM,aAAa,GAAU,EAAE,CAAC;QAEhC,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,kBAAkB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;QAExE,OAAO,GAAG,EAAE;YACR,KAAK,MAAM,YAAY,IAAI,aAAa,EAAE,CAAC;gBACvC,YAAY,EAAE,MAAM,EAAE,CAAC;YAC3B,CAAC;QACL,CAAC,CAAC;IACN,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;AACjB,CAAC","sourcesContent":["import * as SQLite from \"expo-sqlite\";\nimport { useDevToolsPluginClient } from \"expo/devtools\";\nimport { useEffect } from \"react\";\n\nexport default function useDrizzleStudio(db: SQLite.SQLiteDatabase | null) {\n const client = useDevToolsPluginClient(\"expo-drizzle-studio-plugin\");\n\n const transferData = async (e: {\n sql: string;\n params: (string | number)[];\n arrayMode: boolean;\n id: string;\n }) => {\n if (!db) return;\n try {\n const statement = await db.prepareAsync(e.sql);\n let executed;\n if (e.arrayMode) {\n executed = await statement.executeForRawResultAsync(e.params);\n } else {\n executed = await statement.executeAsync(e.params);\n }\n\n const data = await executed.getAllAsync();\n client?.sendMessage(`transferData-${e.id}`, { from: \"app\", data });\n } catch (error) {\n console.error(error);\n }\n };\n\n useEffect(() => {\n const subscriptions: any[] = [];\n\n subscriptions.push(client?.addMessageListener(\"getData\", transferData));\n\n return () => {\n for (const subscription of subscriptions) {\n subscription?.remove();\n }\n };\n }, [client]);\n}\n"]}
--------------------------------------------------------------------------------
/expo-module.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "platforms": ["devtools"],
3 | "devtools": {
4 | "webpageRoot": "dist"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "expo-drizzle-studio-plugin",
3 | "version": "0.1.1",
4 | "description": "A new DevTools plugin created by create-dev-plugin",
5 | "main": "build/index.js",
6 | "types": "build/index.d.ts",
7 | "sideEffects": false,
8 | "scripts": {
9 | "build": "expo-module build",
10 | "pack": "npm pack",
11 | "build:all": "expo-module prepare && cd webui && npx expo export -p web --output-dir ../dist",
12 | "clean": "expo-module clean",
13 | "prepare": "expo-module prepare",
14 | "prepublishOnly": "expo-module prepare && expo-module prepublishOnly && cd webui && npx expo export -p web --output-dir ../dist",
15 | "web:dev": "cd webui && npx expo start -w",
16 | "web:export": "cd webui && npx expo export -p web --output-dir ../dist"
17 | },
18 | "keywords": [
19 | "expo",
20 | "devtools"
21 | ],
22 | "files": [
23 | "build",
24 | "dist",
25 | "expo-module.config.json"
26 | ],
27 | "license": "MIT",
28 | "dependencies": {
29 | "expo-sqlite": "^15.0.3"
30 | },
31 | "devDependencies": {
32 | "expo": "~52.0.0",
33 | "expo-module-scripts": "^3.1.0",
34 | "typescript": "^5.1.3"
35 | },
36 | "peerDependencies": {
37 | "expo": "*"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | const useDrizzleStudio =
2 | process.env.NODE_ENV !== "production"
3 | ? require("./useDrizzleStudio").default
4 | : () => {};
5 |
6 | export { useDrizzleStudio };
7 |
--------------------------------------------------------------------------------
/src/useDrizzleStudio.tsx:
--------------------------------------------------------------------------------
1 | import * as SQLite from "expo-sqlite";
2 | import { useDevToolsPluginClient } from "expo/devtools";
3 | import { useEffect } from "react";
4 |
5 | export default function useDrizzleStudio(db: SQLite.SQLiteDatabase | null) {
6 | const client = useDevToolsPluginClient("expo-drizzle-studio-plugin");
7 |
8 | const transferData = async (e: {
9 | sql: string;
10 | params: (string | number)[];
11 | arrayMode: boolean;
12 | id: string;
13 | }) => {
14 | if (!db) return;
15 | try {
16 | const statement = await db.prepareAsync(e.sql);
17 | let executed;
18 | if (e.arrayMode) {
19 | executed = await statement.executeForRawResultAsync(e.params);
20 | } else {
21 | executed = await statement.executeAsync(e.params);
22 | }
23 |
24 | const data = await executed.getAllAsync();
25 | client?.sendMessage(`transferData-${e.id}`, { from: "app", data });
26 | } catch (error) {
27 | console.error(error);
28 | }
29 | };
30 |
31 | useEffect(() => {
32 | const subscriptions: any[] = [];
33 |
34 | subscriptions.push(client?.addMessageListener("getData", transferData));
35 |
36 | return () => {
37 | for (const subscription of subscriptions) {
38 | subscription?.remove();
39 | }
40 | };
41 | }, [client, db]);
42 | }
43 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | // @generated by expo-module-scripts
2 | {
3 | "extends": "expo-module-scripts/tsconfig.base",
4 | "compilerOptions": {
5 | "outDir": "./build"
6 | },
7 | "include": ["./src"],
8 | "exclude": ["**/__mocks__/*", "**/__tests__/*"]
9 | }
10 |
--------------------------------------------------------------------------------
/webui/App.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DevToolsPluginClient,
3 | useDevToolsPluginClient,
4 | type EventSubscription
5 | } from "expo/devtools";
6 | import React, { ComponentPropsWithoutRef, useEffect, useRef } from "react";
7 | import ReactDOM from "react-dom";
8 | import ReactDOMClient from "react-dom/client";
9 | import { View } from "react-native";
10 | // @ts-ignore
11 | import StudioScript from "./studio.js";
12 |
13 | declare global {
14 | interface Window {
15 | client: DevToolsPluginClient;
16 | }
17 | namespace JSX {
18 | interface IntrinsicElements {
19 | "drizzle-studio": ComponentPropsWithoutRef<"div"> & {
20 | "ref": React.RefObject<{
21 | reset: () => void;
22 | }>;
23 | "css-variables": string;
24 | };
25 | }
26 | }
27 | }
28 |
29 | const drizzleStudioFunc = new Function("React", "ReactDOM", "ReactDOMClient", StudioScript);
30 |
31 | const lightCssVariables = {
32 | "--page-background": "#F0F0F0",
33 | "--background": "#FFFFFF",
34 | "--foreground": "#171717",
35 | "--popover": "#FFFFFF",
36 | "--popover-foreground": "#171717",
37 | "--primary": "#0A0A0A",
38 | "--primary-foreground": "#EAEAEA",
39 | "--secondary": "#F0F0F0",
40 | "--secondary-foreground": "#171717",
41 | "--muted": "#F8F8F8",
42 | "--muted-foreground": "#454545",
43 | "--accent": "#F0F0F0",
44 | "--accent-foreground": "#0A0A0A",
45 | "--destructive": "#D93035",
46 | "--destructive-foreground": "#FEEBEE",
47 | "--border": "#EBEBEB",
48 | "--input": "#EBEBEB",
49 | "--number-color": "#0068D6",
50 | "--property-name": "#171717",
51 | "--cursor": "#171717",
52 | "--line-number": "#A8A8A8",
53 | "--string-value": "#297A3A",
54 | "--boolean-value": "#7820BC",
55 | "--highlight-background": "#F0F0F070",
56 | "--editor-background": "#FFFFFF",
57 | "--selection": "#F0F0F0",
58 | "--fold-gutter-hover": "#D6D9E4",
59 | "--sql-variable-color": "#171717",
60 | "--sql-method-color": "#0068D6",
61 | "--sql-property-color": "#171717",
62 | "--sql-number-value-color": "#BD2864",
63 | "--sql-string-value-color": "#297A3A",
64 | "--sql-boolean-value-color": "#BD2864",
65 | "--editor-font": "Menlo",
66 | "--table-font": "Roboto Mono",
67 | "--interface-font": "Open Sans",
68 | "--filters-toolbar": "#F8F8F8",
69 | "--edit": "#FFF6E5",
70 | "--edit-foreground": "#A45200",
71 | "--submit": "#46A557",
72 | "--submit-foreground": "#EFFBEF",
73 | "--radius": "0.5rem",
74 | "--checkbox-radius": "2px",
75 | "--filter-icon-set": "default",
76 | "--sort-icon-set": "default",
77 | "--download-icon-set": "default",
78 | "--theme-icon-set": "default",
79 | "--chevron-icon-set": "default",
80 | "--refresh-icon-set": "default",
81 | "--table-icon-set": "default",
82 | "--sidebar-icon-set": "default",
83 | "--sql-runner-icon-set": "default"
84 | };
85 |
86 | export default function App() {
87 | const client = useDevToolsPluginClient("expo-drizzle-studio-plugin");
88 | const studioRef = useRef<{
89 | reset: () => void;
90 | }>(null);
91 |
92 | useEffect(() => {
93 | if (client) {
94 | window.client = client;
95 | drizzleStudioFunc(React, ReactDOM, ReactDOMClient);
96 | }
97 |
98 | return () => {
99 | // reset the studio when the component unmounts
100 | if (studioRef.current?.reset) {
101 | studioRef.current.reset();
102 | }
103 | };
104 | }, [client]);
105 |
106 | // add the Roboto Mono font to the page
107 | useEffect(() => {
108 | const existingLink = document.querySelector('link[data-font="Roboto Mono"]');
109 |
110 | if (existingLink) {
111 | return;
112 | }
113 |
114 | const fontLink = document.createElement("link");
115 | fontLink.rel = "stylesheet";
116 | fontLink.href = `https://fonts.googleapis.com/css?family=Roboto+Mono:wght@100;200;300;400;500;600;700;800;900&display=swap`;
117 | fontLink.setAttribute("data-font", "Roboto Mono");
118 | document.head.appendChild(fontLink);
119 | }, []);
120 |
121 | useEffect(() => {
122 | const subscriptions: EventSubscription[] = [];
123 |
124 | return () => {
125 | for (const subscription of subscriptions) {
126 | subscription?.remove();
127 | }
128 | };
129 | }, [client]);
130 |
131 | return (
132 |
140 |
148 |
149 | );
150 | }
151 |
--------------------------------------------------------------------------------
/webui/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "web": {
4 | "bundler": "metro"
5 | },
6 | "experiments": {
7 | "baseUrl": "/_expo/plugins/expo-drizzle-studio-plugin"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/webui/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ["babel-preset-expo"],
5 | plugins: [["inline-import", { extensions: [".js"] }]]
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/webui/metro.config.js:
--------------------------------------------------------------------------------
1 | const { getDefaultConfig } = require("expo/metro-config");
2 | /** @type {import('expo/metro-config').MetroConfig} */
3 | const config = getDefaultConfig(__dirname);
4 | config.resolver.sourceExts.push("js"); // <--- add this
5 | module.exports = config;
6 |
--------------------------------------------------------------------------------
/webui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "expo-drizzle-studio-plugin-webui",
3 | "version": "0.1.1-private",
4 | "main": "expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "@expo/metro-runtime": "~4.0.0",
13 | "babel-plugin-inline-import": "^3.0.0",
14 | "expo": "~52.0.0",
15 | "expo-status-bar": "~2.0.0",
16 | "react": "18.3.1",
17 | "react-dom": "18.3.1",
18 | "react-native": "0.76.2",
19 | "react-native-web": "~0.19.13"
20 | },
21 | "devDependencies": {
22 | "@babel/core": "^7.20.0",
23 | "@types/react": "~18.3.12",
24 | "typescript": "^5.1.3"
25 | },
26 | "private": true
27 | }
28 |
--------------------------------------------------------------------------------
/webui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 |
4 | "compilerOptions": {
5 | "target": "ES2020",
6 | "useDefineForClassFields": true,
7 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
8 | "module": "ESNext",
9 | "skipLibCheck": true,
10 |
11 | /* Bundler mode */
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 |
19 | /* Linting */
20 | "strict": true,
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "noFallthroughCasesInSwitch": true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------