@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 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-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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,6 @@
1
+ node_modules/
2
+ dist/
3
+ *.tsbuildinfo
4
+ server/bin/
5
+ server/obj/
6
+ server/data/
@@ -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,8 @@
1
+ using TPlatform.Server;
2
+
3
+ var builder = WebApplication.CreateBuilder(args);
4
+ builder.AddTPlatform();
5
+ // t-platform:extensions
6
+ var app = builder.Build();
7
+ app.MapTPlatform();
8
+ app.Run();
@@ -0,0 +1,11 @@
1
+ {
2
+ "profiles": {
3
+ "http": {
4
+ "commandName": "Project",
5
+ "applicationUrl": "http://localhost:5168",
6
+ "environmentVariables": {
7
+ "ASPNETCORE_ENVIRONMENT": "Development"
8
+ }
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "TPlatform": {
3
+ "DataRoot": "./data"
4
+ }
5
+ }
@@ -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,5 @@
1
+ import { expectValidExtension } from '@vitaliysimkin/t-platform-ui/testing'
2
+ import { it } from 'vitest'
3
+ import ext from './index'
4
+
5
+ it('dashboard: valid extension', () => expectValidExtension(ext))
@@ -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,5 @@
1
+ export default {
2
+ title: 'Dashboard',
3
+ ok: 'Server is up.',
4
+ down: 'Server is down.',
5
+ }
@@ -0,0 +1,6 @@
1
+ import { defineMock } from '@vitaliysimkin/t-platform-ui/mock'
2
+
3
+ // Frontend-only extension: mock paths are absolute (the core `GET /api/health`).
4
+ export default defineMock((m) => {
5
+ m.get('/api/health', () => ({ status: 'ok' }), { public: true })
6
+ })
@@ -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,5 @@
1
+ // <t-platform:extensions>
2
+ import dashboard from './dashboard'
3
+
4
+ export const extensions = [dashboard]
5
+ // </t-platform:extensions>
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ import { platform } from '@vitaliysimkin/t-platform-ui/vite'
2
+ import { defineConfig } from 'vite'
3
+
4
+ export default defineConfig({
5
+ plugins: [platform({ name: '__NAME__', apiProxy: 'http://localhost:5168' })],
6
+ })
@@ -0,0 +1,5 @@
1
+ import { platformVitest } from '@vitaliysimkin/t-platform-ui/testing/vitest'
2
+ import { mergeConfig } from 'vitest/config'
3
+ import viteConfig from './vite.config.ts'
4
+
5
+ export default mergeConfig(viteConfig, platformVitest())
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.