├── .gitignore ├── LICENSE ├── README.md └── relay-entrypoints ├── .babelrc ├── .gitignore ├── extractQueriesAndModules.js ├── js-resource-loader.js ├── package-lock.json ├── package.json ├── public └── .gitkeep ├── queries.json ├── react-env.d.ts ├── relay.config.js ├── src ├── EntryPointConfig.ts ├── ErrorBoundary.ts ├── Home.entrypoint.ts ├── Home.tsx ├── JSResource.ts ├── RelayEnvironment.ts ├── Repository.entrypoint.ts ├── Repository.route.ts ├── Repository.tsx ├── Root.entrypoint.ts ├── Root.route.ts ├── Root.tsx ├── RouteConfig.ts ├── Router.tsx ├── ServerEntry.ts ├── Shell.tsx ├── __generated__ │ └── RepositoryQuery.graphql.ts └── nullThrows.ts ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Relay Tools 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typescript-relayjs-examples 2 | RelayJS typescript examples 3 | -------------------------------------------------------------------------------- /relay-entrypoints/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "relay", 4 | "@babel/plugin-proposal-optional-chaining", 5 | "@babel/plugin-proposal-nullish-coalescing-operator" 6 | ], 7 | "presets": [ 8 | "@babel/preset-typescript", 9 | [ 10 | "@babel/preset-react", 11 | { 12 | "runtime": "automatic", 13 | "development": true 14 | } 15 | ] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /relay-entrypoints/.gitignore: -------------------------------------------------------------------------------- 1 | out -------------------------------------------------------------------------------- /relay-entrypoints/extractQueriesAndModules.js: -------------------------------------------------------------------------------- 1 | const server = require("./out/server/server"); 2 | 3 | const { queries, modules } = server.getQueriesAndModulesForUrl( 4 | "/facebook/relay" 5 | ); 6 | console.log(queries, modules); 7 | -------------------------------------------------------------------------------- /relay-entrypoints/js-resource-loader.js: -------------------------------------------------------------------------------- 1 | const babel = require("@babel/core"); 2 | 3 | module.exports = function jsResourceLoader(content, map, meta) { 4 | function babelPlugin({ types: t }) { 5 | const jsResourceAbsoluteModulePath = "./JSResource"; 6 | const jsrIdent = t.identifier("JSResource"); 7 | let program = null; 8 | let imported = false; 9 | function addImport() { 10 | if (!imported) { 11 | imported = true; 12 | const importDefaultSpecifier = t.importDefaultSpecifier(jsrIdent); 13 | const importDeclaration = t.importDeclaration( 14 | [importDefaultSpecifier], 15 | t.stringLiteral(jsResourceAbsoluteModulePath) 16 | ); 17 | program.unshiftContainer("body", importDeclaration); 18 | } 19 | } 20 | 21 | function requireResolveWeak(modulePath) { 22 | return t.callExpression( 23 | t.memberExpression( 24 | t.identifier("require"), 25 | t.identifier("resolveWeak") 26 | ), 27 | [modulePath] 28 | ); 29 | } 30 | 31 | const processed = []; 32 | 33 | return { 34 | name: "ast-transform", // not required 35 | visitor: { 36 | Program(path) { 37 | program = path; 38 | }, 39 | Import(path) { 40 | const fn = path.getFunctionParent(); 41 | if (processed.includes(path.node)) return; 42 | processed.push(path.node); 43 | if (fn && t.isCallExpression(path.parent)) { 44 | const callExpression = path.parent; 45 | const modulePath = callExpression.arguments[0]; 46 | t.assertStringLiteral(modulePath); 47 | fn.replaceWith( 48 | t.callExpression(t.identifier("JSResource"), [ 49 | requireResolveWeak(modulePath), 50 | fn.node, 51 | ]) 52 | ); 53 | addImport(); 54 | } 55 | }, 56 | }, 57 | }; 58 | } 59 | return babel.transformSync(content, { 60 | plugins: [babelPlugin], 61 | }).code; 62 | }; 63 | -------------------------------------------------------------------------------- /relay-entrypoints/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "webpack-dev-server --config webpack.config.js", 4 | "build": "webpack --config webpack.config.js" 5 | }, 6 | "dependencies": { 7 | "body-parser": "^1.19.0", 8 | "history": "^5.0.0", 9 | "react": "0.0.0-experimental-94c0244ba", 10 | "react-dom": "0.0.0-experimental-94c0244ba", 11 | "react-relay": "0.0.0-experimental-183bdd28", 12 | "react-router": "^5.2.0", 13 | "react-router-config": "^5.1.1", 14 | "relay-runtime": "^10.0.1" 15 | }, 16 | "devDependencies": { 17 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", 18 | "@babel/plugin-proposal-optional-chaining": "^7.11.0", 19 | "@babel/preset-react": "^7.10.4", 20 | "@babel/preset-typescript": "^7.10.4", 21 | "@octokit/graphql-schema": "^8.28.0", 22 | "@types/history": "^4.7.8", 23 | "@types/react": "^16.9.49", 24 | "@types/react-dom": "^16.9.8", 25 | "@types/react-relay": "^7.0.9", 26 | "@types/react-router": "^5.1.8", 27 | "@types/react-router-config": "^5.0.1", 28 | "@types/relay-compiler": "^8.0.0", 29 | "@types/relay-runtime": "^10.0.3", 30 | "@types/webpack": "^4.41.22", 31 | "@types/webpack-env": "^1.15.3", 32 | "babel-loader": "^8.1.0", 33 | "babel-plugin-relay": "^10.0.1", 34 | "html-webpack-plugin": "^4.4.1", 35 | "relay-compiler": "^10.0.1", 36 | "relay-compiler-language-typescript": "^13.0.1", 37 | "relay-config": "^10.0.1", 38 | "typescript": "^4.0.2", 39 | "webpack": "^4.44.2", 40 | "webpack-cli": "^3.3.12", 41 | "webpack-dev-server": "^3.11.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /relay-entrypoints/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relay-tools/typescript-relayjs-examples/6e5b36c2fad2b6eaef7a39c040c60485f093dffe/relay-entrypoints/public/.gitkeep -------------------------------------------------------------------------------- /relay-entrypoints/queries.json: -------------------------------------------------------------------------------- 1 | { 2 | "8aa70460f97b04dea4549c71282be858": "query RepositoryQuery(\n $owner: String!\n $name: String!\n) {\n repository(owner: $owner, name: $name) {\n name\n id\n }\n}\n", 3 | "4a45f359f7fe4a783fae695058a98919": "query RepositoryQuery(\n $owner: String!\n $name: String!\n) {\n repository(owner: $owner, name: $name) {\n nameWithOwner\n id\n }\n}\n" 4 | } -------------------------------------------------------------------------------- /relay-entrypoints/react-env.d.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | /// 4 | /// 5 | -------------------------------------------------------------------------------- /relay-entrypoints/relay.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | customScalars: {}, 3 | schema: "./node_modules/@octokit/graphql-schema/schema.graphql", 4 | language: "typescript", 5 | src: "./src", 6 | persistOutput: "./queries.json", 7 | }; 8 | -------------------------------------------------------------------------------- /relay-entrypoints/src/EntryPointConfig.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import type { EntryPoint } from "react-relay/lib/relay-experimental/EntryPointTypes"; 3 | 4 | export function createEntryPoint

( 5 | config: EntryPoint 6 | ): EntryPoint { 7 | return { 8 | name: config.root.getModuleId(), 9 | root: config.root, 10 | getPreloadProps: config.getPreloadProps, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /relay-entrypoints/src/ErrorBoundary.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Component, ReactNode } from "react"; 4 | 5 | type Props = { 6 | children: ReactNode; 7 | renderError: (error: Error | null, retry: () => void) => ReactNode; 8 | }; 9 | 10 | type State = { 11 | hasError: boolean; 12 | error: Error | null; 13 | }; 14 | 15 | export default class ErrorBoundary extends Component { 16 | static getDerivedStateFromError(error: Error) { 17 | // Update state so the next render will show the fallback UI. 18 | return { hasError: true, error }; 19 | } 20 | 21 | constructor(props: Props) { 22 | super(props); 23 | this.state = { 24 | hasError: false, 25 | error: null, 26 | }; 27 | } 28 | 29 | componentDidCatch(error: Error) { 30 | console.error(error); 31 | } 32 | 33 | render() { 34 | return this.state.hasError 35 | ? this.props.renderError(this.state.error, () => { 36 | this.setState({ hasError: false, error: null }); 37 | }) 38 | : this.props.children; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /relay-entrypoints/src/Home.entrypoint.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { createEntryPoint } from "./EntryPointConfig"; 4 | 5 | export default createEntryPoint({ 6 | root: () => import("./Home"), 7 | getPreloadProps() { 8 | return {}; 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /relay-entrypoints/src/Home.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | const Home = () => { 4 | return ( 5 |

6 |

Home!

7 |
8 | ); 9 | }; 10 | 11 | export default Home; 12 | -------------------------------------------------------------------------------- /relay-entrypoints/src/JSResource.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | class JSResourceImpl { 4 | private _loader: () => Promise; 5 | private _result: T | null; 6 | private _error: unknown | null; 7 | private _promise: Promise | null; 8 | private _moduleId: string | number; 9 | constructor(moduleId: string | number, loader: () => Promise) { 10 | this._loader = loader; 11 | this._result = null; 12 | this._error = null; 13 | this._promise = null; 14 | this._moduleId = moduleId; 15 | this._populateIfLoaded(); 16 | } 17 | 18 | load() { 19 | console.log("load!!"); 20 | this._populateIfLoaded(); 21 | let promise = this._promise; 22 | if (promise == null) { 23 | promise = this._loader().then( 24 | (result: any) => { 25 | this._result = result.default; 26 | return result.default; 27 | }, 28 | (error) => { 29 | this._error = error; 30 | throw error; 31 | } 32 | ); 33 | 34 | this._promise = promise; 35 | } 36 | 37 | return promise; 38 | } 39 | 40 | get() { 41 | if (this._result != null) { 42 | console.log("loaded"); 43 | return this._result; 44 | } 45 | } 46 | 47 | getModuleIfRequired() { 48 | return this.get(); 49 | } 50 | 51 | read() { 52 | this._populateIfLoaded(); 53 | if (this._result != null) { 54 | return this._result; 55 | } else if (this._error != null) { 56 | throw this._error; 57 | } else { 58 | throw this._promise; 59 | } 60 | } 61 | getModuleId(): string { 62 | return this._moduleId.toString(); 63 | } 64 | 65 | _populateIfLoaded() { 66 | const moduleIsLoaded = __webpack_modules__.hasOwnProperty(this._moduleId); 67 | if (this._result == null || this._promise == null) { 68 | if (moduleIsLoaded) { 69 | const m = __webpack_require__(this._moduleId); 70 | this._promise = Promise.resolve(m.default); 71 | this._result = m.default; 72 | } 73 | } 74 | } 75 | } 76 | 77 | const resourceMap: Map> = new Map(); 78 | 79 | export type JSResource = { 80 | getModuleIfRequired(): T | null; 81 | load(): Promise; 82 | }; 83 | 84 | export default function JSResource( 85 | moduleId: string | number, 86 | loader: () => Promise 87 | ): JSResource { 88 | let resource = resourceMap.get(moduleId); 89 | if (resource == null) { 90 | resource = new JSResourceImpl(moduleId, loader); 91 | resourceMap.set(moduleId, resource); 92 | } 93 | return resource as JSResource; 94 | } 95 | -------------------------------------------------------------------------------- /relay-entrypoints/src/RelayEnvironment.ts: -------------------------------------------------------------------------------- 1 | import { Environment, Network, Store, RecordSource } from "relay-runtime"; 2 | 3 | const network = Network.create((request, variables, cacheConfig) => { 4 | return fetch("/api/graphql", { 5 | headers: { 6 | "content-type": "application/json", 7 | accept: "application/json", 8 | }, 9 | method: "POST", 10 | body: JSON.stringify({ 11 | query: request.id ?? request.text, 12 | variables, 13 | }), 14 | }) 15 | .then((response) => response.json()) 16 | .then((payload) => 17 | Array.isArray(payload.errors) ? Promise.reject(payload) : payload 18 | ); 19 | }); 20 | 21 | const RelayEnvironment = new Environment({ 22 | network, 23 | store: new Store(new RecordSource(), { gcReleaseBufferSize: 10 }), 24 | }); 25 | 26 | export { RelayEnvironment }; 27 | -------------------------------------------------------------------------------- /relay-entrypoints/src/Repository.entrypoint.ts: -------------------------------------------------------------------------------- 1 | import { createEntryPoint } from "./EntryPointConfig"; 2 | import RepositoryQuery from "./__generated__/RepositoryQuery.graphql"; 3 | 4 | export default createEntryPoint<{ owner: string; name: string }>({ 5 | root: () => import("./Repository"), 6 | getPreloadProps(params) { 7 | return { 8 | queries: { 9 | repositoryQuery: { 10 | parameters: RepositoryQuery, 11 | variables: { 12 | owner: params.owner, 13 | name: params.name, 14 | }, 15 | }, 16 | }, 17 | }; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /relay-entrypoints/src/Repository.route.ts: -------------------------------------------------------------------------------- 1 | import { createRoute } from "./RouteConfig"; 2 | import RootRoute from "./Root.route"; 3 | import RepositoryEntrypoint from "./Repository.entrypoint"; 4 | 5 | export default createRoute({ 6 | parent: RootRoute, 7 | path: "/:owner/:name", 8 | exact: true, 9 | entryPoint: RepositoryEntrypoint, 10 | }); 11 | -------------------------------------------------------------------------------- /relay-entrypoints/src/Repository.tsx: -------------------------------------------------------------------------------- 1 | import { PreloadedQuery } from "react-relay/lib/relay-experimental/EntryPointTypes"; 2 | import { usePreloadedQuery, graphql } from "react-relay/hooks"; 3 | import { RepositoryQuery } from "./__generated__/RepositoryQuery.graphql"; 4 | 5 | export default function Repository(props: { 6 | queries: { repositoryQuery: PreloadedQuery }; 7 | }) { 8 | const data = usePreloadedQuery( 9 | graphql` 10 | query RepositoryQuery($owner: String!, $name: String!) { 11 | repository(owner: $owner, name: $name) { 12 | nameWithOwner 13 | } 14 | } 15 | `, 16 | props.queries.repositoryQuery 17 | ); 18 | 19 | return

{data.repository?.nameWithOwner}

; 20 | } 21 | -------------------------------------------------------------------------------- /relay-entrypoints/src/Root.entrypoint.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { createEntryPoint } from "./EntryPointConfig"; 4 | 5 | export default createEntryPoint({ 6 | root: () => import("./Root"), 7 | getPreloadProps() { 8 | return {}; 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /relay-entrypoints/src/Root.route.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { createRoute } from "./RouteConfig"; 4 | import RootEntryPoint from "./Root.entrypoint"; 5 | export default createRoute({ 6 | entryPoint: RootEntryPoint, 7 | }); 8 | -------------------------------------------------------------------------------- /relay-entrypoints/src/Root.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { ReactNode, Suspense } from "react"; 4 | 5 | function Fallback() { 6 | return ( 7 |
8 |

Loading...

9 |
10 | ); 11 | } 12 | 13 | export default function Root(props: { props: { children: ReactNode } }) { 14 | console.log(props); 15 | 16 | return ( 17 | }>{props.props.children ?? null} 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /relay-entrypoints/src/RouteConfig.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import type { EntryPoint } from "react-relay/lib/relay-experimental/EntryPointTypes"; 4 | 5 | export function createRoute(routeConfig: { 6 | parent?: Route; 7 | path?: string; 8 | exact?: boolean; 9 | entryPoint: EntryPoint; 10 | }): Route { 11 | return routeConfig; 12 | } 13 | 14 | type Route = Readonly<{ 15 | parent?: Route; 16 | path?: string; 17 | exact?: boolean; 18 | entryPoint: EntryPoint; 19 | }>; 20 | -------------------------------------------------------------------------------- /relay-entrypoints/src/Router.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import { Suspense } from "react"; 3 | import { EntryPoint } from "react-relay/lib/relay-experimental/EntryPointTypes"; 4 | import { createBrowserHistory, Location } from "history"; 5 | import { matchRoutes, MatchedRoute } from "react-router-config"; 6 | 7 | import { 8 | // @ts-expect-error 9 | loadEntryPoint, 10 | EntryPointContainer, 11 | RelayEnvironmentProvider, 12 | } from "react-relay/hooks"; 13 | 14 | import ErrorBoundary from "./ErrorBoundary"; 15 | import { RelayEnvironment } from "./RelayEnvironment"; 16 | const context = require.context(".", true, /\.route\.ts$/); 17 | 18 | const routes = context.keys().map((moduleId) => { 19 | const module = context(moduleId); 20 | return module.default; 21 | }); 22 | 23 | const environmentProvider = { 24 | getEnvironment() { 25 | return RelayEnvironment; 26 | }, 27 | }; 28 | 29 | type Route = { 30 | path?: string; 31 | exact?: boolean; 32 | entryPoint: EntryPoint<{}, {}>; 33 | routes: Route[]; 34 | }; 35 | 36 | const routeConfig: Route[] = []; 37 | 38 | for (const route of routes) { 39 | if (route.parent) { 40 | let foundParent = false; 41 | for (const possibleParent of routes) { 42 | if (possibleParent === route.parent) { 43 | possibleParent.routes = possibleParent.routes || []; 44 | possibleParent.routes.push(route); 45 | foundParent = true; 46 | break; 47 | } 48 | } 49 | if (!foundParent) { 50 | throw new Error("Unknown route parent"); 51 | } 52 | } else { 53 | routeConfig.push(route); 54 | } 55 | } 56 | 57 | console.log(routeConfig); 58 | 59 | function createRouter() { 60 | const history = createBrowserHistory(); 61 | const initialMatches = matchRoute(routeConfig, history.location); 62 | const initialEntries = prepareMatches(initialMatches); 63 | let currentEntry = { 64 | location: history.location, 65 | entries: initialEntries, 66 | }; 67 | console.log(currentEntry); 68 | 69 | return { 70 | currentEntry, 71 | }; 72 | } 73 | 74 | /** 75 | * Match the current location to the corresponding route entry. 76 | */ 77 | function matchRoute(routes: Route[], location: Location) { 78 | const pathname = location.pathname; 79 | const matchedRoutes = matchRoutes(routes, pathname); 80 | if (!Array.isArray(matchedRoutes) || matchedRoutes.length === 0) { 81 | throw new Error("No route for " + pathname); 82 | } 83 | return matchedRoutes; 84 | } 85 | 86 | function prepareMatches(matches: MatchedRoute<{}>[]) { 87 | return matches.map(({ match, route }) => { 88 | const entryPoint = loadEntryPoint( 89 | environmentProvider, 90 | route.entryPoint, 91 | match.params 92 | ); 93 | return { entryPoint, match }; 94 | }); 95 | } 96 | 97 | const router = createRouter(); 98 | 99 | function Router() { 100 | const { currentEntry } = router; 101 | 102 | const reversedEntries = currentEntry.entries.slice().reverse(); 103 | const firstEntry = reversedEntries[0]; 104 | 105 | let routeComponent = ( 106 | 110 | ); 111 | 112 | for (let ii = 1; ii < reversedEntries.length; ii++) { 113 | const nextItem = reversedEntries[ii]; 114 | routeComponent = ( 115 | 119 | ); 120 | } 121 | 122 | return ( 123 | 124 | "Error"}> 125 | {routeComponent} 126 | 127 | 128 | ); 129 | } 130 | 131 | export default Router; 132 | -------------------------------------------------------------------------------- /relay-entrypoints/src/ServerEntry.ts: -------------------------------------------------------------------------------- 1 | import { matchRoutes, MatchedRoute } from "react-router-config"; 2 | import { EntryPoint } from "react-relay/lib/relay-experimental/EntryPointTypes"; 3 | import { Location, parsePath } from "history"; 4 | const context = require.context(".", true, /\.route\.ts$/); 5 | 6 | const routes = context.keys().map((moduleId) => { 7 | const module = context(moduleId); 8 | return module.default; 9 | }); 10 | 11 | type Route = { 12 | path?: string; 13 | exact?: boolean; 14 | entryPoint: EntryPoint<{}, {}>; 15 | routes: Route[]; 16 | }; 17 | 18 | const routeConfig: Route[] = []; 19 | 20 | for (const route of routes) { 21 | if (route.parent) { 22 | let foundParent = false; 23 | for (const possibleParent of routes) { 24 | if (possibleParent === route.parent) { 25 | possibleParent.routes = possibleParent.routes || []; 26 | possibleParent.routes.push(route); 27 | foundParent = true; 28 | break; 29 | } 30 | } 31 | if (!foundParent) { 32 | throw new Error("Unknown route parent"); 33 | } 34 | } else { 35 | routeConfig.push(route); 36 | } 37 | } 38 | 39 | /** 40 | * Match the current location to the corresponding route entry. 41 | */ 42 | function matchRoute(routes: Route[], pathname: string) { 43 | const matchedRoutes = matchRoutes(routes, pathname); 44 | if (!Array.isArray(matchedRoutes) || matchedRoutes.length === 0) { 45 | throw new Error("No route for " + pathname); 46 | } 47 | return matchedRoutes; 48 | } 49 | 50 | export function getQueriesAndModulesForUrl(url: string) { 51 | const partialPath = parsePath(url); 52 | 53 | const matches = matchRoute(routeConfig, partialPath.pathname!); 54 | 55 | const modules = []; 56 | const queries = []; 57 | 58 | const entryPoints = []; 59 | while (matches.length !== 0) { 60 | const matched = matches.pop(); 61 | const route = matched?.route; 62 | const match = matched?.match; 63 | entryPoints.push(route?.entryPoint); 64 | while (entryPoints.length !== 0) { 65 | const entryPoint = entryPoints.pop(); 66 | modules.push(entryPoint.root.getModuleId()); 67 | const props = entryPoint.getPreloadProps(match?.params); 68 | if (props.queries) { 69 | for (const queryname of Object.keys(props.queries)) { 70 | if (props.queries.hasOwnProperty(queryname)) { 71 | const query = props.queries[queryname]; 72 | queries.push(query); 73 | } 74 | } 75 | } 76 | 77 | if (props.entryPoints) { 78 | for (const subEntryPointName of Object.keys(props.entryPoints)) { 79 | if (props.entryPoints.hasOwnProperty(subEntryPointName)) { 80 | const subEntryPoint = props.entryPoints[subEntryPointName]; 81 | entryPoints.push(subEntryPoint); 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | return { modules, queries }; 89 | } 90 | -------------------------------------------------------------------------------- /relay-entrypoints/src/Shell.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { unstable_createRoot } from "react-dom"; 4 | import { nullThrows } from "./nullThrows"; 5 | import Router from "./Router"; 6 | 7 | const node = document.getElementById("app"); 8 | nullThrows(node, "no app node found"); 9 | const root = unstable_createRoot(node); 10 | 11 | root.render(); 12 | -------------------------------------------------------------------------------- /relay-entrypoints/src/__generated__/RepositoryQuery.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @ts-nocheck 4 | /* @relayHash 4a45f359f7fe4a783fae695058a98919 */ 5 | 6 | import { ConcreteRequest } from "relay-runtime"; 7 | export type RepositoryQueryVariables = { 8 | owner: string; 9 | name: string; 10 | }; 11 | export type RepositoryQueryResponse = { 12 | readonly repository: { 13 | readonly nameWithOwner: string; 14 | } | null; 15 | }; 16 | export type RepositoryQuery = { 17 | readonly response: RepositoryQueryResponse; 18 | readonly variables: RepositoryQueryVariables; 19 | }; 20 | 21 | 22 | 23 | /* 24 | query RepositoryQuery( 25 | $owner: String! 26 | $name: String! 27 | ) { 28 | repository(owner: $owner, name: $name) { 29 | nameWithOwner 30 | id 31 | } 32 | } 33 | */ 34 | 35 | const node: ConcreteRequest = (function(){ 36 | var v0 = { 37 | "defaultValue": null, 38 | "kind": "LocalArgument", 39 | "name": "name" 40 | }, 41 | v1 = { 42 | "defaultValue": null, 43 | "kind": "LocalArgument", 44 | "name": "owner" 45 | }, 46 | v2 = [ 47 | { 48 | "kind": "Variable", 49 | "name": "name", 50 | "variableName": "name" 51 | }, 52 | { 53 | "kind": "Variable", 54 | "name": "owner", 55 | "variableName": "owner" 56 | } 57 | ], 58 | v3 = { 59 | "alias": null, 60 | "args": null, 61 | "kind": "ScalarField", 62 | "name": "nameWithOwner", 63 | "storageKey": null 64 | }; 65 | return { 66 | "fragment": { 67 | "argumentDefinitions": [ 68 | (v0/*: any*/), 69 | (v1/*: any*/) 70 | ], 71 | "kind": "Fragment", 72 | "metadata": null, 73 | "name": "RepositoryQuery", 74 | "selections": [ 75 | { 76 | "alias": null, 77 | "args": (v2/*: any*/), 78 | "concreteType": "Repository", 79 | "kind": "LinkedField", 80 | "name": "repository", 81 | "plural": false, 82 | "selections": [ 83 | (v3/*: any*/) 84 | ], 85 | "storageKey": null 86 | } 87 | ], 88 | "type": "Query", 89 | "abstractKey": null 90 | }, 91 | "kind": "Request", 92 | "operation": { 93 | "argumentDefinitions": [ 94 | (v1/*: any*/), 95 | (v0/*: any*/) 96 | ], 97 | "kind": "Operation", 98 | "name": "RepositoryQuery", 99 | "selections": [ 100 | { 101 | "alias": null, 102 | "args": (v2/*: any*/), 103 | "concreteType": "Repository", 104 | "kind": "LinkedField", 105 | "name": "repository", 106 | "plural": false, 107 | "selections": [ 108 | (v3/*: any*/), 109 | { 110 | "alias": null, 111 | "args": null, 112 | "kind": "ScalarField", 113 | "name": "id", 114 | "storageKey": null 115 | } 116 | ], 117 | "storageKey": null 118 | } 119 | ] 120 | }, 121 | "params": { 122 | "id": "4a45f359f7fe4a783fae695058a98919", 123 | "metadata": {}, 124 | "name": "RepositoryQuery", 125 | "operationKind": "query", 126 | "text": null 127 | } 128 | }; 129 | })(); 130 | (node as any).hash = 'eec3c75ce7b55fdc389904b17ee1ba63'; 131 | export default node; 132 | -------------------------------------------------------------------------------- /relay-entrypoints/src/nullThrows.ts: -------------------------------------------------------------------------------- 1 | export function nullThrows( 2 | thing: T | null | undefined, 3 | message: string 4 | ): asserts thing is T { 5 | if (thing == null) { 6 | throw new Error(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /relay-entrypoints/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "jsx": "preserve" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /relay-entrypoints/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const https = require("https"); 4 | const fs = require("fs"); 5 | const bodyParser = require("body-parser"); 6 | 7 | module.exports = [ 8 | { 9 | mode: "development", 10 | entry: "./src/Shell.tsx", 11 | resolve: { 12 | extensions: [".ts", ".tsx", ".js"], 13 | }, 14 | output: { 15 | publicPath: "/", 16 | path: __dirname + "/out/client", 17 | }, 18 | devServer: { 19 | contentBase: __dirname + "/public", 20 | port: 8731, 21 | historyApiFallback: true, 22 | before: (app, server, compiler) => { 23 | app.use(bodyParser.json()); 24 | }, 25 | after: (app, server, compiler) => { 26 | app.post("/api/graphql", (req, res) => { 27 | if (req.header("content-type") === "application/json") { 28 | const body = req.body; 29 | fs.readFile("./queries.json", (err, data) => { 30 | const queries = JSON.parse(data); 31 | const postData = JSON.stringify({ 32 | query: queries[body.query] || body.query, 33 | variables: body.variables, 34 | }); 35 | const options = { 36 | hostname: "api.github.com", 37 | //port: 443, 38 | path: "/graphql", 39 | method: "POST", 40 | headers: { 41 | "User-Agent": req.header("user-agent"), 42 | Authorization: 43 | "Bearer " + process.env.REACT_APP_GITHUB_AUTH_TOKEN, 44 | "Content-Type": "application/json", 45 | "Content-Length": Buffer.byteLength(postData), 46 | }, 47 | }; 48 | 49 | const apiReq = https.request(options, (githubResponse) => { 50 | res.status(githubResponse.statusCode); 51 | githubResponse.setEncoding("utf8"); 52 | 53 | githubResponse.on("data", (chunk) => { 54 | res.write(chunk); 55 | }); 56 | githubResponse.on("end", () => { 57 | res.end(); 58 | }); 59 | }); 60 | 61 | apiReq.on("error", (error) => { 62 | res.status(500).send({ message: error.message }); 63 | }); 64 | apiReq.write(postData); 65 | apiReq.end(); 66 | }); 67 | } else { 68 | res.status(415).end(); 69 | } 70 | }); 71 | }, 72 | }, 73 | module: { 74 | rules: [ 75 | { 76 | test: /\.entrypoint\.ts$/, 77 | use: [ 78 | { 79 | loader: "./js-resource-loader", 80 | }, 81 | ], 82 | }, 83 | { 84 | test: /\.(tsx?)$/, 85 | exclude: /node_modules/, 86 | use: [ 87 | { 88 | loader: "babel-loader", 89 | options: { 90 | // extends: path.resolve(__dirname, "../babel.config.js") 91 | }, 92 | }, 93 | ], 94 | }, 95 | ], 96 | }, 97 | plugins: [ 98 | //new JSResourcePlugin(), 99 | new HtmlWebpackPlugin({ 100 | inject: false, 101 | templateContent: ({ htmlWebpackPlugin }) => ` 102 | 103 | 104 | ${htmlWebpackPlugin.tags.headTags} 105 | 106 | 107 |
108 | ${htmlWebpackPlugin.tags.bodyTags.slice(0, 1)} 109 | 110 | 111 | ${htmlWebpackPlugin.tags.bodyTags.slice(1)} 112 | 113 | 114 | `, 115 | }), 116 | ], 117 | optimization: { 118 | runtimeChunk: { 119 | name: (entrypoint) => `runtime~${entrypoint.name}`, 120 | }, 121 | }, 122 | }, 123 | { 124 | target: "node", 125 | mode: "development", 126 | entry: { server: "./src/ServerEntry.ts" }, 127 | resolve: { 128 | extensions: [".ts", ".tsx", ".js"], 129 | }, 130 | output: { 131 | libraryTarget: "commonjs", 132 | publicPath: "/", 133 | path: __dirname + "/out/server", 134 | }, 135 | module: { 136 | rules: [ 137 | { 138 | test: /\.entrypoint\.ts$/, 139 | use: [ 140 | { 141 | loader: "./js-resource-loader", 142 | }, 143 | ], 144 | }, 145 | { 146 | test: /\.(tsx?)$/, 147 | exclude: /node_modules/, 148 | use: [ 149 | { 150 | loader: "babel-loader", 151 | options: { 152 | // extends: path.resolve(__dirname, "../babel.config.js") 153 | }, 154 | }, 155 | ], 156 | }, 157 | ], 158 | }, 159 | }, 160 | ]; 161 | --------------------------------------------------------------------------------