├── .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 | --------------------------------------------------------------------------------