rwsdk 0.0.79-test.20250506144558
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +7 -0
- package/bin/rw-scripts.mjs +31 -0
- package/dist/lib/$.d.mts +8 -0
- package/dist/lib/$.mjs +5 -0
- package/dist/lib/compileTsModule.d.mts +1 -0
- package/dist/lib/compileTsModule.mjs +27 -0
- package/dist/lib/constants.d.mts +7 -0
- package/dist/lib/constants.mjs +9 -0
- package/dist/lib/findWranglerConfig.d.mts +1 -0
- package/dist/lib/findWranglerConfig.mjs +12 -0
- package/dist/lib/getShortName.d.mts +1 -0
- package/dist/lib/getShortName.mjs +2 -0
- package/dist/lib/setupEnvFiles.d.mts +4 -0
- package/dist/lib/setupEnvFiles.mjs +31 -0
- package/dist/llms/index.d.ts +3 -0
- package/dist/llms/index.js +35 -0
- package/dist/llms/rules/interruptors.d.ts +1 -0
- package/dist/llms/rules/interruptors.js +243 -0
- package/dist/llms/rules/middleware.d.ts +1 -0
- package/dist/llms/rules/middleware.js +71 -0
- package/dist/llms/rules/react.d.ts +1 -0
- package/dist/llms/rules/react.js +106 -0
- package/dist/llms/rules/request-response.d.ts +1 -0
- package/dist/llms/rules/request-response.js +209 -0
- package/dist/runtime/client.d.ts +15 -0
- package/dist/runtime/client.js +56 -0
- package/dist/runtime/constants.d.ts +1 -0
- package/dist/runtime/constants.js +1 -0
- package/dist/runtime/entries/auth.d.ts +1 -0
- package/dist/runtime/entries/auth.js +1 -0
- package/dist/runtime/entries/client.d.ts +3 -0
- package/dist/runtime/entries/client.js +3 -0
- package/dist/runtime/entries/router.d.ts +2 -0
- package/dist/runtime/entries/router.js +2 -0
- package/dist/runtime/entries/worker.d.ts +11 -0
- package/dist/runtime/entries/worker.js +11 -0
- package/dist/runtime/error.d.ts +6 -0
- package/dist/runtime/error.js +8 -0
- package/dist/runtime/imports/client.d.ts +4 -0
- package/dist/runtime/imports/client.js +20 -0
- package/dist/runtime/imports/worker.d.ts +5 -0
- package/dist/runtime/imports/worker.js +16 -0
- package/dist/runtime/lib/auth/index.d.ts +1 -0
- package/dist/runtime/lib/auth/index.js +1 -0
- package/dist/runtime/lib/auth/session.d.ts +50 -0
- package/dist/runtime/lib/auth/session.js +148 -0
- package/dist/runtime/lib/links.d.ts +14 -0
- package/dist/runtime/lib/links.js +38 -0
- package/dist/runtime/lib/realtime/client.d.ts +7 -0
- package/dist/runtime/lib/realtime/client.js +154 -0
- package/dist/runtime/lib/realtime/constants.d.ts +1 -0
- package/dist/runtime/lib/realtime/constants.js +1 -0
- package/dist/runtime/lib/realtime/durableObject.d.ts +29 -0
- package/dist/runtime/lib/realtime/durableObject.js +185 -0
- package/dist/runtime/lib/realtime/renderRealtimeClients.d.ts +7 -0
- package/dist/runtime/lib/realtime/renderRealtimeClients.js +6 -0
- package/dist/runtime/lib/realtime/shared.d.ts +10 -0
- package/dist/runtime/lib/realtime/shared.js +10 -0
- package/dist/runtime/lib/realtime/validateUpgradeRequest.d.ts +6 -0
- package/dist/runtime/lib/realtime/validateUpgradeRequest.js +29 -0
- package/dist/runtime/lib/realtime/worker.d.ts +3 -0
- package/dist/runtime/lib/realtime/worker.js +18 -0
- package/dist/runtime/lib/router.d.ts +34 -0
- package/dist/runtime/lib/router.js +115 -0
- package/dist/runtime/lib/streams/consumeEventStream.d.ts +4 -0
- package/dist/runtime/lib/streams/consumeEventStream.js +13 -0
- package/dist/runtime/lib/turnstile/TurnstileScript.d.ts +1 -0
- package/dist/runtime/lib/turnstile/TurnstileScript.js +2 -0
- package/dist/runtime/lib/turnstile/turnstile.d.ts +3 -0
- package/dist/runtime/lib/turnstile/turnstile.js +3 -0
- package/dist/runtime/lib/turnstile/useTurnstile.d.ts +4 -0
- package/dist/runtime/lib/turnstile/useTurnstile.js +23 -0
- package/dist/runtime/lib/turnstile/verifyTurnstileToken.d.ts +4 -0
- package/dist/runtime/lib/turnstile/verifyTurnstileToken.js +15 -0
- package/dist/runtime/lib/utils.d.ts +1 -0
- package/dist/runtime/lib/utils.js +1 -0
- package/dist/runtime/register/client.d.ts +1 -0
- package/dist/runtime/register/client.js +5 -0
- package/dist/runtime/register/worker.d.ts +3 -0
- package/dist/runtime/register/worker.js +34 -0
- package/dist/runtime/render/createClientManifest.d.ts +1 -0
- package/dist/runtime/render/createClientManifest.js +7 -0
- package/dist/runtime/render/createModuleMap.d.ts +1 -0
- package/dist/runtime/render/createModuleMap.js +13 -0
- package/dist/runtime/render/injectRSCPayload.d.ts +3 -0
- package/dist/runtime/render/injectRSCPayload.js +79 -0
- package/dist/runtime/render/renderToRscStream.d.ts +5 -0
- package/dist/runtime/render/renderToRscStream.js +46 -0
- package/dist/runtime/render/transformRscToHtmlStream.d.ts +7 -0
- package/dist/runtime/render/transformRscToHtmlStream.js +18 -0
- package/dist/runtime/requestInfo/types.d.ts +11 -0
- package/dist/runtime/requestInfo/types.js +1 -0
- package/dist/runtime/requestInfo/worker.d.ts +5 -0
- package/dist/runtime/requestInfo/worker.js +33 -0
- package/dist/runtime/script.d.ts +5 -0
- package/dist/runtime/script.js +8 -0
- package/dist/runtime/worker.d.ts +13 -0
- package/dist/runtime/worker.js +150 -0
- package/dist/scripts/__sdk.d.mts +1 -0
- package/dist/scripts/__sdk.mjs +14 -0
- package/dist/scripts/build-vendor-bundles.d.mts +1 -0
- package/dist/scripts/build-vendor-bundles.mjs +92 -0
- package/dist/scripts/check-client-assets.d.mts +1 -0
- package/dist/scripts/check-client-assets.mjs +153 -0
- package/dist/scripts/debug-sync.d.mts +1 -0
- package/dist/scripts/debug-sync.mjs +43 -0
- package/dist/scripts/dev-init.d.mts +1 -0
- package/dist/scripts/dev-init.mjs +26 -0
- package/dist/scripts/ensure-deploy-env.d.mts +1 -0
- package/dist/scripts/ensure-deploy-env.mjs +219 -0
- package/dist/scripts/ensure-env.d.mts +1 -0
- package/dist/scripts/ensure-env.mjs +9 -0
- package/dist/scripts/migrate-new.d.mts +1 -0
- package/dist/scripts/migrate-new.mjs +51 -0
- package/dist/scripts/worker-run.d.mts +1 -0
- package/dist/scripts/worker-run.mjs +81 -0
- package/dist/vite/aliasByEnvPlugin.d.mts +2 -0
- package/dist/vite/aliasByEnvPlugin.mjs +11 -0
- package/dist/vite/asyncSetupPlugin.d.mts +6 -0
- package/dist/vite/asyncSetupPlugin.mjs +23 -0
- package/dist/vite/checkIsUsingPrisma.d.mts +3 -0
- package/dist/vite/checkIsUsingPrisma.mjs +9 -0
- package/dist/vite/configPlugin.d.mts +9 -0
- package/dist/vite/configPlugin.mjs +122 -0
- package/dist/vite/copyPrismaWasmPlugin.d.mts +4 -0
- package/dist/vite/copyPrismaWasmPlugin.mjs +32 -0
- package/dist/vite/index.d.mts +1 -0
- package/dist/vite/index.mjs +1 -0
- package/dist/vite/injectVitePreamblePlugin.d.mts +5 -0
- package/dist/vite/injectVitePreamblePlugin.mjs +20 -0
- package/dist/vite/invalidateCacheIfPrismaClientChanged.d.mts +3 -0
- package/dist/vite/invalidateCacheIfPrismaClientChanged.mjs +27 -0
- package/dist/vite/miniflarePlugin.d.mts +9 -0
- package/dist/vite/miniflarePlugin.mjs +135 -0
- package/dist/vite/moveStaticAssetsPlugin.d.mts +4 -0
- package/dist/vite/moveStaticAssetsPlugin.mjs +13 -0
- package/dist/vite/reactConditionsResolverPlugin.d.mts +6 -0
- package/dist/vite/reactConditionsResolverPlugin.mjs +176 -0
- package/dist/vite/redwoodPlugin.d.mts +12 -0
- package/dist/vite/redwoodPlugin.mjs +81 -0
- package/dist/vite/requestUtils.d.mts +6 -0
- package/dist/vite/requestUtils.mjs +35 -0
- package/dist/vite/transformClientEntryPlugin.d.mts +5 -0
- package/dist/vite/transformClientEntryPlugin.mjs +64 -0
- package/dist/vite/transformJsxScriptTagsPlugin.d.mts +8 -0
- package/dist/vite/transformJsxScriptTagsPlugin.mjs +304 -0
- package/dist/vite/transformJsxScriptTagsPlugin.test.d.mts +1 -0
- package/dist/vite/transformJsxScriptTagsPlugin.test.mjs +333 -0
- package/dist/vite/useClientLookupPlugin.d.mts +9 -0
- package/dist/vite/useClientLookupPlugin.mjs +33 -0
- package/dist/vite/useClientPlugin.d.mts +8 -0
- package/dist/vite/useClientPlugin.mjs +295 -0
- package/dist/vite/useClientPlugin.test.d.mts +1 -0
- package/dist/vite/useClientPlugin.test.mjs +1204 -0
- package/dist/vite/useServerPlugin.d.mts +2 -0
- package/dist/vite/useServerPlugin.mjs +47 -0
- package/dist/vite/virtualPlugin.d.mts +2 -0
- package/dist/vite/virtualPlugin.mjs +18 -0
- package/dist/vite/vitePreamblePlugin.d.mts +1 -0
- package/dist/vite/vitePreamblePlugin.mjs +11 -0
- package/package.json +144 -0
- package/vendor/dist/react.development.js +1707 -0
- package/vendor/dist/react.development.js.map +1 -0
- package/vendor/dist/react.production.js +804 -0
- package/vendor/dist/react.production.js.map +1 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 RedwoodJS Inc
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# RedwoodSDK
|
|
2
|
+
|
|
3
|
+
RedwoodSDK is a composable framework for building server-driven webapps on Cloudflare. It starts as a **Vite plugin** that unlocks **SSR**, **React Server Components**, **Server Functions**, and **realtime**. Its standards-based router—with middleware and interruptors—gives you precise control over every request and response. With built-in access to Cloudflare’s Workers, Database (D1), Storage (R2), Queues, and AI—and full local emulation via Miniflare—development feels just like production.
|
|
4
|
+
|
|
5
|
+
→ [Quick start](https://docs.rwsdk.com/getting-started/quick-start/)
|
|
6
|
+
→ [Docs](https://docs.rwsdk.com/)
|
|
7
|
+
→ [Discord](https://discord.gg/redwoodjs)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { $ as $base } from "execa";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
const ROOT_DIR = path.resolve(__dirname, "..");
|
|
10
|
+
const BIN_DIR = path.resolve(ROOT_DIR, "node_modules", ".bin");
|
|
11
|
+
|
|
12
|
+
const ARGS = process.argv.slice(2);
|
|
13
|
+
const SCRIPT_NAME = ARGS[0];
|
|
14
|
+
|
|
15
|
+
const $ = $base({
|
|
16
|
+
shell: true,
|
|
17
|
+
stdio: "inherit",
|
|
18
|
+
reject: false,
|
|
19
|
+
env: {
|
|
20
|
+
PATH: `${process.env.PATH}:${BIN_DIR}`,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const main = async () => {
|
|
25
|
+
const result =
|
|
26
|
+
await $`node ${path.resolve(ROOT_DIR, "dist", "scripts", SCRIPT_NAME)}.mjs ${ARGS.slice(1).join(" ")}`;
|
|
27
|
+
|
|
28
|
+
process.exitCode = result.exitCode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
main();
|
package/dist/lib/$.d.mts
ADDED
package/dist/lib/$.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const compileTsModule: (tsCode: string) => string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export const compileTsModule = (tsCode) => {
|
|
4
|
+
const tsConfigPath = "./tsconfig.json";
|
|
5
|
+
// Find the nearest tsconfig.json
|
|
6
|
+
const configPath = ts.findConfigFile(path.dirname(tsConfigPath), ts.sys.fileExists, path.basename(tsConfigPath));
|
|
7
|
+
if (!configPath) {
|
|
8
|
+
throw new Error(`Could not find a valid tsconfig.json at path: ${tsConfigPath}`);
|
|
9
|
+
}
|
|
10
|
+
// Read and parse tsconfig.json
|
|
11
|
+
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
12
|
+
if (configFile.error) {
|
|
13
|
+
throw new Error(`Error reading tsconfig.json: ${ts.formatDiagnostic(configFile.error, ts.createCompilerHost({}))}`);
|
|
14
|
+
}
|
|
15
|
+
const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(configPath));
|
|
16
|
+
const compilerOptions = parsedConfig.options;
|
|
17
|
+
// Transpile the TypeScript code using the compiler options
|
|
18
|
+
const output = ts.transpileModule(tsCode, {
|
|
19
|
+
compilerOptions,
|
|
20
|
+
reportDiagnostics: true,
|
|
21
|
+
});
|
|
22
|
+
if (output.diagnostics && output.diagnostics.length) {
|
|
23
|
+
const diagnosticMessages = output.diagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
|
|
24
|
+
throw new Error(`TypeScript Compilation Errors:\n${diagnosticMessages.join("\n")}`);
|
|
25
|
+
}
|
|
26
|
+
return output.outputText; // Compiled JavaScript code
|
|
27
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const ROOT_DIR: string;
|
|
2
|
+
export declare const SRC_DIR: string;
|
|
3
|
+
export declare const DIST_DIR: string;
|
|
4
|
+
export declare const VENDOR_ROOT_DIR: string;
|
|
5
|
+
export declare const VENDOR_SRC_DIR: string;
|
|
6
|
+
export declare const VENDOR_DIST_DIR: string;
|
|
7
|
+
export declare const VENDOR_REACT_SSR_PATH: string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
const __dirname = new URL(".", import.meta.url).pathname;
|
|
3
|
+
export const ROOT_DIR = resolve(__dirname, "..", "..");
|
|
4
|
+
export const SRC_DIR = resolve(ROOT_DIR, "src");
|
|
5
|
+
export const DIST_DIR = resolve(ROOT_DIR, "dist");
|
|
6
|
+
export const VENDOR_ROOT_DIR = resolve(ROOT_DIR, "vendor");
|
|
7
|
+
export const VENDOR_SRC_DIR = resolve(VENDOR_ROOT_DIR, "src");
|
|
8
|
+
export const VENDOR_DIST_DIR = resolve(VENDOR_ROOT_DIR, "dist");
|
|
9
|
+
export const VENDOR_REACT_SSR_PATH = resolve(VENDOR_DIST_DIR, "react-ssr.js");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const findWranglerConfig: (projectRootDir: string) => Promise<string>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { pathExists } from "fs-extra";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
export const findWranglerConfig = async (projectRootDir) => {
|
|
4
|
+
const configFiles = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
5
|
+
for (const file of configFiles) {
|
|
6
|
+
const fullPath = resolve(projectRootDir, file);
|
|
7
|
+
if (await pathExists(fullPath)) {
|
|
8
|
+
return fullPath;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
throw new Error("No wrangler configuration file found.");
|
|
12
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getShortName: (file: string, root: string) => string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { symlink, copyFile } from "node:fs/promises";
|
|
3
|
+
import { pathExists } from "fs-extra";
|
|
4
|
+
export async function setupEnvFiles({ rootDir, }) {
|
|
5
|
+
const envPath = resolve(rootDir, ".env");
|
|
6
|
+
const devVarsPath = resolve(rootDir, ".dev.vars");
|
|
7
|
+
const envExamplePath = resolve(rootDir, ".env.example");
|
|
8
|
+
const envExists = await pathExists(envPath);
|
|
9
|
+
const devVarsExists = await pathExists(devVarsPath);
|
|
10
|
+
const envExampleExists = await pathExists(envExamplePath);
|
|
11
|
+
// If .env.example exists but .env doesn't, copy from example to .env
|
|
12
|
+
if (!envExists && !devVarsExists && envExampleExists) {
|
|
13
|
+
try {
|
|
14
|
+
await copyFile(envExamplePath, envPath);
|
|
15
|
+
console.log("Created .env file from .env.example");
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
console.warn("Failed to copy .env.example to .env:", error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Create symlink from .env to .dev.vars if needed
|
|
22
|
+
if ((await pathExists(envPath)) && !devVarsExists) {
|
|
23
|
+
try {
|
|
24
|
+
await symlink(envPath, devVarsPath);
|
|
25
|
+
console.log("Created symlink from .env to .dev.vars");
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.warn("Failed to create symlink from .env to .dev.vars:", error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { interruptors } from "./rules/interruptors.js";
|
|
2
|
+
import { middleware } from "./rules/middleware.js";
|
|
3
|
+
import { react } from "./rules/react.js";
|
|
4
|
+
import { requestResponse } from "./rules/request-response.js";
|
|
5
|
+
const rules = [
|
|
6
|
+
{
|
|
7
|
+
name: "rwsdk-interruptors",
|
|
8
|
+
description: "RedwoodSDK: Request Interruptors",
|
|
9
|
+
rule: interruptors,
|
|
10
|
+
alwaysApply: false,
|
|
11
|
+
globs: ["worker.tsx", "src/app/**/routes.ts", "src/app/**/*/routes.ts"],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "rwsdk-middleware",
|
|
15
|
+
description: "RedwoodSDK: Middleware",
|
|
16
|
+
rule: middleware,
|
|
17
|
+
alwaysApply: false,
|
|
18
|
+
globs: ["worker.tsx", "middleware.ts", "middleware.tsx"],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "rwsdk-react",
|
|
22
|
+
description: "RedwoodSDK: React, React Server Components, and React Server Functions Rules",
|
|
23
|
+
rule: react,
|
|
24
|
+
alwaysApply: false,
|
|
25
|
+
globs: ["src/app/**/*/*.tsx", "Document.tsx"],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "rwsdk-request-response",
|
|
29
|
+
description: "RedwoodSDK: Request handling and responses",
|
|
30
|
+
rule: requestResponse,
|
|
31
|
+
alwaysApply: false,
|
|
32
|
+
globs: ["worker.tsc", "src/app/**/routes.ts", "src/app/**/*/routes.ts"],
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
export default rules;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const interruptors = "\n\n# RedwoodSDK: Request Interruptors\n\nYou're an expert at Cloudflare, TypeScript, and building web apps with RedwoodSDK. Generate high quality **RedwoodSDK interruptors** (middleware functions) that adhere to the following best practices:\n\n## Guidelines\n\n1. Create focused, single-responsibility interruptors\n2. Organize interruptors in dedicated files (e.g., `interruptors.ts`, `interceptors.ts`, or `middleware.ts`)\n3. Compose interruptors to create more complex validation chains\n4. Use typed parameters and return values\n5. Include clear error handling and user feedback\n\n## What are Interruptors?\n\nInterruptors are middleware functions that run before your route handlers. They can:\n\n- Validate user authentication and authorization\n- Transform request data\n- Validate inputs\n- Rate limit requests\n- Log activity\n- Redirect users based on conditions\n- Short-circuit request handling with early responses\n\n## Example Templates\n\n### Basic Interruptor Structure\n\n```tsx\nasync function myInterruptor({ request, params, ctx }) {\n // Perform checks or transformations here\n\n // Return modified context to pass to the next interruptor or handler\n ctx.someAddedData = \"value\";\n\n // OR return a Response to short-circuit the request\n // return new Response('Unauthorized', { status: 401 });\n}\n```\n\n### Authentication Interruptors\n\n```tsx\nexport async function requireAuth({ request, ctx }) {\n if (!ctx.user) {\n return new Response(null, {\n status: 302,\n headers: { Location: \"/user/login\" },\n });\n }\n}\n\nexport async function requireAdmin({ request, ctx }) {\n if (!ctx?.user?.isAdmin) {\n return new Response(null, {\n status: 302,\n headers: { Location: \"/user/login\" },\n });\n }\n}\n```\n\n### Input Validation Interruptor\n\n```tsx\nimport { z } from \"zod\";\n\n// Create a reusable validator interruptor\nexport function validateInput(schema) {\n return async function validateInputInterruptor({ request, ctx }) {\n try {\n const data = await request.json();\n const validated = (ctx.data = schema.parse(data));\n } catch (error) {\n return Response.json(\n { error: \"Validation failed\", details: error.errors },\n { status: 400 },\n );\n }\n };\n}\n\n// Usage example with a Zod schema\nconst userSchema = z.object({\n name: z.string().min(2),\n email: z.string().email(),\n age: z.number().min(18).optional(),\n});\n\nexport const validateUser = validateInput(userSchema);\n```\n\n### Logging Interruptor\n\n```tsx\nexport async function logRequests({ request, ctx }) {\n const start = Date.now();\n\n // Add a function to the context that will log when called\n ctx.logCompletion: (response) => {\n const duration = Date.now() - start;\n const status = response.status;\n console.log(\n `${request.method} ${request.url} - ${status} (${duration}ms)`,\n );\n },\n };\n}\n\n// Usage in a route handler\nroute('/', [\n logRequests,\n async ({request, ctx}) => {\n // Call the logging function\n ctx.logCompletion(response);\n return Response.json({ success: true });;\n },\n]);\n```\n\n### Composing Multiple Interruptors\n\n```tsx\nimport { route } from \"rwsdk/router\";\nimport {\n requireAuth,\n validateUser,\n apiRateLimit,\n logRequests,\n} from \"@/app/interruptors\";\n\n// Combine multiple interruptors\nroute(\"/api/users\", [\n logRequests, // Log all requests\n requireAuth, // Ensure user is authenticated\n validateUser, // Validate user input\n async ({ request, ctx }) => {\n // Handler receives validated data and session from interruptors\n const newUser = await db.user.create({\n data: {\n /* ... */,\n createdBy: ctx.user.userId,\n },\n });\n\n return Response.json(newUser, { status: 201 });\n },\n ],\n});\n```\n\n### Role-Based Access Control\n\n```tsx\nimport { getSession } from \"rwsdk/auth\";\n\n// Create a function that generates role-based interruptors\nexport function hasRole(allowedRoles) {\n return async function hasRoleInterruptor({ request, ctx }) {\n const session = await getSession(request);\n\n if (!session) {\n return Response.redirect(\"/login\");\n }\n\n if (!allowedRoles.includes(session.role)) {\n return Response.json({ error: \"Unauthorized\" }, { status: 403 });\n }\n\n return { ...ctx, session };\n };\n}\n\n// Create specific role-based interruptors\nexport const isAdmin = hasRole([\"ADMIN\"]);\nexport const isEditor = hasRole([\"ADMIN\", \"EDITOR\"]);\nexport const isUser = hasRole([\"ADMIN\", \"EDITOR\", \"USER\"]);\n```\n\n### Organization with Co-located Interruptors\n\nCreate a file at `./src/app/interruptors.ts`:\n\n```tsx\nimport { getSession } from \"rwsdk/auth\";\n\n// Authentication interruptors\nexport async function requireAuth({ request, ctx }) {\n const session = await getSession(request);\n\n if (!session) {\n return Response.redirect(\"/login\");\n }\n\n return { ...ctx, session };\n}\n\n// Role-based interruptors\nexport function hasRole(allowedRoles) {\n return async function hasRoleInterruptor({ request, ctx }) {\n const session = await getSession(request);\n\n if (!session) {\n return Response.redirect(\"/login\");\n }\n\n if (!allowedRoles.includes(session.role)) {\n return Response.json({ error: \"Unauthorized\" }, { status: 403 });\n }\n\n return { ...ctx, session };\n };\n}\n\nexport const isAdmin = hasRole([\"ADMIN\"]);\nexport const isEditor = hasRole([\"ADMIN\", \"EDITOR\"]);\n\n// Other common interruptors\nexport async function logRequests({ request, ctx }) {\n console.log(`${request.method} ${request.url}`);\n return ctx;\n}\n```\n\nThen import these interruptors in your route files:\n\n```tsx\n// src/app/pages/admin/routes.ts\nimport { route } from \"rwsdk/router\";\nimport { isAdmin, logRequests } from \"@/app/interruptors\";\n\nimport { AdminDashboard } from \"./AdminDashboard\";\nimport { UserManagement } from \"./UserManagement\";\n\nexport const routes = [\n route(\"/\", [isAdmin, logRequests, AdminDashboard]),\n route(\"/users\", [isAdmin, logRequests, UserManagement]),\n];\n```\n\n";
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
export const interruptors = `
|
|
2
|
+
|
|
3
|
+
# RedwoodSDK: Request Interruptors
|
|
4
|
+
|
|
5
|
+
You're an expert at Cloudflare, TypeScript, and building web apps with RedwoodSDK. Generate high quality **RedwoodSDK interruptors** (middleware functions) that adhere to the following best practices:
|
|
6
|
+
|
|
7
|
+
## Guidelines
|
|
8
|
+
|
|
9
|
+
1. Create focused, single-responsibility interruptors
|
|
10
|
+
2. Organize interruptors in dedicated files (e.g., \`interruptors.ts\`, \`interceptors.ts\`, or \`middleware.ts\`)
|
|
11
|
+
3. Compose interruptors to create more complex validation chains
|
|
12
|
+
4. Use typed parameters and return values
|
|
13
|
+
5. Include clear error handling and user feedback
|
|
14
|
+
|
|
15
|
+
## What are Interruptors?
|
|
16
|
+
|
|
17
|
+
Interruptors are middleware functions that run before your route handlers. They can:
|
|
18
|
+
|
|
19
|
+
- Validate user authentication and authorization
|
|
20
|
+
- Transform request data
|
|
21
|
+
- Validate inputs
|
|
22
|
+
- Rate limit requests
|
|
23
|
+
- Log activity
|
|
24
|
+
- Redirect users based on conditions
|
|
25
|
+
- Short-circuit request handling with early responses
|
|
26
|
+
|
|
27
|
+
## Example Templates
|
|
28
|
+
|
|
29
|
+
### Basic Interruptor Structure
|
|
30
|
+
|
|
31
|
+
\`\`\`tsx
|
|
32
|
+
async function myInterruptor({ request, params, ctx }) {
|
|
33
|
+
// Perform checks or transformations here
|
|
34
|
+
|
|
35
|
+
// Return modified context to pass to the next interruptor or handler
|
|
36
|
+
ctx.someAddedData = "value";
|
|
37
|
+
|
|
38
|
+
// OR return a Response to short-circuit the request
|
|
39
|
+
// return new Response('Unauthorized', { status: 401 });
|
|
40
|
+
}
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
### Authentication Interruptors
|
|
44
|
+
|
|
45
|
+
\`\`\`tsx
|
|
46
|
+
export async function requireAuth({ request, ctx }) {
|
|
47
|
+
if (!ctx.user) {
|
|
48
|
+
return new Response(null, {
|
|
49
|
+
status: 302,
|
|
50
|
+
headers: { Location: "/user/login" },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function requireAdmin({ request, ctx }) {
|
|
56
|
+
if (!ctx?.user?.isAdmin) {
|
|
57
|
+
return new Response(null, {
|
|
58
|
+
status: 302,
|
|
59
|
+
headers: { Location: "/user/login" },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
\`\`\`
|
|
64
|
+
|
|
65
|
+
### Input Validation Interruptor
|
|
66
|
+
|
|
67
|
+
\`\`\`tsx
|
|
68
|
+
import { z } from "zod";
|
|
69
|
+
|
|
70
|
+
// Create a reusable validator interruptor
|
|
71
|
+
export function validateInput(schema) {
|
|
72
|
+
return async function validateInputInterruptor({ request, ctx }) {
|
|
73
|
+
try {
|
|
74
|
+
const data = await request.json();
|
|
75
|
+
const validated = (ctx.data = schema.parse(data));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return Response.json(
|
|
78
|
+
{ error: "Validation failed", details: error.errors },
|
|
79
|
+
{ status: 400 },
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Usage example with a Zod schema
|
|
86
|
+
const userSchema = z.object({
|
|
87
|
+
name: z.string().min(2),
|
|
88
|
+
email: z.string().email(),
|
|
89
|
+
age: z.number().min(18).optional(),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const validateUser = validateInput(userSchema);
|
|
93
|
+
\`\`\`
|
|
94
|
+
|
|
95
|
+
### Logging Interruptor
|
|
96
|
+
|
|
97
|
+
\`\`\`tsx
|
|
98
|
+
export async function logRequests({ request, ctx }) {
|
|
99
|
+
const start = Date.now();
|
|
100
|
+
|
|
101
|
+
// Add a function to the context that will log when called
|
|
102
|
+
ctx.logCompletion: (response) => {
|
|
103
|
+
const duration = Date.now() - start;
|
|
104
|
+
const status = response.status;
|
|
105
|
+
console.log(
|
|
106
|
+
\`\${request.method} \${request.url} - \${status} (\${duration}ms)\`,
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Usage in a route handler
|
|
113
|
+
route('/', [
|
|
114
|
+
logRequests,
|
|
115
|
+
async ({request, ctx}) => {
|
|
116
|
+
// Call the logging function
|
|
117
|
+
ctx.logCompletion(response);
|
|
118
|
+
return Response.json({ success: true });;
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
\`\`\`
|
|
122
|
+
|
|
123
|
+
### Composing Multiple Interruptors
|
|
124
|
+
|
|
125
|
+
\`\`\`tsx
|
|
126
|
+
import { route } from "rwsdk/router";
|
|
127
|
+
import {
|
|
128
|
+
requireAuth,
|
|
129
|
+
validateUser,
|
|
130
|
+
apiRateLimit,
|
|
131
|
+
logRequests,
|
|
132
|
+
} from "@/app/interruptors";
|
|
133
|
+
|
|
134
|
+
// Combine multiple interruptors
|
|
135
|
+
route("/api/users", [
|
|
136
|
+
logRequests, // Log all requests
|
|
137
|
+
requireAuth, // Ensure user is authenticated
|
|
138
|
+
validateUser, // Validate user input
|
|
139
|
+
async ({ request, ctx }) => {
|
|
140
|
+
// Handler receives validated data and session from interruptors
|
|
141
|
+
const newUser = await db.user.create({
|
|
142
|
+
data: {
|
|
143
|
+
/* ... */,
|
|
144
|
+
createdBy: ctx.user.userId,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return Response.json(newUser, { status: 201 });
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
\`\`\`
|
|
153
|
+
|
|
154
|
+
### Role-Based Access Control
|
|
155
|
+
|
|
156
|
+
\`\`\`tsx
|
|
157
|
+
import { getSession } from "rwsdk/auth";
|
|
158
|
+
|
|
159
|
+
// Create a function that generates role-based interruptors
|
|
160
|
+
export function hasRole(allowedRoles) {
|
|
161
|
+
return async function hasRoleInterruptor({ request, ctx }) {
|
|
162
|
+
const session = await getSession(request);
|
|
163
|
+
|
|
164
|
+
if (!session) {
|
|
165
|
+
return Response.redirect("/login");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!allowedRoles.includes(session.role)) {
|
|
169
|
+
return Response.json({ error: "Unauthorized" }, { status: 403 });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { ...ctx, session };
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Create specific role-based interruptors
|
|
177
|
+
export const isAdmin = hasRole(["ADMIN"]);
|
|
178
|
+
export const isEditor = hasRole(["ADMIN", "EDITOR"]);
|
|
179
|
+
export const isUser = hasRole(["ADMIN", "EDITOR", "USER"]);
|
|
180
|
+
\`\`\`
|
|
181
|
+
|
|
182
|
+
### Organization with Co-located Interruptors
|
|
183
|
+
|
|
184
|
+
Create a file at \`./src/app/interruptors.ts\`:
|
|
185
|
+
|
|
186
|
+
\`\`\`tsx
|
|
187
|
+
import { getSession } from "rwsdk/auth";
|
|
188
|
+
|
|
189
|
+
// Authentication interruptors
|
|
190
|
+
export async function requireAuth({ request, ctx }) {
|
|
191
|
+
const session = await getSession(request);
|
|
192
|
+
|
|
193
|
+
if (!session) {
|
|
194
|
+
return Response.redirect("/login");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { ...ctx, session };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Role-based interruptors
|
|
201
|
+
export function hasRole(allowedRoles) {
|
|
202
|
+
return async function hasRoleInterruptor({ request, ctx }) {
|
|
203
|
+
const session = await getSession(request);
|
|
204
|
+
|
|
205
|
+
if (!session) {
|
|
206
|
+
return Response.redirect("/login");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!allowedRoles.includes(session.role)) {
|
|
210
|
+
return Response.json({ error: "Unauthorized" }, { status: 403 });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { ...ctx, session };
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export const isAdmin = hasRole(["ADMIN"]);
|
|
218
|
+
export const isEditor = hasRole(["ADMIN", "EDITOR"]);
|
|
219
|
+
|
|
220
|
+
// Other common interruptors
|
|
221
|
+
export async function logRequests({ request, ctx }) {
|
|
222
|
+
console.log(\`\${request.method} \${request.url}\`);
|
|
223
|
+
return ctx;
|
|
224
|
+
}
|
|
225
|
+
\`\`\`
|
|
226
|
+
|
|
227
|
+
Then import these interruptors in your route files:
|
|
228
|
+
|
|
229
|
+
\`\`\`tsx
|
|
230
|
+
// src/app/pages/admin/routes.ts
|
|
231
|
+
import { route } from "rwsdk/router";
|
|
232
|
+
import { isAdmin, logRequests } from "@/app/interruptors";
|
|
233
|
+
|
|
234
|
+
import { AdminDashboard } from "./AdminDashboard";
|
|
235
|
+
import { UserManagement } from "./UserManagement";
|
|
236
|
+
|
|
237
|
+
export const routes = [
|
|
238
|
+
route("/", [isAdmin, logRequests, AdminDashboard]),
|
|
239
|
+
route("/users", [isAdmin, logRequests, UserManagement]),
|
|
240
|
+
];
|
|
241
|
+
\`\`\`
|
|
242
|
+
|
|
243
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const middleware = "\n\n# RedwoodSDK: Middleware\n\nYou're an expert at Cloudflare, TypeScript, and building web apps with RedwoodSDK. Generate high quality **RedwoodSDK middleware** that adhere to the following best practices:\n\n## Guidelines\n\n1. Create focused, single-responsibility middleware functions\n2. Organize middleware in dedicated files (e.g., `middleware.ts`, `middleware.tsx`)\n3. Use typed parameters and return values\n4. Include clear error handling and logging\n5. Follow the principle of least privilege\n6. Implement proper security headers and CORS policies\n7. Optimize for performance with caching strategies\n\n## What is Middleware?\n\nMiddleware functions in RedwoodSDK are functions that run on every request before your route handlers. They can:\n\n- Add security headers\n- Handle CORS\n- Implement caching strategies\n- Add request/response logging\n- Transform request/response data\n- Implement rate limiting\n- Add performance monitoring\n- Handle error boundaries\n- Setup sessions\n- Authenticate users\n\n## Example Templates\n\n### Basic Middleware Structure\n\n```tsx\nexport default defineApp([\n setCommonHeaders(),\n async ({ ctx, request, headers }) => {\n await setupDb(env);\n setupSessionStore(env);\n try {\n // Grab the session's data.\n ctx.session = await sessions.load(request);\n } catch (error) {\n if (error instanceof ErrorResponse && error.code === 401) {\n await sessions.remove(request, headers);\n headers.set(\"Location\", \"/user/login\");\n\n return new Response(null, {\n status: 302,\n headers,\n });\n }\n\n throw error;\n }\n\n // Populate the ctx with the user's data\n if (ctx.session?.userId) {\n ctx.user = await db.user.findUnique({\n where: {\n id: ctx.session.userId,\n },\n });\n }\n },\n // Route handlers\n]);\n```\n";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const middleware = `
|
|
2
|
+
|
|
3
|
+
# RedwoodSDK: Middleware
|
|
4
|
+
|
|
5
|
+
You're an expert at Cloudflare, TypeScript, and building web apps with RedwoodSDK. Generate high quality **RedwoodSDK middleware** that adhere to the following best practices:
|
|
6
|
+
|
|
7
|
+
## Guidelines
|
|
8
|
+
|
|
9
|
+
1. Create focused, single-responsibility middleware functions
|
|
10
|
+
2. Organize middleware in dedicated files (e.g., \`middleware.ts\`, \`middleware.tsx\`)
|
|
11
|
+
3. Use typed parameters and return values
|
|
12
|
+
4. Include clear error handling and logging
|
|
13
|
+
5. Follow the principle of least privilege
|
|
14
|
+
6. Implement proper security headers and CORS policies
|
|
15
|
+
7. Optimize for performance with caching strategies
|
|
16
|
+
|
|
17
|
+
## What is Middleware?
|
|
18
|
+
|
|
19
|
+
Middleware functions in RedwoodSDK are functions that run on every request before your route handlers. They can:
|
|
20
|
+
|
|
21
|
+
- Add security headers
|
|
22
|
+
- Handle CORS
|
|
23
|
+
- Implement caching strategies
|
|
24
|
+
- Add request/response logging
|
|
25
|
+
- Transform request/response data
|
|
26
|
+
- Implement rate limiting
|
|
27
|
+
- Add performance monitoring
|
|
28
|
+
- Handle error boundaries
|
|
29
|
+
- Setup sessions
|
|
30
|
+
- Authenticate users
|
|
31
|
+
|
|
32
|
+
## Example Templates
|
|
33
|
+
|
|
34
|
+
### Basic Middleware Structure
|
|
35
|
+
|
|
36
|
+
\`\`\`tsx
|
|
37
|
+
export default defineApp([
|
|
38
|
+
setCommonHeaders(),
|
|
39
|
+
async ({ ctx, request, headers }) => {
|
|
40
|
+
await setupDb(env);
|
|
41
|
+
setupSessionStore(env);
|
|
42
|
+
try {
|
|
43
|
+
// Grab the session's data.
|
|
44
|
+
ctx.session = await sessions.load(request);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error instanceof ErrorResponse && error.code === 401) {
|
|
47
|
+
await sessions.remove(request, headers);
|
|
48
|
+
headers.set("Location", "/user/login");
|
|
49
|
+
|
|
50
|
+
return new Response(null, {
|
|
51
|
+
status: 302,
|
|
52
|
+
headers,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Populate the ctx with the user's data
|
|
60
|
+
if (ctx.session?.userId) {
|
|
61
|
+
ctx.user = await db.user.findUnique({
|
|
62
|
+
where: {
|
|
63
|
+
id: ctx.session.userId,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
// Route handlers
|
|
69
|
+
]);
|
|
70
|
+
\`\`\`
|
|
71
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const react = "\n# React, React Server Components, and React Server Functions Rules\n\n## React Server Components (RSC)\n\n1. By default, all components are server components unless explicitly marked as client components.\n2. Server components are rendered on the server as HTML and streamed to the browser.\n3. Server components cannot include client-side interactivity (state, effects, event handlers).\n4. Server components can directly fetch data and include it in the initial payload.\n5. Server components can be async and can be wrapped in Suspense boundaries.\n\nExample:\n\n```tsx\nexport default function MyServerComponent() {\n return <div>Hello, from the server!</div>;\n}\n```\n\n## Client Components\n\n1. Must be explicitly marked with the \"use client\" directive at the top of the file.\n2. Required when the component needs:\n - Interactivity (click handlers, state management)\n - Browser APIs\n - Event listeners\n - Client-side effects\n - Client-side routing\n3. Will be hydrated by React in the browser.\n\nExample:\n\n```tsx\n\"use client\";\n\nexport default function MyClientComponent() {\n return <button onClick={() => console.log(\"clicked\")}>Click me</button>;\n}\n```\n\n## Data Fetching in Server Components\n\n1. Server components can directly fetch data without useEffect or other client-side data fetching methods.\n2. Use Suspense boundaries to handle loading states for async server components.\n3. Pass context (ctx) through props to child components that need it.\n\nExample:\n\n```tsx\nexport async function TodoList({ ctx }) {\n const todos = await db.todo.findMany({ where: { userId: ctx.user.id } });\n\n return (\n <ol>\n {todos.map((todo) => (\n <li key={todo.id}>{todo.title}</li>\n ))}\n </ol>\n );\n}\n```\n\n## Server Functions\n\n1. Must be marked with the \"use server\" directive at the top of the file.\n2. Can be imported and used in client components.\n3. Execute on the server when called from client components.\n4. Have access to the request context via requestInfo.ctx.\n5. Can handle form submissions and other server-side operations.\n\nExample:\n\n```tsx\n\"use server\";\n\nimport { requestInfo } from \"rwsdk/worker\";\n\nexport async function addTodo(formData: FormData) {\n const { ctx } = requestInfo;\n const title = formData.get(\"title\");\n await db.todo.create({ data: { title, userId: ctx.user.id } });\n}\n```\n\n## Context Usage\n\n1. Context is available to all server components and server functions.\n2. Access context via:\n - requestInfo in server functions:\n ```\n import { requestInfo } from \"rwsdk/worker\";\n const { ctx } = requestInfo\n ```\n3. Context is populated by middleware and interruptors and is request-scoped.\n\n## Best Practices\n\n1. Keep server components as the default choice unless client-side interactivity is needed.\n2. Use client components only when necessary to minimize the JavaScript bundle size.\n3. Leverage server components for data fetching and initial rendering.\n4. Use Suspense boundaries appropriately for loading states.\n5. Keep client components as small as possible, moving server-side logic to server components or server functions.\n6. Always mark client components with \"use client\" directive.\n7. Always mark server functions with \"use server\" directive.\n\n";
|