@vitaliysimkin/create-t-platform-app 0.0.0-stage → 1.0.0-next.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/index.js +91 -0
- package/package.json +22 -4
- package/template/README.md +13 -0
- package/template/gitignore +6 -0
- package/template/index.html +12 -0
- package/template/package.json +33 -0
- package/template/server/App.Server.csproj +11 -0
- package/template/server/Program.cs +8 -0
- package/template/server/Properties/launchSettings.json +11 -0
- package/template/server/appsettings.json +5 -0
- package/template/src/extensions/dashboard/DashboardPage.test.ts +11 -0
- package/template/src/extensions/dashboard/contract.test.ts +5 -0
- package/template/src/extensions/dashboard/index.ts +18 -0
- package/template/src/extensions/dashboard/messages/en.ts +5 -0
- package/template/src/extensions/dashboard/mock.ts +6 -0
- package/template/src/extensions/dashboard/pages/DashboardPage.ts +17 -0
- package/template/src/extensions/index.ts +5 -0
- package/template/src/main.ts +18 -0
- package/template/tests/smoke.test.ts +13 -0
- package/template/tsconfig.json +19 -0
- package/template/vite.config.ts +6 -0
- package/template/vitest.config.ts +5 -0
- package/README.md +0 -3
package/index.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `npm create @vitaliysimkin/t-platform-app [dir]`: copies template/ (FE app + BE glue). Node >= 22.
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
6
|
+
import { createInterface } from 'node:readline/promises'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { parseArgs } from 'node:util'
|
|
9
|
+
|
|
10
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
const TEMPLATE = join(here, 'template')
|
|
12
|
+
const DEFAULT_SERVER_CORE = '../../t-platform/backend/src/TPlatform.Server/TPlatform.Server.csproj'
|
|
13
|
+
const fail = (msg) => {
|
|
14
|
+
console.error(`error: ${msg}`)
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { values: o, positionals } = parseArgs({
|
|
19
|
+
allowPositionals: true,
|
|
20
|
+
options: {
|
|
21
|
+
name: { type: 'string' },
|
|
22
|
+
title: { type: 'string' },
|
|
23
|
+
'core-source': { type: 'string' },
|
|
24
|
+
'server-core': { type: 'string' },
|
|
25
|
+
'no-install': { type: 'boolean', default: false },
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
let dir = positionals[0]
|
|
30
|
+
let { name, title } = o
|
|
31
|
+
if (process.stdin.isTTY && (!dir || !name || !title)) {
|
|
32
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
33
|
+
const ask = async (q, def) => (await rl.question(`${q} (${def}): `)).trim() || def
|
|
34
|
+
dir ??= await ask('Folder', 't-platform-app')
|
|
35
|
+
name ??= await ask('Name', basename(resolve(dir)))
|
|
36
|
+
title ??= await ask('Title', name)
|
|
37
|
+
rl.close()
|
|
38
|
+
}
|
|
39
|
+
dir ??= 't-platform-app'
|
|
40
|
+
name ??= basename(resolve(dir))
|
|
41
|
+
title ??= name
|
|
42
|
+
|
|
43
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name))
|
|
44
|
+
fail(`invalid name "${name}": use lowercase letters, digits and "-" (^[a-z][a-z0-9-]*$).`)
|
|
45
|
+
const target = resolve(dir)
|
|
46
|
+
if (existsSync(target) && readdirSync(target).length) fail(`${target}: folder is not empty.`)
|
|
47
|
+
|
|
48
|
+
const own = JSON.parse(readFileSync(join(here, 'package.json'), 'utf8')).version
|
|
49
|
+
const vars = {
|
|
50
|
+
__NAME__: name,
|
|
51
|
+
__TITLE__: title,
|
|
52
|
+
// exact pin: the core released with this scaffolder, or a local tarball/folder (tests)
|
|
53
|
+
__CORE__: o['core-source'] ? `file:${resolve(o['core-source'])}` : own,
|
|
54
|
+
__SERVER_CORE__: o['server-core'] ? resolve(o['server-core']) : DEFAULT_SERVER_CORE,
|
|
55
|
+
}
|
|
56
|
+
const fill = (s) =>
|
|
57
|
+
s.replace(/__(NAME|TITLE|CORE|SERVER_CORE)__/g, (k) =>
|
|
58
|
+
k === '__TITLE__' ? vars[k].replace(/['"<>&\\]/g, '') : vars[k],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
for (const f of walk(TEMPLATE)) {
|
|
62
|
+
// npm pack drops `.gitignore`, so the template keeps it as `gitignore`
|
|
63
|
+
const out = join(target, relative(TEMPLATE, f).replace(/(^|[\\/])gitignore$/, '$1.gitignore'))
|
|
64
|
+
mkdirSync(dirname(out), { recursive: true })
|
|
65
|
+
writeFileSync(out, fill(readFileSync(f, 'utf8')))
|
|
66
|
+
}
|
|
67
|
+
console.log(`created: ${target}`)
|
|
68
|
+
|
|
69
|
+
if (!o['no-install']) {
|
|
70
|
+
const r = spawnSync('npm', ['install'], {
|
|
71
|
+
cwd: target,
|
|
72
|
+
stdio: 'inherit',
|
|
73
|
+
shell: process.platform === 'win32',
|
|
74
|
+
})
|
|
75
|
+
if (r.status !== 0) fail('npm install failed.')
|
|
76
|
+
// agent material ships with the core: AGENTS.md and `.claude/skills/t-platform-*`
|
|
77
|
+
const agents = join(target, 'node_modules/@vitaliysimkin/t-platform-ui/agents')
|
|
78
|
+
if (!existsSync(agents)) fail(`${agents}: not found.`)
|
|
79
|
+
cpSync(join(agents, 'AGENTS.section.md'), join(target, 'AGENTS.md'))
|
|
80
|
+
for (const s of readdirSync(join(agents, 'skills')))
|
|
81
|
+
cpSync(join(agents, 'skills', s), join(target, '.claude/skills', `t-platform-${s}`), {
|
|
82
|
+
recursive: true,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
console.log(`Next:\n cd ${relative(process.cwd(), target) || '.'}\n npm run dev:mock`)
|
|
86
|
+
|
|
87
|
+
function walk(d) {
|
|
88
|
+
return readdirSync(d, { withFileTypes: true }).flatMap((e) =>
|
|
89
|
+
e.isDirectory() ? walk(join(d, e.name)) : [join(d, e.name)],
|
|
90
|
+
)
|
|
91
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitaliysimkin/create-t-platform-app",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0-next.0",
|
|
4
|
+
"description": "Create a t-platform service: FE app + small BE glue (`npm create @vitaliysimkin/t-platform-app`).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/vitaliysimkin/t-platform.git",
|
|
9
|
+
"directory": "ui/packages/create-app"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"create-t-platform-app": "index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"index.js",
|
|
19
|
+
"template"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# `__NAME__`
|
|
2
|
+
|
|
3
|
+
Frontend (`src/`, Vue + `@vitaliysimkin/t-platform-ui`) and backend glue (`server/`, .NET 10 + `TPlatform.Server`).
|
|
4
|
+
|
|
5
|
+
| Command | What |
|
|
6
|
+
| ----------------------------------------------------------------- | ------------------------------------------------- |
|
|
7
|
+
| `npm run dev:mock` | Frontend with the mock backend |
|
|
8
|
+
| `npm run server` + `npm run dev` | Real backend on :5168 + frontend (proxies `/api`) |
|
|
9
|
+
| `npm test`, `npm run typecheck`, `npm run check`, `npm run build` | Checks and build |
|
|
10
|
+
| `npx t-platform new extension <id>` | New extension (FE + BE part) |
|
|
11
|
+
|
|
12
|
+
`server/App.Server.csproj` references `TPlatform.Server` by path until it ships on NuGet: `TPlatformServerPath`, by default a `t-platform` clone next to this folder.
|
|
13
|
+
Override it with `dotnet build server -p:TPlatformServerPath=/path/to/TPlatform.Server.csproj`.
|
|
@@ -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>__TITLE__</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,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__NAME__",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"dev:mock": "vite --mode mock",
|
|
9
|
+
"server": "dotnet run --project server",
|
|
10
|
+
"build": "vite build",
|
|
11
|
+
"typecheck": "vue-tsc --noEmit",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"check": "t-platform check"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@iconify/vue": "^5.0.5",
|
|
17
|
+
"@vitaliysimkin/t-components": "^1.5.1",
|
|
18
|
+
"@vitaliysimkin/t-platform-ui": "__CORE__",
|
|
19
|
+
"@vueuse/core": "^14.4.0",
|
|
20
|
+
"vue": "^3.5.44",
|
|
21
|
+
"vue-i18n": "^11.4.18",
|
|
22
|
+
"vue-router": "^4.6.4"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22.0.0",
|
|
26
|
+
"@vue/test-utils": "^2.5.1",
|
|
27
|
+
"happy-dom": "^21.0.4",
|
|
28
|
+
"typescript": "~6.0.3",
|
|
29
|
+
"vite": "^8.3.5",
|
|
30
|
+
"vitest": "^5.1.0",
|
|
31
|
+
"vue-tsc": "^3.3.14"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
2
|
+
<PropertyGroup>
|
|
3
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
4
|
+
<Nullable>enable</Nullable>
|
|
5
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
6
|
+
<TPlatformServerPath Condition="'$(TPlatformServerPath)' == ''">__SERVER_CORE__</TPlatformServerPath>
|
|
7
|
+
</PropertyGroup>
|
|
8
|
+
<ItemGroup>
|
|
9
|
+
<ProjectReference Include="$(TPlatformServerPath)" />
|
|
10
|
+
</ItemGroup>
|
|
11
|
+
</Project>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { mountExtension } from '@vitaliysimkin/t-platform-ui/testing'
|
|
2
|
+
import { expect, it } from 'vitest'
|
|
3
|
+
import dashboard from './index'
|
|
4
|
+
|
|
5
|
+
it('dashboard page shows the server health from the mock', async () => {
|
|
6
|
+
const t = await mountExtension(dashboard)
|
|
7
|
+
expect(t.router.currentRoute.value.path).toBe('/dashboard')
|
|
8
|
+
expect(t.wrapper.find('h1').text()).toBe('Dashboard')
|
|
9
|
+
expect(t.wrapper.find('.dashboard-status').text()).toBe('Server is up.')
|
|
10
|
+
expect(t.http.last('GET', '/api/health')).toBeDefined()
|
|
11
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineExtension, defineExtensionStore } from '@vitaliysimkin/t-platform-ui'
|
|
2
|
+
import en from './messages/en'
|
|
3
|
+
|
|
4
|
+
export const useDashboard = defineExtensionStore('dashboard', (ctx) => ctx)
|
|
5
|
+
|
|
6
|
+
/** Frontend-only home page; it reads the core `GET /api/health`. */
|
|
7
|
+
export default defineExtension({
|
|
8
|
+
id: 'dashboard',
|
|
9
|
+
title: 'Dashboard',
|
|
10
|
+
icon: 'platform:grid',
|
|
11
|
+
home: { route: '/dashboard' },
|
|
12
|
+
messages: { en },
|
|
13
|
+
routes: [
|
|
14
|
+
{ path: '/dashboard', name: 'dashboard', component: () => import('./pages/DashboardPage') },
|
|
15
|
+
],
|
|
16
|
+
menu: [{ title: 'Dashboard', icon: 'platform:grid', route: '/dashboard', order: 0 }],
|
|
17
|
+
mock: () => import('./mock'),
|
|
18
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineComponent, h } from 'vue'
|
|
2
|
+
import { useDashboard } from '../index'
|
|
3
|
+
|
|
4
|
+
export default defineComponent({
|
|
5
|
+
name: 'DashboardPage',
|
|
6
|
+
setup() {
|
|
7
|
+
const ctx = useDashboard()
|
|
8
|
+
const health = ctx.resource(() => ctx.http.get<{ status: string }>('/api/health'), {
|
|
9
|
+
initial: { status: '' },
|
|
10
|
+
})
|
|
11
|
+
const status = () =>
|
|
12
|
+
health.error.value ??
|
|
13
|
+
(health.loaded.value ? ctx.t(health.data.value.status === 'ok' ? 'ok' : 'down') : '…')
|
|
14
|
+
return () =>
|
|
15
|
+
h('section', [h('h1', ctx.t('title')), h('p', { class: 'dashboard-status' }, status())])
|
|
16
|
+
},
|
|
17
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import '@vitaliysimkin/t-platform-ui/styles.css'
|
|
2
|
+
import { createPlatformApp, sessionCookieAuth } from '@vitaliysimkin/t-platform-ui'
|
|
3
|
+
import { extensions } from './extensions'
|
|
4
|
+
|
|
5
|
+
createPlatformApp({
|
|
6
|
+
name: '__NAME__',
|
|
7
|
+
title: '__TITLE__',
|
|
8
|
+
extensions,
|
|
9
|
+
auth: {
|
|
10
|
+
provider: sessionCookieAuth({
|
|
11
|
+
meUrl: '/api/me',
|
|
12
|
+
loginUrl: '/api/auth/login',
|
|
13
|
+
logoutUrl: '/api/auth/logout',
|
|
14
|
+
setupUrl: '/api/auth/setup',
|
|
15
|
+
}),
|
|
16
|
+
},
|
|
17
|
+
backendExtensions: {},
|
|
18
|
+
}).mount('#app')
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { mountExtension } from '@vitaliysimkin/t-platform-ui/testing'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import dashboard from '../src/extensions/dashboard'
|
|
4
|
+
import { extensions } from '../src/extensions'
|
|
5
|
+
|
|
6
|
+
describe.each(['desktop', 'mobile'] as const)('smoke (%s)', (viewport) => {
|
|
7
|
+
it('boots all extensions and opens the home page', async () => {
|
|
8
|
+
const others = extensions.filter((e) => e !== dashboard)
|
|
9
|
+
const t = await mountExtension(dashboard, { extensions: others, viewport, route: '/' })
|
|
10
|
+
// the home page (dashboard) is rendered at `/`
|
|
11
|
+
expect(t.wrapper.find('.dashboard-status').exists()).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"erasableSyntaxOnly": true,
|
|
9
|
+
"verbatimModuleSyntax": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"allowImportingTsExtensions": true,
|
|
16
|
+
"types": ["node", "vite/client"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src", "tests", "vite.config.ts", "vitest.config.ts"]
|
|
19
|
+
}
|
package/README.md
DELETED