mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
## Summary - Adds the generated `@stackframe/tanstack-start` workspace package registration. - Adds TanStack Start platform macros/dependencies to the SDK template and generator. - Adds TanStack Start cookie/token-store support plus the handler SSR guard needed by Start. ## Scope This intentionally excludes Dashboard V2 routes, hooks, components, app shell logic, and dashboard API type additions. Those stay in the existing dashboard PR/branch. ## Validation - `pnpm install --lockfile-only --ignore-scripts` - `pnpm install --ignore-scripts` - `pnpm -C packages/template lint src/components-page/stack-handler-client.tsx src/lib/cookie.ts src/lib/stack-app/apps/implementations/client-app-impl.ts` Package typecheck was attempted with `pnpm -C packages/template typecheck`, but the clean worktree lacks generated package declaration outputs for workspace dependencies such as `@stackframe/stack-shared` and `@stackframe/stack-ui`. Per repo instructions, package builds/codegen are not run by agents. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * TanStack Start integration: published SDK package, example demo app, dashboard onboarding flow, framework-aware CTAs/docs, and a TanStack-specific provider for client-only auth routes. * Improved client/server auth: safer runtime guards and consistent cookie/token-store behavior across SSR and client. * **Documentation** * New Integrations guide and expanded getting-started/setup docs with TanStack Start examples and env/key guidance. * **Chores** * Template, build, tooling, and demo config updates to support the new platform. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
219 lines
6.8 KiB
TypeScript
219 lines
6.8 KiB
TypeScript
import fs from "fs";
|
||
import path from "path";
|
||
import { COMMENT_BLOCK, COMMENT_LINE, PLATFORMS, copyFromSrcToDest, processMacros, withGeneratorLock, writeFileSyncIfChanged } from "./utils";
|
||
|
||
/**
|
||
* Main function to generate from a template:
|
||
* 1. Ensures the destination exists.
|
||
* 2. Copies from src to dest (applying a composite editFn).
|
||
* 3. Removes any items in dest that aren’t in src.
|
||
* 4. Cleans up empty folders.
|
||
*
|
||
* The composite editFn encapsulates the hard rules:
|
||
* - Global ignores (e.g. node_modules, dist, etc.)
|
||
* - Skipping source package.json.
|
||
* - Renaming package-template.json -> package.json.
|
||
* - Inserting header comments into .tsx, .ts, or .js files.
|
||
* - Adding a comment field in package.json files.
|
||
*
|
||
* Custom editFns provided in options can further modify content.
|
||
*/
|
||
function generateFromTemplate(options: {
|
||
src: string;
|
||
dest: string;
|
||
editFn?: (relativePath: string, content: string) => string;
|
||
filterFn?: (relativePath: string) => boolean;
|
||
destFn?: (relativePath: string) => string;
|
||
}) {
|
||
const { src, dest, editFn, filterFn, destFn } = options;
|
||
|
||
// Composite edit function that applies the hard rules first,
|
||
// then defers to any custom edit function.
|
||
function compositeEditFn(
|
||
relativePath: string,
|
||
content: string
|
||
): string {
|
||
let newContent: string = editFn ? editFn(relativePath, content) : content;
|
||
|
||
// For .tsx, .ts, or .js files, add header comments.
|
||
if (/\.(tsx|ts|js)$/.test(relativePath)) {
|
||
const hasShebang =
|
||
newContent.startsWith("#") ||
|
||
newContent.startsWith('"') ||
|
||
newContent.startsWith("'");
|
||
let shebangLine = "";
|
||
let contentWithoutShebang = newContent;
|
||
if (hasShebang) {
|
||
const lines = newContent.split("\n");
|
||
shebangLine = lines[0] + "\n\n";
|
||
contentWithoutShebang = lines.slice(1).join("\n");
|
||
}
|
||
newContent = shebangLine + COMMENT_BLOCK + contentWithoutShebang;
|
||
}
|
||
|
||
// If the resulting file is package.json, add a comment field to the JSON.
|
||
if (path.basename(relativePath) === "package.json") {
|
||
const jsonObj = JSON.parse(newContent);
|
||
newContent = JSON.stringify({ "//": COMMENT_LINE, ...jsonObj }, null, 2) + "\n";
|
||
}
|
||
|
||
return newContent;
|
||
}
|
||
|
||
function compositeDestFn(relativePath: string) {
|
||
if (relativePath === "package-template.json") {
|
||
return "package.json";
|
||
}
|
||
|
||
if (destFn) {
|
||
return destFn(relativePath);
|
||
}
|
||
return relativePath;
|
||
}
|
||
|
||
function compositeFilterFn(relativePath: string) {
|
||
const ignores = ["node_modules", "dist", ".turbo", ".gitignore", "package.json"];
|
||
for (const ignore of ignores) {
|
||
if (relativePath.startsWith(ignore)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (filterFn) {
|
||
return filterFn(relativePath);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// Ensure the destination directory exists.
|
||
if (!fs.existsSync(dest)) {
|
||
fs.mkdirSync(dest, { recursive: true });
|
||
}
|
||
|
||
copyFromSrcToDest({
|
||
srcDir: src,
|
||
destDir: dest,
|
||
editFn: compositeEditFn,
|
||
filterFn: compositeFilterFn,
|
||
destFn: compositeDestFn,
|
||
destRemoveSkipFn: (relativePath) => {
|
||
return relativePath.startsWith("node_modules") || relativePath.startsWith("dist") || relativePath.startsWith(".turbo");
|
||
},
|
||
});
|
||
}
|
||
|
||
function processPackageJson(path: string, content: string) {
|
||
let jsonObj: any;
|
||
try {
|
||
jsonObj = JSON.parse(content);
|
||
} catch (error) {
|
||
throw new Error(`Failed to parse package.json at ${path}`, { cause: error });
|
||
}
|
||
return JSON.stringify({ "//": `${COMMENT_LINE} (FOR package.json FILES, PLEASE EDIT package-template.json)`, ...jsonObj }, null, 2) + "\n";
|
||
}
|
||
|
||
function baseEditFn(options: {
|
||
relativePath: string,
|
||
content: string,
|
||
platforms: string[]
|
||
}) {
|
||
if (options.relativePath.startsWith("src/generated")) {
|
||
return options.content;
|
||
}
|
||
const result = processMacros(options.content, options.platforms);
|
||
if (options.relativePath === 'package-template.json') {
|
||
return processPackageJson(options.relativePath, result);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
|
||
withGeneratorLock(async () => {
|
||
const baseDir = path.resolve(__dirname, "..", "packages");
|
||
const srcDir = path.resolve(baseDir, "template");
|
||
const tanstackStartOnlyTemplateFiles = new Set([
|
||
"src/tanstack-start-server-context.combined.ts",
|
||
"src/tanstack-start-server-context.default.ts",
|
||
"src/tanstack-start-server-context.server.ts",
|
||
]);
|
||
const templateOnlyFiles = new Set([
|
||
"src/tanstack-start-server-context.d.ts",
|
||
]);
|
||
|
||
// Copy package-template.json to package.json in the template,
|
||
// applying macros and adding a comment field.
|
||
const packageTemplateContent = fs.readFileSync(
|
||
path.join(srcDir, "package-template.json"),
|
||
"utf-8"
|
||
);
|
||
const processedPackageJson = processMacros(packageTemplateContent, PLATFORMS["template"]);
|
||
writeFileSyncIfChanged(
|
||
path.join(srcDir, "package.json"),
|
||
processPackageJson(path.join(srcDir, "package-template.json"), processedPackageJson)
|
||
);
|
||
|
||
generateFromTemplate({
|
||
src: srcDir,
|
||
dest: path.resolve(baseDir, "js"),
|
||
editFn: (relativePath, content) => {
|
||
return baseEditFn({ relativePath, content, platforms: PLATFORMS["js"] });
|
||
},
|
||
filterFn: (relativePath) => {
|
||
const ignores = [
|
||
"postcss.config.js",
|
||
"tailwind.config.js",
|
||
"quetzal.config.json",
|
||
"components.json",
|
||
".env",
|
||
".env.local",
|
||
"scripts/",
|
||
"quetzal-translations/",
|
||
"src/components/",
|
||
"src/components-page/",
|
||
"src/generated/",
|
||
"src/providers/",
|
||
"src/global.css",
|
||
"src/global.d.ts",
|
||
];
|
||
|
||
if (tanstackStartOnlyTemplateFiles.has(relativePath) || templateOnlyFiles.has(relativePath)) {
|
||
return false;
|
||
} else if (ignores.some((ignorePath) => relativePath.startsWith(ignorePath)) || relativePath.endsWith(".tsx")) {
|
||
return false;
|
||
} else {
|
||
return true;
|
||
}
|
||
},
|
||
});
|
||
|
||
generateFromTemplate({
|
||
src: srcDir,
|
||
dest: path.resolve(baseDir, "stack"),
|
||
editFn: (relativePath, content) => {
|
||
return baseEditFn({ relativePath, content, platforms: PLATFORMS["next"] });
|
||
},
|
||
filterFn: (relativePath) => !tanstackStartOnlyTemplateFiles.has(relativePath),
|
||
});
|
||
|
||
generateFromTemplate({
|
||
src: srcDir,
|
||
dest: path.resolve(baseDir, "react"),
|
||
editFn: (relativePath, content) => {
|
||
return baseEditFn({ relativePath, content, platforms: PLATFORMS["react"] });
|
||
},
|
||
filterFn: (relativePath) => !tanstackStartOnlyTemplateFiles.has(relativePath),
|
||
});
|
||
|
||
generateFromTemplate({
|
||
src: srcDir,
|
||
dest: path.resolve(baseDir, "tanstack-start"),
|
||
editFn: (relativePath, content) => {
|
||
return baseEditFn({ relativePath, content, platforms: PLATFORMS["tanstack-start"] });
|
||
},
|
||
filterFn: (relativePath) => !templateOnlyFiles.has(relativePath),
|
||
});
|
||
}).catch((error) => {
|
||
console.error(error);
|
||
process.exit(1);
|
||
});
|