@ericminassian/plat 0.1.0 → 0.2.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/dist/build.d.ts +4 -1
- package/dist/build.js +4 -7
- package/dist/build.js.map +1 -1
- package/dist/commands/common.d.ts +2 -2
- package/dist/commands/common.js +2 -2
- package/dist/commands/common.js.map +1 -1
- package/dist/commands/lifecycle.js +4 -3
- package/dist/commands/lifecycle.js.map +1 -1
- package/dist/commands/manage.js +8 -8
- package/dist/commands/manage.js.map +1 -1
- package/dist/commands/observe.js +5 -5
- package/dist/commands/observe.js.map +1 -1
- package/dist/deploy.js +2 -2
- package/dist/deploy.js.map +1 -1
- package/dist/dev/crons.d.ts +1 -1
- package/dist/dev/index.js +1 -1
- package/dist/dev/index.js.map +1 -1
- package/dist/dev/local-stage.js +1 -1
- package/dist/dev/router.js +1 -1
- package/dist/dev/router.js.map +1 -1
- package/dist/dev/test.js +1 -1
- package/dist/dev/test.js.map +1 -1
- package/dist/init.d.ts +7 -0
- package/dist/init.js +15 -3
- package/dist/init.js.map +1 -1
- package/dist/project.d.ts +6 -7
- package/dist/project.js +78 -21
- package/dist/project.js.map +1 -1
- package/dist/util/esbuild.d.ts +2 -0
- package/dist/util/esbuild.js +9 -0
- package/dist/util/esbuild.js.map +1 -0
- package/package.json +4 -4
- package/src/build.ts +7 -10
- package/src/commands/common.ts +3 -3
- package/src/commands/lifecycle.ts +4 -3
- package/src/commands/manage.ts +8 -8
- package/src/commands/observe.ts +5 -5
- package/src/deploy.ts +2 -2
- package/src/dev/crons.ts +1 -1
- package/src/dev/index.ts +1 -1
- package/src/dev/local-stage.ts +1 -1
- package/src/dev/router.ts +1 -1
- package/src/dev/test.ts +1 -1
- package/src/init.ts +16 -3
- package/src/project.ts +76 -22
- package/src/util/esbuild.ts +10 -0
- package/template/AGENTS.md +23 -18
- package/template/plat.config.ts +31 -0
- package/template/src/server.ts +3 -3
- package/template/src/worker.ts +5 -5
- package/template/tsconfig.json +1 -1
- package/template/platform.json +0 -18
package/src/project.ts
CHANGED
|
@@ -1,47 +1,64 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import {
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import {
|
|
6
|
+
APP_CONFIG_FILE,
|
|
7
|
+
ManifestError,
|
|
8
|
+
parseManifest,
|
|
9
|
+
type ResolvedManifest,
|
|
10
|
+
} from '@ericminassian/platform-core';
|
|
11
|
+
import { build as esbuild } from 'esbuild';
|
|
12
|
+
import { buildFailureList } from './util/esbuild.ts';
|
|
6
13
|
import { CliError, EXIT } from './util/output.ts';
|
|
7
14
|
|
|
8
|
-
|
|
15
|
+
/** Apps configured with JSON before `plat.config.ts`; only detected to say how to move. */
|
|
16
|
+
const LEGACY_MANIFEST_FILE = 'platform.json';
|
|
9
17
|
|
|
10
18
|
export interface Project {
|
|
11
|
-
/** Directory containing
|
|
19
|
+
/** Directory containing plat.config.ts. */
|
|
12
20
|
root: string;
|
|
13
|
-
/**
|
|
21
|
+
/** The config's default export as JSON: exactly what is validated, built and deployed. */
|
|
14
22
|
raw: Record<string, unknown>;
|
|
15
23
|
manifest: ResolvedManifest;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
|
-
/** Walk up from `cwd` to the nearest
|
|
19
|
-
|
|
26
|
+
/** Walk up from `cwd` to the nearest directory containing `name`. */
|
|
27
|
+
function findUp(cwd: string, name: string): string | null {
|
|
20
28
|
let dir = resolve(cwd);
|
|
21
29
|
for (;;) {
|
|
22
|
-
if (existsSync(join(dir,
|
|
30
|
+
if (existsSync(join(dir, name))) return dir;
|
|
23
31
|
const parent = dirname(dir);
|
|
24
32
|
if (parent === dir) return null;
|
|
25
33
|
dir = parent;
|
|
26
34
|
}
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
/** Walk up from `cwd` to the nearest plat.config.ts. */
|
|
38
|
+
export function findProjectRoot(cwd: string): string | null {
|
|
39
|
+
return findUp(cwd, APP_CONFIG_FILE);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function loadProject(cwd: string): Promise<Project> {
|
|
30
43
|
const root = findProjectRoot(cwd);
|
|
31
44
|
if (!root) {
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
const legacy = findUp(cwd, LEGACY_MANIFEST_FILE);
|
|
46
|
+
throw new CliError(`no ${APP_CONFIG_FILE} found in ${cwd} or its parents`, {
|
|
47
|
+
hint: legacy
|
|
48
|
+
? `${join(legacy, LEGACY_MANIFEST_FILE)} is no longer read: move its fields into ${APP_CONFIG_FILE} as \`export default defineApp({ ... })\` from '@ericminassian/platform/config'`
|
|
49
|
+
: 'run this inside an app, or create one with `plat init <dir> --name <app>`',
|
|
34
50
|
exitCode: EXIT.usage,
|
|
35
51
|
});
|
|
36
52
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
throw new CliError(`${MANIFEST_FILE} is not valid JSON: ${(err as Error).message}`, {
|
|
53
|
+
const exported = await importConfig(root);
|
|
54
|
+
if (exported === null || typeof exported !== 'object' || Array.isArray(exported)) {
|
|
55
|
+
throw new CliError(`${APP_CONFIG_FILE} must default-export the app config`, {
|
|
56
|
+
hint: "export default defineApp({ name: '...' }) from '@ericminassian/platform/config'",
|
|
42
57
|
exitCode: EXIT.usage,
|
|
43
58
|
});
|
|
44
59
|
}
|
|
60
|
+
// The platform receives JSON, so validate (and later deploy) the JSON form.
|
|
61
|
+
const raw = JSON.parse(JSON.stringify(exported)) as Record<string, unknown>;
|
|
45
62
|
try {
|
|
46
63
|
return { root, raw, manifest: parseManifest(raw) };
|
|
47
64
|
} catch (err) {
|
|
@@ -50,17 +67,54 @@ export function loadProject(cwd: string): Project {
|
|
|
50
67
|
}
|
|
51
68
|
}
|
|
52
69
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Bundles the config (its relative imports included, packages left external)
|
|
72
|
+
* into `.plat/` and imports that, like Vite does for vite.config.ts. This
|
|
73
|
+
* works whatever the app's package `type` is, and a fresh file name means a
|
|
74
|
+
* changed config is always re-read.
|
|
75
|
+
*/
|
|
76
|
+
async function importConfig(root: string): Promise<unknown> {
|
|
77
|
+
const outfile = join(root, '.plat', `config-${process.pid}-${Date.now()}.mjs`);
|
|
78
|
+
try {
|
|
79
|
+
await esbuild({
|
|
80
|
+
entryPoints: [join(root, APP_CONFIG_FILE)],
|
|
81
|
+
outfile,
|
|
82
|
+
bundle: true,
|
|
83
|
+
packages: 'external',
|
|
84
|
+
platform: 'node',
|
|
85
|
+
format: 'esm',
|
|
86
|
+
target: 'node24',
|
|
87
|
+
sourcemap: 'inline',
|
|
88
|
+
logLevel: 'silent',
|
|
89
|
+
absWorkingDir: root,
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
throw new CliError(`could not compile ${APP_CONFIG_FILE}:${buildFailureList(err)}`, {
|
|
93
|
+
exitCode: EXIT.usage,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const mod = (await import(pathToFileURL(outfile).href)) as { default?: unknown };
|
|
98
|
+
return mod.default;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
throw new CliError(`${APP_CONFIG_FILE} threw while loading: ${(err as Error).message}`, {
|
|
101
|
+
exitCode: EXIT.usage,
|
|
102
|
+
});
|
|
103
|
+
} finally {
|
|
104
|
+
rmSync(outfile, { force: true });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** App name from `--app`, else the nearest plat.config.ts. */
|
|
109
|
+
export async function resolveAppName(opt: string | undefined, cwd = process.cwd()): Promise<string> {
|
|
55
110
|
if (opt) return opt;
|
|
56
|
-
|
|
57
|
-
if (!root) {
|
|
111
|
+
if (!findProjectRoot(cwd)) {
|
|
58
112
|
throw new CliError('no app given', {
|
|
59
113
|
hint: 'pass --app <name> or run inside an app directory',
|
|
60
114
|
exitCode: EXIT.usage,
|
|
61
115
|
});
|
|
62
116
|
}
|
|
63
|
-
return loadProject(
|
|
117
|
+
return (await loadProject(cwd)).manifest.name;
|
|
64
118
|
}
|
|
65
119
|
|
|
66
120
|
export function cliVersion(): string {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** An esbuild failure as `file:line: message` items (or the error's message), for a CliError list. */
|
|
2
|
+
export function buildFailureList(err: unknown): string {
|
|
3
|
+
const errors = (
|
|
4
|
+
err as { errors?: Array<{ text: string; location?: { file: string; line: number } | null }> }
|
|
5
|
+
).errors;
|
|
6
|
+
const lines = errors?.length
|
|
7
|
+
? errors.map((e) => (e.location ? `${e.location.file}:${e.location.line}: ${e.text}` : e.text))
|
|
8
|
+
: [(err as Error).message];
|
|
9
|
+
return `\n - ${lines.join('\n - ')}`;
|
|
10
|
+
}
|
package/template/AGENTS.md
CHANGED
|
@@ -6,10 +6,13 @@ the app; the platform provisions, deploys, routes, signs users in and monitors i
|
|
|
6
6
|
|
|
7
7
|
## Golden rules
|
|
8
8
|
|
|
9
|
-
1. **No infrastructure code and no AWS SDK in this repo.** Declare what you need in `
|
|
10
|
-
through `@ericminassian/platform`. No CDK, CloudFormation, Terraform or `@aws-sdk/*` imports.
|
|
11
|
-
2.
|
|
12
|
-
`src/worker.ts
|
|
9
|
+
1. **No infrastructure code and no AWS SDK in this repo.** Declare what you need in `plat.config.ts`; use
|
|
10
|
+
it through `@ericminassian/platform`. No CDK, CloudFormation, Terraform or `@aws-sdk/*` imports.
|
|
11
|
+
2. **`plat.config.ts` is the source of truth for names and payload types.** `pnpm typecheck` fails unless
|
|
12
|
+
`src/worker.ts` handles exactly its queues, events and crons, and every `queue()`, `events.publish()`,
|
|
13
|
+
`storage()` and `secrets.get()` uses a declared name with the declared payload type. Declare message
|
|
14
|
+
types there (`queue<EmailJob>()`, `event<TodoCreated>()`) rather than annotating handlers, and keep the
|
|
15
|
+
`Register` block at the end of the file.
|
|
13
16
|
3. **Never commit secrets.** Use `plat secrets set NAME` for deployed stages and `.env.local` (git-ignored)
|
|
14
17
|
for `plat dev`. Declare each name under `secrets`.
|
|
15
18
|
4. **Run `pnpm typecheck && pnpm test` before you push.** CI runs the same checks, plus `plat test` when
|
|
@@ -19,7 +22,7 @@ the app; the platform provisions, deploys, routes, signs users in and monitors i
|
|
|
19
22
|
|
|
20
23
|
| Path | What |
|
|
21
24
|
| ---------------------------- | -------------------------------------------------------------------------------------- |
|
|
22
|
-
| `
|
|
25
|
+
| `plat.config.ts` | Everything the app needs (web, auth, db, queues, events, crons, storage, secrets, env) |
|
|
23
26
|
| `src/server.ts` | HTTP API (Hono). `export default app`. Routes under `/api/*` |
|
|
24
27
|
| `src/worker.ts` | Background handlers: `defineWorker({ queues, events, crons })` |
|
|
25
28
|
| `src/types.ts` | Types shared by server, worker and web |
|
|
@@ -29,7 +32,7 @@ the app; the platform provisions, deploys, routes, signs users in and monitors i
|
|
|
29
32
|
|
|
30
33
|
Requests for `/api/*` and `/__platform/*` go to the server; files go to `web/dist`. Any other path
|
|
31
34
|
serves `index.html`, which is how client-side routing works. Change the server prefixes with
|
|
32
|
-
`web.serverRoutes` (default `[
|
|
35
|
+
`web.serverRoutes` (default `['/api/']`).
|
|
33
36
|
|
|
34
37
|
## SDK reference (`@ericminassian/platform`)
|
|
35
38
|
|
|
@@ -44,8 +47,8 @@ import { defineWorker, invokeJob } from '@ericminassian/platform/worker';
|
|
|
44
47
|
| `db.collection` | `const todos = db.collection<Todo>('todos', { partition: 'userId', sort: 'id' })` |
|
|
45
48
|
| `.get/.put/.update/.delete` | `await todos.put(t, { ifNotExists: true })`, `await todos.update({ userId, id }, { done: true })` |
|
|
46
49
|
| `.query` | `await todos.query({ userId }, { beginsWith: '2026', limit: 20, reverse: true })` → `{ items, cursor }` |
|
|
47
|
-
| `queue(name).send` | `await queue
|
|
48
|
-
| `events.publish` | `await events.publish('todo.created', { id })`
|
|
50
|
+
| `queue(name).send` | `await queue('emails').send({ to, subject, body }, { delaySeconds: 60 })` |
|
|
51
|
+
| `events.publish` | `await events.publish('todo.created', { userId, id, title })` |
|
|
49
52
|
| `storage(name)` | `await storage('uploads').put(key, bytes, { contentType })`, `.get`, `.getText`, `.list(prefix)`, `.delete` |
|
|
50
53
|
| | `await storage('uploads').uploadUrl(key, { contentType })`: browser-direct upload URL (HTTP PUT) |
|
|
51
54
|
| `secrets.get` | `await secrets.get('STRIPE_KEY')` (throws if unset), `secrets.getOptional(...)` |
|
|
@@ -54,7 +57,7 @@ import { defineWorker, invokeJob } from '@ericminassian/platform/worker';
|
|
|
54
57
|
| `auth.loginUrl/logoutUrl` | `auth.loginUrl('/dashboard')` → `/__platform/auth/login?redirect=...` |
|
|
55
58
|
| `metrics` | `metrics.count('signups')`, `metrics.value('cart.total', 42.5, 'usd')`: free, shows up in the console |
|
|
56
59
|
| `log` | `log.info('charged', { orderId })`, `log.error('failed', err)`: structured, traced, searchable |
|
|
57
|
-
| `defineWorker` | `defineWorker({ queues: { emails: async (msg) => {} }, events: {...}, crons: {...} })`
|
|
60
|
+
| `defineWorker` | `defineWorker({ queues: { emails: async (msg) => {} }, events: {...}, crons: {...} })`; `msg.body` is typed |
|
|
58
61
|
| `invokeJob` | `await invokeJob('reindex', { id })`: async call to an `invoke` handler on this stage's worker |
|
|
59
62
|
|
|
60
63
|
User ids (`user.id`) are stable and shared across all platform apps. Every request, job, database call,
|
|
@@ -63,32 +66,34 @@ queue send and `fetch` is traced automatically. Don't add your own tracing or me
|
|
|
63
66
|
## Recipes
|
|
64
67
|
|
|
65
68
|
- **New collection or index.** Declare it in code; no infra change is needed. `db: true` must be in
|
|
66
|
-
`
|
|
69
|
+
`plat.config.ts`. For a secondary index:
|
|
67
70
|
`db.collection<User>('users', { partition: 'id', indexes: { byEmail: { slot: 1, partition: 'email' } } })`,
|
|
68
71
|
then `users.query({ email }, { index: 'byEmail' })`.
|
|
69
72
|
- Slots 1-3 are fixed GSIs. Never reuse a slot for a different key in the same collection.
|
|
70
73
|
- For expiry, set `ttl: 'expiresAt'` (epoch seconds).
|
|
71
74
|
- **Queue.**
|
|
72
|
-
1. Add `
|
|
75
|
+
1. Add `thumbnails: queue<ThumbnailJob>({ batchSize: 5 })` under `queues` in `plat.config.ts`, with
|
|
76
|
+
`ThumbnailJob` in `src/types.ts`.
|
|
73
77
|
2. Add a `queues.thumbnails` handler in `src/worker.ts`.
|
|
74
78
|
3. Send with `queue('thumbnails').send(...)`.
|
|
75
79
|
|
|
76
80
|
Throwing in the handler retries the message; after `maxReceiveCount` (default 5) it goes to the
|
|
77
81
|
dead-letter queue, which you can inspect and redrive from the console.
|
|
78
|
-
- **Event subscription.** Add
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
- **Event subscription.** Add `'order.paid': event<OrderPaid>()` under `events`, then add an
|
|
83
|
+
`events['order.paid']` handler. Publish with `events.publish('order.paid', detail)`.
|
|
84
|
+
- **Cron.** Add `digest: '0 8 * * 1-5'` under `crons` (5-field cron, UTC; or
|
|
85
|
+
`{ schedule: '...', timezone: 'America/Los_Angeles' }`, or `'rate(15 minutes)'`), then add a
|
|
81
86
|
`crons.digest` handler.
|
|
82
|
-
- **File store.** Add `
|
|
87
|
+
- **File store.** Add `'avatars'` to `storage`, then use `storage('avatars')`. For browser uploads, return
|
|
83
88
|
`uploadUrl(...)` from an API route and `PUT` the file to it.
|
|
84
89
|
- **Secret.**
|
|
85
|
-
1. Add `
|
|
90
|
+
1. Add `'STRIPE_KEY'` to `secrets` in `plat.config.ts`.
|
|
86
91
|
2. Set it: `plat secrets set STRIPE_KEY --stage prod`, or `--shared` for all stages.
|
|
87
92
|
3. Locally, put `STRIPE_KEY=...` in `.env.local`.
|
|
88
93
|
4. Read it: `await secrets.get('STRIPE_KEY')`.
|
|
89
94
|
- **Protected route.** Call `const user = await auth.requireUser(c.req.raw)` first. To require sign-in
|
|
90
|
-
for everything, set `
|
|
91
|
-
- **Plain config.** Use `
|
|
95
|
+
for everything, set `auth: { required: true }`.
|
|
96
|
+
- **Plain config.** Use `env: { FEATURE_X: 'on' }`, read via `process.env.FEATURE_X`. Not for secrets.
|
|
92
97
|
- **Custom metric.** Use `metrics.count('name')` or `metrics.value('name', n)`. It appears in the console
|
|
93
98
|
and in `plat metrics`.
|
|
94
99
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineApp, event, queue } from '@ericminassian/platform/config';
|
|
2
|
+
import type { EmailJob, TodoCreated } from './src/types.ts';
|
|
3
|
+
|
|
4
|
+
// Everything this app needs from the platform. `plat` evaluates this file and
|
|
5
|
+
// provisions what it declares; the SDK checks names and payload types in code
|
|
6
|
+
// against it (see `Register` below).
|
|
7
|
+
const app = defineApp({
|
|
8
|
+
name: 'template-app',
|
|
9
|
+
description: 'A todo app on the platform',
|
|
10
|
+
web: {
|
|
11
|
+
server: 'src/server.ts',
|
|
12
|
+
static: 'web/dist',
|
|
13
|
+
build: 'vite build --config web/vite.config.ts',
|
|
14
|
+
dev: { command: 'vite --config web/vite.config.ts', port: 5173 },
|
|
15
|
+
spa: true,
|
|
16
|
+
},
|
|
17
|
+
auth: true,
|
|
18
|
+
db: true,
|
|
19
|
+
queues: { emails: queue<EmailJob>() },
|
|
20
|
+
events: { 'todo.created': event<TodoCreated>() },
|
|
21
|
+
crons: { cleanup: '0 3 * * *' },
|
|
22
|
+
storage: ['uploads'],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export default app;
|
|
26
|
+
|
|
27
|
+
declare module '@ericminassian/platform' {
|
|
28
|
+
interface Register {
|
|
29
|
+
app: typeof app;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/template/src/server.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { auth, db, events, metrics, app as platform, queue } from '@ericminassia
|
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
3
|
import { validator } from 'hono/validator';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import type {
|
|
5
|
+
import type { Todo } from './types.ts';
|
|
6
6
|
|
|
7
7
|
// One collection per entity. Partition by user so each user's todos are one query.
|
|
8
8
|
// Sort keys are time-ordered ids, so `query` returns oldest first.
|
|
@@ -50,9 +50,9 @@ const api = new Hono()
|
|
|
50
50
|
createdAt: new Date().toISOString(),
|
|
51
51
|
};
|
|
52
52
|
await todos.put(todo, { ifNotExists: true });
|
|
53
|
-
await events.publish
|
|
53
|
+
await events.publish('todo.created', { userId: user.id, id: todo.id, title: todo.title });
|
|
54
54
|
if (user.email) {
|
|
55
|
-
await queue
|
|
55
|
+
await queue('emails').send({ to: user.email, subject: 'New todo', body: todo.title });
|
|
56
56
|
}
|
|
57
57
|
metrics.count('todos.created');
|
|
58
58
|
return c.json({ todo }, 201);
|
package/template/src/worker.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { log, metrics } from '@ericminassian/platform';
|
|
2
|
-
import { defineWorker
|
|
3
|
-
import type { EmailJob, TodoCreated } from './types.ts';
|
|
2
|
+
import { defineWorker } from '@ericminassian/platform/worker';
|
|
4
3
|
|
|
5
|
-
//
|
|
4
|
+
// Handles exactly the `queues`, `events` and `crons` in plat.config.ts (a missing
|
|
5
|
+
// or extra handler is a type error), with the payload types declared there.
|
|
6
6
|
// Throwing retries the job (queues: up to maxReceiveCount, then the dead-letter queue).
|
|
7
7
|
export default defineWorker({
|
|
8
8
|
queues: {
|
|
9
|
-
emails: async (message
|
|
9
|
+
emails: async (message) => {
|
|
10
10
|
// Swap in a real email provider here (store its API key with `plat secrets set`).
|
|
11
11
|
log.info('would send email', {
|
|
12
12
|
to: message.body.to,
|
|
@@ -18,7 +18,7 @@ export default defineWorker({
|
|
|
18
18
|
},
|
|
19
19
|
|
|
20
20
|
events: {
|
|
21
|
-
'todo.created': async (event
|
|
21
|
+
'todo.created': async (event) => {
|
|
22
22
|
log.info('todo created', { id: event.detail.id, userId: event.detail.userId });
|
|
23
23
|
},
|
|
24
24
|
},
|
package/template/tsconfig.json
CHANGED
package/template/platform.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://raw.githubusercontent.com/eric-minassian/platform/main/packages/core/platform.schema.json",
|
|
3
|
-
"name": "template-app",
|
|
4
|
-
"description": "A todo app on the platform",
|
|
5
|
-
"web": {
|
|
6
|
-
"server": "src/server.ts",
|
|
7
|
-
"static": "web/dist",
|
|
8
|
-
"build": "vite build --config web/vite.config.ts",
|
|
9
|
-
"dev": { "command": "vite --config web/vite.config.ts", "port": 5173 },
|
|
10
|
-
"spa": true
|
|
11
|
-
},
|
|
12
|
-
"auth": true,
|
|
13
|
-
"db": true,
|
|
14
|
-
"queues": { "emails": {} },
|
|
15
|
-
"events": ["todo.created"],
|
|
16
|
-
"crons": { "cleanup": "0 3 * * *" },
|
|
17
|
-
"storage": ["uploads"]
|
|
18
|
-
}
|