create-phial 0.0.1

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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/index.js +5 -0
  4. package/package.json +45 -0
  5. package/templates/default/README.md +54 -0
  6. package/templates/default/_gitignore +7 -0
  7. package/templates/default/app/app.config.ts +1 -0
  8. package/templates/default/app/app.vue +30 -0
  9. package/templates/default/app/assets/.gitkeep +1 -0
  10. package/templates/default/app/components/.gitkeep +1 -0
  11. package/templates/default/app/composables/.gitkeep +1 -0
  12. package/templates/default/app/error.vue +27 -0
  13. package/templates/default/app/loader.ts +8 -0
  14. package/templates/default/app/middleware/.gitkeep +1 -0
  15. package/templates/default/app/middleware/blog-trace.ts +7 -0
  16. package/templates/default/app/middleware/post-trace.ts +7 -0
  17. package/templates/default/app/middleware/request-meta.ts +7 -0
  18. package/templates/default/app/pages/action.ts +15 -0
  19. package/templates/default/app/pages/blog/[slug]/loader.ts +10 -0
  20. package/templates/default/app/pages/blog/[slug]/loading.ts +46 -0
  21. package/templates/default/app/pages/blog/[slug]/middleware.ts +1 -0
  22. package/templates/default/app/pages/blog/[slug]/page.ts +24 -0
  23. package/templates/default/app/pages/blog/_middleware.ts +1 -0
  24. package/templates/default/app/pages/jsx/page.tsx +36 -0
  25. package/templates/default/app/pages/layout.ts +49 -0
  26. package/templates/default/app/pages/loader.ts +11 -0
  27. package/templates/default/app/pages/loading.ts +55 -0
  28. package/templates/default/app/pages/page.ts +158 -0
  29. package/templates/default/app/pages/sfc/page.vue +27 -0
  30. package/templates/default/app/utils/.gitkeep +1 -0
  31. package/templates/default/package.json +25 -0
  32. package/templates/default/phial.config.ts +10 -0
  33. package/templates/default/public/.gitkeep +1 -0
  34. package/templates/default/server/context.ts +3 -0
  35. package/templates/default/server/middleware/server-trace-route.ts +13 -0
  36. package/templates/default/server/middleware/server-trace-scope.ts +13 -0
  37. package/templates/default/server/middleware/server-trace.ts +13 -0
  38. package/templates/default/server/routes/api/_middleware.ts +1 -0
  39. package/templates/default/server/routes/api/ping.ts +31 -0
  40. package/templates/default/server/routes/robots.txt.ts +9 -0
  41. package/templates/default/tsconfig.json +26 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maofeng <hornjs@qq.com> (https://github.com/hornjs)
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,53 @@
1
+ # create-phial
2
+
3
+ Scaffold a new [Phial](https://github.com/hornjs/phial) application.
4
+
5
+ ## Usage
6
+
7
+ With `pnpm`:
8
+
9
+ ```bash
10
+ pnpm create phial my-app
11
+ cd my-app
12
+ pnpm install
13
+ pnpm dev
14
+ ```
15
+
16
+ With `npm`:
17
+
18
+ ```bash
19
+ npm create phial@latest my-app
20
+ cd my-app
21
+ npm install
22
+ npm run dev
23
+ ```
24
+
25
+ With `yarn`:
26
+
27
+ ```bash
28
+ yarn create phial my-app
29
+ cd my-app
30
+ yarn install
31
+ yarn dev
32
+ ```
33
+
34
+ ## Options
35
+
36
+ ```
37
+ Usage: create-phial <project-dir> [options]
38
+
39
+ Options:
40
+ --template <name> Scaffold template name. Supported: default, zero-config
41
+ --package-manager <name> Package manager to suggest/install with: pnpm, npm, yarn, bun
42
+ --install Install dependencies after scaffolding
43
+ --force Allow writing into a non-empty target directory
44
+ -h, --help Show this help message
45
+ ```
46
+
47
+ ## Templates
48
+
49
+ - `default` - Full-featured template with examples for pages, middleware, loaders, and server routes.
50
+
51
+ ## License
52
+
53
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import{createRequire as e}from"node:module";import{spawn as t}from"node:child_process";import{mkdir as n,readFile as r,readdir as i,stat as a,writeFile as o}from"node:fs/promises";import{basename as s,dirname as c,join as l,relative as u,resolve as d}from"node:path";import f from"node:process";import{fileURLToPath as p}from"node:url";const m=d(c(p(import.meta.url)),`../templates`),h=`default`,g=new Map([[`default`,`default`],[`zero-config`,`default`]]);async function _(e={}){let t=d(d(e.cwd??f.cwd()),e.target??`phial-app`),n=d(m,T(e.template)),r=e.packageManager??D(),i=e.install??!1,a=e.force??!1;await x(n),await S(t,{force:a});let o=A(s(t)),c=await M();return await C(n,t,{packageManager:r,packageName:o,projectName:s(t),phialVersion:`^${c}`,buildCommand:k(r,`build`),devCommand:k(r,`dev`),startCommand:k(r,`start`),installCommand:O(r)}),i&&await j(t,r),{targetDir:t,packageManager:r,install:i}}function v(e){let t=[...e],n={template:h,packageManager:D(),install:!1,force:!1};for(;t.length>0;){let e=t.shift();if(e){if(e===`--help`||e===`-h`)return{help:!0};if(e===`--install`){n.install=!0;continue}if(e===`--no-install`){n.install=!1;continue}if(e===`--force`){n.force=!0;continue}if(e===`--template`){let e=t.shift();if(!e)throw Error(`Missing value for --template`);n.template=e;continue}if(e===`--package-manager`){let e=t.shift();if(!e)throw Error(`Missing value for --package-manager`);n.packageManager=E(e);continue}if(e.startsWith(`--`))throw Error(`Unknown option: ${e}`);if(!n.target){n.target=e;continue}throw Error(`Unexpected argument: ${e}`)}}return n}function y(){console.log([`Usage: create-phial <project-dir> [options]`,``,`Options:`,` --template <name> Scaffold template name. Supported: default, zero-config`,` --package-manager <name> Package manager to suggest/install with: pnpm, npm, yarn, bun`,` --install Install dependencies after scaffolding`,` --force Allow writing into a non-empty target directory`,` -h, --help Show this help message`].join(`
4
+ `))}function b(e,t=f.cwd()){let n=u(t,e.targetDir)||`.`,r=e.packageManager,i=[``,`Scaffolded Phial app in ${e.targetDir}`,``,`Next steps:`];n!==`.`&&i.push(` cd ${n}`),e.install||i.push(` ${O(r)}`),i.push(` ${k(r,`dev`)}`),console.log(i.join(`
5
+ `))}async function x(e){if(!(await a(e).catch(()=>null))?.isDirectory())throw Error(`Unknown template: ${s(e)}`)}async function S(e,t){let r=await a(e).catch(()=>null);if(!r){await n(e,{recursive:!0});return}if(!r.isDirectory())throw Error(`Target path is not a directory: ${e}`);if((await i(e)).length>0&&!t.force)throw Error(`Target directory is not empty: ${e}. Use --force to continue.`)}async function C(e,t,a){let s=await i(e,{withFileTypes:!0});for(let i of s){let s=l(e,i.name),u=l(t,i.name);if(i.name===`_gitignore`&&(u=l(t,`.gitignore`)),i.isDirectory()){await n(u,{recursive:!0}),await C(s,u,a);continue}let d=w(await r(s,`utf8`),a);await n(c(u),{recursive:!0}),await o(u,d)}}function w(e,t){return e.replace(/\{\{\s*(\w+)\s*\}\}/g,(e,n)=>n in t?String(t[n]):``)}function T(e){let t=String(e??h).trim(),n=g.get(t);if(!n)throw Error(`Unsupported template: ${t}`);return n}function E(e){switch(e){case`pnpm`:case`npm`:case`yarn`:case`bun`:return e;default:throw Error(`Unsupported package manager: ${e}`)}}function D(){let e=f.env.npm_config_user_agent??``;return e.startsWith(`pnpm/`)?`pnpm`:e.startsWith(`yarn/`)?`yarn`:e.startsWith(`bun/`)?`bun`:`npm`}function O(e){return`${e} install`}function k(e,t){return e===`npm`?`npm run ${t}`:e===`bun`?`bun run ${t}`:`${e} ${t}`}function A(e){return e.trim().toLowerCase().replace(/[^a-z0-9._-]+/g,`-`).replace(/^[._-]+/,``).replace(/[._-]+$/,``)||`phial-app`}async function j(e,n){let r=n,i=[`install`];await new Promise((a,o)=>{let s=t(r,i,{cwd:e,stdio:`inherit`,shell:f.platform===`win32`});s.on(`exit`,e=>{if(e===0){a();return}o(Error(`${n} install failed with exit code ${e??`null`}`))}),s.on(`error`,o)})}async function M(){let t=e(import.meta.url).resolve(`phial/package.json`),n=JSON.parse(await r(t,`utf8`));if(typeof n.version!=`string`||n.version.length===0)throw Error(`Installed phial package is missing a version: ${t}`);return n.version}export{_ as createPhialApp,v as parseArgs,y as printHelp,b as printNextSteps};
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "create-phial",
3
+ "version": "0.0.1",
4
+ "description": "Scaffold a new Phial application.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Maofeng <hornjs@qq.com>",
8
+ "keywords": [
9
+ "phial",
10
+ "scaffold",
11
+ "ssr",
12
+ "vue"
13
+ ],
14
+ "homepage": "https://github.com/author/library#readme",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/author/library.git"
18
+ },
19
+ "files": [
20
+ "LICENSE",
21
+ "README.md",
22
+ "dist",
23
+ "package.json",
24
+ "templates"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "require": "./dist/index.js",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
33
+ "sideEffects": false,
34
+ "dependencies": {
35
+ "cac": "^7.0.0",
36
+ "picocolors": "^1.1.1",
37
+ "phial": "latest"
38
+ },
39
+ "bin": {
40
+ "create-phial": "./dist/index.js"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }
@@ -0,0 +1,54 @@
1
+ # {{projectName}}
2
+
3
+ A Phial application scaffolded by `create-phial`.
4
+
5
+ ## Included examples
6
+
7
+ - `/` uses Vue `h()` render functions.
8
+ - `/jsx` uses Vue JSX/TSX.
9
+ - `/sfc` uses a Vue single-file component.
10
+ - `/blog/hello-world` shows a dynamic file route.
11
+
12
+ The template also supports:
13
+
14
+ - `app/loader.ts` for app-shell data exposed through `useAppData()`
15
+ - `app/middleware/*.ts` for named reusable middleware
16
+ - `app/pages/loading.*` for route loading boundaries
17
+ - `app/pages/**/_middleware.ts` for directory-scoped route middleware
18
+ - `server/routes/**/*.ts` for raw HTTP handlers exported as plain objects
19
+ - `server/middleware/*.ts` for named reusable server middleware
20
+ - `server/routes/**/_middleware.ts` for directory-scoped server middleware
21
+ - `phial.config.ts -> server.middleware` for global server middleware
22
+ - `phial prepare` for generating `.phial/types/`
23
+
24
+ ## Development
25
+
26
+ ```bash
27
+ {{installCommand}}
28
+ {{devCommand}}
29
+ ```
30
+
31
+ The scaffold also defines `pnpm prepare`, which runs `phial prepare` and refreshes `.phial/types/` so app/server middleware completion and generated app-route types stay current.
32
+
33
+ Server routes are path-owned: if a `server/routes` pattern overlaps with an `app/pages` pattern, Phial throws during scanning instead of splitting ownership by method.
34
+
35
+ ## Build
36
+
37
+ ```bash
38
+ {{installCommand}}
39
+ {{buildCommand}}
40
+ ```
41
+
42
+ By default Phial writes the production build to:
43
+
44
+ - `.output/public/` for client assets
45
+ - `.output/server/index.js` for the server bundle
46
+
47
+ If you explicitly set `vite.build.outDir`, Phial will use that directory for the client build.
48
+
49
+ ## Start
50
+
51
+ ```bash
52
+ {{installCommand}}
53
+ {{startCommand}}
54
+ ```
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ .DS_Store
3
+ .phial
4
+ .output
5
+ dist
6
+ .vite
7
+ *.local
@@ -0,0 +1 @@
1
+ export default {};
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { useAppData } from "phial";
4
+
5
+ interface AppData {
6
+ theme: "light" | "sepia";
7
+ requestedAt: string;
8
+ }
9
+
10
+ const appData = useAppData<AppData>();
11
+ const theme = computed(() => appData.value?.theme ?? "sepia");
12
+ const bodyStyle = computed(() =>
13
+ theme.value === "light"
14
+ ? "margin: 0; background: #f5f7fb; color: #172033;"
15
+ : "margin: 0; background: #f7f4ef; color: #1b1b18;",
16
+ );
17
+ </script>
18
+
19
+ <template>
20
+ <html lang="en">
21
+ <head>
22
+ <title>{{ projectName }}</title>
23
+ <meta charset="utf-8" />
24
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
25
+ </head>
26
+ <body :data-theme="theme" :style="bodyStyle">
27
+ <slot />
28
+ </body>
29
+ </html>
30
+ </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ error?:
4
+ | {
5
+ message?: string;
6
+ }
7
+ | string;
8
+ routeId?: string;
9
+ }>();
10
+ </script>
11
+
12
+ <template>
13
+ <main
14
+ data-phial-error=""
15
+ style="
16
+ margin: 0 auto;
17
+ max-width: 720px;
18
+ padding: 48px 24px;
19
+ font-family: ui-sans-serif, system-ui;
20
+ line-height: 1.6;
21
+ "
22
+ >
23
+ <h1>Something broke</h1>
24
+ <p>{{ typeof error === "string" ? error : (error?.message ?? "Unexpected error") }}</p>
25
+ <p v-if="routeId" style="color: #6e665d">Failed route: {{ routeId }}</p>
26
+ </main>
27
+ </template>
@@ -0,0 +1,8 @@
1
+ export default async function loadAppData(request: Request) {
2
+ const url = new URL(request.url);
3
+
4
+ return {
5
+ theme: url.searchParams.get("theme") === "light" ? "light" : "sepia",
6
+ requestedAt: new Date().toISOString(),
7
+ };
8
+ }
@@ -0,0 +1,7 @@
1
+ export default async function blogTraceMiddleware(
2
+ context: { params: Record<string, string> },
3
+ next: () => Promise<Response | void>,
4
+ ) {
5
+ void context.params;
6
+ return next();
7
+ }
@@ -0,0 +1,7 @@
1
+ export default async function postTraceMiddleware(
2
+ context: { params: Record<string, string> },
3
+ next: () => Promise<Response | void>,
4
+ ) {
5
+ void context.params;
6
+ return next();
7
+ }
@@ -0,0 +1,7 @@
1
+ export default async function requestMetaMiddleware(
2
+ context: { request: Request },
3
+ next: () => Promise<Response | void>,
4
+ ) {
5
+ void context.request;
6
+ return next();
7
+ }
@@ -0,0 +1,15 @@
1
+ export async function action({ formData }: { formData: FormData }) {
2
+ const name = String(formData.get("name") ?? "").trim();
3
+ if (!name) {
4
+ return {
5
+ ok: false,
6
+ message: "Please enter a name before submitting the demo form.",
7
+ };
8
+ }
9
+
10
+ return {
11
+ ok: true,
12
+ message: `Saved a greeting for ${name}.`,
13
+ submittedAt: new Date().toISOString(),
14
+ };
15
+ }
@@ -0,0 +1,10 @@
1
+ export async function loader({ params }: { params: Record<string, string> }) {
2
+ await new Promise((resolve) => setTimeout(resolve, 220));
3
+ const slug = params.slug ?? "unknown";
4
+
5
+ return {
6
+ slug,
7
+ middlewareTrace: `directory:${slug} > route:${slug}`,
8
+ summary: `Loaded post "${slug}" through a file route.`,
9
+ };
10
+ }
@@ -0,0 +1,46 @@
1
+ import { defineComponent, h } from "vue";
2
+
3
+ type RouteLoadingProps = {
4
+ routeId: string;
5
+ location: string;
6
+ };
7
+
8
+ export default defineComponent({
9
+ name: "ExampleBlogLoading",
10
+ props: {
11
+ routeId: {
12
+ type: String,
13
+ required: true,
14
+ },
15
+ location: {
16
+ type: String,
17
+ required: true,
18
+ },
19
+ },
20
+ setup(props: Pick<RouteLoadingProps, "routeId" | "location">) {
21
+ return () =>
22
+ h(
23
+ "article",
24
+ {
25
+ style:
26
+ "display: grid; gap: 12px; margin-top: 12px; padding: 18px; border-radius: 14px; background: #f5f5f2; border: 1px solid #dad8d1;",
27
+ },
28
+ [
29
+ h(
30
+ "h1",
31
+ {
32
+ style: "margin: 0;",
33
+ },
34
+ "Loading blog article...",
35
+ ),
36
+ h(
37
+ "p",
38
+ {
39
+ style: "margin: 0; color: #6c6c66;",
40
+ },
41
+ `Preparing ${props.location}`,
42
+ ),
43
+ ],
44
+ );
45
+ },
46
+ });
@@ -0,0 +1 @@
1
+ export default ["post-trace"];
@@ -0,0 +1,24 @@
1
+ import { defineComponent, h } from "vue";
2
+ import { useLoaderData, useRoute } from "phial";
3
+
4
+ interface RouteData {
5
+ middlewareTrace: string;
6
+ slug: string;
7
+ summary: string;
8
+ }
9
+
10
+ export default defineComponent({
11
+ name: "BlogPostPage",
12
+ setup() {
13
+ const route = useRoute();
14
+ const data = useLoaderData<RouteData>();
15
+
16
+ return () =>
17
+ h("section", null, [
18
+ h("h1", null, data.value?.slug ?? "Unknown post"),
19
+ h("p", null, data.value?.summary ?? ""),
20
+ h("p", null, `Middleware trace: ${data.value?.middlewareTrace ?? "missing"}`),
21
+ h("p", null, `Current route path: ${route.value.fullPath}`),
22
+ ]);
23
+ },
24
+ });
@@ -0,0 +1 @@
1
+ export default ["blog-trace"];
@@ -0,0 +1,36 @@
1
+ import { computed, defineComponent } from "vue";
2
+ import { RouterLink, useAppData, useRoute } from "phial";
3
+
4
+ interface AppData {
5
+ theme: "light" | "sepia";
6
+ }
7
+
8
+ export default defineComponent({
9
+ name: "JsxExamplePage",
10
+ setup() {
11
+ const route = useRoute();
12
+ const appData = useAppData<AppData>();
13
+ const theme = computed(() => appData.value?.theme ?? "sepia");
14
+
15
+ return () => (
16
+ <section>
17
+ <h1>Vue JSX example</h1>
18
+ <p>
19
+ This route is implemented with a <code>.tsx</code> file.
20
+ </p>
21
+ <ul>
22
+ <li>
23
+ Current path: <strong>{route.value.fullPath}</strong>
24
+ </li>
25
+ <li>
26
+ Theme from app loader: <strong>{theme.value}</strong>
27
+ </li>
28
+ </ul>
29
+ <p>
30
+ Navigate to the <RouterLink to="/sfc">SFC example</RouterLink> or back to the{" "}
31
+ <RouterLink to="/">h() example</RouterLink>.
32
+ </p>
33
+ </section>
34
+ );
35
+ },
36
+ });
@@ -0,0 +1,49 @@
1
+ import { computed, defineComponent, h } from "vue";
2
+ import { RouterLink, useAppData } from "phial";
3
+
4
+ interface AppData {
5
+ theme: "light" | "sepia";
6
+ requestedAt: string;
7
+ }
8
+
9
+ export default defineComponent({
10
+ name: "RootLayout",
11
+ setup(_, { slots }) {
12
+ const appData = useAppData<AppData>();
13
+ const requestedAt = computed(() => appData.value?.requestedAt ?? "");
14
+ const theme = computed(() => appData.value?.theme ?? "sepia");
15
+
16
+ return () =>
17
+ h(
18
+ "div",
19
+ {
20
+ style:
21
+ "font-family: ui-sans-serif, system-ui; margin: 0 auto; max-width: 720px; padding: 48px 24px; line-height: 1.6;",
22
+ },
23
+ [
24
+ h(
25
+ "header",
26
+ {
27
+ style:
28
+ "display: flex; gap: 16px; align-items: center; margin-bottom: 32px; flex-wrap: wrap;",
29
+ },
30
+ [
31
+ h("strong", null, "{{projectName}}"),
32
+ h(RouterLink, { to: "/" }, { default: () => "h()" }),
33
+ h(RouterLink, { to: "/jsx" }, { default: () => "JSX" }),
34
+ h(RouterLink, { to: "/sfc" }, { default: () => "SFC" }),
35
+ h(RouterLink, { to: "/blog/hello-world" }, { default: () => "Dynamic Route" }),
36
+ h(
37
+ "span",
38
+ {
39
+ style: "margin-left: auto; font-size: 13px; color: #6e665d;",
40
+ },
41
+ `theme=${theme.value} · shell=${requestedAt.value}`,
42
+ ),
43
+ ],
44
+ ),
45
+ h("main", null, slots.default?.()),
46
+ ],
47
+ );
48
+ },
49
+ });
@@ -0,0 +1,11 @@
1
+ export async function loader({ request }: { request: Request }) {
2
+ await new Promise((resolve) => setTimeout(resolve, 120));
3
+ const url = new URL(request.url);
4
+
5
+ return {
6
+ fromAppMiddleware: true,
7
+ requestPath: url.pathname,
8
+ message: "Hello from the root page loader.",
9
+ renderedAt: new Date().toISOString(),
10
+ };
11
+ }
@@ -0,0 +1,55 @@
1
+ import { computed, defineComponent, h } from "vue";
2
+
3
+ type RouteLoadingProps = {
4
+ routeId: string;
5
+ location: string;
6
+ previousLocation?: string;
7
+ action: string;
8
+ };
9
+
10
+ export default defineComponent({
11
+ name: "ExampleRootLoading",
12
+ props: {
13
+ routeId: {
14
+ type: String,
15
+ required: true,
16
+ },
17
+ location: {
18
+ type: String,
19
+ required: true,
20
+ },
21
+ previousLocation: {
22
+ type: String,
23
+ default: "",
24
+ },
25
+ action: {
26
+ type: String,
27
+ required: true,
28
+ },
29
+ },
30
+ setup(props: RouteLoadingProps) {
31
+ const description = computed(() => {
32
+ const previous = props.previousLocation ? ` from ${props.previousLocation}` : "";
33
+ return `Navigating to ${props.location}${previous} (${props.action})`;
34
+ });
35
+
36
+ return () =>
37
+ h(
38
+ "section",
39
+ {
40
+ style:
41
+ "margin-top: 24px; padding: 18px; border: 1px dashed #c9bda9; border-radius: 14px; background: #f7f1e8; color: #6a5843;",
42
+ },
43
+ [
44
+ h("strong", null, "Root layout loading..."),
45
+ h(
46
+ "p",
47
+ {
48
+ style: "margin: 10px 0 0;",
49
+ },
50
+ description.value,
51
+ ),
52
+ ],
53
+ );
54
+ },
55
+ });
@@ -0,0 +1,158 @@
1
+ import { computed, defineComponent, h } from "vue";
2
+ import {
3
+ useActionData,
4
+ useLoaderData,
5
+ useNavigation,
6
+ useRouteLoaderData,
7
+ useSubmit,
8
+ } from "phial";
9
+
10
+ interface LayoutLoaderData {
11
+ fromAppMiddleware: boolean;
12
+ message: string;
13
+ renderedAt: string;
14
+ requestPath: string;
15
+ }
16
+
17
+ interface LayoutActionData {
18
+ ok: boolean;
19
+ message: string;
20
+ submittedAt?: string;
21
+ }
22
+
23
+ export default defineComponent({
24
+ name: "HomePage",
25
+ setup() {
26
+ const data = useLoaderData();
27
+ const layoutData = useRouteLoaderData<LayoutLoaderData>("layout");
28
+ const actionData = useActionData();
29
+ const layoutActionData = useActionData<LayoutActionData>("layout");
30
+ const navigation = useNavigation();
31
+ const submit = useSubmit();
32
+ const renderedAt = computed(() => layoutData.value?.renderedAt ?? "");
33
+ const submittedAt = computed(() => layoutActionData.value?.submittedAt ?? "");
34
+ const feedbackTone = computed(() =>
35
+ layoutActionData.value?.ok === false ? "#8a2d1f" : "#1f5f3b",
36
+ );
37
+
38
+ async function handleSubmit(event: Event) {
39
+ const form = event.currentTarget as HTMLFormElement | null;
40
+ if (!form) {
41
+ return;
42
+ }
43
+
44
+ event.preventDefault();
45
+
46
+ try {
47
+ await submit(form);
48
+ } catch {
49
+ // Keep example errors local so the page shows inline action feedback.
50
+ }
51
+ }
52
+
53
+ return () =>
54
+ h("section", null, [
55
+ h("h1", null, "Vue h() example"),
56
+ h(
57
+ "p",
58
+ null,
59
+ "This homepage is implemented with a render function and also demonstrates loader/action data.",
60
+ ),
61
+ h("p", null, [
62
+ "Current page loader: ",
63
+ h("strong", null, data.value ? "available" : "none"),
64
+ ]),
65
+ h("p", null, layoutData.value?.message ?? ""),
66
+ h(
67
+ "p",
68
+ null,
69
+ `App middleware active: ${layoutData.value?.fromAppMiddleware ? "yes" : "no"}`,
70
+ ),
71
+ h("p", null, `Request path seen by middleware: ${layoutData.value?.requestPath ?? "/"}`),
72
+ h("p", null, `SSR time: ${renderedAt.value}`),
73
+ h("ul", null, [
74
+ h("li", null, "Visit /jsx for the Vue JSX example."),
75
+ h("li", null, "Visit /sfc for the Vue single-file component example."),
76
+ h("li", null, "Visit /blog/hello-world for the dynamic route example."),
77
+ ]),
78
+ h(
79
+ "form",
80
+ {
81
+ method: "post",
82
+ onSubmit: handleSubmit,
83
+ style:
84
+ "display: grid; gap: 12px; margin-top: 28px; padding: 18px; border: 1px solid #d9d0c2; border-radius: 14px; background: #fffdfa;",
85
+ },
86
+ [
87
+ h("strong", null, "Action demo"),
88
+ h(
89
+ "label",
90
+ {
91
+ style: "display: grid; gap: 6px;",
92
+ },
93
+ [
94
+ h("span", null, "Your name?"),
95
+ h("input", {
96
+ name: "name",
97
+ type: "text",
98
+ placeholder: "Ada Lovelace",
99
+ style:
100
+ "padding: 10px 12px; border: 1px solid #c9bda9; border-radius: 10px; font: inherit;",
101
+ }),
102
+ ],
103
+ ),
104
+ h(
105
+ "div",
106
+ {
107
+ style: "display: flex; gap: 12px; align-items: center; flex-wrap: wrap;",
108
+ },
109
+ [
110
+ h(
111
+ "button",
112
+ {
113
+ type: "submit",
114
+ disabled: navigation.isSubmitting.value,
115
+ style:
116
+ "padding: 10px 14px; border: 0; border-radius: 999px; background: #1b1b18; color: #f7f4ef; font: inherit; cursor: pointer;",
117
+ },
118
+ navigation.isSubmitting.value ? "Submitting..." : "Submit action",
119
+ ),
120
+ h(
121
+ "span",
122
+ {
123
+ style: "font-size: 14px; color: #6e665d;",
124
+ },
125
+ `Action state: ${navigation.isSubmitting.value ? "submitting" : layoutActionData.value ? "success" : "idle"}`,
126
+ ),
127
+ ],
128
+ ),
129
+ h(
130
+ "p",
131
+ {
132
+ style: "margin: 0; color: #6e665d;",
133
+ },
134
+ `Current page action data: ${actionData.value ? "available" : "none"}`,
135
+ ),
136
+ layoutActionData.value
137
+ ? h(
138
+ "p",
139
+ {
140
+ style: `margin: 0; color: ${feedbackTone.value};`,
141
+ },
142
+ layoutActionData.value.message,
143
+ )
144
+ : null,
145
+ layoutActionData.value?.submittedAt
146
+ ? h(
147
+ "p",
148
+ {
149
+ style: "margin: 0; font-size: 14px; color: #6e665d;",
150
+ },
151
+ `Submitted at: ${submittedAt.value}`,
152
+ )
153
+ : null,
154
+ ],
155
+ ),
156
+ ]);
157
+ },
158
+ });
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { RouterLink, useAppData, useRoute } from "phial";
4
+
5
+ const route = useRoute();
6
+ const appData = useAppData<{ theme: "light" | "sepia" }>();
7
+ const theme = computed(() => appData.value?.theme ?? "sepia");
8
+ </script>
9
+
10
+ <template>
11
+ <section>
12
+ <h1>Vue SFC example</h1>
13
+ <p>This route is implemented with a <code>.vue</code> file.</p>
14
+ <ul>
15
+ <li>
16
+ Current path: <strong>{{ route.value.fullPath }}</strong>
17
+ </li>
18
+ <li>
19
+ Theme from app loader: <strong>{{ theme }}</strong>
20
+ </li>
21
+ </ul>
22
+ <p>
23
+ Navigate to the <RouterLink to="/jsx">JSX example</RouterLink> or back to the
24
+ <RouterLink to="/">h() example</RouterLink>.
25
+ </p>
26
+ </section>
27
+ </template>
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "{{packageName}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "phial dev",
7
+ "build": "phial build",
8
+ "prepare": "phial prepare",
9
+ "start": "phial start",
10
+ "typecheck": "vue-tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@hornjs/fest": "^0.0.4",
14
+ "phial": "{{phialVersion}}",
15
+ "h3": "^1.15.0",
16
+ "vue": "^3.5.30",
17
+ "vue-router": "^4.6.4"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^24.11.0",
21
+ "typescript": "~5.9.3",
22
+ "vite": "^8.0.0",
23
+ "vue-tsc": "^3.2.5"
24
+ }
25
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "phial/vite-plugin";
2
+
3
+ export default defineConfig({
4
+ server: {
5
+ middleware: ["server-trace"],
6
+ },
7
+ dev: {
8
+ port: 3000,
9
+ },
10
+ });
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,3 @@
1
+ import { createContextKey } from "@hornjs/fest/utils";
2
+
3
+ export const serverTraceKey = createContextKey<string[]>([]);
@@ -0,0 +1,13 @@
1
+ import type { ServerMiddleware } from "@hornjs/fest";
2
+ import { serverTraceKey } from "../context";
3
+
4
+ const serverTraceRoute: ServerMiddleware = async (request, next) => {
5
+ const url = new URL(request.url);
6
+ const trace = request.context.get(serverTraceKey);
7
+ const nextTrace = [...trace, `route:${url.pathname}:${request.method}`];
8
+
9
+ request.context.set(serverTraceKey, nextTrace);
10
+ return next(request);
11
+ };
12
+
13
+ export default serverTraceRoute;
@@ -0,0 +1,13 @@
1
+ import type { ServerMiddleware } from "@hornjs/fest";
2
+ import { serverTraceKey } from "../context";
3
+
4
+ const serverTraceScope: ServerMiddleware = async (request, next) => {
5
+ const url = new URL(request.url);
6
+ const trace = request.context.get(serverTraceKey);
7
+ const nextTrace = [...trace, `directory:${url.pathname}:${request.method}`];
8
+
9
+ request.context.set(serverTraceKey, nextTrace);
10
+ return next(request);
11
+ };
12
+
13
+ export default serverTraceScope;
@@ -0,0 +1,13 @@
1
+ import type { ServerMiddleware } from "@hornjs/fest";
2
+ import { serverTraceKey } from "../context";
3
+
4
+ const serverTrace: ServerMiddleware = async (request, next) => {
5
+ const url = new URL(request.url);
6
+ const trace = request.context.get(serverTraceKey);
7
+ const nextTrace = [...trace, `global:${url.pathname}:${request.method}`];
8
+
9
+ request.context.set(serverTraceKey, nextTrace);
10
+ return next(request);
11
+ };
12
+
13
+ export default serverTrace;
@@ -0,0 +1 @@
1
+ export default ["server-trace-scope"];
@@ -0,0 +1,31 @@
1
+ import type { ServerRequest } from "@hornjs/fest";
2
+ import { serverTraceKey } from "../../context";
3
+
4
+ export default {
5
+ middleware: ["server-trace-route"],
6
+ meta: {
7
+ kind: "api",
8
+ },
9
+ GET(request: ServerRequest) {
10
+ const { searchParams } = new URL(request.url);
11
+ const trace = request.context.get(serverTraceKey);
12
+
13
+ return {
14
+ ok: true,
15
+ method: "GET",
16
+ query: searchParams.get("message") ?? null,
17
+ trace,
18
+ };
19
+ },
20
+ async POST(request: ServerRequest) {
21
+ const trace = request.context.get(serverTraceKey);
22
+ const body = await request.text();
23
+
24
+ return {
25
+ ok: true,
26
+ method: "POST",
27
+ body: body || null,
28
+ trace,
29
+ };
30
+ },
31
+ };
@@ -0,0 +1,9 @@
1
+ export default {
2
+ GET() {
3
+ return new Response("User-agent: *\nAllow: /\n", {
4
+ headers: {
5
+ "content-type": "text/plain; charset=utf-8",
6
+ },
7
+ });
8
+ },
9
+ };
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "moduleResolution": "bundler",
7
+ "jsx": "preserve",
8
+ "jsxImportSource": "vue",
9
+ "strict": true,
10
+ "skipLibCheck": true,
11
+ "esModuleInterop": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "types": ["node", "vite/client"]
16
+ },
17
+ "include": [
18
+ "app/**/*.ts",
19
+ "app/**/*.tsx",
20
+ "app/**/*.vue",
21
+ "server/**/*.ts",
22
+ ".phial/types/**/*.d.ts",
23
+ "phial.config.ts"
24
+ ],
25
+ "exclude": ["node_modules", ".output", "dist"]
26
+ }