create-zerotal 1.0.0
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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/package.json +43 -0
- package/src/index.ts +136 -0
- package/src/prompts.ts +81 -0
- package/src/scaffold.ts +104 -0
- package/templates/admin/_env.example +2 -0
- package/templates/admin/_gitignore +4 -0
- package/templates/admin/app/admin/ProductResource.ts +192 -0
- package/templates/admin/app/admin/SettingsResource.ts +37 -0
- package/templates/admin/app/admin/UserResource.ts +103 -0
- package/templates/admin/app/admin/index.ts +46 -0
- package/templates/admin/app/admin/shared.ts +36 -0
- package/templates/admin/app/admin/widgets.ts +51 -0
- package/templates/admin/app/models/Product.ts +21 -0
- package/templates/admin/app/models/Setting.ts +14 -0
- package/templates/admin/app/models/User.ts +20 -0
- package/templates/admin/bootstrap/app.ts +16 -0
- package/templates/admin/bootstrap/providers.ts +23 -0
- package/templates/admin/config/app.ts +22 -0
- package/templates/admin/config/auth.ts +5 -0
- package/templates/admin/config/cache.ts +7 -0
- package/templates/admin/config/database.ts +14 -0
- package/templates/admin/config/session.ts +10 -0
- package/templates/admin/database/seeders/DatabaseSeeder.ts +50 -0
- package/templates/admin/package.json +18 -0
- package/templates/admin/tests/admin_test.ts +58 -0
- package/templates/admin/tsconfig.json +20 -0
- package/templates/admin/zt.ts +15 -0
- package/templates/api/_env.example +4 -0
- package/templates/api/_gitignore +5 -0
- package/templates/api/app/controllers/AuthController.ts +69 -0
- package/templates/api/app/controllers/WelcomeController.ts +10 -0
- package/templates/api/app/middleware/RequireAuth.ts +10 -0
- package/templates/api/app/models/User.ts +16 -0
- package/templates/api/bootstrap/app.ts +19 -0
- package/templates/api/config/app.ts +8 -0
- package/templates/api/config/database.ts +6 -0
- package/templates/api/config/notifications.ts +13 -0
- package/templates/api/config/queue.ts +6 -0
- package/templates/api/config/session.ts +10 -0
- package/templates/api/database/migrations/0001_create_users_table.ts +18 -0
- package/templates/api/package.json +16 -0
- package/templates/api/routes/index.ts +13 -0
- package/templates/api/tests/README.md +77 -0
- package/templates/api/tests/auth.test.ts +134 -0
- package/templates/api/tests/helpers.ts +36 -0
- package/templates/api/tsconfig.json +14 -0
- package/templates/api/zt.ts +16 -0
- package/templates/flow/_env.example +2 -0
- package/templates/flow/_gitignore +6 -0
- package/templates/flow/app/flow/layouts/app.tsx +52 -0
- package/templates/flow/app/flow/pages/about.tsx +35 -0
- package/templates/flow/app/flow/pages/contact.tsx +47 -0
- package/templates/flow/app/flow/pages/index.tsx +48 -0
- package/templates/flow/bootstrap/app.ts +8 -0
- package/templates/flow/bootstrap/providers.ts +6 -0
- package/templates/flow/config/app.ts +28 -0
- package/templates/flow/package.json +20 -0
- package/templates/flow/public/zt.svg +17 -0
- package/templates/flow/resources/css/app.css +1 -0
- package/templates/flow/resources/js/app.ts +10 -0
- package/templates/flow/resources/js/env.d.ts +23 -0
- package/templates/flow/tests/smoke.test.ts +56 -0
- package/templates/flow/tsconfig.json +18 -0
- package/templates/flow/zt.ts +15 -0
- package/templates/minimal/_env.example +2 -0
- package/templates/minimal/_gitignore +6 -0
- package/templates/minimal/bootstrap/app.ts +9 -0
- package/templates/minimal/config/app.ts +28 -0
- package/templates/minimal/package.json +20 -0
- package/templates/minimal/resources/css/app.css +1 -0
- package/templates/minimal/resources/js/app.ts +2 -0
- package/templates/minimal/resources/js/components/Logo.tsx +48 -0
- package/templates/minimal/resources/js/env.d.ts +13 -0
- package/templates/minimal/resources/js/layouts/AppLayout.tsx +56 -0
- package/templates/minimal/resources/js/pages/about.tsx +30 -0
- package/templates/minimal/resources/js/pages/contact.tsx +49 -0
- package/templates/minimal/resources/js/pages/welcome.tsx +42 -0
- package/templates/minimal/routes/index.ts +16 -0
- package/templates/minimal/tests/smoke.test.ts +51 -0
- package/templates/minimal/tsconfig.json +19 -0
- package/templates/minimal/zt.ts +15 -0
- package/templates/react/_env.example +4 -0
- package/templates/react/_gitignore +5 -0
- package/templates/react/app/exceptions/Handler.ts +55 -0
- package/templates/react/app/routes/about.ts +8 -0
- package/templates/react/app/routes/contact.ts +31 -0
- package/templates/react/app/routes/index.ts +11 -0
- package/templates/react/bootstrap/app.ts +12 -0
- package/templates/react/bootstrap/providers.ts +10 -0
- package/templates/react/config/app.ts +25 -0
- package/templates/react/config/inertia.ts +7 -0
- package/templates/react/config/session.ts +11 -0
- package/templates/react/package.json +26 -0
- package/templates/react/public/zt.svg +17 -0
- package/templates/react/resources/app.html +39 -0
- package/templates/react/resources/css/app.css +206 -0
- package/templates/react/resources/js/Components/Button.tsx +67 -0
- package/templates/react/resources/js/Components/Card.tsx +40 -0
- package/templates/react/resources/js/Components/Code.tsx +33 -0
- package/templates/react/resources/js/Components/Field.tsx +125 -0
- package/templates/react/resources/js/Components/FlashToasts.tsx +79 -0
- package/templates/react/resources/js/Components/Icons.tsx +149 -0
- package/templates/react/resources/js/Components/ThemeToggle.tsx +47 -0
- package/templates/react/resources/js/Layouts/AppLayout.tsx +163 -0
- package/templates/react/resources/js/app.tsx +29 -0
- package/templates/react/resources/js/env.d.ts +13 -0
- package/templates/react/resources/js/lib/cn.ts +13 -0
- package/templates/react/resources/js/lib/site.ts +11 -0
- package/templates/react/resources/js/pages/about.tsx +146 -0
- package/templates/react/resources/js/pages/contact.tsx +152 -0
- package/templates/react/resources/js/pages/error.tsx +91 -0
- package/templates/react/resources/js/pages/home.tsx +199 -0
- package/templates/react/resources/js/pages.generated.ts +13 -0
- package/templates/react/resources/js/types.ts +17 -0
- package/templates/react/tests/smoke.test.ts +69 -0
- package/templates/react/tsconfig.json +21 -0
- package/templates/react/zt.ts +15 -0
- package/templates/vue/_env.example +4 -0
- package/templates/vue/_gitignore +5 -0
- package/templates/vue/app/routes/about.ts +7 -0
- package/templates/vue/app/routes/contact.ts +7 -0
- package/templates/vue/app/routes/index.ts +8 -0
- package/templates/vue/bootstrap/app.ts +8 -0
- package/templates/vue/bootstrap/providers.ts +6 -0
- package/templates/vue/config/app.ts +25 -0
- package/templates/vue/config/inertia.ts +7 -0
- package/templates/vue/package.json +24 -0
- package/templates/vue/resources/app.html +13 -0
- package/templates/vue/resources/css/app.css +1 -0
- package/templates/vue/resources/js/Layouts/AppLayout.vue +63 -0
- package/templates/vue/resources/js/app.tsx +19 -0
- package/templates/vue/resources/js/env.d.ts +20 -0
- package/templates/vue/resources/js/pages/about.vue +41 -0
- package/templates/vue/resources/js/pages/contact.vue +54 -0
- package/templates/vue/resources/js/pages/home.vue +54 -0
- package/templates/vue/resources/js/pages.generated.ts +12 -0
- package/templates/vue/tests/smoke.test.ts +46 -0
- package/templates/vue/tsconfig.json +21 -0
- package/templates/vue/zt.ts +15 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scaffold boots, serves its pages, and validates its form.
|
|
3
|
+
*
|
|
4
|
+
* A template ships with a `test` script, so it ships with something for that
|
|
5
|
+
* script to run — and this is the assertion that catches a broken starter:
|
|
6
|
+
* providers register, routes resolve, and a request completes. Delete it once
|
|
7
|
+
* you have real tests, or keep it as the shape to copy.
|
|
8
|
+
*/
|
|
9
|
+
import { beforeAll, afterAll, describe, test, expect } from 'bun:test';
|
|
10
|
+
import { createTestApp, type TestApp } from '@zerotal/testing';
|
|
11
|
+
|
|
12
|
+
let app: TestApp;
|
|
13
|
+
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
// Session encryption needs a key. Seed a deterministic one for tests; `??=`
|
|
16
|
+
// leaves a real key alone when the app's .env is present.
|
|
17
|
+
Bun.env.APP_KEY ??= 'test-app-key-aaaaaaaaaaaaaaaaaaaaaaaa';
|
|
18
|
+
app = await createTestApp(() => import('../bootstrap/app.ts').then((m) => m.default));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterAll(() => app.close());
|
|
22
|
+
|
|
23
|
+
describe('scaffold', () => {
|
|
24
|
+
test('boots and serves the home page', async () => {
|
|
25
|
+
const res = await app.get('/');
|
|
26
|
+
|
|
27
|
+
res.assertOk();
|
|
28
|
+
res.assertInertia('home');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('renders the other pages', async () => {
|
|
32
|
+
(await app.get('/about')).assertInertia('about');
|
|
33
|
+
(await app.get('/contact')).assertInertia('contact');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('answers HEAD on the home route, as probes and load balancers do', async () => {
|
|
37
|
+
const res = await app.head('/');
|
|
38
|
+
expect(res.status).toBeLessThan(400);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('returns 404 for an unregistered path', async () => {
|
|
42
|
+
(await app.get('/definitely-not-a-route')).assertNotFound();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The contact form's round trip, which is the pattern the starter exists to
|
|
48
|
+
* show: validate on the server, and on failure redirect back carrying the
|
|
49
|
+
* messages and the input.
|
|
50
|
+
*/
|
|
51
|
+
describe('contact form', () => {
|
|
52
|
+
test('sends the visitor back with errors when the input is bad', async () => {
|
|
53
|
+
const res = await app.postForm('/contact', { name: 'A', email: 'nope', message: 'short' });
|
|
54
|
+
|
|
55
|
+
res.assertRedirect('/contact');
|
|
56
|
+
res.assertInvalid(['name', 'email', 'message']);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('accepts a valid submission', async () => {
|
|
60
|
+
const res = await app.postForm('/contact', {
|
|
61
|
+
name: 'Ada Lovelace',
|
|
62
|
+
email: 'ada@example.com',
|
|
63
|
+
message: 'This message is comfortably long enough to pass validation.',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
res.assertRedirect('/contact');
|
|
67
|
+
res.assertValid();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"exactOptionalPropertyTypes": true,
|
|
10
|
+
"noUncheckedIndexedAccess": true,
|
|
11
|
+
"emitDecoratorMetadata": false,
|
|
12
|
+
"jsx": "react-jsx",
|
|
13
|
+
"jsxImportSource": "react",
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"paths": {
|
|
16
|
+
"@app/*": ["./app/*"],
|
|
17
|
+
"@pages/*": ["./resources/js/pages/*"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"exclude": ["node_modules", ".zerotal"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// zt.ts — Zerotal CLI entry point.
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────
|
|
4
|
+
// Managed by the framework. All orchestration lives in @zerotal/core's
|
|
5
|
+
// startZerotal(); this file just hands it the app's bootstrap module.
|
|
6
|
+
//
|
|
7
|
+
// Run commands with: bun zt <command>
|
|
8
|
+
// bun zt serve [--port=3000] [--dev] Start the HTTP server
|
|
9
|
+
// bun zt worker Start the background worker
|
|
10
|
+
// bun zt test Run tests with the app booted
|
|
11
|
+
// bun zt list Show all available commands
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────
|
|
13
|
+
import { startZerotal } from "zerotal";
|
|
14
|
+
|
|
15
|
+
await startZerotal(() => import("./bootstrap/app"));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { env, requireEnv } from "zerotal";
|
|
2
|
+
import { AppConfig } from "zerotal/config";
|
|
3
|
+
|
|
4
|
+
export default AppConfig({
|
|
5
|
+
name: "{{name}}",
|
|
6
|
+
url: env("APP_URL", "http://localhost:3000"),
|
|
7
|
+
/**
|
|
8
|
+
* The APP_KEY is used for encryption and should be set to a random 32
|
|
9
|
+
* character string. Use `bun zt key:generate` to generate a secure key.
|
|
10
|
+
*/
|
|
11
|
+
key: requireEnv("APP_KEY"),
|
|
12
|
+
|
|
13
|
+
cors: {
|
|
14
|
+
origin: env("CORS_ORIGIN", "*"),
|
|
15
|
+
credentials: false,
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
throttle: {
|
|
19
|
+
maxAttempts: 120,
|
|
20
|
+
windowSeconds: 60,
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
// Frontend assets are built by the Inertia pipeline (`bun zt inertia:build`),
|
|
24
|
+
// not the generic asset bundler — so no `assets` block is needed here.
|
|
25
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"zt": "bun zt.ts",
|
|
8
|
+
"dev": "bun zt.ts serve --dev",
|
|
9
|
+
"start": "bun zt.ts serve",
|
|
10
|
+
"build": "bun zt.ts inertia:build --production",
|
|
11
|
+
"test": "bun zt.ts test"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@inertiajs/vue3": "^2.0.0",
|
|
15
|
+
"@zerotal/inertia": "{{zerotal_version}}",
|
|
16
|
+
"vue": "^3.5.0",
|
|
17
|
+
"zerotal": "{{zerotal_version}}"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@vue/compiler-sfc": "^3.5.0",
|
|
21
|
+
"bun-plugin-tailwind": "latest",
|
|
22
|
+
"tailwindcss": "^4.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Zerotal Example</title>
|
|
7
|
+
<link rel="stylesheet" href="/assets/app.css" />
|
|
8
|
+
<script type="module" src="/assets/app.js"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<!-- @inertia -->
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { Link, usePage } from "@inertiajs/vue3";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared application shell — sticky header with active-aware navigation.
|
|
7
|
+
*
|
|
8
|
+
* Use it as an Inertia persistent layout so the header survives client-side
|
|
9
|
+
* navigations and only the page body re-renders — in a page's script setup:
|
|
10
|
+
* defineOptions({ layout: AppLayout });
|
|
11
|
+
*/
|
|
12
|
+
const NAV = [
|
|
13
|
+
{ href: "/", label: "Home" },
|
|
14
|
+
{ href: "/about", label: "About" },
|
|
15
|
+
{ href: "/contact", label: "Contact" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const navLink =
|
|
19
|
+
"rounded-lg px-3 py-2 text-sm font-medium text-gray-600 transition hover:bg-gray-100 hover:text-gray-900";
|
|
20
|
+
const activeLink = "bg-indigo-50 text-indigo-700";
|
|
21
|
+
|
|
22
|
+
const page = usePage();
|
|
23
|
+
const url = computed(() => page.url);
|
|
24
|
+
|
|
25
|
+
const isActive = (href: string): boolean =>
|
|
26
|
+
href === "/" ? url.value === "/" : url.value.startsWith(href);
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<div
|
|
31
|
+
class="min-h-screen bg-linear-to-b from-white via-white to-indigo-50 text-gray-900 antialiased"
|
|
32
|
+
>
|
|
33
|
+
<header
|
|
34
|
+
class="sticky top-0 z-10 border-b border-gray-200/70 bg-white/80 backdrop-blur"
|
|
35
|
+
>
|
|
36
|
+
<nav class="mx-auto flex max-w-5xl items-center justify-between px-6 py-3">
|
|
37
|
+
<Link href="/" class="flex items-center gap-2 font-bold tracking-tight">
|
|
38
|
+
<span
|
|
39
|
+
class="grid h-7 w-7 place-items-center rounded-lg bg-indigo-600 text-sm text-white"
|
|
40
|
+
>
|
|
41
|
+
K
|
|
42
|
+
</span>
|
|
43
|
+
<span>Zerotal</span>
|
|
44
|
+
</Link>
|
|
45
|
+
|
|
46
|
+
<div class="flex items-center gap-1">
|
|
47
|
+
<Link
|
|
48
|
+
v-for="item in NAV"
|
|
49
|
+
:key="item.href"
|
|
50
|
+
:href="item.href"
|
|
51
|
+
:class="[navLink, isActive(item.href) ? activeLink : '']"
|
|
52
|
+
>
|
|
53
|
+
{{ item.label }}
|
|
54
|
+
</Link>
|
|
55
|
+
</div>
|
|
56
|
+
</nav>
|
|
57
|
+
</header>
|
|
58
|
+
|
|
59
|
+
<main class="mx-auto max-w-3xl px-6 py-14">
|
|
60
|
+
<slot />
|
|
61
|
+
</main>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createInertiaApp } from "@inertiajs/vue3";
|
|
2
|
+
import { createApp, h, type DefineComponent } from "vue";
|
|
3
|
+
import { pages } from "./pages.generated.ts";
|
|
4
|
+
import "../css/app.css";
|
|
5
|
+
|
|
6
|
+
createInertiaApp({
|
|
7
|
+
// Map a component name (e.g. "Users/Index") to its lazily-loaded module.
|
|
8
|
+
// `pages` is generated from the configured pages dir by `inertia:build`.
|
|
9
|
+
resolve: async (name): Promise<DefineComponent> => {
|
|
10
|
+
const page = pages[name];
|
|
11
|
+
if (!page) throw new Error(`Inertia page not found: "${name}"`);
|
|
12
|
+
return (await page()).default as DefineComponent;
|
|
13
|
+
},
|
|
14
|
+
setup({ el, App, props, plugin }) {
|
|
15
|
+
createApp({ render: () => h(App, props) })
|
|
16
|
+
.use(plugin)
|
|
17
|
+
.mount(el as Element);
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Ambient declarations for non-code assets imported from TypeScript and bundled
|
|
2
|
+
// by `bun zt serve` (see the `assets` block in config/app.ts). These let
|
|
3
|
+
// side-effect imports like `import "../css/app.css"` type-check.
|
|
4
|
+
// Vue Single-File Components, compiled by @zerotal/inertia's Bun plugin.
|
|
5
|
+
declare module "*.vue" {
|
|
6
|
+
import type { DefineComponent } from "vue";
|
|
7
|
+
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
|
|
8
|
+
export default component;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module "*.css";
|
|
12
|
+
declare module "*.scss";
|
|
13
|
+
declare module "*.svg";
|
|
14
|
+
declare module "*.png";
|
|
15
|
+
declare module "*.jpg";
|
|
16
|
+
declare module "*.jpeg";
|
|
17
|
+
declare module "*.gif";
|
|
18
|
+
declare module "*.webp";
|
|
19
|
+
declare module "*.woff";
|
|
20
|
+
declare module "*.woff2";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AppLayout from "../Layouts/AppLayout.vue";
|
|
3
|
+
|
|
4
|
+
defineOptions({ layout: AppLayout });
|
|
5
|
+
|
|
6
|
+
defineProps<{
|
|
7
|
+
title: string;
|
|
8
|
+
}>();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<section class="py-4">
|
|
13
|
+
<h1 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
|
14
|
+
{{ title }}
|
|
15
|
+
</h1>
|
|
16
|
+
<div class="mt-6 space-y-4 text-lg leading-relaxed text-gray-600">
|
|
17
|
+
<p>
|
|
18
|
+
This is an example app built with
|
|
19
|
+
<span class="font-medium text-gray-900">Zerotal</span> and its
|
|
20
|
+
<span class="font-medium text-gray-900">Inertia + Vue</span> frontend stack.
|
|
21
|
+
</p>
|
|
22
|
+
<p>
|
|
23
|
+
Each file under
|
|
24
|
+
<code
|
|
25
|
+
class="rounded bg-gray-100 px-1.5 py-0.5 font-mono text-sm text-indigo-600"
|
|
26
|
+
>
|
|
27
|
+
resources/js/pages
|
|
28
|
+
</code>
|
|
29
|
+
becomes a component you can render from a route — this page lives in
|
|
30
|
+
<code
|
|
31
|
+
class="rounded bg-gray-100 px-1.5 py-0.5 font-mono text-sm text-indigo-600"
|
|
32
|
+
>
|
|
33
|
+
about.vue
|
|
34
|
+
</code>
|
|
35
|
+
and is served at <span class="font-medium text-gray-900">/about</span>.
|
|
36
|
+
Navigation between pages is handled by Inertia, so it feels instant — no
|
|
37
|
+
full reloads.
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AppLayout from "../Layouts/AppLayout.vue";
|
|
3
|
+
|
|
4
|
+
defineOptions({ layout: AppLayout });
|
|
5
|
+
|
|
6
|
+
defineProps<{
|
|
7
|
+
title: string;
|
|
8
|
+
}>();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<section class="py-4">
|
|
13
|
+
<h1 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
|
14
|
+
{{ title }}
|
|
15
|
+
</h1>
|
|
16
|
+
<p class="mt-4 text-lg leading-relaxed text-gray-600">
|
|
17
|
+
Have a question or feedback? We'd love to hear from you.
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<div class="mt-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
|
21
|
+
<dl class="space-y-4 text-sm">
|
|
22
|
+
<div class="flex items-center justify-between gap-4">
|
|
23
|
+
<dt class="font-medium text-gray-500">Email</dt>
|
|
24
|
+
<dd>
|
|
25
|
+
<a
|
|
26
|
+
href="mailto:hello@example.com"
|
|
27
|
+
class="font-medium text-indigo-600 hover:text-indigo-700"
|
|
28
|
+
>
|
|
29
|
+
hello@example.com
|
|
30
|
+
</a>
|
|
31
|
+
</dd>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="flex items-center justify-between gap-4">
|
|
34
|
+
<dt class="font-medium text-gray-500">GitHub</dt>
|
|
35
|
+
<dd>
|
|
36
|
+
<a
|
|
37
|
+
href="https://github.com"
|
|
38
|
+
class="font-medium text-indigo-600 hover:text-indigo-700"
|
|
39
|
+
>
|
|
40
|
+
github.com/your-org
|
|
41
|
+
</a>
|
|
42
|
+
</dd>
|
|
43
|
+
</div>
|
|
44
|
+
</dl>
|
|
45
|
+
|
|
46
|
+
<a
|
|
47
|
+
href="mailto:hello@example.com"
|
|
48
|
+
class="mt-6 inline-block w-full rounded-lg bg-indigo-600 px-4 py-2.5 text-center text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700"
|
|
49
|
+
>
|
|
50
|
+
Email us
|
|
51
|
+
</a>
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Link } from "@inertiajs/vue3";
|
|
3
|
+
import AppLayout from "../Layouts/AppLayout.vue";
|
|
4
|
+
|
|
5
|
+
// Inertia persistent layout — preserved across client-side navigations.
|
|
6
|
+
defineOptions({ layout: AppLayout });
|
|
7
|
+
|
|
8
|
+
defineProps<{
|
|
9
|
+
title: string;
|
|
10
|
+
message: string;
|
|
11
|
+
}>();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<section class="py-10 text-center">
|
|
16
|
+
<span
|
|
17
|
+
class="inline-flex items-center gap-2 rounded-full bg-green-50 px-3 py-1 text-sm font-medium text-green-700 ring-1 ring-green-600/20"
|
|
18
|
+
>
|
|
19
|
+
<span class="h-2 w-2 rounded-full bg-green-500"></span>
|
|
20
|
+
The install worked successfully!
|
|
21
|
+
</span>
|
|
22
|
+
|
|
23
|
+
<h1
|
|
24
|
+
class="mx-auto mt-6 max-w-2xl text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl"
|
|
25
|
+
>
|
|
26
|
+
{{ title.replace(/!$/, "") }} with <span class="text-indigo-600">Vue</span>
|
|
27
|
+
</h1>
|
|
28
|
+
|
|
29
|
+
<p class="mx-auto mt-4 max-w-xl text-lg leading-relaxed text-gray-600">
|
|
30
|
+
{{ message }} Edit
|
|
31
|
+
<code
|
|
32
|
+
class="rounded bg-gray-100 px-1.5 py-0.5 font-mono text-sm text-indigo-600"
|
|
33
|
+
>
|
|
34
|
+
resources/js/pages/home.vue
|
|
35
|
+
</code>
|
|
36
|
+
to get started.
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<div class="mt-8 flex items-center justify-center gap-3">
|
|
40
|
+
<Link
|
|
41
|
+
href="/about"
|
|
42
|
+
class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700"
|
|
43
|
+
>
|
|
44
|
+
Learn more
|
|
45
|
+
</Link>
|
|
46
|
+
<Link
|
|
47
|
+
href="/contact"
|
|
48
|
+
class="rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50"
|
|
49
|
+
>
|
|
50
|
+
Get in touch
|
|
51
|
+
</Link>
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Auto-generated by @zerotal/inertia — do not edit manually.
|
|
2
|
+
// Regenerate with: bun zt inertia:build
|
|
3
|
+
//
|
|
4
|
+
// IMPORTANT: These are dynamic import THUNKS, not static imports.
|
|
5
|
+
// Bun.build with splitting:true creates one .js chunk per page.
|
|
6
|
+
// Converting to static imports will bundle ALL pages into app.js.
|
|
7
|
+
|
|
8
|
+
export const pages: Record<string, () => Promise<{ default: unknown }>> = {
|
|
9
|
+
'about': () => import('./pages/about.vue'),
|
|
10
|
+
'contact': () => import('./pages/contact.vue'),
|
|
11
|
+
'home': () => import('./pages/home.vue'),
|
|
12
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scaffold boots and serves its pages.
|
|
3
|
+
*
|
|
4
|
+
* A template ships with a `test` script, so it ships with something for that
|
|
5
|
+
* script to run — and this is the assertion that catches a broken starter:
|
|
6
|
+
* providers register, routes resolve, and a request completes. Delete it once
|
|
7
|
+
* you have real tests, or keep it as the shape to copy.
|
|
8
|
+
*/
|
|
9
|
+
import { beforeAll, afterAll, describe, test, expect } from 'bun:test';
|
|
10
|
+
import { createTestApp, type TestApp } from '@zerotal/testing';
|
|
11
|
+
|
|
12
|
+
let app: TestApp;
|
|
13
|
+
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
// Session encryption needs a key. Seed a deterministic one for tests; `??=`
|
|
16
|
+
// leaves a real key alone when the app's .env is present.
|
|
17
|
+
Bun.env.APP_KEY ??= 'test-app-key-aaaaaaaaaaaaaaaaaaaaaaaa';
|
|
18
|
+
app = await createTestApp(() => import('../bootstrap/app.ts').then((m) => m.default));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterAll(() => app.close());
|
|
22
|
+
|
|
23
|
+
describe('scaffold', () => {
|
|
24
|
+
test('boots and serves the home page', async () => {
|
|
25
|
+
const res = await app.get('/');
|
|
26
|
+
|
|
27
|
+
res.assertOk();
|
|
28
|
+
// assertInertia reads the page object out of the response, so the test says
|
|
29
|
+
// which component rendered rather than matching on markup that will change.
|
|
30
|
+
res.assertInertia('home');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('renders the other pages', async () => {
|
|
34
|
+
(await app.get('/about')).assertInertia('about');
|
|
35
|
+
(await app.get('/contact')).assertInertia('contact');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('answers HEAD on the home route, as probes and load balancers do', async () => {
|
|
39
|
+
const res = await app.head('/');
|
|
40
|
+
expect(res.status).toBeLessThan(400);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('returns 404 for an unregistered path', async () => {
|
|
44
|
+
(await app.get('/definitely-not-a-route')).assertNotFound();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"exactOptionalPropertyTypes": true,
|
|
10
|
+
"noUncheckedIndexedAccess": true,
|
|
11
|
+
"emitDecoratorMetadata": false,
|
|
12
|
+
"jsx": "react-jsx",
|
|
13
|
+
"jsxImportSource": "vue",
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"paths": {
|
|
16
|
+
"@app/*": ["./app/*"],
|
|
17
|
+
"@pages/*": ["./resources/js/pages/*"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"exclude": ["node_modules", ".zerotal"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// zt.ts — Zerotal CLI entry point.
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────
|
|
4
|
+
// Managed by the framework. All orchestration lives in @zerotal/core's
|
|
5
|
+
// startZerotal(); this file just hands it the app's bootstrap module.
|
|
6
|
+
//
|
|
7
|
+
// Run commands with: bun zt <command>
|
|
8
|
+
// bun zt serve [--port=3000] [--dev] Start the HTTP server
|
|
9
|
+
// bun zt worker Start the background worker
|
|
10
|
+
// bun zt test Run tests with the app booted
|
|
11
|
+
// bun zt list Show all available commands
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────
|
|
13
|
+
import { startZerotal } from "zerotal";
|
|
14
|
+
|
|
15
|
+
await startZerotal(() => import("./bootstrap/app"));
|