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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/index.js +5 -0
- package/package.json +45 -0
- package/templates/default/README.md +54 -0
- package/templates/default/_gitignore +7 -0
- package/templates/default/app/app.config.ts +1 -0
- package/templates/default/app/app.vue +30 -0
- package/templates/default/app/assets/.gitkeep +1 -0
- package/templates/default/app/components/.gitkeep +1 -0
- package/templates/default/app/composables/.gitkeep +1 -0
- package/templates/default/app/error.vue +27 -0
- package/templates/default/app/loader.ts +8 -0
- package/templates/default/app/middleware/.gitkeep +1 -0
- package/templates/default/app/middleware/blog-trace.ts +7 -0
- package/templates/default/app/middleware/post-trace.ts +7 -0
- package/templates/default/app/middleware/request-meta.ts +7 -0
- package/templates/default/app/pages/action.ts +15 -0
- package/templates/default/app/pages/blog/[slug]/loader.ts +10 -0
- package/templates/default/app/pages/blog/[slug]/loading.ts +46 -0
- package/templates/default/app/pages/blog/[slug]/middleware.ts +1 -0
- package/templates/default/app/pages/blog/[slug]/page.ts +24 -0
- package/templates/default/app/pages/blog/_middleware.ts +1 -0
- package/templates/default/app/pages/jsx/page.tsx +36 -0
- package/templates/default/app/pages/layout.ts +49 -0
- package/templates/default/app/pages/loader.ts +11 -0
- package/templates/default/app/pages/loading.ts +55 -0
- package/templates/default/app/pages/page.ts +158 -0
- package/templates/default/app/pages/sfc/page.vue +27 -0
- package/templates/default/app/utils/.gitkeep +1 -0
- package/templates/default/package.json +25 -0
- package/templates/default/phial.config.ts +10 -0
- package/templates/default/public/.gitkeep +1 -0
- package/templates/default/server/context.ts +3 -0
- package/templates/default/server/middleware/server-trace-route.ts +13 -0
- package/templates/default/server/middleware/server-trace-scope.ts +13 -0
- package/templates/default/server/middleware/server-trace.ts +13 -0
- package/templates/default/server/routes/api/_middleware.ts +1 -0
- package/templates/default/server/routes/api/ping.ts +31 -0
- package/templates/default/server/routes/robots.txt.ts +9 -0
- 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 @@
|
|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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,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
|
+
}
|