@c9up/ream 0.2.24 → 0.2.26

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 (96) hide show
  1. package/README.md +2 -0
  2. package/dist/Codemods.d.ts +13 -0
  3. package/dist/Codemods.d.ts.map +1 -1
  4. package/dist/Codemods.js +158 -23
  5. package/dist/Codemods.js.map +1 -1
  6. package/dist/Ignitor.d.ts +28 -0
  7. package/dist/Ignitor.d.ts.map +1 -1
  8. package/dist/Ignitor.js +15 -15
  9. package/dist/Ignitor.js.map +1 -1
  10. package/dist/console/Kernel.d.ts.map +1 -1
  11. package/dist/console/Kernel.js +20 -1
  12. package/dist/console/Kernel.js.map +1 -1
  13. package/dist/console/cliui.d.ts +9 -245
  14. package/dist/console/cliui.d.ts.map +1 -1
  15. package/dist/console/cliui.js +9 -583
  16. package/dist/console/cliui.js.map +1 -1
  17. package/dist/console/make/commands.d.ts +19 -0
  18. package/dist/console/make/commands.d.ts.map +1 -0
  19. package/dist/console/make/commands.js +252 -0
  20. package/dist/console/make/commands.js.map +1 -0
  21. package/dist/console/make/eject.d.ts +28 -0
  22. package/dist/console/make/eject.d.ts.map +1 -0
  23. package/dist/console/make/eject.js +161 -0
  24. package/dist/console/make/eject.js.map +1 -0
  25. package/dist/console/make/flush.d.ts +51 -0
  26. package/dist/console/make/flush.d.ts.map +1 -0
  27. package/dist/console/make/flush.js +93 -0
  28. package/dist/console/make/flush.js.map +1 -0
  29. package/dist/console/make/generators.d.ts +36 -0
  30. package/dist/console/make/generators.d.ts.map +1 -0
  31. package/dist/console/make/generators.js +148 -0
  32. package/dist/console/make/generators.js.map +1 -0
  33. package/dist/console/make/naming.d.ts +32 -0
  34. package/dist/console/make/naming.d.ts.map +1 -0
  35. package/dist/console/make/naming.js +82 -0
  36. package/dist/console/make/naming.js.map +1 -0
  37. package/dist/console/make/resolve.d.ts +46 -0
  38. package/dist/console/make/resolve.d.ts.map +1 -0
  39. package/dist/console/make/resolve.js +113 -0
  40. package/dist/console/make/resolve.js.map +1 -0
  41. package/dist/console/ui.d.ts +9 -15
  42. package/dist/console/ui.d.ts.map +1 -1
  43. package/dist/console/ui.js +11 -24
  44. package/dist/console/ui.js.map +1 -1
  45. package/dist/dev/hmr.d.ts +52 -4
  46. package/dist/dev/hmr.d.ts.map +1 -1
  47. package/dist/dev/hmr.js +75 -9
  48. package/dist/dev/hmr.js.map +1 -1
  49. package/dist/dev/hotGlob.d.ts +10 -3
  50. package/dist/dev/hotGlob.d.ts.map +1 -1
  51. package/dist/dev/hotGlob.js +28 -3
  52. package/dist/dev/hotGlob.js.map +1 -1
  53. package/dist/hot.js +56 -3
  54. package/dist/hot.js.map +1 -1
  55. package/dist/index.d.ts +3 -3
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +4 -2
  58. package/dist/index.js.map +1 -1
  59. package/dist/stubs/index.d.ts +11 -0
  60. package/dist/stubs/index.d.ts.map +1 -0
  61. package/dist/stubs/index.js +12 -0
  62. package/dist/stubs/index.js.map +1 -0
  63. package/dist/stubs/template.d.ts +63 -0
  64. package/dist/stubs/template.d.ts.map +1 -0
  65. package/dist/stubs/template.js +209 -0
  66. package/dist/stubs/template.js.map +1 -0
  67. package/events.win32-x64-msvc.node +0 -0
  68. package/index.win32-x64-msvc.node +0 -0
  69. package/package.json +7 -1
  70. package/scheduler.win32-x64-msvc.node +0 -0
  71. package/src/Codemods.ts +206 -29
  72. package/src/Ignitor.ts +44 -14
  73. package/src/console/Kernel.ts +21 -1
  74. package/src/console/cliui.ts +44 -754
  75. package/src/console/make/commands.ts +270 -0
  76. package/src/console/make/eject.ts +163 -0
  77. package/src/console/make/flush.ts +125 -0
  78. package/src/console/make/generators.ts +176 -0
  79. package/src/console/make/naming.ts +85 -0
  80. package/src/console/make/resolve.ts +134 -0
  81. package/src/console/ui.ts +12 -24
  82. package/src/dev/hmr.ts +102 -9
  83. package/src/dev/hotGlob.ts +28 -3
  84. package/src/hot.ts +59 -3
  85. package/src/index.ts +17 -3
  86. package/src/stubs/index.ts +14 -0
  87. package/src/stubs/template.ts +262 -0
  88. package/stubs/make/command.stub +29 -0
  89. package/stubs/make/controller.stub +30 -0
  90. package/stubs/make/entity.stub +11 -0
  91. package/stubs/make/event.stub +13 -0
  92. package/stubs/make/listener.stub +13 -0
  93. package/stubs/make/middleware.stub +16 -0
  94. package/stubs/make/provider.stub +26 -0
  95. package/stubs/make/service.stub +27 -0
  96. package/stubs/make/validator.stub +10 -0
package/src/hot.ts CHANGED
@@ -27,7 +27,7 @@ import { existsSync, writeSync } from 'node:fs'
27
27
  import { readFile } from 'node:fs/promises'
28
28
  import { registerHooks } from 'node:module'
29
29
  import { dirname, relative, resolve as resolvePath } from 'node:path'
30
- import { fileURLToPath } from 'node:url'
30
+ import { fileURLToPath, pathToFileURL } from 'node:url'
31
31
  import { fullReloadNotice } from './dev/fullReload.js'
32
32
  import { hotReloadHappened } from './dev/hmr.js'
33
33
  import { readHotHookConfig } from './dev/hotConfig.js'
@@ -66,9 +66,60 @@ const restartFiles = new Set(
66
66
  .filter((file) => existsSync(file)),
67
67
  )
68
68
 
69
+ /**
70
+ * `metaFiles` entries that asked for a restart.
71
+ *
72
+ * Non-module files the application owns — translations, view templates — live
73
+ * in `reamrc.ts` because the BUILD has to copy them. A change to one is
74
+ * invisible to the import graph, so the only way it can do anything in
75
+ * development is by being named here.
76
+ *
77
+ * `reloadServer: false` is the interesting default, and the one upstream picks
78
+ * for translations: the entry exists so the file ships, and an edit does NOT
79
+ * restart. NAMED DEVIATION — upstream still logs such a change, because its
80
+ * watcher covers the whole project anyway. This one watches a fixed list of
81
+ * directories on purpose (`resources/` belongs to the asset watcher, and a
82
+ * stylesheet edit must not restart the server), so a `false` entry is simply
83
+ * not watched. The observable behaviour is the same: nothing happens.
84
+ */
85
+ async function readReloadPatterns(): Promise<string[]> {
86
+ try {
87
+ const rc = (await import(pathToFileURL(resolvePath(root, 'reamrc.ts')).href)) as {
88
+ default?: { metaFiles?: Array<{ pattern?: unknown; reloadServer?: unknown }> }
89
+ }
90
+ return (rc.default?.metaFiles ?? [])
91
+ .filter((entry) => entry.reloadServer === true && typeof entry.pattern === 'string')
92
+ .map((entry) => String(entry.pattern))
93
+ } catch {
94
+ // No rc file, or one that does not load. The server itself reports a broken
95
+ // rc far more clearly than the loader could, and a dev server that refuses
96
+ // to start because of a watch list helps nobody.
97
+ return []
98
+ }
99
+ }
100
+
101
+ const reloadPatterns = await readReloadPatterns()
102
+
103
+ /**
104
+ * The directory a pattern lives under — everything before its first wildcard.
105
+ *
106
+ * `resources/lang/**\/*.json` is watched as `resources/lang`. Watching the
107
+ * project root instead would pull in `node_modules` and the build output.
108
+ */
109
+ function watchRootOf(pattern: string): string {
110
+ const segments = pattern.replace(/^\.\//, '').split('/')
111
+ const fixed: string[] = []
112
+ for (const segment of segments) {
113
+ if (/[*?{[]/.test(segment)) break
114
+ fixed.push(segment)
115
+ }
116
+ // A pattern that is a bare glob has no directory to narrow to.
117
+ return fixed.slice(0, -1).join('/') || fixed.join('/')
118
+ }
119
+
69
120
  const relativeToRoot = (file: string): string => relative(root, file).replace(/\\/g, '/')
70
121
  const matchesBoundary = globMatcher(boundaries)
71
- const matchesRestartGlob = globMatcher(restart)
122
+ const matchesRestartGlob = globMatcher([...restart, ...reloadPatterns])
72
123
 
73
124
  const graph = new HotGraph({
74
125
  isBoundary: (file) => matchesBoundary(relativeToRoot(file)),
@@ -126,7 +177,12 @@ function sayNow(line: string): void {
126
177
 
127
178
  watchProject({
128
179
  root,
129
- directories: WATCH_DIRS,
180
+ directories: [
181
+ ...WATCH_DIRS,
182
+ // Whatever a `reloadServer: true` entry named, and nothing else: the
183
+ // directories above are the ones the graph can reason about.
184
+ ...reloadPatterns.map(watchRootOf).filter((dir) => dir !== ''),
185
+ ],
130
186
  files: [...restartFiles].map((file) => relative(root, file)),
131
187
  onChange: (file) => {
132
188
  const decision = graph.decide(file)
package/src/index.ts CHANGED
@@ -272,8 +272,22 @@ export type {
272
272
 
273
273
  // ─── Codemods ───────────────────────────────────────────────
274
274
 
275
- export type { Codemods } from './Codemods.js'
276
- export { createCodemods } from './Codemods.js'
275
+ export {
276
+ type Codemods,
277
+ createCodemods,
278
+ type GeneratedStub,
279
+ type StubState,
280
+ } from './Codemods.js'
281
+ export {
282
+ compile as compileStub,
283
+ escapeHtml,
284
+ generate as generateStub,
285
+ render as renderStub,
286
+ type StubBlock,
287
+ StubSyntaxError,
288
+ type StubValue,
289
+ type TemplateOptions,
290
+ } from './stubs/template.js'
277
291
 
278
292
  // ─── Errors ─────────────────────────────────────────────────
279
293
 
@@ -385,4 +399,4 @@ export {
385
399
  StatsTracker,
386
400
  } from './scheduler/index.js'
387
401
 
388
- export const VERSION = '0.2.24'
402
+ export const VERSION = '0.2.26'
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `@c9up/ream/stubs` — the stub engine, and where this package keeps its own.
3
+ *
4
+ * Every Ream package exports a `stubsRoot` from this subpath: it is what
5
+ * `codemods.makeUsingStub(stubsRoot, 'make/controller.stub')` takes, and what
6
+ * `ream eject --pkg` reads to copy a template into the application.
7
+ */
8
+
9
+ import * as path from 'node:path'
10
+
11
+ export * from './template.js'
12
+
13
+ /** `<package>/stubs` — resolved from this file, so it survives the build. */
14
+ export const stubsRoot = path.join(import.meta.dirname, '..', '..', 'stubs')
@@ -0,0 +1,262 @@
1
+ /**
2
+ * The stub template engine.
3
+ *
4
+ * A port of the one upstream uses (tempura), grammar for grammar: comments,
5
+ * declared props, inline variables, conditionals, loops, custom blocks, and
6
+ * the two interpolation forms. A template is compiled into a JavaScript
7
+ * function once and then called — the same design, for the same reason: a
8
+ * tree-walking interpreter re-reads the template on every render, and a stub
9
+ * is rendered in a loop by the generators.
10
+ *
11
+ * {{! a comment }}
12
+ * {{#expect name, entity }}
13
+ * {{#var plural = name + 's' }}
14
+ * {{#if entity.isModel }}…{{#elif entity.isView }}…{{#else}}…{{/if}}
15
+ * {{#each items as item, index }}…{{/each}}
16
+ * {{ name }} the value
17
+ * {{{ name }}} the value, unescaped
18
+ *
19
+ * NAMED DEVIATION — upstream escapes HTML in `{{ }}` and leaves `{{{ }}}`
20
+ * raw, because its templates are web pages. Ours are SOURCE FILES: escaping
21
+ * `"` into `&quot;` there produces code that does not parse, and the mistake
22
+ * is silent. So `{{ }}` does not escape by default, `{{{ }}}` is accepted as
23
+ * the same thing (a stub written for upstream keeps working), and an `escape`
24
+ * option turns it back on for anyone templating markup.
25
+ *
26
+ * `new Function` compiles the result. That is how upstream does it too, and
27
+ * the trust boundary is the same: a stub ships inside a package whose
28
+ * `configure` hook is already executing.
29
+ */
30
+
31
+ /** What a template may read. Scalars, lists and nested records. */
32
+ export type StubValue =
33
+ | string
34
+ | number
35
+ | boolean
36
+ | null
37
+ | undefined
38
+ | StubValue[]
39
+ | { [key: string]: StubValue }
40
+
41
+ export type StubState = Record<string, StubValue>
42
+
43
+ /** A custom block — `{{#myBlock key=value }}` calls it with those arguments. */
44
+ export type StubBlock = (args: Record<string, StubValue>) => string
45
+
46
+ export interface TemplateOptions {
47
+ /** Names the template may read even when the state does not carry them. */
48
+ props?: string[]
49
+ /** Custom `{{#name}}` blocks. */
50
+ blocks?: Record<string, StubBlock>
51
+ /** Applied to every `{{ }}`. Identity by default — see the header. */
52
+ escape?: (value: unknown) => string
53
+ }
54
+
55
+ /** Raised when a template cannot be read, naming what is wrong with it. */
56
+ export class StubSyntaxError extends Error {
57
+ readonly code = 'E_REAM_STUB_SYNTAX'
58
+ constructor(message: string) {
59
+ super(message)
60
+ this.name = 'StubSyntaxError'
61
+ }
62
+ }
63
+
64
+ /** HTML escaping, for a stub that does template markup. */
65
+ export function escapeHtml(value: unknown): string {
66
+ const text = value === null || value === undefined ? '' : String(value)
67
+ return text.replace(/[&"<>]/g, (char) => {
68
+ if (char === '&') return '&amp;'
69
+ if (char === '"') return '&quot;'
70
+ if (char === '<') return '&lt;'
71
+ return '&gt;'
72
+ })
73
+ }
74
+
75
+ /** The default: a source file wants its quotes and its angle brackets back. */
76
+ function asIs(value: unknown): string {
77
+ return value === null || value === undefined ? '' : String(value)
78
+ }
79
+
80
+ const CURLY = /{{{?\s*([\s\S]*?)\s*}}}?/g
81
+ const BLOCK = /^#\s*(\w[\w\d]*)\s*([\s\S]*)/
82
+ const ARGS = /([a-zA-Z$_][^\s=]*)\s*=\s*((["`'])(?:(?=(\\?))\4.)*?\3|{[^}]*}|\[[^\]]*]|\S+)/g
83
+
84
+ /**
85
+ * Compile a template into the body of a function.
86
+ *
87
+ * Kept separate from `compile` so a test can read what a template became —
88
+ * the generated source is the only place a subtle mistake shows itself.
89
+ */
90
+ export function generate(input: string, options: TemplateOptions = {}): string {
91
+ const blocks = options.blocks ?? {}
92
+ const declared = new Set<string>(options.props ?? [])
93
+ const stack: string[] = []
94
+
95
+ let body = ''
96
+ let literal = ''
97
+ let last = 0
98
+ CURLY.lastIndex = 0
99
+
100
+ /** Flush the literal text collected since the last tag. */
101
+ const flush = (): void => {
102
+ if (literal.length > 0) {
103
+ body += `${body ? 'x+=' : '='}\`${literal}\`;`
104
+ } else if (body.length === 0) {
105
+ body = '="";'
106
+ }
107
+ literal = ''
108
+ }
109
+
110
+ let match: RegExpExecArray | null = CURLY.exec(input)
111
+ while (match !== null) {
112
+ literal += escapeForTemplate(input.slice(last, match.index))
113
+ last = match.index + match[0].length
114
+
115
+ const inner = (match[1] ?? '').trim()
116
+ const first = inner.charAt(0)
117
+
118
+ if (first === '!') {
119
+ // A comment. It leaves no trace in the output.
120
+ } else if (first === '#') {
121
+ flush()
122
+ const parsed = BLOCK.exec(inner)
123
+ if (parsed === null) {
124
+ throw new StubSyntaxError(`Unreadable block: {{${inner}}}`)
125
+ }
126
+ const action = parsed[1] ?? ''
127
+ const rest = (parsed[2] ?? '').trim()
128
+ body += openBlock(action, rest, stack, declared, blocks)
129
+ } else if (first === '/') {
130
+ const closing = inner.slice(1).trim()
131
+ const opened = stack.pop()
132
+ flush()
133
+ if (opened === undefined) {
134
+ throw new StubSyntaxError(`Closed "${closing}" with nothing open.`)
135
+ }
136
+ if (opened !== closing) {
137
+ throw new StubSyntaxError(`Expected to close "${opened}"; closed "${closing}" instead.`)
138
+ }
139
+ body += '}'
140
+ } else {
141
+ // Both forms interpolate the same way — see the header.
142
+ literal += `\${$$e(${inner})}`
143
+ }
144
+
145
+ match = CURLY.exec(input)
146
+ }
147
+
148
+ if (stack.length > 0) {
149
+ throw new StubSyntaxError(`Unterminated "${stack.pop()}" block.`)
150
+ }
151
+ if (last < input.length) {
152
+ literal += escapeForTemplate(input.slice(last))
153
+ }
154
+ flush()
155
+
156
+ const bindings = declared.size > 0 ? `{${[...declared].join()}}=$$s,x` : ' x'
157
+ return `var${bindings}${body}return x`
158
+ }
159
+
160
+ function openBlock(
161
+ action: string,
162
+ rest: string,
163
+ stack: string[],
164
+ declared: Set<string>,
165
+ blocks: Record<string, StubBlock>,
166
+ ): string {
167
+ if (action === 'expect') {
168
+ // Names the template reads. Declaring them is what lets an unknown one
169
+ // fail as `x is not defined` rather than as `undefined` in the output.
170
+ for (const key of rest.split(/[\n\r\s\t]*,[\n\r\s\t]*/g)) {
171
+ if (key !== '') declared.add(key)
172
+ }
173
+ return ''
174
+ }
175
+
176
+ if (action === 'var') {
177
+ const at = rest.indexOf('=')
178
+ if (at === -1) {
179
+ throw new StubSyntaxError(`{{#var}} needs an assignment: {{#var ${rest}}}`)
180
+ }
181
+ const name = rest.slice(0, at).trim()
182
+ const value = rest
183
+ .slice(at + 1)
184
+ .trim()
185
+ .replace(/;$/, '')
186
+ return `var ${name}=${value};`
187
+ }
188
+
189
+ if (action === 'each') {
190
+ stack.push(action)
191
+ const at = rest.indexOf(' as ')
192
+ if (at === -1) {
193
+ return `for(var i=0,$$a=${rest};i<$$a.length;i++){`
194
+ }
195
+ const list = rest.slice(0, at).trim()
196
+ const [item = 'item', index = 'i'] = rest
197
+ .slice(at + 4)
198
+ .trim()
199
+ .replace(/[()\s]/g, '')
200
+ .split(',')
201
+ return `for(var ${index}=0,${item},$$a=${list};${index}<$$a.length;${index}++){${item}=$$a[${index}];`
202
+ }
203
+
204
+ if (action === 'if') {
205
+ stack.push(action)
206
+ return `if(${rest}){`
207
+ }
208
+ if (action === 'elif') return `}else if(${rest}){`
209
+ if (action === 'else') return '}else{'
210
+
211
+ if (action in blocks) {
212
+ // `{{#note tone="warn" }}` — the arguments become one object.
213
+ const args: string[] = []
214
+ let found: RegExpExecArray | null = ARGS.exec(rest)
215
+ while (found !== null) {
216
+ args.push(`${found[1]}:${found[2]}`)
217
+ found = ARGS.exec(rest)
218
+ }
219
+ ARGS.lastIndex = 0
220
+ return `x+=$$b.${action}(${args.length > 0 ? `{${args.join()}}` : '{}'});`
221
+ }
222
+
223
+ throw new StubSyntaxError(
224
+ `Unknown "${action}" block. Known: expect, var, if, elif, else, each${
225
+ Object.keys(blocks).length > 0 ? `, ${Object.keys(blocks).join(', ')}` : ''
226
+ }.`,
227
+ )
228
+ }
229
+
230
+ /** Protect the literal text from the template literal it is about to live in. */
231
+ function escapeForTemplate(text: string): string {
232
+ return text.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${')
233
+ }
234
+
235
+ /** Compile a template into a render function. */
236
+ export function compile(
237
+ input: string,
238
+ options: TemplateOptions = {},
239
+ ): (state: StubState) => string {
240
+ const source = generate(input, options)
241
+ let render: (escaper: (value: unknown) => string, blocks: unknown, state: StubState) => string
242
+ try {
243
+ // eslint-disable-next-line no-new-func -- the whole point of a compiler
244
+ render = new Function('$$e', '$$b', '$$s', source) as typeof render
245
+ } catch (error) {
246
+ throw new StubSyntaxError(
247
+ `This stub does not compile: ${error instanceof Error ? error.message : String(error)}`,
248
+ )
249
+ }
250
+ const escaper = options.escape ?? asIs
251
+ const blocks = options.blocks ?? {}
252
+ return (state: StubState) => render(escaper, blocks, state)
253
+ }
254
+
255
+ /** Compile and render in one go. */
256
+ export function render(
257
+ input: string,
258
+ state: StubState = {},
259
+ options: TemplateOptions = {},
260
+ ): string {
261
+ return compile(input, { props: Object.keys(state), ...options })(state)
262
+ }
@@ -0,0 +1,29 @@
1
+ ---
2
+ to: commands/{{ fileName }}.ts
3
+ ---
4
+ import { BaseCommand, flags } from '@c9up/ream/console'
5
+ import type { CommandOptions } from '@c9up/ream/console'
6
+
7
+ export default class {{ className }} extends BaseCommand {
8
+ static commandName = '{{ name }}'
9
+ static description = 'TODO describe what this command does'
10
+
11
+ /**
12
+ * `startApp` boots providers and the container before `run()`. Off by
13
+ * default: a command that only touches the filesystem has no
14
+ * reason to open a database connection. Turn it on to reach `this.app`.
15
+ */
16
+ static options: CommandOptions = { startApp: false }
17
+
18
+ @flags.boolean({ description: 'Report what would happen without doing it' })
19
+ declare dryRun: boolean
20
+
21
+ async run(): Promise<void> {
22
+ if (this.dryRun) {
23
+ this.logger.info('Dry run — nothing was changed.')
24
+ return
25
+ }
26
+
27
+ this.logger.success('{{ name }} ran.')
28
+ }
29
+ }
@@ -0,0 +1,30 @@
1
+ ---
2
+ to: app/{{ module }}/{{ className }}.ts
3
+ ---
4
+ import type { HttpContext } from '@c9up/ream'
5
+
6
+ export class {{ className }} {
7
+ async index({ response }: HttpContext) {
8
+ response.status(200).json([])
9
+ }
10
+
11
+ async show({ params, response }: HttpContext) {
12
+ const { id } = params
13
+ response.status(200).json({ id })
14
+ }
15
+
16
+ async store({ request, response }: HttpContext) {
17
+ const data = request.body()
18
+ response.status(201).json({ created: true })
19
+ }
20
+
21
+ async update({ params, response }: HttpContext) {
22
+ const { id } = params
23
+ response.status(200).json({ id, updated: true })
24
+ }
25
+
26
+ async destroy({ params, response }: HttpContext) {
27
+ const { id } = params
28
+ response.status(204).send('')
29
+ }
30
+ }
@@ -0,0 +1,11 @@
1
+ ---
2
+ to: app/{{ module }}/{{ name }}.ts
3
+ ---
4
+ import { Entity, Column, PrimaryKey, BaseEntity } from '@c9up/atlas'
5
+
6
+ @Entity('{{ tableName }}')
7
+ export class {{ className }} extends BaseEntity {
8
+ @PrimaryKey() id!: string
9
+ @Column() createdAt!: string
10
+ @Column() updatedAt!: string
11
+ }
@@ -0,0 +1,13 @@
1
+ ---
2
+ to: app/events/{{ fileName }}.ts
3
+ ---
4
+ import { BaseEvent } from '@c9up/ream/events'
5
+
6
+ export default class {{ className }} extends BaseEvent {
7
+ /** Name listeners subscribe to. Defaults to the class name when omitted. */
8
+ static eventName = '{{ fileName }}'
9
+
10
+ constructor(public payload: Record<string, unknown>) {
11
+ super()
12
+ }
13
+ }
@@ -0,0 +1,13 @@
1
+ ---
2
+ to: app/listeners/{{ fileName }}.ts
3
+ ---
4
+ {{ importLine }}/**
5
+ * Register it with the emitter:
6
+ * {{ registration }}
7
+ */
8
+ export default class {{ className }} {
9
+ async handle(event: {{ eventType }}): Promise<void> {
10
+ // React to the event.
11
+ void event
12
+ }
13
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ to: app/middleware/{{ fileName }}_middleware.ts
3
+ ---
4
+ import type { HttpContext } from '@c9up/ream'
5
+
6
+ /**
7
+ * Register it in `start/kernel.ts`:
8
+ * {{ registration }}
9
+ */
10
+ export default class {{ className }} {
11
+ async handle(ctx: HttpContext, next: () => Promise<void>) {
12
+ // Runs before the route handler.
13
+ await next()
14
+ // Runs after it — the response is available here.
15
+ }
16
+ }
@@ -0,0 +1,26 @@
1
+ ---
2
+ to: providers/{{ className }}.ts
3
+ ---
4
+ import { Provider } from '@c9up/ream'
5
+
6
+ export default class {{ className }} extends Provider {
7
+ register() {
8
+ // Register bindings in the container
9
+ }
10
+
11
+ async boot() {
12
+ // Connect and verify
13
+ }
14
+
15
+ async start() {
16
+ // Runs before HTTP server starts
17
+ }
18
+
19
+ async ready() {
20
+ // Application operational
21
+ }
22
+
23
+ async shutdown() {
24
+ // Cleanup
25
+ }
26
+ }
@@ -0,0 +1,27 @@
1
+ ---
2
+ to: app/{{ module }}/{{ className }}.ts
3
+ ---
4
+ import { Service } from '@c9up/ream'
5
+
6
+ @Service()
7
+ export class {{ className }} {
8
+ async findAll() {
9
+ return []
10
+ }
11
+
12
+ async findById(id: string) {
13
+ return null
14
+ }
15
+
16
+ async create(data: Record<string, unknown>) {
17
+ return data
18
+ }
19
+
20
+ async update(id: string, data: Record<string, unknown>) {
21
+ return { id, ...data }
22
+ }
23
+
24
+ async delete(id: string) {
25
+ return { id }
26
+ }
27
+ }
@@ -0,0 +1,10 @@
1
+ ---
2
+ to: app/{{ module }}/{{ className }}.ts
3
+ ---
4
+ import { rules, schema } from '@c9up/rune'
5
+
6
+ export const {{ className }} = schema({
7
+ // Define validation rules
8
+ // name: rules.string().min(1).max(255),
9
+ // email: rules.string().email(),
10
+ })