├── README.md
├── .prettierrc
├── pnpm-workspace.yaml
├── packages
└── next-sandbox
│ ├── src
│ ├── index.ts
│ ├── icons
│ │ ├── play-icon.tsx
│ │ ├── loader-icon.tsx
│ │ └── log-icon.tsx
│ ├── sandbox-server.tsx
│ ├── with-sandbox.tsx
│ ├── highlighted-json.tsx
│ ├── sandbox-context.tsx
│ ├── log-drawer.tsx
│ ├── style.css
│ └── sandbox-ui.tsx
│ ├── tsconfig.json
│ ├── README.md
│ └── package.json
├── apps
├── docs
│ ├── .eslintrc.json
│ ├── app
│ │ ├── favicon.ico
│ │ ├── global.css
│ │ ├── api
│ │ │ └── search
│ │ │ │ └── route.ts
│ │ ├── (home)
│ │ │ ├── sandbox
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── action.ts
│ │ │ ├── page.tsx
│ │ │ └── layout.tsx
│ │ ├── docs
│ │ │ ├── layout.tsx
│ │ │ └── [[...slug]]
│ │ │ │ └── page.tsx
│ │ ├── docs-og
│ │ │ └── [...slug]
│ │ │ │ └── route.tsx
│ │ ├── layout.config.tsx
│ │ └── layout.tsx
│ ├── public
│ │ ├── banner.png
│ │ └── avatars
│ │ │ ├── theo.png
│ │ │ ├── dakdevs.png
│ │ │ ├── bedesqui.png
│ │ │ └── lermatroid.png
│ ├── postcss.config.mjs
│ ├── static
│ │ ├── demo-screenshot-dark.png
│ │ └── demo-screenshot-light.png
│ ├── lib
│ │ ├── source.ts
│ │ └── metadata.ts
│ ├── content
│ │ └── docs
│ │ │ ├── meta.json
│ │ │ ├── enable.mdx
│ │ │ ├── index.mdx
│ │ │ ├── before-render.mdx
│ │ │ └── functions.mdx
│ ├── source.config.ts
│ ├── .gitignore
│ ├── next.config.mjs
│ ├── README.md
│ ├── tsconfig.json
│ ├── package.json
│ └── components
│ │ ├── illustrations
│ │ ├── arrow-down.tsx
│ │ ├── arrow-right.tsx
│ │ └── light.tsx
│ │ ├── hero.tsx
│ │ ├── feature.tsx
│ │ ├── how.tsx
│ │ └── tweets.tsx
└── test
│ ├── app
│ ├── favicon.ico
│ ├── page.tsx
│ ├── globals.css
│ ├── sandbox-action.ts
│ └── layout.tsx
│ ├── public
│ ├── vercel.svg
│ ├── window.svg
│ ├── file.svg
│ ├── globe.svg
│ └── next.svg
│ ├── next.config.ts
│ ├── postcss.config.mjs
│ ├── eslint.config.mjs
│ ├── tailwind.config.ts
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── package.json
│ └── README.md
├── .gitignore
├── .github
└── dependabot.yml
├── .prettierignore
├── turbo.json
├── package.json
└── LICENSE
/README.md:
--------------------------------------------------------------------------------
1 | packages/next-sandbox/README.md
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "tabWidth": 2
4 | }
5 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'apps/*'
3 | - 'packages/*'
4 |
--------------------------------------------------------------------------------
/packages/next-sandbox/src/index.ts:
--------------------------------------------------------------------------------
1 | export { withSandbox } from './with-sandbox';
2 |
--------------------------------------------------------------------------------
/apps/docs/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "next/typescript"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules
3 |
4 | # build
5 | dist
6 |
7 | # Turbo
8 | .turbo
9 |
--------------------------------------------------------------------------------
/apps/docs/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1weiho/next-sandbox/HEAD/apps/docs/app/favicon.ico
--------------------------------------------------------------------------------
/apps/test/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1weiho/next-sandbox/HEAD/apps/test/app/favicon.ico
--------------------------------------------------------------------------------
/apps/docs/public/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1weiho/next-sandbox/HEAD/apps/docs/public/banner.png
--------------------------------------------------------------------------------
/apps/docs/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | '@tailwindcss/postcss': {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/apps/docs/public/avatars/theo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1weiho/next-sandbox/HEAD/apps/docs/public/avatars/theo.png
--------------------------------------------------------------------------------
/apps/docs/public/avatars/dakdevs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1weiho/next-sandbox/HEAD/apps/docs/public/avatars/dakdevs.png
--------------------------------------------------------------------------------
/apps/docs/public/avatars/bedesqui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1weiho/next-sandbox/HEAD/apps/docs/public/avatars/bedesqui.png
--------------------------------------------------------------------------------
/apps/docs/public/avatars/lermatroid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1weiho/next-sandbox/HEAD/apps/docs/public/avatars/lermatroid.png
--------------------------------------------------------------------------------
/apps/docs/static/demo-screenshot-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1weiho/next-sandbox/HEAD/apps/docs/static/demo-screenshot-dark.png
--------------------------------------------------------------------------------
/apps/docs/static/demo-screenshot-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1weiho/next-sandbox/HEAD/apps/docs/static/demo-screenshot-light.png
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 |
--------------------------------------------------------------------------------
/apps/test/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Build
2 | dist
3 |
4 | # Package
5 | pnpm-lock.yaml
6 | node_modules
7 |
8 | # Next.js
9 | .next
10 |
11 | # Turbo
12 | .turbo
13 |
--------------------------------------------------------------------------------
/apps/test/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from 'next';
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | };
6 |
7 | export default nextConfig;
8 |
--------------------------------------------------------------------------------
/apps/docs/app/global.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 | @import 'fumadocs-ui/css/neutral.css';
3 | @import 'fumadocs-ui/css/preset.css';
4 |
5 | @source '../node_modules/fumadocs-ui/dist/**/*.js';
6 |
--------------------------------------------------------------------------------
/apps/test/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/apps/docs/app/api/search/route.ts:
--------------------------------------------------------------------------------
1 | import { source } from '@/lib/source';
2 | import { createFromSource } from 'fumadocs-core/search/server';
3 |
4 | export const { GET } = createFromSource(source);
5 |
--------------------------------------------------------------------------------
/apps/docs/lib/source.ts:
--------------------------------------------------------------------------------
1 | import { docs } from '@/.source';
2 | import { loader } from 'fumadocs-core/source';
3 |
4 | export const source = loader({
5 | baseUrl: '/docs',
6 | source: docs.toFumadocsSource(),
7 | });
8 |
--------------------------------------------------------------------------------
/packages/next-sandbox/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "target": "ES2022",
5 | "module": "ESNext",
6 | "moduleResolution": "bundler"
7 | },
8 | "include": ["src"]
9 | }
10 |
--------------------------------------------------------------------------------
/apps/docs/app/(home)/sandbox/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 |
3 | export default function Layout({ children }: { children: ReactNode }) {
4 | return
{children}
;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/docs/lib/metadata.ts:
--------------------------------------------------------------------------------
1 | import { createMetadataImage } from 'fumadocs-core/server';
2 | import { source } from './source';
3 |
4 | export const metadataImage = createMetadataImage({
5 | imageRoute: '/docs-og',
6 | source,
7 | });
8 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Framework",
3 | "description": "The docs framework",
4 | "icon": "Building2",
5 | "root": true,
6 | "pages": [
7 | "---Introduction---",
8 | "index",
9 | "---Props---",
10 | "functions",
11 | "before-render",
12 | "enable"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "ui": "tui",
4 | "tasks": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
8 | },
9 | "dev": {
10 | "persistent": true,
11 | "cache": false
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-sandbox-monorepo",
3 | "private": true,
4 | "scripts": {
5 | "dev": "turbo dev",
6 | "format": "prettier --write .",
7 | "format:check": "prettier --check ."
8 | },
9 | "devDependencies": {
10 | "prettier": "^3.5.3",
11 | "turbo": "^2.4.0"
12 | },
13 | "packageManager": "pnpm@10.2.0"
14 | }
15 |
--------------------------------------------------------------------------------
/apps/docs/source.config.ts:
--------------------------------------------------------------------------------
1 | import { remarkInstall } from 'fumadocs-docgen';
2 | import { defineDocs, defineConfig } from 'fumadocs-mdx/config';
3 |
4 | export const docs = defineDocs({
5 | dir: 'content/docs',
6 | });
7 |
8 | export default defineConfig({
9 | mdxOptions: {
10 | // MDX options
11 | remarkPlugins: [remarkInstall],
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/apps/test/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { withSandbox } from 'next-sandbox';
2 | import { getAllPosts, seedPosts } from './sandbox-action';
3 |
4 | export default withSandbox({
5 | functions: [
6 | {
7 | name: 'Get All Posts',
8 | function: getAllPosts,
9 | },
10 | {
11 | name: 'Seed Posts',
12 | function: seedPosts,
13 | },
14 | ],
15 | });
16 |
--------------------------------------------------------------------------------
/apps/test/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/test/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # deps
2 | /node_modules
3 |
4 | # generated content
5 | .contentlayer
6 | .content-collections
7 | .source
8 |
9 | # test & build
10 | /coverage
11 | /.next/
12 | /out/
13 | /build
14 | *.tsbuildinfo
15 |
16 | # misc
17 | .DS_Store
18 | *.pem
19 | /.pnp
20 | .pnp.js
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | # others
26 | .env*.local
27 | .vercel
28 | next-env.d.ts
--------------------------------------------------------------------------------
/apps/docs/next.config.mjs:
--------------------------------------------------------------------------------
1 | import { createMDX } from 'fumadocs-mdx/next';
2 |
3 | const withMDX = createMDX();
4 |
5 | /** @type {import('next').NextConfig} */
6 | const config = {
7 | reactStrictMode: true,
8 | images: {
9 | remotePatterns: [
10 | {
11 | protocol: 'https',
12 | hostname: 'pbs.twimg.com',
13 | },
14 | ],
15 | },
16 | };
17 |
18 | export default withMDX(config);
19 |
--------------------------------------------------------------------------------
/apps/docs/app/docs/layout.tsx:
--------------------------------------------------------------------------------
1 | import { DocsLayout } from 'fumadocs-ui/layouts/docs';
2 | import type { ReactNode } from 'react';
3 | import { baseOptions } from '@/app/layout.config';
4 | import { source } from '@/lib/source';
5 |
6 | export default function Layout({ children }: { children: ReactNode }) {
7 | return (
8 |
9 | {children}
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/apps/test/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --background: #ffffff;
7 | --foreground: #171717;
8 | }
9 |
10 | @media (prefers-color-scheme: dark) {
11 | :root {
12 | --background: #0a0a0a;
13 | --foreground: #ededed;
14 | }
15 | }
16 |
17 | body {
18 | color: var(--foreground);
19 | background: var(--background);
20 | font-family: Arial, Helvetica, sans-serif;
21 | }
22 |
--------------------------------------------------------------------------------
/apps/test/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from 'path';
2 | import { fileURLToPath } from 'url';
3 | import { FlatCompat } from '@eslint/eslintrc';
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends('next/core-web-vitals', 'next/typescript'),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/apps/test/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss';
2 |
3 | export default {
4 | content: [
5 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | background: 'var(--background)',
13 | foreground: 'var(--foreground)',
14 | },
15 | },
16 | },
17 | plugins: [],
18 | } satisfies Config;
19 |
--------------------------------------------------------------------------------
/apps/docs/app/(home)/sandbox/page.tsx:
--------------------------------------------------------------------------------
1 | import { withSandbox } from 'next-sandbox';
2 | import { getAllPosts, seedPosts, throwError } from './action';
3 |
4 | export default withSandbox({
5 | functions: [
6 | {
7 | name: 'Get All Posts',
8 | function: getAllPosts,
9 | },
10 | {
11 | name: 'Seed Posts',
12 | function: seedPosts,
13 | },
14 | {
15 | name: 'Test Throw Error',
16 | function: throwError,
17 | },
18 | ],
19 | });
20 |
--------------------------------------------------------------------------------
/apps/docs/app/docs-og/[...slug]/route.tsx:
--------------------------------------------------------------------------------
1 | import { generateOGImage } from 'fumadocs-ui/og';
2 | import { metadataImage } from '../../../lib/metadata';
3 |
4 | export const GET = metadataImage.createAPI((page) => {
5 | return generateOGImage({
6 | primaryTextColor: 'rgb(127, 187, 224)',
7 | title: page.data.title,
8 | description: page.data.description,
9 | site: 'Next Sandbox',
10 | });
11 | });
12 |
13 | export function generateStaticParams() {
14 | return metadataImage.generateParams();
15 | }
16 |
--------------------------------------------------------------------------------
/packages/next-sandbox/src/icons/play-icon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | const PlayIcon = (props: React.SVGProps) => (
4 |
17 | );
18 |
19 | export default PlayIcon;
20 |
--------------------------------------------------------------------------------
/apps/docs/app/(home)/page.tsx:
--------------------------------------------------------------------------------
1 | import Feature from '@/components/feature';
2 | import Hero from '@/components/hero';
3 | import How from '@/components/how';
4 | import Light from '@/components/illustrations/light';
5 | import Tweets from '@/components/tweets';
6 |
7 | export default function HomePage() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/next-sandbox/src/sandbox-server.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SandboxUI } from './sandbox-ui';
3 |
4 | interface SandboxServerProps {
5 | enable?: boolean;
6 | beforeRender?: () => Promise;
7 | }
8 |
9 | export async function SandboxServer({
10 | enable,
11 | beforeRender,
12 | }: SandboxServerProps) {
13 | if (!enable) {
14 | const { notFound } = await import('next/navigation');
15 | notFound();
16 | }
17 |
18 | if (beforeRender) {
19 | await beforeRender();
20 | }
21 |
22 | return ;
23 | }
24 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/enable.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: enable
3 | description: Enable or disable the sandbox route.
4 | ---
5 |
6 | ## Usage
7 |
8 | `enable` is a boolean prop to enable or disable the sandbox route. If you set it to `false`, it will call `notFound()` from `next/navigation` internally. A common use case is to check if the current environment is production or not.
9 |
10 | ```tsx
11 | import { withSandbox } from 'next-sandbox';
12 |
13 | export default withSandbox({
14 | enable: process.env.NODE_ENV !== 'production', // [!code ++]
15 | // ...
16 | });
17 | ```
18 |
--------------------------------------------------------------------------------
/packages/next-sandbox/src/icons/loader-icon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | const LoaderIcon = (props: React.SVGProps) => (
4 |
17 | );
18 |
19 | export default LoaderIcon;
20 |
--------------------------------------------------------------------------------
/apps/test/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/packages/next-sandbox/src/icons/log-icon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | const LogIcon = (props: React.SVGProps) => (
4 |
18 | );
19 |
20 | export default LogIcon;
21 |
--------------------------------------------------------------------------------
/apps/test/app/sandbox-action.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | interface Post {
4 | id: number;
5 | name: string;
6 | }
7 |
8 | const posts: Post[] = [
9 | {
10 | id: 1,
11 | name: 'Hello World',
12 | },
13 | ];
14 |
15 | const sleep = (time = 3000) =>
16 | new Promise((resolve) => setTimeout(resolve, time));
17 |
18 | export const getAllPosts = async () => {
19 | 'use server';
20 |
21 | return posts;
22 | };
23 |
24 | export const seedPosts = async () => {
25 | await sleep(1000);
26 |
27 | // throw new Error('Failed to connect to the database.');
28 |
29 | return {
30 | success: true,
31 | message: 'Seed 1000 posts to the database.',
32 | };
33 | };
34 |
--------------------------------------------------------------------------------
/apps/docs/README.md:
--------------------------------------------------------------------------------
1 | # docs
2 |
3 | This is a Next.js application generated with
4 | [Create Fumadocs](https://github.com/fuma-nama/fumadocs).
5 |
6 | Run development server:
7 |
8 | ```bash
9 | npm run dev
10 | # or
11 | pnpm dev
12 | # or
13 | yarn dev
14 | ```
15 |
16 | Open http://localhost:3000 with your browser to see the result.
17 |
18 | ## Learn More
19 |
20 | To learn more about Next.js and Fumadocs, take a look at the following
21 | resources:
22 |
23 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
24 | features and API.
25 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
26 | - [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs
27 |
--------------------------------------------------------------------------------
/apps/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/apps/test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "react": "^19.0.0",
13 | "react-dom": "^19.0.0",
14 | "next": "15.2.4"
15 | },
16 | "devDependencies": {
17 | "typescript": "^5",
18 | "@types/node": "^20",
19 | "@types/react": "^19",
20 | "@types/react-dom": "^19",
21 | "postcss": "^8",
22 | "tailwindcss": "^3.4.1",
23 | "eslint": "^9",
24 | "eslint-config-next": "15.1.6",
25 | "@eslint/eslintrc": "^3",
26 | "next-sandbox": "workspace:*"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/apps/docs/app/(home)/sandbox/action.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | interface Post {
4 | id: number;
5 | name: string;
6 | }
7 |
8 | const posts: Post[] = [
9 | {
10 | id: 1,
11 | name: 'Hello World',
12 | },
13 | ];
14 |
15 | const sleep = (time = 3000) =>
16 | new Promise((resolve) => setTimeout(resolve, time));
17 |
18 | export const getAllPosts = async () => {
19 | await sleep(200);
20 |
21 | return posts;
22 | };
23 |
24 | export const seedPosts = async () => {
25 | await sleep(1000);
26 |
27 | return {
28 | success: true,
29 | message: 'Seed 1000 posts to the database.',
30 | };
31 | };
32 |
33 | export const throwError = async () => {
34 | await sleep(1000);
35 |
36 | throw new Error('Failed to connect to the database.');
37 | };
38 |
--------------------------------------------------------------------------------
/packages/next-sandbox/src/with-sandbox.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SandboxProvider } from './sandbox-context';
3 | import { SandboxServer } from './sandbox-server';
4 |
5 | export interface SandboxFunction {
6 | name: string;
7 | function: () => Promise;
8 | }
9 |
10 | export interface SandboxConfig {
11 | functions: SandboxFunction[];
12 | enable?: boolean;
13 | beforeRender?: () => Promise;
14 | }
15 |
16 | export function withSandbox({
17 | functions,
18 | enable = true,
19 | beforeRender,
20 | }: SandboxConfig) {
21 | return function SandboxWrapper() {
22 | return (
23 |
24 |
25 |
26 | );
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/apps/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "ESNext",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "strict": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "bundler",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "paths": {
19 | "@/.source": ["./.source/index.ts"],
20 | "@/*": ["./*"]
21 | },
22 | "plugins": [
23 | {
24 | "name": "next"
25 | }
26 | ]
27 | },
28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
29 | "exclude": ["node_modules"]
30 | }
31 |
--------------------------------------------------------------------------------
/apps/test/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 | import { Geist, Geist_Mono } from 'next/font/google';
3 | import './globals.css';
4 |
5 | const geistSans = Geist({
6 | variable: '--font-geist-sans',
7 | subsets: ['latin'],
8 | });
9 |
10 | const geistMono = Geist_Mono({
11 | variable: '--font-geist-mono',
12 | subsets: ['latin'],
13 | });
14 |
15 | export const metadata: Metadata = {
16 | title: 'Create Next App',
17 | description: 'Generated by create next app',
18 | };
19 |
20 | export default function RootLayout({
21 | children,
22 | }: Readonly<{
23 | children: React.ReactNode;
24 | }>) {
25 | return (
26 |
27 |
30 | {children}
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/apps/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "next build",
7 | "dev": "next dev",
8 | "start": "next start",
9 | "postinstall": "fumadocs-mdx"
10 | },
11 | "dependencies": {
12 | "fumadocs-core": "15.0.15",
13 | "fumadocs-docgen": "^2.0.0",
14 | "fumadocs-mdx": "11.5.6",
15 | "fumadocs-ui": "15.0.15",
16 | "lucide-react": "^0.487.0",
17 | "next": "15.2.4",
18 | "react": "^19.0.0",
19 | "react-dom": "^19.0.0",
20 | "next-sandbox": "workspace:*"
21 | },
22 | "devDependencies": {
23 | "@tailwindcss/postcss": "^4.0.17",
24 | "@types/mdx": "^2.0.13",
25 | "@types/node": "22.13.8",
26 | "@types/react": "^19.0.10",
27 | "@types/react-dom": "^19.0.4",
28 | "eslint": "^9",
29 | "eslint-config-next": "15.2.0",
30 | "postcss": "^8.5.3",
31 | "tailwindcss": "^4.0.9",
32 | "typescript": "^5.8.2"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/apps/docs/app/layout.config.tsx:
--------------------------------------------------------------------------------
1 | import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
2 |
3 | /**
4 | * Shared layout configurations
5 | *
6 | * you can customise layouts individually from:
7 | * Home Layout: app/(home)/layout.tsx
8 | * Docs Layout: app/docs/layout.tsx
9 | */
10 | export const baseOptions: BaseLayoutProps = {
11 | githubUrl: 'https://github.com/1weiho/next-sandbox',
12 | nav: {
13 | title: (
14 | <>
15 | {/* */}
23 | Next Sandbox
24 | >
25 | ),
26 | },
27 | links: [
28 | {
29 | text: 'Documentation',
30 | url: '/docs',
31 | active: 'nested-url',
32 | },
33 | {
34 | text: 'Playground',
35 | url: '/sandbox',
36 | },
37 | ],
38 | };
39 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Get Started
3 | description: Getting started with Next Sandbox.
4 | ---
5 |
6 | Next Sandbox is a lightweight package for testing and monitoring server actions in your Next.js application. It provides a simple UI to execute actions, view logs, and measure execution times.
7 |
8 | ## Features
9 |
10 | - **Execute Actions**: Run server actions directly from the UI.
11 | - **View Logs & Metrics**: Monitor execution status, logs, and performance metrics (AVG, P75, P95).
12 |
13 | ## Installation
14 |
15 | ```package-install
16 | next-sandbox
17 | ```
18 |
19 | ## Usage
20 |
21 | Create a dedicated route for sandbox usage and directly export `withSandbox` in `page.tsx`:
22 |
23 | ```tsx title="app/sandbox/page.tsx"
24 | import { withSandbox } from 'next-sandbox';
25 | import { seedPosts } from './seed-posts';
26 |
27 | export default withSandbox({
28 | functions: [
29 | {
30 | name: 'Seed Posts',
31 | function: seedPosts,
32 | },
33 | ],
34 | });
35 | ```
36 |
--------------------------------------------------------------------------------
/apps/test/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/docs/components/illustrations/arrow-down.tsx:
--------------------------------------------------------------------------------
1 | import type { SVGProps } from 'react';
2 |
3 | const ArrowDown = (props: SVGProps) => (
4 |
26 | );
27 |
28 | export default ArrowDown;
29 |
--------------------------------------------------------------------------------
/apps/docs/components/illustrations/arrow-right.tsx:
--------------------------------------------------------------------------------
1 | import type { SVGProps } from 'react';
2 |
3 | const ArrowRight = (props: SVGProps) => (
4 |
26 | );
27 |
28 | export default ArrowRight;
29 |
--------------------------------------------------------------------------------
/packages/next-sandbox/README.md:
--------------------------------------------------------------------------------
1 | # Next Sandbox
2 |
3 | Next Sandbox is a lightweight package for testing and monitoring server actions in your Next.js application. It provides a simple UI to execute actions, view logs, and measure execution times.
4 |
5 | ## Features
6 |
7 | - **Execute Actions**: Run server actions directly from the UI.
8 | - **View Logs & Metrics**: Monitor execution status, logs, and performance metrics (AVG, P75, P95).
9 |
10 | ## Installation
11 |
12 | ```bash
13 | pnpm i next-sandbox
14 | ```
15 |
16 | ## Usage
17 |
18 | Create a dedicated route for sandbox usage and directly export `withSandbox` in `page.tsx`:
19 |
20 | `app/sandbox/page.tsx`
21 |
22 | ```tsx
23 | import { withSandbox } from 'next-sandbox';
24 | import { seedPosts } from './seed-posts';
25 |
26 | export default withSandbox({
27 | functions: [
28 | {
29 | name: 'Seed Posts',
30 | function: seedPosts,
31 | },
32 | ],
33 | });
34 | ```
35 |
36 | ## Documentation
37 |
38 | Find the full API reference and examples in the [documentation](https://next-sandbox.1wei.dev/docs).
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Yiwei Ho
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/apps/docs/app/(home)/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 | import { HomeLayout } from 'fumadocs-ui/layouts/home';
3 | import { baseOptions } from '@/app/layout.config';
4 |
5 | export default function Layout({ children }: { children: ReactNode }) {
6 | return (
7 |
8 | {children}
9 |
10 |
11 | );
12 | }
13 |
14 | const Footer = () => {
15 | return (
16 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/apps/docs/components/hero.tsx:
--------------------------------------------------------------------------------
1 | import { BookOpen, Github } from 'lucide-react';
2 | import Link from 'next/link';
3 |
4 | const Hero = () => {
5 | return (
6 |
7 |
8 | Next Sandbox
9 |
10 |
11 | A lightweight tool for testing and monitoring
12 |
13 | server actions in Next.js.
14 |
15 |
32 |
33 | );
34 | };
35 |
36 | export default Hero;
37 |
--------------------------------------------------------------------------------
/apps/test/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/next-sandbox/src/highlighted-json.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function syntaxHighlightReact(jsonStr: string) {
4 | if (!jsonStr) return null;
5 |
6 | const regex =
7 | /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g;
8 | let lastIndex = 0;
9 | const tokens = [];
10 | let match;
11 | let key = 0;
12 |
13 | while ((match = regex.exec(jsonStr)) !== null) {
14 | if (match.index > lastIndex) {
15 | tokens.push(jsonStr.slice(lastIndex, match.index));
16 | }
17 | const token = match[0];
18 | let cls = 'number';
19 | if (/^"/.test(token)) {
20 | cls = /:$/.test(token) ? 'key' : 'string';
21 | } else if (/true|false/.test(token)) {
22 | cls = 'boolean';
23 | } else if (/null/.test(token)) {
24 | cls = 'null';
25 | }
26 | tokens.push(
27 |
28 | {token}
29 | ,
30 | );
31 | lastIndex = regex.lastIndex;
32 | }
33 |
34 | if (lastIndex < jsonStr.length) {
35 | tokens.push(jsonStr.slice(lastIndex));
36 | }
37 |
38 | return tokens;
39 | }
40 |
41 | interface HighlightedJSONProps {
42 | jsonStr: string;
43 | }
44 |
45 | export default function HighlightedJSON({ jsonStr }: HighlightedJSONProps) {
46 | const tokens = syntaxHighlightReact(jsonStr);
47 |
48 | return {tokens};
49 | }
50 |
--------------------------------------------------------------------------------
/apps/docs/components/feature.tsx:
--------------------------------------------------------------------------------
1 | import { Activity, Cable, LucideIcon, ShieldUser } from 'lucide-react';
2 |
3 | const Feature = () => {
4 | return (
5 |
6 |
11 |
16 |
21 |
22 | );
23 | };
24 |
25 | const Card = ({
26 | icon: Icon,
27 | title,
28 | description,
29 | }: {
30 | icon: LucideIcon;
31 | title: string;
32 | description: string;
33 | }) => {
34 | return (
35 |
36 |
37 |
38 |
39 |
{title}
40 |
{description}
41 |
42 | );
43 | };
44 |
45 | export default Feature;
46 |
--------------------------------------------------------------------------------
/apps/docs/components/illustrations/light.tsx:
--------------------------------------------------------------------------------
1 | import type { SVGProps } from 'react';
2 |
3 | const Light = (props: SVGProps) => (
4 |
51 | );
52 |
53 | export default Light;
54 |
--------------------------------------------------------------------------------
/packages/next-sandbox/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-sandbox",
3 | "version": "0.1.2",
4 | "description": "Server action sandbox for Next.js.",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "files": [
9 | "dist"
10 | ],
11 | "exports": {
12 | "import": {
13 | "types": "./dist/index.d.mts",
14 | "default": "./dist/index.mjs"
15 | },
16 | "require": {
17 | "types": "./dist/index.d.ts",
18 | "default": "./dist/index.js"
19 | }
20 | },
21 | "scripts": {
22 | "build": "bunchee",
23 | "dev": "bunchee --watch"
24 | },
25 | "keywords": [
26 | "react",
27 | "nextjs",
28 | "sandbox"
29 | ],
30 | "author": "Yiwei Ho",
31 | "license": "MIT",
32 | "homepage": "https://next-sandbox.1wei.dev/",
33 | "repository": {
34 | "type": "git",
35 | "url": "https://github.com/1weiho/next-sandbox.git"
36 | },
37 | "bugs": {
38 | "url": "https://github.com/1weiho/next-sandbox/issues"
39 | },
40 | "devDependencies": {
41 | "@types/react": "^19.0.8",
42 | "@types/react-dom": "^19.0.3",
43 | "bunchee": "^6.3.2",
44 | "next": "^15.2.4",
45 | "react": "^19.0.0",
46 | "react-dom": "^19.0.0",
47 | "typescript": "^5.8.2"
48 | },
49 | "peerDependencies": {
50 | "react": "^16.8 || ^17.0 || ^18.0 || ^19.0",
51 | "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0",
52 | "next": ">=13.0.0"
53 | },
54 | "dependencies": {
55 | "vaul": "^1.1.2"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/apps/docs/app/docs/[[...slug]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { source } from '@/lib/source';
2 | import {
3 | DocsPage,
4 | DocsBody,
5 | DocsDescription,
6 | DocsTitle,
7 | } from 'fumadocs-ui/page';
8 | import { notFound } from 'next/navigation';
9 | import defaultMdxComponents from 'fumadocs-ui/mdx';
10 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
11 | import { metadataImage } from '@/lib/metadata';
12 |
13 | export default async function Page(props: {
14 | params: Promise<{ slug?: string[] }>;
15 | }) {
16 | const params = await props.params;
17 | const page = source.getPage(params.slug);
18 | if (!page) notFound();
19 |
20 | const MDX = page.data.body;
21 |
22 | return (
23 |
28 | {page.data.title}
29 | {page.data.description}
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | export async function generateStaticParams() {
38 | return source.generateParams();
39 | }
40 |
41 | export async function generateMetadata(props: {
42 | params: Promise<{ slug?: string[] }>;
43 | }) {
44 | const params = await props.params;
45 | const page = source.getPage(params.slug);
46 | if (!page) notFound();
47 |
48 | return metadataImage.withImage(page.slugs, {
49 | title: page.data.title,
50 | description: page.data.description,
51 | });
52 | }
53 |
--------------------------------------------------------------------------------
/apps/test/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
37 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/before-render.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: beforeRender
3 | description: A function executes on the server before the page is rendered.
4 | ---
5 |
6 | ## `beforeRender()`
7 |
8 | An async function that runs on the server before rendering the UI, where you can perform authentication, permission checks, etc., is particularly useful when you want to run a sandbox page in a production environment.
9 |
10 | ## Usage
11 |
12 | You can directly pass the server logic to be executed in the `beforeRender` parameter, since `withSandbox()` renders a Server Component. This means your logic will run on the server before the Sandbox UI is rendered.
13 |
14 | {/* prettier-ignore-start */}
15 | ```tsx title="app/sandbox/page.tsx"
16 | import { auth } from '@/lib/auth';
17 | import { withSandbox } from 'next-sandbox';
18 | import { forbidden } from 'next/navigation';
19 |
20 | export default withSandbox({
21 | beforeRender: async () => { // [!code ++]
22 | const user = await auth(); // [!code ++]
23 | if (user.role !== 'admin') { // [!code ++]
24 | forbidden(); // [!code ++]
25 | } // [!code ++]
26 | }, // [!code ++]
27 | // ...
28 | });
29 | ```
30 | {/* prettier-ignore-end */}
31 |
32 | ## Protect server actions
33 |
34 | Due to server actions, Next.js allocates a separate POST endpoint for it at build time, which is unrelated to the `beforeRender` we define, as it only restricts the rendering of the UI. Therefore, the best practice is to also **perform strict authentication within server actions**, which ensures that even if someone directly accesses the server actions endpoint, they cannot execute it successfully.
35 |
--------------------------------------------------------------------------------
/apps/docs/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import './global.css';
2 | import { RootProvider } from 'fumadocs-ui/provider';
3 | import type { Metadata } from 'next';
4 | import { Geist } from 'next/font/google';
5 | import type { ReactNode } from 'react';
6 |
7 | const geist = Geist({
8 | subsets: ['latin'],
9 | });
10 |
11 | const baseUrl =
12 | process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'
13 | ? new URL('https://next-sandbox.1wei.dev')
14 | : process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
15 | ? new URL(`https://${process.env.NEXT_PUBLIC_VERCEL_URL}`)
16 | : new URL('http://localhost:3000');
17 |
18 | export const metadata: Metadata = {
19 | title: {
20 | template: '%s | Next Sandbox',
21 | default: 'Next Sandbox',
22 | },
23 | description:
24 | 'A lightweight tool for testing and monitoring server actions in Next.js.',
25 | openGraph: {
26 | title: 'Next Sandbox',
27 | description:
28 | 'A lightweight tool for testing and monitoring server actions in Next.js.',
29 | url: 'https://next-sandbox.1wei.dev',
30 | siteName: 'Next Sandbox',
31 | images: '/banner.png',
32 | },
33 | twitter: {
34 | card: 'summary_large_image',
35 | site: '@1weiho',
36 | creator: '@1weiho',
37 | images: '/banner.png',
38 | },
39 | metadataBase: baseUrl,
40 | };
41 |
42 | export default function Layout({ children }: { children: ReactNode }) {
43 | return (
44 |
45 |
46 | {children}
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/apps/docs/components/how.tsx:
--------------------------------------------------------------------------------
1 | import DemoScreenshotDark from '@/static/demo-screenshot-dark.png';
2 | import DemoScreenshotLight from '@/static/demo-screenshot-light.png';
3 | import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
4 | import Image from 'next/image';
5 | import ArrowDown from './illustrations/arrow-down';
6 | import ArrowRight from './illustrations/arrow-right';
7 |
8 | const exampleCode = `import { withSandbox } from 'next-sandbox';
9 | import { getAllPosts, seedPosts } from './sandbox-function';
10 |
11 | export default withSandbox({
12 | functions: [
13 | {
14 | name: 'Get All Posts',
15 | function: getAllPosts,
16 | },
17 | {
18 | name: 'Seed Posts',
19 | function: seedPosts,
20 | },
21 | ],
22 | });`;
23 |
24 | const How = () => {
25 | return (
26 |
27 |
How to use
28 |
29 |
34 |
35 |
41 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default How;
54 |
--------------------------------------------------------------------------------
/apps/docs/components/tweets.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | const Tweets = () => {
4 | return (
5 |
6 |
Loved by Developers
7 |
8 |
9 |
16 |
23 |
30 |
37 |
38 |
39 | );
40 | };
41 |
42 | const Tweet = ({
43 | username,
44 | name,
45 | avatarUrl,
46 | content,
47 | url,
48 | }: {
49 | username: string;
50 | name: string;
51 | avatarUrl: string;
52 | content: string;
53 | url: string;
54 | }) => {
55 | return (
56 |
61 |
62 |
69 |
70 |
{name}
71 |
72 | @{username}
73 |
74 |
75 |
76 | {content}
77 |
78 | );
79 | };
80 |
81 | export default Tweets;
82 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/functions.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: functions
3 | description: The array of server actions.
4 | ---
5 |
6 | ## Server Actions
7 |
8 | Server Actions are special async functions that run on the server. They must include `'use server'` at the top of their definition so Next.js can recognize and execute them correctly.
9 |
10 |
11 |
15 | Learn more about server actions in Next.js
16 |
17 |
18 |
19 | ## Defining Server Actions
20 |
21 | Server actions can be defined in two ways:
22 |
23 | ### Within the Same File
24 |
25 | Define server actions directly above withSandbox, ensuring the 'use server' directive is placed at the top of each action:
26 |
27 | ```tsx title="app/sandbox/page.tsx"
28 | import { withSandbox } from 'next-sandbox';
29 |
30 | const seedUsers = async () => {
31 | 'use server'; // [!code highlight]
32 |
33 | // Some db calls
34 | };
35 |
36 | export default withSandbox({
37 | functions: [
38 | {
39 | name: 'Seed Users',
40 | function: seedUsers,
41 | },
42 | ],
43 | });
44 | ```
45 |
46 | ### Importing from Another File
47 |
48 | You can define server actions in separate files and import them where needed. Ensure the 'use server' directive is included at the top of the external file:
49 |
50 | ```ts title="actions/users.ts"
51 | 'use server'; // [!code highlight]
52 |
53 | export const seedUsers = async () => {
54 | // Some db calls
55 | };
56 | ```
57 |
58 | Then import the action:
59 |
60 | ```tsx title="app/sandbox/page.tsx"
61 | import { withSandbox } from 'next-sandbox';
62 | import { seedUsers } from '@/actions/users'; // [!code highlight]
63 |
64 | export default withSandbox({
65 | functions: [
66 | {
67 | name: 'Seed Users',
68 | function: seedUsers,
69 | },
70 | ],
71 | });
72 | ```
73 |
74 | ## Avoid Inline Server Actions
75 |
76 |
77 | Avoid defining server actions inline within functions, as this pattern is not
78 | supported and may cause unexpected behavior.
79 |
80 |
81 | Incorrect usage example:
82 |
83 | ```tsx title="app/sandbox/page.tsx"
84 | import { withSandbox } from 'next-sandbox';
85 |
86 | export default withSandbox({
87 | functions: [
88 | {
89 | name: 'Seed Users',
90 | function: async () => {
91 | // [!code --]
92 | 'use server'; // [!code --]
93 | // [!code --]
94 | // Some db calls // [!code --]
95 | }, // [!code --]
96 | },
97 | ],
98 | });
99 | ```
100 |
--------------------------------------------------------------------------------
/packages/next-sandbox/src/sandbox-context.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { createContext, useContext, useState } from 'react';
4 | import type { SandboxFunction } from './with-sandbox';
5 |
6 | interface ExecutionRecord {
7 | output: string;
8 | status: 'success' | 'error';
9 | duration: number;
10 | timestamp: string;
11 | }
12 |
13 | interface SandboxContextType {
14 | functions: SandboxFunction[];
15 | executionRecords: Record;
16 | executeFunction: (name: string) => Promise;
17 | executing: Record;
18 | }
19 |
20 | const SandboxContext = createContext(undefined);
21 |
22 | export function SandboxProvider({
23 | children,
24 | functions,
25 | }: {
26 | children: React.ReactNode;
27 | functions: SandboxFunction[];
28 | }) {
29 | const [executionRecords, setExecutionRecords] = useState<
30 | Record
31 | >({});
32 | const [executing, setExecuting] = useState>({});
33 |
34 | const executeFunction = async (name: string) => {
35 | const func = functions.find((f) => f.name === name);
36 | if (!func) return;
37 |
38 | setExecuting((prev) => ({ ...prev, [name]: true }));
39 | const startTime = Date.now();
40 |
41 | try {
42 | const result = await func.function();
43 | const endTime = Date.now();
44 |
45 | const record: ExecutionRecord = {
46 | output: JSON.stringify(result, null, 2),
47 | status: 'success',
48 | duration: endTime - startTime,
49 | timestamp: new Date().toISOString(),
50 | };
51 |
52 | setExecutionRecords((prev) => ({
53 | ...prev,
54 | [name]: [...(prev[name] || []), record],
55 | }));
56 | } catch (error) {
57 | const endTime = Date.now();
58 |
59 | const record: ExecutionRecord = {
60 | output: String(error),
61 | status: 'error',
62 | duration: endTime - startTime,
63 | timestamp: new Date().toISOString(),
64 | };
65 |
66 | setExecutionRecords((prev) => ({
67 | ...prev,
68 | [name]: [...(prev[name] || []), record],
69 | }));
70 | } finally {
71 | setExecuting((prev) => ({ ...prev, [name]: false }));
72 | }
73 | };
74 |
75 | return (
76 |
84 | {children}
85 |
86 | );
87 | }
88 |
89 | export function useSandbox() {
90 | const context = useContext(SandboxContext);
91 | if (context === undefined) {
92 | throw new Error('useSandbox must be used within a SandboxProvider');
93 | }
94 | return context;
95 | }
96 |
--------------------------------------------------------------------------------
/packages/next-sandbox/src/log-drawer.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Drawer } from 'vaul';
4 | import React from 'react';
5 | import { useSandbox } from './sandbox-context';
6 | import LoaderIcon from './icons/loader-icon';
7 | import PlayIcon from './icons/play-icon';
8 | import HighlightedJSON from './highlighted-json';
9 |
10 | interface LogDrawerProps {
11 | open: boolean;
12 | onOpenChange: (open: boolean) => void;
13 | functionName: string | null;
14 | }
15 |
16 | export default function LogDrawer({
17 | open,
18 | onOpenChange,
19 | functionName,
20 | }: LogDrawerProps) {
21 | const { executionRecords, executeFunction, executing } = useSandbox();
22 | const isExecuting = executing[functionName] || false;
23 |
24 | const currentRecords = functionName
25 | ? executionRecords[functionName] || []
26 | : [];
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {functionName ?? 'No Function Selected'}
38 |
39 |
52 |
53 |
54 |
55 | {currentRecords.length === 0 ? (
56 |
No logs
57 | ) : (
58 | [...currentRecords].reverse().map((record, idx) => (
59 |
60 | {record.status === 'success' ? (
61 |
62 | ) : (
63 |
64 | {record.output}
65 |
66 | )}
67 |
68 |
69 | {new Date(record.timestamp).toLocaleTimeString() ??
70 | 'Just'}
71 |
72 |
73 |
74 | Duration: {record.duration}ms
75 |
76 |
81 | {record.status}
82 |
83 |
84 |
85 |
86 | ))
87 | )}
88 |
89 |
90 |
91 |
92 |
93 |
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/packages/next-sandbox/src/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color-white: #ffffff;
3 | --color-gray-50: #f9fafb;
4 | --color-gray-100: #f3f4f6;
5 | --color-gray-300: #e5e7eb;
6 | --color-gray-600: #6b7280;
7 | --color-gray-900: #111827;
8 | --color-success: #10b981;
9 | --color-danger: #f87171;
10 | --color-blue: #378dd8;
11 |
12 | --radius-lg: 1rem;
13 | --radius-md: 0.5rem;
14 | --padding-md: 1rem;
15 | --padding-lg: 1.5rem;
16 | --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
17 | --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
18 |
19 | --bg-body: var(--color-white);
20 | --bg-card: var(--color-white);
21 | --bg-panel: var(--color-gray-50);
22 | --bg-light: var(--color-gray-100);
23 | --border-color: var(--color-gray-300);
24 | --text-color: var(--color-gray-900);
25 | --text-muted: var(--color-gray-600);
26 | }
27 |
28 | @media (prefers-color-scheme: dark) {
29 | :root {
30 | --bg-body: #1f2937;
31 | --bg-card: #1f2937;
32 | --bg-panel: #374151;
33 | --bg-light: #4b5563;
34 | --border-color: #4b5563;
35 | --text-color: #f9fafb;
36 | --text-muted: #9ca3af;
37 |
38 | --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.5);
39 | --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.5);
40 | }
41 | }
42 |
43 | /* Main UI */
44 | .sandbox-container {
45 | max-width: 960px;
46 | margin: 0 auto;
47 | padding: var(--padding-lg);
48 | }
49 |
50 | .sandbox-title {
51 | font-size: 1.2rem;
52 | font-weight: 600;
53 | margin-bottom: 1.5rem;
54 | }
55 |
56 | .functions-wrapper {
57 | background-color: var(--bg-panel);
58 | padding: var(--padding-lg);
59 | border-radius: var(--radius-lg);
60 | box-shadow: var(--shadow-sm);
61 | }
62 |
63 | .functions-header {
64 | display: flex;
65 | align-items: center;
66 | margin-bottom: 1.5rem;
67 | }
68 |
69 | .functions-label {
70 | font-size: 1rem;
71 | font-weight: 500;
72 | color: var(--text-muted);
73 | margin-right: 0.5rem;
74 | }
75 |
76 | .functions-count {
77 | font-size: 0.875rem;
78 | background-color: var(--bg-light);
79 | padding: 0.25rem 0.5rem;
80 | border-radius: var(--radius-md);
81 | color: var(--text-color);
82 | }
83 |
84 | .function-card {
85 | display: flex;
86 | justify-content: space-between;
87 | align-items: center;
88 | background-color: var(--bg-card);
89 | border-radius: var(--radius-md);
90 | box-shadow: var(--shadow-sm);
91 | padding: var(--padding-md);
92 | margin-bottom: 1rem;
93 | }
94 |
95 | .function-left {
96 | display: flex;
97 | align-items: center;
98 | gap: 1rem;
99 | }
100 |
101 | .function-name {
102 | font-size: 1rem;
103 | font-weight: 500;
104 | margin: 0;
105 | }
106 |
107 | .function-right {
108 | display: flex;
109 | align-items: center;
110 | gap: 1rem;
111 | }
112 |
113 | .function-metrics {
114 | display: grid;
115 | grid-template-columns: repeat(4, 50px);
116 | gap: 1.5rem;
117 | }
118 |
119 | .metric {
120 | display: flex;
121 | flex-direction: column;
122 | align-items: flex-start;
123 | }
124 |
125 | .metric-label {
126 | font-size: 0.75rem;
127 | color: var(--text-muted);
128 | }
129 |
130 | .metric-content {
131 | display: flex;
132 | align-items: center;
133 | gap: 2px;
134 | }
135 |
136 | .metic-indicator {
137 | border-radius: 999px;
138 | height: 4px;
139 | width: 4px;
140 | background-color: var(--indicator-color);
141 | }
142 |
143 | .metric-value {
144 | font-size: 0.875rem;
145 | font-weight: 500;
146 | }
147 |
148 | .function-actions {
149 | display: flex;
150 | gap: 0.5rem;
151 | }
152 |
153 | .icon-button {
154 | border: none;
155 | background-color: var(--bg-light);
156 | cursor: pointer;
157 | padding: 0.5rem;
158 | border-radius: var(--radius-md);
159 | transition: filter 0.2s ease;
160 | }
161 |
162 | .icon-button:hover {
163 | filter: brightness(90%);
164 | }
165 |
166 | .execute-button[data-executing='true']:hover {
167 | cursor: not-allowed;
168 | }
169 |
170 | .icon {
171 | width: 1rem;
172 | height: 1rem;
173 | color: var(--text-muted);
174 | }
175 |
176 | .spinner {
177 | animation: rotate 1s linear infinite;
178 | }
179 |
180 | @keyframes rotate {
181 | 100% {
182 | transform: rotate(360deg);
183 | }
184 | }
185 |
186 | /* Drawer */
187 | .drawer-overlay {
188 | position: fixed;
189 | top: 0;
190 | right: 0;
191 | bottom: 0;
192 | left: 0;
193 | background-color: rgba(0, 0, 0, 0.4);
194 | z-index: 9999;
195 | }
196 |
197 | .drawer-content {
198 | position: fixed;
199 | top: 8px;
200 | right: 8px;
201 | bottom: 8px;
202 | z-index: 10000;
203 | outline: none;
204 | width: 600px;
205 | display: flex;
206 | transform: var(--initial-transform, calc(100% + 8px));
207 | transition: transform 0.3s ease;
208 | }
209 |
210 | .drawer-inner {
211 | background-color: var(--bg-card);
212 | height: 100%;
213 | width: 100%;
214 | flex-grow: 1;
215 | padding: 20px;
216 | display: flex;
217 | flex-direction: column;
218 | border-radius: 16px;
219 | }
220 |
221 | .drawer-inner-container {
222 | max-width: 600px;
223 | margin: 0;
224 | height: 100%;
225 | display: flex;
226 | flex-direction: column;
227 | }
228 |
229 | .drawer-head {
230 | margin-top: 32px;
231 | margin-bottom: 32px;
232 | padding: 0 4px;
233 | display: flex;
234 | align-items: center;
235 | justify-content: space-between;
236 | }
237 |
238 | .drawer-title {
239 | font-weight: 700;
240 | color: var(--text-color);
241 | font-size: 1.1rem;
242 | }
243 |
244 | .no-logs {
245 | color: var(--text-muted);
246 | }
247 |
248 | .logs-container {
249 | overflow-y: auto;
250 | display: flex;
251 | flex-direction: column;
252 | gap: 16px;
253 | }
254 |
255 | .log-item {
256 | background: var(--bg-card);
257 | border: 1px solid var(--border-color);
258 | border-radius: 8px;
259 | padding: 16px;
260 | display: flex;
261 | flex-direction: column;
262 | gap: 12px;
263 | }
264 |
265 | .log-content {
266 | background: var(--bg-light);
267 | padding: 12px;
268 | border-radius: 8px;
269 | white-space: pre-wrap;
270 | margin: 0;
271 | font-size: 0.9rem;
272 | overflow-x: auto;
273 | }
274 |
275 | .log-content .string {
276 | color: var(--color-success);
277 | }
278 | .log-content .number {
279 | color: darkorange;
280 | }
281 | .log-content .boolean {
282 | color: var(--color-blue);
283 | }
284 | .log-content .null {
285 | color: rgb(195, 120, 215);
286 | }
287 | .log-content .key {
288 | color: var(--color-danger);
289 | }
290 |
291 | .log-error {
292 | color: var(--color-danger);
293 | }
294 |
295 | .log-meta {
296 | display: flex;
297 | align-items: center;
298 | justify-content: space-between;
299 | flex-wrap: wrap;
300 | }
301 |
302 | .log-time {
303 | color: var(--text-muted);
304 | font-size: 0.8rem;
305 | }
306 |
307 | .log-meta-end {
308 | display: flex;
309 | align-items: center;
310 | gap: 8px;
311 | }
312 |
313 | .log-duration {
314 | color: var(--text-muted);
315 | font-size: 0.8rem;
316 | }
317 |
318 | .log-status {
319 | padding: 3px 8px;
320 | border-radius: 10px;
321 | color: var(--color-white);
322 | font-weight: 500;
323 | font-size: 0.8rem;
324 | }
325 |
326 | .log-status.success {
327 | background-color: var(--color-success);
328 | }
329 |
330 | .log-status.fail {
331 | background-color: var(--color-danger);
332 | }
333 |
--------------------------------------------------------------------------------
/packages/next-sandbox/src/sandbox-ui.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { useSandbox } from './sandbox-context';
5 | import './style.css';
6 | import PlayIcon from './icons/play-icon';
7 | import LogIcon from './icons/log-icon';
8 | import LogDrawer from './log-drawer';
9 | import LoaderIcon from './icons/loader-icon';
10 |
11 | function calculateMetrics(durations: number[]) {
12 | const sorted = [...durations].sort((a, b) => a - b);
13 | const sum = durations.reduce((acc, cur) => acc + cur, 0);
14 | const avg = sum / durations.length;
15 |
16 | const p75Index = Math.ceil(0.75 * sorted.length) - 1;
17 | const p95Index = Math.ceil(0.95 * sorted.length) - 1;
18 | const p75 = sorted[p75Index];
19 | const p95 = sorted[p95Index];
20 |
21 | return { avg, p75, p95 };
22 | }
23 |
24 | export function SandboxUI() {
25 | const { functions, executionRecords, executeFunction, executing } =
26 | useSandbox();
27 |
28 | const [openLogDrawer, setOpenLogDrawer] = React.useState(false);
29 | const [selectedFunctionName, setSelectedFunctionName] = React.useState<
30 | string | null
31 | >(null);
32 |
33 | const handleViewLogs = (funcName: string) => {
34 | setSelectedFunctionName(funcName);
35 | setOpenLogDrawer(true);
36 | };
37 |
38 | return (
39 | <>
40 |
45 |
46 |
47 |
Next Sandbox
48 |
49 |
50 | Function
51 | {functions.length}
52 |
53 |
54 | {functions.map((func, index) => {
55 | const records = executionRecords[func.name] || [];
56 | const durations = records.map((r) => r.duration);
57 | const metrics =
58 | durations.length > 0 ? calculateMetrics(durations) : null;
59 | const lastRecord = records[records.length - 1];
60 | const status = lastRecord ? lastRecord.status : 'Not executed';
61 | const latest =
62 | durations.length > 0 ? durations[durations.length - 1] : null;
63 | const isExecuting = executing[func.name] || false;
64 |
65 | return (
66 |
67 |
68 |
{func.name}
69 | {status !== 'Not executed' && (
70 |
75 | {status}
76 |
77 | )}
78 |
79 |
80 |
81 |
82 |
AVG
83 |
84 |
92 |
93 | {metrics ? `${Math.round(metrics.avg)}ms` : 'N/A'}
94 |
95 |
96 |
97 |
98 |
P75
99 |
100 |
108 |
109 | {metrics ? `${metrics.p75}ms` : 'N/A'}
110 |
111 |
112 |
113 |
114 |
P95
115 |
116 |
124 |
125 | {metrics ? `${metrics.p95}ms` : 'N/A'}
126 |
127 |
128 |
129 |
130 |
Latest
131 |
132 |
140 |
141 | {latest !== null ? `${latest}ms` : 'N/A'}
142 |
143 |
144 |
145 |
146 |
147 |
154 |
167 |
168 |
169 |
170 | );
171 | })}
172 |
173 |
174 | >
175 | );
176 | }
177 |
--------------------------------------------------------------------------------