@procedures/procedure 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/README.md +7 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/procedures/procedure/new.d.ts +23 -7
- package/dist/procedures/procedure/new.d.ts.map +1 -1
- package/dist/procedures/procedure/new.js +200 -48
- package/dist/procedures/procedure/new.js.map +1 -1
- package/dist/procedures/procedure/registry.d.ts.map +1 -1
- package/dist/procedures/procedure/registry.js +3 -1
- package/dist/procedures/procedure/registry.js.map +1 -1
- package/package.json +3 -3
- package/src/procedures/procedure/new.ts +227 -51
- package/src/procedures/procedure/registry.ts +3 -1
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* The scaffolded code compiles with the strict options of the workspace (deep dive CLI-13):
|
|
9
9
|
* - the handler names its unused input `_input` (noUnusedParameters);
|
|
10
10
|
* - types.ts is created, or gets `import { z } from "zod"`, when it needs it;
|
|
11
|
-
* - the registration goes into the defineModule
|
|
12
|
-
*
|
|
11
|
+
* - the registration goes into the list that defineModule gets in src/module.ts: an array literal,
|
|
12
|
+
* or the array of a variable that module.ts declares. A package of the old kind, without
|
|
13
|
+
* module.ts, gets it in the registerProcedures([...]) array of src/register.ts.
|
|
13
14
|
* A name whose path, file, types or registration exists already is an error, and nothing is
|
|
14
15
|
* written. Before, "a.b.c" and "a.c" wrote the same file, and existing types were reused silently.
|
|
16
|
+
* A module whose list procedure.new cannot find is an error too, and nothing is written.
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
import { join } from "node:path";
|
|
@@ -219,40 +221,213 @@ export function ensureNamedImports(source: string, moduleName: string, names: st
|
|
|
219
221
|
return addImports(source, [`import { ${names.join(", ")} } from "${moduleName}";\n`]);
|
|
220
222
|
}
|
|
221
223
|
|
|
222
|
-
/** The index
|
|
224
|
+
/** The index after the string or comment that starts at `i`, or `i` when code starts there */
|
|
225
|
+
function skipNonCode(source: string, i: number): number {
|
|
226
|
+
const ch = source[i];
|
|
227
|
+
if (ch === "/" && source[i + 1] === "/") {
|
|
228
|
+
const end = source.indexOf("\n", i);
|
|
229
|
+
return end === -1 ? source.length : end;
|
|
230
|
+
}
|
|
231
|
+
if (ch === "/" && source[i + 1] === "*") {
|
|
232
|
+
const end = source.indexOf("*/", i + 2);
|
|
233
|
+
return end === -1 ? source.length : end + 2;
|
|
234
|
+
}
|
|
235
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
236
|
+
for (let j = i + 1; j < source.length; j++) {
|
|
237
|
+
if (source[j] === "\\") j++;
|
|
238
|
+
else if (source[j] === ch) return j + 1;
|
|
239
|
+
}
|
|
240
|
+
return source.length;
|
|
241
|
+
}
|
|
242
|
+
return i;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** For each character of the source: true when it is code, false in a string or a comment */
|
|
246
|
+
function codeMask(source: string): boolean[] {
|
|
247
|
+
const mask = new Array<boolean>(source.length).fill(true);
|
|
248
|
+
for (let i = 0; i < source.length; i++) {
|
|
249
|
+
const skip = skipNonCode(source, i);
|
|
250
|
+
if (skip !== i) {
|
|
251
|
+
mask.fill(false, i, skip);
|
|
252
|
+
i = skip - 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return mask;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The index of the bracket that closes the bracket at `open` (strings and comments are skipped) */
|
|
223
259
|
function closingBracket(source: string, open: number): number {
|
|
224
260
|
let depth = 0;
|
|
225
|
-
let quote: string | undefined;
|
|
226
261
|
for (let i = open; i < source.length; i++) {
|
|
227
|
-
const
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
else if (ch === quote) quote = undefined;
|
|
262
|
+
const skip = skipNonCode(source, i);
|
|
263
|
+
if (skip !== i) {
|
|
264
|
+
i = skip - 1;
|
|
231
265
|
continue;
|
|
232
266
|
}
|
|
233
|
-
|
|
234
|
-
|
|
267
|
+
const ch = source[i];
|
|
268
|
+
if (ch === "[") depth++;
|
|
235
269
|
else if (ch === "]" && --depth === 0) return i;
|
|
236
270
|
}
|
|
237
271
|
return -1;
|
|
238
272
|
}
|
|
239
273
|
|
|
274
|
+
/** The start and the end of one argument of a call, without the spaces and comments around it */
|
|
275
|
+
interface ArgumentRange {
|
|
276
|
+
start: number;
|
|
277
|
+
end: number;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The arguments of the call whose "(" is at `open`. Undefined when the call does not end. */
|
|
281
|
+
function callArguments(source: string, open: number): ArgumentRange[] | undefined {
|
|
282
|
+
const args: ArgumentRange[] = [];
|
|
283
|
+
let depth = 0;
|
|
284
|
+
let first = -1;
|
|
285
|
+
let last = -1;
|
|
286
|
+
const push = (): void => {
|
|
287
|
+
if (first !== -1) args.push({ start: first, end: last });
|
|
288
|
+
first = -1;
|
|
289
|
+
last = -1;
|
|
290
|
+
};
|
|
291
|
+
for (let i = open; i < source.length; i++) {
|
|
292
|
+
const skip = skipNonCode(source, i);
|
|
293
|
+
if (skip !== i) {
|
|
294
|
+
// A string is a part of the argument. A comment is not.
|
|
295
|
+
if (source[i] !== "/") {
|
|
296
|
+
if (first === -1) first = i;
|
|
297
|
+
last = skip;
|
|
298
|
+
}
|
|
299
|
+
i = skip - 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const ch = source[i]!;
|
|
303
|
+
if (/\s/.test(ch)) continue;
|
|
304
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
305
|
+
if (++depth === 1) continue;
|
|
306
|
+
} else if (ch === ")" || ch === "]" || ch === "}") {
|
|
307
|
+
if (--depth === 0) {
|
|
308
|
+
push();
|
|
309
|
+
return args;
|
|
310
|
+
}
|
|
311
|
+
} else if (ch === "," && depth === 1) {
|
|
312
|
+
push();
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (first === -1) first = i;
|
|
316
|
+
last = i + 1;
|
|
317
|
+
}
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
240
321
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
322
|
+
* The index of the line where the statement at `index` starts, above its leading comments: code
|
|
323
|
+
* that goes there comes before the statement.
|
|
243
324
|
*/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
325
|
+
function statementStart(source: string, index: number): number {
|
|
326
|
+
let start = source.lastIndexOf("\n", index - 1) + 1;
|
|
327
|
+
for (;;) {
|
|
328
|
+
const before = source.slice(0, start).replace(/\s*$/, "");
|
|
329
|
+
if (before.endsWith("*/")) {
|
|
330
|
+
const comment = before.lastIndexOf("/*");
|
|
331
|
+
if (comment === -1) return start;
|
|
332
|
+
start = source.lastIndexOf("\n", comment - 1) + 1;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const lineStart = before.lastIndexOf("\n") + 1;
|
|
336
|
+
if (before.length > 0 && before.slice(lineStart).trimStart().startsWith("//")) {
|
|
337
|
+
start = lineStart;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
return start;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** The declaration of `name` in code as an array literal: the start of the statement and the "[" */
|
|
345
|
+
function arrayDeclaration(source: string, name: string, mask: boolean[]): { statement: number; open: number } | undefined {
|
|
346
|
+
const escaped = name.replace(/\$/g, "\\$");
|
|
347
|
+
const pattern = new RegExp(`(?<![\\w$.])(?:const|let|var)\\s+${escaped}\\s*(?::[^=;]*)?=\\s*\\[`, "g");
|
|
348
|
+
for (const match of source.matchAll(pattern)) {
|
|
349
|
+
if (mask[match.index]) {
|
|
350
|
+
return { statement: statementStart(source, match.index), open: match.index + match[0].length - 1 };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** The argument of each call that holds the procedures */
|
|
357
|
+
const LIST_ARGUMENT: Readonly<Record<string, number>> = { defineModule: 1, registerProcedures: 0, registerAll: 0 };
|
|
358
|
+
|
|
359
|
+
/** The source with the item in the list, and the place for the definition of the item */
|
|
360
|
+
export interface RegistrationEdit {
|
|
361
|
+
source: string;
|
|
362
|
+
/** The definition of the item goes before this index, so that the list can use it */
|
|
363
|
+
before: number;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** An argument as a short text for a message */
|
|
367
|
+
function shortText(text: string): string {
|
|
368
|
+
const line = text.replace(/\s+/g, " ");
|
|
369
|
+
return line.length > 60 ? `${line.slice(0, 57)}...` : line;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Add an item to the list of procedures of the first defineModule("name", list),
|
|
374
|
+
* registerProcedures(list) or registerAll(list) call. The list is an array literal, the variable of
|
|
375
|
+
* an array that this file declares, or `[...variable]`. Gives the reason as an error when the
|
|
376
|
+
* source has no such list. Before, the item went into the first array after the call, also when
|
|
377
|
+
* that array was the argument of `registerModules` (the ics and geo packages).
|
|
378
|
+
*/
|
|
379
|
+
export function addToRegistration(source: string, item: string): RegistrationEdit | { error: string } {
|
|
380
|
+
const mask = codeMask(source);
|
|
381
|
+
// The first call in code, not in a comment or a string, and not the declaration of the function
|
|
382
|
+
const call = [...source.matchAll(/\b(defineModule|registerProcedures|registerAll)\s*\(/g)].find(
|
|
383
|
+
(match) => mask[match.index] && !/\bfunction\s*$/.test(source.slice(0, match.index))
|
|
384
|
+
);
|
|
251
385
|
if (!call) {
|
|
252
|
-
return
|
|
386
|
+
return { error: "the file has no defineModule, registerProcedures or registerAll call" };
|
|
387
|
+
}
|
|
388
|
+
const fn = call[1]!;
|
|
389
|
+
const args = callArguments(source, call.index + call[0].length - 1);
|
|
390
|
+
const list = args?.[LIST_ARGUMENT[fn]!];
|
|
391
|
+
if (!list) {
|
|
392
|
+
return { error: `the ${fn} call has no list of procedures` };
|
|
393
|
+
}
|
|
394
|
+
const text = source.slice(list.start, list.end);
|
|
395
|
+
|
|
396
|
+
// The definition goes before the code that registers the procedures: the module, the register
|
|
397
|
+
// function or the call
|
|
398
|
+
const anchor =
|
|
399
|
+
/^export\s+const\s+\w+\s*(:\s*\w+\s*)?=\s*defineModule\s*\(|^(export\s+)?(async\s+)?function\s+register\w*\s*\(|^\s*(registerProcedures|PROCEDURE_REGISTRY\.registerAll)\s*\(/m.exec(
|
|
400
|
+
source
|
|
401
|
+
);
|
|
402
|
+
const callStatement = anchor && anchor.index <= call.index ? anchor.index : statementStart(source, call.index);
|
|
403
|
+
|
|
404
|
+
let target: { open: number; before: number } | undefined;
|
|
405
|
+
if (text.startsWith("[") && closingBracket(source, list.start) === list.end - 1) {
|
|
406
|
+
// [...variable]: the item goes into the array of the variable, when this file declares it
|
|
407
|
+
const spread = /^\[\s*\.\.\.\s*([A-Za-z_$][\w$]*)\s*,?\s*\]$/.exec(text);
|
|
408
|
+
const declared = spread ? arrayDeclaration(source, spread[1]!, mask) : undefined;
|
|
409
|
+
target = declared ? { open: declared.open, before: declared.statement } : { open: list.start, before: callStatement };
|
|
410
|
+
} else if (/^[A-Za-z_$][\w$]*$/.test(text)) {
|
|
411
|
+
const declared = arrayDeclaration(source, text, mask);
|
|
412
|
+
if (!declared) {
|
|
413
|
+
return { error: `${fn} gets "${text}", and this file does not declare "${text}" as an array` };
|
|
414
|
+
}
|
|
415
|
+
target = { open: declared.open, before: declared.statement };
|
|
416
|
+
} else {
|
|
417
|
+
return { error: `${fn} gets "${shortText(text)}", which is not an array or a variable of this file` };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const updated = insertItem(source, target.open, item);
|
|
421
|
+
if (updated === undefined) {
|
|
422
|
+
return { error: `the list of ${fn} has no closing bracket` };
|
|
253
423
|
}
|
|
254
|
-
|
|
255
|
-
|
|
424
|
+
// The item goes after the place of the definition, so that index does not move
|
|
425
|
+
return { source: updated, before: target.before };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Add an item to the array whose "[" is at `open`. Undefined when the array does not end. */
|
|
429
|
+
function insertItem(source: string, open: number, item: string): string | undefined {
|
|
430
|
+
const close = closingBracket(source, open);
|
|
256
431
|
if (close === -1) {
|
|
257
432
|
return undefined;
|
|
258
433
|
}
|
|
@@ -312,26 +487,28 @@ register();
|
|
|
312
487
|
|
|
313
488
|
/**
|
|
314
489
|
* Put the procedure into module.ts (or register.ts): its imports, its definition, and its place
|
|
315
|
-
* in the
|
|
490
|
+
* in the list of procedures of the module. Gives the reason as an error when the file has no such
|
|
491
|
+
* list (see `addToRegistration`).
|
|
316
492
|
*/
|
|
317
|
-
export function registerSource(
|
|
493
|
+
export function registerSource(
|
|
494
|
+
source: string,
|
|
495
|
+
layout: ProcedureLayout,
|
|
496
|
+
description: string,
|
|
497
|
+
importPath: string
|
|
498
|
+
): string | { error: string } {
|
|
318
499
|
const { camelName, pascalName } = layout;
|
|
319
|
-
const
|
|
320
|
-
if (
|
|
321
|
-
return
|
|
500
|
+
const edit = addToRegistration(source, `${camelName}Procedure`);
|
|
501
|
+
if ("error" in edit) {
|
|
502
|
+
return edit;
|
|
322
503
|
}
|
|
323
|
-
|
|
324
|
-
|
|
504
|
+
// The definition goes before the list that uses it, then the imports go after the last import
|
|
505
|
+
const withDefinition =
|
|
506
|
+
edit.source.slice(0, edit.before) + generateRegistration(layout, description) + edit.source.slice(edit.before);
|
|
507
|
+
const updated = ensureNamedImports(withDefinition, "@procedures/client", ["createProcedure", "zodAdapter"]);
|
|
508
|
+
return addImports(updated, [
|
|
325
509
|
`import { ${camelName} } from "${importPath}";\n`,
|
|
326
510
|
`import { ${pascalName}InputSchema, ${pascalName}OutputSchema, type ${pascalName}Input, type ${pascalName}Output } from "./types.js";\n`,
|
|
327
511
|
]);
|
|
328
|
-
// The definition goes before the module, or before the code that registers the procedures
|
|
329
|
-
const anchor =
|
|
330
|
-
/^export\s+const\s+\w+\s*(:\s*\w+\s*)?=\s*defineModule\s*\(|^(export\s+)?(async\s+)?function\s+register\w*\s*\(|^\s*(registerProcedures|PROCEDURE_REGISTRY\.registerAll)\s*\(/m.exec(
|
|
331
|
-
updated
|
|
332
|
-
);
|
|
333
|
-
const at = anchor ? anchor.index : updated.length;
|
|
334
|
-
return updated.slice(0, at) + generateRegistration(layout, description) + updated.slice(at);
|
|
335
512
|
}
|
|
336
513
|
|
|
337
514
|
/**
|
|
@@ -401,6 +578,13 @@ export async function procedureNew(input: ProcedureNewInput, ctx: ProcedureConte
|
|
|
401
578
|
}
|
|
402
579
|
const registerBase = existingRegister ?? REGISTER_TEMPLATE;
|
|
403
580
|
const newRegister = registerSource(registerBase, layout, description, importPath);
|
|
581
|
+
// Without the list of the module, the procedure would not be registered: write nothing
|
|
582
|
+
if (typeof newRegister !== "string") {
|
|
583
|
+
return fail(
|
|
584
|
+
`${registerFile} has no list of procedures that procedure.new can change: ${newRegister.error}. ` +
|
|
585
|
+
`Nothing was written. Give the module an array of procedures in ${registerName}, and start procedure.new again.`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
404
588
|
|
|
405
589
|
if (input.dryRun) {
|
|
406
590
|
const [indexExists] = await Promise.all([pathExists(namespaceIndex, ctx)]);
|
|
@@ -416,15 +600,13 @@ export async function procedureNew(input: ProcedureNewInput, ctx: ProcedureConte
|
|
|
416
600
|
modified: [
|
|
417
601
|
...(indexExists ? [namespaceIndex] : []),
|
|
418
602
|
...(existingTypes !== undefined ? [typesFile] : []),
|
|
419
|
-
...(existingRegister !== undefined
|
|
603
|
+
...(existingRegister !== undefined ? [registerFile] : []),
|
|
420
604
|
],
|
|
421
605
|
operations: [
|
|
422
606
|
`Would create procedure file: ${procedureFile}`,
|
|
423
607
|
`Would create/update index: ${namespaceIndex}`,
|
|
424
608
|
existingTypes === undefined ? `Would create ${typesFile}` : `Would append types to: ${typesFile}`,
|
|
425
|
-
|
|
426
|
-
? `Would not change ${registerFile}: it has no defineModule or registerProcedures array`
|
|
427
|
-
: `Would register the procedure in: ${registerFile}`,
|
|
609
|
+
`Would register the procedure in: ${registerFile}`,
|
|
428
610
|
],
|
|
429
611
|
errors: [],
|
|
430
612
|
};
|
|
@@ -473,17 +655,11 @@ export async function procedureNew(input: ProcedureNewInput, ctx: ProcedureConte
|
|
|
473
655
|
}
|
|
474
656
|
|
|
475
657
|
// Step 4: Register the procedure
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
operations.push(`${existingRegister === undefined ? "Creating" : "Updating"} registration: ${registerFile}`);
|
|
482
|
-
await writeText(registerFile, newRegister, ctx);
|
|
483
|
-
(existingRegister === undefined ? created : modified).push(registerFile);
|
|
484
|
-
if (existingRegister === undefined) {
|
|
485
|
-
operations.push(`Export the procedures from src/index.ts, and give the package a "./register" entry (see lib new)`);
|
|
486
|
-
}
|
|
658
|
+
operations.push(`${existingRegister === undefined ? "Creating" : "Updating"} registration: ${registerFile}`);
|
|
659
|
+
await writeText(registerFile, newRegister, ctx);
|
|
660
|
+
(existingRegister === undefined ? created : modified).push(registerFile);
|
|
661
|
+
if (existingRegister === undefined) {
|
|
662
|
+
operations.push(`Export the procedures from src/index.ts, and give the package a "./register" entry (see lib new)`);
|
|
487
663
|
}
|
|
488
664
|
|
|
489
665
|
operations.push(`Next: implement ${segments.join(".")} in ${procedureFile}, then run pnpm build`);
|
|
@@ -67,7 +67,9 @@ export const procedureExportProcedure: AnyProcedure = defineProcedure({
|
|
|
67
67
|
? keyToPath(input.path)
|
|
68
68
|
: input.path;
|
|
69
69
|
|
|
70
|
-
const
|
|
70
|
+
const found = (ctx?.registry ?? PROCEDURE_REGISTRY).get(path);
|
|
71
|
+
// A procedure outside the expose rule of the host is not found (roadmap R17)
|
|
72
|
+
const proc = found && (!ctx?.expose || ctx.expose(found.path)) ? found : undefined;
|
|
71
73
|
|
|
72
74
|
if (!proc) {
|
|
73
75
|
return {
|