Coder Social home page Coder Social logo

honox's Introduction

HonoX

HonoX is a simple and fast meta-framework for creating full-stack websites or Web APIs - (formerly Sonik). It stands on the shoulders of giants; built on Hono, Vite, and UI libraries.

Note: HonoX is currently in the "alpha stage". Breaking changes are introduced without following semantic versioning.

Features

  • File-based routing - You can create a large application like Next.js.
  • Fast SSR - Rendering is ultra-fast thanks to Hono.
  • BYOR - You can bring your own renderer, not only one using hono/jsx.
  • Islands hydration - If you want interactions, create an island. JavaScript is hydrated only for it.
  • Middleware - It works as Hono, so you can use a lot of Hono's middleware.

Installing

You can install the honox package from the npm.

npm install hono honox

Starter template

If you are starting a new HonoX project, use the hono-create command. Run the following and choose x-basic.

npm create hono@latest

Get Started - Basic

Let's create a basic HonoX application using hono/jsx as a renderer. This application has no client JavaScript and renders JSX on the server side.

Project Structure

Below is a typical project structure for a HonoX application.

.
├── app
│   ├── global.d.ts // global type definitions
│   ├── routes
│   │   ├── _404.tsx // not found page
│   │   ├── _error.tsx // error page
│   │   ├── _renderer.tsx // renderer definition
│   │   ├── about
│   │   │   └── [name].tsx // matches `/about/:name`
│   │   └── index.tsx // matches `/`
│   └── server.ts // server entry file
├── package.json
├── tsconfig.json
└── vite.config.ts

vite.config.ts

The minimum Vite setup for development is as follows:

import { defineConfig } from 'vite'
import honox from 'honox/vite'

export default defineConfig({
  plugins: [honox()],
})

Server Entry File

A server entry file is required. The file should be placed at app/server.ts. This file is first called by the Vite during the development or build phase.

In the entry file, simply initialize your app using the createApp() function. app will be an instance of Hono, so you can use Hono's middleware and the showRoutes() in hono/dev.

// app/server.ts
import { createApp } from 'honox/server'
import { showRoutes } from 'hono/dev'

const app = createApp()

showRoutes(app)

export default app

Routes

There are three ways to define routes.

1. createRoute()

Each route should return an array of Handler | MiddlewareHandler. createRoute() is a helper function to return it. You can write a route for a GET request with default export.

// `createRoute()` helps you create handlers
import { createRoute } from 'honox/factory'

export default createRoute((c) => {
  return c.render(
    <div>
      <h1>Hello!</h1>
    </div>
  )
})

You can also handle methods other than GET by export POST, PUT, and DELETE.

import { createRoute } from 'honox/factory'
import { getCookie, setCookie } from 'hono/cookie'

export const POST = createRoute(async (c) => {
  const { name } = await c.req.parseBody<{ name: string }>()
  setCookie(c, 'name', name)
  return c.redirect('/')
})

export default createRoute((c) => {
  const name = getCookie(c, 'name') ?? 'no name'
  return c.render(
    <div>
      <h1>Hello, {name}!</h1>
      <form method='POST'>
        <input type='text' name='name' placeholder='name' />
        <input type='submit' />
      </form>
    </div>
  )
})

2. Using Hono instance

You can create API endpoints by exporting an instance of the Hono object.

// app/routes/about/index.ts
import { Hono } from 'hono'

const app = new Hono()

// matches `/about/:name`
app.get('/:name', (c) => {
  const name = c.req.param('name')
  return c.json({
    'your name is': name,
  })
})

export default app

3. Just return JSX

Or simply, you can just return JSX.

export default function Home(_c: Context) {
  return <h1>Welcome!</h1>
}

Renderer

Define your renderer - the middleware that does c.setRender() - by writing it in _renderer.tsx.

Before writing _renderer.tsx, write the Renderer type definition in global.d.ts.

// app/global.d.ts
import type {} from 'hono'

type Head = {
  title?: string
}

declare module 'hono' {
  interface ContextRenderer {
    (content: string | Promise<string>, head?: Head): Response | Promise<Response>
  }
}

The JSX Renderer middleware allows you to create a Renderer as follows:

// app/routes/_renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'

export default jsxRenderer(({ children, title }) => {
  return (
    <html lang='en'>
      <head>
        <meta charset='UTF-8' />
        <meta name='viewport' content='width=device-width, initial-scale=1.0' />
        {title ? <title>{title}</title> : <></>}
      </head>
      <body>{children}</body>
    </html>
  )
})

The _renderer.tsx is applied under each directory, and the app/routes/posts/_renderer.tsx is applied in app/routes/posts/*.

Not Found page

You can write a custom Not Found page in _404.tsx.

// app/routes/_404.tsx
import { NotFoundHandler } from 'hono'

const handler: NotFoundHandler = (c) => {
  return c.render(<h1>Sorry, Not Found...</h1>)
}

export default handler

Error Page

You can write a custom Error page in _error.tsx.

// app/routes/_error.tsx
import { ErrorHandler } from 'hono'

const handler: ErrorHandler = (e, c) => {
  return c.render(<h1>Error! {e.message}</h1>)
}

export default handler

Get Started - with Client

Let's create an application that includes a client side. Here, we will use hono/jsx/dom.

Project Structure

Below is the project structure of a minimal application including a client side:

.
├── app
│   ├── client.ts // client entry file
│   ├── global.d.ts
│   ├── islands
│   │   └── counter.tsx // island component
│   ├── routes
│   │   ├── _renderer.tsx
│   │   └── index.tsx
│   └── server.ts
├── package.json
├── tsconfig.json
└── vite.config.ts

Renderer

This is a _renderer.tsx, which will load the /app/client.ts entry file for the client. It will load the JavaScript file for production according to the variable import.meta.env.PROD. And renders the inside of <HasIslands /> if there are islands on that page.

// app/routes/_renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'
import { HasIslands } from 'honox/server'

export default jsxRenderer(({ children }) => {
  return (
    <html lang='en'>
      <head>
        <meta charset='UTF-8' />
        <meta name='viewport' content='width=device-width, initial-scale=1.0' />
        {import.meta.env.PROD ? (
          <HasIslands>
            <script type='module' src='/static/client.js'></script>
          </HasIslands>
        ) : (
          <script type='module' src='/app/client.ts'></script>
        )}
      </head>
      <body>{children}</body>
    </html>
  )
})

If you have a manifest file in dist/.vite/manifest.json, you can easily write it using <Script />.

// app/routes/_renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'
import { Script } from 'honox/server'

export default jsxRenderer(({ children }) => {
  return (
    <html lang='en'>
      <head>
        <meta charset='UTF-8' />
        <meta name='viewport' content='width=device-width, initial-scale=1.0' />
        <Script src='/app/client.ts' />
      </head>
      <body>{children}</body>
    </html>
  )
})

Note: Since <HasIslands /> can slightly affect build performance when used, it is recommended that you do not use it in the development environment, but only at build time. <Script /> does not cause performance degradation during development, so it's better to use it.

Client Entry File

A client side entry file should be in app/client.ts. Simply, write createClient().

// app/client.ts
import { createClient } from 'honox/client'

createClient()

Interactions

If you want to add interactions to your page, create Island components. Islands components should be:

  • Placed under app/islands directory or named with _ prefix and island.tsx suffix like _componentName.island.tsx.
  • Should export default function.

For example, you can write an interactive component such as the following counter:

// app/islands/counter.tsx
import { useState } from 'hono/jsx'

export default function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

When you load the component in a route file, it is rendered as Server-Side rendering and JavaScript is also sent to the client side.

// app/routes/index.tsx
import { createRoute } from 'honox/factory'
import Counter from '../islands/counter'

export default createRoute((c) => {
  return c.render(
    <div>
      <h1>Hello</h1>
      <Counter />
    </div>
  )
})

Note: You cannot access a Context object in Island components. Therefore, you should pass the value from components outside of the Island.

import { useRequestContext } from 'hono/jsx-renderer'
import Counter from '../islands/counter.tsx'

export default function Component() {
  const c = useRequestContext()
  return <Counter init={parseInt(c.req.query('count') ?? '0', 10)} />
}

BYOR - Bring Your Own Renderer

You can bring your own renderer using a UI library like React, Preact, Solid, or others.

Note: We may not provide supports for the renderer you bring.

React case

You can define a renderer using @hono/react-renderer. Install the modules first.

npm i @hono/react-renderer react react-dom hono
npm i -D @types/react @types/react-dom

Define the Props that the renderer will receive in global.d.ts.

// global.d.ts
import '@hono/react-renderer'

declare module '@hono/react-renderer' {
  interface Props {
    title?: string
  }
}

The following is an example of app/routes/_renderer.tsx.

// app/routes/_renderer.tsx
import { reactRenderer } from '@hono/react-renderer'

export default reactRenderer(({ children, title }) => {
  return (
    <html lang='en'>
      <head>
        <meta charSet='UTF-8' />
        <meta name='viewport' content='width=device-width, initial-scale=1.0' />
        {import.meta.env.PROD ? (
          <script type='module' src='/static/client.js'></script>
        ) : (
          <script type='module' src='/app/client.ts'></script>
        )}
        {title ? <title>{title}</title> : ''}
      </head>
      <body>{children}</body>
    </html>
  )
})

The app/client.ts will be like this.

// app/client.ts
import { createClient } from 'honox/client'

createClient({
  hydrate: async (elem, root) => {
    const { hydrateRoot } = await import('react-dom/client')
    hydrateRoot(root, elem)
  },
  createElement: async (type: any, props: any) => {
    const { createElement } = await import('react')
    return createElement(type, props)
  },
})

Guides

Nested Layouts

If you are using the JSX Renderer middleware, you can nest layouts using <Layout />.

// app/routes/posts/_renderer.tsx

import { jsxRenderer } from 'hono/jsx-renderer'

export default jsxRenderer(({ children, Layout }) => {
  return (
    <Layout>
      <nav>Posts Menu</nav>
      <div>{children}</div>
    </Layout>
  )
})

Passing Additional Props in Nested Layouts

Props passed to nested renderers do not automatically propagate to the parent renderers. To ensure that the parent layouts receive the necessary props, you should explicitly pass them from the nested component. Here's how you can achieve that:

Let's start with our route handler:

// app/routes/nested/index.tsx
export default createRoute((c) => {
  return c.render(<div>Content</div>, { title: 'Dashboard' })
})

Now, let's take a look at our nested renderer:

// app/routes/nested/_renderer.tsx
export default jsxRenderer(({ children, Layout, title }) => {
  return (
    <Layout title={title}>
      {/* Pass the title prop to the parent renderer */}
      <main>{children}</main>
    </Layout>
  )
})

In this setup, all the props sent to the nested renderer's are consumed by the parent renderer:

// app/routes/_renderer.tsx
export default jsxRenderer(({ children, title }) => {
  return (
    <html lang='en'>
      <head>
        <title>{title}</title> {/* Use the title prop here */}
      </head>
      <body>
        {children} {/* Insert the Layout's children here */}
      </body>
    </html>
  )
})

Using Middleware

You can use Hono's Middleware in each root file with the same syntax as Hono. For example, to validate a value with the Zod Validator, do the following:

import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const schema = z.object({
  name: z.string().max(10),
})

export const POST = createRoute(zValidator('form', schema), async (c) => {
  const { name } = c.req.valid('form')
  setCookie(c, 'name', name)
  return c.redirect('/')
})

Alternatively, you can use a _middleware.(ts|tsx) file in a directory to have that middleware applied to the current route, as well as all child routes. Middleware is ran in the order that it is listed within the array.

// /app/routes/_middleware.ts
import { createRoute } from 'honox/factory'
import { logger } from 'hono/logger'
import { secureHeaders } from 'hono/secure-headers'

export default createRoute(logger(), secureHeaders(), ...<more-middleware>)

Trailing Slash

By default, trailing slashes are removed if the root file is an index file such as index.tsx or index.mdx. However, if you set the trailingSlash option to true as the following, the trailing slash is not removed.

import { createApp } from 'honox/server'

const app = createApp({
  trailingSlash: true,
})

Like the followings:

  • trailingSlash is false (default): app/routes/path/index.mdx => /path
  • trailingSlash is true: app/routes/path/index.mdx => /path/

Using Tailwind CSS

Given that HonoX is Vite-centric, if you wish to utilize Tailwind CSS, simply adhere to the official instructions.

Prepare tailwind.config.js and postcss.config.js:

// tailwind.config.js
export default {
  content: ['./app/**/*.tsx'],
  theme: {
    extend: {},
  },
  plugins: [],
}
// postcss.config.js
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

Write app/style.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

Import it in a renderer file:

// app/routes/_renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'

export default jsxRenderer(({ children }) => {
  return (
    <html lang='en'>
      <head>
        <meta charset='UTF-8' />
        <meta name='viewport' content='width=device-width, initial-scale=1.0' />
        {import.meta.env.PROD ? (
          <link href='/static/assets/style.css' rel='stylesheet' />
        ) : (
          <link href='/app/style.css' rel='stylesheet' />
        )}
      </head>
      <body>{children}</body>
    </html>
  )
})

Finally, add vite.config.ts configuration to output assets for the production.

import honox from 'honox/vite'
import { defineConfig } from 'vite'

export default defineConfig(({ mode }) => {
  if (mode === 'client') {
    return {
      build: {
        rollupOptions: {
          input: ['/app/style.css'],
          output: {
            assetFileNames: 'static/assets/[name].[ext]',
          },
        },
      },
    }
  } else {
    return {
      plugins: [honox()],
    }
  }
})

MDX

MDX can also be used. Here is the vite.config.ts.

import devServer from '@hono/vite-dev-server'
import mdx from '@mdx-js/rollup'
import honox from 'honox/vite'
import remarkFrontmatter from 'remark-frontmatter'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
import { defineConfig } from 'vite'

const entry = './app/server.ts'

export default defineConfig(() => {
  return {
    plugins: [
      honox(),
      devServer({ entry }),
      mdx({
        jsxImportSource: 'hono/jsx',
        remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter],
      }),
    ],
  }
})

Blog site can be created.

// app/routes/index.tsx
import type { Meta } from '../types'

export default function Top() {
  const posts = import.meta.glob<{ frontmatter: Meta }>('./posts/*.mdx', {
    eager: true,
  })
  return (
    <div>
      <h2>Posts</h2>
      <ul class='article-list'>
        {Object.entries(posts).map(([id, module]) => {
          if (module.frontmatter) {
            return (
              <li>
                <a href={`${id.replace(/\.mdx$/, '')}`}>{module.frontmatter.title}</a>
              </li>
            )
          }
        })}
      </ul>
    </div>
  )
}

Cloudflare Bindings

If you want to use Cloudflare's Bindings in your development environment, create wrangler.toml and configure it properly.

name = "my-project-name"
compatibility_date = "2023-12-01"
pages_build_output_dir = "./dist"

# [vars]
# MY_VARIABLE = "production_value"

# [[kv_namespaces]]
# binding = "MY_KV_NAMESPACE"
# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

In vite.config.ts, use the Cloudflare Adapter in @hono/vite-dev-server.

import honox from 'honox/vite'
import adapter from '@hono/vite-dev-server/cloudflare'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    honox({
      devServer: {
        adapter,
      },
    }),
  ],
})

Deployment

Since a HonoX instance is essentially a Hono instance, it can be deployed on any platform that Hono supports.

Cloudflare Pages

Setup the vite.config.ts:

// vite.config.ts
import { defineConfig } from 'vite'
import honox from 'honox/vite'
import pages from '@hono/vite-cloudflare-pages'

export default defineConfig({
  plugins: [honox(), pages()],
})

If you want to include client side scripts and assets:

// vite.config.ts
import pages from '@hono/vite-cloudflare-pages'
import honox from 'honox/vite'
import client from 'honox/vite/client'
import { defineConfig } from 'vite'

export default defineConfig(({ mode }) => {
  if (mode === 'client') {
    return {
      plugins: [client()],
    }
  } else {
    return {
      plugins: [honox(), pages()],
    }
  }
})

Build command (including a client):

vite build --mode client && vite build

Deploy with the following commands after the build. Ensure you have Wrangler installed:

wrangler pages deploy ./dist

SSG - Static Site Generation

Using Hono's SSG feature, you can generate static HTML for each route.

import { defineConfig } from 'vite'
import honox from 'honox/vite'
import ssg from '@hono/vite-ssg'

const entry = './app/server.ts'

export default defineConfig(() => {
  return {
    plugins: [honox(), ssg({ entry })],
  }
})

If you want to include client side scripts and assets:

// vite.config.ts
import ssg from '@hono/vite-ssg'
import honox from 'honox/vite'
import client from 'honox/vite/client'
import { defineConfig } from 'vite'

const entry = './app/server.ts'

export default defineConfig(({ mode }) => {
  if (mode === 'client') {
    return {
      plugins: [client()],
    }
  } else {
    return {
      build: {
        emptyOutDir: false,
      },
      plugins: [honox(), ssg({ entry })],
    }
  }
})

Build command (including a client):

vite build --mode client && vite build

You can also deploy it to Cloudflare Pages.

wrangler pages deploy ./dist

Examples

Related projects

Authors

License

MIT

honox's People

Contributors

yusukebe avatar usualoma avatar bruceharrison1984 avatar berlysia avatar luzefiru avatar nismit avatar yutakobayashidev avatar yossydev avatar azukiazusa1 avatar hiroki23 avatar kosei28 avatar laiso avatar mrtska avatar newraina avatar

Stargazers

Harhao avatar Hassadee Pimsuwan avatar 傅月池 avatar Mate Papp avatar Michele Memoli avatar Gabriel Martins avatar 底なし沼の魔女 avatar broki avatar SweetieRick avatar Bojie Yang avatar nick avatar Derik avatar Yuri Mutti avatar S. MohammadMahdi Zamanian avatar Gavin.Kwoe avatar 阿宝哥 avatar Iyasa avatar Otstar Lin avatar YS Chung avatar Alex Rigler avatar kh avatar Daiki avatar Backend_Paul  avatar Emiel van de Laar avatar  avatar Soumyajit Pathak avatar Takeda.M avatar Milad Nekofar avatar teramotodaiki avatar horizon avatar anyinfa avatar BeiXiao avatar Ricky de Laveaga avatar Lakubu Mayanda David avatar Ikhlash Mulyanurahman avatar Xavier Brinon avatar araya avatar Tomoki Yamauchi avatar  avatar Amir avatar Hong Minhee (洪 民憙) avatar Joan Silio avatar Dan Gamble avatar fzn0x avatar Rui Wu avatar 陈随易 avatar devcomfort avatar Muhammad Ilham Jaya  avatar  avatar Aliaksandr Zhukau avatar Maiah Macariola avatar Sean avatar Gabriel Egli avatar  avatar Emily Medhurst avatar Legrand Thomas avatar Mustafa Eray avatar Kato, H. avatar Tao Sasaki avatar Stepan Samutichev avatar José Dulanto avatar Kennan Fattahillah avatar Ardi Setiawan avatar yuim avatar Jordan Skomer avatar AI FirstD3V avatar Yuki Kodama avatar Ken Sonoda avatar Benjamin Dos Santos avatar Gonçalo Mendes Cabrita avatar Adam Gray avatar Gilbert Gathara avatar Kaarthik Andavar avatar Hilman Ibnu Assiddiq avatar warman avatar Nail avatar Matija Osrečki avatar Mislav avatar Jose Albizures avatar mannymu avatar Jesse Holden avatar Jamie Haywood avatar Viliam Kopecký avatar  avatar  avatar Marwan Hilmi avatar Petar Paunovic avatar Sophy SEM avatar Marcin Bielak avatar Muhammad Zakir Ramadhan  avatar sakamotor avatar BunyIp avatar Chris McMordie avatar Christopher Robin avatar Peter Kadlot avatar Brian Kim avatar daichan4649 avatar Timur Brachkow avatar Diogo Ribeiro avatar Andy Lu avatar

Watchers

 avatar  avatar Flip van Haaren avatar  avatar devcomfort avatar Ame_x avatar

honox's Issues

README is missing Vite details

I'm trying to get a basic example of file-based routing working. The README provides a basic vite.config.ts file, but it doesn't describe the changes needed in package.json to use Vite. I tried changing the "dev" script to just run "vite". But when I run that script with bun dev I get the following error:

Error: Cannot find module @rollup/rollup-darwin-x64. npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). Please try `npm i` again after removing both package-lock.json and node_modules directory.

Doing what it suggests didn't fix the error for me. I'm running in macOS.

Add `_middleware.ts` file

What is the feature you are proposing?

Would it be possible to create a _middleware.ts file that would automatically add the middleware to routes at and below it? Cumulative _middleware declarations would be merged, with higher ones taking precedence in the ordering.

//_middleware.ts
import { someMiddleware } from '../middleware/someMiddleware'
import { someOtherMiddleware } from '../middleware/someOtherMiddleware'

// middleware would be ran in the order declared here
exports default [ someMiddleware, someOtherMiddleware ]

This would make it unnecessary to have to redeclare the middleware pipeline for each route, or manually create a Hono router just to gain access to the use method.

Directory structure would look like:

.
└── app/
    └── routes/
        ├── some-path/
        │   ├── uses-middleware/
        │   │   └── index.ts    <--uses _middleware
        │   ├── _middleware.ts
        │   └── index.ts        <--uses _middleware
        └── index.ts

Local D1 DB persists to wrong directory not picked up by vite

What version of HonoX are you using?

0.1.3

What steps can reproduce the bug?

Clone the examples repository https://github.com/yusukebe/honox-examples/tree/main/projects/blog
cd to projects/blog
run npx wrangler d1 execute hono-blog-demo --local --file=./blog.sql
run yarn dev

What is the expected behavior?

Local DB is created and when you launch the app it works with seeded db from blog.sql

What do you see instead?

D1_ERROR: no such table: articles

Additional information

the local db is being created in .wrangler, the vite dev plugin is looking in .mf

If you run build and then wrangler pages dev ./dist this works, as the wrangler command shims the database in .wrangler and is available to honox

It seems there needs to be something tweaked with the miniflare setup to pickup local d1 correctly

How to use packages written in commonjs on the server

I'm hitting a wall trying to make my server do the stuff I need it to do with packages that are written in commonjs using require.
I'm not super familiar with vite's bundling process so there's little I can investigate here.

The error I'm getting is:
[vite] Internal server error: require is not defined

`index.get.tsx` will be `.get`

What version of HonoX are you using?

0.1.0

What steps can reproduce the bug?

Put the route file on app/routes/posts/index.get.tsx:

And export app:

import { Hono } from 'hono'

const app = new Hono()

app.get((c) => {
  return c.render('GET!')
})

export default app

What is the expected behavior?

The routing will be:

GET  /posts/index.get

OR 404.

What do you see instead?

GET  /posts/.get

Additional information

No response

disableSSG from hono/ssg is not work with HonoX

What version of HonoX are you using?

0.1.1

What steps can reproduce the bug?

  1. npx create hono@latest , select "x-basic"
  2. Add routes using Hono instance or createRoute and use disableSSG from "hono/ssg"
  3. Generate pages by build.ts that runs https://github.com/honojs/vite-plugins/blob/55e3abfb8017c7ee9606458da665e3f09e0d428f/packages/ssg/src/ssg.ts#L34-L50 , or use "@hono/vite-ssg"
  4. Generated codes include text files that contains "SSG is disabled"

What is the expected behavior?

Not even a text file containing "SSG is disabled" is left behind, and nothing is generated from the disableSSG route.

What do you see instead?

Text files that contains "SSG is disabled"

Additional information

This is runtime agnostic issue. Reproduced in Node v21.6.0, Bun 1.0.23, Deno 1.39.4

got an error when I try to use react

What version of HonoX are you using?

0.1.0

What steps can reproduce the bug?

Follow the example below to set up react and run npm run dev.
https://github.com/honojs/honox?tab=readme-ov-file#byor---bring-your-own-renderer

What is the expected behavior?

no error

What do you see instead?

[vite] Internal server error: module is not defined
      at eval (/.../node_modules/react/jsx-dev-runtime.js:8:3)
      at instantiateModule (file:///.../node_modules/vite/dist/node/chunks/dep-stQc5rCc.js:54696:15)

Additional information

The following changes were made to avoid this

diff --git a/src/vite/index.ts b/src/vite/index.ts
index 2318e86..e9bd409 100644
--- a/src/vite/index.ts
+++ b/src/vite/index.ts
@@ -47,7 +47,7 @@ function honox(options?: Options): PluginOption[] {
       config: () => {
         return {
           ssr: {
-            noExternal: true,
+            noExternal: ['honox'],
           },
         }
       },

More than one [dynamic-path-parm] causes regex error

What version of HonoX are you using?

0.1.0

What steps can reproduce the bug?

  • create a new project via npm create@hono and x-basic
  • add a directory structure with 2 dynamic path parameters
  • Start the server, and observe the error

What is the expected behavior?

I would expect any number of dynamic path parameters to work correctly

What do you see instead?

Hono crashes

[vite] Internal server error: Invalid regular expression: /^/:arg1]/[arg2(?:|/.*)$/: Unterminated character class

Additional information

In either case (and possibly more) declaring more than one dynamic path parameter causes an error.

// double nested directories
.
└── app/
    └── routes/
        ├── [arg1]/
        │   └── [arg2]/
        │       └── index.tsx
        └── index.ts
// nested directory with dynamic path on the file
.
└── app/
    └── routes/
        ├── [arg1]/
        │   └── [arg2].tsx
        └── index.ts

The issue can be worked around by exporting a Hono router and manually writing the paths

Formal documentation?

What is the feature you are proposing?

The current readme is getting pretty long. Can we get a formal documentation repo (similar to Hono) to start moving some of this stuff over?

Or perhaps add a section to the Hono docs specifically for HonoX.

Support `@hono/zod-openapi` type routes

What is the feature you are proposing?

I found the @hono/zod-openapi to be great for producing OpenApi documentation fairly painlessly. The current HonoX API is very close to what @hono/zod-openapi does.

https://github.com/honojs/middleware/tree/main/packages/swagger-ui#with-openapihono-usage

Is it possible to get the same behavior in HonoX? I know you can declare routes in the "classic" Hono fashion to make use of this, but I was wondering if there is a way to make this a first-class feature of HonoX.

Can't create a xbasic hono app

What version of HonoX are you using?

create-hono version 0.4.0

What steps can reproduce the bug?

I've tried creating a x-basic hono app with Deno, yarn and npm but I still get the same error. I'd run npm create hono@latest or yarn create hono my-app or deno run -A npm:create-hono my-app

What is the expected behavior?

Create an x-basic starter file like this

├── app
│   ├── global.d.ts // global type definitions
│   ├── routes
│   │   ├── _404.tsx // not found page
│   │   ├── _error.tsx // error page
│   │   ├── _renderer.tsx // renderer definition
│   │   ├── about
│   │   │   └── [name].tsx // matches /about/:name
│   │   └── index.tsx // matches /
│   └── server.ts // server entry file
├── package.json
├── tsconfig.json
└── vite.config.ts

What do you see instead?

`C:\Users\fredr\AppData\Local\npm-cache_npx\6bbb1d6f54609fb5\node_modules\create-hono\bin:9918
this[_onError](new ZlibError(err));
^

ZlibError: zlib: unexpected end of file
at Unzip.write (C:\Users\fredr\AppData\Local\npm-cache_npx\6bbb1d6f54609fb5\node_modules\create-hono\bin:9918:26)
at Unzip.flush (C:\Users\fredr\AppData\Local\npm-cache_npx\6bbb1d6f54609fb5\node_modules\create-hono\bin:9883:14)
at Unzip.end (C:\Users\fredr\AppData\Local\npm-cache_npx\6bbb1d6f54609fb5\node_modules\create-hono\bin:9888:14)
at Unpack.end (C:\Users\fredr\AppData\Local\npm-cache_npx\6bbb1d6f54609fb5\node_modules\create-hono\bin:12740:25)
at Pipe.end (C:\Users\fredr\AppData\Local\npm-cache_npx\6bbb1d6f54609fb5\node_modules\create-hono\bin:9338:21)
at [emitEnd2] (C:\Users\fredr\AppData\Local\npm-cache_npx\6bbb1d6f54609fb5\node_modules\create-hono\bin:9699:13)
at [emitEnd] (C:\Users\fredr\AppData\Local\npm-cache_npx\6bbb1d6f54609fb5\node_modules\create-hono\bin:9686:25)
at ReadStream.emit (C:\Users\fredr\AppData\Local\npm-cache_npx\6bbb1d6f54609fb5\node_modules\create-hono\bin:9643:31)
at ReadStream.emit (C:\Users\fredr\AppData\Local\npm-cache_npx\6bbb1d6f54609fb5\node_modules\create-hono\bin:12103:26)
at [maybeEmitEnd] (C:\Users\fredr\AppData\Local\npm-cache_npx\6bbb1d6f54609fb5\node_modules\create-hono\bin:9629:16) {
code: 'Z_BUF_ERROR',
errno: -5,
recoverable: false,
file: 'C:\Users\fredr\.degit\github\honojs\starter/cb09fdcd58830a1ef005be1e418a0f235c9e69d3.tar.gz',
cwd: 'C:/Users/fredr/OneDrive/Desktop/WORKSPACE/Honox/my-app',
tarCode: 'TAR_ABORT'
}

Node.js v20.9.0
npm ERR! code 1
npm ERR! path C:\Users\fredr\OneDrive\Desktop\WORKSPACE\Honox
npm ERR! command failed
npm ERR! command C:\WINDOWS\system32\cmd.exe /d /s /c create-hono my-app`

Additional information

Node version - 20.9.0
OS - Windows 10 pro Build 19045

`/static/client` not found after build

What version of HonoX are you using?

0.1.5

What steps can reproduce the bug?

  • follow step to renderer react
  • include react and react-dom to vite config on ssr.external
  • npm run build
  • npm run preview

What is the expected behavior?

Client component should be found and work

What do you see instead?

GET /static/client.js net::ERR_ABORTED 404 (Not Found)

Additional information

Repo to reproduce: https://github.com/iamyuu/play-honox-react

Inside directory `/dist/static/`

pWLA5K1T 2024-02-23 at 11 04 PM@2x

Circular dependency?

What version of HonoX are you using?

[email protected], [email protected]

What steps can reproduce the bug?

bun create hono my-app
// select x-basic
cd my-app
bun run build

What is the expected behavior?

No warnings.

What do you see instead?

Export "use" of module "node_modules/hono/dist/jsx/hooks/index.js" was reexported through module "node_modules/hono/dist/jsx/dom/index.js" while both modules are dependencies of each other and will end up in different chunks by current Rollup settings. This scenario is not well supported at the moment as it will produce a circular dependency between chunks and will likely lead to broken execution order.
Either change the import in "node_modules/honox/dist/client/runtime.js" to point directly to the exporting module or reconfigure "output.manualChunks" to ensure these modules end up in the same chunk.
Screenshot 2024-02-24 at 23 45 20

Additional information

No response

ENV in Honox

What is the feature you are proposing?

I looked through the documentation and I did not see anywhere documenting how to use ENVs. I checked VITE and saw that I can use ENV by creating an .env file and adding the VITE_ prefix to the env name. Sample VITE_HELLO=WORLD.
Is this the best way or is there another?

`Bun.serve()` did not return a Response object.

What version of HonoX are you using?

0.1.3+2

What steps can reproduce the bug?

Use dev container to setup environment, and bun i then bun dev in the container.
Here's my boilerplate to reproduce the bug.
https://github.com/ryuujo1573/blue/tree/a23b3b0099b9be3043116c33c18f2431f0dcf077

Everything should be ok, as it is fine in my wsl2 host (ubuntu focal).

What is the expected behavior?

No response

What do you see instead?

image

Additional information

No response

Can islands support shared state?

Hi there! Really loving honox!

I'm experimenting with using it as a react renderer and am running into issues with islands sharing context or state. Specifically I'm trying to wrap some islands that use react query with a parent island that sets the query client context.

Should this be possible or am I misunderstanding now islands should behave?

Thanks so much!

Islands component that accepts children

What is the feature you are proposing?

Current islands component can not render children in the client side.

If we were achieve this, it means we have "Donut Components" pattern.

One of the reasons is that the child is not passed in this part, but in addition to this, it seems necessary to modify the hydrate process when an island exists within an island.

SSG + Client script

What is the feature you are proposing?

(update) Does not work even with the steps written in the README , there is no client bundles!

Currently, @hono/vite-ssg and honox/vite/client combination cannot work in naive way.

  1. vite build --mode client && vite build results the latter build cleanups the former.
  2. Failed to vite build . A generated client script referenced by Script component cannot resolve by Vite.
current `vite.config.ts` file example
// vite.config.ts
import honox from "honox/vite";
import client from "honox/vite/client";
import { defineConfig } from "vite";
import ssg from "@hono/vite-ssg";

const entry = "./app/server.ts";

export default defineConfig(({ mode, command }) => {
  const plugins =
    mode === "client"
      ? [client()]
      : [honox(), ssg({ entry })];

  return {
    build: {
      rollupOptions: {
        input: ["./app/style.css"],
        output: {
          assetFileNames: "static/assets/[name].[ext]",
        },
      }
    },
    plugins,
  };
});
An error log of server build
Error: [vite]: Rollup failed to resolve import "/static/client-qvhPWvF1.js" from "(repo-root)/.hono/index.html".
This is most likely unintended because it can break your application at runtime.
If you do want to externalize this module explicitly add it to
`build.rollupOptions.external`
    at viteWarn (file://(repo-root)/node_modules/vite/dist/node/chunks/dep-94_H5fT6.js:67040:27)
    at onRollupWarning (file://(repo-root)/node_modules/vite/dist/node/chunks/dep-94_H5fT6.js:67068:9)
    at onwarn (file://(repo-root)/node_modules/vite/dist/node/chunks/dep-94_H5fT6.js:66777:13)
    at file://(repo-root)/node_modules/rollup/dist/es/shared/node-entry.js:17457:13
    at Object.logger [as onLog] (file://(repo-root)/node_modules/rollup/dist/es/shared/node-entry.js:19117:9)
    at ModuleLoader.handleInvalidResolvedId (file://(repo-root)/node_modules/rollup/dist/es/shared/node-entry.js:18061:26)
    at file://(repo-root)/node_modules/rollup/dist/es/shared/node-entry.js:18019:26
error: script "build" exited with code 1

Use of build.rollupOptions.external does not work. A generated script element will be removed in a generated HTML.

WORKAROUND for 1: Write a custom plugin that build.emptyOutDir set to false.
This derives from build.emptyOutDir set to true by @hono/vite-ssg .

WORKAROUND for 2: Use resolve.alias.

`vite.config.ts` file with workaround
// vite.config.ts
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import honox from "honox/vite";
import client from "honox/vite/client";
import { defineConfig } from "vite";
import ssg from "@hono/vite-ssg";

const entry = "./app/server.ts";

export default defineConfig(({ mode, command }) => {
  const plugins =
    mode === "client"
      ? [client()]
      : [
          honox({ entry }),
          ssg({ entry }),
          {
            config() {
              return { build: { emptyOutDir: false } };
            },
          },
        ];

  return {
    build: {
      rollupOptions: {
        input: ["./app/style.css"],
        output: {
          assetFileNames: "static/assets/[name].[ext]",
        },
      }
    },
    plugins,
    resolve: {
      alias: [
        {
          find: /^\/static\/(.*?)\.js/,
          replacement: resolve(
            // Node 18 support, for 20 or upper, `import.meta.dirname` also works
            dirname(fileURLToPath(import.meta.url)),
            "dist/static/$1.js"
          ),
        },
      ],
    },
  };
});

Reload when add or remove a route file

What is the feature you are proposing?

This is like a bug.

The dev server does not restart/reload the app when adding/removing a route file. This is a @hono/vite-dev-server matter. I have some ideas to resolve this issue, but these are not super smart ways. Anyway, I'll add a change as like work-around.

Cursor on an input box gets kicked out with a specific condition

What version of HonoX are you using?

0.1.3 / Hono: 4.0.3

What steps can reproduce the bug?

  1. Clone the repository: https://github.com/wataruoguchi/honox-repro and run npm install && npm run dev.
  2. Type multiple characters (e.g., "1111") on each input element.
  3. Your cursor gets kicked out on every key press with the third input element.

The UI

Screenshot 2024-02-16 at 2 33 10 PM

The issue occurs with a very specific condition.

  1. The input element has a corresponding label. (Please see hasLabel in the code)
  2. The corresponding state (str) is NOT rendered in a sibling element of the input element. (form.tsx vs form2.tsx)

What is the expected behavior?

The input cursor stays in the input element so that you can type more than one character at a time.

What do you see instead?

The input cursor gets kicked out, hence, you can only type one character at a time. You need to click on the input element every time you type in it.

Additional information

No response

Note for the development

This is like a note for the development of HonoX. It will be edited.

  • Versioning: Since it is currently in the "alpha" phase, semantic versioning will not be followed. Even if new features are added, they may be released as patch
  • Contributing: Contributions are welcome; HonoX is not yet complete, but it is simple. So it will be easy to contribute!
  • Tryring: It would also be a contributor to let us know your feedback after you use it.

In the honox-island tag, SVG does not display properly

What version of HonoX are you using?

0.1.3

What steps can reproduce the bug?

image

SVG tags do not render properly when they are in honox-island.

What is the expected behavior?

Render SVG normally

What do you see instead?

SVG does not display properly

Additional information

No response

[suggestion] base-x routing path-parameter type is any

Note

You can ignore if you can not get this issue clearly.
I understand this honox project is alpha version.

What is the feature you are proposing?

I need type suggestion for path parameter(c.req.param()) in createRoute.

What version of HonoX are you using?

0.1.0

What steps can reproduce the bug?

create a new project via pnpm create@hono and x-basic
add a directory structure with 2 dynamic path parameters (app/routes/companies/[companyId]/[postId]/index.ts)

What is the expected behavior?

When I type c.req.param(' then the VSCode(TS Server) suggest 'companyId' and 'postId'

What do you see instead?

When I type c.req.param(' then the VSCode(TS Server) suggest nothing

Additional information

example code

app/routes/companies/[companyId]/[postId]/index.ts

import { zValidator } from '@hono/zod-validator'
import { createRoute } from 'honox/factory'
import { object, string } from 'zod'

const paramsSchema = object({
  companyId: string(),
  postId: string(),
})

// before route.get('companies/:companyId/:postId',async (c) => {
// try const GET = createRoute<{ in: { param: ParamSchema } }>(async (c) => {
const GET = createRoute(zValidator('param', paramsSchema), async (c) => {
  // no suggestion for 'companyId' because param<any>(key: string) is any Generics
  const companyId = c.req.param('companyId')
  const postId = c.req.param('postId')

  return c.json({})
})

// biome-ignore lint/style/noDefaultExport: Router specification
export default GET
// type 
const GET: [H<Env, any, {
    in: {
        param: {
            companyId: string;
            postId: string;
        };
    };
    out: {
        param: {
            companyId: string;
            postId: string;
        };
    };
}, Promise<Response & TypedResponse<...>>>, H<...>]

Type Error Occurs When Trying to Use React with createClient

What version of HonoX are you using?

0.1.4

What steps can reproduce the bug?

I attempted to set up the React renderer as described in the README, but encountered a type error.

Write ./app/client.ts.

import { createClient } from "honox/client";

createClient({
  hydrate: async (elem, root) => {
    const { hydrateRoot } = await import("react-dom/client");
    hydrateRoot(root, elem);
  },
  createElement: async (type: any, props: any) => {
    const { createElement } = await import("react");
    return createElement(type, props);
  },
});

Type Error Occurs.

Screenshot 2024-02-23 at 6 50 34

1. For the part hydrateRoot(root, elem);

Argument of type 'Node' is not assignable to parameter of type 'ReactNode'.

2. For the part createElement

Type '(type: any, props: any) => Promise<React.CElement<any, React.Component<any, any, any>>>' is not assignable to type 'CreateElement'.
  Type 'Promise<CElement<any, Component<any, any, any>>>' is not assignable to type 'Node | Promise<Node>'.
    Type 'Promise<CElement<any, Component<any, any, any>>>' is not assignable to type 'Promise<Node>'.
      Type 'ComponentElement<any, Component<any, any, any>>' is missing the following properties from type 'Node': baseURI, childNodes, firstChild, isConnected, and 46 more.

What is the expected behavior?

I expect that no type errors will occur.

What do you see instead?

No response

Additional information

It seems the same issue occurs at https://github.com/yusukebe/honox-playground/tree/main/projects/react.
If necessary, I can create a minimal sandbox to reproduce the issue.

Directory based router

What is the feature you are proposing?

I would like to do directory-based routing like SvelteKit and Next.js App Router.
Either in a way like (+)page.ts or separate files for each HTTP method and route them like +get.ts and +post.ts.

Middleware registered in `server.ts` doesn't work

  • Create a new app using npm create hono@latest
  • Select x-basic
  • Alter server.ts to include some middleware:
//app/server.ts

import { cors } from 'hono/cors';
import { createApp } from 'honox/server';
import { poweredBy } from 'hono/powered-by';
import { secureHeaders } from 'hono/secure-headers';
import { showRoutes } from 'hono/dev';
import { timing } from 'hono/timing';

const app = createApp();
app.use(
  cors(),
  secureHeaders(),
  timing(),
  poweredBy(),
  (ctx, next) => {
    console.log('middleware!');
    return next();
  }
);

showRoutes(app);

export default app;

Upon running, none of the middleware seems to be working. None of the response headers are altered per the middleware. This can also be verified because the middleware! text is never logged to the output.

Directories with leading dot are ignored

Not sure if this goes here or over at Vite, since the problem stems from their side. See the following discussion post I made over there:

vitejs/vite#15859

I wasn't sure if it was possible to alter the globbing pattern or add an additional one from the context of this plugin to additionally add those hidden/dot files back in.

I don't know how flexible fast-glob is, but could something similar to this work?
https://askubuntu.com/a/1452417


One work-around here is to just create a Hono router in the directory and manually create the path (avoiding using directories), so there is a path forward if this cannot be fixed at this level.

Great work on this regardless!

Special attributes to remove elements

For example, you want to remove the script tags on a page if the page does not load islands.

Like:

<script honox-only-has-islands />

And also, we can write this:

<script honox-only-env-prod />

[suggestion] Add Cloudflare DevServer config to Docs

My previous Hono app used the @hono/vite-dev-server package to setup local Cloudflare services. HonoX includes this, and it is all configured via the honox plugin, but the docs don't make this immediately obvious.

import { defineConfig } from 'vite';
import client from 'honox/vite/client';
import honox from 'honox/vite';
import pages from '@hono/vite-cloudflare-pages';

export default defineConfig(({ mode }) => {
  if (mode === 'client') {
    return {
      plugins: [client()],
    };
  } else {
    return {
      plugins: [
        honox({
          devServer: {
            cf: {
              d1Databases: { TORCH_DATABASE: 'TORCH_DATABASE' },
              d1Persist: '../../.wrangler/state/v3/d1',
              r2Buckets: ['TORCH_R2'],
              r2Persist: '../../.wrangler/state/v3/r2',
              kvNamespaces: ['TORCH_AUTH'],
              kvPersist: '../../.wrangler/state/v3/kv',
            },
          },
        }),
        pages(),
      ],
    };
  }
});

re-export `<Style />` in `hono/css` from `honox/server`

What is the feature you are proposing?

Hi!

How about that re-export <Style /> in hono/css from honox/server?

The Script and Style are provided from separate sources, which would be confusing if you were set up without a template.

I agree that hono/css itself is not an essential package for honox, so I would prefer to distribute it from a separate location.

I posted the issue to see if there is some other better way to distribute the information, even if it is not this way.

like this:

import { Script, Style } from 'honox/server'

Screenshot 2024-02-21 at 0 51 30

Support import css files

What is the feature you are proposing?

Now, in order to use tailwindcss with HonoX, we need to configure it with vite and add link tags for dev and prod envs in head.
This seems a bit troublesome.

It would be good if we could just import it as following:

import "../style.css"

If there is already a way to achieve this, please comment it.

[suggestion] createApp config in server.ts

What is the feature you are proposing?

Thank you for developing a great framework!
Currently, I am developing using Honox. I've set the routes directory to src/app/routes, and to accommodate this, I made the following changes in src/app/server.ts:

const app = createApp({
  root: "/src/app/routes",
  RENDERER: import.meta.glob("/src/app/routes/**/_renderer.tsx", {
    eager: true,
  }),
  ROUTES: import.meta.glob("/src/app/routes/**/[!_]*.(ts|tsx|mdx)", {
    eager: true,
  }),
});

Even though root is set, I found it inconvenient that changes to RENDERER and ROUTES are required for pages to reflect.
Upon inspecting the source code, I noticed that all four strings ROUTES, RENDERER, NOT_FOUND, and ERROR contain /app/routes. Therefore, if root is set in the config, how about we automatically insert root into these parts unless there's specific config for ROUTES, RENDERER, NOT_FOUND, and ERROR?

For example, for NOT_FOUND_FILE:

 const NOT_FOUND_FILE =
    options?.NOT_FOUND ??
    import.meta.glob<NotFoundFile>(`${root}/**/_404.(ts|tsx)`, {
      eager: true,
    })

Import `jsx` function from "hono/jsx/dom/jsx-runtime"

requires honojs/hono#2162

Import jsx from "hono/jsx/dom/jsx-runtime" to reduce the client size.

before

image

after

image

type of createElement

Since different versions of jsx transformer have different arguments, it is difficult to define the type, but since honox does not currently use either children or keys, I think we can just assume that there are two arguments.

patch

I can't make a PR because I can't fork, so I'll just put up a patch for now.

diff --git a/README.md b/README.md
index ff646d9..827aab6 100644
--- a/README.md
+++ b/README.md
@@ -403,9 +403,9 @@ createClient({
     const { hydrateRoot } = await import('react-dom/client')
     hydrateRoot(root, elem)
   },
-  createElement: async (type: any, props: any, ...children: any[]) => {
+  createElement: async (type: any, props: any) => {
     const { createElement } = await import('react')
-    return createElement(type, props, ...children)
+    return createElement(type, props)
   },
 })
diff --git a/src/client/client.ts b/src/client/client.ts
index 3d11cda..810d0c3 100644
--- a/src/client/client.ts
+++ b/src/client/client.ts
@@ -1,5 +1,5 @@
-import { jsx as jsxFn } from 'hono/jsx'
 import { render } from 'hono/jsx/dom'
+import { jsx as jsxFn } from 'hono/jsx/dom/jsx-runtime'
 import { COMPONENT_NAME, DATA_SERIALIZED_PROPS } from '../constants.js'
 import type { CreateElement, Hydrate } from '../types.js'
 
diff --git a/src/types.ts b/src/types.ts
index 214acb5..1149d51 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,5 +1,5 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 
 /** JSX */
-export type CreateElement = (type: any, props: any, ...children: any[]) => Node | Promise<Node>
+export type CreateElement = (type: any, props: any) => Node | Promise<Node>
 export type Hydrate = (children: Node, parent: Element) => void | Promise<void>

Islands detection in a non-route file

What is the feature you are proposing?

Current HasIsland component only detects islands in a route file.

If in a non-route component could detect using island, it could be something similar to "use client" in React.

We often want to use components with interactions at the end of the tree. It's not something we can immediately judge based on the route.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.