@pauldvlp/vp-react-ts-nestjs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/index.js +188 -0
- package/package.json +46 -0
- package/template/README.md +122 -0
- package/template/_dockerignore +8 -0
- package/template/_gitignore +30 -0
- package/template/apps/api/Dockerfile +32 -0
- package/template/apps/api/_env.example +3 -0
- package/template/apps/api/_gitignore +8 -0
- package/template/apps/api/package.json +33 -0
- package/template/apps/api/src/app.module.ts +33 -0
- package/template/apps/api/src/common/zod-validation.pipe.ts +16 -0
- package/template/apps/api/src/config/config.module.ts +11 -0
- package/template/apps/api/src/config/env.ts +14 -0
- package/template/apps/api/src/health/health.controller.ts +9 -0
- package/template/apps/api/src/health/health.module.ts +8 -0
- package/template/apps/api/src/items/items.controller.ts +25 -0
- package/template/apps/api/src/items/items.module.ts +10 -0
- package/template/apps/api/src/items/items.service.spec.ts +26 -0
- package/template/apps/api/src/items/items.service.ts +24 -0
- package/template/apps/api/src/main.ts +30 -0
- package/template/apps/api/tsconfig.json +28 -0
- package/template/apps/api/vite.config.ts +35 -0
- package/template/apps/web/_gitignore +24 -0
- package/template/apps/web/index.html +13 -0
- package/template/apps/web/package.json +27 -0
- package/template/apps/web/public/favicon.svg +4 -0
- package/template/apps/web/src/App.tsx +48 -0
- package/template/apps/web/src/main.tsx +9 -0
- package/template/apps/web/tsconfig.app.json +31 -0
- package/template/apps/web/tsconfig.json +4 -0
- package/template/apps/web/tsconfig.node.json +23 -0
- package/template/apps/web/vite.config.ts +45 -0
- package/template/package.json +26 -0
- package/template/packages/contracts/package.json +24 -0
- package/template/packages/contracts/src/index.ts +18 -0
- package/template/packages/contracts/tsconfig.json +19 -0
- package/template/packages/contracts/vite.config.ts +20 -0
- package/template/pnpm-workspace.yaml +29 -0
- package/template/tsconfig.json +9 -0
- package/template/vite.config.ts +24 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
|
|
2
|
+
import { createItemSchema, type CreateItem, type Item } from '@app/contracts'
|
|
3
|
+
|
|
4
|
+
import { ZodValidationPipe } from '../common/zod-validation.pipe'
|
|
5
|
+
import { ItemsService } from './items.service'
|
|
6
|
+
|
|
7
|
+
@Controller('items')
|
|
8
|
+
export class ItemsController {
|
|
9
|
+
constructor(private readonly items: ItemsService) {}
|
|
10
|
+
|
|
11
|
+
@Get()
|
|
12
|
+
findAll(): Item[] {
|
|
13
|
+
return this.items.findAll()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Get(':id')
|
|
17
|
+
findOne(@Param('id') id: string): Item {
|
|
18
|
+
return this.items.findOne(id)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Post()
|
|
22
|
+
create(@Body(new ZodValidationPipe(createItemSchema)) body: CreateItem): Item {
|
|
23
|
+
return this.items.create(body)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Test } from '@nestjs/testing'
|
|
2
|
+
import { beforeEach, describe, expect, it } from 'vite-plus/test'
|
|
3
|
+
|
|
4
|
+
import { ItemsService } from './items.service'
|
|
5
|
+
|
|
6
|
+
// Building the module through Nest's DI container is the real proof that decorator metadata is being
|
|
7
|
+
// emitted by vite-plus's Oxc transform — without it, `Test.createTestingModule(...).compile()` would throw.
|
|
8
|
+
describe('ItemsService', () => {
|
|
9
|
+
let service: ItemsService
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
const moduleRef = await Test.createTestingModule({ providers: [ItemsService] }).compile()
|
|
13
|
+
service = moduleRef.get(ItemsService)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('creates and lists items', () => {
|
|
17
|
+
const created = service.create({ name: 'first' })
|
|
18
|
+
expect(created.id).toBeTruthy()
|
|
19
|
+
expect(service.findAll()).toHaveLength(1)
|
|
20
|
+
expect(service.findOne(created.id)).toEqual(created)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('throws for an unknown id', () => {
|
|
24
|
+
expect(() => service.findOne('nope')).toThrow()
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Injectable, NotFoundException } from '@nestjs/common'
|
|
2
|
+
import type { CreateItem, Item } from '@app/contracts'
|
|
3
|
+
|
|
4
|
+
/** A throwaway in-memory store so the template runs with zero infra. Swap for a real repository. */
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class ItemsService {
|
|
7
|
+
private readonly items: Item[] = []
|
|
8
|
+
|
|
9
|
+
findAll(): Item[] {
|
|
10
|
+
return this.items
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
findOne(id: string): Item {
|
|
14
|
+
const item = this.items.find((i) => i.id === id)
|
|
15
|
+
if (!item) throw new NotFoundException(`Item ${id} not found`)
|
|
16
|
+
return item
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
create(input: CreateItem): Item {
|
|
20
|
+
const item: Item = { id: crypto.randomUUID(), name: input.name, createdAt: new Date().toISOString() }
|
|
21
|
+
this.items.push(item)
|
|
22
|
+
return item
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import 'reflect-metadata'
|
|
2
|
+
|
|
3
|
+
import { NestFactory } from '@nestjs/core'
|
|
4
|
+
import { Logger } from 'nestjs-pino'
|
|
5
|
+
// __SWAGGER_IMPORT__
|
|
6
|
+
import { AppModule } from './app.module'
|
|
7
|
+
import { ENV, type Env } from './config/env'
|
|
8
|
+
|
|
9
|
+
async function bootstrap() {
|
|
10
|
+
const app = await NestFactory.create(AppModule, { bufferLogs: true })
|
|
11
|
+
app.useLogger(app.get(Logger))
|
|
12
|
+
app.setGlobalPrefix('api')
|
|
13
|
+
// __SWAGGER_SETUP__
|
|
14
|
+
const env = app.get<Env>(ENV)
|
|
15
|
+
await app.listen(env.PORT)
|
|
16
|
+
return app
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const app = bootstrap()
|
|
20
|
+
|
|
21
|
+
// Clean restart under `vite-node --watch`: `accept()` makes vite-node re-execute this module in place
|
|
22
|
+
// (a plain change would full-reload without disposing), and `dispose` closes the running server first
|
|
23
|
+
// so the next bootstrap() doesn't hit EADDRINUSE on the same port. `import.meta.hot` is undefined in
|
|
24
|
+
// the production SSR build, so this whole block is tree-shaken away there.
|
|
25
|
+
if (import.meta.hot) {
|
|
26
|
+
import.meta.hot.accept()
|
|
27
|
+
import.meta.hot.dispose(async () => {
|
|
28
|
+
await (await app).close()
|
|
29
|
+
})
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2022",
|
|
4
|
+
"lib": ["ES2023"],
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"types": ["node", "vite/client"],
|
|
9
|
+
|
|
10
|
+
/* NestJS needs decorators + metadata; param properties and decorators are emitted by vite-plus's
|
|
11
|
+
Oxc transform, so `erasableSyntaxOnly` / `verbatimModuleSyntax` are intentionally left off here. */
|
|
12
|
+
"experimentalDecorators": true,
|
|
13
|
+
"emitDecoratorMetadata": true,
|
|
14
|
+
|
|
15
|
+
"strict": true,
|
|
16
|
+
"strictPropertyInitialization": false,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"esModuleInterop": true,
|
|
19
|
+
"allowImportingTsExtensions": true,
|
|
20
|
+
"noEmit": true,
|
|
21
|
+
|
|
22
|
+
/* Linting */
|
|
23
|
+
"noUnusedLocals": true,
|
|
24
|
+
"noUnusedParameters": true,
|
|
25
|
+
"noFallthroughCasesInSwitch": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src", "vite.config.ts"]
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineConfig } from 'vite-plus'
|
|
2
|
+
|
|
3
|
+
// NestJS relies on `emitDecoratorMetadata` for its dependency injection. vite-plus's built-in Oxc
|
|
4
|
+
// transform emits it natively (driven by `experimentalDecorators` + `emitDecoratorMetadata` in
|
|
5
|
+
// tsconfig.json), so no extra transform plugin is needed — the api is a plain vite-plus package and
|
|
6
|
+
// `vp check` / `vp test` pick it up with zero wiring.
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
build: {
|
|
9
|
+
// SSR build: bundle the entry to dist/main.js and externalize node_modules (so Nest's optional
|
|
10
|
+
// dynamic requires aren't dragged into the bundle). The workspace contracts package is source
|
|
11
|
+
// TypeScript, so it must be inlined rather than externalized — see `ssr.noExternal` below.
|
|
12
|
+
ssr: 'src/main.ts',
|
|
13
|
+
outDir: 'dist',
|
|
14
|
+
target: 'node22'
|
|
15
|
+
},
|
|
16
|
+
ssr: {
|
|
17
|
+
noExternal: ['@app/contracts']
|
|
18
|
+
},
|
|
19
|
+
lint: {
|
|
20
|
+
plugins: ['typescript', 'oxc'],
|
|
21
|
+
rules: {
|
|
22
|
+
'vite-plus/prefer-vite-plus-imports': 'error'
|
|
23
|
+
},
|
|
24
|
+
options: {
|
|
25
|
+
typeAware: true,
|
|
26
|
+
typeCheck: true
|
|
27
|
+
},
|
|
28
|
+
jsPlugins: [
|
|
29
|
+
{
|
|
30
|
+
name: 'vite-plus',
|
|
31
|
+
specifier: 'vite-plus/oxlint-plugin'
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
dist-ssr
|
|
13
|
+
*.local
|
|
14
|
+
|
|
15
|
+
# Editor directories and files
|
|
16
|
+
.vscode/*
|
|
17
|
+
!.vscode/extensions.json
|
|
18
|
+
.idea
|
|
19
|
+
.DS_Store
|
|
20
|
+
*.suo
|
|
21
|
+
*.ntvs*
|
|
22
|
+
*.njsproj
|
|
23
|
+
*.sln
|
|
24
|
+
*.sw?
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>web</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@app/web",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vp dev",
|
|
8
|
+
"build": "tsc -b && vp build",
|
|
9
|
+
"lint": "vp lint",
|
|
10
|
+
"preview": "vp preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@app/contracts": "workspace:*",
|
|
14
|
+
"react": "^19.2.7",
|
|
15
|
+
"react-dom": "^19.2.7"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@rolldown/plugin-babel": "catalog:",
|
|
19
|
+
"@types/node": "^24.13.2",
|
|
20
|
+
"@types/react": "^19.2.17",
|
|
21
|
+
"@types/react-dom": "^19.2.3",
|
|
22
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
23
|
+
"typescript": "~6.0.2",
|
|
24
|
+
"vite": "catalog:",
|
|
25
|
+
"vite-plus": "catalog:"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Item } from '@app/contracts'
|
|
2
|
+
import { useEffect, useState, type FormEvent } from 'react'
|
|
3
|
+
|
|
4
|
+
// `/api/*` is proxied to the NestJS server in dev (see vite.config.ts), so these are same-origin calls.
|
|
5
|
+
const fetchItems = (): Promise<Item[]> => fetch('/api/items').then((res) => res.json())
|
|
6
|
+
|
|
7
|
+
const createItem = (name: string): Promise<Item> =>
|
|
8
|
+
fetch('/api/items', {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11
|
+
body: JSON.stringify({ name })
|
|
12
|
+
}).then((res) => res.json())
|
|
13
|
+
|
|
14
|
+
export const App = () => {
|
|
15
|
+
const [items, setItems] = useState<Item[]>([])
|
|
16
|
+
const [name, setName] = useState('')
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
void fetchItems().then(setItems)
|
|
20
|
+
}, [])
|
|
21
|
+
|
|
22
|
+
const add = async (event: FormEvent) => {
|
|
23
|
+
event.preventDefault()
|
|
24
|
+
if (!name.trim()) return
|
|
25
|
+
const created = await createItem(name.trim())
|
|
26
|
+
setItems((prev) => [...prev, created])
|
|
27
|
+
setName('')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<main style={{ fontFamily: 'system-ui, sans-serif', maxWidth: 480, margin: '4rem auto', padding: '0 1rem' }}>
|
|
32
|
+
<h1>Vite+ · React + NestJS</h1>
|
|
33
|
+
<p>A minimal full-stack starter. The list below is served by the NestJS api.</p>
|
|
34
|
+
|
|
35
|
+
<form onSubmit={add} style={{ display: 'flex', gap: 8, margin: '1.5rem 0' }}>
|
|
36
|
+
<input value={name} onChange={(event) => setName(event.target.value)} placeholder='New item name' style={{ flex: 1, padding: 8 }} />
|
|
37
|
+
<button type='submit'>Add</button>
|
|
38
|
+
</form>
|
|
39
|
+
|
|
40
|
+
<ul>
|
|
41
|
+
{items.map((item) => (
|
|
42
|
+
<li key={item.id}>{item.name}</li>
|
|
43
|
+
))}
|
|
44
|
+
</ul>
|
|
45
|
+
{items.length === 0 && <p style={{ opacity: 0.6 }}>No items yet — add one above.</p>}
|
|
46
|
+
</main>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "es2023",
|
|
5
|
+
"lib": ["ES2023", "DOM"],
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"types": ["vite-plus/client"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"jsx": "react-jsx",
|
|
17
|
+
|
|
18
|
+
/* Linting */
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
|
|
24
|
+
/* Aliases */
|
|
25
|
+
"paths": {
|
|
26
|
+
"@app/web/*": ["./src/*"],
|
|
27
|
+
"@app/contracts": ["../../packages/contracts/src/index.ts"]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"include": ["src"]
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "es2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"types": ["node"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"module": "nodenext",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
/* Linting */
|
|
17
|
+
"noUnusedLocals": true,
|
|
18
|
+
"noUnusedParameters": true,
|
|
19
|
+
"erasableSyntaxOnly": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true
|
|
21
|
+
},
|
|
22
|
+
"include": ["vite.config.ts"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import babel from '@rolldown/plugin-babel'
|
|
2
|
+
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
|
|
3
|
+
import { fileURLToPath, URL } from 'url'
|
|
4
|
+
import { defineConfig, lazyPlugins } from 'vite-plus'
|
|
5
|
+
|
|
6
|
+
// https://vite.dev/config/
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
server: {
|
|
9
|
+
port: __WEB_PORT__,
|
|
10
|
+
// Same-origin in dev: requests to `/api/*` are forwarded to the NestJS server, so no CORS and no
|
|
11
|
+
// absolute API URLs in the client code.
|
|
12
|
+
proxy: {
|
|
13
|
+
'/api': 'http://localhost:__API_PORT__'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
lint: {
|
|
17
|
+
plugins: ['react', 'typescript', 'oxc'],
|
|
18
|
+
rules: {
|
|
19
|
+
'react/rules-of-hooks': 'error',
|
|
20
|
+
'react/only-export-components': [
|
|
21
|
+
'warn',
|
|
22
|
+
{
|
|
23
|
+
allowConstantExport: true
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
'vite-plus/prefer-vite-plus-imports': 'error'
|
|
27
|
+
},
|
|
28
|
+
options: {
|
|
29
|
+
typeAware: true,
|
|
30
|
+
typeCheck: true
|
|
31
|
+
},
|
|
32
|
+
jsPlugins: [
|
|
33
|
+
{
|
|
34
|
+
name: 'vite-plus',
|
|
35
|
+
specifier: 'vite-plus/oxlint-plugin'
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
plugins: lazyPlugins(() => [react(), babel({ presets: [reactCompilerPreset()] })]),
|
|
40
|
+
resolve: {
|
|
41
|
+
alias: {
|
|
42
|
+
'@app/web': fileURLToPath(new URL('./src', import.meta.url))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vp run -r dev",
|
|
8
|
+
"build": "vp run -r build",
|
|
9
|
+
"check": "vp check",
|
|
10
|
+
"ready": "vp check && vp run -r build"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"babel-plugin-react-compiler": "catalog:",
|
|
14
|
+
"vite-plus": "catalog:"
|
|
15
|
+
},
|
|
16
|
+
"devEngines": {
|
|
17
|
+
"packageManager": {
|
|
18
|
+
"name": "pnpm",
|
|
19
|
+
"version": "11.9.0",
|
|
20
|
+
"onFail": "download"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.18.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@app/contracts",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"lint": "vp lint",
|
|
13
|
+
"check": "vp check"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"zod": "^3.25.76"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "catalog:",
|
|
20
|
+
"typescript": "catalog:",
|
|
21
|
+
"vite": "catalog:",
|
|
22
|
+
"vite-plus": "catalog:"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared API contracts. Both ends import from here so there is a single source of truth:
|
|
5
|
+
* the api validates request bodies against these schemas (via the ZodValidationPipe) and the
|
|
6
|
+
* web app uses the inferred types to type its `fetch` responses.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const itemSchema = z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
name: z.string().min(1),
|
|
12
|
+
createdAt: z.string()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const createItemSchema = itemSchema.pick({ name: true })
|
|
16
|
+
|
|
17
|
+
export type Item = z.infer<typeof itemSchema>
|
|
18
|
+
export type CreateItem = z.infer<typeof createItemSchema>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"types": ["vite-plus/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noUnusedParameters": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src", "vite.config.ts"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineConfig } from 'vite-plus'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
lint: {
|
|
5
|
+
plugins: ['typescript', 'oxc'],
|
|
6
|
+
rules: {
|
|
7
|
+
'vite-plus/prefer-vite-plus-imports': 'error'
|
|
8
|
+
},
|
|
9
|
+
options: {
|
|
10
|
+
typeAware: true,
|
|
11
|
+
typeCheck: true
|
|
12
|
+
},
|
|
13
|
+
jsPlugins: [
|
|
14
|
+
{
|
|
15
|
+
name: 'vite-plus',
|
|
16
|
+
specifier: 'vite-plus/oxlint-plugin'
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
packages:
|
|
2
|
+
- apps/*
|
|
3
|
+
- packages/*
|
|
4
|
+
|
|
5
|
+
catalogMode: prefer
|
|
6
|
+
|
|
7
|
+
catalog:
|
|
8
|
+
'@types/node': ^24
|
|
9
|
+
typescript: ^5
|
|
10
|
+
vite: npm:@voidzero-dev/vite-plus-core@latest
|
|
11
|
+
vitest: 4.1.9
|
|
12
|
+
vite-plus: ^0.2.1
|
|
13
|
+
babel-plugin-react-compiler: ^1.0.0
|
|
14
|
+
'@rolldown/plugin-babel': ^0.2.3
|
|
15
|
+
overrides:
|
|
16
|
+
vite: 'catalog:'
|
|
17
|
+
vitest: 'catalog:'
|
|
18
|
+
peerDependencyRules:
|
|
19
|
+
allowAny:
|
|
20
|
+
- vite
|
|
21
|
+
- vitest
|
|
22
|
+
allowedVersions:
|
|
23
|
+
vite: '*'
|
|
24
|
+
vitest: '*'
|
|
25
|
+
allowBuilds:
|
|
26
|
+
esbuild: true
|
|
27
|
+
# Telemetry-only postinstall pulled transitively via @nestjs/swagger -> swagger-ui-dist. Declared
|
|
28
|
+
# explicitly (false = do not run) so pnpm doesn't fail with ERR_PNPM_IGNORED_BUILDS.
|
|
29
|
+
'@scarf/scarf': false
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defaultExclude, defineConfig } from 'vite-plus'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
// vite-plus's defaultExclude is only node_modules + .git (no dist), so a root-level
|
|
6
|
+
// `vp test` would otherwise scan compiled build output. Keep build outputs out of the scan.
|
|
7
|
+
exclude: [...defaultExclude, '**/dist/**', '**/build/**']
|
|
8
|
+
},
|
|
9
|
+
fmt: {
|
|
10
|
+
jsxSingleQuote: true,
|
|
11
|
+
printWidth: 160,
|
|
12
|
+
semi: false,
|
|
13
|
+
singleQuote: true,
|
|
14
|
+
trailingComma: 'none'
|
|
15
|
+
},
|
|
16
|
+
lint: {
|
|
17
|
+
jsPlugins: [{ name: 'vite-plus', specifier: 'vite-plus/oxlint-plugin' }],
|
|
18
|
+
rules: { 'vite-plus/prefer-vite-plus-imports': 'error' },
|
|
19
|
+
options: { typeAware: true, typeCheck: true }
|
|
20
|
+
},
|
|
21
|
+
run: {
|
|
22
|
+
cache: true
|
|
23
|
+
}
|
|
24
|
+
})
|