├── web ├── .nvmrc ├── .eslintrc.json ├── src │ ├── react-app-env.d.ts │ ├── decs.d.ts │ ├── config │ │ ├── consts.ts │ │ └── templates.ts │ ├── styles │ │ ├── global.ts │ │ └── themes.ts │ ├── static │ │ ├── types.ts │ │ ├── safe-json.ts │ │ └── templates.ts │ ├── components │ │ ├── graph-viewer │ │ │ ├── log-bar.tsx │ │ │ ├── styles.ts │ │ │ ├── graph │ │ │ │ ├── directed-edge │ │ │ │ │ ├── utils.ts │ │ │ │ │ ├── styles.ts │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── vertice.tsx │ │ │ ├── loader.tsx │ │ │ ├── index.tsx │ │ │ └── progress-bar.tsx │ │ ├── app │ │ │ ├── footer.tsx │ │ │ └── index.tsx │ │ ├── carbon-ads │ │ │ ├── index.tsx │ │ │ └── carbon-ads.css │ │ └── function-form │ │ │ ├── code-editor │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ │ ├── styles.ts │ │ │ └── index.tsx │ ├── hooks │ │ ├── use-form-input.ts │ │ ├── use-interval.ts │ │ └── use-local-storage-state.ts │ ├── icons │ │ ├── next.tsx │ │ ├── previous.tsx │ │ ├── first.tsx │ │ ├── last.tsx │ │ └── logo.tsx │ ├── lib │ │ ├── either.ts │ │ └── google-analytics.tsx │ ├── api.ts │ ├── types.ts │ └── logic │ │ ├── extractors.test.ts │ │ ├── extractors.ts │ │ ├── language-handler.ts │ │ └── language-handler.test.ts ├── .env.template ├── public │ └── icon │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-150x150.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── browserconfig.xml │ │ ├── site.webmanifest │ │ └── safari-pinned-tab.svg ├── jest.config.js ├── .prettierrc ├── next-env.d.ts ├── next.config.js ├── app │ ├── registry.tsx │ ├── page.tsx │ └── layout.tsx ├── tsconfig.json └── package.json ├── lambda ├── .nvmrc ├── .dockerignore ├── jest.config.js ├── src │ ├── errors │ │ ├── common.ts │ │ ├── tree.ts │ │ └── child-process.ts │ ├── utils │ │ ├── object-map.ts │ │ └── either.ts │ ├── static │ │ ├── types.ts │ │ ├── safe-json.ts │ │ └── templates.ts │ ├── config.ts │ ├── runner │ │ ├── index.ts │ │ └── steps │ │ │ ├── initial-tree.ts │ │ │ ├── final-tree.ts │ │ │ ├── intermediate-tree.ts │ │ │ └── source-code.ts │ ├── index.ts │ ├── validations │ │ ├── stdout.ts │ │ └── event.ts │ └── types.ts ├── tests │ ├── safe-json.test.ts │ ├── utils │ │ └── object-map.test.ts │ ├── validations │ │ └── stdout.test.ts │ ├── runner │ │ ├── tree-viewer.test.ts │ │ └── user-code.test.ts │ └── handler.test.ts ├── Dockerfile ├── package.json └── tsconfig.json ├── .gitignore ├── terraform ├── outputs.tf ├── variables.tf └── main.tf ├── LICENSE ├── assets └── logo.svg └── README.md /web/.nvmrc: -------------------------------------------------------------------------------- 1 | 20.18.0 -------------------------------------------------------------------------------- /lambda/.nvmrc: -------------------------------------------------------------------------------- 1 | 14.15.0 -------------------------------------------------------------------------------- /lambda/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build -------------------------------------------------------------------------------- /web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next" 3 | } 4 | -------------------------------------------------------------------------------- /web/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/.env.template: -------------------------------------------------------------------------------- 1 | APP_AWS_REGION= 2 | APP_AWS_ACCESS_KEY= 3 | APP_AWS_SECRET_KEY= 4 | APP_AWS_LAMBDA_FUNCTION= -------------------------------------------------------------------------------- /web/public/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brpapa/recursion-tree-visualizer/HEAD/web/public/icon/favicon.ico -------------------------------------------------------------------------------- /web/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testMatch: ['**/*.test.ts'] 5 | } 6 | -------------------------------------------------------------------------------- /web/public/icon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brpapa/recursion-tree-visualizer/HEAD/web/public/icon/favicon-16x16.png -------------------------------------------------------------------------------- /web/public/icon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brpapa/recursion-tree-visualizer/HEAD/web/public/icon/favicon-32x32.png -------------------------------------------------------------------------------- /web/public/icon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brpapa/recursion-tree-visualizer/HEAD/web/public/icon/mstile-150x150.png -------------------------------------------------------------------------------- /lambda/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testMatch: ['**/*.test.ts'] 5 | } 6 | -------------------------------------------------------------------------------- /web/public/icon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brpapa/recursion-tree-visualizer/HEAD/web/public/icon/apple-touch-icon.png -------------------------------------------------------------------------------- /web/public/icon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brpapa/recursion-tree-visualizer/HEAD/web/public/icon/android-chrome-192x192.png -------------------------------------------------------------------------------- /web/public/icon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brpapa/recursion-tree-visualizer/HEAD/web/public/icon/android-chrome-512x512.png -------------------------------------------------------------------------------- /web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": true, 6 | "arrowParens": "always", 7 | "proseWrap": "never", 8 | "jsxSingleQuote": true 9 | } -------------------------------------------------------------------------------- /web/src/decs.d.ts: -------------------------------------------------------------------------------- 1 | import 'styled-components' 2 | import type { Theme } from './styles/themes' 3 | 4 | declare module 'styled-components' { 5 | export interface DefaultTheme extends Theme {} 6 | } 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | coverage 5 | .vscode 6 | *.DS_Store 7 | .env 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | .next 12 | *.tfvars 13 | *.tfstate* 14 | .terraform** -------------------------------------------------------------------------------- /web/src/config/consts.ts: -------------------------------------------------------------------------------- 1 | import { ThemeName } from '../styles/themes' 2 | import { Language } from '../types' 3 | 4 | export const DEFAULT_THEME_TYPE: ThemeName = 'light' 5 | 6 | export const LANGUAGES: Language[] = ['node', 'python', 'golang'] 7 | -------------------------------------------------------------------------------- /web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /lambda/src/errors/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used for expected errors, that is, deterministic and meaningful errors that convey the business logic and domain. For unexpected errors, like network or file access failures, throw exceptions. 3 | */ 4 | export interface Error { 5 | type: T 6 | reason: string 7 | } 8 | -------------------------------------------------------------------------------- /terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "ecr_repository_url" { 2 | value = aws_ecr_repository.this.repository_url 3 | } 4 | 5 | output "lambda_function_arn" { 6 | value = aws_lambda_function.this.arn 7 | } 8 | 9 | output "api_gw_url" { 10 | value = aws_api_gateway_deployment.api_deployment.invoke_url 11 | } 12 | -------------------------------------------------------------------------------- /web/public/icon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | /** @type {import('next').NextConfig} */ 3 | const config = { 4 | compiler: { 5 | styledComponents: true 6 | }, 7 | experimental: { 8 | typedRoutes: true, 9 | scrollRestoration: true, 10 | }, 11 | } 12 | 13 | return config 14 | } 15 | -------------------------------------------------------------------------------- /lambda/src/utils/object-map.ts: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | export const objectMap = ( 3 | obj: Record, 4 | callback: (value: T, key: string, index: number) => R 5 | ) => { 6 | return Object.fromEntries( 7 | Object.entries(obj).map( 8 | ([key, value], index) => [key, callback(value, key, index)] 9 | ) 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /web/src/styles/global.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components' 2 | 3 | export default createGlobalStyle` 4 | * { 5 | font-family: ${({ theme }) => theme.fonts.body}; 6 | color: ${({ theme }) => theme.colors.contrast}; 7 | font-size: 15px; 8 | } 9 | 10 | *, 11 | *:after, 12 | *:before { 13 | margin: 0; 14 | padding: 0; 15 | box-sizing: border-box; 16 | } 17 | 18 | *:focus { 19 | outline: none; 20 | } 21 | ` 22 | -------------------------------------------------------------------------------- /lambda/src/static/types.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE SHOULD BE SYMLINKED BETWEEN lambda/src/static/types.ts and web/src/static/types.ts, SO ANY CHANGES AFFECTS BOTH 2 | 3 | export type FunctionData = { 4 | body: string 5 | params?: Param[] 6 | returnType?: string 7 | globalVariables?: GlobalVar[] 8 | } 9 | 10 | export type Param = { 11 | name: string 12 | type?: string 13 | initialValue: string 14 | } 15 | 16 | export type GlobalVar = { 17 | name: string 18 | value: string 19 | } -------------------------------------------------------------------------------- /web/src/static/types.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE IS SYMLINKED BETWEEN packages/lambda/src/static/types.ts and packages/web/src/static/types.ts, SO ANY CHANGES AFFECTS BOTH 2 | 3 | export type FunctionData = { 4 | body: string 5 | params?: Param[] 6 | returnType?: string 7 | globalVariables?: GlobalVar[] 8 | } 9 | 10 | export type Param = { 11 | name: string 12 | type?: string 13 | initialValue: string 14 | } 15 | 16 | export type GlobalVar = { 17 | name: string 18 | value: string 19 | } -------------------------------------------------------------------------------- /lambda/src/config.ts: -------------------------------------------------------------------------------- 1 | import { SupportedLanguages } from './types' 2 | 3 | require('dotenv').config() 4 | 5 | export const supportedLanguages: SupportedLanguages[] = ['node', 'python', 'golang'] 6 | 7 | export const DEFAULT_TIMEOUT_MS = 28000 8 | export const DEFAULT_MAX_RECURSIVE_CALLS = 256 9 | export const DEFAULT_TMP_DIR_PATH = '/tmp' 10 | export const DEFAULT_TMP_FILE_MAX_SIZE_BYTES = 384e6 // because the size of ephemeral storage (/tmp folder) configured on AWS Lambda function is 512*10^6 bytes -------------------------------------------------------------------------------- /web/public/icon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /web/src/components/graph-viewer/log-bar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | type Props = { 5 | text: string 6 | } 7 | 8 | const LogBar = ({ text }: Props) => { 9 | return {text} 10 | } 11 | 12 | export default LogBar 13 | 14 | const Paragraph = styled.p` 15 | font-size: 15px; 16 | flex-grow: 0; 17 | text-align: center; 18 | font-weight: bold; 19 | font-family: ${({ theme }) => theme.fonts.mono}; 20 | color: ${({ theme }) => theme.colors.primary}; 21 | ` 22 | -------------------------------------------------------------------------------- /lambda/tests/safe-json.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from '@jest/globals' 2 | import { safeParse, safeStringify } from '../src/static/safe-json' 3 | 4 | describe('Json issues when a function call return +/- Infinity number', () => { 5 | const obj = { a: NaN, b: -Infinity, c: Infinity } 6 | const text = '{"a":"NaN","b":"-Infinity","c":"Infinity"}' 7 | 8 | it('Safe stringify', () => { 9 | expect(safeStringify(obj)).toEqual(text) 10 | }) 11 | 12 | it('Safe parse', () => { 13 | expect(safeParse(text)).toEqual(obj) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "profile" { 2 | type = string 3 | default = "default" 4 | } 5 | 6 | variable "region" { 7 | type = string 8 | } 9 | 10 | variable "aws_account_id" { 11 | type = string 12 | } 13 | 14 | variable "aws_access_key" { 15 | type = string 16 | sensitive = true 17 | } 18 | 19 | variable "aws_secret_key" { 20 | type = string 21 | sensitive = true 22 | } 23 | 24 | variable "image_name" { 25 | type = string 26 | } 27 | 28 | variable "function_name" { 29 | type = string 30 | } 31 | 32 | variable "api_name" { 33 | type = string 34 | } 35 | -------------------------------------------------------------------------------- /web/src/config/templates.ts: -------------------------------------------------------------------------------- 1 | import { Template, FunctionData, Language } from '../types' 2 | import { templates as staticTemplates } from '../static/templates' 3 | 4 | type TemplateData = { name: string; fnData: Record } 5 | 6 | const templates: Record = { 7 | custom: { 8 | name: 'Custom', 9 | fnData: { 10 | node: { 11 | body: ' // type your own code here', 12 | }, 13 | python: { 14 | body: ' # type your own code here', 15 | }, 16 | golang: { 17 | body: ' // type your own code here', 18 | }, 19 | }, 20 | }, 21 | ...staticTemplates, 22 | } 23 | 24 | export default templates 25 | -------------------------------------------------------------------------------- /lambda/tests/utils/object-map.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from '@jest/globals' 2 | import { objectMap } from '../../src/utils/object-map' 3 | 4 | describe('Object map', () => { 5 | it('Should be handle with string key', () => { 6 | const obj = { a: 1, b: 2 } 7 | 8 | const mapped1 = objectMap(obj, (v) => 2 * v) 9 | expect(mapped1).toEqual({ a: 2, b: 4 }) 10 | expect(obj === mapped1).toBeFalsy() 11 | 12 | const mapped2 = objectMap(obj, (v, k) => v + k) 13 | expect(mapped2).toEqual({ a: '1a', b: '2b' }) 14 | }) 15 | // it('should be handle with number key', () => { 16 | // expect(objectMap({ 0: 1, 1: 2 }, (v) => 2 * v)).toEqual({ 0: 2, 1: 4 }) 17 | // }) 18 | }) 19 | -------------------------------------------------------------------------------- /web/src/components/graph-viewer/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { LogoIcon as LogoIconBase } from '../../icons/logo' 3 | 4 | export const Container = styled.div` 5 | flex-grow: 1; 6 | 7 | height: calc(100% - 1.6em); 8 | border: 1px solid ${({ theme }) => theme.colors.border}; 9 | background-color: ${({ theme }) => theme.colors.foreground}; 10 | border-radius: 8px; 11 | box-shadow: 10px 10px 50px rgb(120, 120, 120, 0.05); 12 | 13 | display: flex; 14 | flex-direction: column; 15 | ` 16 | 17 | export const LogoIcon = styled(LogoIconBase)` 18 | width: 100%; 19 | height: 100%; 20 | padding: 5em; 21 | color: ${({ theme }) => theme.colors.contrast}; 22 | opacity: 0.03; 23 | ` -------------------------------------------------------------------------------- /web/src/hooks/use-form-input.ts: -------------------------------------------------------------------------------- 1 | import useLocalStorageState from './use-local-storage-state' 2 | 3 | type HTMLELements = HTMLSelectElement | HTMLTextAreaElement | HTMLInputElement 4 | 5 | /* with local storage persistence by default */ 6 | const useFormInput = ( 7 | localStorageKey: string, 8 | initialValue: string, 9 | validate?: (value: string) => boolean 10 | ) => { 11 | const [value, setValue] = useLocalStorageState(localStorageKey, initialValue) 12 | 13 | function onChange(e: React.ChangeEvent) { 14 | if (validate && !validate(e.target.value)) return 15 | setValue(e.target.value) 16 | } 17 | 18 | return [{ value, onChange }, setValue] as const 19 | } 20 | 21 | export default useFormInput 22 | -------------------------------------------------------------------------------- /web/src/icons/next.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SVGProps } from 'react' 3 | 4 | export const NextIcon = (props: SVGProps) => ( 5 | 21 | ) 22 | 23 | 24 | -------------------------------------------------------------------------------- /web/src/icons/previous.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SVGProps } from 'react' 3 | 4 | export const PreviousIcon = (props: SVGProps) => ( 5 | 21 | ) 22 | 23 | 24 | -------------------------------------------------------------------------------- /lambda/Dockerfile: -------------------------------------------------------------------------------- 1 | # This base image already contains the Amazon Lambda Runtime Interface Client (RIC) for run server in production, and the Runtime Interface Emulator (RIE) for run server locally 2 | FROM amazon/aws-lambda-nodejs:14 3 | 4 | RUN yum -y install \ 5 | python3.x86_64 \ 6 | golang.x86_64 7 | 8 | RUN node --version && \ 9 | python3 --version && \ 10 | go version 11 | 12 | # /tmp is the only writable directory for any lambda function, and golang writes build cache to path defined by this env 13 | ENV XDG_CACHE_HOME '/tmp/.cache' 14 | 15 | ENV DEBUG 'app:*' 16 | 17 | COPY ["package.json", "package-lock.json*", "${LAMBDA_TASK_ROOT}/"] 18 | RUN npm install 19 | 20 | COPY . ${LAMBDA_TASK_ROOT} 21 | RUN npm run build 22 | 23 | CMD ["dist/index.handler"] 24 | -------------------------------------------------------------------------------- /web/src/components/app/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const Footer = () => { 5 | return ( 6 | 7 | Made with ♥ by Bruno Papa{' '}•{' '} 8 | 12 | Github 13 | 14 | 15 | ) 16 | } 17 | 18 | export default Footer 19 | 20 | export const StyledFooter = styled.footer` 21 | font-size: 0.8em; 22 | flex-grow: 0; 23 | text-align: center; 24 | margin-top: 0.5em; 25 | 26 | opacity: 0.35; 27 | color: ${({ theme }) => theme.colors.contrast}; 28 | a { 29 | color: ${({ theme }) => theme.colors.contrast}; 30 | } 31 | ` 32 | -------------------------------------------------------------------------------- /web/src/hooks/use-interval.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react' 2 | 3 | // executa callback enquanto delay não for null 4 | const useInterval = (callback: () => void, delay: number | null) => { 5 | const latestCallback = useRef(() => {}) 6 | 7 | useEffect(() => { 8 | latestCallback.current = callback 9 | }, [callback]) 10 | 11 | // reinicia o intervalo com um novo delay se delay mudar para algum valor não nulo 12 | // não reinicia o intervalo se a callback mudar, mas latestCallback sempre refencia a última callback 13 | useEffect(() => { 14 | if (delay !== null) { 15 | const id = setInterval(() => latestCallback.current(), delay) 16 | return () => clearInterval(id) 17 | } 18 | }, [delay]) 19 | } 20 | 21 | export default useInterval 22 | -------------------------------------------------------------------------------- /web/src/hooks/use-local-storage-state.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | type Return = [T, React.Dispatch>] 4 | 5 | const isBrowser = typeof window !== 'undefined' 6 | 7 | const useLocalStorageState = (key: string, initialValue: T): Return => { 8 | const [storedValue, setStoredValue] = React.useState(() => { 9 | if (!isBrowser) return initialValue 10 | 11 | const value = localStorage.getItem(key) 12 | // prettier-ignore 13 | return value 14 | ? JSON.parse(value) 15 | : initialValue instanceof Function 16 | ? initialValue() 17 | : initialValue 18 | }) 19 | 20 | React.useEffect(() => { 21 | if (!isBrowser) return 22 | 23 | localStorage.setItem(key, JSON.stringify(storedValue)) 24 | }, [storedValue, key]) 25 | 26 | return [storedValue, setStoredValue] 27 | } 28 | 29 | export default useLocalStorageState 30 | -------------------------------------------------------------------------------- /web/src/icons/first.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SVGProps } from 'react' 3 | 4 | export const FirstIcon = (props: SVGProps) => ( 5 | 21 | ) 22 | -------------------------------------------------------------------------------- /web/src/icons/last.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SVGProps } from 'react' 3 | 4 | export const LastIcon = (props: SVGProps) => ( 5 | 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /web/src/components/carbon-ads/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | import './carbon-ads.css' 3 | 4 | export function CarbonAds() { 5 | const divRefAds = useCarbonAds() 6 | 7 | return
8 | } 9 | 10 | // retorna uma ref para o nó do dom