├── .prettierignore ├── packages ├── create │ ├── .npmignore │ ├── .gitignore │ ├── template │ │ ├── gitignore │ │ ├── .eslintrc.json │ │ ├── src │ │ │ ├── content │ │ │ │ └── site.toml │ │ │ ├── styles │ │ │ │ └── style.css │ │ │ └── pages │ │ │ │ └── index.jsx │ │ ├── radish.env.d.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── bin │ │ └── cli.js │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── argv.ts │ │ └── init.ts ├── docs │ ├── src │ │ ├── content │ │ │ ├── site.toml │ │ │ └── docs │ │ │ │ ├── guides │ │ │ │ ├── section.toml │ │ │ │ ├── offline.md │ │ │ │ ├── assets.md │ │ │ │ ├── pages.md │ │ │ │ └── content.md │ │ │ │ └── getting-started │ │ │ │ ├── section.toml │ │ │ │ ├── getting-started.md │ │ │ │ ├── folder-structure.md │ │ │ │ └── overview.md │ │ ├── components │ │ │ ├── Footer │ │ │ │ ├── style.module.css │ │ │ │ └── index.tsx │ │ │ ├── Sidebar │ │ │ │ ├── night.svg │ │ │ │ ├── menu.react.svg │ │ │ │ ├── day.svg │ │ │ │ ├── github.react.svg │ │ │ │ ├── bullet.svg │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ ├── Button │ │ │ │ ├── index.tsx │ │ │ │ ├── primary.svg │ │ │ │ ├── ghost-day.svg │ │ │ │ ├── ghost-night.svg │ │ │ │ └── style.module.css │ │ │ └── Head.tsx │ │ ├── images │ │ │ ├── favicon.ico │ │ │ ├── blob2-night.svg │ │ │ ├── blob1-night.svg │ │ │ ├── blob2-day.svg │ │ │ ├── blob1-day.svg │ │ │ ├── blob3-day.svg │ │ │ ├── blob3-night.svg │ │ │ ├── wordmark.svg │ │ │ └── logo.svg │ │ ├── styles │ │ │ ├── grandstander.woff2 │ │ │ ├── odudomono-regular.woff2 │ │ │ ├── odudomono-semibold.woff2 │ │ │ ├── fonts.css │ │ │ ├── reset.css │ │ │ ├── syntax.css │ │ │ └── style.css │ │ ├── pages │ │ │ ├── docs │ │ │ │ ├── left.react.svg │ │ │ │ ├── right.react.svg │ │ │ │ ├── style.module.css │ │ │ │ └── $index.tsx │ │ │ ├── style.module.css │ │ │ └── index.tsx │ │ ├── hooks │ │ │ └── useContent.ts │ │ └── js │ │ │ └── index.bundle.ts │ ├── .gitignore │ ├── radish.env.d.ts │ ├── .eslintrc.json │ ├── package.json │ └── tsconfig.json ├── radish │ ├── .gitignore │ ├── src │ │ ├── core │ │ │ ├── inject.js │ │ │ ├── esm.ts │ │ │ ├── types.ts │ │ │ ├── websocket.ts │ │ │ ├── svg.ts │ │ │ ├── loaders.ts │ │ │ ├── render.ts │ │ │ ├── index.ts │ │ │ ├── js.ts │ │ │ ├── page.ts │ │ │ ├── result.ts │ │ │ ├── serve.ts │ │ │ ├── css.ts │ │ │ ├── content.ts │ │ │ └── bundle.ts │ │ ├── lib │ │ │ ├── internal.d.ts │ │ │ ├── Head.tsx │ │ │ ├── index.ts │ │ │ ├── Document.tsx │ │ │ └── sw.ts │ │ ├── util │ │ │ ├── ansi.ts │ │ │ ├── fetch.ts │ │ │ └── argv.ts │ │ └── cli │ │ │ └── index.ts │ ├── .npmignore │ ├── bin │ │ └── cli.js │ ├── tsconfig.json │ ├── README.md │ ├── .eslintrc.json │ ├── package.json │ └── global.d.ts ├── prettier │ ├── .prettierrc.json │ └── package.json ├── eslint │ ├── .npmignore │ ├── package.json │ └── index.js └── tsconfig │ ├── package.json │ └── tsconfig.json ├── .husky └── pre-commit ├── .gitignore ├── README.md ├── package.json ├── .github └── workflows │ └── docs.yml └── images └── logo.svg /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /packages/create/.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/create/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /packages/docs/src/content/site.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/radish/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /packages/docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /packages/create/template/gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /packages/create/template/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": ["radish"] } 2 | -------------------------------------------------------------------------------- /packages/docs/radish.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/radish/src/core/inject.js: -------------------------------------------------------------------------------- 1 | export * as React from "react"; 2 | -------------------------------------------------------------------------------- /packages/create/template/src/content/site.toml: -------------------------------------------------------------------------------- 1 | title = "My Radish Site" 2 | -------------------------------------------------------------------------------- /packages/create/template/radish.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/guides/section.toml: -------------------------------------------------------------------------------- 1 | order = 2 2 | title = "Guides" 3 | -------------------------------------------------------------------------------- /packages/create/template/src/styles/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /packages/prettier/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { "trailingComma": "none", "arrowParens": "avoid" } 2 | -------------------------------------------------------------------------------- /packages/docs/src/components/Footer/style.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | text-align: center; 3 | } 4 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/getting-started/section.toml: -------------------------------------------------------------------------------- 1 | order = 1 2 | title = "Getting Started" 3 | -------------------------------------------------------------------------------- /packages/eslint/.npmignore: -------------------------------------------------------------------------------- 1 | # macos 2 | .DS_Store 3 | 4 | # node 5 | node_modules 6 | yarn-error.log 7 | -------------------------------------------------------------------------------- /packages/docs/src/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakelazaroff/radish/HEAD/packages/docs/src/images/favicon.ico -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn prettier --ignore-unknown --write . 5 | git add -A . 6 | -------------------------------------------------------------------------------- /packages/docs/src/styles/grandstander.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakelazaroff/radish/HEAD/packages/docs/src/styles/grandstander.woff2 -------------------------------------------------------------------------------- /packages/docs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["radish"], 3 | "rules": { 4 | "@typescript-eslint/no-non-null-assertion": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/docs/src/styles/odudomono-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakelazaroff/radish/HEAD/packages/docs/src/styles/odudomono-regular.woff2 -------------------------------------------------------------------------------- /packages/docs/src/styles/odudomono-semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakelazaroff/radish/HEAD/packages/docs/src/styles/odudomono-semibold.woff2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macos 2 | .DS_Store 3 | 4 | # node 5 | .npmrc 6 | node_modules 7 | yarn-error.log 8 | 9 | # typescript 10 | tsconfig.tsbuildinfo 11 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radish/tsconfig", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "main": "tsconfig.json" 6 | } 7 | -------------------------------------------------------------------------------- /packages/radish/src/lib/internal.d.ts: -------------------------------------------------------------------------------- 1 | declare module "CONTENT_INDEX" { 2 | export const content: any; // eslint-disable-line @typescript-eslint/no-explicit-any 3 | } 4 | -------------------------------------------------------------------------------- /packages/radish/.npmignore: -------------------------------------------------------------------------------- 1 | # macos 2 | .DS_Store 3 | 4 | # node 5 | .npmrc 6 | yarn-error.log 7 | tsconfig.tsbuildinfo 8 | 9 | # radish 10 | tsconfig.json 11 | src 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Radish 3 |
4 | 5 | Thanks for checking out Radish! For documentation, check out the [website](https://radishjs.com). 6 | -------------------------------------------------------------------------------- /packages/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radish/prettier", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "main": ".prettierrc.json", 6 | "devDependencies": { 7 | "prettier": "^2.6.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/radish/src/core/esm.ts: -------------------------------------------------------------------------------- 1 | export default function esm(src: string): Promise { 2 | const dataUri = 3 | "data:text/javascript;charset=utf-8," + encodeURIComponent(src); 4 | return import(dataUri); 5 | } 6 | -------------------------------------------------------------------------------- /packages/create/bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import cli from "../build/index.js"; 3 | 4 | import { createRequire } from "module"; 5 | const { version } = createRequire(import.meta.url)("../package.json"); 6 | 7 | cli(process.argv.slice(2), version); 8 | -------------------------------------------------------------------------------- /packages/create/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@radish/tsconfig/tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "build" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "build"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/radish/bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import cli from "../build/cli/index.js"; 3 | 4 | import { createRequire } from "module"; 5 | const { version } = createRequire(import.meta.url)("../package.json"); 6 | 7 | cli(process.argv.slice(2), version); 8 | -------------------------------------------------------------------------------- /packages/radish/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@radish/tsconfig/tsconfig", 3 | "compilerOptions": { 4 | "lib": ["esnext", "webworker", "dom.iterable"], 5 | "outDir": "build", 6 | "allowJs": true 7 | }, 8 | "include": ["src/**/*"], 9 | "exclude": ["node_modules", "build"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/radish/README.md: -------------------------------------------------------------------------------- 1 | # Radish 2 | 3 | Radish is a React-based static site generator that outputs plain HTML and CSS. 4 | 5 | To get started, run this in your terminal: 6 | 7 | ``` 8 | npx create-radish my-website 9 | ``` 10 | 11 | For documentation, check out the [website](https://radishjs.com). 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "radish-farm", 4 | "license": "MIT", 5 | "prettier": "@radish/prettier", 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "devDependencies": { 10 | "husky": "^7.0.0" 11 | }, 12 | "scripts": { 13 | "prepare": "husky install" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/create/template/src/pages/index.jsx: -------------------------------------------------------------------------------- 1 | import { Head, useContent } from "radish"; 2 | 3 | export default function Index() { 4 | const content = useContent(); 5 | 6 | return ( 7 | <> 8 | 9 | {content.site.title} 10 | 11 |

{content.site.title}

12 |

Hello, world!

13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/radish/src/core/types.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentType } from "react"; 2 | import type { HelmetServerState } from "react-helmet-async"; 3 | 4 | import type { Props } from "../lib/Document"; 5 | 6 | export type PageProps = Props; 7 | 8 | export interface Page { 9 | default: ComponentType; 10 | paths?(content: any): string[]; 11 | head: { helmet: HelmetServerState }; 12 | layout?: boolean; 13 | } 14 | -------------------------------------------------------------------------------- /packages/docs/src/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | // lib 2 | import clsx from "clsx"; 3 | 4 | import css from "./style.module.css"; 5 | 6 | interface Props { 7 | className?: string; 8 | } 9 | 10 | export default function Footer(props: Props) { 11 | const { className } = props; 12 | 13 | return ( 14 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/radish/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { "node": true }, 3 | "plugins": ["@typescript-eslint"], 4 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 5 | "parser": "@typescript-eslint/parser", 6 | "parserOptions": { 7 | "ecmaFeatures": { "jsx": true }, 8 | "ecmaVersion": "latest", 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "ban-ts-comment": { 13 | "ts-ignore": "allow-with-description" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/docs/src/components/Sidebar/night.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /packages/radish/src/core/websocket.ts: -------------------------------------------------------------------------------- 1 | // lib 2 | import { WebSocketServer } from "ws"; 3 | 4 | interface WebSocketOptions { 5 | port: number; 6 | } 7 | 8 | export function websocket(options: WebSocketOptions) { 9 | const wss = new WebSocketServer({ 10 | port: options.port 11 | }); 12 | 13 | return { 14 | refresh() { 15 | for (const client of wss.clients) 16 | client.send(JSON.stringify({ type: "refresh" })); 17 | }, 18 | close() { 19 | wss.close(); 20 | } 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-radish", 3 | "version": "0.1.5", 4 | "main": "./index.js", 5 | "license": "MIT", 6 | "repository": "https://github.com/jakelazaroff/radish", 7 | "homepage": "https://radishjs.com", 8 | "devDependencies": { 9 | "@typescript-eslint/eslint-plugin": "^5.14.0", 10 | "@typescript-eslint/parser": "^5.14.0", 11 | "eslint-plugin-jsx-a11y": "^6.5.1", 12 | "eslint-plugin-react": "^7.29.3" 13 | }, 14 | "peerDependencies": { 15 | "eslint": "^8.10.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/radish/src/core/svg.ts: -------------------------------------------------------------------------------- 1 | // sys 2 | import * as fs from "node:fs"; 3 | 4 | // lib 5 | import type { Plugin } from "esbuild"; 6 | import { transform } from "@svgr/core"; 7 | 8 | export const svgPlugin: Plugin = { 9 | name: "svg", 10 | setup(build) { 11 | build.onLoad({ filter: /\.react\.svg$/ }, async args => { 12 | const svg = await fs.promises.readFile(args.path, "utf-8"); 13 | const contents = await transform(svg, {}, { filePath: args.path }); 14 | 15 | return { contents, loader: "jsx" }; 16 | }); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /packages/docs/src/components/Sidebar/menu.react.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | -------------------------------------------------------------------------------- /packages/docs/src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import clsx from "clsx"; 3 | 4 | import css from "./style.module.css"; 5 | 6 | interface Props { 7 | children: ReactNode; 8 | href: string; 9 | kind?: "primary" | "ghost"; 10 | } 11 | 12 | export default function Button(props: Props) { 13 | const { children, href, kind = "primary" } = props; 14 | 15 | return ( 16 | 20 | {children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/eslint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { node: true }, 3 | plugins: ["react", "@typescript-eslint", "jsx-a11y"], 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react/recommended", 8 | "plugin:react/jsx-runtime", 9 | "plugin:jsx-a11y/recommended" 10 | ], 11 | parser: "@typescript-eslint/parser", 12 | parserOptions: { 13 | ecmaFeatures: { jsx: true }, 14 | ecmaVersion: "latest", 15 | sourceType: "module" 16 | }, 17 | settings: { 18 | react: { 19 | version: "detect" 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - run: yarn 15 | working-directory: packages/radish 16 | 17 | - run: yarn build 18 | working-directory: packages/docs 19 | 20 | - uses: peaceiris/actions-gh-pages@v3 21 | with: 22 | publish_branch: pages 23 | publish_dir: packages/docs/build 24 | cname: radishjs.com 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /packages/docs/src/pages/docs/left.react.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/docs/src/components/Button/primary.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /packages/create/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "radish-template", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "radish": "^0.1.25", 8 | "react": "^17.0.2", 9 | "react-dom": "^17.0.2" 10 | }, 11 | "devDependencies": { 12 | "@types/react": "^17.0.40", 13 | "@types/react-dom": "^17.0.13", 14 | "eslint": "^8.10.0", 15 | "eslint-config-radish": "^0.1.3", 16 | "typescript": "^4.6.2" 17 | }, 18 | "scripts": { 19 | "clean": "rm -rf build", 20 | "build": "radish build", 21 | "dev": "radish dev", 22 | "lint": "radish lint" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/docs/src/pages/docs/right.react.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/docs/src/styles/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Grandstander"; 3 | src: url("./grandstander.woff2") format("woff2 supports variations"), 4 | url("./grandstander.woff2") format("woff2-variations"); 5 | font-weight: 100 1000; 6 | font-stretch: 25% 151%; 7 | font-display: swap; 8 | } 9 | 10 | @font-face { 11 | font-family: "Odudo Mono"; 12 | src: url("./odudomono-regular.woff2") format("woff2"); 13 | font-weight: 400; 14 | font-display: swap; 15 | } 16 | 17 | @font-face { 18 | font-family: "Odudo Mono"; 19 | src: url("./odudomono-semibold.woff2") format("woff2"); 20 | font-weight: 600; 21 | font-display: swap; 22 | } 23 | -------------------------------------------------------------------------------- /packages/docs/src/styles/reset.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | * { 8 | margin: 0; 9 | line-height: calc(1em + 1.4rem); 10 | } 11 | 12 | html, 13 | body { 14 | height: 100%; 15 | } 16 | 17 | html { 18 | font-size: 62.5%; 19 | } 20 | 21 | body { 22 | font-size: 1.6rem; 23 | -webkit-font-smoothing: antialiased; 24 | } 25 | 26 | img, 27 | picture, 28 | video, 29 | canvas, 30 | svg { 31 | display: block; 32 | max-width: 100%; 33 | } 34 | 35 | input, 36 | button, 37 | textarea, 38 | select { 39 | font: inherit; 40 | color: inherit; 41 | } 42 | 43 | p, 44 | h1, 45 | h2, 46 | h3, 47 | h4, 48 | h5, 49 | h6 { 50 | overflow-wrap: break-word; 51 | } 52 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/getting-started/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 2 3 | title: Getting Started 4 | --- 5 | 6 | Getting started with Radish is easy! Radish has a project generator that will create a folder with everything you need. 7 | 8 | ```txt 9 | npx create-radish my-website 10 | cd my-website 11 | npm install 12 | npm run dev 13 | ``` 14 | 15 | Or, if you use [Yarn](https://yarnpkg.com): 16 | 17 | ```txt 18 | yarn create radish my-website 19 | cd my-website 20 | yarn 21 | yarn dev 22 | ``` 23 | 24 | The default project uses JavaScript. To create a TypeScript project instead, just add `--typescript` to the create command. 25 | 26 | Once your project is created, visit [http://localhost:8000](http://localhost:8000) to get started! 27 | -------------------------------------------------------------------------------- /packages/create/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-radish", 3 | "version": "0.1.13", 4 | "type": "module", 5 | "module": "build/index.js", 6 | "license": "MIT", 7 | "repository": "https://github.com/jakelazaroff/radish", 8 | "homepage": "https://radishjs.com", 9 | "prettier": "@radish/prettier", 10 | "files": [ 11 | "bin", 12 | "build", 13 | "template" 14 | ], 15 | "devDependencies": { 16 | "@radish/prettier": "*", 17 | "@radish/tsconfig": "*", 18 | "typescript": "^4.6.2" 19 | }, 20 | "scripts": { 21 | "clean": "rm -rf build tsconfig.tsbuildinfo", 22 | "build": "yarn clean && yarn tsc", 23 | "prepare": "yarn build" 24 | }, 25 | "bin": { 26 | "create-radish": "./bin/cli.js" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "prettier": "@radish/prettier", 7 | "dependencies": { 8 | "clsx": "^1.1.1", 9 | "radish": "^0.2.8", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0" 12 | }, 13 | "devDependencies": { 14 | "@radish/prettier": "*", 15 | "@radish/tsconfig": "*", 16 | "@types/react": "^18.0.25", 17 | "@types/react-dom": "^18.0.9", 18 | "eslint": "^8.10.0", 19 | "eslint-config-radish": "^0.1.3", 20 | "typescript": "^4.6.2" 21 | }, 22 | "scripts": { 23 | "clean": "rm -rf build", 24 | "build": "radish build", 25 | "dev": "radish dev", 26 | "lint": "radish lint" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/radish/src/core/loaders.ts: -------------------------------------------------------------------------------- 1 | export const images = { 2 | ".png": "file", 3 | ".gif": "file", 4 | ".ico": "file", 5 | ".jpg": "file", 6 | ".jpeg": "file", 7 | ".svg": "file", 8 | ".webp": "file" 9 | } as const; 10 | 11 | export const audio = { 12 | ".aac": "file", 13 | ".flac": "file", 14 | ".mp3": "file", 15 | ".ogg": "file", 16 | ".wav": "file" 17 | } as const; 18 | 19 | export const video = { 20 | ".mp4": "file", 21 | ".webm": "file" 22 | } as const; 23 | 24 | export const fonts = { 25 | ".eot": "file", 26 | ".otf": "file", 27 | ".ttf": "file", 28 | ".woff": "file", 29 | ".woff2": "file" 30 | } as const; 31 | 32 | export default { 33 | ...images, 34 | ...audio, 35 | ...video, 36 | ...fonts 37 | }; 38 | -------------------------------------------------------------------------------- /packages/docs/src/components/Head.tsx: -------------------------------------------------------------------------------- 1 | import { Head } from "radish"; 2 | 3 | import favicon from "images/favicon.ico"; 4 | 5 | interface Props { 6 | title?: string; 7 | } 8 | 9 | export default function DocHead(props: Props) { 10 | const { title = "Radish" } = props; 11 | 12 | return ( 13 | 14 | 15 | {title} 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | const colorScheme = ` 24 | const scheme = localStorage.getItem("colorscheme"); 25 | if (["day", "night"].includes(scheme)) document.documentElement.classList.add(scheme); 26 | `.trim(); 27 | -------------------------------------------------------------------------------- /packages/radish/src/lib/Head.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { 3 | Helmet, 4 | HelmetProvider, 5 | HtmlProps, 6 | BodyProps 7 | } from "react-helmet-async"; 8 | 9 | interface Props { 10 | children: ReactNode; 11 | html?: HtmlProps; 12 | body?: BodyProps; 13 | } 14 | 15 | export default function Head(props: Props) { 16 | const { children, html, body } = props; 17 | 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | } 24 | 25 | export function HeadProvider(props: Props) { 26 | return ( 27 | 28 | {props.children} 29 | 30 | ); 31 | } 32 | 33 | HeadProvider.context = {}; 34 | -------------------------------------------------------------------------------- /packages/radish/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // lib 2 | import type { ComponentType } from "react"; 3 | 4 | // radish 5 | import { content } from "CONTENT_INDEX"; 6 | 7 | interface AnyMap { 8 | [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any 9 | } 10 | 11 | export type Content = T & { 12 | Component: ComponentType; 13 | }; 14 | 15 | export interface ContentMap { 16 | [key: string]: Content | ContentMap; 17 | } 18 | 19 | export interface ResourcePageProps { 20 | path: string; 21 | } 22 | 23 | export type ResourcePage = ComponentType; 24 | export type Paths = (content: T) => string[]; 25 | 26 | export function useContent(): T { 27 | return content; 28 | } 29 | 30 | export { default as Head, HeadProvider } from "./Head"; 31 | -------------------------------------------------------------------------------- /packages/docs/src/components/Button/ghost-day.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | -------------------------------------------------------------------------------- /packages/docs/src/components/Button/ghost-night.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | -------------------------------------------------------------------------------- /packages/docs/src/images/blob2-night.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /packages/docs/src/images/blob1-night.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/getting-started/folder-structure.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 3 3 | title: Folder Structure 4 | --- 5 | 6 | A Radish project's folder structure looks like this: 7 | 8 | ```txt 9 | build/ 10 | ┗━ public/ 11 | src/ 12 | ┣━ content/ 13 | ┣━ pages/ 14 | ┗━ styles/ 15 | ┗━ style.css 16 | ``` 17 | 18 | - All your source code goes in `src` 19 | - `src/pages` is a special folder; Radish turns all the React component files in here into static HTML pages 20 | - `src/content` is a special folder; Radish reads all the files in here and provides it to your pages as a JavaScript object 21 | - `src/styles/style.css` is a special file; Radish uses this to import all your styles with a `` tag 22 | - When you build your site, all the HTML files go in `build` 23 | - When you build your site, all the public assets (CSS, JS, images, etc) go in `build/public` 24 | -------------------------------------------------------------------------------- /packages/docs/src/images/blob2-day.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /packages/docs/src/components/Sidebar/day.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /packages/docs/src/images/blob1-day.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /packages/radish/src/util/ansi.ts: -------------------------------------------------------------------------------- 1 | type Primitive = string | number; 2 | // prettier-ignore 3 | type ColorFn> = (arg?: T) => 4 | T extends Primitive ? string : ColorFn; 5 | 6 | /** Create an ANSI color */ 7 | function fn(seq: number) { 8 | return >(arg?: T): T => { 9 | // if the argument is a function, prefix that function's return value with the ANSI code 10 | if (typeof arg === "function") 11 | return ((arg2: T) => `\u001b[${seq}m${arg(arg2)}`) as T; 12 | 13 | // if the argument is a primitive, prefix it with the ANSI code and follow it with a reset code 14 | return `\u001b[${seq}m${arg}\u001b[0m` as T; 15 | }; 16 | } 17 | 18 | export const underline = fn(4); 19 | export const red = fn(31); 20 | export const green = fn(32); 21 | export const yellow = fn(33); 22 | export const cyan = fn(96); 23 | export const bold = fn(1); 24 | export const dim = fn(2); 25 | -------------------------------------------------------------------------------- /packages/tsconfig/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["esnext"], 4 | "skipLibCheck": true, 5 | "target": "esnext", 6 | 7 | // build options 8 | "jsx": "react-jsx", 9 | "removeComments": true, 10 | "declaration": true, 11 | 12 | // module resolution 13 | "module": "es2020", 14 | "moduleResolution": "node", 15 | "isolatedModules": true, 16 | 17 | // ecmascript features 18 | "downlevelIteration": true, 19 | "esModuleInterop": true, 20 | "allowSyntheticDefaultImports": true, 21 | 22 | // strict typechecking 23 | "strict": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "noImplicitAny": true, 26 | "noImplicitReturns": true, 27 | "noImplicitThis": true, 28 | "noUnusedLocals": true, 29 | "noUnusedParameters": true, 30 | "importsNotUsedAsValues": "error", 31 | "forceConsistentCasingInFileNames": true, 32 | "noUncheckedIndexedAccess": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/create/template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["esnext"], 5 | "skipLibCheck": true, 6 | 7 | // build options 8 | "baseUrl": "src", 9 | "noEmit": true, 10 | 11 | // module resolution 12 | "module": "es2020", 13 | "moduleResolution": "node", 14 | "isolatedModules": true, 15 | 16 | // ecmascript features 17 | "downlevelIteration": true, 18 | "esModuleInterop": true, 19 | 20 | // strict typechecking 21 | "strict": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noImplicitAny": true, 24 | "noImplicitReturns": true, 25 | "noImplicitThis": true, 26 | "noUnusedLocals": true, 27 | "noUnusedParameters": true, 28 | "importsNotUsedAsValues": "error", 29 | "forceConsistentCasingInFileNames": true, 30 | "noUncheckedIndexedAccess": true 31 | }, 32 | "include": ["src/**/*", "radish.env.d.ts"], 33 | "exclude": ["node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /packages/docs/src/images/blob3-day.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /packages/docs/src/images/blob3-night.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /packages/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["esnext", "dom"], 5 | "skipLibCheck": true, 6 | 7 | // build options 8 | "baseUrl": "src", 9 | "noEmit": true, 10 | "jsx": "react", 11 | 12 | // module resolution 13 | "module": "es2020", 14 | "moduleResolution": "node", 15 | "isolatedModules": true, 16 | 17 | // ecmascript features 18 | "downlevelIteration": true, 19 | "esModuleInterop": true, 20 | 21 | // strict typechecking 22 | "strict": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noImplicitAny": true, 25 | "noImplicitReturns": true, 26 | "noImplicitThis": true, 27 | "noUnusedLocals": true, 28 | "noUnusedParameters": true, 29 | "importsNotUsedAsValues": "error", 30 | "forceConsistentCasingInFileNames": true, 31 | "noUncheckedIndexedAccess": true 32 | }, 33 | "include": ["src/**/*", "radish.env.d.ts"], 34 | "exclude": ["node_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/guides/offline.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 4 3 | title: Offline 4 | --- 5 | 6 | When you build your website, Radish includes a [service worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) to improve performance and make it work offline. 7 | 8 | By default, Radish will always request new versions of your pages, and it will cache static assets for a year. You can configure this time by setting the `max-age` cache control directive from your webserver. If you do override it, you should try to keep assets cached for a long time; since Radish changes asset filenames whenever the contents change, they are guaranteed to be up-to-date. 9 | 10 | To prevent the service worker from caching individual files, set the `no-store` cache control directive on your webserver. To disable the service worker entirely, build your website with the flag `--service-worker=disabled`. 11 | 12 | Note that if you host your static assets on a different domain, the service worker won't be able to cache them. 13 | -------------------------------------------------------------------------------- /packages/docs/src/hooks/useContent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useContent as useRadishContent, 3 | Content as RadishContent 4 | } from "radish"; 5 | 6 | export interface Content { 7 | docs: { 8 | [key: string]: { 9 | section: { title: string; order: number }; 10 | } & { [key: string]: RadishContent<{ title: string; order: number }> }; 11 | }; 12 | } 13 | 14 | export default function useContent() { 15 | return useRadishContent(); 16 | } 17 | 18 | export function useSections() { 19 | const content = useContent(); 20 | const docs = content.docs; 21 | 22 | return Object.entries(docs) 23 | .sort(([, a], [, b]) => a.section.order - b.section.order) 24 | .map(([slug, { section, ...etc }]) => [slug, section.title, etc] as const); 25 | } 26 | 27 | export function usePages() { 28 | const sections = useSections(); 29 | return sections.flatMap(([section, , pages]) => 30 | Object.entries(pages) 31 | .sort(([, a], [, b]) => a.order - b.order) 32 | .map(([slug, page]) => [section, slug, page] as const) 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/docs/src/components/Button/style.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block; 3 | position: relative; 4 | color: var(--color-background); 5 | text-decoration: none; 6 | font-size: var(--ms-1); 7 | letter-spacing: -0.1ch; 8 | height: 6rem; 9 | line-height: 6.6rem; 10 | padding: 0 3.6rem; 11 | white-space: nowrap; 12 | } 13 | 14 | .button::before { 15 | content: ""; 16 | display: block; 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | width: 100%; 21 | border-width: 3rem; 22 | border-style: solid; 23 | border-image: url("./primary.svg") 50% 16% fill stretch; 24 | z-index: -1; 25 | } 26 | 27 | .button.ghost { 28 | color: var(--color-main-text); 29 | } 30 | 31 | .button.ghost::before { 32 | border-image-source: url("./ghost-day.svg"); 33 | } 34 | 35 | :global(html.night) .button.ghost::before { 36 | border-image-source: url("./ghost-night.svg"); 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :global(html:not(.day)) .button.ghost::before { 41 | border-image-source: url("./ghost-night.svg"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/docs/src/components/Sidebar/github.react.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /packages/radish/src/util/fetch.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import https from "https"; 3 | 4 | export default function fetch(url: string) { 5 | return new Promise((resolve, reject) => { 6 | const lib = url.startsWith("https") ? https : http; 7 | lib.get(url, res => { 8 | if (!res.statusCode) 9 | return reject(new Error(`GET "${url}" returned no status.`)); 10 | 11 | if ([301, 302, 307].includes(res.statusCode)) { 12 | if (!res.headers.location) { 13 | // prettier-ignore 14 | const err = new Error(`GET "${url}" returned status ${res.statusCode} but no location header.`); 15 | return reject(err); 16 | } 17 | 18 | return fetch(new URL(res.headers.location, url).toString()) 19 | .then(resolve) 20 | .catch(reject); 21 | } 22 | 23 | if (res.statusCode === 200) { 24 | const chunks: any[] = []; 25 | res.on("data", chunk => chunks.push(chunk)); 26 | res.on("end", () => resolve(Buffer.concat(chunks).toString())); 27 | } else reject(new Error(`GET ${url} returned status ${res.statusCode}`)); 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /packages/docs/src/components/Sidebar/bullet.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /packages/create/src/index.ts: -------------------------------------------------------------------------------- 1 | // sys 2 | import * as path from "node:path"; 3 | 4 | // lib 5 | 6 | import init from "./init.js"; 7 | import argv from "./argv.js"; 8 | 9 | export default function cli(args: string[], version: string) { 10 | const flags = argv(args); 11 | 12 | if (flags.version) return console.log(version); 13 | 14 | const [dir] = flags._; 15 | if (!dir || flags.help) return help(); 16 | 17 | init({ 18 | dir: path.resolve(dir), 19 | typescript: Boolean(flags.typescript) 20 | }); 21 | } 22 | 23 | function help() { 24 | const lines: string[] = []; 25 | 26 | lines.push(`🌱\n`, `Usage: create-radish [options]\n`); 27 | lines.push( 28 | `Options:`, 29 | ...formatOptions( 30 | [` --typescript`, "generate a typescript project"], 31 | [` --help`, "display help"], 32 | [` --version`, "display version"] 33 | ) 34 | ); 35 | 36 | const output = lines.map(line => " " + line).join("\n"); 37 | console.log(`\n${output}\n`); 38 | } 39 | 40 | function formatOptions(...options: [string, string][]) { 41 | let max = options 42 | .map(([flag]) => flag.length) 43 | .reduce((a, b) => Math.max(a, b)); 44 | max += 4; 45 | 46 | return options.map(([flag, desc]) => flag.padEnd(max) + desc); 47 | } 48 | -------------------------------------------------------------------------------- /packages/radish/src/core/render.ts: -------------------------------------------------------------------------------- 1 | // lib 2 | import { createElement } from "react"; 3 | import { renderToStaticMarkup } from "react-dom/server"; 4 | import type { HelmetServerState } from "react-helmet-async"; 5 | 6 | import type { Page, PageProps } from "./types"; 7 | 8 | export default function render(page: Page, props: PageProps) { 9 | const markup = renderToStaticMarkup( 10 | createElement(page.default, props) 11 | ).replace(/<\/radish:noop>/g, ""); 12 | 13 | if (page.layout === false) return markup; 14 | return html(markup, page.head.helmet); 15 | } 16 | 17 | function html(markup: string, helmet: HelmetServerState) { 18 | const html = helmet.htmlAttributes.toString(); 19 | const body = helmet.bodyAttributes.toString(); 20 | return [ 21 | ``, 22 | ``, 23 | ` `, 24 | ` ` + helmet.title.toString().replace(rh, ""), 25 | ` ` + helmet.meta.toString().replace(rh, ""), 26 | ` ` + helmet.link.toString().replace(rh, ""), 27 | ` ` + helmet.script.toString().replace(rh, ""), 28 | ` `, 29 | ` `, 30 | markup, 31 | ` `, 32 | `` 33 | ].join("\n"); 34 | } 35 | 36 | const rh = / data-rh="true"/g; 37 | -------------------------------------------------------------------------------- /packages/docs/src/pages/style.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | max-width: 104rem; 3 | min-width: 32rem; 4 | margin: 0 auto; 5 | padding: 4rem; 6 | } 7 | 8 | .header { 9 | margin: 7.2rem 0 9.6rem; 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | } 14 | 15 | .logo { 16 | display: block; 17 | height: 18rem; 18 | width: 50rem; 19 | background: url("images/logo.svg") no-repeat; 20 | background-size: contain; 21 | text-indent: -999rem; 22 | margin-bottom: 2.4rem; 23 | max-width: 100%; 24 | } 25 | 26 | .subtitle { 27 | font-size: 2.4rem; 28 | text-align: center; 29 | margin-bottom: 9.6rem; 30 | } 31 | 32 | .ctas { 33 | display: grid; 34 | justify-items: center; 35 | gap: 1.6rem; 36 | list-style: none; 37 | padding: 0; 38 | } 39 | 40 | @media (min-width: 540px) { 41 | .ctas { 42 | grid-auto-flow: column; 43 | } 44 | } 45 | 46 | .features { 47 | display: grid; 48 | grid-template-columns: 1fr; 49 | gap: 2rem; 50 | } 51 | 52 | @media (min-width: 720px) { 53 | .features { 54 | grid-template-columns: 1fr 1fr; 55 | } 56 | } 57 | 58 | .feature { 59 | border-width: 2.4rem; 60 | border-style: solid; 61 | border-image: var(--blob-1) 25% 10% fill stretch; 62 | } 63 | 64 | .feature:nth-child(2n + 1) { 65 | border-image-source: var(--blob-2); 66 | } 67 | 68 | .featureTitle { 69 | font-size: var(--ms-1); 70 | font-weight: 900; 71 | } 72 | 73 | .footer { 74 | margin: 2.4rem 0; 75 | } 76 | -------------------------------------------------------------------------------- /packages/docs/src/pages/docs/style.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | padding-bottom: 2.4rem; 3 | } 4 | 5 | @media (min-width: 960px) { 6 | .wrapper { 7 | display: grid; 8 | grid-template-columns: auto minmax(0, 1fr); 9 | align-items: start; 10 | } 11 | } 12 | 13 | .sidebar { 14 | position: sticky; 15 | top: 0; 16 | padding: 1.8rem; 17 | padding-top: 1.2rem; 18 | background-color: var(--color-background); 19 | box-shadow: 0 0.1rem 0 0 var(--color-surface); 20 | } 21 | 22 | @media (min-width: 960px) { 23 | .sidebar { 24 | padding: 4rem; 25 | padding-top: 3.4rem; 26 | box-shadow: none; 27 | } 28 | } 29 | 30 | .content { 31 | padding: 4rem; 32 | } 33 | 34 | .neighbors { 35 | display: flex; 36 | justify-content: space-between; 37 | list-style: none; 38 | padding: 0; 39 | margin-top: 7.2rem; 40 | } 41 | 42 | .neighbor.next { 43 | margin-left: auto; 44 | } 45 | 46 | .neighbor.prev { 47 | margin-right: auto; 48 | } 49 | 50 | .neighborLink { 51 | display: block; 52 | text-decoration: none; 53 | font-size: var(--ms-1); 54 | } 55 | 56 | .neighborLabel { 57 | display: flex; 58 | align-items: center; 59 | gap: 0.8rem; 60 | color: #999; 61 | font-weight: 400; 62 | font-size: var(--ms-0); 63 | line-height: var(--ms-0); 64 | } 65 | 66 | .neighborLabel > svg { 67 | margin-top: -0.4rem; 68 | } 69 | 70 | .neighbor.prev .neighborLabel { 71 | justify-content: flex-start; 72 | } 73 | 74 | .neighbor.next .neighborLabel { 75 | justify-content: flex-end; 76 | } 77 | 78 | .footer { 79 | grid-column: 2; 80 | margin-top: 2.4rem; 81 | } 82 | -------------------------------------------------------------------------------- /packages/radish/src/core/index.ts: -------------------------------------------------------------------------------- 1 | // sys 2 | import * as path from "node:path"; 3 | import * as child from "node:child_process"; 4 | 5 | import { bundle, BundleOptions } from "./bundle.js"; 6 | import { serve } from "./serve.js"; 7 | import { websocket } from "./websocket.js"; 8 | 9 | export async function build(options: BundleOptions) { 10 | const src = path.resolve(options.src); 11 | const dest = path.resolve(options.dest); 12 | 13 | const ok = await bundle({ ...options, src, dest }); 14 | if (!ok) process.exit(1); 15 | } 16 | 17 | interface DevOptions { 18 | src: string; 19 | dest: string; 20 | port?: number; 21 | } 22 | 23 | export async function dev(options: DevOptions) { 24 | const src = path.resolve(options.src); 25 | const dest = path.resolve(options.dest); 26 | 27 | const port = options.port ?? 8000; 28 | const ws = websocket({ port: port + 1 }); 29 | const ok = await bundle({ 30 | src, 31 | dest, 32 | public: "/public", 33 | watch: true, 34 | websocket: port + 1, 35 | onRebuild: ws.refresh 36 | }); 37 | 38 | if (!ok) { 39 | ws.close(); 40 | process.exit(1); 41 | } 42 | 43 | serve({ dir: dest, port }); 44 | } 45 | 46 | interface LintOptions { 47 | src: string; 48 | } 49 | 50 | export async function lint(options: LintOptions) { 51 | const src = path.resolve(options.src); 52 | 53 | child.exec( 54 | `npx eslint --ext .jsx --ext .tsx ${src}`, 55 | (err, stdout, stderr) => { 56 | if (err) return console.error(err); 57 | console.log(stdout); 58 | console.error(stderr); 59 | } 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /packages/radish/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "radish", 3 | "version": "0.2.8", 4 | "type": "module", 5 | "module": "./build/lib/index.js", 6 | "types": "./build/lib/index.d.ts", 7 | "license": "MIT", 8 | "repository": "https://github.com/jakelazaroff/radish", 9 | "homepage": "https://radishjs.com", 10 | "engines": { 11 | "node": ">=16.0.0" 12 | }, 13 | "prettier": "@radish/prettier", 14 | "dependencies": { 15 | "@mdx-js/mdx": "^2.1.5", 16 | "@svgr/core": "^6.5.1", 17 | "esbuild": "~0.15.15", 18 | "globby": "^13.1.1", 19 | "gray-matter": "^4.0.3", 20 | "js-yaml": "^4.1.0", 21 | "lightningcss": "^1.16.1", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0", 24 | "react-helmet-async": "^1.3.0", 25 | "rehype-highlight": "^6.0.0", 26 | "remark-gfm": "^3.0.1", 27 | "toml": "^3.0.0", 28 | "ws": "^8.5.0" 29 | }, 30 | "devDependencies": { 31 | "@radish/prettier": "*", 32 | "@radish/tsconfig": "*", 33 | "@types/js-yaml": "^4.0.5", 34 | "@types/node": "^18.11.9", 35 | "@types/prettier": "^2.4.4", 36 | "@types/react": "^18.0.25", 37 | "@types/react-dom": "^18.0.9", 38 | "@types/ws": "^8.5.3", 39 | "@typescript-eslint/eslint-plugin": "^5.14.0", 40 | "@typescript-eslint/parser": "^5.16.0", 41 | "eslint": "^8.11.0", 42 | "typescript": "^4.6.2" 43 | }, 44 | "peerDependencies": { 45 | "react": "^18.0.0", 46 | "react-dom": "^18.0.0" 47 | }, 48 | "scripts": { 49 | "clean": "rm -rf build tsconfig.tsbuildinfo", 50 | "build": "yarn clean && tsc", 51 | "prepare": "yarn build" 52 | }, 53 | "bin": { 54 | "radish": "./bin/cli.js" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/radish/src/lib/Document.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | // @ts-ignore 4 | import { Head, HeadProvider } from "radish"; 5 | 6 | // @ts-ignore 7 | import style from "styles/style.css"; 8 | 9 | interface Preload { 10 | href: string; 11 | as: string; 12 | } 13 | 14 | export interface Props { 15 | children?: ReactNode; 16 | path: string; 17 | serviceWorker?: boolean; 18 | websocket?: number; 19 | preload?: Preload[]; 20 | } 21 | 22 | export default function Document(props: Props) { 23 | const { children, serviceWorker, websocket, preload = [] } = props; 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | {preload.map((resource, i) => ( 31 | 32 | ))} 33 | {serviceWorker ? : null} 34 | {websocket ? : null} 35 | 36 | {children} 37 | 38 | ); 39 | } 40 | 41 | Document.head = HeadProvider.context; 42 | 43 | const sw = ` 44 | if ("serviceWorker" in navigator) { 45 | window.addEventListener('load', () => { 46 | navigator.serviceWorker.register("/sw.js", { type: "module" }) 47 | .catch(error => console.error("Couldn't load service worker", error)); 48 | }); 49 | }`.trim(); 50 | 51 | const ws = (port: number) => { 52 | return ` 53 | const ws = new WebSocket("ws://localhost:${port}"); 54 | ws.onmessage = msg => { 55 | const data = JSON.parse(msg.data); 56 | if (data.type === "refresh") location.reload(); 57 | }`.trim(); 58 | }; 59 | -------------------------------------------------------------------------------- /packages/radish/src/core/js.ts: -------------------------------------------------------------------------------- 1 | // sys 2 | import * as fs from "node:fs"; 3 | import * as path from "node:path"; 4 | 5 | // lib 6 | import esbuild, { Plugin } from "esbuild"; 7 | 8 | interface Options { 9 | dest: string; 10 | prefix: string; 11 | } 12 | 13 | export const jsPlugin = (options: Options): Plugin => ({ 14 | name: "js", 15 | setup(build) { 16 | build.onLoad({ filter: /\.bundle\.[jt]sx?$/ }, async args => { 17 | const r = await esbuild.build({ 18 | entryPoints: [args.path], 19 | entryNames: "[name]-[hash]", 20 | outdir: options.dest, 21 | bundle: true, 22 | write: false, 23 | minify: true, 24 | metafile: true, 25 | format: "esm" 26 | }); 27 | 28 | const [file, ...rest] = r.outputFiles; 29 | if (!file) throw Error("No output file returned."); 30 | if (rest.length > 1) throw Error("Too many output files returned."); 31 | 32 | // calculate the file hash 33 | const [, hash] = file.path.match(/.+\.bundle-(\w+)\.js/) ?? [], 34 | basename = path.basename(file.path, `.bundle-${hash}.js`), 35 | filename = `${basename}-${hash}.js`; 36 | 37 | // write the bundled js to the public directory 38 | await fs.promises.mkdir(options.dest, { recursive: true }); 39 | await fs.promises.writeFile(path.join(options.dest, filename), file.text); 40 | 41 | // return the path to the bundled js as a text string 42 | return { 43 | contents: path.join(options.prefix, filename), 44 | loader: "text", 45 | errors: r.errors, 46 | warnings: r.warnings, 47 | watchFiles: Object.keys(r.metafile?.inputs ?? {}) 48 | }; 49 | }); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /packages/radish/src/core/page.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as path from "node:path"; 3 | import * as url from "node:url"; 4 | 5 | // lib 6 | import type { Plugin } from "esbuild"; 7 | 8 | interface PageOptions { 9 | src: string; 10 | } 11 | 12 | const __filename = url.fileURLToPath(import.meta.url); 13 | const __dirname = path.dirname(__filename); 14 | 15 | export const pagePlugin = (options: PageOptions): Plugin => ({ 16 | name: "pages", 17 | setup(build) { 18 | const doc = path.resolve(__dirname, "../lib/Document"), 19 | docRE = new RegExp(doc.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ".js$"); 20 | 21 | build.onResolve({ filter: /\/pages\/.+\.[jt]sx$/ }, async args => { 22 | if (args.kind === "entry-point") 23 | return { path: args.path, namespace: "page" }; 24 | 25 | return { namespace: "file" }; 26 | }); 27 | 28 | build.onLoad({ filter: /\.[jt]sx$/, namespace: "page" }, async args => { 29 | const contents = [ 30 | `import Document from "${doc}";`, 31 | `import Component, * as page from "${args.path}";`, 32 | `export default function Page(props) {`, 33 | ` return (`, 34 | ` `, 35 | ` `, 36 | ` `, 37 | ` );`, 38 | `}`, 39 | `export const layout = page.layout;`, 40 | `export const paths = page.paths;`, 41 | `export const head = Document.head;` 42 | ].join("\n"); 43 | 44 | return { contents, loader: "jsx", resolveDir: options.src }; 45 | }); 46 | 47 | build.onLoad({ filter: docRE }, async args => { 48 | const contents = await fs.promises.readFile(args.path); 49 | return { contents, loader: "jsx", resolveDir: options.src }; 50 | }); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /packages/radish/src/core/result.ts: -------------------------------------------------------------------------------- 1 | import type * as esbuild from "esbuild"; 2 | 3 | export interface RadishBuildError { 4 | type: string; 5 | message: string; 6 | lineText: string; 7 | line: number; 8 | column: number; 9 | } 10 | 11 | export interface RadishBuildResult { 12 | inputFile: string; 13 | outputFile?: string; 14 | error?: RadishBuildError; 15 | } 16 | 17 | export function fromSuccess(input: string, output: string): RadishBuildResult { 18 | return { 19 | inputFile: input, 20 | outputFile: output 21 | }; 22 | } 23 | 24 | export function fromRenderError(e: Error): RadishBuildResult { 25 | const [, frame] = e.stack?.split("\n") || []; 26 | if (!frame) throw e; 27 | 28 | const [, body = "", l, c] = 29 | frame.match(/\s*at \w+ \((.*):(\d+):(\d+)\)/) ?? []; 30 | if (!l || !c) throw e; 31 | const lineNo = Number(l), 32 | colNo = Number(c); 33 | 34 | const src = decodeURIComponent(body); 35 | const lines = src.split("\n"); 36 | let file = "", 37 | fileLineNo = 0; 38 | for (let i = lineNo; i >= 0; i--) { 39 | const line = lines[i]; 40 | if (line?.startsWith("// ")) { 41 | file = line.slice(3); 42 | fileLineNo = lineNo - i; 43 | break; 44 | } 45 | } 46 | 47 | if (!file) throw e; 48 | 49 | return { 50 | inputFile: file, 51 | error: { 52 | type: e.name, 53 | message: e.message, 54 | lineText: lines[lineNo - 1] ?? "", 55 | line: fileLineNo, 56 | column: colNo 57 | } 58 | }; 59 | } 60 | 61 | export function fromBuildError(result: esbuild.Message): RadishBuildResult { 62 | return { 63 | inputFile: result.location?.file ?? "", 64 | error: { 65 | type: "BundleError", 66 | message: result.text, 67 | lineText: result.location?.lineText ?? "", 68 | line: result.location?.line ?? 0, 69 | column: result.location?.column ?? 0 70 | } 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /packages/create/src/argv.ts: -------------------------------------------------------------------------------- 1 | export default function argv(argv: string[]) { 2 | const result: { _: any[]; [key: string]: any } = { _: [] }; 3 | 4 | let positional = false; 5 | let currentKey = ""; 6 | 7 | for (const arg of argv) { 8 | // if we've passed a "--", the rest of the args are positional 9 | if (positional) { 10 | result._.push(parse(arg)); 11 | continue; 12 | } 13 | 14 | // if the arg starts with hyphens, it's a flag 15 | if (arg.startsWith("-")) { 16 | // if we're waiting on a value for a previous flag, we can just set it to a boolean 17 | if (currentKey) result[currentKey] = true; 18 | 19 | const [key, value] = arg.replace(/^-+/, "").split(/=(.*)/); 20 | 21 | // if there's no key, the arg is "--"; the rest of the arguments will be positional 22 | if (!key) { 23 | if (currentKey) result[currentKey] = true; 24 | positional = true; 25 | currentKey = ""; 26 | continue; 27 | } 28 | 29 | // if there's no value, we'll wait to check the next arg 30 | if (!value) { 31 | currentKey = key; 32 | continue; 33 | } 34 | 35 | // if there's both a key and a value, we can just set them directly 36 | result[key] = value; 37 | currentKey = ""; 38 | continue; 39 | } 40 | 41 | // if the arg isn't a flag and there's a current key, the arg is the value 42 | if (currentKey) { 43 | result[currentKey] = parse(arg); 44 | currentKey = ""; 45 | continue; 46 | } 47 | 48 | // if the arg isn't a flag and there's no current key, it's positional 49 | result._.push(parse(arg)); 50 | } 51 | 52 | // if the last argument was a flag, we can set it to true now 53 | if (currentKey) result[currentKey] = true; 54 | 55 | return result; 56 | } 57 | 58 | function parse(arg: string) { 59 | // if it's a number, return a number 60 | const num = Number(arg); 61 | if (!isNaN(num)) return num; 62 | 63 | // if it's "true" or "false", return a boolean 64 | if (arg === "true") return true; 65 | if (arg === "false") return false; 66 | 67 | // otherwise, return a string 68 | return arg; 69 | } 70 | -------------------------------------------------------------------------------- /packages/radish/src/util/argv.ts: -------------------------------------------------------------------------------- 1 | export default function argv(argv: string[]) { 2 | const result: { _: any[]; [key: string]: any } = { _: [] }; 3 | 4 | let positional = false; 5 | let currentKey = ""; 6 | 7 | for (const arg of argv) { 8 | // if we've passed a "--", the rest of the args are positional 9 | if (positional) { 10 | result._.push(parse(arg)); 11 | continue; 12 | } 13 | 14 | // if the arg starts with hyphens, it's a flag 15 | if (arg.startsWith("-")) { 16 | // if we're waiting on a value for a previous flag, we can just set it to a boolean 17 | if (currentKey) result[currentKey] = true; 18 | 19 | const [key, value] = arg.replace(/^-+/, "").split(/=(.*)/); 20 | 21 | // if there's no key, the arg is "--"; the rest of the arguments will be positional 22 | if (!key) { 23 | if (currentKey) result[currentKey] = true; 24 | positional = true; 25 | currentKey = ""; 26 | continue; 27 | } 28 | 29 | // if there's no value, we'll wait to check the next arg 30 | if (!value) { 31 | currentKey = key; 32 | continue; 33 | } 34 | 35 | // if there's both a key and a value, we can just set them directly 36 | result[key] = value; 37 | currentKey = ""; 38 | continue; 39 | } 40 | 41 | // if the arg isn't a flag and there's a current key, the arg is the value 42 | if (currentKey) { 43 | result[currentKey] = parse(arg); 44 | currentKey = ""; 45 | continue; 46 | } 47 | 48 | // if the arg isn't a flag and there's no current key, it's positional 49 | result._.push(parse(arg)); 50 | } 51 | 52 | // if the last argument was a flag, we can set it to true now 53 | if (currentKey) result[currentKey] = true; 54 | 55 | return result; 56 | } 57 | 58 | function parse(arg: string) { 59 | // if it's a number, return a number 60 | const num = Number(arg); 61 | if (!isNaN(num)) return num; 62 | 63 | // if it's "true" or "false", return a boolean 64 | if (arg === "true") return true; 65 | if (arg === "false") return false; 66 | 67 | // otherwise, return a string 68 | return arg; 69 | } 70 | -------------------------------------------------------------------------------- /packages/docs/src/js/index.bundle.ts: -------------------------------------------------------------------------------- 1 | export type {}; 2 | 3 | const navigation = document.querySelector("#navigation") as HTMLElement; 4 | 5 | function resetNav() { 6 | navigation.removeAttribute("aria-hidden"); 7 | const focusable = navigation.querySelectorAll("[tabindex='-1']"); 8 | for (const el of Array.from(focusable)) { 9 | el.removeAttribute("tabindex"); 10 | } 11 | } 12 | 13 | function hideNav() { 14 | navigation.setAttribute("aria-hidden", "true"); 15 | const focusable = 16 | navigation.querySelectorAll("a, button, input"); 17 | for (const el of Array.from(focusable)) { 18 | el.tabIndex = -1; 19 | } 20 | } 21 | 22 | function showNav() { 23 | navigation.setAttribute("aria-hidden", "false"); 24 | const focusable = navigation.querySelectorAll("[tabindex='-1']"); 25 | for (const el of Array.from(focusable)) { 26 | el.removeAttribute("tabindex"); 27 | } 28 | } 29 | 30 | const mq = matchMedia("(min-width: 960px)"); 31 | let mobile = !mq.matches; 32 | mq.onchange = e => { 33 | mobile = !e.matches; 34 | if (mobile) hideNav(); 35 | else resetNav(); 36 | }; 37 | 38 | if (mobile) hideNav(); 39 | 40 | const toggles = document.querySelectorAll(`[data-js="navigation"]`); 41 | for (const el of Array.from(toggles)) { 42 | const toggle = el as HTMLElement; 43 | toggle.onclick = () => { 44 | const hidden = navigation.getAttribute("aria-hidden") !== "false"; 45 | if (hidden) showNav(); 46 | else hideNav(); 47 | }; 48 | } 49 | 50 | navigation.addEventListener("focusout", e => { 51 | if (!mobile) return; 52 | 53 | const focused = e.relatedTarget as HTMLElement | null; 54 | if (focused && !navigation.contains(focused)) hideNav(); 55 | }); 56 | 57 | navigation.addEventListener("keydown", e => { 58 | if (!mobile) return; 59 | 60 | if (e.code === "Escape") hideNav(); 61 | }); 62 | 63 | const themes = document.querySelectorAll(`[data-js="colorscheme"]`); 64 | for (const el of Array.from(themes)) { 65 | const button = el as HTMLButtonElement; 66 | button.onclick = () => { 67 | localStorage.setItem("colorscheme", button.value); 68 | document.documentElement.classList.remove("night", "day"); 69 | document.documentElement.classList.add(button.value); 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /packages/docs/src/content/docs/guides/assets.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 3 3 | title: Assets 4 | --- 5 | 6 | When you import assets in your components or styles, or reference them with `url()` in your content files, Radish will load the asset automatically. For static assets like images, Radish returns the URL for you to use in your components. However, Radish treats certain assets differently: 7 | 8 | ## CSS 9 | 10 | Radish automatically bundles and links `src/styles/style.css`. Any global styles, such as resets and fonts, should either go directly in this file or be `@import`ed into it: 11 | 12 | ```css 13 | /* styles/style.css */ 14 | 15 | @import "./fonts.css"; 16 | 17 | *, 18 | *::before, 19 | *::after { 20 | box-sixing: border-box; 21 | } 22 | ``` 23 | 24 | If you have no global styles, you can simply leave `src/styles/style.css` blank. 25 | 26 | Radish treats files with the `.module.css` extension as CSS modules, and return an object of class names: 27 | 28 | ```css 29 | /* style.module.css */ 30 | 31 | .wrapper { 32 | border: 1px solid pink; 33 | } 34 | 35 | .title { 36 | font-style: italic; 37 | } 38 | ``` 39 | 40 | ```jsx 41 | // pages/page.jsx 42 | 43 | import css from "./style.module.css"; 44 | 45 | function Page() { 46 | return ( 47 |
48 |

Hello, world!

49 |
50 | ); 51 | } 52 | ``` 53 | 54 | ## JavaScript 55 | 56 | Radish doesn't export any JavaScript by default, but you can still bundle and load scripts onto your webpages. If you import a script with the extension `.bundle.ts` or `.bundle.js`. Radish will bundle the script and return a URL that you can use in a `