@lumerahq/cli 0.19.9-dev.1 → 0.19.9-dev.3
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/index.js
CHANGED
|
@@ -219,25 +219,25 @@ async function main() {
|
|
|
219
219
|
switch (command) {
|
|
220
220
|
// Resource commands
|
|
221
221
|
case "plan":
|
|
222
|
-
await import("./resources-
|
|
222
|
+
await import("./resources-4M4LMSSN.js").then((m) => m.plan(args.slice(1)));
|
|
223
223
|
break;
|
|
224
224
|
case "apply":
|
|
225
|
-
await import("./resources-
|
|
225
|
+
await import("./resources-4M4LMSSN.js").then((m) => m.apply(args.slice(1)));
|
|
226
226
|
break;
|
|
227
227
|
case "pull":
|
|
228
|
-
await import("./resources-
|
|
228
|
+
await import("./resources-4M4LMSSN.js").then((m) => m.pull(args.slice(1)));
|
|
229
229
|
break;
|
|
230
230
|
case "destroy":
|
|
231
|
-
await import("./resources-
|
|
231
|
+
await import("./resources-4M4LMSSN.js").then((m) => m.destroy(args.slice(1)));
|
|
232
232
|
break;
|
|
233
233
|
case "list":
|
|
234
|
-
await import("./resources-
|
|
234
|
+
await import("./resources-4M4LMSSN.js").then((m) => m.list(args.slice(1)));
|
|
235
235
|
break;
|
|
236
236
|
case "show":
|
|
237
|
-
await import("./resources-
|
|
237
|
+
await import("./resources-4M4LMSSN.js").then((m) => m.show(args.slice(1)));
|
|
238
238
|
break;
|
|
239
239
|
case "diff":
|
|
240
|
-
await import("./resources-
|
|
240
|
+
await import("./resources-4M4LMSSN.js").then((m) => m.diff(args.slice(1)));
|
|
241
241
|
break;
|
|
242
242
|
// Development
|
|
243
243
|
case "dev":
|
|
@@ -121,6 +121,7 @@ var collectionSchemaRule = {
|
|
|
121
121
|
};
|
|
122
122
|
|
|
123
123
|
// src/lib/hooks-parse.ts
|
|
124
|
+
import { Parser } from "acorn";
|
|
124
125
|
var VALID_HOOK_TRIGGERS = [
|
|
125
126
|
"before_create",
|
|
126
127
|
"after_create",
|
|
@@ -137,179 +138,272 @@ var KNOWN_HOOK_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
137
138
|
"enabled",
|
|
138
139
|
"metadata"
|
|
139
140
|
]);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
for (const
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
prev = ch;
|
|
156
|
-
continue;
|
|
141
|
+
function isLiteralString(n) {
|
|
142
|
+
return !!n && n.type === "Literal" && typeof n.value === "string";
|
|
143
|
+
}
|
|
144
|
+
function getPropertyKey(prop) {
|
|
145
|
+
const key = prop.key;
|
|
146
|
+
if (!key) return void 0;
|
|
147
|
+
if (key.type === "Identifier") return key.name;
|
|
148
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
function objectExpressionToLiteral(node) {
|
|
152
|
+
const result = {};
|
|
153
|
+
for (const prop of node.properties) {
|
|
154
|
+
if (prop.type !== "Property") {
|
|
155
|
+
throw new Error("spread or shorthand properties are not supported");
|
|
157
156
|
}
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
prev = ch;
|
|
161
|
-
continue;
|
|
157
|
+
if (prop.computed) {
|
|
158
|
+
throw new Error("computed keys are not supported");
|
|
162
159
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
160
|
+
const key = getPropertyKey(prop);
|
|
161
|
+
if (key === void 0) throw new Error("non-string property key");
|
|
162
|
+
result[key] = literalNodeToValue(prop.value);
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
function literalNodeToValue(node) {
|
|
167
|
+
switch (node.type) {
|
|
168
|
+
case "Literal":
|
|
169
|
+
return node.value;
|
|
170
|
+
case "ObjectExpression":
|
|
171
|
+
return objectExpressionToLiteral(node);
|
|
172
|
+
case "ArrayExpression":
|
|
173
|
+
return node.elements.map(
|
|
174
|
+
(el) => el === null ? null : literalNodeToValue(el)
|
|
175
|
+
);
|
|
176
|
+
case "UnaryExpression": {
|
|
177
|
+
const arg = node.argument;
|
|
178
|
+
if (node.operator === "-" && arg.type === "Literal" && typeof arg.value === "number") {
|
|
179
|
+
return -arg.value;
|
|
180
|
+
}
|
|
181
|
+
throw new Error(`unsupported unary operator '${String(node.operator)}'`);
|
|
167
182
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
case "TemplateLiteral": {
|
|
184
|
+
const expressions = node.expressions;
|
|
185
|
+
const quasis = node.quasis;
|
|
186
|
+
if (expressions.length === 0 && quasis.length === 1) {
|
|
187
|
+
return quasis[0].value.cooked;
|
|
188
|
+
}
|
|
189
|
+
throw new Error("template literals with interpolation are not supported");
|
|
172
190
|
}
|
|
173
|
-
|
|
174
|
-
|
|
191
|
+
default:
|
|
192
|
+
throw new Error(`expected a literal value, got ${node.type}`);
|
|
175
193
|
}
|
|
176
|
-
const keys = [];
|
|
177
|
-
const keyRe = /(?:^|,)\s*(?:['"]([^'"]+)['"]|(\w+))\s*:/g;
|
|
178
|
-
let m;
|
|
179
|
-
while ((m = keyRe.exec(stripped)) !== null) {
|
|
180
|
-
keys.push(m[1] || m[2]);
|
|
181
|
-
}
|
|
182
|
-
return keys;
|
|
183
194
|
}
|
|
184
|
-
function
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const enabled = configMatch[1].match(/enabled\s*:\s*(true|false)/)?.[1];
|
|
191
|
-
const name = configMatch[1].match(/name\s*:\s*['"]([^'"]+)['"]/)?.[1];
|
|
192
|
-
if (!collection || !trigger) return null;
|
|
193
|
-
const hook = {
|
|
194
|
-
external_id: externalId || "",
|
|
195
|
-
collection,
|
|
196
|
-
trigger,
|
|
197
|
-
enabled: enabled !== "false"
|
|
195
|
+
function parseHookFile(content) {
|
|
196
|
+
const finding = {
|
|
197
|
+
configFound: false,
|
|
198
|
+
configKeys: [],
|
|
199
|
+
configIssues: [],
|
|
200
|
+
defaultHandlerFound: false
|
|
198
201
|
};
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
202
|
+
let ast;
|
|
203
|
+
try {
|
|
204
|
+
ast = Parser.parse(content, {
|
|
205
|
+
ecmaVersion: "latest",
|
|
206
|
+
sourceType: "module",
|
|
207
|
+
locations: true
|
|
208
|
+
});
|
|
209
|
+
} catch (err) {
|
|
210
|
+
const e = err;
|
|
211
|
+
finding.syntaxError = {
|
|
212
|
+
message: e.message ?? String(err),
|
|
213
|
+
line: e.loc?.line,
|
|
214
|
+
column: e.loc?.column
|
|
215
|
+
};
|
|
216
|
+
return finding;
|
|
217
|
+
}
|
|
218
|
+
let configNode;
|
|
219
|
+
let handlerNode;
|
|
220
|
+
for (const node of ast.body) {
|
|
221
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
222
|
+
const decl = node.declaration;
|
|
223
|
+
if (decl && decl.type === "VariableDeclaration") {
|
|
224
|
+
for (const d of decl.declarations) {
|
|
225
|
+
if (d.id?.type === "Identifier" && d.id.name === "config" && d.init?.type === "ObjectExpression") {
|
|
226
|
+
configNode = d.init;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} else if (node.type === "ExportDefaultDeclaration") {
|
|
231
|
+
const d = node.declaration;
|
|
232
|
+
if (d.type === "FunctionDeclaration" || d.type === "FunctionExpression" || d.type === "ArrowFunctionExpression") {
|
|
233
|
+
handlerNode = d;
|
|
234
|
+
}
|
|
206
235
|
}
|
|
207
236
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
237
|
+
if (configNode) {
|
|
238
|
+
finding.configFound = true;
|
|
239
|
+
const valueNodes = {};
|
|
240
|
+
for (const prop of configNode.properties) {
|
|
241
|
+
if (prop.type !== "Property") continue;
|
|
242
|
+
const key = getPropertyKey(prop);
|
|
243
|
+
if (key === void 0) continue;
|
|
244
|
+
finding.configKeys.push(key);
|
|
245
|
+
valueNodes[key] = prop.value;
|
|
246
|
+
}
|
|
247
|
+
for (const k of finding.configKeys) {
|
|
248
|
+
if (!KNOWN_HOOK_CONFIG_KEYS.has(k)) {
|
|
249
|
+
finding.configIssues.push({ kind: "unknown-key", key: k });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const readString = (field) => {
|
|
253
|
+
const n = valueNodes[field];
|
|
254
|
+
if (!n) return void 0;
|
|
255
|
+
if (isLiteralString(n)) return n.value;
|
|
256
|
+
finding.configIssues.push({ kind: "non-literal-field", field });
|
|
257
|
+
return void 0;
|
|
258
|
+
};
|
|
259
|
+
const external_id = readString("external_id");
|
|
260
|
+
const collection = readString("collection");
|
|
261
|
+
const trigger = readString("trigger");
|
|
262
|
+
const name = readString("name");
|
|
263
|
+
if (!collection && !("collection" in valueNodes)) {
|
|
264
|
+
finding.configIssues.push({ kind: "missing-required-field", field: "collection" });
|
|
265
|
+
}
|
|
266
|
+
if (!trigger && !("trigger" in valueNodes)) {
|
|
267
|
+
finding.configIssues.push({ kind: "missing-required-field", field: "trigger" });
|
|
268
|
+
}
|
|
269
|
+
if (trigger && !VALID_HOOK_TRIGGERS.includes(trigger)) {
|
|
270
|
+
finding.configIssues.push({ kind: "invalid-trigger", value: trigger });
|
|
271
|
+
}
|
|
272
|
+
let enabled;
|
|
273
|
+
if ("enabled" in valueNodes) {
|
|
274
|
+
const n = valueNodes.enabled;
|
|
275
|
+
if (n.type === "Literal" && typeof n.value === "boolean") {
|
|
276
|
+
enabled = n.value;
|
|
277
|
+
} else {
|
|
278
|
+
finding.configIssues.push({
|
|
279
|
+
kind: "invalid-enabled",
|
|
280
|
+
rawSource: content.slice(n.start, n.end)
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
let metadata;
|
|
285
|
+
if ("metadata" in valueNodes) {
|
|
286
|
+
const n = valueNodes.metadata;
|
|
287
|
+
if (n.type === "ObjectExpression") {
|
|
288
|
+
try {
|
|
289
|
+
metadata = objectExpressionToLiteral(n);
|
|
290
|
+
} catch (e) {
|
|
291
|
+
finding.configIssues.push({
|
|
292
|
+
kind: "metadata-unparseable",
|
|
293
|
+
reason: e.message
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
finding.configIssues.push({
|
|
298
|
+
kind: "metadata-unparseable",
|
|
299
|
+
reason: "metadata must be an inline object literal"
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (collection && trigger) {
|
|
304
|
+
finding.hook = {
|
|
305
|
+
external_id: external_id ?? "",
|
|
306
|
+
collection,
|
|
307
|
+
trigger,
|
|
308
|
+
enabled: enabled ?? true,
|
|
309
|
+
...name !== void 0 ? { name } : {},
|
|
310
|
+
...metadata !== void 0 ? { metadata } : {}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
216
313
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
314
|
+
if (handlerNode) {
|
|
315
|
+
finding.defaultHandlerFound = true;
|
|
316
|
+
const body = handlerNode.body;
|
|
317
|
+
if (body) {
|
|
318
|
+
if (body.type === "BlockStatement") {
|
|
319
|
+
finding.scriptBody = content.slice(body.start + 1, body.end - 1).trim();
|
|
320
|
+
} else {
|
|
321
|
+
finding.scriptBody = `return ${content.slice(body.start, body.end)};`;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
222
324
|
}
|
|
223
|
-
return
|
|
325
|
+
return finding;
|
|
326
|
+
}
|
|
327
|
+
function parseHookConfig(content) {
|
|
328
|
+
const finding = parseHookFile(content);
|
|
329
|
+
return finding.hook ?? null;
|
|
224
330
|
}
|
|
225
|
-
function
|
|
226
|
-
|
|
331
|
+
function extractHookScript(content) {
|
|
332
|
+
const finding = parseHookFile(content);
|
|
333
|
+
return finding.scriptBody ?? "";
|
|
227
334
|
}
|
|
228
335
|
|
|
229
336
|
// src/lib/lint/rules/hook-verify.ts
|
|
230
|
-
function error2(target, message) {
|
|
337
|
+
function error2(target, message, line) {
|
|
231
338
|
return {
|
|
232
339
|
ruleId: "hook-verify",
|
|
233
340
|
target,
|
|
234
341
|
severity: "error",
|
|
235
|
-
message
|
|
342
|
+
message,
|
|
343
|
+
...line !== void 0 ? { line } : {}
|
|
236
344
|
};
|
|
237
345
|
}
|
|
346
|
+
function messageForIssue(issue) {
|
|
347
|
+
switch (issue.kind) {
|
|
348
|
+
case "missing-required-field":
|
|
349
|
+
return `Hook config is missing required field '${issue.field}'.`;
|
|
350
|
+
case "unknown-key":
|
|
351
|
+
return `Unknown config key '${issue.key}'. Known keys: ${[...KNOWN_HOOK_CONFIG_KEYS].join(", ")}.`;
|
|
352
|
+
case "invalid-trigger":
|
|
353
|
+
return `Invalid trigger '${issue.value}'. Must be one of: ${VALID_HOOK_TRIGGERS.join(", ")}.`;
|
|
354
|
+
case "invalid-enabled":
|
|
355
|
+
return `Invalid 'enabled' value \`${issue.rawSource}\`. Must be a literal \`true\` or \`false\`.`;
|
|
356
|
+
case "metadata-unparseable":
|
|
357
|
+
return `Could not parse 'metadata': ${issue.reason}. It must be an inline object literal with literal values (strings, numbers, booleans, null, arrays, or nested literal objects).`;
|
|
358
|
+
case "non-literal-field":
|
|
359
|
+
return `Field '${issue.field}' must be a string literal.`;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
238
362
|
var hookVerifyRule = {
|
|
239
363
|
id: "hook-verify",
|
|
240
364
|
description: "Validates hook file structure (config export, trigger, known keys, default handler) and verifies the script compiles via the backend.",
|
|
241
365
|
appliesTo: ["hook"],
|
|
242
366
|
async check(target, ctx) {
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
367
|
+
const finding = parseHookFile(target.source);
|
|
368
|
+
if (finding.syntaxError) {
|
|
245
369
|
return [
|
|
246
370
|
error2(
|
|
247
371
|
target,
|
|
248
|
-
|
|
372
|
+
`Could not parse hook file: ${finding.syntaxError.message}`,
|
|
373
|
+
finding.syntaxError.line
|
|
249
374
|
)
|
|
250
375
|
];
|
|
251
376
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
errors.push(
|
|
377
|
+
if (!finding.configFound) {
|
|
378
|
+
return [
|
|
255
379
|
error2(
|
|
256
380
|
target,
|
|
257
|
-
"
|
|
381
|
+
"Missing `export const config = {...}`. Hook files must export a config object with at least `collection` and `trigger`."
|
|
258
382
|
)
|
|
259
|
-
|
|
383
|
+
];
|
|
260
384
|
}
|
|
261
|
-
|
|
385
|
+
const errors = [];
|
|
386
|
+
if (!finding.hook?.external_id && !ctx.appName) {
|
|
262
387
|
errors.push(
|
|
263
388
|
error2(
|
|
264
389
|
target,
|
|
265
|
-
"
|
|
390
|
+
"Hook config is missing `external_id` and no app name is configured to derive one from the file name."
|
|
266
391
|
)
|
|
267
392
|
);
|
|
268
393
|
}
|
|
269
|
-
if (!
|
|
394
|
+
if (!finding.defaultHandlerFound) {
|
|
270
395
|
errors.push(
|
|
271
396
|
error2(
|
|
272
397
|
target,
|
|
273
|
-
|
|
398
|
+
"Missing default exported handler. Hook files must `export default function(ctx) { ... }` (async or arrow allowed)."
|
|
274
399
|
)
|
|
275
400
|
);
|
|
276
401
|
}
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
const keys = extractTopLevelConfigKeys(configBody);
|
|
280
|
-
for (const key of keys) {
|
|
281
|
-
if (!KNOWN_HOOK_CONFIG_KEYS.has(key)) {
|
|
282
|
-
errors.push(
|
|
283
|
-
error2(
|
|
284
|
-
target,
|
|
285
|
-
`Unknown config key '${key}'. Known keys: ${[...KNOWN_HOOK_CONFIG_KEYS].join(", ")}.`
|
|
286
|
-
)
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
if (keys.includes("enabled")) {
|
|
291
|
-
const m = configBody.match(/enabled\s*:\s*([^,}\n]+)/);
|
|
292
|
-
const raw = m ? m[1].trim().replace(/,\s*$/, "").trim() : "";
|
|
293
|
-
if (raw !== "true" && raw !== "false") {
|
|
294
|
-
errors.push(
|
|
295
|
-
error2(
|
|
296
|
-
target,
|
|
297
|
-
`Invalid 'enabled' value '${raw}'. Must be a literal \`true\` or \`false\`.`
|
|
298
|
-
)
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
if (keys.includes("metadata") && !config.metadata) {
|
|
303
|
-
errors.push(
|
|
304
|
-
error2(
|
|
305
|
-
target,
|
|
306
|
-
"Could not parse 'metadata'. It must be an inline object literal with simple key/value pairs (e.g. `{ threshold: 100 }`); nested objects and non-literal values are not supported."
|
|
307
|
-
)
|
|
308
|
-
);
|
|
309
|
-
}
|
|
402
|
+
for (const issue of finding.configIssues) {
|
|
403
|
+
errors.push(error2(target, messageForIssue(issue)));
|
|
310
404
|
}
|
|
311
405
|
if (errors.length > 0) return errors;
|
|
312
|
-
let script =
|
|
406
|
+
let script = finding.scriptBody ?? "";
|
|
313
407
|
if (ctx.appName) {
|
|
314
408
|
script = script.replaceAll("{{app}}", ctx.appName);
|
|
315
409
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumerahq/cli",
|
|
3
|
-
"version": "0.19.9-dev.
|
|
3
|
+
"version": "0.19.9-dev.3",
|
|
4
4
|
"description": "CLI for building and deploying Lumera apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"templates"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
+
"acorn": "^8.16.0",
|
|
17
18
|
"archiver": "^7.0.1",
|
|
18
19
|
"dotenv": "^16.4.7",
|
|
19
20
|
"open": "^10.1.0",
|