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