@holo-js/cli 0.1.9 → 0.2.1
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/bin/holo.mjs +179 -81
- package/dist/broadcast-III5MB3R.mjs +203 -0
- package/dist/broadcast-ZIFYFOUQ.mjs +203 -0
- package/dist/{cache-ETOIQ5IG.mjs → cache-634WUR3T.mjs} +6 -6
- package/dist/cache-7J7DIOP6.mjs +66 -0
- package/dist/{cache-migrations-2GGI4TJK.mjs → cache-migrations-2NBEUF2T.mjs} +50 -30
- package/dist/cache-migrations-S2LJMDOQ.mjs +173 -0
- package/dist/{chunk-WRZFATUT.mjs → chunk-4OHJC3GL.mjs} +232 -143
- package/dist/{chunk-ASTSSSL2.mjs → chunk-5TEH2QPK.mjs} +99 -122
- package/dist/{chunk-F4MT6GBK.mjs → chunk-FGQ2I2YH.mjs} +1 -1
- package/dist/chunk-I7QBCEV7.mjs +33 -0
- package/dist/{chunk-R6BWRY3E.mjs → chunk-ILU426CF.mjs} +3 -1
- package/dist/{chunk-IMOGEKB4.mjs → chunk-J76GH2DR.mjs} +229 -119
- package/dist/{chunk-HB4Q7VYK.mjs → chunk-KS5TWO75.mjs} +30 -273
- package/dist/{chunk-57SJ566R.mjs → chunk-LBJAJLKU.mjs} +1 -1
- package/dist/{chunk-BAFQ2GOA.mjs → chunk-LXGQCG56.mjs} +1 -1
- package/dist/chunk-MCVRN7KX.mjs +3308 -0
- package/dist/chunk-SFRAGRHY.mjs +472 -0
- package/dist/{chunk-7JR73TOH.mjs → chunk-VP2E62DF.mjs} +36 -25
- package/dist/{chunk-5EU32E7X.mjs → chunk-VRGB6DIS.mjs} +116 -12
- package/dist/{chunk-SRPGIWCF.mjs → chunk-YEFJBN56.mjs} +2 -2
- package/dist/{config-ARLE6PKR.mjs → config-MD27U4FM.mjs} +3 -3
- package/dist/{dev-6RG5SSZ7.mjs → dev-M2GGURAX.mjs} +9 -7
- package/dist/dev-PBNFQK6Y.mjs +44 -0
- package/dist/{discovery-FCVGQQVD.mjs → discovery-GWTBF5RZ.mjs} +3 -3
- package/dist/{generators-UI2LJK3O.mjs → generators-BZJ53PUU.mjs} +13 -36
- package/dist/generators-DEPLONDJ.mjs +520 -0
- package/dist/index.mjs +181 -83
- package/dist/{media-migrations-JQSDCC7S.mjs → media-migrations-5EISZBSD.mjs} +9 -20
- package/dist/media-migrations-NMUWBEKE.mjs +106 -0
- package/dist/{queue-BY3PLH4I.mjs → queue-FRAVPNFJ.mjs} +12 -12
- package/dist/queue-GY7BWGTX.mjs +625 -0
- package/dist/{queue-migrations-YZUKEZK7.mjs → queue-migrations-J7YPIKRB.mjs} +13 -12
- package/dist/queue-migrations-O24ERNFF.mjs +167 -0
- package/dist/{runtime-BI343WHS.mjs → runtime-2AA7ZLJ6.mjs} +9 -9
- package/dist/{runtime-ZKD6URAV.mjs → runtime-GSXF4NB3.mjs} +1 -1
- package/dist/runtime-HGK2MWSC.mjs +57 -0
- package/dist/runtime-worker.d.ts +2 -0
- package/dist/runtime-worker.mjs +276 -0
- package/dist/{scaffold-UBOS2NZR.mjs → scaffold-DEOTRALR.mjs} +9 -5
- package/dist/scaffold-Y232IGYS.mjs +139 -0
- package/dist/{security-TYPVOYGF.mjs → security-MZW2CJKS.mjs} +6 -6
- package/dist/security-XVG673UR.mjs +71 -0
- package/package.json +9 -7
- package/dist/broadcast-VR46UZEL.mjs +0 -84
- package/dist/chunk-ZXDU7RHU.mjs +0 -9
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveStringFlag
|
|
3
|
+
} from "./chunk-VRGB6DIS.mjs";
|
|
4
|
+
import {
|
|
5
|
+
runProjectPrepare
|
|
6
|
+
} from "./chunk-5TEH2QPK.mjs";
|
|
7
|
+
import "./chunk-FGQ2I2YH.mjs";
|
|
8
|
+
import {
|
|
9
|
+
hasRegisteredCreateTableMigration,
|
|
10
|
+
hasRegisteredMigrationSlug,
|
|
11
|
+
nextMigrationTemplate
|
|
12
|
+
} from "./chunk-LXGQCG56.mjs";
|
|
13
|
+
import {
|
|
14
|
+
writeLine
|
|
15
|
+
} from "./chunk-I7QBCEV7.mjs";
|
|
16
|
+
import {
|
|
17
|
+
ensureAbsent,
|
|
18
|
+
fileExists
|
|
19
|
+
} from "./chunk-LBJAJLKU.mjs";
|
|
20
|
+
import "./chunk-D7O4SU6N.mjs";
|
|
21
|
+
import {
|
|
22
|
+
collectFiles,
|
|
23
|
+
prepareProjectDiscovery
|
|
24
|
+
} from "./chunk-VP2E62DF.mjs";
|
|
25
|
+
import "./chunk-4OHJC3GL.mjs";
|
|
26
|
+
import {
|
|
27
|
+
ensureGeneratedSchemaPlaceholder,
|
|
28
|
+
ensureProjectConfig
|
|
29
|
+
} from "./chunk-YEFJBN56.mjs";
|
|
30
|
+
import {
|
|
31
|
+
ensureSuffix,
|
|
32
|
+
loadGeneratedProjectRegistry,
|
|
33
|
+
relativeImportPath,
|
|
34
|
+
renderBroadcastTemplate,
|
|
35
|
+
renderChannelTemplate,
|
|
36
|
+
renderEventTemplate,
|
|
37
|
+
renderFactoryTemplate,
|
|
38
|
+
renderJobTemplate,
|
|
39
|
+
renderListenerTemplate,
|
|
40
|
+
renderMarkdownMailTemplate,
|
|
41
|
+
renderModelTemplate,
|
|
42
|
+
renderMultiListenerTemplate,
|
|
43
|
+
renderObserverTemplate,
|
|
44
|
+
renderSeederTemplate,
|
|
45
|
+
resolveArtifactPath,
|
|
46
|
+
resolveNameInfo,
|
|
47
|
+
splitRequestedName,
|
|
48
|
+
toKebabCase,
|
|
49
|
+
toPascalCase,
|
|
50
|
+
toSnakeCase
|
|
51
|
+
} from "./chunk-J76GH2DR.mjs";
|
|
52
|
+
import {
|
|
53
|
+
makeProjectRelativePath,
|
|
54
|
+
resolveDefaultArtifactPath,
|
|
55
|
+
writeTextFile
|
|
56
|
+
} from "./chunk-ILU426CF.mjs";
|
|
57
|
+
|
|
58
|
+
// src/generators.ts
|
|
59
|
+
import { createHash } from "crypto";
|
|
60
|
+
import { readFile } from "fs/promises";
|
|
61
|
+
import { basename, extname, resolve } from "path";
|
|
62
|
+
import { normalizeMigrationSlug } from "@holo-js/db";
|
|
63
|
+
var MAIL_VIEW_SCAFFOLDING_UNAVAILABLE_MESSAGE = 'View-backed mail scaffolding requires a renderView runtime binding, which the first-party app scaffolds do not configure yet. Use "--markdown" instead.';
|
|
64
|
+
function hasRegisteredModelName(registry, modelName) {
|
|
65
|
+
return Boolean(registry?.models.some((entry) => entry.name === modelName));
|
|
66
|
+
}
|
|
67
|
+
function findRegisteredModelByTableName(registry, tableName) {
|
|
68
|
+
return registry?.models.find((entry) => entry.tableName === tableName);
|
|
69
|
+
}
|
|
70
|
+
async function findGeneratedModelSourceByTableName(projectRoot, modelsPath, tableName) {
|
|
71
|
+
const files = await collectFiles(resolve(projectRoot, modelsPath));
|
|
72
|
+
const generatedTableReference = tableName;
|
|
73
|
+
for (const filePath of files) {
|
|
74
|
+
const contents = await readFile(filePath, "utf8");
|
|
75
|
+
if (containsDefineModelTableReference(contents, generatedTableReference)) {
|
|
76
|
+
return basename(filePath, extname(filePath));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
function containsDefineModelTableReference(contents, tableName) {
|
|
82
|
+
let index = 0;
|
|
83
|
+
while (index < contents.length) {
|
|
84
|
+
const nextReference = contents.indexOf("defineModel", index);
|
|
85
|
+
if (nextReference === -1) return false;
|
|
86
|
+
if (isInsideComment(contents, nextReference)) {
|
|
87
|
+
index = nextReference + "defineModel".length;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const before = contents[nextReference - 1];
|
|
91
|
+
const after = contents[nextReference + "defineModel".length];
|
|
92
|
+
const hasIdentifierBoundary = !isIdentifierCharacter(before) && !isIdentifierCharacter(after);
|
|
93
|
+
if (!hasIdentifierBoundary) {
|
|
94
|
+
index = nextReference + "defineModel".length;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const openParenIndex = skipWhitespace(contents, nextReference + "defineModel".length);
|
|
98
|
+
if (contents[openParenIndex] !== "(") {
|
|
99
|
+
index = nextReference + "defineModel".length;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const firstArgumentIndex = skipWhitespace(contents, openParenIndex + 1);
|
|
103
|
+
const quote = contents[firstArgumentIndex];
|
|
104
|
+
if (quote !== "'" && quote !== '"' && quote !== "`") {
|
|
105
|
+
index = firstArgumentIndex;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const literal = readStringLiteral(contents, firstArgumentIndex, quote);
|
|
109
|
+
if (literal?.value === tableName) return true;
|
|
110
|
+
index = literal?.endIndex ?? firstArgumentIndex + 1;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function isInsideComment(contents, position) {
|
|
115
|
+
let index = 0;
|
|
116
|
+
while (index < position) {
|
|
117
|
+
const current = contents[index];
|
|
118
|
+
const next = contents[index + 1];
|
|
119
|
+
if (current === "'" || current === '"' || current === "`") {
|
|
120
|
+
index = readStringLiteral(contents, index, current)?.endIndex ?? index + 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (current === "/" && next === "/") {
|
|
124
|
+
const end = contents.indexOf("\n", index + 2);
|
|
125
|
+
if (end === -1 || end >= position) return true;
|
|
126
|
+
index = end + 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (current === "/" && next === "*") {
|
|
130
|
+
const end = contents.indexOf("*/", index + 2);
|
|
131
|
+
if (end === -1 || end + 2 >= position) return true;
|
|
132
|
+
index = end + 2;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
index += 1;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
function isIdentifierCharacter(value) {
|
|
140
|
+
return typeof value === "string" && /[$\w]/.test(value);
|
|
141
|
+
}
|
|
142
|
+
function skipWhitespace(contents, startIndex) {
|
|
143
|
+
let index = startIndex;
|
|
144
|
+
while (/\s/.test(contents[index] ?? "")) {
|
|
145
|
+
index += 1;
|
|
146
|
+
}
|
|
147
|
+
return index;
|
|
148
|
+
}
|
|
149
|
+
function readStringLiteral(contents, startIndex, quote) {
|
|
150
|
+
let value = "";
|
|
151
|
+
let index = startIndex + 1;
|
|
152
|
+
while (index < contents.length) {
|
|
153
|
+
const current = contents[index];
|
|
154
|
+
if (current === "\\") {
|
|
155
|
+
const escaped = contents[index + 1];
|
|
156
|
+
if (typeof escaped === "string") {
|
|
157
|
+
value += escaped;
|
|
158
|
+
index += 2;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (current === quote) {
|
|
163
|
+
return { value, endIndex: index + 1 };
|
|
164
|
+
}
|
|
165
|
+
if (typeof current === "string") {
|
|
166
|
+
value += current;
|
|
167
|
+
}
|
|
168
|
+
index += 1;
|
|
169
|
+
}
|
|
170
|
+
return void 0;
|
|
171
|
+
}
|
|
172
|
+
function hasRegisteredJobName(registry, jobName) {
|
|
173
|
+
return Boolean(registry?.jobs.some((entry) => entry.name === jobName));
|
|
174
|
+
}
|
|
175
|
+
function hasRegisteredEventName(registry, eventName) {
|
|
176
|
+
return Boolean(registry?.events.some((entry) => entry.name === eventName));
|
|
177
|
+
}
|
|
178
|
+
function hasRegisteredListenerId(registry, listenerId) {
|
|
179
|
+
return Boolean(registry?.listeners.some((entry) => entry.id === listenerId));
|
|
180
|
+
}
|
|
181
|
+
function toChannelTemplateFileStem(pattern) {
|
|
182
|
+
const rawSegments = pattern.trim().replace(/\\/g, "/").replace(/^\.+/, "").split(/[/.]/).map((segment) => segment.trim()).filter(Boolean);
|
|
183
|
+
const hasOnlyWildcardSegments = rawSegments.length > 0 && rawSegments.every((segment) => /^\{[^}]+\}$/.test(segment));
|
|
184
|
+
const normalized = pattern.trim().replace(/\\/g, "/").replace(/^\.+/, "").replace(/\{([^}]+)\}/g, "$1").replace(/[^a-z0-9/]+/gi, "-").replace(/-+/g, "-").replace(/^[-/]+|[-/]+$/g, "");
|
|
185
|
+
const fileStem = normalized.split("/").filter(Boolean).map((segment) => toKebabCase(segment)).join("/") || "channel";
|
|
186
|
+
if (!hasOnlyWildcardSegments) {
|
|
187
|
+
return fileStem;
|
|
188
|
+
}
|
|
189
|
+
const suffix = createHash("sha1").update(pattern.trim()).digest("hex").slice(0, 8);
|
|
190
|
+
return `${fileStem}-${suffix}`;
|
|
191
|
+
}
|
|
192
|
+
function resolveChannelArtifactPath(projectRoot, channelsPath, pattern, registry) {
|
|
193
|
+
const fileStem = toChannelTemplateFileStem(pattern);
|
|
194
|
+
const filePath = resolveArtifactPath(projectRoot, channelsPath, "", `${fileStem}.ts`);
|
|
195
|
+
const relativePath = makeProjectRelativePath(projectRoot, filePath);
|
|
196
|
+
const collidesWithDifferentRegisteredPattern = registry.channels.some((entry) => {
|
|
197
|
+
return entry.sourcePath === relativePath && entry.pattern !== pattern;
|
|
198
|
+
});
|
|
199
|
+
if (!collidesWithDifferentRegisteredPattern) {
|
|
200
|
+
return filePath;
|
|
201
|
+
}
|
|
202
|
+
const suffix = createHash("sha1").update(pattern.trim()).digest("hex").slice(0, 8);
|
|
203
|
+
return resolveArtifactPath(projectRoot, channelsPath, "", `${fileStem}-${suffix}.ts`);
|
|
204
|
+
}
|
|
205
|
+
function resolveConfiguredBroadcastPath(project) {
|
|
206
|
+
const configuredPaths = project.config.paths;
|
|
207
|
+
return configuredPaths.broadcast ?? "server/broadcast";
|
|
208
|
+
}
|
|
209
|
+
function resolveConfiguredChannelsPath(project) {
|
|
210
|
+
const configuredPaths = project.config.paths;
|
|
211
|
+
return configuredPaths.channels ?? "server/channels";
|
|
212
|
+
}
|
|
213
|
+
async function runMakeModel(io, projectRoot, input) {
|
|
214
|
+
const project = await ensureProjectConfig(projectRoot);
|
|
215
|
+
const generatedSchemaFilePath = await ensureGeneratedSchemaPlaceholder(projectRoot, project.config);
|
|
216
|
+
const registry = await loadGeneratedProjectRegistry(projectRoot) ?? await prepareProjectDiscovery(projectRoot, project.config);
|
|
217
|
+
const requestedName = String(input.args[0] ?? "");
|
|
218
|
+
const options = {
|
|
219
|
+
migration: input.flags.migration === true,
|
|
220
|
+
observer: input.flags.observer === true,
|
|
221
|
+
seeder: input.flags.seeder === true,
|
|
222
|
+
factory: input.flags.factory === true
|
|
223
|
+
};
|
|
224
|
+
const nameInfo = resolveNameInfo(requestedName);
|
|
225
|
+
const explicitTableName = resolveStringFlag(input.flags, "table");
|
|
226
|
+
const tableName = explicitTableName ? toSnakeCase(explicitTableName) : nameInfo.tableName;
|
|
227
|
+
const modelFilePath = resolveArtifactPath(projectRoot, project.config.paths.models, nameInfo.directory, `${nameInfo.baseName}.ts`);
|
|
228
|
+
const observerInfo = resolveNameInfo(`${requestedName}Observer`, { suffix: "Observer" });
|
|
229
|
+
const observerFilePath = resolveArtifactPath(projectRoot, project.config.paths.observers, observerInfo.directory, `${observerInfo.baseName}.ts`);
|
|
230
|
+
const seederInfo = resolveNameInfo(`${requestedName}Seeder`, { suffix: "Seeder" });
|
|
231
|
+
const seederFilePath = resolveArtifactPath(projectRoot, project.config.paths.seeders, seederInfo.directory, `${seederInfo.baseName}.ts`);
|
|
232
|
+
const factoryInfo = resolveNameInfo(`${requestedName}Factory`, { suffix: "Factory" });
|
|
233
|
+
const factoryFilePath = resolveArtifactPath(projectRoot, project.config.paths.factories, factoryInfo.directory, `${factoryInfo.baseName}.ts`);
|
|
234
|
+
if (await fileExists(modelFilePath) || hasRegisteredModelName(registry, nameInfo.baseName)) {
|
|
235
|
+
throw new Error(`Model with the same name already exists: ${nameInfo.baseName}.`);
|
|
236
|
+
}
|
|
237
|
+
if (options.migration) {
|
|
238
|
+
const migrationName = normalizeMigrationSlug(`create_${tableName}_table`);
|
|
239
|
+
if (hasRegisteredMigrationSlug(registry, migrationName) || hasRegisteredCreateTableMigration(registry, tableName)) {
|
|
240
|
+
throw new Error(`A migration for table "${tableName}" already exists.`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const existingTableModel = findRegisteredModelByTableName(registry, tableName);
|
|
244
|
+
if (existingTableModel) {
|
|
245
|
+
throw new Error(`Discovered duplicate model "${existingTableModel.name}" for table "${tableName}".`);
|
|
246
|
+
}
|
|
247
|
+
const existingGeneratedModelName = await findGeneratedModelSourceByTableName(
|
|
248
|
+
projectRoot,
|
|
249
|
+
project.config.paths.models,
|
|
250
|
+
tableName
|
|
251
|
+
);
|
|
252
|
+
if (existingGeneratedModelName) {
|
|
253
|
+
throw new Error(`Discovered duplicate model "${existingGeneratedModelName}" for table "${tableName}".`);
|
|
254
|
+
}
|
|
255
|
+
await ensureAbsent(modelFilePath);
|
|
256
|
+
if (options.observer) {
|
|
257
|
+
await ensureAbsent(observerFilePath);
|
|
258
|
+
}
|
|
259
|
+
if (options.seeder) {
|
|
260
|
+
await ensureAbsent(seederFilePath);
|
|
261
|
+
}
|
|
262
|
+
if (options.factory) {
|
|
263
|
+
await ensureAbsent(factoryFilePath);
|
|
264
|
+
}
|
|
265
|
+
if (options.observer) {
|
|
266
|
+
await writeTextFile(observerFilePath, renderObserverTemplate(observerInfo.baseName));
|
|
267
|
+
}
|
|
268
|
+
await writeTextFile(modelFilePath, renderModelTemplate({
|
|
269
|
+
tableName,
|
|
270
|
+
generatedSchemaImportPath: relativeImportPath(modelFilePath, generatedSchemaFilePath),
|
|
271
|
+
...options.observer ? {
|
|
272
|
+
observerImportPath: relativeImportPath(modelFilePath, observerFilePath),
|
|
273
|
+
observerClassName: observerInfo.baseName
|
|
274
|
+
} : {}
|
|
275
|
+
}));
|
|
276
|
+
if (options.factory) {
|
|
277
|
+
await writeTextFile(factoryFilePath, renderFactoryTemplate(
|
|
278
|
+
relativeImportPath(factoryFilePath, modelFilePath),
|
|
279
|
+
nameInfo.baseName
|
|
280
|
+
));
|
|
281
|
+
}
|
|
282
|
+
if (options.seeder) {
|
|
283
|
+
await writeTextFile(seederFilePath, renderSeederTemplate(seederInfo.snakeStem));
|
|
284
|
+
}
|
|
285
|
+
if (options.migration) {
|
|
286
|
+
const migrationName = normalizeMigrationSlug(`create_${tableName}_table`);
|
|
287
|
+
const migrationTemplate = await nextMigrationTemplate(
|
|
288
|
+
migrationName,
|
|
289
|
+
resolve(projectRoot, project.config.paths.migrations)
|
|
290
|
+
);
|
|
291
|
+
const migrationFilePath = resolveDefaultArtifactPath(projectRoot, project.config.paths.migrations, migrationTemplate.fileName);
|
|
292
|
+
await writeTextFile(migrationFilePath, migrationTemplate.contents);
|
|
293
|
+
}
|
|
294
|
+
await runProjectPrepare(projectRoot);
|
|
295
|
+
writeLine(io.stdout, `Created model: ${makeProjectRelativePath(projectRoot, modelFilePath)}`);
|
|
296
|
+
if (options.migration) {
|
|
297
|
+
writeLine(io.stdout, `Registered migration for ${nameInfo.baseName}.`);
|
|
298
|
+
}
|
|
299
|
+
if (options.seeder) {
|
|
300
|
+
writeLine(io.stdout, `Registered seeder for ${nameInfo.baseName}.`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async function runMakeMigration(io, projectRoot, input) {
|
|
304
|
+
const project = await ensureProjectConfig(projectRoot);
|
|
305
|
+
const registry = await loadGeneratedProjectRegistry(projectRoot) ?? await prepareProjectDiscovery(projectRoot, project.config);
|
|
306
|
+
const requestedName = String(input.args[0] ?? "");
|
|
307
|
+
const createTable = typeof input.flags.create === "string" ? normalizeMigrationSlug(input.flags.create) : void 0;
|
|
308
|
+
const alterTable = typeof input.flags.table === "string" ? normalizeMigrationSlug(input.flags.table) : void 0;
|
|
309
|
+
if (createTable && alterTable) {
|
|
310
|
+
throw new Error('Use either "--create" or "--table", not both.');
|
|
311
|
+
}
|
|
312
|
+
const requestedSlug = normalizeMigrationSlug(requestedName);
|
|
313
|
+
if (createTable) {
|
|
314
|
+
if (hasRegisteredCreateTableMigration(registry, createTable)) {
|
|
315
|
+
throw new Error(`A migration for table "${createTable}" already exists.`);
|
|
316
|
+
}
|
|
317
|
+
} else if (alterTable) {
|
|
318
|
+
if (hasRegisteredMigrationSlug(registry, requestedSlug)) {
|
|
319
|
+
throw new Error(`A migration named "${requestedSlug}" already exists.`);
|
|
320
|
+
}
|
|
321
|
+
} else if (hasRegisteredMigrationSlug(registry, requestedSlug)) {
|
|
322
|
+
throw new Error(`A migration named "${requestedSlug}" already exists.`);
|
|
323
|
+
}
|
|
324
|
+
const migrationTemplate = await nextMigrationTemplate(
|
|
325
|
+
requestedSlug,
|
|
326
|
+
resolve(projectRoot, project.config.paths.migrations),
|
|
327
|
+
{
|
|
328
|
+
...createTable ? { kind: "create_table", tableName: createTable } : {},
|
|
329
|
+
...alterTable ? { kind: "alter_table", tableName: alterTable } : {}
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
const migrationFilePath = resolveDefaultArtifactPath(projectRoot, project.config.paths.migrations, migrationTemplate.fileName);
|
|
333
|
+
await writeTextFile(migrationFilePath, migrationTemplate.contents);
|
|
334
|
+
await runProjectPrepare(projectRoot);
|
|
335
|
+
writeLine(io.stdout, `Created migration: ${makeProjectRelativePath(projectRoot, migrationFilePath)}`);
|
|
336
|
+
}
|
|
337
|
+
async function runMakeSeeder(io, projectRoot, input) {
|
|
338
|
+
const project = await ensureProjectConfig(projectRoot);
|
|
339
|
+
const info = resolveNameInfo(String(input.args[0] ?? ""), { suffix: "Seeder" });
|
|
340
|
+
const filePath = resolveArtifactPath(projectRoot, project.config.paths.seeders, info.directory, `${info.baseName}.ts`);
|
|
341
|
+
await ensureAbsent(filePath);
|
|
342
|
+
await writeTextFile(filePath, renderSeederTemplate(info.snakeStem));
|
|
343
|
+
await runProjectPrepare(projectRoot);
|
|
344
|
+
writeLine(io.stdout, `Created seeder: ${makeProjectRelativePath(projectRoot, filePath)}`);
|
|
345
|
+
}
|
|
346
|
+
async function runMakeJob(io, projectRoot, input) {
|
|
347
|
+
const project = await ensureProjectConfig(projectRoot);
|
|
348
|
+
const registry = await loadGeneratedProjectRegistry(projectRoot) ?? await prepareProjectDiscovery(projectRoot, project.config);
|
|
349
|
+
const requestedName = String(input.args[0] ?? "");
|
|
350
|
+
const nameParts = splitRequestedName(requestedName);
|
|
351
|
+
const directory = nameParts.directory.split("/").filter(Boolean).map((segment) => toKebabCase(segment)).join("/");
|
|
352
|
+
const fileStem = toKebabCase(nameParts.rawBaseName);
|
|
353
|
+
const filePath = resolveArtifactPath(projectRoot, project.config.paths.jobs, directory, `${fileStem}.ts`);
|
|
354
|
+
const jobName = [...directory ? directory.split("/") : [], fileStem].join(".");
|
|
355
|
+
if (await fileExists(filePath) || hasRegisteredJobName(registry, jobName)) {
|
|
356
|
+
throw new Error(`Job with the same name already exists: ${jobName}.`);
|
|
357
|
+
}
|
|
358
|
+
await ensureAbsent(filePath);
|
|
359
|
+
await writeTextFile(filePath, renderJobTemplate());
|
|
360
|
+
await runProjectPrepare(projectRoot);
|
|
361
|
+
writeLine(io.stdout, `Created job: ${makeProjectRelativePath(projectRoot, filePath)}`);
|
|
362
|
+
}
|
|
363
|
+
async function runMakeEvent(io, projectRoot, input) {
|
|
364
|
+
const project = await ensureProjectConfig(projectRoot);
|
|
365
|
+
const registry = await loadGeneratedProjectRegistry(projectRoot) ?? await prepareProjectDiscovery(projectRoot, project.config);
|
|
366
|
+
const requestedName = String(input.args[0] ?? "");
|
|
367
|
+
const nameParts = splitRequestedName(requestedName);
|
|
368
|
+
const directory = nameParts.directory.split("/").filter(Boolean).map((segment) => toKebabCase(segment)).join("/");
|
|
369
|
+
const fileStem = toKebabCase(nameParts.rawBaseName);
|
|
370
|
+
const filePath = resolveArtifactPath(projectRoot, project.config.paths.events, directory, `${fileStem}.ts`);
|
|
371
|
+
const eventName = [...directory ? directory.split("/") : [], fileStem].join(".");
|
|
372
|
+
if (await fileExists(filePath) || hasRegisteredEventName(registry, eventName)) {
|
|
373
|
+
throw new Error(`Event with the same name already exists: ${eventName}.`);
|
|
374
|
+
}
|
|
375
|
+
await ensureAbsent(filePath);
|
|
376
|
+
await writeTextFile(filePath, renderEventTemplate(eventName));
|
|
377
|
+
await runProjectPrepare(projectRoot);
|
|
378
|
+
writeLine(io.stdout, `Created event: ${makeProjectRelativePath(projectRoot, filePath)}`);
|
|
379
|
+
}
|
|
380
|
+
async function runMakeBroadcast(io, projectRoot, input) {
|
|
381
|
+
const project = await ensureProjectConfig(projectRoot);
|
|
382
|
+
const registry = await loadGeneratedProjectRegistry(projectRoot) ?? await prepareProjectDiscovery(projectRoot, project.config);
|
|
383
|
+
const requestedName = String(input.args[0] ?? "");
|
|
384
|
+
const nameParts = splitRequestedName(requestedName);
|
|
385
|
+
const directory = nameParts.directory.split("/").filter(Boolean).map((segment) => toKebabCase(segment)).join("/");
|
|
386
|
+
const fileStem = toKebabCase(nameParts.rawBaseName);
|
|
387
|
+
const broadcastPath = resolveConfiguredBroadcastPath(project);
|
|
388
|
+
const filePath = resolveArtifactPath(projectRoot, broadcastPath, directory, `${fileStem}.ts`);
|
|
389
|
+
const eventName = [...directory ? directory.split("/") : [], fileStem].join(".");
|
|
390
|
+
if (await fileExists(filePath) || registry.broadcast.some((entry) => entry.name === eventName)) {
|
|
391
|
+
throw new Error(`Broadcast with the same name already exists: ${eventName}.`);
|
|
392
|
+
}
|
|
393
|
+
await ensureAbsent(filePath);
|
|
394
|
+
await writeTextFile(filePath, renderBroadcastTemplate(eventName));
|
|
395
|
+
await runProjectPrepare(projectRoot);
|
|
396
|
+
writeLine(io.stdout, `Created broadcast: ${makeProjectRelativePath(projectRoot, filePath)}`);
|
|
397
|
+
}
|
|
398
|
+
async function runMakeChannel(io, projectRoot, input) {
|
|
399
|
+
const project = await ensureProjectConfig(projectRoot);
|
|
400
|
+
const registry = await loadGeneratedProjectRegistry(projectRoot) ?? await prepareProjectDiscovery(projectRoot, project.config);
|
|
401
|
+
const pattern = String(input.args[0] ?? "").trim();
|
|
402
|
+
if (!pattern) {
|
|
403
|
+
throw new Error("A channel pattern is required.");
|
|
404
|
+
}
|
|
405
|
+
if (registry.channels.some((entry) => entry.pattern === pattern)) {
|
|
406
|
+
throw new Error(`Channel with the same pattern already exists: ${pattern}.`);
|
|
407
|
+
}
|
|
408
|
+
const channelsPath = resolveConfiguredChannelsPath(project);
|
|
409
|
+
const filePath = resolveChannelArtifactPath(projectRoot, channelsPath, pattern, registry);
|
|
410
|
+
await ensureAbsent(filePath);
|
|
411
|
+
await writeTextFile(filePath, renderChannelTemplate(pattern));
|
|
412
|
+
await runProjectPrepare(projectRoot);
|
|
413
|
+
writeLine(io.stdout, `Created channel: ${makeProjectRelativePath(projectRoot, filePath)}`);
|
|
414
|
+
}
|
|
415
|
+
async function runMakeListener(io, projectRoot, input) {
|
|
416
|
+
const project = await ensureProjectConfig(projectRoot);
|
|
417
|
+
const registry = await loadGeneratedProjectRegistry(projectRoot) ?? await prepareProjectDiscovery(projectRoot, project.config);
|
|
418
|
+
const requestedName = String(input.args[0] ?? "");
|
|
419
|
+
const requestedEvents = Array.isArray(input.flags.event) ? input.flags.event.map((value) => String(value).trim()).filter(Boolean) : [String(input.flags.event ?? "").trim()].filter(Boolean);
|
|
420
|
+
const eventNames = [...new Set(requestedEvents)];
|
|
421
|
+
const eventEntries = eventNames.map((eventName) => {
|
|
422
|
+
const entry = registry?.events.find((candidate) => candidate.name === eventName);
|
|
423
|
+
if (!entry) {
|
|
424
|
+
throw new Error(`Unknown event: ${eventName}.`);
|
|
425
|
+
}
|
|
426
|
+
return entry;
|
|
427
|
+
});
|
|
428
|
+
const nameParts = splitRequestedName(requestedName);
|
|
429
|
+
const directory = nameParts.directory.split("/").filter(Boolean).map((segment) => toKebabCase(segment)).join("/");
|
|
430
|
+
const fileStem = toKebabCase(nameParts.rawBaseName);
|
|
431
|
+
const filePath = resolveArtifactPath(projectRoot, project.config.paths.listeners, directory, `${fileStem}.ts`);
|
|
432
|
+
const listenerId = [...directory ? directory.split("/") : [], fileStem].join(".");
|
|
433
|
+
if (await fileExists(filePath) || hasRegisteredListenerId(registry, listenerId)) {
|
|
434
|
+
throw new Error(`Listener with the same id already exists: ${listenerId}.`);
|
|
435
|
+
}
|
|
436
|
+
const templateEvents = eventEntries.map((eventEntry, index) => {
|
|
437
|
+
const sourceEventBaseName = eventEntry.sourcePath.split("/").pop().replace(/\.[^.]+$/, "");
|
|
438
|
+
const importName = `${toPascalCase(sourceEventBaseName)}Event${index + 1}`;
|
|
439
|
+
const importPath = relativeImportPath(filePath, resolve(projectRoot, eventEntry.sourcePath));
|
|
440
|
+
return {
|
|
441
|
+
importName,
|
|
442
|
+
importStatement: eventEntry.exportName && eventEntry.exportName !== "default" ? `import { ${eventEntry.exportName} as ${importName} } from '${importPath}'` : `import ${importName} from '${importPath}'`
|
|
443
|
+
};
|
|
444
|
+
});
|
|
445
|
+
await ensureAbsent(filePath);
|
|
446
|
+
await writeTextFile(
|
|
447
|
+
filePath,
|
|
448
|
+
templateEvents.length === 1 ? renderListenerTemplate(templateEvents[0].importStatement, templateEvents[0].importName) : renderMultiListenerTemplate(templateEvents)
|
|
449
|
+
);
|
|
450
|
+
await runProjectPrepare(projectRoot);
|
|
451
|
+
writeLine(io.stdout, `Created listener: ${makeProjectRelativePath(projectRoot, filePath)}`);
|
|
452
|
+
}
|
|
453
|
+
async function runMakeObserver(io, projectRoot, input) {
|
|
454
|
+
const project = await ensureProjectConfig(projectRoot);
|
|
455
|
+
const info = resolveNameInfo(String(input.args[0] ?? ""), { suffix: "Observer" });
|
|
456
|
+
const filePath = resolveArtifactPath(projectRoot, project.config.paths.observers, info.directory, `${info.baseName}.ts`);
|
|
457
|
+
await ensureAbsent(filePath);
|
|
458
|
+
await writeTextFile(filePath, renderObserverTemplate(info.baseName));
|
|
459
|
+
await runProjectPrepare(projectRoot);
|
|
460
|
+
writeLine(io.stdout, `Created observer: ${makeProjectRelativePath(projectRoot, filePath)}`);
|
|
461
|
+
}
|
|
462
|
+
async function runMakeFactory(io, projectRoot, input) {
|
|
463
|
+
const project = await ensureProjectConfig(projectRoot);
|
|
464
|
+
const info = resolveNameInfo(String(input.args[0] ?? ""), { suffix: "Factory" });
|
|
465
|
+
const filePath = resolveArtifactPath(projectRoot, project.config.paths.factories, info.directory, `${info.baseName}.ts`);
|
|
466
|
+
const baseName = info.baseStem;
|
|
467
|
+
const modelInfo = splitRequestedName(baseName);
|
|
468
|
+
const modelFilePath = resolveArtifactPath(
|
|
469
|
+
projectRoot,
|
|
470
|
+
project.config.paths.models,
|
|
471
|
+
info.directory,
|
|
472
|
+
`${toPascalCase(modelInfo.rawBaseName)}.ts`
|
|
473
|
+
);
|
|
474
|
+
await ensureAbsent(filePath);
|
|
475
|
+
await writeTextFile(filePath, renderFactoryTemplate(
|
|
476
|
+
relativeImportPath(filePath, modelFilePath),
|
|
477
|
+
toPascalCase(modelInfo.rawBaseName)
|
|
478
|
+
));
|
|
479
|
+
await runProjectPrepare(projectRoot);
|
|
480
|
+
writeLine(io.stdout, `Created factory: ${makeProjectRelativePath(projectRoot, filePath)}`);
|
|
481
|
+
}
|
|
482
|
+
async function runMakeMail(io, projectRoot, input) {
|
|
483
|
+
await ensureProjectConfig(projectRoot);
|
|
484
|
+
const requestedName = String(input.args[0] ?? "");
|
|
485
|
+
const templateType = input.flags.type === "view" ? "view" : "markdown";
|
|
486
|
+
if (templateType === "view") {
|
|
487
|
+
throw new Error(MAIL_VIEW_SCAFFOLDING_UNAVAILABLE_MESSAGE);
|
|
488
|
+
}
|
|
489
|
+
const nameParts = splitRequestedName(requestedName);
|
|
490
|
+
const directory = nameParts.directory.split("/").filter(Boolean).map((segment) => toKebabCase(segment)).join("/");
|
|
491
|
+
const fileStem = toKebabCase(nameParts.rawBaseName);
|
|
492
|
+
const mailName = ensureSuffix(toPascalCase(nameParts.rawBaseName), "Mail");
|
|
493
|
+
const inputTypeName = `${mailName}Input`;
|
|
494
|
+
const mailFilePath = resolveArtifactPath(projectRoot, "server/mail", directory, `${fileStem}.ts`);
|
|
495
|
+
await ensureAbsent(mailFilePath);
|
|
496
|
+
await writeTextFile(mailFilePath, renderMarkdownMailTemplate(mailName, inputTypeName));
|
|
497
|
+
await runProjectPrepare(projectRoot);
|
|
498
|
+
writeLine(io.stdout, `Created mail: ${makeProjectRelativePath(projectRoot, mailFilePath)}`);
|
|
499
|
+
}
|
|
500
|
+
var generatorInternals = {
|
|
501
|
+
toChannelTemplateFileStem
|
|
502
|
+
};
|
|
503
|
+
export {
|
|
504
|
+
generatorInternals,
|
|
505
|
+
hasRegisteredEventName,
|
|
506
|
+
hasRegisteredJobName,
|
|
507
|
+
hasRegisteredListenerId,
|
|
508
|
+
hasRegisteredModelName,
|
|
509
|
+
runMakeBroadcast,
|
|
510
|
+
runMakeChannel,
|
|
511
|
+
runMakeEvent,
|
|
512
|
+
runMakeFactory,
|
|
513
|
+
runMakeJob,
|
|
514
|
+
runMakeListener,
|
|
515
|
+
runMakeMail,
|
|
516
|
+
runMakeMigration,
|
|
517
|
+
runMakeModel,
|
|
518
|
+
runMakeObserver,
|
|
519
|
+
runMakeSeeder
|
|
520
|
+
};
|