@temir.ra/create-hono-spa 0.1.0-pre.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/CHANGELOG.md +5 -0
- package/README.md +214 -0
- package/buildinfo.txt +1 -0
- package/dist/cli.bundle.js +4 -0
- package/dist/cli.bundle.js.map +11 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +26 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +8 -0
- package/package.json +44 -0
- package/template/CHANGELOG.md +5 -0
- package/template/README.md +115 -0
- package/template/assets/@scope/package-name/favicon.svg +3 -0
- package/template/assets/@scope/package-name/images/mobile-screenshot.png +0 -0
- package/template/assets/@scope/package-name/images/wide-screenshot.png +0 -0
- package/template/assets/@scope/package-name/index.html +25 -0
- package/template/assets/@scope/package-name/manifest.webmanifest +26 -0
- package/template/gitignore +7 -0
- package/template/package.json +55 -0
- package/template/scripts/build-lib-bundle.ts +156 -0
- package/template/scripts/buildinfo.ts +25 -0
- package/template/scripts/cdn-rewrite-map.json +1 -0
- package/template/src/constants.ts +14 -0
- package/template/src/index.ts +0 -0
- package/template/src/spa.ts +135 -0
- package/template/tests/buildinfo.test.ts +23 -0
- package/template/tsconfig.build.json +19 -0
- package/template/tsconfig.json +37 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Introduction
|
|
2
|
+
|
|
3
|
+
A template for single-page applications distributed as Hono sub-app libraries. The generated package bundles a frontend shell (HTML, JS, CSS, PWA manifest, favicon) as static assets and exposes a `createSpa()` function that returns a `Hono` instance serving them. Intended to be consumed by a [`@temir.ra/create-hono-server`](https://www.npmjs.com/package/@temir.ra/create-hono-server) backend, where it is mounted as a route group.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Quick Start](#quick-start)
|
|
8
|
+
2. [AI Assistant Context](#ai-assistant-context)
|
|
9
|
+
3. [Documentation](#documentation)
|
|
10
|
+
1. [`template/`](#template)
|
|
11
|
+
2. [Architecture](#architecture)
|
|
12
|
+
3. [`src/spa.ts`](#srcspa-ts)
|
|
13
|
+
4. [`src/constants.ts`](#srcconstants-ts)
|
|
14
|
+
5. [`assets/`](#assets)
|
|
15
|
+
4. [DevOps](#devops)
|
|
16
|
+
1. [Change Management](#change-management)
|
|
17
|
+
2. [Publish](#publish)
|
|
18
|
+
1. [npmjs.org](#npmjsorg)
|
|
19
|
+
2. [Custom registry](#custom-registry)
|
|
20
|
+
|
|
21
|
+
# Quick Start
|
|
22
|
+
|
|
23
|
+
*`bun create` caches the template package - a newer published version will not be picked up automatically. Pin the version or clear the cache to use the latest.*
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# placeholder:
|
|
27
|
+
# <NEW_PACKAGE: <NEW_PACKAGE>
|
|
28
|
+
# <@_VERSION: <@_VERSION>
|
|
29
|
+
|
|
30
|
+
# identify the latest version of the template package as <@_VERSION.
|
|
31
|
+
bun info "@temir.ra/create-hono-spa" version
|
|
32
|
+
# create a new library from the template version
|
|
33
|
+
bun create --no-install --no-git "@temir.ra/hono-spa<@_VERSION>" <NEW_PACKAGE>
|
|
34
|
+
|
|
35
|
+
# or
|
|
36
|
+
|
|
37
|
+
# clear package manager cache to ensure the latest template version is used
|
|
38
|
+
bun pm cache rm
|
|
39
|
+
# create a new library from the latest template version
|
|
40
|
+
bun create --no-install --no-git "@temir.ra/hono-spa" <NEW_PACKAGE>
|
|
41
|
+
|
|
42
|
+
# dependencies must be installed manually
|
|
43
|
+
cd <NEW_PACKAGE>
|
|
44
|
+
bun install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
# AI Assistant Context
|
|
48
|
+
|
|
49
|
+
To generate an AI coding assistant context file for this project:
|
|
50
|
+
|
|
51
|
+
> Generate an AI coding assistant context file. Analyze the project structure and source files, using this README as the primary reference for architecture and conventions. Give particular attention to: the SPA-as-library abstraction and how assets are served via import.meta.url without any file copying on the consuming server, the `./spa` export condition and createSpa() as the library's public API, and the base href patching mechanism that enables client-side routing regardless of mount path.
|
|
52
|
+
|
|
53
|
+
# Documentation
|
|
54
|
+
|
|
55
|
+
The following sections explain the configurations and conventions baked into the generated package.
|
|
56
|
+
|
|
57
|
+
## `template/`
|
|
58
|
+
|
|
59
|
+
The files in `template/` were created from the [`@temir.ra/ts-lib@0.5.0`](https://www.npmjs.com/package/@temir.ra/create-ts-lib/v/0.5.0) template and then updated to fit the needs of a Hono SPA template.
|
|
60
|
+
|
|
61
|
+
```powershell
|
|
62
|
+
bun create --no-install --no-git --force "@temir.ra/ts-lib@0.5.0" "template/"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
[Official documentation](https://hono.dev/docs/) provided by the Hono team is a great resource.
|
|
66
|
+
|
|
67
|
+
## Architecture
|
|
68
|
+
|
|
69
|
+
The generated SPA package is a library - not a runnable server. It abstracts three things from the consuming server:
|
|
70
|
+
|
|
71
|
+
**Asset serving** - All frontend assets (HTML shell, JS bundle, CSS, PWA manifest, favicon) are resolved from the package directory via `import.meta.url` and served by the sub-app itself. The consuming server does not configure static file serving or copy any files.
|
|
72
|
+
|
|
73
|
+
**Client-side routing** - The `/app/*` wildcard returns `index.html` for every navigation path. The `<base href>` tag is patched at request time from `basePath` and `path`, so relative asset URLs resolve correctly wherever the sub-app is mounted.
|
|
74
|
+
|
|
75
|
+
**PWA scaffold** - The `assets/` directory ships with the structure expected by browsers for PWA installation: manifest, favicon, and screenshots.
|
|
76
|
+
|
|
77
|
+
The consuming server treats the package as an opaque route group passed to `endpointGroups`. See [`@temir.ra/create-hono-server`](https://www.npmjs.com/package/@temir.ra/create-hono-server) for the mount pattern.
|
|
78
|
+
|
|
79
|
+
## `src/spa.ts`
|
|
80
|
+
|
|
81
|
+
**Built-in routes:**
|
|
82
|
+
|
|
83
|
+
| Path | Description |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `/health` | Returns `ok` with status 200 |
|
|
86
|
+
| `/buildinfo` | Returns the contents of `buildinfo.txt` |
|
|
87
|
+
| `/app/*` | Serves `index.html` with runtime substitutions applied |
|
|
88
|
+
| `/<faviconPath>` | Serves `favicon.svg` as `image/svg+xml` |
|
|
89
|
+
| `/<webmanifestPath>` | Serves `manifest.webmanifest` as `application/manifest+json` |
|
|
90
|
+
| `/<indexJsPath>` | Serves `dist/index.bundle.js` |
|
|
91
|
+
| `/<indexCssPath>` | Serves `dist/index.css` |
|
|
92
|
+
| `/<siteCssPath>` | Serves `dist/site.css` |
|
|
93
|
+
|
|
94
|
+
**Options:**
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
type CreateSpaOptions = {
|
|
98
|
+
basePath?: string; // outer base path matching the server host's basePath
|
|
99
|
+
path?: string; // sub-path for this SPA within the server
|
|
100
|
+
metaDescriptionContent?: string; // replaces content="" in the description meta tag
|
|
101
|
+
faviconPath?: string; // serve path for the favicon (default: 'favicon.svg')
|
|
102
|
+
webmanifestPath?: string; // default: 'manifest.webmanifest'
|
|
103
|
+
vAppContainerId?: string; // id attribute of the root app container div
|
|
104
|
+
indexJsPath?: string; // default: 'index.js'
|
|
105
|
+
indexCssPath?: string; // default: 'index.css'
|
|
106
|
+
siteCssPath?: string; // default: 'site.css'
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## `src/constants.ts`
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
export const packageUrl = new URL('../', import.meta.url);
|
|
114
|
+
export const buildinfoUrl = new URL('buildinfo.txt', packageUrl);
|
|
115
|
+
export const distUrl = new URL('dist/', packageUrl);
|
|
116
|
+
export const indexJsUrl = new URL('index.bundle.js', distUrl);
|
|
117
|
+
export const assetsUrl = new URL('assets/@scope/package-name/', packageUrl);
|
|
118
|
+
export const indexHtmlUrl = new URL('index.html', assetsUrl);
|
|
119
|
+
// ...
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Replace `@scope/package-name` with your actual package name when adapting the template. Update `constants.ts` and rename the `assets/@scope/package-name/` directory to match.
|
|
123
|
+
|
|
124
|
+
## `assets/`
|
|
125
|
+
|
|
126
|
+
The template provides a PWA shell scaffold:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
assets/
|
|
130
|
+
└── @scope/
|
|
131
|
+
└── package-name/
|
|
132
|
+
├── index.html ← SPA entry point; base href patched at request time
|
|
133
|
+
├── favicon.svg
|
|
134
|
+
├── manifest.webmanifest
|
|
135
|
+
└── images/
|
|
136
|
+
├── mobile-screenshot.png ← used in manifest for PWA install UI
|
|
137
|
+
└── wide-screenshot.png
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`assets/` is included in the `files` field of `package.json` and published with the package. The naming convention (`assets/@scope/package-name/`) follows the asset resolution contract from [`@temir.ra/create-ts-lib`](https://www.npmjs.com/package/@temir.ra/create-ts-lib#asset-resolution) - scoped directories prevent naming collisions when multiple SPA packages coexist in the same server.
|
|
141
|
+
|
|
142
|
+
# DevOps
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# remove dist/ and tsconfig.build.tsbuildinfo
|
|
146
|
+
bun run clean
|
|
147
|
+
|
|
148
|
+
# remove dist/ only
|
|
149
|
+
bun run clean:dist
|
|
150
|
+
|
|
151
|
+
# remove tsconfig.build.tsbuildinfo only
|
|
152
|
+
bun run clean:tsbuildinfo
|
|
153
|
+
|
|
154
|
+
# compile + bundle
|
|
155
|
+
bun run build
|
|
156
|
+
|
|
157
|
+
# create a new test hono spa in example/
|
|
158
|
+
bun run dist/cli.bundle.js -- example
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Publish** - see [Publish](#publish).
|
|
162
|
+
|
|
163
|
+
## Change Management
|
|
164
|
+
|
|
165
|
+
1. Create a new branch for the change.
|
|
166
|
+
2. Make the changes and commit.
|
|
167
|
+
3. Bump the version in [`package.json`](package.json).
|
|
168
|
+
4. Add an entry for the new version in [`CHANGELOG.md`](CHANGELOG.md).
|
|
169
|
+
5. Pull request the branch.
|
|
170
|
+
6. After merge, run `bun run build` - ensures artifacts are current before publish.
|
|
171
|
+
7. Publish.
|
|
172
|
+
|
|
173
|
+
## Publish
|
|
174
|
+
|
|
175
|
+
See the following sources to configure the target registry and authentication.
|
|
176
|
+
|
|
177
|
+
- [Configuring npm - `npmrc`](https://docs.npmjs.com/cli/v10/configuring-npm/npmrc)
|
|
178
|
+
- [Bun package manager - `install.registry`](https://bun.com/docs/runtime/bunfig#install-scopes)
|
|
179
|
+
|
|
180
|
+
⚠️ Package Scope and the authentication for the target registry must be aligned.
|
|
181
|
+
|
|
182
|
+
### `npmjs.org`
|
|
183
|
+
|
|
184
|
+
Publish to the public npm registry.
|
|
185
|
+
|
|
186
|
+
```powershell
|
|
187
|
+
# authenticate
|
|
188
|
+
npm login
|
|
189
|
+
# publish
|
|
190
|
+
bun publish --registry https://registry.npmjs.org/ --access public
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Custom registry
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# placeholder:
|
|
197
|
+
# <SCOPE_WITHOUT_AT: <SCOPE_WITHOUT_AT>
|
|
198
|
+
# <REGISTRY_URL: <REGISTRY_URL>
|
|
199
|
+
# <BUN_PUBLISH_AUTH_TOKEN: <BUN_PUBLISH_AUTH_TOKEN>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`~/.bunfig.toml` or `bunfig.toml`:
|
|
203
|
+
|
|
204
|
+
```toml
|
|
205
|
+
[install.scopes]
|
|
206
|
+
"<SCOPE_WITHOUT_AT>" = { url = "<REGISTRY_URL>", token = "$BUN_PUBLISH_AUTH_TOKEN" }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```powershell
|
|
210
|
+
# authenticate
|
|
211
|
+
$env:BUN_PUBLISH_AUTH_TOKEN = "<BUN_PUBLISH_AUTH_TOKEN>"
|
|
212
|
+
# publish
|
|
213
|
+
bun publish
|
|
214
|
+
```
|
package/buildinfo.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0-pre.1+bad3cf6
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{cpSync as s,readFileSync as f,renameSync as u,writeFileSync as P}from"fs";import{resolve as t}from"path";import{resolve as o}from"path";import{fileURLToPath as h}from"url";var d=new URL("../",import.meta.url),n=o(h(d)),m=o(n,"template/"),p=o(n,"CHANGELOG.md"),g=o(n,"buildinfo.txt"),l=o(n,"README.md");try{let r=process.argv[2];if(!r)throw Error("Package name argument is required. Usage: `create-hono-spa <package-name>`");let a=r.replace(/\\/g,"/"),e=t(process.cwd(),a);s(m,e,{recursive:!0}),s(p,t(e,"CHANGELOG-template.md")),s(g,t(e,"buildinfo-template.txt")),s(l,t(e,"README-template.md"));let c=t(e,"package.json"),i=JSON.parse(f(c,"utf-8"));i.name=a,P(c,JSON.stringify(i,null,2)),u(t(e,"gitignore"),t(e,".gitignore")),console.log(`Template has been successfully instantiated at '${e}' with package name '${a}'.`)}catch(r){let a=r instanceof Error?r:Error(String(r));console.error("Error:",a.message),process.exit(1)}
|
|
3
|
+
|
|
4
|
+
//# debugId=22A62DDB3C86A9D764756E2164756E21
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["..\\src\\cli.ts", "..\\src\\constants.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"#!/usr/bin/env node\r\n\r\nimport { cpSync, readFileSync, renameSync, writeFileSync } from 'fs';\r\nimport { resolve, basename } from 'path';\r\nimport {\r\n templatePath,\r\n changelogPath,\r\n buildinfoPath,\r\n readmePath\r\n} from './constants.js';\r\n\r\n\r\ntry {\r\n\r\n const packageNameArgument = process.argv[2];\r\n if (!packageNameArgument) throw new Error('Package name argument is required. Usage: `create-hono-spa <package-name>`');\r\n const packageName = packageNameArgument.replace(/\\\\/g, '/');\r\n\r\n const destinationPath = resolve(process.cwd(), packageName);\r\n\r\n cpSync(templatePath, destinationPath, { recursive: true });\r\n cpSync(changelogPath, resolve(destinationPath, 'CHANGELOG-template.md'));\r\n cpSync(buildinfoPath, resolve(destinationPath, 'buildinfo-template.txt'));\r\n cpSync(readmePath, resolve(destinationPath, 'README-template.md'));\r\n\r\n const packageJsonPath = resolve(destinationPath, 'package.json');\r\n const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));\r\n packageJson.name = packageName;\r\n writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));\r\n\r\n renameSync(resolve(destinationPath, 'gitignore'), resolve(destinationPath, '.gitignore'));\r\n\r\n console.log(`Template has been successfully instantiated at '${destinationPath}' with package name '${packageName}'.`);\r\n\r\n}\r\ncatch (error) {\r\n const err = error instanceof Error ? error : new Error(String(error));\r\n console.error('Error:', err.message);\r\n process.exit(1);\r\n}\r\n",
|
|
6
|
+
"import { resolve } from 'path';\r\nimport { fileURLToPath } from 'url';\r\n\r\n\r\nexport const packageUrl: URL = new URL('../', import.meta.url);\r\nexport const packagePath: string = resolve(fileURLToPath(packageUrl));\r\n\r\nexport const templatePath: string = resolve(packagePath, 'template/');\r\n\r\nexport const changelogPath: string = resolve(packagePath, 'CHANGELOG.md');\r\nexport const buildinfoPath: string = resolve(packagePath, 'buildinfo.txt');\r\nexport const readmePath: string = resolve(packagePath, 'README.md');\r\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": ";AAEA,iBAAS,kBAAQ,gBAAc,mBAAY,WAC3C,kBAAS,aCHT,kBAAS,aACT,wBAAS,YAGF,IAAM,EAAkB,IAAI,IAAI,MAAO,YAAY,GAAG,EAChD,EAAsB,EAAQ,EAAc,CAAU,CAAC,EAEvD,EAAuB,EAAQ,EAAa,WAAW,EAEvD,EAAwB,EAAQ,EAAa,cAAc,EAC3D,EAAwB,EAAQ,EAAa,eAAe,EAC5D,EAAqB,EAAQ,EAAa,WAAW,EDClE,GAAI,CAEA,IAAM,EAAsB,QAAQ,KAAK,GACzC,GAAI,CAAC,EAAqB,MAAU,MAAM,4EAA4E,EACtH,IAAM,EAAc,EAAoB,QAAQ,MAAO,GAAG,EAEpD,EAAkB,EAAQ,QAAQ,IAAI,EAAG,CAAW,EAE1D,EAAO,EAAc,EAAiB,CAAE,UAAW,EAAK,CAAC,EACzD,EAAO,EAAe,EAAQ,EAAiB,uBAAuB,CAAC,EACvE,EAAO,EAAe,EAAQ,EAAiB,wBAAwB,CAAC,EACxE,EAAO,EAAY,EAAQ,EAAiB,oBAAoB,CAAC,EAEjE,IAAM,EAAkB,EAAQ,EAAiB,cAAc,EACzD,EAAc,KAAK,MAAM,EAAa,EAAiB,OAAO,CAAC,EACrE,EAAY,KAAO,EACnB,EAAc,EAAiB,KAAK,UAAU,EAAa,KAAM,CAAC,CAAC,EAEnE,EAAW,EAAQ,EAAiB,WAAW,EAAG,EAAQ,EAAiB,YAAY,CAAC,EAExF,QAAQ,IAAI,mDAAmD,yBAAuC,KAAe,EAGzH,MAAO,EAAO,CACV,IAAM,EAAM,aAAiB,MAAQ,EAAY,MAAM,OAAO,CAAK,CAAC,EACpE,QAAQ,MAAM,SAAU,EAAI,OAAO,EACnC,QAAQ,KAAK,CAAC",
|
|
9
|
+
"debugId": "22A62DDB3C86A9D764756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cpSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
3
|
+
import { resolve, basename } from 'path';
|
|
4
|
+
import { templatePath, changelogPath, buildinfoPath, readmePath } from './constants.js';
|
|
5
|
+
try {
|
|
6
|
+
const packageNameArgument = process.argv[2];
|
|
7
|
+
if (!packageNameArgument)
|
|
8
|
+
throw new Error('Package name argument is required. Usage: `create-hono-spa <package-name>`');
|
|
9
|
+
const packageName = packageNameArgument.replace(/\\/g, '/');
|
|
10
|
+
const destinationPath = resolve(process.cwd(), packageName);
|
|
11
|
+
cpSync(templatePath, destinationPath, { recursive: true });
|
|
12
|
+
cpSync(changelogPath, resolve(destinationPath, 'CHANGELOG-template.md'));
|
|
13
|
+
cpSync(buildinfoPath, resolve(destinationPath, 'buildinfo-template.txt'));
|
|
14
|
+
cpSync(readmePath, resolve(destinationPath, 'README-template.md'));
|
|
15
|
+
const packageJsonPath = resolve(destinationPath, 'package.json');
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
17
|
+
packageJson.name = packageName;
|
|
18
|
+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
19
|
+
renameSync(resolve(destinationPath, 'gitignore'), resolve(destinationPath, '.gitignore'));
|
|
20
|
+
console.log(`Template has been successfully instantiated at '${destinationPath}' with package name '${packageName}'.`);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
24
|
+
console.error('Error:', err.message);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const packageUrl: URL;
|
|
2
|
+
export declare const packagePath: string;
|
|
3
|
+
export declare const templatePath: string;
|
|
4
|
+
export declare const changelogPath: string;
|
|
5
|
+
export declare const buildinfoPath: string;
|
|
6
|
+
export declare const readmePath: string;
|
|
7
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,EAAE,GAAqC,CAAC;AAC/D,eAAO,MAAM,WAAW,EAAE,MAA2C,CAAC;AAEtE,eAAO,MAAM,YAAY,EAAE,MAA0C,CAAC;AAEtE,eAAO,MAAM,aAAa,EAAE,MAA6C,CAAC;AAC1E,eAAO,MAAM,aAAa,EAAE,MAA8C,CAAC;AAC3E,eAAO,MAAM,UAAU,EAAE,MAA0C,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
export const packageUrl = new URL('../', import.meta.url);
|
|
4
|
+
export const packagePath = resolve(fileURLToPath(packageUrl));
|
|
5
|
+
export const templatePath = resolve(packagePath, 'template/');
|
|
6
|
+
export const changelogPath = resolve(packagePath, 'CHANGELOG.md');
|
|
7
|
+
export const buildinfoPath = resolve(packagePath, 'buildinfo.txt');
|
|
8
|
+
export const readmePath = resolve(packagePath, 'README.md');
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@temir.ra/create-hono-spa",
|
|
3
|
+
"version": "0.1.0-pre.1",
|
|
4
|
+
"description": "SPA template with Hono server backend",
|
|
5
|
+
"author": "temir.ra",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"typescript",
|
|
9
|
+
"template",
|
|
10
|
+
"hono",
|
|
11
|
+
"spa",
|
|
12
|
+
"bun"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://git.chimps.quest/trs/create-hono-spa.git"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"imports": {
|
|
20
|
+
"#src/*.js": "./src/*.ts"
|
|
21
|
+
},
|
|
22
|
+
"bin": "./dist/cli.bundle.js",
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"buildinfo.txt",
|
|
27
|
+
"template"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"clean:dist": "rm -rf dist/",
|
|
31
|
+
"clean:tsbuildinfo": "rm -f tsconfig.build.tsbuildinfo",
|
|
32
|
+
"clean": "bun run clean:dist && bun run clean:tsbuildinfo",
|
|
33
|
+
"prebuild": "bun run scripts/buildinfo.ts",
|
|
34
|
+
"tests": "bun test",
|
|
35
|
+
"build": "bun run build:lib && bun run build:cli-bundle",
|
|
36
|
+
"build:lib": "tsc --project tsconfig.build.json",
|
|
37
|
+
"build:cli-bundle": "bun build src/cli.ts --entry-naming \"[dir]/[name].bundle.[ext]\" --outdir dist --target node --format esm --minify --sourcemap=external",
|
|
38
|
+
"typecheck": "tsc --noEmit"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/bun": "latest",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Introduction
|
|
2
|
+
|
|
3
|
+
*<INTRO TEXT>*
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Quick Start](#quick-start)
|
|
8
|
+
2. [AI Assistant Context](#ai-assistant-context)
|
|
9
|
+
3. [Documentation](#documentation)
|
|
10
|
+
4. [DevOps](#devops)
|
|
11
|
+
1. [Change Management](#change-management)
|
|
12
|
+
2. [Publish](#publish)
|
|
13
|
+
1. [npmjs.org](#npmjsorg)
|
|
14
|
+
2. [Custom registry](#custom-registry)
|
|
15
|
+
|
|
16
|
+
# Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# remove dist/ and tsconfig.build.tsbuildinfo
|
|
20
|
+
bun run clean
|
|
21
|
+
|
|
22
|
+
# remove dist/ only
|
|
23
|
+
bun run clean:dist
|
|
24
|
+
|
|
25
|
+
# remove tsconfig.build.tsbuildinfo only
|
|
26
|
+
bun run clean:tsbuildinfo
|
|
27
|
+
|
|
28
|
+
# compile + bundle
|
|
29
|
+
bun run build
|
|
30
|
+
|
|
31
|
+
# run tests
|
|
32
|
+
bun run tests
|
|
33
|
+
|
|
34
|
+
# run src/dev.ts in watch mode
|
|
35
|
+
bun run dev
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Publish** - see [Publish](#publish).
|
|
39
|
+
|
|
40
|
+
# AI Assistant Context
|
|
41
|
+
|
|
42
|
+
To generate an AI coding assistant context file for this project:
|
|
43
|
+
|
|
44
|
+
> Generate an AI coding assistant context file. `README-template.md` is available now and documents the architectural conventions of this package — use it as your primary source. The generated context file must be self-contained: `README-template.md` will be deleted after context generation, so do not reference it in the output. Give particular attention to: the dual tsconfig setup and the import constraints it imposes, the `#src/*.js` alias and where it may and may not be used, the `./spa` export condition and createSpa() as the library's public API, the asset resolution via import.meta.url and why it is reliable when the package is loaded externalized, and the base href patching mechanism for client-side routing.
|
|
45
|
+
|
|
46
|
+
# Documentation
|
|
47
|
+
|
|
48
|
+
*<DOCUMENTATION>*
|
|
49
|
+
|
|
50
|
+
## Asset Resolution
|
|
51
|
+
|
|
52
|
+
*<!-- Remove this section if the library has no runtime assets. Replace `@scope/lib-name` throughout with the actual package name. -->*
|
|
53
|
+
|
|
54
|
+
Assets are placed under `assets/@scope/lib-name/` (scoped to the package name to prevent collisions). They are resolved at runtime via `import.meta.url` and loaded with `fetch()` (browser) or `readFile` (Node). `assets/` must be listed in `files` in `package.json`.
|
|
55
|
+
|
|
56
|
+
`import.meta.url` is reliable when the library is loaded as a discrete module (from CDN or `node_modules/`). When the consumer bundles the library, they must copy the assets - see below.
|
|
57
|
+
|
|
58
|
+
### For consumers bundling this library
|
|
59
|
+
|
|
60
|
+
Copy `node_modules/@scope/lib-name/assets/` into your build output alongside the bundle, preserving the relative path. No code configuration is required - only the file copy.
|
|
61
|
+
|
|
62
|
+
# DevOps
|
|
63
|
+
|
|
64
|
+
## Change Management
|
|
65
|
+
|
|
66
|
+
1. Create a new branch for the change.
|
|
67
|
+
2. Make the changes and commit.
|
|
68
|
+
3. Bump the version in [`package.json`](package.json).
|
|
69
|
+
4. Add an entry for the new version in [`CHANGELOG.md`](CHANGELOG.md).
|
|
70
|
+
5. Pull request the branch.
|
|
71
|
+
6. After merge, run `bun run build` - ensures artifacts are current before publish.
|
|
72
|
+
7. Publish.
|
|
73
|
+
|
|
74
|
+
## Publish
|
|
75
|
+
|
|
76
|
+
See the following sources to configure the target registry and authentication.
|
|
77
|
+
|
|
78
|
+
- [Configuring npm - `npmrc`](https://docs.npmjs.com/cli/v10/configuring-npm/npmrc)
|
|
79
|
+
- [Bun package manager - `install.registry`](https://bun.com/docs/runtime/bunfig#install-scopes)
|
|
80
|
+
|
|
81
|
+
⚠️ Package Scope and the authentication for the target registry must be aligned.
|
|
82
|
+
|
|
83
|
+
### `npmjs.org`
|
|
84
|
+
|
|
85
|
+
Publish to the public npm registry.
|
|
86
|
+
|
|
87
|
+
```powershell
|
|
88
|
+
# authenticate
|
|
89
|
+
npm login
|
|
90
|
+
# publish
|
|
91
|
+
bun publish --registry https://registry.npmjs.org/ --access public
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Custom registry
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# placeholder:
|
|
98
|
+
# <SCOPE_WITHOUT_AT: <SCOPE_WITHOUT_AT>
|
|
99
|
+
# <REGISTRY_URL: <REGISTRY_URL>
|
|
100
|
+
# <BUN_PUBLISH_AUTH_TOKEN: <BUN_PUBLISH_AUTH_TOKEN>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`~/.bunfig.toml` or `bunfig.toml`:
|
|
104
|
+
|
|
105
|
+
```toml
|
|
106
|
+
[install.scopes]
|
|
107
|
+
"<SCOPE_WITHOUT_AT>" = { url = "<REGISTRY_URL>", token = "$BUN_PUBLISH_AUTH_TOKEN" }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```powershell
|
|
111
|
+
# authenticate
|
|
112
|
+
$env:BUN_PUBLISH_AUTH_TOKEN = "<BUN_PUBLISH_AUTH_TOKEN>"
|
|
113
|
+
# publish
|
|
114
|
+
bun publish
|
|
115
|
+
```
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<base href="/" />
|
|
3
|
+
|
|
4
|
+
<html lang="en">
|
|
5
|
+
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8" />
|
|
8
|
+
<meta name="description" content="" />
|
|
9
|
+
<meta name="viewport" content="width=device-width, interactive-widget=resizes-content" />
|
|
10
|
+
|
|
11
|
+
<link rel="icon" href="favicon.svg" sizes="any" type="image/svg+xml" />
|
|
12
|
+
<link rel="manifest" href="manifest.webmanifest" />
|
|
13
|
+
|
|
14
|
+
</head>
|
|
15
|
+
|
|
16
|
+
<body>
|
|
17
|
+
|
|
18
|
+
<div id="" class="v-app-container"></div>
|
|
19
|
+
<script type="module" src="index.js"></script>
|
|
20
|
+
<link rel="stylesheet" href="index.css" />
|
|
21
|
+
<link rel="stylesheet" href="site.css" />
|
|
22
|
+
|
|
23
|
+
</body>
|
|
24
|
+
|
|
25
|
+
</html>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "",
|
|
3
|
+
"short_name": "",
|
|
4
|
+
"id": "",
|
|
5
|
+
"start_url": "",
|
|
6
|
+
"display": "standalone",
|
|
7
|
+
"icons": [
|
|
8
|
+
{
|
|
9
|
+
"src": "favicon.svg",
|
|
10
|
+
"sizes": "192x192",
|
|
11
|
+
"type": "image/svg+xml"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"screenshots": [
|
|
15
|
+
{
|
|
16
|
+
"src": "images/wide-screenshot.png",
|
|
17
|
+
"type": "image/png",
|
|
18
|
+
"sizes": "1280x720"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"src": "images/mobile-screenshot.png",
|
|
22
|
+
"type": "image/png",
|
|
23
|
+
"sizes": "414x896"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "",
|
|
3
|
+
"version": "0.0.0+FE",
|
|
4
|
+
"description": "",
|
|
5
|
+
"author": "",
|
|
6
|
+
"license": "",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"typescript"
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": ""
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"entrypoint": "./src/index.ts",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"browser": "./dist/index.bundle.js",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./spa": {
|
|
23
|
+
"entrypoint": "./src/spa.ts",
|
|
24
|
+
"types": "./dist/spa.d.ts",
|
|
25
|
+
"browser": "./dist/spa.bundle.js",
|
|
26
|
+
"import": "./dist/spa.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"imports": {
|
|
30
|
+
"#src/*.js": "./src/*.ts"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"CHANGELOG.md",
|
|
35
|
+
"buildinfo.txt",
|
|
36
|
+
"assets"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"clean:dist": "rm -rf dist/",
|
|
40
|
+
"clean:tsbuildinfo": "rm -f tsconfig.build.tsbuildinfo",
|
|
41
|
+
"clean": "bun run clean:dist && bun run clean:tsbuildinfo",
|
|
42
|
+
"prebuild": "bun run scripts/buildinfo.ts",
|
|
43
|
+
"tests": "bun test",
|
|
44
|
+
"build": "bun run build:lib && bun run build:lib-bundle",
|
|
45
|
+
"build:lib": "tsc --project tsconfig.build.json",
|
|
46
|
+
"build:lib-bundle": "bun run scripts/build-lib-bundle.ts",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"dev": "bun run --watch src/dev.ts"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/bun": "latest",
|
|
52
|
+
"typescript": "^5.9.3",
|
|
53
|
+
"hono": "^4.12.5"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import CDN_REWRITE_MAP from './cdn-rewrite-map.json';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
interface ExportConditions {
|
|
7
|
+
[key: string]: string | undefined;
|
|
8
|
+
import?: string;
|
|
9
|
+
types?: string;
|
|
10
|
+
browser?: string;
|
|
11
|
+
entrypoint?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface DependencyMap {
|
|
15
|
+
[packageName: string]: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PackageManifest {
|
|
19
|
+
version: string;
|
|
20
|
+
exports?: Record<string, ExportConditions>;
|
|
21
|
+
dependencies?: DependencyMap;
|
|
22
|
+
devDependencies?: DependencyMap;
|
|
23
|
+
peerDependencies?: DependencyMap;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getManifest(packageIdentifier?: string): PackageManifest {
|
|
27
|
+
|
|
28
|
+
const manifestPath = packageIdentifier
|
|
29
|
+
? join('node_modules', packageIdentifier, 'package.json')
|
|
30
|
+
: 'package.json';
|
|
31
|
+
|
|
32
|
+
let manifest: PackageManifest;
|
|
33
|
+
try {
|
|
34
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
35
|
+
} catch {
|
|
36
|
+
if (packageIdentifier)
|
|
37
|
+
throw new Error("[scripts/build-lib-bundle.ts] Could not read manifest for '" + packageIdentifier + "' at '" + manifestPath + "'. Is it installed?");
|
|
38
|
+
else
|
|
39
|
+
throw new Error("[scripts/build-lib-bundle.ts] Could not read package manifest at '" + manifestPath + "'.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return manifest;
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getManifestEntrypoints(packageManifest: PackageManifest): string[] {
|
|
47
|
+
|
|
48
|
+
const exports = packageManifest.exports;
|
|
49
|
+
if (!exports) throw new Error('[scripts/build-lib-bundle.ts] No exports field found in package.json.');
|
|
50
|
+
|
|
51
|
+
const entrypoints = Object.entries(exports)
|
|
52
|
+
.map(([key, conditions]) => {
|
|
53
|
+
if (!conditions.entrypoint) throw new Error(`[scripts/build-lib-bundle.ts] Export '${key}' does not have an 'entrypoint' condition.`);
|
|
54
|
+
return conditions.entrypoint;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return entrypoints;
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getPackageVersion(manifest: PackageManifest, packageIdentifier?: string): string {
|
|
62
|
+
|
|
63
|
+
let version: string | undefined;
|
|
64
|
+
if (packageIdentifier) {
|
|
65
|
+
|
|
66
|
+
const dependencies = {
|
|
67
|
+
...manifest.dependencies,
|
|
68
|
+
...manifest.devDependencies,
|
|
69
|
+
...manifest.peerDependencies,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
version = dependencies[packageIdentifier];
|
|
73
|
+
if (!version) throw new Error(`[scripts/build-lib-bundle.ts] Package '${packageIdentifier}' is not listed in dependencies.`);
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
if (!manifest.version) throw new Error('[scripts/build-lib-bundle.ts] Package manifest does not contain a version field.');
|
|
78
|
+
version = manifest.version;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return version;
|
|
82
|
+
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveCdnUrl(importSpecifier: string, urlTemplate: string): string {
|
|
86
|
+
const manifest = getManifest();
|
|
87
|
+
const version = getPackageVersion(manifest, importSpecifier);
|
|
88
|
+
return urlTemplate.replace('<VERSION>', version);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const cdnRewritePlugin = {
|
|
92
|
+
name: 'cdn-rewrite',
|
|
93
|
+
setup(build: any) {
|
|
94
|
+
|
|
95
|
+
const resolved = new Map<string, string>();
|
|
96
|
+
for (const [importSpecifier, urlTemplate] of Object.entries(CDN_REWRITE_MAP) as [string, string][]) {
|
|
97
|
+
const url = resolveCdnUrl(importSpecifier, urlTemplate);
|
|
98
|
+
resolved.set(importSpecifier, url);
|
|
99
|
+
console.log(`[cdn-rewrite] '${importSpecifier}' → '${url}'`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
build.onResolve({ filter: /\*/ }, (args: any) => {
|
|
103
|
+
const url = resolved.get(args.path);
|
|
104
|
+
if (url) return { path: url, external: true };
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
const entrypoints = getManifestEntrypoints(getManifest());
|
|
112
|
+
console.log('[scripts/build-lib-bundle.ts] Entrypoints:', entrypoints);
|
|
113
|
+
|
|
114
|
+
let buildResult;
|
|
115
|
+
|
|
116
|
+
console.log('[scripts/build-lib-bundle.ts] Starting ESM bundle build...');
|
|
117
|
+
buildResult = await Bun.build({
|
|
118
|
+
entrypoints,
|
|
119
|
+
outdir: 'dist',
|
|
120
|
+
naming: '[dir]/[name].bundle.[ext]',
|
|
121
|
+
target: 'browser',
|
|
122
|
+
format: 'esm',
|
|
123
|
+
minify: true,
|
|
124
|
+
sourcemap: 'external',
|
|
125
|
+
plugins: [cdnRewritePlugin],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!buildResult.success) {
|
|
129
|
+
console.error('[scripts/build-lib-bundle.ts] Build failed:');
|
|
130
|
+
for (const message of buildResult.logs) {
|
|
131
|
+
console.error(message);
|
|
132
|
+
}
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
console.log('[scripts/build-lib-bundle.ts] ESM bundle build completed successfully.');
|
|
136
|
+
|
|
137
|
+
console.log('[scripts/build-lib-bundle.ts] Starting IIFE bundle build...');
|
|
138
|
+
buildResult = await Bun.build({
|
|
139
|
+
entrypoints,
|
|
140
|
+
outdir: 'dist',
|
|
141
|
+
naming: '[dir]/[name].iife.[ext]',
|
|
142
|
+
target: 'browser',
|
|
143
|
+
format: 'iife',
|
|
144
|
+
minify: true,
|
|
145
|
+
sourcemap: 'external',
|
|
146
|
+
plugins: [cdnRewritePlugin],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!buildResult.success) {
|
|
150
|
+
console.error('[scripts/build-lib-bundle.ts] Build failed:');
|
|
151
|
+
for (const message of buildResult.logs) {
|
|
152
|
+
console.error(message);
|
|
153
|
+
}
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
console.log('[scripts/build-lib-bundle.ts] IIFE bundle build completed successfully.');
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const BUILD_INFO_FILE = 'buildinfo.txt';
|
|
6
|
+
const GIT_COMMAND = 'git rev-parse --short HEAD';
|
|
7
|
+
|
|
8
|
+
const version: string = JSON.parse(readFileSync('package.json', 'utf-8')).version;
|
|
9
|
+
|
|
10
|
+
let gitHash = '';
|
|
11
|
+
try {
|
|
12
|
+
gitHash = execSync(GIT_COMMAND, { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
13
|
+
.toString()
|
|
14
|
+
.trim();
|
|
15
|
+
} catch { }
|
|
16
|
+
|
|
17
|
+
const buildinfo = gitHash
|
|
18
|
+
? version.includes('+')
|
|
19
|
+
? `${version}.${gitHash}`
|
|
20
|
+
: `${version}+${gitHash}`
|
|
21
|
+
: version;
|
|
22
|
+
|
|
23
|
+
writeFileSync(BUILD_INFO_FILE, buildinfo.trim(), 'utf-8');
|
|
24
|
+
|
|
25
|
+
console.log(`'${BUILD_INFO_FILE}' has been updated with build info: ${buildinfo}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
export const packageUrl: URL = new URL('../', import.meta.url);
|
|
3
|
+
|
|
4
|
+
export const buildinfoUrl: URL = new URL('buildinfo.txt', packageUrl);
|
|
5
|
+
|
|
6
|
+
export const distUrl: URL = new URL('dist/', packageUrl);
|
|
7
|
+
export const indexJsUrl: URL = new URL('index.bundle.js', distUrl);
|
|
8
|
+
export const indexCssUrl: URL = new URL('index.css', distUrl);
|
|
9
|
+
export const siteCssUrl: URL = new URL('site.css', distUrl);
|
|
10
|
+
|
|
11
|
+
export const assetsUrl: URL = new URL('assets/@scope/package-name/', packageUrl);
|
|
12
|
+
export const indexHtmlUrl: URL = new URL('index.html', assetsUrl);
|
|
13
|
+
export const webmanifestUrl: URL = new URL('manifest.webmanifest', assetsUrl);
|
|
14
|
+
export const faviconUrl: URL = new URL('favicon.svg', assetsUrl);
|
|
File without changes
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Hono, type Env } from 'hono';
|
|
2
|
+
import {
|
|
3
|
+
buildinfoUrl,
|
|
4
|
+
faviconUrl,
|
|
5
|
+
indexCssUrl,
|
|
6
|
+
indexHtmlUrl,
|
|
7
|
+
indexJsUrl,
|
|
8
|
+
siteCssUrl,
|
|
9
|
+
webmanifestUrl
|
|
10
|
+
} from './constants.js';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
type CreateSpaOptions = {
|
|
14
|
+
basePath?: string,
|
|
15
|
+
path?: string,
|
|
16
|
+
metaDescriptionContent?: string,
|
|
17
|
+
faviconPath?: string,
|
|
18
|
+
webmanifestPath?: string,
|
|
19
|
+
indexCssPath?: string,
|
|
20
|
+
vAppContainerId?: string,
|
|
21
|
+
indexJsPath?: string,
|
|
22
|
+
siteCssPath?: string,
|
|
23
|
+
}
|
|
24
|
+
export function createSpa<E extends Env>(options: CreateSpaOptions): Hono<E> {
|
|
25
|
+
|
|
26
|
+
const trimmedBasePath = options.basePath ? options.basePath.trim().replace(/^\/+/, '').replace(/\/+$/, '') : '';
|
|
27
|
+
const trimmedPath = options.path ? options.path.trim().replace(/^\/+/, '').replace(/\/+$/, '') : '';
|
|
28
|
+
const baseHref =
|
|
29
|
+
(trimmedBasePath ? `/${trimmedBasePath}/` : '/')
|
|
30
|
+
+
|
|
31
|
+
(trimmedPath ? `${trimmedPath}/` : '')
|
|
32
|
+
;
|
|
33
|
+
|
|
34
|
+
const spa = new Hono<E>().basePath(`/${trimmedPath}`);
|
|
35
|
+
|
|
36
|
+
spa.get('/health', (c) => c.text('OK'));
|
|
37
|
+
spa.get('/buildinfo', async (c) => {
|
|
38
|
+
try {
|
|
39
|
+
const buildInfo = await fetch(buildinfoUrl).then(res => res.text());
|
|
40
|
+
return c.text(buildInfo);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw error;
|
|
43
|
+
return c.status(500);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
spa.get('/app/*', async (c) => {
|
|
48
|
+
try {
|
|
49
|
+
|
|
50
|
+
let indexHtml = await fetch(indexHtmlUrl).then(res => res.text());
|
|
51
|
+
|
|
52
|
+
indexHtml = indexHtml.replace('<base href="/" />', `<base href="${baseHref}" />`);
|
|
53
|
+
|
|
54
|
+
if (options.metaDescriptionContent)
|
|
55
|
+
indexHtml = indexHtml.replace('<meta name="description" content="" />', `<meta name="description" content="${options.metaDescriptionContent}" />`);
|
|
56
|
+
|
|
57
|
+
if (options.faviconPath)
|
|
58
|
+
indexHtml = indexHtml.replace('<link rel="icon" href="favicon.svg" sizes="any" type="image/svg+xml" />', `<link rel="icon" href="${options.faviconPath}" sizes="any" type="image/svg+xml" />`);
|
|
59
|
+
|
|
60
|
+
if (options.webmanifestPath)
|
|
61
|
+
indexHtml = indexHtml.replace('<link rel="manifest" href="manifest.webmanifest" />', `<link rel="manifest" href="${options.webmanifestPath}" />`);
|
|
62
|
+
|
|
63
|
+
if (options.vAppContainerId)
|
|
64
|
+
indexHtml = indexHtml.replace('<div id="" class="v-app-container"></div>', `<div id="${options.vAppContainerId}" class="v-app-container"></div>`);
|
|
65
|
+
|
|
66
|
+
if (options.indexJsPath)
|
|
67
|
+
indexHtml = indexHtml.replace('<script type="module" src="index.js"></script>', `<script type="module" src="${options.indexJsPath}"></script>`);
|
|
68
|
+
|
|
69
|
+
if (options.indexCssPath)
|
|
70
|
+
indexHtml = indexHtml.replace('<link rel="stylesheet" href="index.css" />', `<link rel="stylesheet" href="${options.indexCssPath}" />`);
|
|
71
|
+
|
|
72
|
+
if (options.siteCssPath)
|
|
73
|
+
indexHtml = indexHtml.replace('<link rel="stylesheet" href="site.css" />', `<link rel="stylesheet" href="${options.siteCssPath}" />`);
|
|
74
|
+
|
|
75
|
+
return c.html(indexHtml);
|
|
76
|
+
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw error;
|
|
79
|
+
return c.status(500);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
spa.get(`/${options.faviconPath || 'favicon.svg'}`, async (c) => {
|
|
84
|
+
try {
|
|
85
|
+
const svg = await fetch(faviconUrl).then(res => res.text());
|
|
86
|
+
return c.text(svg, 200, { 'Content-Type': 'image/svg+xml' });
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw error;
|
|
89
|
+
return c.status(500);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
spa.get(`/${options.webmanifestPath || 'manifest.webmanifest'}`, async (c) => {
|
|
94
|
+
try {
|
|
95
|
+
const webmanifest = await fetch(webmanifestUrl).then(res => res.json());
|
|
96
|
+
return c.json(webmanifest, 200, { 'Content-Type': 'application/manifest+json', });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw error;
|
|
99
|
+
return c.status(500);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
spa.get(`/${options.indexJsPath || 'index.js'}`, async (c) => {
|
|
104
|
+
try {
|
|
105
|
+
const js = await fetch(indexJsUrl).then(res => res.text());
|
|
106
|
+
return c.text(js, 200, { 'Content-Type': 'application/javascript' });
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw error;
|
|
109
|
+
return c.status(500);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
spa.get(`/${options.indexCssPath || 'index.css'}`, async (c) => {
|
|
114
|
+
try {
|
|
115
|
+
const css = await fetch(indexCssUrl).then(res => res.text());
|
|
116
|
+
return c.text(css, 200, { 'Content-Type': 'text/css' });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw error;
|
|
119
|
+
return c.status(500);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
spa.get(`/${options.siteCssPath || 'site.css'}`, async (c) => {
|
|
124
|
+
try {
|
|
125
|
+
const css = await fetch(siteCssUrl).then(res => res.text());
|
|
126
|
+
return c.text(css, 200, { 'Content-Type': 'text/css' });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw error;
|
|
129
|
+
return c.status(500);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return spa;
|
|
134
|
+
|
|
135
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const buildinfoUrl = new URL('../buildinfo.txt', import.meta.url);
|
|
7
|
+
|
|
8
|
+
// taken from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
|
9
|
+
// Captures: [1]=major, [2]=minor, [3]=patch, [4]=pre-release, [5]=build-metadata
|
|
10
|
+
const SEMVER_REGEX =
|
|
11
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
12
|
+
|
|
13
|
+
function readBuildinfo(): string {
|
|
14
|
+
return readFileSync(fileURLToPath(buildinfoUrl), 'utf-8').trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('buildInfo', () => {
|
|
18
|
+
|
|
19
|
+
it('should be a valid semver string', () => {
|
|
20
|
+
expect(readBuildinfo()).toMatch(SEMVER_REGEX);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"module": "nodenext",
|
|
6
|
+
"moduleResolution": "nodenext",
|
|
7
|
+
"emitDeclarationOnly": false,
|
|
8
|
+
},
|
|
9
|
+
"include": [
|
|
10
|
+
"src/**/*.ts"
|
|
11
|
+
],
|
|
12
|
+
"exclude": [
|
|
13
|
+
"src/dev.ts",
|
|
14
|
+
"node_modules",
|
|
15
|
+
"dist",
|
|
16
|
+
"tests",
|
|
17
|
+
"scripts"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2022",
|
|
7
|
+
"DOM"
|
|
8
|
+
],
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"noUncheckedIndexedAccess": true,
|
|
13
|
+
"exactOptionalPropertyTypes": true,
|
|
14
|
+
"noImplicitOverride": true,
|
|
15
|
+
"isolatedDeclarations": true,
|
|
16
|
+
"esModuleInterop": true,
|
|
17
|
+
"composite": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"forceConsistentCasingInFileNames": true,
|
|
20
|
+
"resolveJsonModule": true,
|
|
21
|
+
"declaration": true,
|
|
22
|
+
"declarationMap": true,
|
|
23
|
+
"emitDeclarationOnly": true,
|
|
24
|
+
"outDir": "./dist",
|
|
25
|
+
"rootDir": ".",
|
|
26
|
+
},
|
|
27
|
+
"include": [
|
|
28
|
+
"src/**/*.ts",
|
|
29
|
+
"tests/**/*.ts",
|
|
30
|
+
"scripts/**/*.ts",
|
|
31
|
+
"scripts/**/*.json"
|
|
32
|
+
],
|
|
33
|
+
"exclude": [
|
|
34
|
+
"node_modules",
|
|
35
|
+
"dist"
|
|
36
|
+
]
|
|
37
|
+
}
|