@commercetools/cli-topic-integration-layer 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/commands/integration-layer/extension/build.d.ts +2 -0
- package/dist/commands/integration-layer/extension/build.js +24 -3
- package/dist/commands/integration-layer/extension/build.js.map +1 -1
- package/dist/commands/integration-layer/extension/push.d.ts +15 -0
- package/dist/commands/integration-layer/extension/push.js +137 -3
- package/dist/commands/integration-layer/extension/push.js.map +1 -1
- package/dist/commands/integration-layer/extension/serve.d.ts +19 -0
- package/dist/commands/integration-layer/extension/serve.js +211 -3
- package/dist/commands/integration-layer/extension/serve.js.map +1 -1
- package/dist/commands/integration-layer/extension/status.js +4 -0
- package/dist/commands/integration-layer/extension/status.js.map +1 -1
- package/dist/commands/integration-layer/extension/validate.d.ts +2 -0
- package/dist/commands/integration-layer/extension/validate.js +24 -2
- package/dist/commands/integration-layer/extension/validate.js.map +1 -1
- package/dist/commands/integration-layer/init.js +219 -75
- package/dist/commands/integration-layer/init.js.map +1 -1
- package/dist/lib/awaitBundleState.d.ts +33 -0
- package/dist/lib/awaitBundleState.js +77 -0
- package/dist/lib/awaitBundleState.js.map +1 -0
- package/dist/lib/ilClient.d.ts +46 -2
- package/dist/lib/ilClient.js +14 -2
- package/dist/lib/ilClient.js.map +1 -1
- package/dist/lib/sourceRevision.d.ts +39 -0
- package/dist/lib/sourceRevision.js +95 -0
- package/dist/lib/sourceRevision.js.map +1 -0
- package/dist/lib/tooling/extensions.d.ts +77 -0
- package/dist/lib/tooling/extensions.js +209 -0
- package/dist/lib/tooling/extensions.js.map +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ commercetools integration-layer init <DIR> # scaffold an extension
|
|
|
24
24
|
commercetools integration-layer extension build # bundle src/extension.ts -> dist [local]
|
|
25
25
|
commercetools integration-layer extension validate # local checks + remote compose/breaking gate
|
|
26
26
|
commercetools integration-layer extension push [--force] # build + validate + upload the bundle
|
|
27
|
+
[--source-revision <rev> | --no-source-revision] # provenance; default: detected from git
|
|
27
28
|
commercetools integration-layer extension status # show the project's stored bundle
|
|
28
29
|
commercetools integration-layer extension delete [--yes] # remove the extension subgraph
|
|
29
30
|
|
|
@@ -5,6 +5,8 @@ export default class ExtensionBuild extends Command {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
entry: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
out: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
"extensions-dir": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
10
|
};
|
|
9
11
|
run(): Promise<void>;
|
|
10
12
|
}
|
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
import { Command, Flags } from "@oclif/core";
|
|
2
|
-
import {
|
|
2
|
+
import { defaultEntry, defaultOutfile } from "../../../lib/tooling/build.js";
|
|
3
|
+
import { bundleForFlags } from "../../../lib/tooling/extensions.js";
|
|
3
4
|
export default class ExtensionBuild extends Command {
|
|
4
5
|
static description = "Bundle src/extension.ts into a single self-contained CommonJS artifact";
|
|
5
6
|
static examples = [
|
|
6
7
|
"<%= config.bin %> integration-layer extension build",
|
|
7
8
|
"<%= config.bin %> integration-layer extension build --entry src/extension.ts --out dist/extension.js",
|
|
9
|
+
"<%= config.bin %> integration-layer extension build --all",
|
|
8
10
|
];
|
|
9
11
|
static flags = {
|
|
10
12
|
entry: Flags.string({ description: "extension entry source file", default: defaultEntry() }),
|
|
11
13
|
out: Flags.string({ description: "bundle output file", default: defaultOutfile() }),
|
|
14
|
+
all: Flags.boolean({
|
|
15
|
+
description: "merge every extension under ./extensions/* into ONE combined bundle (the single artifact a project deploys)",
|
|
16
|
+
default: false,
|
|
17
|
+
}),
|
|
18
|
+
"extensions-dir": Flags.string({
|
|
19
|
+
description: "directory holding the extension packages (used with --all)",
|
|
20
|
+
default: "extensions",
|
|
21
|
+
}),
|
|
12
22
|
};
|
|
13
23
|
async run() {
|
|
14
24
|
const { flags } = await this.parse(ExtensionBuild);
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
let built;
|
|
26
|
+
try {
|
|
27
|
+
built = await bundleForFlags({
|
|
28
|
+
all: flags.all,
|
|
29
|
+
extensionsDir: flags["extensions-dir"],
|
|
30
|
+
entry: flags.entry,
|
|
31
|
+
out: flags.out,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
this.error(err.message);
|
|
36
|
+
}
|
|
37
|
+
this.log(`✓ bundled ${built.describe} → ${built.outfile}`);
|
|
17
38
|
}
|
|
18
39
|
}
|
|
19
40
|
//# sourceMappingURL=build.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../../../src/commands/integration-layer/extension/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../../../src/commands/integration-layer/extension/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEpE,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,OAAO;IACjD,MAAM,CAAU,WAAW,GACzB,wEAAwE,CAAC;IAE3E,MAAM,CAAU,QAAQ,GAAG;QACzB,qDAAqD;QACrD,sGAAsG;QACtG,2DAA2D;KAC5D,CAAC;IAEF,MAAM,CAAU,KAAK,GAAG;QACtB,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,6BAA6B,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,CAAC;QAC5F,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,oBAAoB,EAAE,OAAO,EAAE,cAAc,EAAE,EAAE,CAAC;QACnF,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC;YACjB,WAAW,EACT,6GAA6G;YAC/G,OAAO,EAAE,KAAK;SACf,CAAC;QACF,gBAAgB,EAAE,KAAK,CAAC,MAAM,CAAC;YAC7B,WAAW,EAAE,4DAA4D;YACzE,OAAO,EAAE,YAAY;SACtB,CAAC;KACH,CAAC;IAEF,KAAK,CAAC,GAAG;QACP,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QACnD,IAAI,KAAK,CAAC;QACV,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,cAAc,CAAC;gBAC3B,GAAG,EAAE,KAAK,CAAC,GAAG;gBACd,aAAa,EAAE,KAAK,CAAC,gBAAgB,CAAC;gBACtC,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,GAAG,EAAE,KAAK,CAAC,GAAG;aACf,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,KAAK,CAAE,GAAa,CAAC,OAAO,CAAC,CAAC;QACrC,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,aAAa,KAAK,CAAC,QAAQ,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7D,CAAC"}
|
|
@@ -5,8 +5,23 @@ export default class ExtensionPush extends IntegrationLayerCommand {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
entry: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
out: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
"extensions-dir": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
10
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
"source-revision": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
"no-source-revision": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
wait: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
"wait-timeout": import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* Report how the pushed version fared and say whether the command should fail.
|
|
18
|
+
*
|
|
19
|
+
* Only a genuine `failed` verdict is an error. Not being ABLE to find out — an
|
|
20
|
+
* older integration layer, an extension that isn't deployed, a metadata read that
|
|
21
|
+
* errored — is reported honestly and left as a success, because the push itself
|
|
22
|
+
* did land and failing it would break workflows that never had this signal.
|
|
23
|
+
*/
|
|
24
|
+
private reportOutcome;
|
|
10
25
|
/** True when the remote result should abort the push. */
|
|
11
26
|
private reportRemote;
|
|
12
27
|
run(): Promise<void>;
|
|
@@ -1,25 +1,114 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { Flags } from "@oclif/core";
|
|
4
|
-
import {
|
|
4
|
+
import { defaultEntry, defaultOutfile } from "../../../lib/tooling/build.js";
|
|
5
|
+
import { bundleForFlags } from "../../../lib/tooling/extensions.js";
|
|
5
6
|
import { validateBundle, BundleValidationError } from "../../../lib/tooling/validateBundle.js";
|
|
6
7
|
import { pushBundle, remoteValidate } from "../../../lib/ilClient.js";
|
|
8
|
+
import { awaitBundleState } from "../../../lib/awaitBundleState.js";
|
|
9
|
+
import { resolveSourceRevision } from "../../../lib/sourceRevision.js";
|
|
7
10
|
import { IntegrationLayerCommand } from "../../../lib/base.js";
|
|
11
|
+
// How often to re-read the bundle's status while waiting. The extension re-reads its
|
|
12
|
+
// bundle on its own ~30s poll, so a tighter interval just adds requests without
|
|
13
|
+
// learning anything sooner; 5s keeps the command feeling responsive when the
|
|
14
|
+
// extension happens to be mid-poll already.
|
|
15
|
+
const POLL_INTERVAL_MS = 5000;
|
|
16
|
+
/**
|
|
17
|
+
* Name a build the way its author recognises it: the integration layer's version
|
|
18
|
+
* number AND the source revision it was built from. The number is an internal
|
|
19
|
+
* counter — the commit is the identity. Falls back to the bare version when the
|
|
20
|
+
* push recorded no revision.
|
|
21
|
+
*/
|
|
22
|
+
function describeBuild(version, sourceRevision) {
|
|
23
|
+
return sourceRevision
|
|
24
|
+
? `version ${version} (source revision ${sourceRevision})`
|
|
25
|
+
: `version ${version}`;
|
|
26
|
+
}
|
|
8
27
|
export default class ExtensionPush extends IntegrationLayerCommand {
|
|
9
28
|
static description = "Build + validate (local + remote) + upload the bundle, replacing the project's stored one";
|
|
10
29
|
static examples = [
|
|
11
30
|
"<%= config.bin %> integration-layer extension push",
|
|
12
31
|
"<%= config.bin %> integration-layer extension push --force",
|
|
32
|
+
"<%= config.bin %> integration-layer extension push --all",
|
|
33
|
+
"<%= config.bin %> integration-layer extension push --source-revision r48211",
|
|
34
|
+
"<%= config.bin %> integration-layer extension push --no-source-revision",
|
|
13
35
|
];
|
|
14
36
|
static flags = {
|
|
15
37
|
entry: Flags.string({ description: "extension entry source file", default: defaultEntry() }),
|
|
16
38
|
out: Flags.string({ description: "bundle output file", default: defaultOutfile() }),
|
|
39
|
+
all: Flags.boolean({
|
|
40
|
+
description: "merge every extension under ./extensions/* into ONE combined bundle and push that single artifact (the deployed shape)",
|
|
41
|
+
default: false,
|
|
42
|
+
}),
|
|
43
|
+
"extensions-dir": Flags.string({
|
|
44
|
+
description: "directory holding the extension packages (used with --all)",
|
|
45
|
+
default: "extensions",
|
|
46
|
+
}),
|
|
17
47
|
force: Flags.boolean({
|
|
18
48
|
char: "f",
|
|
19
49
|
description: "upload despite a failing REMOTE validation (the local check always hard-fails)",
|
|
20
50
|
default: false,
|
|
21
51
|
}),
|
|
52
|
+
// Provenance. The default is auto-detection from the working copy, so a plain
|
|
53
|
+
// `push` records where the build came from with no ceremony; the flag is for a
|
|
54
|
+
// non-git VCS, a CI build id, or anything else the integrator identifies revisions
|
|
55
|
+
// by (the integration layer stores an opaque string).
|
|
56
|
+
"source-revision": Flags.string({
|
|
57
|
+
description: "version-control revision this build came from (default: detected from the git working copy; also settable via EXTENSION_SOURCE_REVISION)",
|
|
58
|
+
env: "EXTENSION_SOURCE_REVISION",
|
|
59
|
+
exclusive: ["no-source-revision"],
|
|
60
|
+
}),
|
|
61
|
+
"no-source-revision": Flags.boolean({
|
|
62
|
+
description: "push without recording any source revision",
|
|
63
|
+
default: false,
|
|
64
|
+
}),
|
|
65
|
+
// A push only STORES the bundle; the extension loads it on its own poll and
|
|
66
|
+
// reports back. Waiting for that verdict is the default because a push that
|
|
67
|
+
// reports success while the bundle can't actually run is the thing worth
|
|
68
|
+
// catching — especially in CI, where this is the difference between a green
|
|
69
|
+
// pipeline and a green pipeline that shipped a broken extension.
|
|
70
|
+
wait: Flags.boolean({
|
|
71
|
+
description: "wait for the extension to load the pushed version and report back (use --no-wait to return as soon as it is stored)",
|
|
72
|
+
default: true,
|
|
73
|
+
allowNo: true,
|
|
74
|
+
}),
|
|
75
|
+
"wait-timeout": Flags.integer({
|
|
76
|
+
description: "seconds to wait for the pushed version to be confirmed before giving up (the push still stands)",
|
|
77
|
+
default: 180,
|
|
78
|
+
}),
|
|
22
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* Report how the pushed version fared and say whether the command should fail.
|
|
82
|
+
*
|
|
83
|
+
* Only a genuine `failed` verdict is an error. Not being ABLE to find out — an
|
|
84
|
+
* older integration layer, an extension that isn't deployed, a metadata read that
|
|
85
|
+
* errored — is reported honestly and left as a success, because the push itself
|
|
86
|
+
* did land and failing it would break workflows that never had this signal.
|
|
87
|
+
*/
|
|
88
|
+
reportOutcome(outcome, version) {
|
|
89
|
+
switch (outcome.kind) {
|
|
90
|
+
case "running":
|
|
91
|
+
this.log(`✓ ${describeBuild(version, outcome.meta.sourceRevision)} is running — the extension loaded it and published its schema`);
|
|
92
|
+
return false;
|
|
93
|
+
case "failed": {
|
|
94
|
+
this.logToStderr(`✗ ${describeBuild(version, outcome.meta.sourceRevision)} could not be loaded and is NOT in use.`);
|
|
95
|
+
if (outcome.meta.reason)
|
|
96
|
+
this.logToStderr(` reason: ${outcome.meta.reason}`);
|
|
97
|
+
const served = outcome.meta.served;
|
|
98
|
+
this.logToStderr(served == null
|
|
99
|
+
? " no stored version could be loaded, so this project currently has no extension running."
|
|
100
|
+
: ` ${describeBuild(served.version, served.sourceRevision)} is still in use; fix the bundle and push again.`);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
case "superseded":
|
|
104
|
+
this.log(`⚠ another push replaced version ${version} with ${describeBuild(outcome.meta.version, outcome.meta.sourceRevision)} while waiting; not reporting on it`);
|
|
105
|
+
return false;
|
|
106
|
+
case "unknown":
|
|
107
|
+
this.log(`⚠ could not confirm version ${version}: ${outcome.reason}`);
|
|
108
|
+
this.log(" the bundle is stored; check the extension panel in the Merchant Center.");
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
23
112
|
/** True when the remote result should abort the push. */
|
|
24
113
|
reportRemote(result) {
|
|
25
114
|
if (!result.composes) {
|
|
@@ -45,7 +134,19 @@ export default class ExtensionPush extends IntegrationLayerCommand {
|
|
|
45
134
|
const { flags } = await this.parse(ExtensionPush);
|
|
46
135
|
// Bundle, then VALIDATE before we touch the store. Local check first — it always
|
|
47
136
|
// hard-fails, regardless of --force.
|
|
48
|
-
|
|
137
|
+
let outfile;
|
|
138
|
+
let sourceFiles;
|
|
139
|
+
try {
|
|
140
|
+
({ outfile, sourceFiles } = await bundleForFlags({
|
|
141
|
+
all: flags.all,
|
|
142
|
+
extensionsDir: flags["extensions-dir"],
|
|
143
|
+
entry: flags.entry,
|
|
144
|
+
out: flags.out,
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
this.error(err.message);
|
|
149
|
+
}
|
|
49
150
|
let typeDefs;
|
|
50
151
|
try {
|
|
51
152
|
const local = await validateBundle(outfile, sourceFiles);
|
|
@@ -74,10 +175,43 @@ export default class ExtensionPush extends IntegrationLayerCommand {
|
|
|
74
175
|
}
|
|
75
176
|
const bundle = await readFile(outfile, "utf8");
|
|
76
177
|
const filename = `${basename(outfile).replace(/\.[^.]+$/, "")}.cjs`;
|
|
178
|
+
// Which of the integrator's revisions this build came from. Reported so the
|
|
179
|
+
// Merchant Center, the connector's log lines and `{ _extensionBundle }` can all
|
|
180
|
+
// name it. Detected from the working copy unless the caller said otherwise; a
|
|
181
|
+
// build outside version control simply reports none (see sourceRevision.ts).
|
|
182
|
+
const sourceRevision = flags["no-source-revision"]
|
|
183
|
+
? undefined
|
|
184
|
+
: await resolveSourceRevision(flags["source-revision"], process.cwd());
|
|
185
|
+
if (sourceRevision) {
|
|
186
|
+
this.log(`Source revision: ${sourceRevision}`);
|
|
187
|
+
}
|
|
188
|
+
else if (!flags["no-source-revision"]) {
|
|
189
|
+
// Say so rather than staying silent: the operator console and the connector
|
|
190
|
+
// logs will show no revision for this push, and that's worth knowing now.
|
|
191
|
+
this.logToStderr("⚠ no source revision detected (not a git checkout?) — pushing without one. " +
|
|
192
|
+
"Pass --source-revision to record one explicitly.");
|
|
193
|
+
}
|
|
77
194
|
const url = `${baseUrl}/${encodeURIComponent(projectKey)}/extension/bundle`;
|
|
78
195
|
this.log(`Pushing extension bundle (${bundle.length} bytes) → ${url}`);
|
|
79
|
-
const meta = await pushBundle(baseUrl, projectKey, token, bundle, filename);
|
|
196
|
+
const meta = await pushBundle(baseUrl, projectKey, token, bundle, filename, sourceRevision);
|
|
80
197
|
this.log(`✓ stored revision ${meta.version} (${meta.length} bytes, filename ${meta.filename ?? filename})`);
|
|
198
|
+
if (meta.sourceRevision) {
|
|
199
|
+
this.log(` built from ${meta.sourceRevision}`);
|
|
200
|
+
}
|
|
201
|
+
if (!flags.wait) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Storing it isn't running it. Wait for the extension to actually load this
|
|
205
|
+
// version and say so, and fail the command if it couldn't — otherwise a bundle
|
|
206
|
+
// the sandbox refuses to run exits 0 and looks like a successful push.
|
|
207
|
+
this.log(`Waiting for the extension to load version ${meta.version}…`);
|
|
208
|
+
const outcome = await awaitBundleState(baseUrl, projectKey, token, meta.version, {
|
|
209
|
+
timeoutMs: flags["wait-timeout"] * 1000,
|
|
210
|
+
intervalMs: POLL_INTERVAL_MS,
|
|
211
|
+
});
|
|
212
|
+
if (this.reportOutcome(outcome, meta.version)) {
|
|
213
|
+
this.error("The pushed bundle could not be loaded.");
|
|
214
|
+
}
|
|
81
215
|
}
|
|
82
216
|
}
|
|
83
217
|
//# sourceMappingURL=push.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"push.js","sourceRoot":"","sources":["../../../../src/commands/integration-layer/extension/push.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"push.js","sourceRoot":"","sources":["../../../../src/commands/integration-layer/extension/push.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,12 +7,31 @@ export default class ExtensionServe extends IntegrationLayerCommand {
|
|
|
7
7
|
port: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
8
|
compose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
gateway: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
"extensions-dir": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
};
|
|
11
13
|
run(): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* `--all`: merge every extension under `./extensions/*` into ONE federation subgraph —
|
|
16
|
+
* the single bundle a project deploys — and serve it behind a local federated gateway
|
|
17
|
+
* with the deployed integration layer, exactly the two-subgraph shape the router plans
|
|
18
|
+
* over (core + the combined `extensions` subgraph). Like `--gateway` it reaches the
|
|
19
|
+
* integration layer (fetches its SDL, mints an anonymous session), so it needs a
|
|
20
|
+
* `commercetools auth login`. Watches every extension and hot-reloads: any edit
|
|
21
|
+
* re-merges the combined subgraph, and a schema change recomposes + rebuilds the gateway.
|
|
22
|
+
*/
|
|
23
|
+
private runAll;
|
|
12
24
|
/**
|
|
13
25
|
* Start esbuild in watch mode over the current example and feed `onBuild` a freshly
|
|
14
26
|
* loaded subgraph on every (re)build. Resolves once the initial build has produced
|
|
15
27
|
* one (so the caller can start serving); later rebuilds fire `onBuild` again.
|
|
16
28
|
*/
|
|
17
29
|
private watchAndBuild;
|
|
30
|
+
/**
|
|
31
|
+
* The generic watcher underneath {@link watchAndBuild}: esbuild-watch `entry`, and on
|
|
32
|
+
* every successful (re)build load the bundle and hand the raw module to `onReload`.
|
|
33
|
+
* `--all` uses this directly (it needs the module's resolvers to merge subgraphs);
|
|
34
|
+
* single-mode wraps it to build one subgraph.
|
|
35
|
+
*/
|
|
36
|
+
private watchEntry;
|
|
18
37
|
}
|
|
@@ -23,7 +23,8 @@ import { buildSubgraphSchema } from "@apollo/subgraph";
|
|
|
23
23
|
import { context as esbuildContext } from "esbuild";
|
|
24
24
|
import { defaultEntry, defaultOutfile, HOST_PROVIDED_EXTERNALS } from "../../../lib/tooling/build.js";
|
|
25
25
|
import { loadBundleSource } from "../../../lib/tooling/loadBundle.js";
|
|
26
|
-
import { composeWithIntegrationLayer
|
|
26
|
+
import { composeWithIntegrationLayer } from "../../../lib/tooling/compose.js";
|
|
27
|
+
import { discoverExtensions, mergeExtensionSubgraph } from "../../../lib/tooling/extensions.js";
|
|
27
28
|
import { makeGateway, mintAnonymousSession } from "../../../lib/tooling/gateway.js";
|
|
28
29
|
import { fetchSubgraphSdl } from "../../../lib/ilClient.js";
|
|
29
30
|
import { IntegrationLayerCommand } from "../../../lib/base.js";
|
|
@@ -84,9 +85,21 @@ export default class ExtensionServe extends IntegrationLayerCommand {
|
|
|
84
85
|
description: "make /graphql an executable federated gateway over both subgraphs",
|
|
85
86
|
default: false,
|
|
86
87
|
}),
|
|
88
|
+
all: Flags.boolean({
|
|
89
|
+
description: "merge every extension under ./extensions/* into ONE subgraph (the single deployed bundle) and serve it behind a local federated gateway with the integration layer",
|
|
90
|
+
default: false,
|
|
91
|
+
}),
|
|
92
|
+
"extensions-dir": Flags.string({
|
|
93
|
+
description: "directory holding the extension packages (used with --all)",
|
|
94
|
+
default: "extensions",
|
|
95
|
+
}),
|
|
87
96
|
};
|
|
88
97
|
async run() {
|
|
89
98
|
const { flags } = await this.parse(ExtensionServe);
|
|
99
|
+
if (flags.all) {
|
|
100
|
+
await this.runAll(flags);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
90
103
|
const entry = defaultEntry();
|
|
91
104
|
const outfile = defaultOutfile();
|
|
92
105
|
const port = flags.port;
|
|
@@ -253,16 +266,211 @@ export default class ExtensionServe extends IntegrationLayerCommand {
|
|
|
253
266
|
// Keep the process alive; the server + esbuild watcher run until a signal.
|
|
254
267
|
await new Promise(() => { });
|
|
255
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* `--all`: merge every extension under `./extensions/*` into ONE federation subgraph —
|
|
271
|
+
* the single bundle a project deploys — and serve it behind a local federated gateway
|
|
272
|
+
* with the deployed integration layer, exactly the two-subgraph shape the router plans
|
|
273
|
+
* over (core + the combined `extensions` subgraph). Like `--gateway` it reaches the
|
|
274
|
+
* integration layer (fetches its SDL, mints an anonymous session), so it needs a
|
|
275
|
+
* `commercetools auth login`. Watches every extension and hot-reloads: any edit
|
|
276
|
+
* re-merges the combined subgraph, and a schema change recomposes + rebuilds the gateway.
|
|
277
|
+
*/
|
|
278
|
+
async runAll(flags) {
|
|
279
|
+
const port = flags.port;
|
|
280
|
+
const root = process.cwd();
|
|
281
|
+
const extensions = await discoverExtensions(root, flags["extensions-dir"]);
|
|
282
|
+
if (extensions.length === 0) {
|
|
283
|
+
this.error(`no extensions found under ./${flags["extensions-dir"]}/*/src/extension.ts — run --all from the monorepo root`);
|
|
284
|
+
}
|
|
285
|
+
// Reach the integration layer exactly like --gateway: its core-subgraph SDL is the
|
|
286
|
+
// compose baseline, and an anonymous session authenticates the gateway's calls.
|
|
287
|
+
const { baseUrl, projectKey, token } = await this.resolveIlContext(flags);
|
|
288
|
+
const integrationLayerGraphqlUrl = `${baseUrl}/${encodeURIComponent(projectKey)}/graphql`;
|
|
289
|
+
this.log(`Fetching integration-layer subgraph SDL for '${projectKey}' from ${baseUrl} …`);
|
|
290
|
+
const integrationLayerSdl = await fetchSubgraphSdl(baseUrl, projectKey, token);
|
|
291
|
+
const integrationLayerBearer = await mintAnonymousSession(baseUrl, projectKey);
|
|
292
|
+
this.log("Minted an anonymous session for the gateway.");
|
|
293
|
+
// The single combined-extensions subgraph endpoint — the gateway's one data source
|
|
294
|
+
// besides the integration layer, named `extensions` just like the deployed bundle.
|
|
295
|
+
const extensionUrl = `http://localhost:${port}/_extension`;
|
|
296
|
+
// Per-extension loaded module (typeDefs + resolvers), merged into ONE subgraph on
|
|
297
|
+
// each rebuild. The yoga factories/gateway close over these so a reload applies with
|
|
298
|
+
// no restart.
|
|
299
|
+
const modules = new Map();
|
|
300
|
+
let combinedSchema;
|
|
301
|
+
let composed;
|
|
302
|
+
let gatewayYoga;
|
|
303
|
+
let gatewaySupergraphSdl;
|
|
304
|
+
let apolloGateway;
|
|
305
|
+
const log = (m) => this.log(m);
|
|
306
|
+
const warn = (m) => this.logToStderr(m);
|
|
307
|
+
// Merge all extensions into one subgraph, compose with the integration layer, and
|
|
308
|
+
// (re)build the gateway when the supergraph changes. No-op until every extension has
|
|
309
|
+
// produced its first build; a string-compare skips the gateway rebuild for a
|
|
310
|
+
// resolver-only edit (the /_extension hot-reload alone covers it).
|
|
311
|
+
const recompose = async () => {
|
|
312
|
+
if (modules.size < extensions.length)
|
|
313
|
+
return;
|
|
314
|
+
try {
|
|
315
|
+
const merged = mergeExtensionSubgraph([...modules].map(([name, m]) => ({ name, typeDefs: m.typeDefs, resolvers: m.resolvers })));
|
|
316
|
+
combinedSchema = merged.schema;
|
|
317
|
+
composed = composeWithIntegrationLayer(integrationLayerSdl, merged.sdl, {
|
|
318
|
+
integrationLayerUrl: integrationLayerGraphqlUrl,
|
|
319
|
+
extensionUrl,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
warn(`✗ ${err.message}`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!composed.ok) {
|
|
327
|
+
warn("✗ extensions do not compose with the integration layer:");
|
|
328
|
+
for (const e of composed.errors)
|
|
329
|
+
warn(` - ${e}`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
log(`✓ composes with the integration layer — full schema at http://localhost:${port}/composed`);
|
|
333
|
+
if (composed.supergraphSdl === gatewaySupergraphSdl) {
|
|
334
|
+
log(" (gateway plan unchanged — extension reloaded in place)");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const gw = await makeGateway({ supergraphSdl: composed.supergraphSdl, integrationLayerBearer });
|
|
339
|
+
const prev = apolloGateway;
|
|
340
|
+
apolloGateway = gw;
|
|
341
|
+
gatewaySupergraphSdl = composed.supergraphSdl;
|
|
342
|
+
gatewayYoga = createYoga({
|
|
343
|
+
plugins: [useApolloFederation({ gateway: gw })],
|
|
344
|
+
graphqlEndpoint: "/graphql",
|
|
345
|
+
landingPage: false,
|
|
346
|
+
});
|
|
347
|
+
log(`✓ gateway ready at http://localhost:${port}/graphql`);
|
|
348
|
+
if (prev)
|
|
349
|
+
await prev.stop();
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
warn(`✗ gateway build failed: ${err.message}`);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
// Watch + build each extension; awaiting sequentially means the last first-build
|
|
356
|
+
// triggers the initial merge/compose. `recompose` fires again on every later rebuild.
|
|
357
|
+
const contexts = [];
|
|
358
|
+
for (const ext of extensions) {
|
|
359
|
+
const ctx = await this.watchEntry(ext.entry, ext.outfile, async (mod) => {
|
|
360
|
+
const { typeDefs, resolvers } = mod;
|
|
361
|
+
if (typeof typeDefs !== "string" || typeDefs.trim() === "") {
|
|
362
|
+
warn(`✗ extension '${ext.name}' exported no \`typeDefs\` string`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (resolvers === null || typeof resolvers !== "object") {
|
|
366
|
+
warn(`✗ extension '${ext.name}' exported no \`resolvers\` object`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
modules.set(ext.name, { typeDefs, resolvers });
|
|
370
|
+
await recompose();
|
|
371
|
+
});
|
|
372
|
+
contexts.push(ctx);
|
|
373
|
+
}
|
|
374
|
+
if (!combinedSchema) {
|
|
375
|
+
for (const ctx of contexts)
|
|
376
|
+
await ctx.dispose();
|
|
377
|
+
this.error("extensions did not merge into a subgraph (see errors above)");
|
|
378
|
+
}
|
|
379
|
+
if (composed && !composed.ok) {
|
|
380
|
+
warn("⚠ extensions do not compose with the integration layer yet — the gateway is " +
|
|
381
|
+
"unavailable until they do; fix the errors above and save to recompose.");
|
|
382
|
+
}
|
|
383
|
+
// The combined extensions subgraph (executable), at the internal /_extension the
|
|
384
|
+
// gateway reaches over HTTP. `schema: () => …` re-reads the latest merge, so a
|
|
385
|
+
// hot-reload applies with no restart.
|
|
386
|
+
const subgraphYoga = createYoga({
|
|
387
|
+
schema: () => combinedSchema,
|
|
388
|
+
context: () => devContext(),
|
|
389
|
+
graphqlEndpoint: "/_extension",
|
|
390
|
+
landingPage: false,
|
|
391
|
+
});
|
|
392
|
+
// The browsable (not executable) merged API schema.
|
|
393
|
+
const composedYoga = createYoga({
|
|
394
|
+
schema: () => {
|
|
395
|
+
if (!composed?.ok) {
|
|
396
|
+
throw new Error(`extensions do not compose with the integration layer:\n${(composed?.errors ?? []).join("\n")}`);
|
|
397
|
+
}
|
|
398
|
+
return composed.apiSchema;
|
|
399
|
+
},
|
|
400
|
+
graphqlEndpoint: "/composed",
|
|
401
|
+
landingPage: false,
|
|
402
|
+
});
|
|
403
|
+
const server = createServer((req, res) => {
|
|
404
|
+
const path = (req.url ?? "").split("?")[0];
|
|
405
|
+
if (req.method === "GET" && (path === "/schema.graphql" || path === "/supergraph.graphql")) {
|
|
406
|
+
if (!composed?.ok) {
|
|
407
|
+
res.statusCode = 503;
|
|
408
|
+
res.setHeader("content-type", "text/plain");
|
|
409
|
+
res.end(`extensions do not compose with the integration layer:\n${(composed?.errors ?? []).join("\n")}\n`);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
413
|
+
res.end(path === "/schema.graphql" ? composed.apiSdl : composed.supergraphSdl);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (path.startsWith("/composed"))
|
|
417
|
+
return void composedYoga(req, res);
|
|
418
|
+
if (path.startsWith("/graphql")) {
|
|
419
|
+
if (!gatewayYoga) {
|
|
420
|
+
const errs = composed && !composed.ok ? composed.errors : [];
|
|
421
|
+
res.statusCode = 503;
|
|
422
|
+
res.setHeader("content-type", "text/plain");
|
|
423
|
+
res.end(`gateway not ready — extensions do not compose with the integration layer:\n${errs.join("\n")}\n`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
return void gatewayYoga(req, res);
|
|
427
|
+
}
|
|
428
|
+
// /_extension — the combined extensions subgraph.
|
|
429
|
+
return void subgraphYoga(req, res);
|
|
430
|
+
});
|
|
431
|
+
await new Promise((resolve) => server.listen(port, resolve));
|
|
432
|
+
const base = `http://localhost:${port}`;
|
|
433
|
+
const names = extensions.map((e) => e.name).join(", ");
|
|
434
|
+
const lines = [
|
|
435
|
+
`\n🔌 ${extensions.length} extension(s) merged into one subgraph (${names}), live behind a federated gateway:`,
|
|
436
|
+
];
|
|
437
|
+
lines.push(` ${base}/graphql — FEDERATED gateway (combined extensions + deployed integration layer)`);
|
|
438
|
+
lines.push(` ${base}/_extension — the combined extensions subgraph (gateway routes to it)`);
|
|
439
|
+
lines.push(` ${base}/composed — full merged schema (browsable; not executable)`);
|
|
440
|
+
lines.push(` ${base}/schema.graphql, ${base}/supergraph.graphql — SDL (text)`);
|
|
441
|
+
lines.push(` open ${base}/graphql in a browser for GraphiQL`);
|
|
442
|
+
lines.push(` watching ${extensions.length} extension(s) — edit and save to hot-reload\n`);
|
|
443
|
+
this.log(lines.join("\n"));
|
|
444
|
+
const shutdown = () => {
|
|
445
|
+
for (const ctx of contexts)
|
|
446
|
+
void ctx.dispose();
|
|
447
|
+
server.close(() => process.exit(0));
|
|
448
|
+
};
|
|
449
|
+
process.on("SIGINT", shutdown);
|
|
450
|
+
process.on("SIGTERM", shutdown);
|
|
451
|
+
// Keep the process alive; the server + esbuild watchers run until a signal.
|
|
452
|
+
await new Promise(() => { });
|
|
453
|
+
}
|
|
256
454
|
/**
|
|
257
455
|
* Start esbuild in watch mode over the current example and feed `onBuild` a freshly
|
|
258
456
|
* loaded subgraph on every (re)build. Resolves once the initial build has produced
|
|
259
457
|
* one (so the caller can start serving); later rebuilds fire `onBuild` again.
|
|
260
458
|
*/
|
|
261
459
|
async watchAndBuild(entry, outfile, onBuild) {
|
|
460
|
+
return this.watchEntry(entry, outfile, async (mod) => {
|
|
461
|
+
await onBuild(buildSubgraph(mod));
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* The generic watcher underneath {@link watchAndBuild}: esbuild-watch `entry`, and on
|
|
466
|
+
* every successful (re)build load the bundle and hand the raw module to `onReload`.
|
|
467
|
+
* `--all` uses this directly (it needs the module's resolvers to merge subgraphs);
|
|
468
|
+
* single-mode wraps it to build one subgraph.
|
|
469
|
+
*/
|
|
470
|
+
async watchEntry(entry, outfile, onReload) {
|
|
262
471
|
const reload = async () => {
|
|
263
472
|
const source = await readFile(outfile, "utf8");
|
|
264
|
-
|
|
265
|
-
await onBuild(buildSubgraph(mod));
|
|
473
|
+
await onReload(loadBundleSource(source));
|
|
266
474
|
};
|
|
267
475
|
let firstBuildDone;
|
|
268
476
|
const firstBuild = new Promise((resolve) => {
|