@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.
- package/README.md +2 -0
- package/dist/Codemods.d.ts +13 -0
- package/dist/Codemods.d.ts.map +1 -1
- package/dist/Codemods.js +158 -23
- package/dist/Codemods.js.map +1 -1
- package/dist/Ignitor.d.ts +28 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +15 -15
- package/dist/Ignitor.js.map +1 -1
- package/dist/console/Kernel.d.ts.map +1 -1
- package/dist/console/Kernel.js +20 -1
- package/dist/console/Kernel.js.map +1 -1
- package/dist/console/cliui.d.ts +9 -245
- package/dist/console/cliui.d.ts.map +1 -1
- package/dist/console/cliui.js +9 -583
- package/dist/console/cliui.js.map +1 -1
- package/dist/console/make/commands.d.ts +19 -0
- package/dist/console/make/commands.d.ts.map +1 -0
- package/dist/console/make/commands.js +252 -0
- package/dist/console/make/commands.js.map +1 -0
- package/dist/console/make/eject.d.ts +28 -0
- package/dist/console/make/eject.d.ts.map +1 -0
- package/dist/console/make/eject.js +161 -0
- package/dist/console/make/eject.js.map +1 -0
- package/dist/console/make/flush.d.ts +51 -0
- package/dist/console/make/flush.d.ts.map +1 -0
- package/dist/console/make/flush.js +93 -0
- package/dist/console/make/flush.js.map +1 -0
- package/dist/console/make/generators.d.ts +36 -0
- package/dist/console/make/generators.d.ts.map +1 -0
- package/dist/console/make/generators.js +148 -0
- package/dist/console/make/generators.js.map +1 -0
- package/dist/console/make/naming.d.ts +32 -0
- package/dist/console/make/naming.d.ts.map +1 -0
- package/dist/console/make/naming.js +82 -0
- package/dist/console/make/naming.js.map +1 -0
- package/dist/console/make/resolve.d.ts +46 -0
- package/dist/console/make/resolve.d.ts.map +1 -0
- package/dist/console/make/resolve.js +113 -0
- package/dist/console/make/resolve.js.map +1 -0
- package/dist/console/ui.d.ts +9 -15
- package/dist/console/ui.d.ts.map +1 -1
- package/dist/console/ui.js +11 -24
- package/dist/console/ui.js.map +1 -1
- package/dist/dev/hmr.d.ts +52 -4
- package/dist/dev/hmr.d.ts.map +1 -1
- package/dist/dev/hmr.js +75 -9
- package/dist/dev/hmr.js.map +1 -1
- package/dist/dev/hotGlob.d.ts +10 -3
- package/dist/dev/hotGlob.d.ts.map +1 -1
- package/dist/dev/hotGlob.js +28 -3
- package/dist/dev/hotGlob.js.map +1 -1
- package/dist/hot.js +56 -3
- package/dist/hot.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/stubs/index.d.ts +11 -0
- package/dist/stubs/index.d.ts.map +1 -0
- package/dist/stubs/index.js +12 -0
- package/dist/stubs/index.js.map +1 -0
- package/dist/stubs/template.d.ts +63 -0
- package/dist/stubs/template.d.ts.map +1 -0
- package/dist/stubs/template.js +209 -0
- package/dist/stubs/template.js.map +1 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +7 -1
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Codemods.ts +206 -29
- package/src/Ignitor.ts +44 -14
- package/src/console/Kernel.ts +21 -1
- package/src/console/cliui.ts +44 -754
- package/src/console/make/commands.ts +270 -0
- package/src/console/make/eject.ts +163 -0
- package/src/console/make/flush.ts +125 -0
- package/src/console/make/generators.ts +176 -0
- package/src/console/make/naming.ts +85 -0
- package/src/console/make/resolve.ts +134 -0
- package/src/console/ui.ts +12 -24
- package/src/dev/hmr.ts +102 -9
- package/src/dev/hotGlob.ts +28 -3
- package/src/hot.ts +59 -3
- package/src/index.ts +17 -3
- package/src/stubs/index.ts +14 -0
- package/src/stubs/template.ts +262 -0
- package/stubs/make/command.stub +29 -0
- package/stubs/make/controller.stub +30 -0
- package/stubs/make/entity.stub +11 -0
- package/stubs/make/event.stub +13 -0
- package/stubs/make/listener.stub +13 -0
- package/stubs/make/middleware.stub +16 -0
- package/stubs/make/provider.stub +26 -0
- package/stubs/make/service.stub +27 -0
- 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:
|
|
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
|
|
276
|
-
|
|
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.
|
|
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 `"` 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 '&'
|
|
69
|
+
if (char === '"') return '"'
|
|
70
|
+
if (char === '<') return '<'
|
|
71
|
+
return '>'
|
|
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
|
+
}
|