├── .gitignore ├── .prettierrc.json ├── README.md ├── apps ├── backend │ ├── .env.example │ ├── .gitignore │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── createContext.ts │ │ ├── createRouter.ts │ │ ├── index.ts │ │ ├── main.ts │ │ ├── router.ts │ │ └── types.ts │ └── tsconfig.json └── client │ ├── .env │ ├── .env.example │ ├── .gitignore │ ├── .prettierrc.json │ ├── README.md │ ├── index.html │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ ├── App.css │ ├── App.tsx │ ├── Transformer │ │ ├── TransformerPage.tsx │ │ ├── hashCode.tsx │ │ ├── hooks.tsx │ │ ├── store.tsx │ │ ├── tsDefaultValue.ts │ │ └── useTransformerMutation.tsx │ ├── components │ │ └── Show.tsx │ ├── favicon.svg │ ├── index.css │ ├── index.html │ ├── logo.svg │ ├── main.tsx │ ├── trpc.ts │ ├── utils │ │ ├── createContextWithHook.ts │ │ ├── debounce.tsx │ │ ├── toasts.ts │ │ └── useSyncRef.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "arrowParens": "always", 5 | "useTabs": false, 6 | "semi": true, 7 | "singleQuote": false, 8 | "trailingComma": "es5" 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Transformer 🪄 2 | 3 | ![Screenshot 2022-05-03 at 00 46 40](https://user-images.githubusercontent.com/47224540/166338700-e1a749d8-671a-41f8-ae46-d3fda4dfea5d.png) 4 | 5 | 6 | ## from TS to : 7 | 8 | - [x] JSON Schema 9 | - [x] OpenAPI 10 | - [x] Zod 11 | 12 | ## from OpenAPI to : 13 | 14 | - [x] Typescript 15 | - [x] JSON Schema 16 | - [x] Zod 17 | 18 | ## from JSON Schema to : 19 | 20 | - [x] Typescript 21 | - [x] OpenAPI 22 | - [x] Zod 23 | 24 | ### Stack 25 | 26 | - [tRPC](https://trpc.io/) for client-server communication 27 | - [typeconv](https://github.com/grantila/typeconv) for most `TS to X` conversions 28 | - [pnpm](https://pnpm.io/) as packages + workspaces manager 29 | - [turborepo](https://turborepo.org/) for monorepo setup 30 | - [Front template](https://github.com/astahmer/vite-react-chakra-ts) 31 | -------------------------------------------------------------------------------- /apps/backend/.env.example: -------------------------------------------------------------------------------- 1 | PORT=5000 2 | -------------------------------------------------------------------------------- /apps/backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | dist 5 | -------------------------------------------------------------------------------- /apps/backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts,json", 4 | "exec": "pnpm dev-start" 5 | } 6 | -------------------------------------------------------------------------------- /apps/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@transformer/backend", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev-start": "esno -r tsconfig-paths/register ./src/main.ts", 6 | "dev": "nodemon", 7 | "prebuild": "rimraf dist && rimraf temp", 8 | "build": "pnpm prebuild && tsc", 9 | "start": "NODE_ENV=production node dist/main.js" 10 | }, 11 | "devDependencies": { 12 | "@types/js-yaml": "^4.0.5", 13 | "@types/node": "^17.0.15", 14 | "esno": "^0.14.0", 15 | "nodemon": "^2.0.15", 16 | "rimraf": "^3.0.2", 17 | "tsconfig-paths": "^3.12.0" 18 | }, 19 | "dependencies": { 20 | "@anatine/zod-openapi": "^1.3.0", 21 | "@pastable/core": "^0.1.19", 22 | "@trpc/server": "^9.18.0", 23 | "dotenv": "^16.0.0", 24 | "js-yaml": "^4.1.0", 25 | "ts-morph": "^13.0.3", 26 | "ts-to-zod": "^1.8.0", 27 | "tsutils": "^3.21.0", 28 | "typeconv": "^1.7.0", 29 | "zod": "^3.11.6", 30 | "zod-to-json-schema": "^3.14.0", 31 | "zod-to-ts": "^1.0.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/backend/src/createContext.ts: -------------------------------------------------------------------------------- 1 | import * as trpc from "@trpc/server"; 2 | import * as trpcNext from "@trpc/server/adapters/next"; 3 | 4 | /** 5 | * Creates context for an incoming request 6 | * @link https://trpc.io/docs/context 7 | */ 8 | export const createContext = async ({ req, res }: trpcNext.CreateNextContextOptions) => { 9 | // for API-response caching see https://trpc.io/docs/caching 10 | return { 11 | req, 12 | res, 13 | }; 14 | }; 15 | 16 | export type Context = trpc.inferAsyncReturnType; 17 | -------------------------------------------------------------------------------- /apps/backend/src/createRouter.ts: -------------------------------------------------------------------------------- 1 | import * as trpc from "@trpc/server"; 2 | import { Context } from "./createContext"; 3 | 4 | /** 5 | * Helper function to create a router with context 6 | */ 7 | export function createRouter() { 8 | return trpc.router(); 9 | } 10 | -------------------------------------------------------------------------------- /apps/backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { inferProcedureOutput, inferProcedureInput, inferSubscriptionOutput } from "@trpc/server"; 2 | import type { AppRouter } from "./router"; 3 | 4 | export * from "./types"; 5 | export type { AppRouter } from "./router"; 6 | 7 | /** 8 | * Enum containing all api mutation paths 9 | */ 10 | export type TMutation = keyof AppRouter["_def"]["mutations"]; 11 | 12 | export type InferMutationInput = inferProcedureInput< 13 | AppRouter["_def"]["mutations"][TRouteKey] 14 | >; 15 | -------------------------------------------------------------------------------- /apps/backend/src/main.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | 3 | import { createHTTPHandler } from "@trpc/server/adapters/standalone"; 4 | import http from "http"; 5 | import { createContext } from "./createContext"; 6 | import { appRouter } from "./router"; 7 | 8 | const port = process.env.PORT || 5000; 9 | console.log("starting backend on port", port); 10 | const handler = createHTTPHandler({ router: appRouter, createContext }); 11 | 12 | const server = http.createServer((req, res) => { 13 | // Set CORS headers 14 | res.setHeader("Access-Control-Allow-Origin", "*"); 15 | res.setHeader("Access-Control-Request-Method", "*"); 16 | res.setHeader("Access-Control-Allow-Methods", "OPTIONS, GET"); 17 | res.setHeader("Access-Control-Allow-Headers", "*"); 18 | if (req.method === "OPTIONS") { 19 | res.writeHead(200); 20 | res.end(); 21 | return; 22 | } 23 | handler(req, res); 24 | }); 25 | 26 | server.listen(port); 27 | console.log("listening on", port); 28 | -------------------------------------------------------------------------------- /apps/backend/src/router.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceDeclaration, Project, TypeAliasDeclaration } from "ts-morph"; 2 | import { generate } from "ts-to-zod"; 3 | import { 4 | getJsonSchemaReader, 5 | getJsonSchemaWriter, 6 | getOpenApiReader, 7 | getOpenApiWriter, 8 | getTypeScriptReader, 9 | getTypeScriptWriter, 10 | makeConverter, 11 | } from "typeconv"; 12 | import { z } from "zod"; 13 | import { createRouter } from "./createRouter"; 14 | import { getRandomString } from "@pastable/core"; 15 | import { load as loadYaml } from "js-yaml"; 16 | 17 | const tsReader = getTypeScriptReader(); 18 | const tsWriter = getTypeScriptWriter({}); 19 | 20 | const jsonSchemaReader = getJsonSchemaReader(); 21 | const jsonSchemaWriter = getJsonSchemaWriter(); 22 | 23 | const openApiReader = getOpenApiReader(); 24 | 25 | const tsToJsonSchema = makeConverter(tsReader, jsonSchemaWriter); 26 | const jsonSchemaToTs = makeConverter(jsonSchemaReader, tsWriter); 27 | 28 | const openApiToTs = makeConverter(openApiReader, tsWriter); 29 | const openApiToJsonSchema = makeConverter(openApiReader, jsonSchemaWriter); 30 | 31 | const project = new Project({ 32 | useInMemoryFileSystem: true, 33 | skipLoadingLibFiles: true, 34 | compilerOptions: { 35 | skipLibCheck: true, 36 | noLib: true, 37 | skipDefaultLibCheck: true, 38 | noResolve: true, 39 | }, 40 | }); 41 | 42 | // OpenAPIWriterOptions["schemaVersion"] 43 | const openApiSchemaVersionSchema = z.union([ 44 | z.literal("3.0.3"), 45 | z.literal("3.0.2"), 46 | z.literal("3.0.1"), 47 | z.literal("3.0.0"), 48 | z.literal("3.0.0-rc2"), 49 | z.literal("3.0.0-rc1"), 50 | z.literal("3.0.0-rc0"), 51 | z.literal("2.0"), 52 | z.literal("1.2"), 53 | z.literal("1.1"), 54 | z.literal("1.0"), 55 | ]); 56 | 57 | const toOpenApiSchema = z.object({ 58 | value: z.string(), 59 | format: z.union([z.literal("json"), z.literal("yaml")]), 60 | schemaVersion: openApiSchemaVersionSchema.default("3.0.3"), 61 | }); 62 | 63 | export const appRouter = createRouter() 64 | .mutation("tsToZod", { 65 | input: z.string(), 66 | async resolve({ input }) { 67 | const ts = getTransformedTs(input); 68 | const result = generate({ sourceText: ts }); 69 | 70 | const zodResult = result.getZodSchemasFile("./schema"); 71 | return zodResult; 72 | }, 73 | }) 74 | .mutation("tsToOapi", { 75 | input: toOpenApiSchema, 76 | async resolve({ input }) { 77 | const oapiWriter = getOpenApiWriter({ 78 | title: "My API", 79 | version: "v1", 80 | format: input.format, 81 | schemaVersion: input.schemaVersion, 82 | }); 83 | const tsToOapi = makeConverter(tsReader, oapiWriter); 84 | 85 | const ts = getTransformedTs(input.value); 86 | const result = await tsToOapi.convert({ data: ts }); 87 | return result.data; 88 | }, 89 | }) 90 | .mutation("tsToJsonSchema", { 91 | input: z.string(), 92 | async resolve({ input }) { 93 | const ts = getTransformedTs(input); 94 | const result = await tsToJsonSchema.convert({ data: ts }); 95 | return result.data; 96 | }, 97 | }) 98 | .mutation("jsonSchemaToTs", { 99 | input: z.string(), 100 | async resolve({ input }) { 101 | return (await jsonSchemaToTs.convert({ data: input })).data; 102 | }, 103 | }) 104 | .mutation("jsonSchemaToZod", { 105 | input: z.string(), 106 | async resolve({ input }) { 107 | const ts = (await jsonSchemaToTs.convert({ data: input })).data; 108 | 109 | const result = generate({ sourceText: getTransformedTs(ts) }); 110 | 111 | const zodResult = result.getZodSchemasFile("./schema"); 112 | return zodResult; 113 | }, 114 | }) 115 | .mutation("jsonSchemaToOpenApi", { 116 | input: toOpenApiSchema, 117 | async resolve({ input }) { 118 | const oapiWriter = getOpenApiWriter({ 119 | title: "My API", 120 | version: "v1", 121 | format: input.format, 122 | schemaVersion: input.schemaVersion, 123 | }); 124 | const jsonSchemaToOpenApi = makeConverter(jsonSchemaReader, oapiWriter); 125 | const result = await jsonSchemaToOpenApi.convert({ data: input.value }); 126 | return result.data; 127 | }, 128 | }) 129 | .mutation("openApiToTs", { 130 | input: toOpenApiSchema, 131 | async resolve({ input }) { 132 | const data = input.format === "json" ? input.value : JSON.stringify(loadYaml(input.value)); 133 | return (await openApiToTs.convert({ data })).data; 134 | }, 135 | }) 136 | .mutation("openApiToJsonSchema", { 137 | input: toOpenApiSchema, 138 | async resolve({ input }) { 139 | const data = input.format === "json" ? input.value : JSON.stringify(loadYaml(input.value)); 140 | return (await openApiToJsonSchema.convert({ data })).data; 141 | }, 142 | }) 143 | .mutation("openApiToZod", { 144 | input: toOpenApiSchema, 145 | async resolve({ input }) { 146 | const data = input.format === "json" ? input.value : JSON.stringify(loadYaml(input.value)); 147 | const ts = (await openApiToTs.convert({ data })).data; 148 | 149 | const result = generate({ sourceText: getTransformedTs(ts) }); 150 | 151 | const zodResult = result.getZodSchemasFile("./schema"); 152 | return zodResult; 153 | }, 154 | }); 155 | 156 | export type AppRouter = typeof appRouter; 157 | 158 | const autoTransformTs = (node: InterfaceDeclaration | TypeAliasDeclaration) => { 159 | node.setIsExported(true); 160 | node.getTypeParameters().forEach((t) => t.remove()); 161 | }; 162 | 163 | /** Strip out generics, auto-exports interfaces */ 164 | function getTransformedTs(input: string) { 165 | const tsFile = project.createSourceFile(getRandomString() + ".ts", input); 166 | 167 | tsFile.getTypeAliases().forEach(autoTransformTs); 168 | tsFile.getInterfaces().forEach(autoTransformTs); 169 | 170 | tsFile.saveSync(); 171 | const ts = tsFile.getText(); 172 | tsFile.deleteImmediatelySync(); 173 | 174 | return ts; 175 | } 176 | -------------------------------------------------------------------------------- /apps/backend/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { CompilerOptions } from "ts-morph"; 2 | import type { OpenAPIWriterOptions } from "typeconv"; 3 | export { CompilerOptions, OpenAPIWriterOptions }; 4 | -------------------------------------------------------------------------------- /apps/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "module": "commonjs", 5 | "lib": ["es6"], 6 | "target": "es2016", 7 | "declaration": true, 8 | "removeComments": true, 9 | "noImplicitAny": false, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "esModuleInterop": true, 13 | "strict": true, 14 | "allowJs": true, 15 | "strictNullChecks": false, 16 | "forceConsistentCasingInFileNames": true, 17 | "baseUrl": "./src", 18 | "skipLibCheck": true, 19 | "paths": { 20 | "@/*": ["./*"] 21 | } 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /apps/client/.env: -------------------------------------------------------------------------------- 1 | VITE_BACKEND_URL="http://localhost:5123" 2 | -------------------------------------------------------------------------------- /apps/client/.env.example: -------------------------------------------------------------------------------- 1 | VITE_BACKEND_URL="http://localhost:5000" 2 | -------------------------------------------------------------------------------- /apps/client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /apps/client/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "arrowParens": "always", 5 | "useTabs": false, 6 | "semi": true, 7 | "singleQuote": false, 8 | "trailingComma": "es5" 9 | } 10 | -------------------------------------------------------------------------------- /apps/client/README.md: -------------------------------------------------------------------------------- 1 | Front SPA with: 2 | 3 | - Framework: [React](https://github.com/facebook/react) 17.0.2 / Typescript 4.3+ 4 | - Dev server / builder: [Vite](https://github.com/vitejs/vite/) 5 | - Router: [React-Router](https://github.com/ReactTraining/react-router/) 6 | - State-management: Global state with 7 | [Jotai](https://github.com/pmndrs/jotai/) + Complex state with 8 | [XState](https://github.com/statelyai/xstate) 9 | - API: [axios](https://github.com/axios/axios) + 10 | [react-query](https://github.com/tannerlinsley/react-query) 11 | - Forms: [React-Hook-Form](https://github.com/react-hook-form/react-hook-form/) 12 | - CSS / Styling: CSS-in-JS using 13 | [Chakra-UI](https://github.com/chakra-ui/chakra-ui) with `Box` etc 14 | - Websockets: 15 | [native WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) 16 | -------------------------------------------------------------------------------- /apps/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Transformer 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@transformer/client", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "tsc && vite build", 7 | "serve": "vite preview" 8 | }, 9 | "dependencies": { 10 | "@babel/core": "^7.17.0", 11 | "@chakra-ui/icons": "^1.1.5", 12 | "@chakra-ui/react": "^1.8.1", 13 | "@emotion/react": "^11.7.1", 14 | "@emotion/styled": "^11.6.0", 15 | "@monaco-editor/react": "^4.3.1", 16 | "@pastable/core": "^0.1.19", 17 | "@trpc/client": "^9.18.0", 18 | "@trpc/react": "^9.18.0", 19 | "@xstate/react": "^2.0.0", 20 | "axios": "^0.25.0", 21 | "framer-motion": "^6.2.3", 22 | "jotai": "^1.5.3", 23 | "monaco-editor": "^0.32.0", 24 | "pino-pretty": "^7.5.1", 25 | "react": "^17.0.2", 26 | "react-dom": "^17.0.2", 27 | "react-hook-form": "^7.25.3", 28 | "react-icons": "^4.3.1", 29 | "react-query": "^3.34.12", 30 | "react-router-dom": "^6.2.1", 31 | "valtio": "^1.2.11", 32 | "xstate": "^4.29.0" 33 | }, 34 | "devDependencies": { 35 | "@transformer/backend": "workspace:^0.0.0", 36 | "@types/react": "^17.0.39", 37 | "@types/react-dom": "^17.0.11", 38 | "@vitejs/plugin-react": "^1.1.4", 39 | "prettier": "^2.5.1", 40 | "prettier-plugin-sorted": "^2.0.0", 41 | "typescript": "^4.5.5", 42 | "vite": "^2.7.13", 43 | "vite-plugin-pwa": "^0.11.13" 44 | }, 45 | "importSort": { 46 | ".js, jsx, .ts, .tsx": { 47 | "options": { 48 | "ignoreTsConfig": true, 49 | "extraAliases": [ 50 | "@/*" 51 | ], 52 | "bottomAliases": [ 53 | "/assets" 54 | ] 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /apps/client/src/App.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /apps/client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | 3 | import { ChakraProvider, Flex, extendTheme } from "@chakra-ui/react"; 4 | import { QueryClient, QueryClientProvider } from "react-query"; 5 | 6 | import { BrowserRouter, Route, Routes } from "react-router-dom"; 7 | import { TransformerPage } from "./Transformer/TransformerPage"; 8 | import { trpc } from "./trpc"; 9 | 10 | const queryClient = new QueryClient(); 11 | const trpcClient = trpc.createClient({ url: (import.meta.env.VITE_BACKEND_URL as string) || "http://localhost:5000" }); 12 | 13 | const theme = extendTheme({ config: { initialColorMode: "light" } }); 14 | 15 | function App() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | } /> 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /apps/client/src/Transformer/TransformerPage.tsx: -------------------------------------------------------------------------------- 1 | import { CloseIcon, EditIcon, InfoIcon, MoonIcon, SunIcon } from "@chakra-ui/icons"; 2 | import { 3 | Box, 4 | Button, 5 | Checkbox, 6 | CheckboxGroup, 7 | Divider, 8 | Heading, 9 | IconButton, 10 | Radio, 11 | RadioGroup, 12 | Select, 13 | SimpleGrid, 14 | Spinner, 15 | Stack, 16 | Text, 17 | Tooltip, 18 | useColorMode, 19 | useColorModeValue, 20 | } from "@chakra-ui/react"; 21 | import Editor from "@monaco-editor/react"; 22 | import { OpenAPIWriterOptions } from "@transformer/backend/src"; 23 | import { useAtom } from "jotai"; 24 | import { useAtomValue } from "jotai/utils"; 25 | import { useRef, useState } from "react"; 26 | import { useSnapshot } from "valtio"; 27 | import { ref } from "valtio/vanilla"; 28 | import { Show } from "../components/Show"; 29 | import { 30 | useJsonSchemaToOpenApi, 31 | useJsonSchemaToTs, 32 | useJsonSchemaToZod, 33 | useOpenApiToJsonSchema, 34 | useOpenApiToTs, 35 | useOpenApiToZod, 36 | useTsToJsonSchema, 37 | useTsToOapi, 38 | useTsToZod, 39 | } from "./hooks"; 40 | import { 41 | clearTexts, 42 | currentOpenApiProxy, 43 | destinationsAtom, 44 | editorRefs, 45 | openApiSchemaVersions, 46 | OutputDestination, 47 | resetToDefault, 48 | sourceOptions, 49 | textsProxy, 50 | } from "./store"; 51 | import { useTransformerMutation } from "./useTransformerMutation"; 52 | 53 | export function TransformerPage() { 54 | const tsToOapi = useTsToOapi(); 55 | const tsToJsonSchema = useTsToJsonSchema(); 56 | const tsToZod = useTsToZod(); 57 | 58 | const openApiToTs = useOpenApiToTs(); 59 | const openApiToJsonSchema = useOpenApiToJsonSchema(); 60 | const openApiToZod = useOpenApiToZod(); 61 | 62 | const jsonSchemaToTs = useJsonSchemaToTs(); 63 | const jsonSchemaToOpenApi = useJsonSchemaToOpenApi(); 64 | const jsonSchemaToZod = useJsonSchemaToZod(); 65 | 66 | const destinations = useAtomValue(destinationsAtom); 67 | 68 | useTransformerMutation({ 69 | tsToOapi, 70 | tsToJsonSchema, 71 | tsToZod, 72 | openApiToTs, 73 | openApiToJsonSchema, 74 | openApiToZod, 75 | jsonSchemaToTs, 76 | jsonSchemaToOpenApi, 77 | jsonSchemaToZod, 78 | }); 79 | 80 | return ( 81 | 82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | ); 97 | } 98 | 99 | function Header() { 100 | const [destinations, setDestinations] = useAtom(destinationsAtom); 101 | const { colorMode, toggleColorMode } = useColorMode(); 102 | 103 | const texts = useSnapshot(textsProxy); 104 | const source = texts.source; 105 | 106 | return ( 107 | 108 | 109 | 110 | 121 | 122 | 123 | 124 | 125 | setDestinations(v as OutputDestination[])} 130 | > 131 | 132 | 133 | Typescript 134 | 135 | 136 | JSON Schema 137 | 138 | 139 | OpenAPI 140 | 141 | 142 | Zod 143 | 144 | 145 | 146 | 147 | 148 | : } 151 | onClick={toggleColorMode} 152 | /> 153 | 154 | Please do NOT use complex TS Generics 155 | 156 | 157 | 158 | 159 | 160 | 161 | ); 162 | } 163 | 164 | function TypescriptColumn() { 165 | const editorText = useRef(textsProxy.ts); 166 | const texts = useSnapshot(textsProxy); 167 | const isReadOnly = texts.source !== "ts"; 168 | 169 | return ( 170 | 171 | (textsProxy.source = "ts")} 177 | > 178 | Typescript 179 | 180 | 181 | { 188 | editorRefs.ts = ref(editorRef!); 189 | editorRefs.monaco = monaco!; 190 | }} 191 | onChange={(value) => { 192 | editorText.current = value!; 193 | 194 | const model = editorRefs.ts!.getModel(); 195 | if (model) { 196 | const markers = editorRefs.monaco!.editor.getModelMarkers({ 197 | resource: model.uri, 198 | }); 199 | // = if no errors 200 | if (!markers.length) { 201 | textsProxy.ts = editorText.current; 202 | } 203 | } 204 | }} 205 | onValidate={(markers) => { 206 | if (markers.length) { 207 | return console.warn("TS Errors", markers); 208 | } 209 | // = if no errors 210 | textsProxy.ts = editorText.current; 211 | }} 212 | /> 213 | 214 | ); 215 | } 216 | 217 | function JsonSchemaColumn({ isLoading }: { isLoading: boolean }) { 218 | const editorText = useRef(textsProxy.jsonSchema); 219 | const texts = useSnapshot(textsProxy); 220 | const isReadOnly = texts.source !== "jsonSchema"; 221 | 222 | return ( 223 | 224 | 225 | (textsProxy.source = "jsonSchema")} 231 | > 232 | JSON Schema 233 | 234 | 235 | 236 | 237 | 238 | 239 | { 245 | editorRefs.jsonSchema = ref!; 246 | ref.setValue(textsProxy.jsonSchema); 247 | }} 248 | onChange={(value) => { 249 | editorText.current = value!; 250 | 251 | const model = editorRefs.jsonSchema!.getModel(); 252 | if (model) { 253 | const markers = editorRefs.monaco!.editor.getModelMarkers({ 254 | resource: model.uri, 255 | }); 256 | // = if no errors 257 | if (!markers.length) { 258 | textsProxy.jsonSchema = editorText.current; 259 | } 260 | } 261 | }} 262 | onValidate={(markers) => { 263 | if (markers.length) { 264 | return console.warn("JSON Errors", markers); 265 | } 266 | // = if no errors 267 | textsProxy.jsonSchema = editorText.current; 268 | }} 269 | /> 270 | 271 | ); 272 | } 273 | 274 | function OpenApiColumn({ isLoading }: { isLoading: boolean }) { 275 | const [isEditingOpenApi, setIsEditingOpenApi] = useState(false); 276 | const currentOpenApi = useSnapshot(currentOpenApiProxy); 277 | 278 | const editorText = useRef(textsProxy.openApi); 279 | const texts = useSnapshot(textsProxy); 280 | const isReadOnly = texts.source !== "openApi"; 281 | 282 | return ( 283 | 284 | 285 | 286 | (textsProxy.source = "openApi")} 292 | > 293 | OpenAPI 294 | 295 | 296 | {currentOpenApi.schemaVersion} ({currentOpenApi.format}) 297 | 298 | (isEditingOpenApi ? setIsEditingOpenApi(false) : setIsEditingOpenApi(true))} 300 | size="sm" 301 | icon={isEditingOpenApi ? : } 302 | aria-label="Edit" 303 | /> 304 | 305 | 306 | 307 | 308 | 309 | 310 | (currentOpenApiProxy.format = v as OpenAPIWriterOptions["format"])} 313 | > 314 | 315 | JSON 316 | YAML 317 | 318 | 319 | 334 | 335 | 336 | 337 | 338 | { 345 | editorRefs.openApi = ref!; 346 | ref.setValue(textsProxy.openApi); 347 | }} 348 | onChange={(value) => { 349 | editorText.current = value!; 350 | 351 | const model = editorRefs.openApi!.getModel(); 352 | if (model) { 353 | const markers = editorRefs.monaco!.editor.getModelMarkers({ 354 | resource: model.uri, 355 | }); 356 | // = if no errors 357 | if (!markers.length) { 358 | textsProxy.openApi = editorText.current; 359 | } 360 | } 361 | }} 362 | onValidate={(markers) => { 363 | if (markers.length) { 364 | return console.warn(currentOpenApi.format + " Errors", markers); 365 | } 366 | // = if no errors 367 | textsProxy.openApi = editorText.current; 368 | }} 369 | /> 370 | 371 | ); 372 | } 373 | 374 | function ZodColumn({ isLoading }: { isLoading: boolean }) { 375 | const editorText = useRef(textsProxy.zod); 376 | const texts = useSnapshot(textsProxy); 377 | const isReadOnly = texts.source !== "zod"; 378 | 379 | return ( 380 | 381 | 382 | 383 | Zod schemas 384 | 385 | 386 | 387 | 388 | 389 | 390 | { 396 | editorRefs.zod = ref!; 397 | ref.setValue(textsProxy.zod); 398 | }} 399 | onChange={(value) => { 400 | editorText.current = value!; 401 | 402 | const model = editorRefs.zod!.getModel(); 403 | if (model) { 404 | const markers = editorRefs.monaco!.editor.getModelMarkers({ 405 | resource: model.uri, 406 | }); 407 | // = if no errors 408 | if (!markers.length) { 409 | textsProxy.zod = editorText.current; 410 | } 411 | } 412 | }} 413 | onValidate={(markers) => { 414 | if (markers.length) { 415 | return console.warn("TS Errors", markers); 416 | } 417 | // = if no errors 418 | textsProxy.zod = editorText.current; 419 | }} 420 | /> 421 | 422 | ); 423 | } 424 | -------------------------------------------------------------------------------- /apps/client/src/Transformer/hashCode.tsx: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0 2 | 3 | export function hashCode(s: string) { 4 | let h = 0; 5 | let i = 0; 6 | for (i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0; 7 | 8 | return h; 9 | } 10 | -------------------------------------------------------------------------------- /apps/client/src/Transformer/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { trpc } from "@/trpc"; 2 | import { toasts } from "@/utils/toasts"; 3 | import { isType } from "@pastable/core"; 4 | import { InferMutationInput, TMutation } from "@transformer/backend/src"; 5 | import { editorRefs, localCache, textsProxy } from "./store"; 6 | import { hashCode } from "./hashCode"; 7 | 8 | const mutationNameToEditorRefName = (name: TMutation) => 9 | (( 10 | { 11 | tsToOapi: "openApi", 12 | tsToJsonSchema: "jsonSchema", 13 | tsToZod: "zod", 14 | // 15 | openApiToJsonSchema: "jsonSchema", 16 | openApiToTs: "ts", 17 | openApiToZod: "zod", 18 | // 19 | jsonSchemaToTs: "ts", 20 | jsonSchemaToZod: "zod", 21 | jsonSchemaToOpenApi: "openApi", 22 | // 23 | // zodToTs: "ts", 24 | // zodToOpenApi: "openApi", 25 | // zodToJsonSchema: "jsonSchema", 26 | } as const 27 | )[name]); 28 | 29 | function makeTransformerMutationHook(name: TMutation) { 30 | return () => 31 | trpc.useMutation(name, { 32 | mutationKey: name, 33 | onSuccess: (result, input) => { 34 | const value = isType>( 35 | input, 36 | name === "tsToOapi" || name === "jsonSchemaToOpenApi" 37 | ) 38 | ? input.value 39 | : input; 40 | localCache.set(`${name}-${hashCode(value)}`, result); 41 | 42 | const refName = mutationNameToEditorRefName(name)!; 43 | const ref = editorRefs[refName]; 44 | if (ref && ref.getValue() !== result) { 45 | return ref.setValue(result); 46 | } 47 | 48 | textsProxy[refName] = result; 49 | }, 50 | onError: (err) => { 51 | console.error(err); 52 | toasts.error(name + ": " + err.message); 53 | }, 54 | }); 55 | } 56 | 57 | export const useTsToOapi = makeTransformerMutationHook("tsToOapi"); 58 | export const useTsToJsonSchema = makeTransformerMutationHook("tsToJsonSchema"); 59 | export const useTsToZod = makeTransformerMutationHook("tsToZod"); 60 | 61 | export const useOpenApiToTs = makeTransformerMutationHook("openApiToTs"); 62 | export const useOpenApiToJsonSchema = makeTransformerMutationHook("openApiToJsonSchema"); 63 | export const useOpenApiToZod = makeTransformerMutationHook("openApiToZod"); 64 | 65 | export const useJsonSchemaToOpenApi = makeTransformerMutationHook("jsonSchemaToOpenApi"); 66 | export const useJsonSchemaToTs = makeTransformerMutationHook("jsonSchemaToTs"); 67 | export const useJsonSchemaToZod = makeTransformerMutationHook("jsonSchemaToTs"); 68 | 69 | // export const useZodToTs = makeTransformerMutationHook("zodToTs"); 70 | -------------------------------------------------------------------------------- /apps/client/src/Transformer/store.tsx: -------------------------------------------------------------------------------- 1 | import { Monaco } from "@monaco-editor/react"; 2 | import type { OpenAPIWriterOptions } from "@transformer/backend/src"; 3 | import { atom } from "jotai"; 4 | import type * as monaco from "monaco-editor/esm/vs/editor/editor.api"; 5 | import { proxy } from "valtio"; 6 | import { tsDefaultValue } from "./tsDefaultValue"; 7 | 8 | export const localCache = new Map(); 9 | 10 | const defaultTexts = { 11 | ts: null as unknown as string, 12 | jsonSchema: null as unknown as string, 13 | openApi: null as unknown as string, 14 | zod: null as unknown as string, 15 | }; 16 | export const textsProxy = proxy({ 17 | source: "ts" as OutputDestination, 18 | ts: tsDefaultValue, 19 | jsonSchema: "", 20 | openApi: "", 21 | zod: "", 22 | }); 23 | 24 | export type OutputDestination = keyof typeof defaultTexts; 25 | export const destinationsAtom = atom(["openApi", "zod"] as OutputDestination[]); 26 | export const sourceAtom = atom("ts"); 27 | export const sourceOptions: OutputDestination[] = ["ts", "jsonSchema", "openApi"]; 28 | 29 | export const prevTextsProxy = proxy(defaultTexts); 30 | 31 | const resetPrevTexts = () => { 32 | prevTextsProxy.ts = defaultTexts.ts; 33 | prevTextsProxy.jsonSchema = defaultTexts.jsonSchema; 34 | prevTextsProxy.openApi = defaultTexts.openApi; 35 | prevTextsProxy.zod = defaultTexts.zod; 36 | }; 37 | export const clearTexts = () => { 38 | resetPrevTexts(); 39 | editorRefs.ts?.setValue(""); 40 | editorRefs.jsonSchema?.setValue(""); 41 | editorRefs.openApi?.setValue(""); 42 | editorRefs.zod?.setValue(""); 43 | }; 44 | export const resetToDefault = () => { 45 | resetPrevTexts(); 46 | textsProxy.source = "ts"; 47 | textsProxy.ts = tsDefaultValue; 48 | editorRefs.ts?.setValue(tsDefaultValue); 49 | }; 50 | 51 | type MaybeEditor = monaco.editor.IStandaloneCodeEditor | null; 52 | 53 | export const editorRefs = { 54 | monaco: null as unknown as Monaco, 55 | ts: null as MaybeEditor, 56 | jsonSchema: null as MaybeEditor, 57 | openApi: null as MaybeEditor, 58 | zod: null as MaybeEditor, 59 | }; 60 | 61 | export const openApiSchemaVersions = [ 62 | "3.0.3", 63 | "3.0.2", 64 | "3.0.1", 65 | "3.0.0", 66 | "3.0.0-rc2", 67 | "3.0.0-rc1", 68 | "3.0.0-rc0", 69 | "2.0", 70 | "1.2", 71 | "1.1", 72 | "1.0", 73 | ] as Array; 74 | export const currentOpenApiProxy = proxy>>({ 75 | format: "json", 76 | schemaVersion: openApiSchemaVersions[0]!, 77 | }); 78 | -------------------------------------------------------------------------------- /apps/client/src/Transformer/tsDefaultValue.ts: -------------------------------------------------------------------------------- 1 | const tsDefaultValue1 = ` 2 | export interface FlowInternalConsumption { 3 | id: number; 4 | item: number; 5 | model: number; 6 | family: number; 7 | sub_section: number; 8 | section: number; 9 | universe: number; 10 | quantity: number; 11 | wacp: number; 12 | currency: string; 13 | creation_date: Date; 14 | reference: number; 15 | profil: string; 16 | }`.trim(); 17 | 18 | export const tsDefaultValue = `type Sort = { 19 | unsorted: boolean; 20 | } 21 | interface Other { 22 | other: string 23 | } 24 | 25 | type TypedObjectUnion = Sort|Other 26 | 27 | type Ids = Array 28 | 29 | interface Pageable { 30 | sort: Sort; 31 | profil: string; 32 | pageNumber: number; 33 | creation_date: Date; 34 | } 35 | 36 | export interface ApiResponse { 37 | content: Content; 38 | pageable: Pageable; 39 | sort: Sort; 40 | obj: { first: string, second: Sort }; 41 | ids: Ids; 42 | literalStr: "lit"; 43 | literalNb: 123; 44 | union: "aaa"|"bbb"|"ccc"; 45 | never: never; 46 | undefined: undefined; 47 | null: null; 48 | any: any; 49 | intersection: Sort & Other; 50 | tuple: [string, number]; 51 | objUnion: TypedObjectUnion 52 | void: void 53 | enum: Enum 54 | optional?: string 55 | symbol: Symbol 56 | true: true; 57 | thiis: this; 58 | arr: string[] 59 | arr2: Array 60 | date: Date 61 | unknown: unknown 62 | record: Record 63 | } 64 | 65 | enum Enum { 66 | first, second, third 67 | } 68 | `; 69 | -------------------------------------------------------------------------------- /apps/client/src/Transformer/useTransformerMutation.tsx: -------------------------------------------------------------------------------- 1 | import { useMemoRef } from "@/utils/useSyncRef"; 2 | import { usePrevious } from "@chakra-ui/react"; 3 | import { useAtom } from "jotai"; 4 | import { useEffect } from "react"; 5 | import { useSnapshot } from "valtio"; 6 | import { subscribeKey } from "valtio/utils"; 7 | import { snapshot } from "valtio/vanilla"; 8 | import { debounce } from "../utils/debounce"; 9 | import { hashCode } from "./hashCode"; 10 | import { 11 | useJsonSchemaToOpenApi, 12 | useJsonSchemaToTs, 13 | useJsonSchemaToZod, 14 | useOpenApiToJsonSchema, 15 | useOpenApiToTs, 16 | useOpenApiToZod, 17 | useTsToJsonSchema, 18 | useTsToOapi, 19 | useTsToZod, 20 | } from "./hooks"; 21 | import { 22 | clearTexts, 23 | currentOpenApiProxy, 24 | destinationsAtom, 25 | editorRefs, 26 | localCache, 27 | prevTextsProxy, 28 | textsProxy, 29 | } from "./store"; 30 | 31 | /** very copy pasty shitty hook but at least that works i guess ? */ 32 | export function useTransformerMutation({ 33 | tsToOapi, 34 | tsToJsonSchema, 35 | tsToZod, 36 | openApiToTs, 37 | openApiToJsonSchema, 38 | openApiToZod, 39 | jsonSchemaToTs, 40 | jsonSchemaToOpenApi, 41 | jsonSchemaToZod, 42 | }: { 43 | tsToOapi: ReturnType; 44 | tsToJsonSchema: ReturnType; 45 | tsToZod: ReturnType; 46 | openApiToTs: ReturnType; 47 | openApiToJsonSchema: ReturnType; 48 | openApiToZod: ReturnType; 49 | jsonSchemaToTs: ReturnType; 50 | jsonSchemaToOpenApi: ReturnType; 51 | jsonSchemaToZod: ReturnType; 52 | }) { 53 | const [destinations, setDestinations] = useAtom(destinationsAtom); 54 | const prevDestinations = usePrevious(destinations); 55 | 56 | const callbackRef = useMemoRef( 57 | debounce((value: string) => { 58 | if (!value) { 59 | clearTexts(); 60 | return console.warn("no value"); 61 | } 62 | 63 | const prev = snapshot(prevTextsProxy); 64 | const texts = snapshot(textsProxy); 65 | 66 | const hasChanged = value !== prev[texts.source]; 67 | const hasDestinationsChanged = destinations.join() !== prevDestinations?.join(); 68 | if (value !== null && prev[texts.source] !== "" && !hasChanged && !hasDestinationsChanged) { 69 | return console.warn("no change", { prev: prev[texts.source], value, destinations, prevDestinations }); 70 | } 71 | prevTextsProxy[texts.source] = value; 72 | 73 | if (texts.source === "ts") { 74 | if (destinations.includes("openApi")) { 75 | const cacheKey = `tsToOapi-${hashCode(value)}`; 76 | if (!localCache.has(cacheKey)) { 77 | const currentOpenApi = snapshot(currentOpenApiProxy); 78 | tsToOapi.mutate({ 79 | value, 80 | format: currentOpenApi.format as "json" | "yaml", 81 | schemaVersion: currentOpenApi.schemaVersion, 82 | }); 83 | prevTextsProxy.openApi = texts.openApi; 84 | } else { 85 | const result = localCache.get(cacheKey); 86 | editorRefs.openApi!.setValue(result); 87 | textsProxy.openApi = result; 88 | console.warn("openApi didnt change, skipping"); 89 | } 90 | } 91 | if (destinations.includes("jsonSchema")) { 92 | const cacheKey = `tsToJsonSchema-${hashCode(value)}`; 93 | if (!localCache.has(cacheKey)) { 94 | tsToJsonSchema.mutate(value); 95 | prevTextsProxy.jsonSchema = texts.jsonSchema; 96 | } else { 97 | const result = localCache.get(cacheKey); 98 | editorRefs.jsonSchema!.setValue(result); 99 | textsProxy.jsonSchema = result; 100 | console.warn("jsonSchema didnt change, skipping"); 101 | } 102 | } 103 | if (destinations.includes("zod")) { 104 | const cacheKey = `tsToZod-${hashCode(value)}`; 105 | if (!localCache.has(cacheKey)) { 106 | tsToZod.mutate(value); 107 | prevTextsProxy.zod = texts.zod; 108 | } else { 109 | const result = localCache.get(cacheKey); 110 | editorRefs.zod!.setValue(result); 111 | textsProxy.zod = result; 112 | console.warn("zod didnt change, skipping"); 113 | } 114 | } 115 | } else if (texts.source === "openApi") { 116 | if (destinations.includes("ts")) { 117 | const cacheKey = `openApiToTs-${hashCode(value)}`; 118 | if (!localCache.has(cacheKey)) { 119 | const currentOpenApi = snapshot(currentOpenApiProxy); 120 | openApiToTs.mutate({ 121 | value, 122 | format: currentOpenApi.format as "json" | "yaml", 123 | schemaVersion: currentOpenApi.schemaVersion, 124 | }); 125 | prevTextsProxy.openApi = texts.openApi; 126 | } else { 127 | const result = localCache.get(cacheKey); 128 | editorRefs.openApi!.setValue(result); 129 | textsProxy.openApi = result; 130 | console.warn("openApi didnt change, skipping"); 131 | } 132 | } 133 | if (destinations.includes("jsonSchema")) { 134 | const cacheKey = `openApiToJsonSchema-${hashCode(value)}`; 135 | if (!localCache.has(cacheKey)) { 136 | const currentOpenApi = snapshot(currentOpenApiProxy); 137 | openApiToJsonSchema.mutate({ 138 | value, 139 | format: currentOpenApi.format as "json" | "yaml", 140 | schemaVersion: currentOpenApi.schemaVersion, 141 | }); 142 | prevTextsProxy.jsonSchema = texts.jsonSchema; 143 | } else { 144 | const result = localCache.get(cacheKey); 145 | editorRefs.jsonSchema!.setValue(result); 146 | textsProxy.jsonSchema = result; 147 | console.warn("jsonSchema didnt change, skipping"); 148 | } 149 | } 150 | if (destinations.includes("zod")) { 151 | const cacheKey = `openApiToZod-${hashCode(value)}`; 152 | if (!localCache.has(cacheKey)) { 153 | const currentOpenApi = snapshot(currentOpenApiProxy); 154 | openApiToZod.mutate({ 155 | value, 156 | format: currentOpenApi.format as "json" | "yaml", 157 | schemaVersion: currentOpenApi.schemaVersion, 158 | }); 159 | prevTextsProxy.zod = texts.zod; 160 | } else { 161 | const result = localCache.get(cacheKey); 162 | editorRefs.zod!.setValue(result); 163 | textsProxy.zod = result; 164 | console.warn("zod didnt change, skipping"); 165 | } 166 | } 167 | } else if (texts.source === "jsonSchema") { 168 | if (destinations.includes("ts")) { 169 | const cacheKey = `jsonSchemaToTs-${hashCode(value)}`; 170 | if (!localCache.has(cacheKey)) { 171 | jsonSchemaToTs.mutate(value); 172 | prevTextsProxy.openApi = texts.openApi; 173 | } else { 174 | const result = localCache.get(cacheKey); 175 | editorRefs.openApi!.setValue(result); 176 | textsProxy.openApi = result; 177 | console.warn("openApi didnt change, skipping"); 178 | } 179 | } 180 | if (destinations.includes("openApi")) { 181 | const cacheKey = `jsonSchemaToOpenApi-${hashCode(value)}`; 182 | if (!localCache.has(cacheKey)) { 183 | const currentOpenApi = snapshot(currentOpenApiProxy); 184 | jsonSchemaToOpenApi.mutate({ 185 | value, 186 | format: currentOpenApi.format as "json" | "yaml", 187 | schemaVersion: currentOpenApi.schemaVersion, 188 | }); 189 | prevTextsProxy.jsonSchema = texts.jsonSchema; 190 | } else { 191 | const result = localCache.get(cacheKey); 192 | editorRefs.jsonSchema!.setValue(result); 193 | textsProxy.jsonSchema = result; 194 | console.warn("jsonSchema didnt change, skipping"); 195 | } 196 | } 197 | if (destinations.includes("zod")) { 198 | const cacheKey = `jsonSchemaToZod-${hashCode(value)}`; 199 | if (!localCache.has(cacheKey)) { 200 | jsonSchemaToZod.mutate(value); 201 | prevTextsProxy.zod = texts.zod; 202 | } else { 203 | const result = localCache.get(cacheKey); 204 | editorRefs.zod!.setValue(result); 205 | textsProxy.zod = result; 206 | console.warn("zod didnt change, skipping"); 207 | } 208 | } 209 | } 210 | }, 300), 211 | [destinations] 212 | ); 213 | 214 | // call API on destinations change 215 | useEffect(() => { 216 | const texts = snapshot(textsProxy); 217 | callbackRef.current(texts[texts.source]); 218 | }, [destinations]); 219 | 220 | const texts = useSnapshot(textsProxy); 221 | 222 | // call API on ts change 223 | useEffect(() => { 224 | const unsubTs = subscribeKey(textsProxy, texts.source, (value) => callbackRef.current(value)); 225 | 226 | return () => { 227 | unsubTs(); 228 | }; 229 | }, [texts.source]); 230 | 231 | // Add self (source) to destinations so that when swapping to another it will also output in that format 232 | useEffect(() => { 233 | if (!destinations.includes(texts.source)) { 234 | setDestinations((prev) => [...prev, texts.source]); 235 | } 236 | }, [texts.source, destinations]); 237 | 238 | return { destinations }; 239 | } 240 | -------------------------------------------------------------------------------- /apps/client/src/components/Show.tsx: -------------------------------------------------------------------------------- 1 | import { WithChildren } from "@pastable/core"; 2 | import { ReactElement } from "react"; 3 | 4 | export function Show({ cond, children }: WithChildren & { cond: boolean }): ReactElement | null { 5 | return cond ? (children as ReactElement) : null; 6 | } 7 | -------------------------------------------------------------------------------- /apps/client/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", 4 | "Droid Sans", "Helvetica Neue", sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | code { 10 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 11 | } 12 | -------------------------------------------------------------------------------- /apps/client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Transformer 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/client/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /apps/client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | 3 | import React from "react"; 4 | import ReactDOM from "react-dom"; 5 | 6 | import App from "./App"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById("root") 13 | ); 14 | -------------------------------------------------------------------------------- /apps/client/src/trpc.ts: -------------------------------------------------------------------------------- 1 | import { createReactQueryHooks } from "@trpc/react"; 2 | import type { AppRouter } from "@transformer/backend/src"; 3 | 4 | export const trpc = createReactQueryHooks(); 5 | -------------------------------------------------------------------------------- /apps/client/src/utils/createContextWithHook.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | // Adapted from https://github.com/chakra-ui/chakra-ui/blob/27eec8de744d05eef5bcbd2de651f3a37370ff2c/packages/react-utils/src/context.ts 4 | 5 | export interface CreateContextOptions { 6 | /** 7 | * If `true`, React will throw if context is `null` or `undefined` 8 | * In some cases, you might want to support nested context, so you can set it to `false` 9 | */ 10 | strict?: boolean; 11 | /** 12 | * Error message to throw if the context is `undefined` 13 | */ 14 | errorMessage?: string; 15 | /** 16 | * The display name of the context 17 | */ 18 | name: string; 19 | /** 20 | * The display name of the context 21 | */ 22 | initialValue?: Initial | undefined; 23 | } 24 | 25 | type CreateContextReturn = [React.Provider, () => T, React.Context]; 26 | 27 | /** 28 | * Creates a named context, provider, and hook. 29 | * 30 | * @param options create context options 31 | */ 32 | export function createContextWithHook( 33 | options: CreateContextOptions 34 | ): CreateContextReturn; 35 | export function createContextWithHook( 36 | name: string, 37 | options?: CreateContextOptions 38 | ): CreateContextReturn; 39 | export function createContextWithHook( 40 | nameOrOptions: string | CreateContextOptions, 41 | optionsProp: CreateContextOptions = { name: undefined as any } 42 | ): CreateContextReturn { 43 | const options = typeof nameOrOptions === "string" ? optionsProp : nameOrOptions; 44 | const name = typeof nameOrOptions === "string" ? nameOrOptions : options.name; 45 | const { 46 | strict = false, 47 | errorMessage = `useContext: "${ 48 | name || "context" 49 | }" is undefined. Seems you forgot to wrap component within the Provider`, 50 | } = options; 51 | 52 | const Context = React.createContext(undefined); 53 | 54 | Context.displayName = name; 55 | 56 | function useContext() { 57 | const context = React.useContext(Context); 58 | 59 | if (!context && strict) { 60 | const error = new Error(errorMessage); 61 | error.name = "ContextError"; 62 | // @ts-ignore 63 | Error.captureStackTrace?.(error, useContext); 64 | throw error; 65 | } 66 | 67 | return context; 68 | } 69 | 70 | return [Context.Provider, useContext, Context] as CreateContextReturn; 71 | } 72 | -------------------------------------------------------------------------------- /apps/client/src/utils/debounce.tsx: -------------------------------------------------------------------------------- 1 | import { AnyFunction } from "@pastable/core"; 2 | 3 | export function debounce(fn: Fn, wait = 100): Fn { 4 | let timeout: any; 5 | return function (...args: any[]) { 6 | clearTimeout(timeout); 7 | // @ts-ignore 8 | timeout = setTimeout(() => fn.call(this, ...args), wait); 9 | } as Fn; 10 | } 11 | -------------------------------------------------------------------------------- /apps/client/src/utils/toasts.ts: -------------------------------------------------------------------------------- 1 | import { ToastId, UseToastOptions, createStandaloneToast } from "@chakra-ui/react"; 2 | import { getRandomString } from "@pastable/core"; 3 | 4 | // Toasts 5 | const toast = createStandaloneToast(); 6 | const baseToastConfig = { duration: 3000, isClosable: true, unique: true }; 7 | 8 | type ToastStatus = Exclude | "default"; 9 | export const toastConfigs: Record = { 10 | default: { ...baseToastConfig }, 11 | success: { ...baseToastConfig, status: "success" }, 12 | error: { ...baseToastConfig, status: "error" }, 13 | info: { ...baseToastConfig, status: "info" }, 14 | warning: { ...baseToastConfig, status: "warning" }, 15 | }; 16 | 17 | const toastMap = new Map(); 18 | export type ToastOptions = UseToastOptions & UniqueToastOptions; 19 | 20 | export function makeToast(title: string, options?: ToastOptions): ReturnType; 21 | export function makeToast(options: ToastOptions): ReturnType; 22 | export function makeToast(titleOrOptions: string | ToastOptions, options?: ToastOptions): ReturnType { 23 | const title = typeof titleOrOptions === "string" ? titleOrOptions : ""; 24 | const config = (typeof titleOrOptions === "string" ? options : titleOrOptions) || { title }; 25 | 26 | if (config.uniqueId) { 27 | config.id = getRandomString(10); 28 | const prevToast = toastMap.get(config.uniqueId); 29 | prevToast && toast.close(prevToast.id!); 30 | toastMap.set(config.uniqueId, config); 31 | } else if (config.unique) { 32 | toast.closeAll(); 33 | } 34 | 35 | return toast(config); 36 | } 37 | 38 | export function defaultToast(title: string, options?: ToastOptions): ReturnType; 39 | export function defaultToast(options: ToastOptions): ReturnType; 40 | export function defaultToast( 41 | titleOrOptions: string | ToastOptions, 42 | options?: ToastOptions 43 | ): ReturnType { 44 | const title = typeof titleOrOptions === "string" ? titleOrOptions : ""; 45 | const config = (typeof titleOrOptions === "string" ? options : titleOrOptions) || { title }; 46 | return makeToast({ ...toastConfigs.default, ...config }); 47 | } 48 | export function successToast(title: string, options?: ToastOptions): ReturnType; 49 | export function successToast(options: ToastOptions): ReturnType; 50 | export function successToast( 51 | titleOrOptions: string | ToastOptions, 52 | options?: ToastOptions 53 | ): ReturnType { 54 | const title = typeof titleOrOptions === "string" ? titleOrOptions : ""; 55 | const config = (typeof titleOrOptions === "string" ? options : titleOrOptions) || { title }; 56 | return makeToast({ ...toastConfigs.success, unique: false, ...config }); 57 | } 58 | export function errorToast(title: string, options?: ToastOptions): ReturnType; 59 | export function errorToast(options: ToastOptions): ReturnType; 60 | export function errorToast( 61 | titleOrOptions: string | ToastOptions, 62 | options?: ToastOptions 63 | ): ReturnType { 64 | const title = typeof titleOrOptions === "string" ? titleOrOptions : ""; 65 | const config = (typeof titleOrOptions === "string" ? options : titleOrOptions) || { title }; 66 | return makeToast({ title: "Une erreur est survenue", ...toastConfigs.error, ...config }); 67 | } 68 | 69 | export function infoToast(title: string, options?: ToastOptions): ReturnType; 70 | export function infoToast(options: ToastOptions): ReturnType; 71 | export function infoToast(titleOrOptions: string | ToastOptions, options?: ToastOptions): ReturnType { 72 | const title = typeof titleOrOptions === "string" ? titleOrOptions : ""; 73 | const config = (typeof titleOrOptions === "string" ? options : titleOrOptions) || { title }; 74 | return makeToast({ ...toastConfigs.info, ...config }); 75 | } 76 | 77 | export function warningToast(title: string, options?: ToastOptions): ReturnType; 78 | export function warningToast(options: ToastOptions): ReturnType; 79 | export function warningToast( 80 | titleOrOptions: string | ToastOptions, 81 | options?: ToastOptions 82 | ): ReturnType { 83 | const title = typeof titleOrOptions === "string" ? titleOrOptions : ""; 84 | const config = (typeof titleOrOptions === "string" ? options : titleOrOptions) || { title }; 85 | return makeToast({ ...toastConfigs.warning, ...config }); 86 | } 87 | 88 | export const toasts = { 89 | default: defaultToast, 90 | success: successToast, 91 | error: errorToast, 92 | info: infoToast, 93 | warning: warningToast, 94 | }; 95 | 96 | // Errors 97 | export const onError = (description: string) => errorToast({ description }); 98 | 99 | interface UniqueToastOptions { 100 | /** When provided, will close previous toasts with the same id */ 101 | uniqueId?: ToastId; 102 | /** When true, will close all other toasts */ 103 | unique?: boolean; 104 | } 105 | -------------------------------------------------------------------------------- /apps/client/src/utils/useSyncRef.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useEffect, useLayoutEffect, useRef } from "react"; 2 | 3 | export function useSyncRef(value: T): React.MutableRefObject { 4 | const ref = useRef(value); 5 | useLayoutEffect(() => void (ref.current = value)); 6 | return ref; 7 | } 8 | 9 | export const useMemoRef = (value: T, deps: DependencyList) => { 10 | const ref = useRef(value); 11 | useEffect(() => void (ref.current = value), deps); 12 | return ref; 13 | }; 14 | -------------------------------------------------------------------------------- /apps/client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "allowJs": false, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "strictNullChecks": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "types": ["vite/client"], 19 | "baseUrl": "./src", 20 | "paths": { 21 | "@/*": ["./*"], 22 | "/assets/*": ["../public/assets/*"] 23 | } 24 | }, 25 | "include": ["./src"] 26 | } 27 | -------------------------------------------------------------------------------- /apps/client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import jotaiDebugLabel from "jotai/babel/plugin-debug-label"; 3 | import jotaiReactRefresh from "jotai/babel/plugin-react-refresh"; 4 | import { defineConfig } from "vite"; 5 | import { VitePWA } from "vite-plugin-pwa"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | base: "/", 10 | root: "./", 11 | build: { outDir: "./dist", sourcemap: true }, 12 | optimizeDeps: { 13 | include: ["@emotion/react"], 14 | exclude: ["fromentries"], 15 | }, 16 | plugins: [ 17 | // https://jotai.org/docs/guides/vite 18 | react({ babel: { plugins: [jotaiDebugLabel, jotaiReactRefresh] } }), 19 | VitePWA(), 20 | ], 21 | resolve: { 22 | alias: [ 23 | { 24 | find: "@", 25 | replacement: "/src", 26 | }, 27 | ], 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transformer", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/astahmer/transformer.git" 6 | }, 7 | "private": true, 8 | "author": "Alexandre Stahmer ", 9 | "scripts": { 10 | "build": "turbo run build", 11 | "dev": "turbo run dev --parallel", 12 | "test": "turbo run test", 13 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 14 | }, 15 | "packageManager": "pnpm@6.24.0", 16 | "devDependencies": { 17 | "prettier": "^2.5.1", 18 | "prettier-plugin-sorted": "^2.0.0", 19 | "rimraf": "^3.0.2", 20 | "turbo": "^1.1.2", 21 | "typescript": "^4.5.5" 22 | }, 23 | "importSort": { 24 | ".js, jsx, .ts, .tsx": { 25 | "options": { 26 | "ignoreTsConfig": true 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in subdirs of packages/ 3 | - "packages/**" 4 | - "apps/**" 5 | # exclude packages that are inside test directories 6 | - "!**/test/**" 7 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseBranch": "origin/main", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": ["^build"] 6 | }, 7 | "client#build": { 8 | "dependsOn": ["^build", "$VITE_BACKEND_URL"] 9 | }, 10 | "test": { 11 | "dependsOn": ["^build"], 12 | "outputs": [] 13 | }, 14 | "dev": { 15 | "cache": false 16 | } 17 | }, 18 | "globalDependencies": ["$GITHUB_TOKEN", "tsconfig.json", ".env.*"] 19 | } 20 | --------------------------------------------------------------------------------