@lakutata/template-web-vue 3.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +9 -0
  3. package/features/i18n/packages/app/package.json +6 -0
  4. package/features/i18n/packages/app/src/locales/en.json +5 -0
  5. package/features/i18n/packages/app/src/locales/zh-CN.json +5 -0
  6. package/features/installer/Dockerfile.installer +10 -0
  7. package/features/installer/package.json +9 -0
  8. package/features/installer/packages/app/package.json +26 -0
  9. package/features/installer/packages/resources/logo.png +0 -0
  10. package/features/installer/packages/resources/package.json +6 -0
  11. package/package.json +23 -0
  12. package/template/Dockerfile +18 -0
  13. package/template/README.md +73 -0
  14. package/template/_dockerignore +10 -0
  15. package/template/_gitignore +13 -0
  16. package/template/package.json +30 -0
  17. package/template/packages/app/package.json +36 -0
  18. package/template/packages/app/src/App.ts +7 -0
  19. package/template/packages/app/src/config/Config.ts +67 -0
  20. package/template/packages/app/src/controllers/ExampleController.ts +32 -0
  21. package/template/packages/app/src/controllers/HealthController.ts +13 -0
  22. package/template/packages/app/src/dtos/GreetDTO.ts +7 -0
  23. package/template/packages/app/src/middlewares/Authenticate.ts +22 -0
  24. package/template/packages/app/src/tests/App.spec.ts +54 -0
  25. package/template/packages/app/tsconfig.json +9 -0
  26. package/template/packages/ui/index.html +12 -0
  27. package/template/packages/ui/package.json +21 -0
  28. package/template/packages/ui/src/App.vue +30 -0
  29. package/template/packages/ui/src/main.ts +4 -0
  30. package/template/packages/ui/tsconfig.json +15 -0
  31. package/template/packages/ui/vite.config.ts +12 -0
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-present Lakutata
4
+
5
+ All rights reserved
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # @lakutata/template-web-vue
2
+
3
+ The Vue web template of the [lakutata](https://github.com/lakutata/lakutata) projects: the service serves its HTTP API
4
+ under `/api` and a Vue single-page application built with Vite (`staticUI`), from one process, one port and one Docker
5
+ image. The project is a workspace: `packages/app` is the service, `packages/ui` the application.
6
+
7
+ ```bash
8
+ npx @lakutata/cli create --name my-app --template web-vue
9
+ ```
@@ -0,0 +1,6 @@
1
+ {
2
+ "scripts": {
3
+ "i18n:check": "lakutata i18n check --dir src/locales",
4
+ "i18n:types": "lakutata i18n types --dir src/locales"
5
+ }
6
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "greeting": {
3
+ "hello": "Hello, {name}!"
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "greeting": {
3
+ "hello": "你好,{name}!"
4
+ }
5
+ }
@@ -0,0 +1,10 @@
1
+ # The installers of Linux (npm run package:linux), made on Linux: the packages of the configuration (lakutata.installer
2
+ # of the package.json) in dist/installer. The ones of another architecture: docker buildx build --platform linux/amd64 ...
3
+ # The apk packages are made on Alpine (node:22-alpine), with Bun when the application has native addons.
4
+ FROM node:22-bookworm AS build
5
+ WORKDIR /project
6
+ COPY . .
7
+ RUN (npm ci --no-audit --no-fund || npm install --no-audit --no-fund) && npm run build && npm run package -w app -- --outdir /installer
8
+
9
+ FROM scratch
10
+ COPY --from=build /installer /
@@ -0,0 +1,9 @@
1
+ {
2
+ "scripts": {
3
+ "package:host": "npm run build && npm run package -w app -- --outdir ../../dist/installer",
4
+ "package:linux": "docker buildx build --file Dockerfile.installer --output type=local,dest=dist/installer ."
5
+ },
6
+ "devDependencies": {
7
+ "@lakutata/builder": "^{$LAKUTATA_VERSION}"
8
+ }
9
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "bin": {
3
+ "{$APP_ID}": "build/App.js"
4
+ },
5
+ "scripts": {
6
+ "package": "lakutata package installer"
7
+ },
8
+ "lakutata": {
9
+ "icon": "../resources/logo.png",
10
+ "installer": {
11
+ "name": "{$APP_ID}",
12
+ "service": {
13
+ "env": {
14
+ "PORT": "8080"
15
+ }
16
+ },
17
+ "linux": {
18
+ "targets": [
19
+ "deb",
20
+ "rpm",
21
+ "tar.gz"
22
+ ]
23
+ }
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "resources",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "description": "The resources of the application: its icon (logo.png, lakutata.icon of the app package), made into the icons of each platform"
6
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@lakutata/template-web-vue",
3
+ "version": "3.0.0-beta.0",
4
+ "description": "The Vue web template of the lakutata projects: an HTTP API serving a Vue single-page application built with Vite, deployed as a Docker image.",
5
+ "keywords": [
6
+ "lakutata",
7
+ "lakutata-template"
8
+ ],
9
+ "license": "MIT",
10
+ "files": [
11
+ "template",
12
+ "features",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "lakutata": {
17
+ "template": "web-vue",
18
+ "features": {
19
+ "i18n": "The messages in several languages (lakutata/com/i18n): src/locales, the language of each request",
20
+ "installer": "The installers of the application (lakutata package installer): deb, rpm and tar.gz on Linux (npm run package:linux in Docker), pkg and tar.gz on macOS, nsis, msi and zip on Windows, its service registered (systemd, launchd, WinSW), its program in the PATH"
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,18 @@
1
+ # The build: the dependencies and the sources, compiled
2
+ FROM node:22-alpine AS build
3
+ WORKDIR /project
4
+ COPY . .
5
+ RUN npm ci --no-audit --no-fund && npm run build && npm prune --omit=dev
6
+
7
+ # The image: the compiled service and its production dependencies, run by an unprivileged user
8
+ FROM node:22-alpine
9
+ ENV NODE_ENV=production PORT=8080
10
+ WORKDIR /project
11
+ COPY --from=build --chown=node:node /project /project
12
+ USER node
13
+ EXPOSE 8080
14
+ # Whether the process answers: the dependencies (database, message broker) are not part of it, the service stays up
15
+ # while they recover
16
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
17
+ CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/health').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"
18
+ CMD ["node", "packages/app/build/App.js"]
@@ -0,0 +1,73 @@
1
+ # {$APP_NAME}
2
+
3
+ {$APP_DESC}
4
+
5
+ A [lakutata](https://github.com/lakutata/lakutata) web application. The project is a workspace: `packages/app` is the
6
+ service, serving its API under `/api` and the Vue application of `packages/ui` (built with Vite); the other packages
7
+ are added in `packages/`.
8
+
9
+ ```bash
10
+ npm run dev # start the service and the Vite dev server, http://localhost:5173
11
+ npm test # build the packages and run their tests
12
+ npm run build # build the packages, in the order of their dependencies (ui, then app)
13
+ npm start # start the built service, http://localhost:8080
14
+ npm run version:patch # give the packages their next version
15
+ npm run docker:build # build the image {$APP_ID}:<version>
16
+ DOCKER_REPOSITORY=registry.example.com/team/{$APP_ID} npm run docker:push # build it for linux/amd64 and push it
17
+ ```
18
+
19
+ The results are `{errno, errMsg, data}`, the errors too (with the status 200). The requests carry
20
+ `Authorization: Bearer <API_TOKEN>` when the environment has an `API_TOKEN`
21
+ (`packages/app/src/middlewares/Authenticate.ts`). `/health` tells whether the process answers.
22
+
23
+ `docker login <registry>` once before pushing: the credentials are kept by Docker.
24
+
25
+ <!-- @if installer -->
26
+ ## The installers
27
+
28
+ `npm run package:host` makes the installers of the platform it runs on (`lakutata package installer`, configured by the
29
+ `lakutata.installer` of `packages/app/package.json`) in `dist/installer`: the application, its command
30
+ `{$APP_ID}` in the PATH, and its service started at the start of the system, with its environment (`PORT`, `MODE`…, kept by
31
+ the upgrades) and its data:
32
+
33
+ - Linux: the deb, rpm and tar.gz packages; the application in `/opt/{$APP_ID}`, its service run by systemd, its environment
34
+ `/etc/{$APP_ID}/{$APP_ID}.env` (`systemctl restart {$APP_ID}` after changing it), its data `/var/lib/{$APP_ID}`.
35
+ `npm run package:linux` makes them in Docker (`Dockerfile.installer`, in `dist/installer`) on any platform.
36
+ - macOS: the pkg package; the application in `/usr/local/lib/{$APP_ID}`, its service run by launchd, its environment
37
+ `/usr/local/etc/{$APP_ID}/{$APP_ID}.env` (`sudo /usr/local/lib/{$APP_ID}/uninstall` removes it).
38
+ - Windows: the installer of NSIS and the package of Windows Installer (msi); the application in
39
+ `C:\Program Files\{$APP_ID}`, its service run by WinSW, its environment `C:\ProgramData\{$APP_ID}\{$APP_ID}.env`
40
+ (`Restart-Service {$APP_ID}` after changing it).
41
+
42
+ <!-- @end -->
43
+ ## Database migrations
44
+
45
+ ```bash
46
+ npm run migration:generate # the migrations of the changes of the entities of the databases
47
+ npm run migration:create -- seed-users # an empty migration, written by hand
48
+ npm run migration:show # the migrations run [X] and pending [ ]
49
+ npm run migration:run # run the pending migrations
50
+ npm run migration:revert # revert the last migration run
51
+ ```
52
+
53
+ The commands (`lakutata migration <action>`) build and start the app, which connects its databases with their
54
+ options (without synchronizing their schemas), does the command and exits; `-- --database <name>` selects a database
55
+ (required to revert when there are several), `-- --fake` marks the migrations as run or reverted without executing
56
+ them. The migrations are written into `packages/app/src/migrations`, compiled with the app and given to the options of the
57
+ databases, run when they connect:
58
+
59
+ ```typescript
60
+ import {buildDatabaseOptions} from 'lakutata/com/database'
61
+ import {Migrations1788773616343} from '../migrations/1788773616343-db.js'
62
+
63
+ components: {
64
+ db: buildDatabaseOptions({
65
+ type: 'postgres',
66
+ url: process.env.DATABASE_URL,
67
+ entities: [/* the entities */],
68
+ synchronize: false,
69
+ migrationsRun: true,
70
+ migrations: [Migrations1788773616343]
71
+ })
72
+ }
73
+ ```
@@ -0,0 +1,10 @@
1
+ .git
2
+ node_modules
3
+ **/node_modules
4
+ **/build
5
+ *.log
6
+ .env
7
+ .env.*
8
+ **/.next
9
+ **/.nuxt
10
+ **/.output
@@ -0,0 +1,13 @@
1
+ node_modules
2
+ build
3
+ dist
4
+ *.log
5
+ .env
6
+ .env.*
7
+ .DS_Store
8
+ .idea
9
+ .vscode
10
+ .next
11
+ next-env.d.ts
12
+ .nuxt
13
+ .output
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "{$APP_ID}-workspace",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "workspaces": [
6
+ "packages/*"
7
+ ],
8
+ "scripts": {
9
+ "build": "lakutata run rebuild",
10
+ "test": "lakutata run test",
11
+ "dev": "lakutata run dev --parallel",
12
+ "migration:generate": "lakutata migration generate --package app",
13
+ "migration:create": "lakutata migration create --package app",
14
+ "migration:run": "lakutata migration run --package app",
15
+ "migration:revert": "lakutata migration revert --package app",
16
+ "migration:show": "lakutata migration show --package app",
17
+ "start": "npm start -w app",
18
+ "version:patch": "lakutata version patch --yes",
19
+ "docker:build": "docker buildx build --tag {$APP_ID}:$npm_package_version --load .",
20
+ "docker:push": "docker buildx build --platform linux/amd64 --tag $DOCKER_REPOSITORY:$npm_package_version --tag $DOCKER_REPOSITORY:latest --push ."
21
+ },
22
+ "devDependencies": {
23
+ "@lakutata/cli": "^{$LAKUTATA_VERSION}",
24
+ "@types/node": "^22.0.0",
25
+ "typescript": "^6.0.3"
26
+ },
27
+ "engines": {
28
+ "node": ">=22"
29
+ }
30
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "app",
3
+ "version": "0.0.1",
4
+ "description": "{$APP_DESC}",
5
+ "author": "{$APP_AUTHOR}",
6
+ "license": "{$APP_LICENSE}",
7
+ "private": true,
8
+ "type": "module",
9
+ "main": "build/App.js",
10
+ "scripts": {
11
+ "clean": "lakutata clean",
12
+ "build": "tsc",
13
+ "rebuild": "npm run clean && npm run build",
14
+ "dev": "npm run rebuild && node build/App.js",
15
+ "start": "node build/App.js",
16
+ "test": "node --test \"build/tests/**/*.spec.js\""
17
+ },
18
+ "dependencies": {
19
+ "compression": "^1.8.1",
20
+ "express": "^5.2.1",
21
+ "lakutata": "^{$LAKUTATA_VERSION}",
22
+ "ui": "^0.0.1"
23
+ },
24
+ "devDependencies": {
25
+ "@types/compression": "^1.8.1",
26
+ "@types/express": "^5.0.6"
27
+ },
28
+ "lakutata": {
29
+ "resources": [
30
+ {
31
+ "from": "../ui/dist",
32
+ "to": "ui"
33
+ }
34
+ ]
35
+ }
36
+ }
@@ -0,0 +1,7 @@
1
+ import {Application, type ILogger} from 'lakutata'
2
+ import {installerConfig} from './config/Config.js'
3
+
4
+ Application
5
+ .run(installerConfig)
6
+ .onLaunched((app: Application, log: ILogger): void => log.info('%s started in %s mode', app.appName, app.mode()))
7
+ .onFatalException((error: Error, log: ILogger): void => log.error('The service cannot start: %s', error.message))
@@ -0,0 +1,67 @@
1
+ import {resourcesPath, type ApplicationOptions} from 'lakutata'
2
+ import {buildEntrypoints} from 'lakutata/com/entrypoint'
3
+ import {expressHTTPEntrypoint, jsonEnvelope, staticUI} from 'lakutata/adapter/express'
4
+ import compression from 'compression'
5
+ import {existsSync} from 'node:fs'
6
+ import {createRequire} from 'node:module'
7
+ import path from 'node:path'
8
+ import {authenticate} from '../middlewares/Authenticate.js'
9
+ import {ExampleController} from '../controllers/ExampleController.js'
10
+ import {HealthController} from '../controllers/HealthController.js'
11
+ //@if i18n
12
+ import {buildI18nOptions, I18nMiddleware} from 'lakutata/com/i18n'
13
+ import en from '../locales/en.json' with {type: 'json'}
14
+ import zhCN from '../locales/zh-CN.json' with {type: 'json'}
15
+ //@end
16
+
17
+ /**
18
+ * The directory of the built application of the ui package, undefined before it is built (in development, Vite serves it):
19
+ * in the resources of the packed application (lakutata package bundle, lakutata package exe: the "lakutata.resources" of its
20
+ * package.json), else in the ui package
21
+ */
22
+ function uiDirectory(): string | undefined {
23
+ const resources: string | undefined = resourcesPath()
24
+ try {
25
+ const directory: string = resources ? path.join(resources, 'ui') : path.join(path.dirname(createRequire(import.meta.url).resolve('ui/package.json')), 'dist')
26
+ return existsSync(path.join(directory, 'index.html')) ? directory : undefined
27
+ } catch {
28
+ return undefined
29
+ }
30
+ }
31
+
32
+ /**
33
+ * The options of the service
34
+ * @param port the port of the HTTP server, the PORT environment variable or 8080 by default
35
+ */
36
+ export function installerConfig(port: number = Number(process.env.PORT ?? 8080)): ApplicationOptions {
37
+ return {
38
+ id: '{$APP_ID}',
39
+ name: '{$APP_NAME}',
40
+ mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
41
+ components: {
42
+ //@if i18n
43
+ //The messages of the languages (src/locales), bundled with the code; created by the first request
44
+ i18n: buildI18nOptions({messages: {'en': en, 'zh-CN': zhCN}}),
45
+ //@end
46
+ entrypoint: buildEntrypoints({
47
+ controllers: [HealthController, ExampleController],
48
+ //@if i18n
49
+ //The messages in the language of each request: ?lang=, the cookie lang, Accept-Language
50
+ middlewares: [I18nMiddleware],
51
+ //@end
52
+ http: expressHTTPEntrypoint({
53
+ port: port,
54
+ //The results and the errors are {errno, errMsg, data}, with the status 200
55
+ respond: jsonEnvelope({errorStatus: 200}),
56
+ body: {limit: '100mb'},
57
+ server: {keepAliveTimeout: 60000, headersTimeout: 65000},
58
+ middlewares: [authenticate],
59
+ before: (app): void => void app.use(compression()),
60
+ //The paths outside of the actions are the pages of the application
61
+ fallback: ((directory: string | undefined) => directory ? staticUI(directory) : undefined)(uiDirectory())
62
+ })
63
+ })
64
+ },
65
+ bootstrap: ['entrypoint']
66
+ }
67
+ }
@@ -0,0 +1,32 @@
1
+ import {Controller} from 'lakutata/com/entrypoint'
2
+ import {GET, Prefix} from 'lakutata/decorator/ctrl'
3
+ import {GreetDTO} from '../dtos/GreetDTO.js'
4
+ //@if i18n
5
+ import {Inject} from 'lakutata/decorator/di'
6
+ import {Translator} from 'lakutata/com/i18n'
7
+ //@end
8
+
9
+ @Prefix({route: '/api/example'})
10
+ export class ExampleController extends Controller {
11
+ //@if i18n
12
+
13
+ /**
14
+ * The messages of the language of the request
15
+ */
16
+ @Inject(Translator)
17
+ protected readonly i18n: Translator
18
+ //@end
19
+
20
+ /**
21
+ * GET /api/example?name=you
22
+ * @param input
23
+ */
24
+ @GET('/', GreetDTO)
25
+ public async greet(input: GreetDTO): Promise<{ message: string }> {
26
+ //@if i18n
27
+ return {message: this.i18n.t('greeting.hello', {name: input.name})}
28
+ //@else
29
+ return {message: `Hello, ${input.name}!`}
30
+ //@end
31
+ }
32
+ }
@@ -0,0 +1,13 @@
1
+ import {Controller} from 'lakutata/com/entrypoint'
2
+ import {GET} from 'lakutata/decorator/ctrl'
3
+
4
+ export class HealthController extends Controller {
5
+
6
+ /**
7
+ * GET /health: the process answers
8
+ */
9
+ @GET('/health')
10
+ public async health(): Promise<{ status: string }> {
11
+ return {status: 'ok'}
12
+ }
13
+ }
@@ -0,0 +1,7 @@
1
+ import {DTO} from 'lakutata'
2
+ import {Expect} from 'lakutata/decorator/dto'
3
+
4
+ export class GreetDTO extends DTO {
5
+ @Expect(DTO.String().optional().default('world').description('Who is greeted'))
6
+ public name: string
7
+ }
@@ -0,0 +1,22 @@
1
+ import {Exception} from 'lakutata'
2
+ import type {ActionInvocation, HTTPContext, Next} from 'lakutata/com/entrypoint'
3
+
4
+ export class UnauthorizedException extends Exception {
5
+ public errno: string = 'E_UNAUTHORIZED'
6
+
7
+ public readonly statusCode: number = 401
8
+ }
9
+
10
+ /**
11
+ * The requests carry "Authorization: Bearer <API_TOKEN>" when the environment has an API_TOKEN (the health check excepted).
12
+ * Replace it with the authentication of the service (a session, a JWT verified with the keys of the identity provider…)
13
+ * @param invocation
14
+ * @param next
15
+ */
16
+ export async function authenticate(invocation: ActionInvocation, next: Next): Promise<unknown> {
17
+ const token: string | undefined = process.env.API_TOKEN
18
+ const context: HTTPContext = invocation.context as HTTPContext
19
+ if (!token || context.route === '/health') return next()
20
+ if (context.request.headers.authorization !== `Bearer ${token}`) throw new UnauthorizedException('The request is not authenticated')
21
+ return next()
22
+ }
@@ -0,0 +1,54 @@
1
+ import {after, before, describe, it} from 'node:test'
2
+ import assert from 'node:assert'
3
+ import {Application} from 'lakutata'
4
+ import {installerConfig} from '../config/Config.js'
5
+
6
+ const PORT: number = Number(process.env.TEST_PORT ?? 18084)
7
+
8
+ describe('{$APP_NAME}', (): void => {
9
+
10
+ let app: Application
11
+
12
+ before(async (): Promise<void> => {
13
+ process.env.API_TOKEN = 'test-token'
14
+ app = await new Promise<Application>((resolve: (app: Application) => void): void => {
15
+ Application.run(installerConfig(PORT)).onLaunched((launched: Application): void => resolve(launched))
16
+ })
17
+ })
18
+
19
+ after(async (): Promise<void> => {
20
+ await app?.dispose()
21
+ })
22
+
23
+ it('answers the health check without authentication', async (): Promise<void> => {
24
+ assert.deepStrictEqual(await (await fetch(`http://127.0.0.1:${PORT}/health`)).json(), {errno: null, errMsg: null, data: {status: 'ok'}})
25
+ })
26
+
27
+ it('greets the authenticated requests', async (): Promise<void> => {
28
+ const response: Response = await fetch(`http://127.0.0.1:${PORT}/api/example?name=you`, {headers: {Authorization: 'Bearer test-token'}})
29
+ assert.deepStrictEqual(await response.json(), {errno: null, errMsg: null, data: {message: 'Hello, you!'}})
30
+ })
31
+ //@if i18n
32
+
33
+ it('greets in the language of the request, its validation errors too', async (): Promise<void> => {
34
+ const headers: Record<string, string> = {'Authorization': 'Bearer test-token', 'Accept-Language': 'zh-CN,zh;q=0.9'}
35
+ assert.deepStrictEqual(await (await fetch(`http://127.0.0.1:${PORT}/api/example?name=you`, {headers: headers})).json(), {errno: null, errMsg: null, data: {message: '你好,you!'}})
36
+ const invalid = await (await fetch(`http://127.0.0.1:${PORT}/api/example?name=`, {headers: headers})).json() as { errMsg: string }
37
+ assert.strictEqual(invalid.errMsg, '"name" 不能为空')
38
+ })
39
+ //@end
40
+
41
+ it('serves the pages of the application', async (): Promise<void> => {
42
+ for (const page of ['/', '/some/route']) {
43
+ const response: Response = await fetch(`http://127.0.0.1:${PORT}${page}`, {headers: {Accept: 'text/html'}})
44
+ assert.strictEqual(response.status, 200)
45
+ assert.match(await response.text(), /<div id="app"><\/div>/)
46
+ }
47
+ })
48
+
49
+ it('refuses the requests without authentication', async (): Promise<void> => {
50
+ const response: Response = await fetch(`http://127.0.0.1:${PORT}/api/example`)
51
+ assert.strictEqual(response.status, 200)
52
+ assert.deepStrictEqual(await response.json(), {errno: 'E_UNAUTHORIZED', errMsg: 'The request is not authenticated', data: null})
53
+ })
54
+ })
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "lakutata/tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "build",
6
+ "sourceMap": true
7
+ },
8
+ "include": ["src"]
9
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{$APP_NAME}</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "ui",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "clean": "lakutata clean dist",
8
+ "build": "vite build",
9
+ "rebuild": "npm run clean && npm run build",
10
+ "dev": "vite",
11
+ "typecheck": "vue-tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "vue": "^3.5.43"
15
+ },
16
+ "devDependencies": {
17
+ "@vitejs/plugin-vue": "^6.0.9",
18
+ "vite": "^8.3.1",
19
+ "vue-tsc": "^3.3.11"
20
+ }
21
+ }
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import {onMounted, ref} from 'vue'
3
+
4
+ /**
5
+ * The results of the API: {errno, errMsg, data}
6
+ */
7
+ type APIResult<T> = {
8
+ errno: string | null
9
+ errMsg: string | null
10
+ data: T
11
+ }
12
+
13
+ const message = ref<string>('…')
14
+
15
+ onMounted(async (): Promise<void> => {
16
+ try {
17
+ const result: APIResult<{ message: string }> = await (await fetch('/api/example?name={$APP_NAME}')).json()
18
+ message.value = result.errno ? `${result.errno}: ${result.errMsg}` : result.data.message
19
+ } catch (error) {
20
+ message.value = (error as Error).message
21
+ }
22
+ })
23
+ </script>
24
+
25
+ <template>
26
+ <main>
27
+ <h1>{$APP_NAME}</h1>
28
+ <p>{{ message }}</p>
29
+ </main>
30
+ </template>
@@ -0,0 +1,4 @@
1
+ import {createApp} from 'vue'
2
+ import App from './App.vue'
3
+
4
+ createApp(App).mount('#app')
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "preserve",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "isolatedModules": true,
12
+ "types": ["vite/client"]
13
+ },
14
+ "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
15
+ }
@@ -0,0 +1,12 @@
1
+ import {defineConfig} from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+
4
+ //In development, the API is the one of the service started by "npm run dev"
5
+ export default defineConfig({
6
+ plugins: [vue()],
7
+ server: {
8
+ proxy: {
9
+ '/api': `http://127.0.0.1:${process.env.PORT ?? 8080}`
10
+ }
11
+ }
12
+ })