libretto 0.5.3-experimental.4 → 0.5.3-experimental.6
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/cli/commands/deploy.js +8 -9
- package/dist/cli/core/deploy-artifact.js +138 -59
- package/dist/cli/workers/run-integration-runtime.js +0 -1
- package/dist/shared/workflow/workflow.d.ts +6 -7
- package/package.json +1 -1
- package/skills/libretto/references/code-generation-rules.md +1 -37
- package/src/cli/commands/deploy.ts +9 -11
- package/src/cli/core/deploy-artifact.ts +192 -77
- package/src/cli/workers/run-integration-runtime.ts +0 -1
- package/src/index.ts +0 -1
- package/src/shared/workflow/workflow.ts +12 -14
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { buildHostedDeployTarball } from "../core/deploy-artifact.js";
|
|
3
4
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
5
|
+
function generateDeploymentName() {
|
|
6
|
+
return `deploy-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
7
|
+
}
|
|
4
8
|
function getConfig() {
|
|
5
9
|
const apiUrl = process.env.LIBRETTO_API_URL;
|
|
6
10
|
const apiKey = process.env.LIBRETTO_API_KEY;
|
|
@@ -58,9 +62,6 @@ const deployInput = SimpleCLI.input({
|
|
|
58
62
|
})
|
|
59
63
|
],
|
|
60
64
|
named: {
|
|
61
|
-
name: SimpleCLI.option(z.string(), {
|
|
62
|
-
help: "Deployment name"
|
|
63
|
-
}),
|
|
64
65
|
description: SimpleCLI.option(z.string().optional(), {
|
|
65
66
|
help: "Deployment description"
|
|
66
67
|
}),
|
|
@@ -83,15 +84,15 @@ const deployCommand = SimpleCLI.command({
|
|
|
83
84
|
experimental: true
|
|
84
85
|
}).input(deployInput).handle(async ({ input }) => {
|
|
85
86
|
const { apiUrl, apiKey } = getConfig();
|
|
87
|
+
const deploymentName = generateDeploymentName();
|
|
86
88
|
console.log("Bundling hosted deployment artifact...");
|
|
87
89
|
const { entryPoint, source } = await buildHostedDeployTarball({
|
|
88
90
|
additionalExternals: input.external,
|
|
89
|
-
deploymentName
|
|
91
|
+
deploymentName,
|
|
90
92
|
entryPoint: input.entryPoint,
|
|
91
93
|
sourceDir: input.sourceDir
|
|
92
94
|
});
|
|
93
95
|
const createPayload = {
|
|
94
|
-
name: input.name,
|
|
95
96
|
source,
|
|
96
97
|
entry_point: entryPoint
|
|
97
98
|
};
|
|
@@ -109,10 +110,8 @@ const deployCommand = SimpleCLI.command({
|
|
|
109
110
|
`Failed to create deployment (${res.status}): ${JSON.stringify(body)}`
|
|
110
111
|
);
|
|
111
112
|
}
|
|
112
|
-
const { deployment_id,
|
|
113
|
-
console.log(
|
|
114
|
-
`Deployment created: ${name} v${version} (${deployment_id})`
|
|
115
|
-
);
|
|
113
|
+
const { deployment_id, status } = body.json;
|
|
114
|
+
console.log(`Deployment created: ${deployment_id}`);
|
|
116
115
|
console.log(`Status: ${status}`);
|
|
117
116
|
if (status === "building") {
|
|
118
117
|
process.stdout.write("Waiting for build");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import {
|
|
4
|
+
cpSync,
|
|
4
5
|
existsSync,
|
|
5
6
|
mkdirSync,
|
|
6
7
|
mkdtempSync,
|
|
@@ -39,6 +40,9 @@ const SOURCE_FILE_EXTENSIONS = [
|
|
|
39
40
|
"/index.cjs"
|
|
40
41
|
];
|
|
41
42
|
const CURRENT_LIBRETTO_VERSION = readCurrentLibrettoVersion();
|
|
43
|
+
const CURRENT_LIBRETTO_PACKAGE_DIR = fileURLToPath(
|
|
44
|
+
new URL("../../..", import.meta.url)
|
|
45
|
+
);
|
|
42
46
|
function readCurrentLibrettoVersion() {
|
|
43
47
|
const packageJsonPath = fileURLToPath(
|
|
44
48
|
new URL("../../../package.json", import.meta.url)
|
|
@@ -103,7 +107,9 @@ function readWorkspacePatterns(rootDir) {
|
|
|
103
107
|
if (existsSync(pnpmWorkspacePath)) {
|
|
104
108
|
const patterns = [];
|
|
105
109
|
let inPackagesBlock = false;
|
|
106
|
-
for (const rawLine of readFileSync(pnpmWorkspacePath, "utf8").split(
|
|
110
|
+
for (const rawLine of readFileSync(pnpmWorkspacePath, "utf8").split(
|
|
111
|
+
/\r?\n/
|
|
112
|
+
)) {
|
|
107
113
|
const trimmed = rawLine.trim();
|
|
108
114
|
if (!inPackagesBlock) {
|
|
109
115
|
if (trimmed === "packages:") {
|
|
@@ -233,7 +239,11 @@ function resolveExportTarget(exportValue, packageDir, replacement) {
|
|
|
233
239
|
if (!(condition in record)) {
|
|
234
240
|
continue;
|
|
235
241
|
}
|
|
236
|
-
const resolved = resolveExportTarget(
|
|
242
|
+
const resolved = resolveExportTarget(
|
|
243
|
+
record[condition],
|
|
244
|
+
packageDir,
|
|
245
|
+
replacement
|
|
246
|
+
);
|
|
237
247
|
if (resolved) {
|
|
238
248
|
return resolved;
|
|
239
249
|
}
|
|
@@ -250,7 +260,7 @@ function resolveExportsSubpath(exportsField, packageDir, subpath) {
|
|
|
250
260
|
if (!exportsField) {
|
|
251
261
|
return null;
|
|
252
262
|
}
|
|
253
|
-
if (subpath === ".") {
|
|
263
|
+
if (subpath === "." && (typeof exportsField === "string" || Array.isArray(exportsField))) {
|
|
254
264
|
const rootExport = resolveExportTarget(exportsField, packageDir);
|
|
255
265
|
if (rootExport) {
|
|
256
266
|
return rootExport;
|
|
@@ -260,7 +270,9 @@ function resolveExportsSubpath(exportsField, packageDir, subpath) {
|
|
|
260
270
|
return null;
|
|
261
271
|
}
|
|
262
272
|
const record = exportsField;
|
|
263
|
-
const hasExplicitSubpathKeys = Object.keys(record).some(
|
|
273
|
+
const hasExplicitSubpathKeys = Object.keys(record).some(
|
|
274
|
+
(key) => key.startsWith(".")
|
|
275
|
+
);
|
|
264
276
|
if (!hasExplicitSubpathKeys) {
|
|
265
277
|
return subpath === "." ? resolveExportTarget(record, packageDir) : null;
|
|
266
278
|
}
|
|
@@ -278,7 +290,10 @@ function resolveExportsSubpath(exportsField, packageDir, subpath) {
|
|
|
278
290
|
if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) {
|
|
279
291
|
continue;
|
|
280
292
|
}
|
|
281
|
-
const replacement = subpath.slice(
|
|
293
|
+
const replacement = subpath.slice(
|
|
294
|
+
prefix.length,
|
|
295
|
+
subpath.length - suffix.length
|
|
296
|
+
);
|
|
282
297
|
const resolved = resolveExportTarget(value, packageDir, replacement);
|
|
283
298
|
if (resolved) {
|
|
284
299
|
return resolved;
|
|
@@ -287,7 +302,11 @@ function resolveExportsSubpath(exportsField, packageDir, subpath) {
|
|
|
287
302
|
return null;
|
|
288
303
|
}
|
|
289
304
|
function resolveWorkspaceSourcePath(info, subpath) {
|
|
290
|
-
const viaExports = resolveExportsSubpath(
|
|
305
|
+
const viaExports = resolveExportsSubpath(
|
|
306
|
+
info.manifest.exports,
|
|
307
|
+
info.dir,
|
|
308
|
+
subpath
|
|
309
|
+
);
|
|
291
310
|
if (viaExports) {
|
|
292
311
|
return viaExports;
|
|
293
312
|
}
|
|
@@ -318,11 +337,17 @@ function workspaceSourcePlugin(workspacePackages, externalPackages) {
|
|
|
318
337
|
if (externalPackages.has(args.path)) {
|
|
319
338
|
return null;
|
|
320
339
|
}
|
|
321
|
-
const match = findMatchingWorkspacePackage(
|
|
340
|
+
const match = findMatchingWorkspacePackage(
|
|
341
|
+
args.path,
|
|
342
|
+
workspacePackages
|
|
343
|
+
);
|
|
322
344
|
if (!match) {
|
|
323
345
|
return null;
|
|
324
346
|
}
|
|
325
|
-
const resolvedPath = resolveWorkspaceSourcePath(
|
|
347
|
+
const resolvedPath = resolveWorkspaceSourcePath(
|
|
348
|
+
match.info,
|
|
349
|
+
match.subpath
|
|
350
|
+
);
|
|
326
351
|
if (!resolvedPath) {
|
|
327
352
|
throw new Error(
|
|
328
353
|
`Unable to resolve workspace import "${args.path}" from ${match.info.dir}.`
|
|
@@ -378,17 +403,12 @@ function resolveDependencyVersion(sourceDir, packageName, fallbackVersion) {
|
|
|
378
403
|
}
|
|
379
404
|
function writeDeployManifest(args) {
|
|
380
405
|
const dependencies = Object.fromEntries(
|
|
381
|
-
[
|
|
382
|
-
|
|
383
|
-
...args.additionalExternals
|
|
384
|
-
].map((packageName) => [
|
|
385
|
-
packageName,
|
|
386
|
-
resolveDependencyVersion(
|
|
387
|
-
args.sourceDir,
|
|
406
|
+
[...BUILT_IN_MANIFEST_DEPENDENCIES, ...args.additionalExternals].map(
|
|
407
|
+
(packageName) => [
|
|
388
408
|
packageName,
|
|
389
|
-
packageName === "libretto" ?
|
|
390
|
-
|
|
391
|
-
|
|
409
|
+
packageName === "libretto" ? args.librettoDependency : resolveDependencyVersion(args.sourceDir, packageName)
|
|
410
|
+
]
|
|
411
|
+
)
|
|
392
412
|
);
|
|
393
413
|
writeFileSync(
|
|
394
414
|
join(args.outputDir, "package.json"),
|
|
@@ -404,6 +424,33 @@ function writeDeployManifest(args) {
|
|
|
404
424
|
) + "\n"
|
|
405
425
|
);
|
|
406
426
|
}
|
|
427
|
+
function shouldVendorCurrentLibretto(versionSpec) {
|
|
428
|
+
return versionSpec.startsWith("file:") || versionSpec.startsWith("link:") || versionSpec.startsWith("workspace:") || versionSpec.startsWith("portal:") || versionSpec.includes("&path:");
|
|
429
|
+
}
|
|
430
|
+
function resolveLibrettoDependency(sourceDir) {
|
|
431
|
+
const versionSpec = resolveDependencyVersion(
|
|
432
|
+
sourceDir,
|
|
433
|
+
"libretto",
|
|
434
|
+
CURRENT_LIBRETTO_VERSION
|
|
435
|
+
);
|
|
436
|
+
if (shouldVendorCurrentLibretto(versionSpec)) {
|
|
437
|
+
return "file:./libretto";
|
|
438
|
+
}
|
|
439
|
+
return versionSpec;
|
|
440
|
+
}
|
|
441
|
+
function copyCurrentLibrettoPackage(outputDir) {
|
|
442
|
+
const bundledLibrettoDir = join(outputDir, "libretto");
|
|
443
|
+
mkdirSync(bundledLibrettoDir, { recursive: true });
|
|
444
|
+
cpSync(
|
|
445
|
+
join(CURRENT_LIBRETTO_PACKAGE_DIR, "dist"),
|
|
446
|
+
join(bundledLibrettoDir, "dist"),
|
|
447
|
+
{ recursive: true }
|
|
448
|
+
);
|
|
449
|
+
cpSync(
|
|
450
|
+
join(CURRENT_LIBRETTO_PACKAGE_DIR, "package.json"),
|
|
451
|
+
join(bundledLibrettoDir, "package.json")
|
|
452
|
+
);
|
|
453
|
+
}
|
|
407
454
|
function formatBuildError(error) {
|
|
408
455
|
if (!(error instanceof Error)) {
|
|
409
456
|
return String(error);
|
|
@@ -450,7 +497,9 @@ function extractExportNamesFromEsmBundle(bundleSource) {
|
|
|
450
497
|
}
|
|
451
498
|
function createBootstrapSource(args) {
|
|
452
499
|
const bundleHash = createHash("sha256").update(args.bundleBuffer).digest("hex").slice(0, 16);
|
|
453
|
-
const bundleBase64 = gzipSync(args.bundleBuffer, { level: 9 }).toString(
|
|
500
|
+
const bundleBase64 = gzipSync(args.bundleBuffer, { level: 9 }).toString(
|
|
501
|
+
"base64"
|
|
502
|
+
);
|
|
454
503
|
const outputPrefix = `${normalizePackageName(args.deploymentName)}-`;
|
|
455
504
|
const hasDefaultExport = args.exportNames.includes("default");
|
|
456
505
|
const exportLines = args.exportNames.filter((name) => name !== "default").map(
|
|
@@ -470,7 +519,9 @@ const BUNDLE_FILENAME = join(
|
|
|
470
519
|
tmpdir(),
|
|
471
520
|
${JSON.stringify(outputPrefix)} + BUNDLE_HASH + ".cjs",
|
|
472
521
|
);
|
|
473
|
-
const
|
|
522
|
+
const nativeRequire = createRequire(
|
|
523
|
+
join(tmpdir(), ${JSON.stringify("libretto-deploy-bootstrap.cjs")}),
|
|
524
|
+
);
|
|
474
525
|
|
|
475
526
|
function ensureBundleFile() {
|
|
476
527
|
if (!existsSync(BUNDLE_FILENAME)) {
|
|
@@ -485,7 +536,7 @@ function ensureBundleFile() {
|
|
|
485
536
|
|
|
486
537
|
function createWorkflowProxy(exportName) {
|
|
487
538
|
return workflow(exportName, async (ctx, input) => {
|
|
488
|
-
const impl =
|
|
539
|
+
const impl = nativeRequire(ensureBundleFile());
|
|
489
540
|
const target = impl[exportName];
|
|
490
541
|
if (!target || typeof target.run !== "function") {
|
|
491
542
|
throw new Error(
|
|
@@ -500,29 +551,19 @@ ${exportLines}
|
|
|
500
551
|
${defaultExportLine}
|
|
501
552
|
`;
|
|
502
553
|
}
|
|
503
|
-
async function
|
|
504
|
-
const absSourceDir = resolve(args.sourceDir);
|
|
505
|
-
ensureSourcePackageManifest(absSourceDir);
|
|
506
|
-
const absEntryPoint = resolveEntryPointPath(absSourceDir, args.entryPoint);
|
|
507
|
-
const tempRoot = mkdtempSync(join(tmpdir(), "libretto-deploy-"));
|
|
508
|
-
const outputDir = join(tempRoot, "deploy");
|
|
509
|
-
mkdirSync(outputDir, { recursive: true });
|
|
510
|
-
const additionalExternals = [...new Set(args.additionalExternals ?? [])];
|
|
511
|
-
const externalPackages = /* @__PURE__ */ new Set([
|
|
512
|
-
...DEFAULT_RUNTIME_EXTERNALS,
|
|
513
|
-
...additionalExternals
|
|
514
|
-
]);
|
|
515
|
-
const workspacePackages = discoverWorkspacePackages(absSourceDir);
|
|
554
|
+
async function writeBundledDeployEntrypoint(args) {
|
|
516
555
|
try {
|
|
517
556
|
const implementationBuild = await build({
|
|
518
|
-
absWorkingDir: absSourceDir,
|
|
557
|
+
absWorkingDir: args.absSourceDir,
|
|
519
558
|
bundle: true,
|
|
520
|
-
entryPoints: [absEntryPoint],
|
|
521
|
-
external: [...externalPackages],
|
|
559
|
+
entryPoints: [args.absEntryPoint],
|
|
560
|
+
external: [...args.externalPackages],
|
|
522
561
|
format: "cjs",
|
|
523
562
|
outfile: "prebundled.cjs",
|
|
524
563
|
platform: "node",
|
|
525
|
-
plugins: [
|
|
564
|
+
plugins: [
|
|
565
|
+
workspaceSourcePlugin(args.workspacePackages, args.externalPackages)
|
|
566
|
+
],
|
|
526
567
|
splitting: false,
|
|
527
568
|
target: "node20",
|
|
528
569
|
write: false
|
|
@@ -531,17 +572,21 @@ async function createHostedDeployPackage(args) {
|
|
|
531
572
|
(file) => file.path.endsWith("prebundled.cjs")
|
|
532
573
|
);
|
|
533
574
|
if (!bundledImplementation) {
|
|
534
|
-
throw new Error(
|
|
575
|
+
throw new Error(
|
|
576
|
+
"Bundler did not produce a deployment implementation file."
|
|
577
|
+
);
|
|
535
578
|
}
|
|
536
579
|
const exportBuild = await build({
|
|
537
|
-
absWorkingDir: absSourceDir,
|
|
580
|
+
absWorkingDir: args.absSourceDir,
|
|
538
581
|
bundle: true,
|
|
539
|
-
entryPoints: [absEntryPoint],
|
|
540
|
-
external: [...externalPackages],
|
|
582
|
+
entryPoints: [args.absEntryPoint],
|
|
583
|
+
external: [...args.externalPackages],
|
|
541
584
|
format: "esm",
|
|
542
585
|
outfile: "entry-exports.js",
|
|
543
586
|
platform: "node",
|
|
544
|
-
plugins: [
|
|
587
|
+
plugins: [
|
|
588
|
+
workspaceSourcePlugin(args.workspacePackages, args.externalPackages)
|
|
589
|
+
],
|
|
545
590
|
splitting: false,
|
|
546
591
|
target: "node20",
|
|
547
592
|
write: false
|
|
@@ -555,11 +600,11 @@ async function createHostedDeployPackage(args) {
|
|
|
555
600
|
const exportNames = extractExportNamesFromEsmBundle(bundledExports.text);
|
|
556
601
|
if (exportNames.length === 0) {
|
|
557
602
|
throw new Error(
|
|
558
|
-
`No named exports were found in ${absEntryPoint}. Hosted deploy expects the entry point to export one or more workflows.`
|
|
603
|
+
`No named exports were found in ${args.absEntryPoint}. Hosted deploy expects the entry point to export one or more workflows.`
|
|
559
604
|
);
|
|
560
605
|
}
|
|
561
606
|
writeFileSync(
|
|
562
|
-
join(outputDir, "index.js"),
|
|
607
|
+
join(args.outputDir, "index.js"),
|
|
563
608
|
createBootstrapSource({
|
|
564
609
|
bundleBuffer: Buffer.from(bundledImplementation.contents),
|
|
565
610
|
deploymentName: args.deploymentName,
|
|
@@ -567,25 +612,59 @@ async function createHostedDeployPackage(args) {
|
|
|
567
612
|
})
|
|
568
613
|
);
|
|
569
614
|
} catch (error) {
|
|
570
|
-
rmSync(tempRoot, { force: true, recursive: true });
|
|
571
615
|
throw new Error(
|
|
572
|
-
`Failed to bundle deploy entry point ${absEntryPoint}.
|
|
616
|
+
`Failed to bundle deploy entry point ${args.absEntryPoint}.
|
|
573
617
|
${formatBuildError(error)}`
|
|
574
618
|
);
|
|
575
619
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
620
|
+
}
|
|
621
|
+
async function createHostedDeployPackage(args) {
|
|
622
|
+
const absSourceDir = resolve(args.sourceDir);
|
|
623
|
+
ensureSourcePackageManifest(absSourceDir);
|
|
624
|
+
const absEntryPoint = resolveEntryPointPath(absSourceDir, args.entryPoint);
|
|
625
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "libretto-deploy-"));
|
|
626
|
+
const outputDir = join(tempRoot, "deploy");
|
|
627
|
+
mkdirSync(outputDir, { recursive: true });
|
|
628
|
+
const librettoDependency = resolveLibrettoDependency(absSourceDir);
|
|
629
|
+
const additionalExternals = [...new Set(args.additionalExternals ?? [])];
|
|
630
|
+
const externalPackages = /* @__PURE__ */ new Set([
|
|
631
|
+
...DEFAULT_RUNTIME_EXTERNALS,
|
|
632
|
+
...additionalExternals
|
|
633
|
+
]);
|
|
634
|
+
const workspacePackages = discoverWorkspacePackages(absSourceDir);
|
|
635
|
+
let callerOwnsTempRoot = false;
|
|
636
|
+
try {
|
|
637
|
+
await writeBundledDeployEntrypoint({
|
|
638
|
+
absEntryPoint,
|
|
639
|
+
absSourceDir,
|
|
640
|
+
deploymentName: args.deploymentName,
|
|
641
|
+
externalPackages,
|
|
642
|
+
outputDir,
|
|
643
|
+
workspacePackages
|
|
644
|
+
});
|
|
645
|
+
if (librettoDependency === "file:./libretto") {
|
|
646
|
+
copyCurrentLibrettoPackage(outputDir);
|
|
647
|
+
}
|
|
648
|
+
writeDeployManifest({
|
|
649
|
+
additionalExternals,
|
|
650
|
+
deploymentName: args.deploymentName,
|
|
651
|
+
librettoDependency,
|
|
652
|
+
outputDir,
|
|
653
|
+
sourceDir: absSourceDir
|
|
654
|
+
});
|
|
655
|
+
callerOwnsTempRoot = true;
|
|
656
|
+
return {
|
|
657
|
+
cleanup: () => {
|
|
658
|
+
rmSync(tempRoot, { force: true, recursive: true });
|
|
659
|
+
},
|
|
660
|
+
entryPoint: "index.js",
|
|
661
|
+
outputDir
|
|
662
|
+
};
|
|
663
|
+
} finally {
|
|
664
|
+
if (!callerOwnsTempRoot) {
|
|
584
665
|
rmSync(tempRoot, { force: true, recursive: true });
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
outputDir
|
|
588
|
-
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
589
668
|
}
|
|
590
669
|
async function buildHostedDeployTarball(args) {
|
|
591
670
|
const deployPackage = await createHostedDeployPackage(args);
|
|
@@ -2,20 +2,19 @@ import { Page } from 'playwright';
|
|
|
2
2
|
import { MinimalLogger } from '../logger/logger.js';
|
|
3
3
|
|
|
4
4
|
declare const LIBRETTO_WORKFLOW_BRAND: unique symbol;
|
|
5
|
-
type LibrettoWorkflowContext
|
|
5
|
+
type LibrettoWorkflowContext = {
|
|
6
6
|
session: string;
|
|
7
7
|
page: Page;
|
|
8
8
|
logger: MinimalLogger;
|
|
9
|
-
services: S;
|
|
10
9
|
credentials?: Record<string, unknown>;
|
|
11
10
|
};
|
|
12
|
-
type LibrettoWorkflowHandler<Input = unknown, Output = unknown
|
|
13
|
-
declare class LibrettoWorkflow<Input = unknown, Output = unknown
|
|
11
|
+
type LibrettoWorkflowHandler<Input = unknown, Output = unknown> = (ctx: LibrettoWorkflowContext, input: Input) => Promise<Output>;
|
|
12
|
+
declare class LibrettoWorkflow<Input = unknown, Output = unknown> {
|
|
14
13
|
readonly [LIBRETTO_WORKFLOW_BRAND] = true;
|
|
15
14
|
readonly name: string;
|
|
16
15
|
private readonly handler;
|
|
17
|
-
constructor(name: string, handler: LibrettoWorkflowHandler<Input, Output
|
|
18
|
-
run(ctx: LibrettoWorkflowContext
|
|
16
|
+
constructor(name: string, handler: LibrettoWorkflowHandler<Input, Output>);
|
|
17
|
+
run(ctx: LibrettoWorkflowContext, input: Input): Promise<Output>;
|
|
19
18
|
}
|
|
20
19
|
type ExportedLibrettoWorkflow = {
|
|
21
20
|
readonly [LIBRETTO_WORKFLOW_BRAND]: true;
|
|
@@ -26,6 +25,6 @@ type WorkflowModuleExports = Record<string, unknown>;
|
|
|
26
25
|
declare function isLibrettoWorkflow(value: unknown): value is ExportedLibrettoWorkflow;
|
|
27
26
|
declare function getWorkflowsFromModuleExports(moduleExports: WorkflowModuleExports): ExportedLibrettoWorkflow[];
|
|
28
27
|
declare function getWorkflowFromModuleExports(moduleExports: WorkflowModuleExports, workflowName: string): ExportedLibrettoWorkflow | null;
|
|
29
|
-
declare function workflow<Input = unknown, Output = unknown
|
|
28
|
+
declare function workflow<Input = unknown, Output = unknown>(name: string, handler: LibrettoWorkflowHandler<Input, Output>): LibrettoWorkflow<Input, Output>;
|
|
30
29
|
|
|
31
30
|
export { type ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, workflow };
|
package/package.json
CHANGED
|
@@ -40,48 +40,12 @@ Key points:
|
|
|
40
40
|
|
|
41
41
|
- `workflow(name, handler)` takes a unique workflow name and returns the workflow object that Libretto can run.
|
|
42
42
|
- `npx libretto run ./file.ts myWorkflow` resolves `myWorkflow` from the workflows exported by `./file.ts`, so export or re-export the workflow from that file directly or through a `workflows` object, and make sure the run argument matches the name passed to `workflow("myWorkflow", ...)`.
|
|
43
|
-
- `ctx` provides `session`, `page`, `logger`, and
|
|
43
|
+
- `ctx` provides `session`, `page`, `logger`, and optional `credentials`
|
|
44
44
|
- `input` comes from `--params '{"query":"foo"}'` or `--params-file params.json` on the CLI
|
|
45
45
|
- Use `await pause(ctx.session)` (or `await pause(session)`) to pause the workflow for debugging. It is a no-op in production.
|
|
46
46
|
- After validation is complete and the workflow is confirmed working end to end, remove all `pause()` calls and pause-only workflow params unless the user explicitly says to keep them.
|
|
47
47
|
- The browser is launched and closed automatically by the CLI. Do not launch or close it in the handler.
|
|
48
48
|
|
|
49
|
-
## Passing Application Dependencies via Services
|
|
50
|
-
|
|
51
|
-
Use the third generic on `workflow<Input, Output, Services>` to inject
|
|
52
|
-
dependencies that exist in your application but not in libretto's runtime
|
|
53
|
-
(DB transactions, API clients, caches, etc.):
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
import { type Transaction } from "./db";
|
|
57
|
-
|
|
58
|
-
type MyServices = { tx?: Transaction };
|
|
59
|
-
|
|
60
|
-
export const myWorkflow = workflow<Input, Output, MyServices>(
|
|
61
|
-
"myWorkflow",
|
|
62
|
-
async (ctx, input) => {
|
|
63
|
-
if (ctx.services.tx) {
|
|
64
|
-
await ctx.services.tx.insert(/* ... */);
|
|
65
|
-
} else {
|
|
66
|
-
ctx.logger.info("No DB transaction — skipping write");
|
|
67
|
-
}
|
|
68
|
-
// ... browser automation ...
|
|
69
|
-
},
|
|
70
|
-
);
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
In production, the caller passes services when invoking `.run()`:
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
await myWorkflow.run(
|
|
77
|
-
{ session: "debug-flow", page, logger, services: { tx } },
|
|
78
|
-
input,
|
|
79
|
-
);
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
When running standalone via `npx libretto run`, services defaults to `{}`,
|
|
83
|
-
so mark fields optional for anything unavailable in that context.
|
|
84
|
-
|
|
85
49
|
## Playwright DOM Interaction Rules
|
|
86
50
|
|
|
87
51
|
Generated code must use Playwright locator APIs for all DOM interactions. Do not use `page.evaluate()` with `document.querySelector`, `querySelectorAll`, `textContent`, `click()`, or other DOM APIs when a Playwright locator can do the same thing.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { buildHostedDeployTarball } from "../core/deploy-artifact.js";
|
|
3
4
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
@@ -7,14 +8,16 @@ type DeploymentStatus = "building" | "ready" | "failed";
|
|
|
7
8
|
type DeploymentResponse = {
|
|
8
9
|
json: {
|
|
9
10
|
deployment_id: string;
|
|
10
|
-
name: string;
|
|
11
|
-
version: number;
|
|
12
11
|
status: DeploymentStatus;
|
|
13
12
|
workflows?: string[] | null;
|
|
14
13
|
build_error?: string | null;
|
|
15
14
|
};
|
|
16
15
|
};
|
|
17
16
|
|
|
17
|
+
function generateDeploymentName(): string {
|
|
18
|
+
return `deploy-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
18
21
|
function getConfig() {
|
|
19
22
|
const apiUrl = process.env.LIBRETTO_API_URL;
|
|
20
23
|
const apiKey = process.env.LIBRETTO_API_KEY;
|
|
@@ -92,9 +95,6 @@ export const deployInput = SimpleCLI.input({
|
|
|
92
95
|
}),
|
|
93
96
|
],
|
|
94
97
|
named: {
|
|
95
|
-
name: SimpleCLI.option(z.string(), {
|
|
96
|
-
help: "Deployment name",
|
|
97
|
-
}),
|
|
98
98
|
description: SimpleCLI.option(z.string().optional(), {
|
|
99
99
|
help: "Deployment description",
|
|
100
100
|
}),
|
|
@@ -127,6 +127,7 @@ export const deployCommand = SimpleCLI.command({
|
|
|
127
127
|
.input(deployInput)
|
|
128
128
|
.handle(async ({ input }) => {
|
|
129
129
|
const { apiUrl, apiKey } = getConfig();
|
|
130
|
+
const deploymentName = generateDeploymentName();
|
|
130
131
|
|
|
131
132
|
// Hosted deploy uploads a generated artifact with a deploy entrypoint and
|
|
132
133
|
// a minimal manifest. Bundled code is embedded in the generated files;
|
|
@@ -134,13 +135,12 @@ export const deployCommand = SimpleCLI.command({
|
|
|
134
135
|
console.log("Bundling hosted deployment artifact...");
|
|
135
136
|
const { entryPoint, source } = await buildHostedDeployTarball({
|
|
136
137
|
additionalExternals: input.external,
|
|
137
|
-
deploymentName
|
|
138
|
+
deploymentName,
|
|
138
139
|
entryPoint: input.entryPoint,
|
|
139
140
|
sourceDir: input.sourceDir,
|
|
140
141
|
});
|
|
141
142
|
|
|
142
143
|
const createPayload: Record<string, unknown> = {
|
|
143
|
-
name: input.name,
|
|
144
144
|
source,
|
|
145
145
|
entry_point: entryPoint,
|
|
146
146
|
};
|
|
@@ -160,10 +160,8 @@ export const deployCommand = SimpleCLI.command({
|
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
const { deployment_id,
|
|
164
|
-
console.log(
|
|
165
|
-
`Deployment created: ${name} v${version} (${deployment_id})`,
|
|
166
|
-
);
|
|
163
|
+
const { deployment_id, status } = body.json;
|
|
164
|
+
console.log(`Deployment created: ${deployment_id}`);
|
|
167
165
|
console.log(`Status: ${status}`);
|
|
168
166
|
|
|
169
167
|
if (status === "building") {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import {
|
|
4
|
+
cpSync,
|
|
4
5
|
existsSync,
|
|
5
6
|
mkdirSync,
|
|
6
7
|
mkdtempSync,
|
|
@@ -77,6 +78,9 @@ const SOURCE_FILE_EXTENSIONS = [
|
|
|
77
78
|
"/index.cjs",
|
|
78
79
|
] as const;
|
|
79
80
|
const CURRENT_LIBRETTO_VERSION = readCurrentLibrettoVersion();
|
|
81
|
+
const CURRENT_LIBRETTO_PACKAGE_DIR = fileURLToPath(
|
|
82
|
+
new URL("../../..", import.meta.url),
|
|
83
|
+
);
|
|
80
84
|
|
|
81
85
|
function readCurrentLibrettoVersion(): string {
|
|
82
86
|
const packageJsonPath = fileURLToPath(
|
|
@@ -157,7 +161,9 @@ function readWorkspacePatterns(rootDir: string): string[] {
|
|
|
157
161
|
const patterns: string[] = [];
|
|
158
162
|
let inPackagesBlock = false;
|
|
159
163
|
|
|
160
|
-
for (const rawLine of readFileSync(pnpmWorkspacePath, "utf8").split(
|
|
164
|
+
for (const rawLine of readFileSync(pnpmWorkspacePath, "utf8").split(
|
|
165
|
+
/\r?\n/,
|
|
166
|
+
)) {
|
|
161
167
|
const trimmed = rawLine.trim();
|
|
162
168
|
if (!inPackagesBlock) {
|
|
163
169
|
if (trimmed === "packages:") {
|
|
@@ -222,7 +228,9 @@ function expandWorkspacePattern(rootDir: string, pattern: string): string[] {
|
|
|
222
228
|
.map((entry) => join(baseDir, entry.name));
|
|
223
229
|
}
|
|
224
230
|
|
|
225
|
-
function discoverWorkspacePackages(
|
|
231
|
+
function discoverWorkspacePackages(
|
|
232
|
+
startDir: string,
|
|
233
|
+
): Map<string, WorkspacePackage> {
|
|
226
234
|
const workspaceRoot = findWorkspaceRoot(startDir);
|
|
227
235
|
if (!workspaceRoot) {
|
|
228
236
|
return new Map();
|
|
@@ -253,8 +261,8 @@ function findMatchingWorkspacePackage(
|
|
|
253
261
|
info: WorkspacePackage;
|
|
254
262
|
subpath: string;
|
|
255
263
|
} | null {
|
|
256
|
-
const names = [...workspacePackages.keys()].sort(
|
|
257
|
-
right.length - left.length,
|
|
264
|
+
const names = [...workspacePackages.keys()].sort(
|
|
265
|
+
(left, right) => right.length - left.length,
|
|
258
266
|
);
|
|
259
267
|
|
|
260
268
|
for (const name of names) {
|
|
@@ -331,7 +339,11 @@ function resolveExportTarget(
|
|
|
331
339
|
if (!(condition in record)) {
|
|
332
340
|
continue;
|
|
333
341
|
}
|
|
334
|
-
const resolved = resolveExportTarget(
|
|
342
|
+
const resolved = resolveExportTarget(
|
|
343
|
+
record[condition],
|
|
344
|
+
packageDir,
|
|
345
|
+
replacement,
|
|
346
|
+
);
|
|
335
347
|
if (resolved) {
|
|
336
348
|
return resolved;
|
|
337
349
|
}
|
|
@@ -356,7 +368,10 @@ function resolveExportsSubpath(
|
|
|
356
368
|
return null;
|
|
357
369
|
}
|
|
358
370
|
|
|
359
|
-
if (
|
|
371
|
+
if (
|
|
372
|
+
subpath === "." &&
|
|
373
|
+
(typeof exportsField === "string" || Array.isArray(exportsField))
|
|
374
|
+
) {
|
|
360
375
|
const rootExport = resolveExportTarget(exportsField, packageDir);
|
|
361
376
|
if (rootExport) {
|
|
362
377
|
return rootExport;
|
|
@@ -368,7 +383,9 @@ function resolveExportsSubpath(
|
|
|
368
383
|
}
|
|
369
384
|
|
|
370
385
|
const record = exportsField as Record<string, unknown>;
|
|
371
|
-
const hasExplicitSubpathKeys = Object.keys(record).some((key) =>
|
|
386
|
+
const hasExplicitSubpathKeys = Object.keys(record).some((key) =>
|
|
387
|
+
key.startsWith("."),
|
|
388
|
+
);
|
|
372
389
|
|
|
373
390
|
if (!hasExplicitSubpathKeys) {
|
|
374
391
|
return subpath === "." ? resolveExportTarget(record, packageDir) : null;
|
|
@@ -389,7 +406,10 @@ function resolveExportsSubpath(
|
|
|
389
406
|
if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) {
|
|
390
407
|
continue;
|
|
391
408
|
}
|
|
392
|
-
const replacement = subpath.slice(
|
|
409
|
+
const replacement = subpath.slice(
|
|
410
|
+
prefix.length,
|
|
411
|
+
subpath.length - suffix.length,
|
|
412
|
+
);
|
|
393
413
|
const resolved = resolveExportTarget(value, packageDir, replacement);
|
|
394
414
|
if (resolved) {
|
|
395
415
|
return resolved;
|
|
@@ -403,7 +423,11 @@ function resolveWorkspaceSourcePath(
|
|
|
403
423
|
info: WorkspacePackage,
|
|
404
424
|
subpath: string,
|
|
405
425
|
): string | null {
|
|
406
|
-
const viaExports = resolveExportsSubpath(
|
|
426
|
+
const viaExports = resolveExportsSubpath(
|
|
427
|
+
info.manifest.exports,
|
|
428
|
+
info.dir,
|
|
429
|
+
subpath,
|
|
430
|
+
);
|
|
407
431
|
if (viaExports) {
|
|
408
432
|
return viaExports;
|
|
409
433
|
}
|
|
@@ -449,12 +473,18 @@ function workspaceSourcePlugin(
|
|
|
449
473
|
return null;
|
|
450
474
|
}
|
|
451
475
|
|
|
452
|
-
const match = findMatchingWorkspacePackage(
|
|
476
|
+
const match = findMatchingWorkspacePackage(
|
|
477
|
+
args.path,
|
|
478
|
+
workspacePackages,
|
|
479
|
+
);
|
|
453
480
|
if (!match) {
|
|
454
481
|
return null;
|
|
455
482
|
}
|
|
456
483
|
|
|
457
|
-
const resolvedPath = resolveWorkspaceSourcePath(
|
|
484
|
+
const resolvedPath = resolveWorkspaceSourcePath(
|
|
485
|
+
match.info,
|
|
486
|
+
match.subpath,
|
|
487
|
+
);
|
|
458
488
|
if (!resolvedPath) {
|
|
459
489
|
throw new Error(
|
|
460
490
|
`Unable to resolve workspace import "${args.path}" from ${match.info.dir}.`,
|
|
@@ -531,21 +561,19 @@ function resolveDependencyVersion(
|
|
|
531
561
|
function writeDeployManifest(args: {
|
|
532
562
|
additionalExternals: readonly string[];
|
|
533
563
|
deploymentName: string;
|
|
564
|
+
librettoDependency: string;
|
|
534
565
|
outputDir: string;
|
|
535
566
|
sourceDir: string;
|
|
536
567
|
}): void {
|
|
537
568
|
const dependencies = Object.fromEntries(
|
|
538
|
-
[
|
|
539
|
-
|
|
540
|
-
...args.additionalExternals,
|
|
541
|
-
].map((packageName) => [
|
|
542
|
-
packageName,
|
|
543
|
-
resolveDependencyVersion(
|
|
544
|
-
args.sourceDir,
|
|
569
|
+
[...BUILT_IN_MANIFEST_DEPENDENCIES, ...args.additionalExternals].map(
|
|
570
|
+
(packageName) => [
|
|
545
571
|
packageName,
|
|
546
|
-
packageName === "libretto"
|
|
547
|
-
|
|
548
|
-
|
|
572
|
+
packageName === "libretto"
|
|
573
|
+
? args.librettoDependency
|
|
574
|
+
: resolveDependencyVersion(args.sourceDir, packageName),
|
|
575
|
+
],
|
|
576
|
+
),
|
|
549
577
|
);
|
|
550
578
|
|
|
551
579
|
writeFileSync(
|
|
@@ -563,13 +591,54 @@ function writeDeployManifest(args: {
|
|
|
563
591
|
);
|
|
564
592
|
}
|
|
565
593
|
|
|
594
|
+
function shouldVendorCurrentLibretto(versionSpec: string): boolean {
|
|
595
|
+
return (
|
|
596
|
+
versionSpec.startsWith("file:") ||
|
|
597
|
+
versionSpec.startsWith("link:") ||
|
|
598
|
+
versionSpec.startsWith("workspace:") ||
|
|
599
|
+
versionSpec.startsWith("portal:") ||
|
|
600
|
+
versionSpec.includes("&path:")
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function resolveLibrettoDependency(sourceDir: string): string {
|
|
605
|
+
const versionSpec = resolveDependencyVersion(
|
|
606
|
+
sourceDir,
|
|
607
|
+
"libretto",
|
|
608
|
+
CURRENT_LIBRETTO_VERSION,
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
if (shouldVendorCurrentLibretto(versionSpec)) {
|
|
612
|
+
return "file:./libretto";
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return versionSpec;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function copyCurrentLibrettoPackage(outputDir: string): void {
|
|
619
|
+
const bundledLibrettoDir = join(outputDir, "libretto");
|
|
620
|
+
mkdirSync(bundledLibrettoDir, { recursive: true });
|
|
621
|
+
cpSync(
|
|
622
|
+
join(CURRENT_LIBRETTO_PACKAGE_DIR, "dist"),
|
|
623
|
+
join(bundledLibrettoDir, "dist"),
|
|
624
|
+
{ recursive: true },
|
|
625
|
+
);
|
|
626
|
+
cpSync(
|
|
627
|
+
join(CURRENT_LIBRETTO_PACKAGE_DIR, "package.json"),
|
|
628
|
+
join(bundledLibrettoDir, "package.json"),
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
566
632
|
function formatBuildError(error: unknown): string {
|
|
567
633
|
if (!(error instanceof Error)) {
|
|
568
634
|
return String(error);
|
|
569
635
|
}
|
|
570
636
|
|
|
571
637
|
const candidate = error as Error & {
|
|
572
|
-
errors?: Array<{
|
|
638
|
+
errors?: Array<{
|
|
639
|
+
location?: { file?: string; line?: number; column?: number };
|
|
640
|
+
text?: string;
|
|
641
|
+
}>;
|
|
573
642
|
};
|
|
574
643
|
if (!Array.isArray(candidate.errors) || candidate.errors.length === 0) {
|
|
575
644
|
return error.message;
|
|
@@ -630,13 +699,16 @@ function createBootstrapSource(args: {
|
|
|
630
699
|
.update(args.bundleBuffer)
|
|
631
700
|
.digest("hex")
|
|
632
701
|
.slice(0, 16);
|
|
633
|
-
const bundleBase64 = gzipSync(args.bundleBuffer, { level: 9 }).toString(
|
|
702
|
+
const bundleBase64 = gzipSync(args.bundleBuffer, { level: 9 }).toString(
|
|
703
|
+
"base64",
|
|
704
|
+
);
|
|
634
705
|
const outputPrefix = `${normalizePackageName(args.deploymentName)}-`;
|
|
635
706
|
const hasDefaultExport = args.exportNames.includes("default");
|
|
636
707
|
const exportLines = args.exportNames
|
|
637
708
|
.filter((name) => name !== "default")
|
|
638
709
|
.map(
|
|
639
|
-
(name) =>
|
|
710
|
+
(name) =>
|
|
711
|
+
`export const ${name} = createWorkflowProxy(${JSON.stringify(name)});`,
|
|
640
712
|
)
|
|
641
713
|
.join("\n");
|
|
642
714
|
const defaultExportLine = hasDefaultExport
|
|
@@ -660,7 +732,9 @@ const BUNDLE_FILENAME = join(
|
|
|
660
732
|
tmpdir(),
|
|
661
733
|
${JSON.stringify(outputPrefix)} + BUNDLE_HASH + ".cjs",
|
|
662
734
|
);
|
|
663
|
-
const
|
|
735
|
+
const nativeRequire = createRequire(
|
|
736
|
+
join(tmpdir(), ${JSON.stringify("libretto-deploy-bootstrap.cjs")}),
|
|
737
|
+
);
|
|
664
738
|
|
|
665
739
|
function ensureBundleFile() {
|
|
666
740
|
if (!existsSync(BUNDLE_FILENAME)) {
|
|
@@ -675,7 +749,7 @@ function ensureBundleFile() {
|
|
|
675
749
|
|
|
676
750
|
function createWorkflowProxy(exportName) {
|
|
677
751
|
return workflow(exportName, async (ctx, input) => {
|
|
678
|
-
const impl =
|
|
752
|
+
const impl = nativeRequire(ensureBundleFile());
|
|
679
753
|
const target = impl[exportName];
|
|
680
754
|
if (!target || typeof target.run !== "function") {
|
|
681
755
|
throw new Error(
|
|
@@ -691,64 +765,57 @@ ${defaultExportLine}
|
|
|
691
765
|
`;
|
|
692
766
|
}
|
|
693
767
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const outputDir = join(tempRoot, "deploy");
|
|
703
|
-
mkdirSync(outputDir, { recursive: true });
|
|
704
|
-
|
|
705
|
-
const additionalExternals = [...new Set(args.additionalExternals ?? [])];
|
|
706
|
-
// These packages stay out of the implementation bundle. The generated
|
|
707
|
-
// package.json carries them into deploy-time installation, and the deployed
|
|
708
|
-
// code resolves them from node_modules.
|
|
709
|
-
const externalPackages = new Set<string>([
|
|
710
|
-
...DEFAULT_RUNTIME_EXTERNALS,
|
|
711
|
-
...additionalExternals,
|
|
712
|
-
]);
|
|
713
|
-
const workspacePackages = discoverWorkspacePackages(absSourceDir);
|
|
714
|
-
|
|
768
|
+
async function writeBundledDeployEntrypoint(args: {
|
|
769
|
+
absEntryPoint: string;
|
|
770
|
+
absSourceDir: string;
|
|
771
|
+
deploymentName: string;
|
|
772
|
+
externalPackages: ReadonlySet<string>;
|
|
773
|
+
outputDir: string;
|
|
774
|
+
workspacePackages: Map<string, WorkspacePackage>;
|
|
775
|
+
}): Promise<void> {
|
|
715
776
|
try {
|
|
716
777
|
// The implementation bundle is CommonJS so the bootstrap can load it lazily
|
|
717
778
|
// with createRequire() after workflow discovery, while external packages
|
|
718
779
|
// continue to load through normal Node module resolution.
|
|
719
780
|
const implementationBuild = await build({
|
|
720
|
-
absWorkingDir: absSourceDir,
|
|
781
|
+
absWorkingDir: args.absSourceDir,
|
|
721
782
|
bundle: true,
|
|
722
|
-
entryPoints: [absEntryPoint],
|
|
723
|
-
external: [...externalPackages],
|
|
783
|
+
entryPoints: [args.absEntryPoint],
|
|
784
|
+
external: [...args.externalPackages],
|
|
724
785
|
format: "cjs",
|
|
725
786
|
outfile: "prebundled.cjs",
|
|
726
787
|
platform: "node",
|
|
727
|
-
plugins: [
|
|
788
|
+
plugins: [
|
|
789
|
+
workspaceSourcePlugin(args.workspacePackages, args.externalPackages),
|
|
790
|
+
],
|
|
728
791
|
splitting: false,
|
|
729
792
|
target: "node20",
|
|
730
793
|
write: false,
|
|
731
794
|
});
|
|
732
795
|
|
|
733
|
-
const bundledImplementation = implementationBuild.outputFiles?.find(
|
|
734
|
-
file.path.endsWith("prebundled.cjs"),
|
|
796
|
+
const bundledImplementation = implementationBuild.outputFiles?.find(
|
|
797
|
+
(file) => file.path.endsWith("prebundled.cjs"),
|
|
735
798
|
);
|
|
736
799
|
if (!bundledImplementation) {
|
|
737
|
-
throw new Error(
|
|
800
|
+
throw new Error(
|
|
801
|
+
"Bundler did not produce a deployment implementation file.",
|
|
802
|
+
);
|
|
738
803
|
}
|
|
739
804
|
|
|
740
805
|
// A separate ESM bundle is used only to read the entry module's exported
|
|
741
806
|
// workflow names. Scanning the CommonJS bundle would also see exports from
|
|
742
807
|
// bundled dependencies, which is not the deploy surface.
|
|
743
808
|
const exportBuild = await build({
|
|
744
|
-
absWorkingDir: absSourceDir,
|
|
809
|
+
absWorkingDir: args.absSourceDir,
|
|
745
810
|
bundle: true,
|
|
746
|
-
entryPoints: [absEntryPoint],
|
|
747
|
-
external: [...externalPackages],
|
|
811
|
+
entryPoints: [args.absEntryPoint],
|
|
812
|
+
external: [...args.externalPackages],
|
|
748
813
|
format: "esm",
|
|
749
814
|
outfile: "entry-exports.js",
|
|
750
815
|
platform: "node",
|
|
751
|
-
plugins: [
|
|
816
|
+
plugins: [
|
|
817
|
+
workspaceSourcePlugin(args.workspacePackages, args.externalPackages),
|
|
818
|
+
],
|
|
752
819
|
splitting: false,
|
|
753
820
|
target: "node20",
|
|
754
821
|
write: false,
|
|
@@ -764,12 +831,12 @@ export async function createHostedDeployPackage(
|
|
|
764
831
|
const exportNames = extractExportNamesFromEsmBundle(bundledExports.text);
|
|
765
832
|
if (exportNames.length === 0) {
|
|
766
833
|
throw new Error(
|
|
767
|
-
`No named exports were found in ${absEntryPoint}. Hosted deploy expects the entry point to export one or more workflows.`,
|
|
834
|
+
`No named exports were found in ${args.absEntryPoint}. Hosted deploy expects the entry point to export one or more workflows.`,
|
|
768
835
|
);
|
|
769
836
|
}
|
|
770
837
|
|
|
771
838
|
writeFileSync(
|
|
772
|
-
join(outputDir, "index.js"),
|
|
839
|
+
join(args.outputDir, "index.js"),
|
|
773
840
|
createBootstrapSource({
|
|
774
841
|
bundleBuffer: Buffer.from(bundledImplementation.contents),
|
|
775
842
|
deploymentName: args.deploymentName,
|
|
@@ -777,29 +844,77 @@ export async function createHostedDeployPackage(
|
|
|
777
844
|
}),
|
|
778
845
|
);
|
|
779
846
|
} catch (error) {
|
|
780
|
-
rmSync(tempRoot, { force: true, recursive: true });
|
|
781
847
|
throw new Error(
|
|
782
|
-
`Failed to bundle deploy entry point ${absEntryPoint}.\n${formatBuildError(error)}`,
|
|
848
|
+
`Failed to bundle deploy entry point ${args.absEntryPoint}.\n${formatBuildError(error)}`,
|
|
783
849
|
);
|
|
784
850
|
}
|
|
851
|
+
}
|
|
785
852
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
deploymentName: args.deploymentName,
|
|
792
|
-
outputDir,
|
|
793
|
-
sourceDir: absSourceDir,
|
|
794
|
-
});
|
|
853
|
+
export async function createHostedDeployPackage(
|
|
854
|
+
args: CreateHostedDeployPackageArgs,
|
|
855
|
+
): Promise<HostedDeployPackage> {
|
|
856
|
+
const absSourceDir = resolve(args.sourceDir);
|
|
857
|
+
ensureSourcePackageManifest(absSourceDir);
|
|
795
858
|
|
|
796
|
-
|
|
797
|
-
|
|
859
|
+
const absEntryPoint = resolveEntryPointPath(absSourceDir, args.entryPoint);
|
|
860
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "libretto-deploy-"));
|
|
861
|
+
const outputDir = join(tempRoot, "deploy");
|
|
862
|
+
mkdirSync(outputDir, { recursive: true });
|
|
863
|
+
const librettoDependency = resolveLibrettoDependency(absSourceDir);
|
|
864
|
+
|
|
865
|
+
const additionalExternals = [...new Set(args.additionalExternals ?? [])];
|
|
866
|
+
// These packages stay out of the implementation bundle. The generated
|
|
867
|
+
// package.json carries them into deploy-time installation, and the deployed
|
|
868
|
+
// code resolves them from node_modules.
|
|
869
|
+
const externalPackages = new Set<string>([
|
|
870
|
+
...DEFAULT_RUNTIME_EXTERNALS,
|
|
871
|
+
...additionalExternals,
|
|
872
|
+
]);
|
|
873
|
+
const workspacePackages = discoverWorkspacePackages(absSourceDir);
|
|
874
|
+
let callerOwnsTempRoot = false;
|
|
875
|
+
|
|
876
|
+
try {
|
|
877
|
+
await writeBundledDeployEntrypoint({
|
|
878
|
+
absEntryPoint,
|
|
879
|
+
absSourceDir,
|
|
880
|
+
deploymentName: args.deploymentName,
|
|
881
|
+
externalPackages,
|
|
882
|
+
outputDir,
|
|
883
|
+
workspacePackages,
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
if (librettoDependency === "file:./libretto") {
|
|
887
|
+
copyCurrentLibrettoPackage(outputDir);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// The generated manifest lists only packages that stay outside the
|
|
891
|
+
// implementation bundle. Hosted deploy installs them into the deployed
|
|
892
|
+
// package, and the deployed code loads them from node_modules.
|
|
893
|
+
writeDeployManifest({
|
|
894
|
+
additionalExternals,
|
|
895
|
+
deploymentName: args.deploymentName,
|
|
896
|
+
librettoDependency,
|
|
897
|
+
outputDir,
|
|
898
|
+
sourceDir: absSourceDir,
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// Success transfers ownership of the temp directory to the caller, who is
|
|
902
|
+
// responsible for invoking cleanup() after the tarball/upload step.
|
|
903
|
+
callerOwnsTempRoot = true;
|
|
904
|
+
return {
|
|
905
|
+
cleanup: () => {
|
|
906
|
+
rmSync(tempRoot, { force: true, recursive: true });
|
|
907
|
+
},
|
|
908
|
+
entryPoint: "index.js",
|
|
909
|
+
outputDir,
|
|
910
|
+
};
|
|
911
|
+
} finally {
|
|
912
|
+
// On any failure before we return, this function still owns the temp dir
|
|
913
|
+
// and must remove it to avoid leaking deploy workspaces in /tmp.
|
|
914
|
+
if (!callerOwnsTempRoot) {
|
|
798
915
|
rmSync(tempRoot, { force: true, recursive: true });
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
outputDir,
|
|
802
|
-
};
|
|
916
|
+
}
|
|
917
|
+
}
|
|
803
918
|
}
|
|
804
919
|
|
|
805
920
|
export async function buildHostedDeployTarball(
|
package/src/index.ts
CHANGED
|
@@ -3,34 +3,32 @@ import type { MinimalLogger } from "../logger/logger.js";
|
|
|
3
3
|
|
|
4
4
|
export const LIBRETTO_WORKFLOW_BRAND = Symbol.for("libretto.workflow");
|
|
5
5
|
|
|
6
|
-
export type LibrettoWorkflowContext
|
|
6
|
+
export type LibrettoWorkflowContext = {
|
|
7
7
|
session: string;
|
|
8
8
|
page: Page;
|
|
9
9
|
logger: MinimalLogger;
|
|
10
|
-
services: S;
|
|
11
10
|
credentials?: Record<string, unknown>;
|
|
12
11
|
};
|
|
13
12
|
|
|
14
|
-
export type LibrettoWorkflowHandler<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
> = (ctx: LibrettoWorkflowContext<S>, input: Input) => Promise<Output>;
|
|
13
|
+
export type LibrettoWorkflowHandler<Input = unknown, Output = unknown> = (
|
|
14
|
+
ctx: LibrettoWorkflowContext,
|
|
15
|
+
input: Input,
|
|
16
|
+
) => Promise<Output>;
|
|
19
17
|
|
|
20
|
-
export class LibrettoWorkflow<Input = unknown, Output = unknown
|
|
18
|
+
export class LibrettoWorkflow<Input = unknown, Output = unknown> {
|
|
21
19
|
public readonly [LIBRETTO_WORKFLOW_BRAND] = true;
|
|
22
20
|
public readonly name: string;
|
|
23
|
-
private readonly handler: LibrettoWorkflowHandler<Input, Output
|
|
21
|
+
private readonly handler: LibrettoWorkflowHandler<Input, Output>;
|
|
24
22
|
|
|
25
23
|
constructor(
|
|
26
24
|
name: string,
|
|
27
|
-
handler: LibrettoWorkflowHandler<Input, Output
|
|
25
|
+
handler: LibrettoWorkflowHandler<Input, Output>,
|
|
28
26
|
) {
|
|
29
27
|
this.name = name;
|
|
30
28
|
this.handler = handler;
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
async run(ctx: LibrettoWorkflowContext
|
|
31
|
+
async run(ctx: LibrettoWorkflowContext, input: Input): Promise<Output> {
|
|
34
32
|
return this.handler(ctx, input);
|
|
35
33
|
}
|
|
36
34
|
}
|
|
@@ -114,9 +112,9 @@ export function getWorkflowFromModuleExports(
|
|
|
114
112
|
return null;
|
|
115
113
|
}
|
|
116
114
|
|
|
117
|
-
export function workflow<Input = unknown, Output = unknown
|
|
115
|
+
export function workflow<Input = unknown, Output = unknown>(
|
|
118
116
|
name: string,
|
|
119
|
-
handler: LibrettoWorkflowHandler<Input, Output
|
|
120
|
-
): LibrettoWorkflow<Input, Output
|
|
117
|
+
handler: LibrettoWorkflowHandler<Input, Output>,
|
|
118
|
+
): LibrettoWorkflow<Input, Output> {
|
|
121
119
|
return new LibrettoWorkflow(name, handler);
|
|
122
120
|
}
|