@deepraxis/app-template 0.0.0-stage → 0.1.0-dev.61eb886
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 +67 -0
- package/README.md +6 -2
- package/SKILL.md +121 -0
- package/package.json +12 -4
- package/template/README.md +53 -0
- package/template/_gitignore +7 -0
- package/template/app/account/page.tsx +45 -0
- package/template/app/admin/page.tsx +49 -0
- package/template/app/api/auth/login/route.ts +30 -0
- package/template/app/api/auth/register/route.ts +24 -0
- package/template/app/api/health/route.ts +10 -0
- package/template/app/api/images/route.ts +15 -0
- package/template/app/globals.css +3 -0
- package/template/app/images/[...key]/route.ts +20 -0
- package/template/app/layout.tsx +35 -0
- package/template/app/login/page.tsx +26 -0
- package/template/app/logout/route.ts +9 -0
- package/template/app/page.tsx +21 -0
- package/template/app/register/page.tsx +25 -0
- package/template/components/ui/button.tsx +24 -0
- package/template/components/ui/card.tsx +17 -0
- package/template/components/ui/input.tsx +14 -0
- package/template/components/ui/label.tsx +6 -0
- package/template/env.d.ts +11 -0
- package/template/lib/auth.test.ts +61 -0
- package/template/lib/auth.ts +107 -0
- package/template/lib/db.ts +19 -0
- package/template/lib/guard.ts +37 -0
- package/template/lib/migrate.test.ts +50 -0
- package/template/lib/migrate.ts +54 -0
- package/template/lib/migrations.ts +5 -0
- package/template/lib/schema.ts +30 -0
- package/template/lib/session.ts +48 -0
- package/template/lib/storage.ts +32 -0
- package/template/lib/users.test.ts +51 -0
- package/template/lib/users.ts +46 -0
- package/template/lib/utils.ts +6 -0
- package/template/migrations/0001_init.sql +27 -0
- package/template/next.config.ts +4 -0
- package/template/package.json +39 -0
- package/template/postcss.config.cjs +1 -0
- package/template/praxis.jsonc +10 -0
- package/template/tailwind.config.ts +7 -0
- package/template/test/fake-d1.ts +38 -0
- package/template/tsconfig.json +17 -0
- package/template/vinext-plugins.ts +77 -0
- package/template/vite.config.ts +8 -0
- package/template/wrangler.jsonc +11 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "praxis-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite dev",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"test": "bun test"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@cloudflare/vite-plugin": "^1",
|
|
13
|
+
"@vinext/cloudflare": "1.1.1",
|
|
14
|
+
"@vitejs/plugin-react": "6.1.2",
|
|
15
|
+
"@vitejs/plugin-rsc": "0.5.38",
|
|
16
|
+
"class-variance-authority": "^0.7.1",
|
|
17
|
+
"clsx": "^2.1.1",
|
|
18
|
+
"drizzle-orm": "^0.44.0",
|
|
19
|
+
"next": "15.5.25",
|
|
20
|
+
"react": "^19.0.0",
|
|
21
|
+
"react-dom": "^19.0.0",
|
|
22
|
+
"react-server-dom-webpack": "^19.2.6",
|
|
23
|
+
"tailwind-merge": "^2.5.0",
|
|
24
|
+
"vinext": "1.2.0",
|
|
25
|
+
"zod": "^3.25.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@cloudflare/workers-types": "^4.20250214.0",
|
|
29
|
+
"@types/bun": "1.3.5",
|
|
30
|
+
"@types/react": "^19.0.0",
|
|
31
|
+
"@types/react-dom": "^19.0.0",
|
|
32
|
+
"autoprefixer": "^10.4.20",
|
|
33
|
+
"postcss": "^8.4.49",
|
|
34
|
+
"tailwindcss": "^3.4.0",
|
|
35
|
+
"typescript": "^5.0.0",
|
|
36
|
+
"vite": "^8",
|
|
37
|
+
"wrangler": "4.151.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://deepraxis.com/schemas/praxis.json",
|
|
3
|
+
"kind": "application",
|
|
4
|
+
"name": "<app-name>",
|
|
5
|
+
// Secrets the app needs. "generate": praxis app deploy creates a random value on the first deploy
|
|
6
|
+
// and keeps it on every later deploy. Set your own with: praxis api application worker secret put …
|
|
7
|
+
"secrets": { "SESSION_SECRET": "generate" },
|
|
8
|
+
// praxis app deploy calls this path after deploying and fails the deploy unless it answers 2xx.
|
|
9
|
+
"healthcheck": "/api/health"
|
|
10
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// A minimal D1 over bun:sqlite: just the surface lib/migrate.ts uses (prepare/bind/all/run and a transactional batch).
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
|
|
4
|
+
class Stmt {
|
|
5
|
+
constructor(readonly db: Database, readonly sql: string, readonly args: unknown[] = []) {}
|
|
6
|
+
bind(...args: unknown[]) {
|
|
7
|
+
return new Stmt(this.db, this.sql, args);
|
|
8
|
+
}
|
|
9
|
+
run() {
|
|
10
|
+
this.db.query(this.sql).run(...(this.args as any[]));
|
|
11
|
+
return { success: true };
|
|
12
|
+
}
|
|
13
|
+
async all<T>() {
|
|
14
|
+
return { results: this.db.query(this.sql).all(...(this.args as any[])) as T[] };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function fakeD1(db = new Database(":memory:")) {
|
|
19
|
+
const log: string[] = [];
|
|
20
|
+
const d1 = {
|
|
21
|
+
prepare: (sql: string) => new Stmt(db, sql),
|
|
22
|
+
async batch(stmts: Stmt[]) {
|
|
23
|
+
db.run("BEGIN");
|
|
24
|
+
try {
|
|
25
|
+
for (const s of stmts) {
|
|
26
|
+
log.push(s.sql);
|
|
27
|
+
s.run();
|
|
28
|
+
}
|
|
29
|
+
db.run("COMMIT");
|
|
30
|
+
} catch (e) {
|
|
31
|
+
db.run("ROLLBACK");
|
|
32
|
+
throw e;
|
|
33
|
+
}
|
|
34
|
+
return stmts.map(() => ({ success: true }));
|
|
35
|
+
},
|
|
36
|
+
} as unknown as Pick<D1Database, "prepare" | "batch">;
|
|
37
|
+
return { d1, db, log };
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"strict": true,
|
|
6
|
+
"noEmit": true,
|
|
7
|
+
"module": "esnext",
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"jsx": "preserve",
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"types": ["@cloudflare/workers-types", "vite/client"],
|
|
12
|
+
"baseUrl": ".",
|
|
13
|
+
"paths": { "@/*": ["./*"] }
|
|
14
|
+
},
|
|
15
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
16
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts", "test"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
|
|
2
|
+
// Vite plugins every vinext Worker build needs (console and its zones, pay, www). Build-time only:
|
|
3
|
+
// vite configs import this; Worker code never does. Typed structurally so the package does not pin
|
|
4
|
+
// a vite version (the apps run vite 8, the repo root hoists vite 7).
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
interface BuildPlugin {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly enforce?: "pre" | "post";
|
|
11
|
+
renderChunk?(code: string): { code: string; map: null } | null;
|
|
12
|
+
options?<T extends object>(opts: T): T & { preserveEntrySignatures: "allow-extension" };
|
|
13
|
+
writeBundle?(output: { dir?: string }): void;
|
|
14
|
+
buildApp?: { order: "post"; handler(): void };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// rolldown's CJS interop helper emits `createRequire(import.meta.url)`, and workerd has no
|
|
18
|
+
// import.meta.url inside a bundled module (deploy error 10021). Pin it to a constant URL.
|
|
19
|
+
const workerdCreateRequire: BuildPlugin = {
|
|
20
|
+
name: "vinext-worker:workerd-create-require",
|
|
21
|
+
renderChunk(code) {
|
|
22
|
+
return code.includes("(import.meta.url)") ? { code: code.replaceAll("(import.meta.url)", '("file:///worker.js")'), map: null } : null;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// A Worker export that other chunks also import (instrumentWorker's telemetry-client, #5970) makes
|
|
27
|
+
// rolldown move the entry body into _next/static/worker-entry-*.js behind a facade, but vinext's
|
|
28
|
+
// root manifests are imported as "./__vinext_*.js" from that chunk: deploy error 10021, No such
|
|
29
|
+
// module. Allowing the entry to export extra bindings keeps the entry body at the root (#6007).
|
|
30
|
+
const workerEntryAtRoot: BuildPlugin = {
|
|
31
|
+
name: "vinext-worker:entry-at-root",
|
|
32
|
+
enforce: "post",
|
|
33
|
+
options(opts) {
|
|
34
|
+
return { ...opts, preserveEntrySignatures: "allow-extension" as const };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const RELATIVE_IMPORT = /(?:\bfrom\s*|\bimport\s*\(?\s*)["'](\.\.?\/[^"']+)["']/g;
|
|
39
|
+
|
|
40
|
+
/** Relative module specifiers in the emitted JS that name no file: what Cloudflare refuses with 10021. */
|
|
41
|
+
export function unresolvedModuleImports(dir: string): string[] {
|
|
42
|
+
const missing: string[] = [];
|
|
43
|
+
for (const entry of fs.readdirSync(dir, { recursive: true, withFileTypes: true })) {
|
|
44
|
+
if (!entry.isFile() || !/\.m?js$/.test(entry.name)) continue;
|
|
45
|
+
const file = path.join(entry.parentPath, entry.name);
|
|
46
|
+
for (const [, specifier] of fs.readFileSync(file, "utf8").matchAll(RELATIVE_IMPORT)) {
|
|
47
|
+
if (!fs.existsSync(path.resolve(path.dirname(file), specifier!))) missing.push(`${path.relative(dir, file)} → ${specifier}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return missing;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fail the build, not the deploy: Cloudflare only checks module resolution at upload time, after a
|
|
54
|
+
// whole deploy layer has been scheduled behind it. vinext writes its manifests while later
|
|
55
|
+
// environments build, so the check waits until every environment is done.
|
|
56
|
+
function workerModulesResolve(): BuildPlugin {
|
|
57
|
+
const dirs = new Set<string>();
|
|
58
|
+
return {
|
|
59
|
+
name: "vinext-worker:modules-resolve",
|
|
60
|
+
enforce: "post",
|
|
61
|
+
writeBundle(output) {
|
|
62
|
+
if (output.dir) dirs.add(output.dir);
|
|
63
|
+
},
|
|
64
|
+
buildApp: {
|
|
65
|
+
order: "post",
|
|
66
|
+
handler() {
|
|
67
|
+
const missing = [...dirs].filter(dir => fs.existsSync(dir)).flatMap(dir => unresolvedModuleImports(dir).map(m => `${dir}: ${m}`));
|
|
68
|
+
if (missing.length > 0) throw new Error(`Worker bundle imports modules that were not emitted (Cloudflare 10021):\n${missing.join("\n")}`);
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Put before vinext() in the plugin list. */
|
|
75
|
+
export function vinextWorkerPlugins(): BuildPlugin[] {
|
|
76
|
+
return [workerdCreateRequire, workerEntryAtRoot, workerModulesResolve()];
|
|
77
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import vinext from "vinext";
|
|
3
|
+
import { cloudflare } from "@cloudflare/vite-plugin";
|
|
4
|
+
import { vinextWorkerPlugins } from "./vinext-plugins";
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [...vinextWorkerPlugins(), vinext(), cloudflare({ viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] } })],
|
|
8
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "praxis-app",
|
|
3
|
+
"compatibility_date": "2025-04-01",
|
|
4
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
5
|
+
"main": "vinext/server/app-router-entry",
|
|
6
|
+
"assets": { "binding": "ASSETS", "not_found_handling": "none" },
|
|
7
|
+
// The platform creates worker-owned resources for these bindings and ignores the ids.
|
|
8
|
+
"d1_databases": [{ "binding": "DB", "database_name": "app-db", "database_id": "local-only" }],
|
|
9
|
+
"r2_buckets": [{ "binding": "IMAGES", "bucket_name": "app-images" }]
|
|
10
|
+
// SESSION_SECRET (secret_text) is passed at deploy time and never written here.
|
|
11
|
+
}
|