@speedkit/cli 4.24.1 → 4.25.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 +26 -0
- package/README.md +45 -1
- package/dist/commands/revalidate.d.ts +21 -0
- package/dist/commands/revalidate.js +128 -0
- package/dist/services/bundler/bundle-service-factory.d.ts +1 -2
- package/dist/services/bundler/bundle-service-factory.js +2 -4
- package/dist/services/bundler/bundle-service.d.ts +3 -24
- package/dist/services/bundler/bundle-service.js +4 -54
- package/dist/services/bundler/bundle-service.spec.js +23 -89
- package/dist/services/document-handler-runtime/document-handler-server.d.ts +18 -1
- package/dist/services/document-handler-runtime/document-handler-server.js +47 -2
- package/dist/services/document-handler-runtime/factory/document-handler-runtime-service-factory.js +2 -6
- package/dist/services/revalidate/index.d.ts +3 -0
- package/dist/services/revalidate/index.js +3 -0
- package/dist/services/revalidate/revalidate-factory.d.ts +7 -0
- package/dist/services/revalidate/revalidate-factory.js +14 -0
- package/dist/services/revalidate/revalidate-model.d.ts +44 -0
- package/dist/services/revalidate/revalidate-model.js +25 -0
- package/dist/services/revalidate/revalidate-service.d.ts +58 -0
- package/dist/services/revalidate/revalidate-service.js +189 -0
- package/oclif.manifest.json +95 -1
- package/package.json +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
# [4.25.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.24.2...v4.25.0) (2026-09-08)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **revalidate:** accept the 202 the API answers for a submitted job ([a3ba85e](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/a3ba85e454b51740bfa0b983e41f828b8ac86598))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **revalidate:** add sk revalidate for post-go-live purges ([5a08843](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/5a0884344bc0dfd007b8eebdb2e70cd90ae068aa))
|
|
12
|
+
* **revalidate:** confirm a job above 100 assets ([f64158f](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/f64158f56d1ba31b21d44051ecf3f87078aaa8e1))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Performance Improvements
|
|
16
|
+
|
|
17
|
+
* **revalidate:** keep the dry-run prefix filter on the index ([97f057c](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/97f057cfaae8c59b2d888a9fdfee04648c20d046))
|
|
18
|
+
|
|
19
|
+
## [4.24.2](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.24.1...v4.24.2) (2026-09-03)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* **onboarding:** match the document handler's own dependency ranges ([414bd14](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/414bd14584778c0e7b1a3f556c311cd997380df7))
|
|
25
|
+
* **onboarding:** resolve document handler modules at runtime ([44aa76e](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/44aa76ef1b7bf846e47b0be7f6e11b850d7dd0e6))
|
|
26
|
+
|
|
1
27
|
## [4.24.1](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.24.0...v4.24.1) (2026-09-02)
|
|
2
28
|
|
|
3
29
|
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ $ npm install -g @speedkit/cli
|
|
|
21
21
|
$ sk COMMAND
|
|
22
22
|
running command...
|
|
23
23
|
$ sk (--version)
|
|
24
|
-
@speedkit/cli/4.
|
|
24
|
+
@speedkit/cli/4.25.0 linux-x64 node-v22.23.2
|
|
25
25
|
$ sk --help [COMMAND]
|
|
26
26
|
USAGE
|
|
27
27
|
$ sk COMMAND
|
|
@@ -92,6 +92,7 @@ By either...
|
|
|
92
92
|
* [`sk param-diff PAGEURL`](#sk-param-diff-pageurl)
|
|
93
93
|
* [`sk prewarm APP PATH`](#sk-prewarm-app-path)
|
|
94
94
|
* [`sk pull CUSTOMERPATH`](#sk-pull-customerpath)
|
|
95
|
+
* [`sk revalidate APP`](#sk-revalidate-app)
|
|
95
96
|
* [`sk wpt-launcher URLS TESTID`](#sk-wpt-launcher-urls-testid)
|
|
96
97
|
|
|
97
98
|
## `sk auto-prewarm CUSTOMERPATH`
|
|
@@ -746,6 +747,49 @@ EXAMPLES
|
|
|
746
747
|
$ sk pull customers/decathlon.de -c production
|
|
747
748
|
```
|
|
748
749
|
|
|
750
|
+
## `sk revalidate APP`
|
|
751
|
+
|
|
752
|
+
Purge or refresh cached assets by prefix and query. Run it after a config that narrows scope goes live, so returning visitors stop receiving the cached HTML.
|
|
753
|
+
|
|
754
|
+
```
|
|
755
|
+
USAGE
|
|
756
|
+
$ sk revalidate APP [--pattern <value>...] [--prefix <value>...] [--query <value>] [--type
|
|
757
|
+
REFRESH|INSTANT|DEPLOYMENT|PURGE] [-d] [--wait] [--yes] [-q]
|
|
758
|
+
|
|
759
|
+
ARGUMENTS
|
|
760
|
+
APP The customer app name
|
|
761
|
+
|
|
762
|
+
FLAGS
|
|
763
|
+
-d, --dryRun Report the assets the filter selects and exit. Changes nothing.
|
|
764
|
+
-q, --quiet less output non interactive
|
|
765
|
+
--pattern=<value>... URL pattern to purge, as a regex anchored at the scheme — mirror what the config excludes,
|
|
766
|
+
e.g. "^https://www.example.com/en/.*-c1234$". Repeatable. The command derives the prefix the
|
|
767
|
+
API requires from the pattern's literal head, and MongoDB reads that same literal as index
|
|
768
|
+
bounds.
|
|
769
|
+
--prefix=<value>... URL prefix, matched against the start of the cached URL. Derived from --pattern by default;
|
|
770
|
+
state it only to override that, or to purge a whole path without a pattern.
|
|
771
|
+
--query=<value> Raw MongoDB query on the asset fields, as JSON. An escape hatch for a rule --pattern cannot
|
|
772
|
+
express, such as a condition on another field. It replaces the query --pattern would build.
|
|
773
|
+
--type=<option> [default: PURGE] PURGE drops the entries and does not rebuild them, which is what an
|
|
774
|
+
out-of-scope URL needs.
|
|
775
|
+
<options: REFRESH|INSTANT|DEPLOYMENT|PURGE>
|
|
776
|
+
--wait Poll the job until it finishes.
|
|
777
|
+
--yes Skip the confirmation a job above 100 assets asks for. Required in a non-interactive shell.
|
|
778
|
+
|
|
779
|
+
DESCRIPTION
|
|
780
|
+
Purge or refresh cached assets by prefix and query. Run it after a config that narrows scope goes live, so returning
|
|
781
|
+
visitors stop receiving the cached HTML.
|
|
782
|
+
|
|
783
|
+
EXAMPLES
|
|
784
|
+
$ sk revalidate <app> --pattern <regex> [--pattern <regex>] [--dryRun] [--wait]
|
|
785
|
+
|
|
786
|
+
$ sk revalidate decathlon --pattern '^https://www\\.decathlon\\.de/damen/.*-c(?:1234|5678)(?:/|\\?|$)' --dryRun
|
|
787
|
+
|
|
788
|
+
$ sk revalidate decathlon --pattern '^https://www\\.decathlon\\.de/damen/.*-c1234(?:/|\\?|$)' --wait
|
|
789
|
+
|
|
790
|
+
$ sk revalidate decathlon --prefix https://www.decathlon.de/damen/ --dryRun
|
|
791
|
+
```
|
|
792
|
+
|
|
749
793
|
## `sk wpt-launcher URLS TESTID`
|
|
750
794
|
|
|
751
795
|
Analyze the performance impact that Speed Kit has compared to the original version.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from "@oclif/core";
|
|
2
|
+
export default class Revalidate extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static args: {
|
|
5
|
+
app: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static flags: {
|
|
8
|
+
pattern: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
prefix: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
query: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
dryRun: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
wait: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
};
|
|
17
|
+
static examples: string[];
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
/** A malformed query would otherwise reach the backend as an opaque 400. */
|
|
20
|
+
private parseQuery;
|
|
21
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Args, Command, Flags } from "@oclif/core";
|
|
2
|
+
import { CLIParameters, CLIParametersChar, CLIParametersExample, } from "../models/cli-parameters.js";
|
|
3
|
+
import { isNonInteractive } from "../helpers/environment.js";
|
|
4
|
+
import { CONFIRMATION_THRESHOLD, REVALIDATE_TYPES, RevalidateContext, RevalidateFactory, } from "../services/revalidate/index.js";
|
|
5
|
+
const SAMPLE_SIZE = 10;
|
|
6
|
+
export default class Revalidate extends Command {
|
|
7
|
+
static description = "Purge or refresh cached assets by prefix and query. Run it after a config that narrows scope goes live, so returning visitors stop receiving the cached HTML.";
|
|
8
|
+
static args = {
|
|
9
|
+
[CLIParameters.AppName]: Args.string({
|
|
10
|
+
name: CLIParameters.AppName,
|
|
11
|
+
required: true,
|
|
12
|
+
description: "The customer app name",
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
static flags = {
|
|
16
|
+
["pattern"]: Flags.string({
|
|
17
|
+
multiple: true,
|
|
18
|
+
description: 'URL pattern to purge, as a regex anchored at the scheme — mirror what the config excludes, e.g. "^https://www.example.com/en/.*-c1234$". Repeatable. The command derives the prefix the API requires from the pattern\'s literal head, and MongoDB reads that same literal as index bounds.',
|
|
19
|
+
}),
|
|
20
|
+
["prefix"]: Flags.string({
|
|
21
|
+
multiple: true,
|
|
22
|
+
description: "URL prefix, matched against the start of the cached URL. Derived from --pattern by default; state it only to override that, or to purge a whole path without a pattern.",
|
|
23
|
+
}),
|
|
24
|
+
["query"]: Flags.string({
|
|
25
|
+
description: "Raw MongoDB query on the asset fields, as JSON. An escape hatch for a rule --pattern cannot express, such as a condition on another field. It replaces the query --pattern would build.",
|
|
26
|
+
}),
|
|
27
|
+
["type"]: Flags.string({
|
|
28
|
+
options: [...REVALIDATE_TYPES],
|
|
29
|
+
default: "PURGE",
|
|
30
|
+
description: "PURGE drops the entries and does not rebuild them, which is what an out-of-scope URL needs.",
|
|
31
|
+
}),
|
|
32
|
+
[CLIParameters.DryRun]: Flags.boolean({
|
|
33
|
+
char: CLIParametersChar.DryRun,
|
|
34
|
+
default: false,
|
|
35
|
+
description: "Report the assets the filter selects and exit. Changes nothing.",
|
|
36
|
+
}),
|
|
37
|
+
["wait"]: Flags.boolean({
|
|
38
|
+
default: false,
|
|
39
|
+
description: "Poll the job until it finishes.",
|
|
40
|
+
}),
|
|
41
|
+
["yes"]: Flags.boolean({
|
|
42
|
+
default: false,
|
|
43
|
+
description: `Skip the confirmation a job above ${CONFIRMATION_THRESHOLD} assets asks for. Required in a non-interactive shell.`,
|
|
44
|
+
}),
|
|
45
|
+
[CLIParameters.Quiet]: Flags.boolean({
|
|
46
|
+
char: CLIParametersChar.Quiet,
|
|
47
|
+
default: false,
|
|
48
|
+
description: "less output non interactive",
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
static examples = [
|
|
52
|
+
`$ sk revalidate <${CLIParameters.AppName}> --pattern <regex> [--pattern <regex>] [--${CLIParameters.DryRun}] [--wait]`,
|
|
53
|
+
`$ sk revalidate ${CLIParametersExample.AppName} --pattern '^https://www\\\\.decathlon\\\\.de/damen/.*-c(?:1234|5678)(?:/|\\\\?|$)' --${CLIParameters.DryRun}`,
|
|
54
|
+
`$ sk revalidate ${CLIParametersExample.AppName} --pattern '^https://www\\\\.decathlon\\\\.de/damen/.*-c1234(?:/|\\\\?|$)' --wait`,
|
|
55
|
+
`$ sk revalidate ${CLIParametersExample.AppName} --prefix ${CLIParametersExample.PageUrl}damen/ --${CLIParameters.DryRun}`,
|
|
56
|
+
];
|
|
57
|
+
async run() {
|
|
58
|
+
const { args, flags } = await this.parse(Revalidate);
|
|
59
|
+
const app = args[CLIParameters.AppName];
|
|
60
|
+
const service = new RevalidateFactory(new RevalidateContext(app, flags[CLIParameters.Quiet])).getService();
|
|
61
|
+
const patterns = flags["pattern"] ?? [];
|
|
62
|
+
const prefixes = flags["prefix"] ?? [];
|
|
63
|
+
if (!patterns.length && !prefixes.length) {
|
|
64
|
+
this.error("state what to purge: pass --pattern, or --prefix", {
|
|
65
|
+
exit: 1,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const filter = service.buildFilter(patterns, prefixes);
|
|
69
|
+
if (flags["query"]) {
|
|
70
|
+
filter.query = this.parseQuery(flags["query"]);
|
|
71
|
+
}
|
|
72
|
+
service.assertFilter(filter);
|
|
73
|
+
this.log(`prefixes: ${filter.prefixes.join(", ")}`);
|
|
74
|
+
const preview = await service.preview(filter, SAMPLE_SIZE);
|
|
75
|
+
this.log(`filter selects ${preview.count} asset(s)`);
|
|
76
|
+
for (const url of preview.sample) {
|
|
77
|
+
this.log(` ${url}`);
|
|
78
|
+
}
|
|
79
|
+
if (preview.count > preview.sample.length) {
|
|
80
|
+
this.log(` … ${preview.count - preview.sample.length} more`);
|
|
81
|
+
}
|
|
82
|
+
if (flags[CLIParameters.DryRun]) {
|
|
83
|
+
this.log("dry run — nothing was purged");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (preview.count === 0) {
|
|
87
|
+
this.log("nothing to do — the filter selects no asset");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const type = flags["type"];
|
|
91
|
+
if (preview.count > CONFIRMATION_THRESHOLD && !flags["yes"]) {
|
|
92
|
+
if (isNonInteractive()) {
|
|
93
|
+
this.error(`${preview.count} assets exceed the ${CONFIRMATION_THRESHOLD}-asset threshold, and this shell cannot ask`, {
|
|
94
|
+
exit: 1,
|
|
95
|
+
suggestions: [
|
|
96
|
+
"Check the --dryRun output, then re-run with --yes.",
|
|
97
|
+
"Or narrow --pattern until it takes only what the config excludes.",
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (!(await service.confirmLargeJob(preview.count, type))) {
|
|
102
|
+
this.log("aborted — nothing was purged");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const statusId = await service.submit(filter, type);
|
|
107
|
+
this.log(`submitted ${type} job ${statusId}`);
|
|
108
|
+
if (!flags["wait"]) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const status = await service.waitFor(statusId);
|
|
112
|
+
this.log(`assets ${status.assetCount ?? 0}, refreshed ${status.refreshedCount ?? 0}, changed ${status.changedCount ?? 0}`);
|
|
113
|
+
if (status.state !== "FINISHED") {
|
|
114
|
+
this.error(`job ${statusId} ended as ${status.state}: ${status.error}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** A malformed query would otherwise reach the backend as an opaque 400. */
|
|
118
|
+
parseQuery(raw) {
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(raw);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
this.error(`--query is not valid JSON: ${error.message}`, {
|
|
124
|
+
exit: 1,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { BundleService } from "./bundle-service.js";
|
|
2
2
|
export declare class BundleServiceFactory {
|
|
3
3
|
private customerFolder?;
|
|
4
|
-
|
|
5
|
-
constructor(customerFolder?: string, additionalModulePaths?: string[]);
|
|
4
|
+
constructor(customerFolder?: string);
|
|
6
5
|
buildService(): BundleService;
|
|
7
6
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { BundleService } from "./bundle-service.js";
|
|
2
2
|
export class BundleServiceFactory {
|
|
3
3
|
customerFolder;
|
|
4
|
-
|
|
5
|
-
constructor(customerFolder, additionalModulePaths = []) {
|
|
4
|
+
constructor(customerFolder) {
|
|
6
5
|
this.customerFolder = customerFolder;
|
|
7
|
-
this.additionalModulePaths = additionalModulePaths;
|
|
8
6
|
}
|
|
9
7
|
buildService() {
|
|
10
|
-
return new BundleService(this.customerFolder
|
|
8
|
+
return new BundleService(this.customerFolder);
|
|
11
9
|
}
|
|
12
10
|
}
|
|
@@ -8,15 +8,11 @@ export type BundleBasePaths = Record<string, string>;
|
|
|
8
8
|
export type BundleResolveFallback = (specifier: string) => Promise<string | null>;
|
|
9
9
|
export declare class BundleService {
|
|
10
10
|
private moduleDiscoveryDir;
|
|
11
|
-
private additionalModulePaths;
|
|
12
11
|
/**
|
|
13
12
|
* @param moduleDiscoveryDir the directory bundled code resolves its own imports from
|
|
14
|
-
* @param additionalModulePaths extra `node_modules` directories to search, like `NODE_PATH`.
|
|
15
|
-
* `sk` installs the packages a customer config declares under `dependencies` into its cache
|
|
16
|
-
* rather than into the customer folder, so esbuild would otherwise never find them.
|
|
17
13
|
*/
|
|
18
|
-
constructor(moduleDiscoveryDir: string
|
|
19
|
-
bundle({ entryPoints, basePaths, define, target, minify, logLevel, alias, platform, resolveFallback, }: {
|
|
14
|
+
constructor(moduleDiscoveryDir: string);
|
|
15
|
+
bundle({ entryPoints, basePaths, define, target, minify, logLevel, alias, platform, resolveFallback, packages, }: {
|
|
20
16
|
entryPoints: BundleEntryPoints;
|
|
21
17
|
basePaths?: BundleBasePaths;
|
|
22
18
|
target?: string | string[];
|
|
@@ -26,25 +22,8 @@ export declare class BundleService {
|
|
|
26
22
|
logLevel?: LogLevel;
|
|
27
23
|
platform?: "browser" | "node" | "neutral";
|
|
28
24
|
resolveFallback?: BundleResolveFallback;
|
|
25
|
+
packages?: "bundle" | "external";
|
|
29
26
|
}): Promise<string>;
|
|
30
|
-
/**
|
|
31
|
-
* Turns each alias target into an absolute path, resolved from the same directory the bundled
|
|
32
|
-
* code resolves its own imports from.
|
|
33
|
-
*
|
|
34
|
-
* esbuild resolves an alias target from its working directory, not from the entry point's
|
|
35
|
-
* resolveDir. `{ "node-fetch": "node-fetch-native" }` therefore only resolved while `sk` ran
|
|
36
|
-
* inside the customer checkout, and failed from anywhere else — even though the bundled code
|
|
37
|
-
* could import the very same module by name.
|
|
38
|
-
*
|
|
39
|
-
* A target that is not installed stays a bare name, so esbuild reports it as before.
|
|
40
|
-
*/
|
|
41
|
-
private resolveAliases;
|
|
42
|
-
/**
|
|
43
|
-
* Looks the target up the way the bundled code would: the customer folder first, then the
|
|
44
|
-
* additional module paths. Returns `null` when it is installed nowhere, so the bare name reaches
|
|
45
|
-
* esbuild and is reported as before.
|
|
46
|
-
*/
|
|
47
|
-
private resolveAliasTarget;
|
|
48
27
|
private entryPointsPlugin;
|
|
49
28
|
baqendPlugin(basePaths: BundleBasePaths): {
|
|
50
29
|
name: string;
|
|
@@ -4,24 +4,18 @@ import { URL } from "node:url";
|
|
|
4
4
|
import { BundleResolveError } from "./error/bundle-resolve-error.js";
|
|
5
5
|
import { BundleFetchError } from "./error/bundle-fetch-error.js";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
|
-
import { createRequire } from "node:module";
|
|
8
7
|
const FALLBACK_NAMESPACE = "bundle-resolve-fallback";
|
|
9
8
|
const RELATIVE_SPECIFIER = /^\.{1,2}\//;
|
|
10
9
|
const importCache = {};
|
|
11
10
|
export class BundleService {
|
|
12
11
|
moduleDiscoveryDir;
|
|
13
|
-
additionalModulePaths;
|
|
14
12
|
/**
|
|
15
13
|
* @param moduleDiscoveryDir the directory bundled code resolves its own imports from
|
|
16
|
-
* @param additionalModulePaths extra `node_modules` directories to search, like `NODE_PATH`.
|
|
17
|
-
* `sk` installs the packages a customer config declares under `dependencies` into its cache
|
|
18
|
-
* rather than into the customer folder, so esbuild would otherwise never find them.
|
|
19
14
|
*/
|
|
20
|
-
constructor(moduleDiscoveryDir
|
|
15
|
+
constructor(moduleDiscoveryDir) {
|
|
21
16
|
this.moduleDiscoveryDir = moduleDiscoveryDir;
|
|
22
|
-
this.additionalModulePaths = additionalModulePaths;
|
|
23
17
|
}
|
|
24
|
-
async bundle({ entryPoints, basePaths, define, target = "es5", minify = true, logLevel = "silent", alias = {}, platform, resolveFallback, }) {
|
|
18
|
+
async bundle({ entryPoints, basePaths, define, target = "es5", minify = true, logLevel = "silent", alias = {}, platform, resolveFallback, packages = "bundle", }) {
|
|
25
19
|
const plugins = [];
|
|
26
20
|
if (basePaths && Object.keys(basePaths).length > 0) {
|
|
27
21
|
plugins.push(this.baqendPlugin(basePaths));
|
|
@@ -43,58 +37,14 @@ export class BundleService {
|
|
|
43
37
|
entryPoints: Object.keys(entryPoints),
|
|
44
38
|
platform: platform || "neutral",
|
|
45
39
|
logLevel,
|
|
46
|
-
|
|
47
|
-
alias
|
|
40
|
+
packages,
|
|
41
|
+
alias,
|
|
48
42
|
});
|
|
49
43
|
return result.outputFiles
|
|
50
44
|
.map((outputFile) => outputFile.text)
|
|
51
45
|
.join("")
|
|
52
46
|
.trim();
|
|
53
47
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Turns each alias target into an absolute path, resolved from the same directory the bundled
|
|
56
|
-
* code resolves its own imports from.
|
|
57
|
-
*
|
|
58
|
-
* esbuild resolves an alias target from its working directory, not from the entry point's
|
|
59
|
-
* resolveDir. `{ "node-fetch": "node-fetch-native" }` therefore only resolved while `sk` ran
|
|
60
|
-
* inside the customer checkout, and failed from anywhere else — even though the bundled code
|
|
61
|
-
* could import the very same module by name.
|
|
62
|
-
*
|
|
63
|
-
* A target that is not installed stays a bare name, so esbuild reports it as before.
|
|
64
|
-
*/
|
|
65
|
-
resolveAliases(alias) {
|
|
66
|
-
if (Object.keys(alias).length === 0) {
|
|
67
|
-
return alias;
|
|
68
|
-
}
|
|
69
|
-
// The filename never has to exist; only its directory decides where the lookup starts.
|
|
70
|
-
const requireFrom = createRequire(resolve(this.moduleDiscoveryDir, "noop.js"));
|
|
71
|
-
return Object.fromEntries(Object.entries(alias).map(([from, to]) => [
|
|
72
|
-
from,
|
|
73
|
-
this.resolveAliasTarget(requireFrom, to) ?? to,
|
|
74
|
-
]));
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Looks the target up the way the bundled code would: the customer folder first, then the
|
|
78
|
-
* additional module paths. Returns `null` when it is installed nowhere, so the bare name reaches
|
|
79
|
-
* esbuild and is reported as before.
|
|
80
|
-
*/
|
|
81
|
-
resolveAliasTarget(requireFrom, target) {
|
|
82
|
-
try {
|
|
83
|
-
return requireFrom.resolve(target);
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
// not in the customer folder — fall through to the additional module paths
|
|
87
|
-
}
|
|
88
|
-
for (const modulePath of this.additionalModulePaths) {
|
|
89
|
-
try {
|
|
90
|
-
return createRequire(resolve(modulePath, "noop.js")).resolve(target);
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
// keep looking
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
48
|
entryPointsPlugin(entryPoints) {
|
|
99
49
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
100
50
|
const me = this;
|
|
@@ -1,62 +1,9 @@
|
|
|
1
1
|
import { expect } from "chai";
|
|
2
2
|
import { after, before, describe, it } from "mocha";
|
|
3
|
-
import {
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { BundleService } from "./bundle-service.js";
|
|
7
|
-
/**
|
|
8
|
-
* The customer checkout installs the modules a document handler imports, so resolution must follow
|
|
9
|
-
* the customer directory rather than the directory `sk` happens to run in. The fixture below is
|
|
10
|
-
* only reachable from the customer directory, which is what makes the assertion meaningful.
|
|
11
|
-
*/
|
|
12
|
-
describe("BundleService alias resolution", () => {
|
|
13
|
-
const MARKER = "fixture-fetch-implementation";
|
|
14
|
-
let customerDirectory;
|
|
15
|
-
before(() => {
|
|
16
|
-
customerDirectory = mkdtempSync(join(tmpdir(), "sk-bundle-"));
|
|
17
|
-
const packageDirectory = join(customerDirectory, "node_modules", "fetch-native-fixture");
|
|
18
|
-
mkdirSync(packageDirectory, { recursive: true });
|
|
19
|
-
writeFileSync(join(packageDirectory, "package.json"), JSON.stringify({
|
|
20
|
-
name: "fetch-native-fixture",
|
|
21
|
-
version: "1.0.0",
|
|
22
|
-
main: "index.js",
|
|
23
|
-
}));
|
|
24
|
-
writeFileSync(join(packageDirectory, "index.js"), `module.exports = "${MARKER}";`);
|
|
25
|
-
});
|
|
26
|
-
after(() => {
|
|
27
|
-
rmSync(customerDirectory, { recursive: true, force: true });
|
|
28
|
-
});
|
|
29
|
-
it("resolves an alias target from the customer directory, not from the working directory", async () => {
|
|
30
|
-
const bundle = await new BundleService(customerDirectory).bundle({
|
|
31
|
-
entryPoints: {
|
|
32
|
-
"handler.js": "module.exports = require('node-fetch');",
|
|
33
|
-
},
|
|
34
|
-
minify: false,
|
|
35
|
-
platform: "node",
|
|
36
|
-
logLevel: "silent",
|
|
37
|
-
alias: { "node-fetch": "fetch-native-fixture" },
|
|
38
|
-
});
|
|
39
|
-
expect(bundle).to.contain(MARKER);
|
|
40
|
-
});
|
|
41
|
-
it("leaves an uninstalled alias target to esbuild", async () => {
|
|
42
|
-
const bundling = new BundleService(customerDirectory).bundle({
|
|
43
|
-
entryPoints: {
|
|
44
|
-
"handler.js": "module.exports = require('node-fetch');",
|
|
45
|
-
},
|
|
46
|
-
minify: false,
|
|
47
|
-
platform: "node",
|
|
48
|
-
logLevel: "silent",
|
|
49
|
-
alias: { "node-fetch": "not-installed-anywhere" },
|
|
50
|
-
});
|
|
51
|
-
const error = await bundling.then(() => undefined, (reason) => reason);
|
|
52
|
-
expect(String(error)).to.contain("not-installed-anywhere");
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
/**
|
|
56
|
-
* A document handler is shared with the Baqend app it is deployed to, so it can require a module
|
|
57
|
-
* that only exists there. The bundler must reach for that copy only after the customer folder has
|
|
58
|
-
* had its chance, and only for the relative specifiers Baqend modules are required by.
|
|
59
|
-
*/
|
|
60
7
|
describe("BundleService resolve fallback", () => {
|
|
61
8
|
const bundleWithFallback = (source, fallback, moduleDiscoveryDir = process.cwd()) => new BundleService(moduleDiscoveryDir).bundle({
|
|
62
9
|
entryPoints: { "handler.js": source },
|
|
@@ -111,68 +58,55 @@ describe("BundleService resolve fallback", () => {
|
|
|
111
58
|
});
|
|
112
59
|
});
|
|
113
60
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
61
|
+
* A document handler imports packages that must not be bundled — a native addon locates its own
|
|
62
|
+
* binary through `import.meta.url`, which an esbuild CJS bundle leaves undefined. Only bare
|
|
63
|
+
* specifiers become runtime requires; the customer's own files still have to be bundled.
|
|
116
64
|
*/
|
|
117
|
-
describe("BundleService
|
|
118
|
-
const MARKER = "fixture-
|
|
119
|
-
let cacheDirectory;
|
|
65
|
+
describe("BundleService external packages", () => {
|
|
66
|
+
const MARKER = "fixture-relative-module";
|
|
120
67
|
let customerDirectory;
|
|
121
|
-
let additionalModulePath;
|
|
122
68
|
before(() => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
additionalModulePath = join(cacheDirectory, "node_modules");
|
|
126
|
-
const packageDirectory = join(additionalModulePath, "additional-fixture");
|
|
127
|
-
mkdirSync(packageDirectory, { recursive: true });
|
|
128
|
-
writeFileSync(join(packageDirectory, "package.json"), JSON.stringify({
|
|
129
|
-
name: "additional-fixture",
|
|
130
|
-
version: "1.0.0",
|
|
131
|
-
main: "index.js",
|
|
132
|
-
}));
|
|
133
|
-
writeFileSync(join(packageDirectory, "index.js"), `module.exports = "${MARKER}";`);
|
|
69
|
+
customerDirectory = mkdtempSync(join(tmpdir(), "sk-external-"));
|
|
70
|
+
writeFileSync(join(customerDirectory, "helper.js"), `module.exports = "${MARKER}";`);
|
|
134
71
|
});
|
|
135
72
|
after(() => {
|
|
136
|
-
rmSync(cacheDirectory, { recursive: true, force: true });
|
|
137
73
|
rmSync(customerDirectory, { recursive: true, force: true });
|
|
138
74
|
});
|
|
139
|
-
it("
|
|
140
|
-
const bundle = await new BundleService(customerDirectory
|
|
141
|
-
additionalModulePath,
|
|
142
|
-
]).bundle({
|
|
75
|
+
it("leaves a package as a runtime require", async () => {
|
|
76
|
+
const bundle = await new BundleService(customerDirectory).bundle({
|
|
143
77
|
entryPoints: {
|
|
144
|
-
"handler.js": "module.exports = require('
|
|
78
|
+
"handler.js": "module.exports = require('parse5');",
|
|
145
79
|
},
|
|
146
80
|
minify: false,
|
|
147
81
|
platform: "node",
|
|
148
82
|
logLevel: "silent",
|
|
83
|
+
packages: "external",
|
|
149
84
|
});
|
|
150
|
-
expect(bundle).to.contain(
|
|
85
|
+
expect(bundle).to.contain('require("parse5")');
|
|
151
86
|
});
|
|
152
|
-
it("
|
|
153
|
-
const
|
|
87
|
+
it("still bundles a relative import", async () => {
|
|
88
|
+
const bundle = await new BundleService(customerDirectory).bundle({
|
|
154
89
|
entryPoints: {
|
|
155
|
-
"handler.js": "module.exports = require('
|
|
90
|
+
"handler.js": "module.exports = require('./helper.js');",
|
|
156
91
|
},
|
|
157
92
|
minify: false,
|
|
158
93
|
platform: "node",
|
|
159
94
|
logLevel: "silent",
|
|
95
|
+
packages: "external",
|
|
160
96
|
});
|
|
161
|
-
|
|
162
|
-
expect(String(error)).to.contain("additional-fixture");
|
|
97
|
+
expect(bundle).to.contain(MARKER);
|
|
163
98
|
});
|
|
164
|
-
it("
|
|
165
|
-
const bundle = await new BundleService(customerDirectory
|
|
166
|
-
additionalModulePath,
|
|
167
|
-
]).bundle({
|
|
99
|
+
it("applies an alias and keeps the target a bare specifier", async () => {
|
|
100
|
+
const bundle = await new BundleService(customerDirectory).bundle({
|
|
168
101
|
entryPoints: {
|
|
169
102
|
"handler.js": "module.exports = require('node-fetch');",
|
|
170
103
|
},
|
|
171
104
|
minify: false,
|
|
172
105
|
platform: "node",
|
|
173
106
|
logLevel: "silent",
|
|
174
|
-
alias: { "node-fetch": "
|
|
107
|
+
alias: { "node-fetch": "node-fetch-native" },
|
|
108
|
+
packages: "external",
|
|
175
109
|
});
|
|
176
|
-
expect(bundle).to.contain(
|
|
110
|
+
expect(bundle).to.contain('require("node-fetch-native")');
|
|
177
111
|
});
|
|
178
112
|
});
|
|
@@ -8,6 +8,7 @@ export declare class DocumentHandlerServer {
|
|
|
8
8
|
private files;
|
|
9
9
|
private bundler;
|
|
10
10
|
private cli;
|
|
11
|
+
private workSpace;
|
|
11
12
|
private verboseLevel;
|
|
12
13
|
private entityManager?;
|
|
13
14
|
private documentHandlerCode;
|
|
@@ -15,7 +16,11 @@ export declare class DocumentHandlerServer {
|
|
|
15
16
|
private database;
|
|
16
17
|
/** Sources of app modules already looked up; `null` marks a lookup that came back empty. */
|
|
17
18
|
private readonly appModules;
|
|
18
|
-
|
|
19
|
+
private moduleResolver;
|
|
20
|
+
/**
|
|
21
|
+
* @param workSpace - Directory the packages a customer config declares are installed into.
|
|
22
|
+
*/
|
|
23
|
+
constructor(customerConfig: CustomerConfig, files: FileListInterface, bundler: BundleService, cli: CliService, workSpace: string, verboseLevel?: boolean, entityManager?: EntityManager);
|
|
19
24
|
transform(contentType: string, html: string, variation: string, url: string, headers: Record<string, string>): Promise<DocumentHandlerResponse>;
|
|
20
25
|
private getTransformFunctionWrapper;
|
|
21
26
|
buildDocumentHandler(): Promise<void>;
|
|
@@ -43,6 +48,18 @@ export declare class DocumentHandlerServer {
|
|
|
43
48
|
*/
|
|
44
49
|
bundleDynamicFetcher(basePaths: Record<string, string>, dynamicFetcher: string): Promise<string>;
|
|
45
50
|
bundleDocumentHandlerCode(name: string, code: string): Promise<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Resolves the packages the bundle leaves as runtime requires.
|
|
53
|
+
*
|
|
54
|
+
* Nothing a document handler imports by name is bundled, because a bundler cannot serve them
|
|
55
|
+
* all: a native addon such as `node-av` locates its own `.node` file through `import.meta.url`,
|
|
56
|
+
* which an esbuild CJS bundle leaves undefined. Requiring at runtime also matches production,
|
|
57
|
+
* where the handler runs against real `node_modules`.
|
|
58
|
+
*
|
|
59
|
+
* The workspace is searched before the CLI's own dependencies, so a config that declares a
|
|
60
|
+
* package under `dependencies` also decides its version.
|
|
61
|
+
*/
|
|
62
|
+
private getModuleResolver;
|
|
46
63
|
/**
|
|
47
64
|
* Loads a module the customer folder does not contain from the app the config is deployed to.
|
|
48
65
|
*
|
|
@@ -12,12 +12,14 @@ import { safe } from "../../helpers/safe.js";
|
|
|
12
12
|
import { VmEmptyResponseError } from "../onboarding/error/vm-empty-response-error.js";
|
|
13
13
|
import { DocumentHandlerTransformError } from "../onboarding/error/document-handler-transform-error.js";
|
|
14
14
|
import { createRequire } from "node:module";
|
|
15
|
+
import { resolve } from "node:path";
|
|
15
16
|
import ApplicationError from "../error-handling/error/application-error.js";
|
|
16
17
|
export class DocumentHandlerServer {
|
|
17
18
|
customerConfig;
|
|
18
19
|
files;
|
|
19
20
|
bundler;
|
|
20
21
|
cli;
|
|
22
|
+
workSpace;
|
|
21
23
|
verboseLevel;
|
|
22
24
|
entityManager;
|
|
23
25
|
documentHandlerCode;
|
|
@@ -25,11 +27,16 @@ export class DocumentHandlerServer {
|
|
|
25
27
|
database;
|
|
26
28
|
/** Sources of app modules already looked up; `null` marks a lookup that came back empty. */
|
|
27
29
|
appModules = new Map();
|
|
28
|
-
|
|
30
|
+
moduleResolver;
|
|
31
|
+
/**
|
|
32
|
+
* @param workSpace - Directory the packages a customer config declares are installed into.
|
|
33
|
+
*/
|
|
34
|
+
constructor(customerConfig, files, bundler, cli, workSpace, verboseLevel = false, entityManager) {
|
|
29
35
|
this.customerConfig = customerConfig;
|
|
30
36
|
this.files = files;
|
|
31
37
|
this.bundler = bundler;
|
|
32
38
|
this.cli = cli;
|
|
39
|
+
this.workSpace = workSpace;
|
|
33
40
|
this.verboseLevel = verboseLevel;
|
|
34
41
|
this.entityManager = entityManager;
|
|
35
42
|
}
|
|
@@ -172,7 +179,7 @@ export class DocumentHandlerServer {
|
|
|
172
179
|
createNodeVmContext() {
|
|
173
180
|
const context = {
|
|
174
181
|
process: this.createProcessPartial(),
|
|
175
|
-
require:
|
|
182
|
+
require: this.getModuleResolver(),
|
|
176
183
|
URL,
|
|
177
184
|
URLSearchParams,
|
|
178
185
|
fetch: this.createFetchMock(),
|
|
@@ -248,6 +255,8 @@ export class DocumentHandlerServer {
|
|
|
248
255
|
platform: "node",
|
|
249
256
|
logLevel: "silent",
|
|
250
257
|
alias: { "node-fetch": "node-fetch-native" },
|
|
258
|
+
// Every package stays a runtime require, see getModuleResolver().
|
|
259
|
+
packages: "external",
|
|
251
260
|
resolveFallback: this.loadAppModule,
|
|
252
261
|
}));
|
|
253
262
|
if (bundling.success === true) {
|
|
@@ -255,6 +264,42 @@ export class DocumentHandlerServer {
|
|
|
255
264
|
}
|
|
256
265
|
throw this.explainUnresolvedModules(bundling.errorObj ?? new Error(bundling.error));
|
|
257
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Resolves the packages the bundle leaves as runtime requires.
|
|
269
|
+
*
|
|
270
|
+
* Nothing a document handler imports by name is bundled, because a bundler cannot serve them
|
|
271
|
+
* all: a native addon such as `node-av` locates its own `.node` file through `import.meta.url`,
|
|
272
|
+
* which an esbuild CJS bundle leaves undefined. Requiring at runtime also matches production,
|
|
273
|
+
* where the handler runs against real `node_modules`.
|
|
274
|
+
*
|
|
275
|
+
* The workspace is searched before the CLI's own dependencies, so a config that declares a
|
|
276
|
+
* package under `dependencies` also decides its version.
|
|
277
|
+
*/
|
|
278
|
+
getModuleResolver() {
|
|
279
|
+
if (this.moduleResolver) {
|
|
280
|
+
return this.moduleResolver;
|
|
281
|
+
}
|
|
282
|
+
// The filename never has to exist; only its directory decides where the lookup starts.
|
|
283
|
+
const requireFromWorkSpace = createRequire(resolve(this.workSpace, "noop.js"));
|
|
284
|
+
const requireFromCli = createRequire(import.meta.url);
|
|
285
|
+
// Resolve before requiring, so a module that exists but throws while it initialises
|
|
286
|
+
// reports that failure rather than falling through to the next candidate.
|
|
287
|
+
this.moduleResolver = (module) => {
|
|
288
|
+
const inWorkSpace = safe(() => requireFromWorkSpace.resolve(module));
|
|
289
|
+
if (inWorkSpace.success === true) {
|
|
290
|
+
return requireFromWorkSpace(inWorkSpace.data);
|
|
291
|
+
}
|
|
292
|
+
const withCli = safe(() => requireFromCli.resolve(module));
|
|
293
|
+
if (withCli.success === true) {
|
|
294
|
+
return requireFromCli(withCli.data);
|
|
295
|
+
}
|
|
296
|
+
throw new ApplicationError(`[documentHandler] could not load module: ${module}`, [
|
|
297
|
+
`"${module}" ships neither with the CLI nor with this customer config.`,
|
|
298
|
+
`Add it to config_customer.json under "dependencies" and start sk again.`,
|
|
299
|
+
]);
|
|
300
|
+
};
|
|
301
|
+
return this.moduleResolver;
|
|
302
|
+
}
|
|
258
303
|
/**
|
|
259
304
|
* Loads a module the customer folder does not contain from the app the config is deployed to.
|
|
260
305
|
*
|
package/dist/services/document-handler-runtime/factory/document-handler-runtime-service-factory.js
CHANGED
|
@@ -36,13 +36,9 @@ export default class DocumentHandlerRuntimeServiceFactory {
|
|
|
36
36
|
const customerConfig = this.getCustomerConfig(files);
|
|
37
37
|
const cliService = new CliServiceFactory().getService();
|
|
38
38
|
await this.installAdditionalDependencies(customerConfig, cliService);
|
|
39
|
-
|
|
40
|
-
// the bundler has to search there too or a declared dependency resolves nowhere.
|
|
41
|
-
const bundler = new BundleServiceFactory(this.context.customerPath, [
|
|
42
|
-
resolve(this.cliConfig.nodeModulesPath, "node_modules"),
|
|
43
|
-
]).buildService();
|
|
39
|
+
const bundler = new BundleServiceFactory(this.context.customerPath).buildService();
|
|
44
40
|
const entityManagerResult = await safe(new EntityManagerFactory().getEntityManager(customerConfig.app));
|
|
45
|
-
return new DocumentHandlerServer(customerConfig, files, bundler, cliService, this.context.verboseLevel, entityManagerResult.success ? entityManagerResult.data : null);
|
|
41
|
+
return new DocumentHandlerServer(customerConfig, files, bundler, cliService, this.cliConfig.nodeModulesPath, this.context.verboseLevel, entityManagerResult.success ? entityManagerResult.data : null);
|
|
46
42
|
}
|
|
47
43
|
prepareIntegrationApi() {
|
|
48
44
|
const integrationApiContext = new IntegrationApiContext(this.context.customerPath, this.context.configName, this.context.fileDependencies, [".git", ".idea", "node_modules"]);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { RevalidateContextInterface } from "./revalidate-model.js";
|
|
2
|
+
import { RevalidateService } from "./revalidate-service.js";
|
|
3
|
+
export declare class RevalidateFactory {
|
|
4
|
+
private context;
|
|
5
|
+
constructor(context: RevalidateContextInterface);
|
|
6
|
+
getService(): RevalidateService;
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CliContext, CliServiceFactory } from "../cli/index.js";
|
|
2
|
+
import { ConfigApiContext, ConfigApiServiceFactory, } from "../config-api/index.js";
|
|
3
|
+
import { RevalidateService } from "./revalidate-service.js";
|
|
4
|
+
export class RevalidateFactory {
|
|
5
|
+
context;
|
|
6
|
+
constructor(context) {
|
|
7
|
+
this.context = context;
|
|
8
|
+
}
|
|
9
|
+
getService() {
|
|
10
|
+
const cli = new CliServiceFactory(new CliContext(this.context.quiet)).getService();
|
|
11
|
+
const configApi = new ConfigApiServiceFactory(new ConfigApiContext(this.context.app)).getService();
|
|
12
|
+
return new RevalidateService(configApi, cli, this.context.app);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export declare const REVALIDATE_TYPES: readonly ["REFRESH", "INSTANT", "DEPLOYMENT", "PURGE"];
|
|
2
|
+
export type RevalidateType = (typeof REVALIDATE_TYPES)[number];
|
|
3
|
+
/**
|
|
4
|
+
* The asset table the dry run reads. It holds one entry per cached asset, so the
|
|
5
|
+
* same filter that the job applies also selects the rows to report.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ASSET_TABLE = "speedKit.Asset";
|
|
8
|
+
/**
|
|
9
|
+
* Above this many assets the command asks before it purges. A filter that takes
|
|
10
|
+
* more than a page of assets is usually a filter mistake, and a purge cannot be
|
|
11
|
+
* undone — the entries are gone until traffic caches them again.
|
|
12
|
+
*/
|
|
13
|
+
export declare const CONFIRMATION_THRESHOLD = 100;
|
|
14
|
+
/**
|
|
15
|
+
* Scope of a revalidation job.
|
|
16
|
+
*
|
|
17
|
+
* `prefixes` is mandatory: the backend rejects a filter without it. It bounds which
|
|
18
|
+
* URLs the job examines. `query` refines that set, and it takes a MongoDB query, so
|
|
19
|
+
* it can express a rule that a prefix cannot — a trailing category id, for example.
|
|
20
|
+
*/
|
|
21
|
+
export interface RevalidateFilterInterface {
|
|
22
|
+
prefixes: string[];
|
|
23
|
+
query?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface RevalidateJobStatusInterface {
|
|
26
|
+
state: "CREATED" | "RUNNING" | "FINISHED" | "ERROR" | "CANCELLED" | "SCHEDULED" | "DEFERRED";
|
|
27
|
+
assetCount?: number;
|
|
28
|
+
refreshedCount?: number;
|
|
29
|
+
changedCount?: number;
|
|
30
|
+
error?: string | null;
|
|
31
|
+
}
|
|
32
|
+
export interface RevalidatePreviewInterface {
|
|
33
|
+
count: number;
|
|
34
|
+
sample: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface RevalidateContextInterface {
|
|
37
|
+
app: string;
|
|
38
|
+
quiet: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare class RevalidateContext implements RevalidateContextInterface {
|
|
41
|
+
readonly app: string;
|
|
42
|
+
readonly quiet: boolean;
|
|
43
|
+
constructor(app: string, quiet?: boolean);
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const REVALIDATE_TYPES = [
|
|
2
|
+
"REFRESH",
|
|
3
|
+
"INSTANT",
|
|
4
|
+
"DEPLOYMENT",
|
|
5
|
+
"PURGE",
|
|
6
|
+
];
|
|
7
|
+
/**
|
|
8
|
+
* The asset table the dry run reads. It holds one entry per cached asset, so the
|
|
9
|
+
* same filter that the job applies also selects the rows to report.
|
|
10
|
+
*/
|
|
11
|
+
export const ASSET_TABLE = "speedKit.Asset";
|
|
12
|
+
/**
|
|
13
|
+
* Above this many assets the command asks before it purges. A filter that takes
|
|
14
|
+
* more than a page of assets is usually a filter mistake, and a purge cannot be
|
|
15
|
+
* undone — the entries are gone until traffic caches them again.
|
|
16
|
+
*/
|
|
17
|
+
export const CONFIRMATION_THRESHOLD = 100;
|
|
18
|
+
export class RevalidateContext {
|
|
19
|
+
app;
|
|
20
|
+
quiet;
|
|
21
|
+
constructor(app, quiet = false) {
|
|
22
|
+
this.app = app;
|
|
23
|
+
this.quiet = quiet;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { CliService } from "../cli/index.js";
|
|
2
|
+
import { ConfigApiService } from "../config-api/index.js";
|
|
3
|
+
import { RevalidateFilterInterface, RevalidateJobStatusInterface, RevalidatePreviewInterface, RevalidateType } from "./revalidate-model.js";
|
|
4
|
+
/**
|
|
5
|
+
* Reads the literal head of an anchored pattern, which is the prefix the job needs.
|
|
6
|
+
*
|
|
7
|
+
* This is the same text MongoDB turns into index bounds, so deriving the prefix
|
|
8
|
+
* here keeps the two in step: the caller states the pattern once, and the prefix
|
|
9
|
+
* cannot drift from it. An escaped character counts as literal, so `\.` yields `.`
|
|
10
|
+
* and the host survives. The walk stops at the first metacharacter.
|
|
11
|
+
*/
|
|
12
|
+
export declare function derivePrefix(pattern: string): string;
|
|
13
|
+
export declare class RevalidateService {
|
|
14
|
+
private readonly configApi;
|
|
15
|
+
private readonly cli;
|
|
16
|
+
private readonly app;
|
|
17
|
+
constructor(configApi: ConfigApiService, cli: CliService, app: string);
|
|
18
|
+
/**
|
|
19
|
+
* Builds the filter the API expects from the patterns the caller stated, and
|
|
20
|
+
* derives the mandatory prefixes from them.
|
|
21
|
+
*/
|
|
22
|
+
buildFilter(patterns: string[], explicitPrefixes: string[]): RevalidateFilterInterface;
|
|
23
|
+
/**
|
|
24
|
+
* Rejects a filter the backend would reject, before the request goes out. The
|
|
25
|
+
* check is cheap here and the server-side error does not say which field is at
|
|
26
|
+
* fault.
|
|
27
|
+
*/
|
|
28
|
+
assertFilter(filter: RevalidateFilterInterface): void;
|
|
29
|
+
/**
|
|
30
|
+
* Reports what the job would take. The Refresh API has no preview mode, so this
|
|
31
|
+
* runs the job's own filter against the asset table instead.
|
|
32
|
+
*/
|
|
33
|
+
preview(filter: RevalidateFilterInterface, sampleSize: number): Promise<RevalidatePreviewInterface>;
|
|
34
|
+
/** Asks before a purge that takes more assets than the threshold allows. */
|
|
35
|
+
confirmLargeJob(count: number, type: RevalidateType): Promise<boolean>;
|
|
36
|
+
/** Submits the job and returns its status id. */
|
|
37
|
+
submit(filter: RevalidateFilterInterface, type: RevalidateType): Promise<string>;
|
|
38
|
+
status(statusId: string): Promise<RevalidateJobStatusInterface>;
|
|
39
|
+
/**
|
|
40
|
+
* Issues the request itself instead of going through the shared client. A
|
|
41
|
+
* submitted job answers 202, and the shared client reports every status other
|
|
42
|
+
* than 200 as an error — which turns an accepted job into "Error: Accepted".
|
|
43
|
+
*/
|
|
44
|
+
private request;
|
|
45
|
+
/** Polls until the job reaches a terminal state, and reports each state change. */
|
|
46
|
+
waitFor(statusId: string): Promise<RevalidateJobStatusInterface>;
|
|
47
|
+
/**
|
|
48
|
+
* Mirrors the job's filter as one MongoDB query, so the dry run reports the set
|
|
49
|
+
* the job takes and not every URL the query alone would match.
|
|
50
|
+
*
|
|
51
|
+
* Each prefix becomes its own anchored expression, and they combine with `$or`.
|
|
52
|
+
* One alternation would read `^(?:a|b)`, whose first character after the anchor
|
|
53
|
+
* is a metacharacter — MongoDB then extracts no literal prefix and scans the
|
|
54
|
+
* collection. Some asset tables are far too large for that. `^literal` keeps
|
|
55
|
+
* each branch on the index.
|
|
56
|
+
*/
|
|
57
|
+
private toMongoFilter;
|
|
58
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import ApplicationError from "../error-handling/error/application-error.js";
|
|
2
|
+
import { ASSET_TABLE, } from "./revalidate-model.js";
|
|
3
|
+
const POLL_INTERVAL = 3000;
|
|
4
|
+
const TERMINAL_STATES = new Set(["FINISHED", "ERROR", "CANCELLED"]);
|
|
5
|
+
/** Escapes a prefix so it can go into the dry-run query as a literal. */
|
|
6
|
+
function escapeRegExp(value) {
|
|
7
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8
|
+
}
|
|
9
|
+
const REGEXP_METACHARACTERS = new Set([
|
|
10
|
+
".",
|
|
11
|
+
"*",
|
|
12
|
+
"+",
|
|
13
|
+
"?",
|
|
14
|
+
"$",
|
|
15
|
+
"{",
|
|
16
|
+
"}",
|
|
17
|
+
"(",
|
|
18
|
+
")",
|
|
19
|
+
"|",
|
|
20
|
+
"[",
|
|
21
|
+
"]",
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* Reads the literal head of an anchored pattern, which is the prefix the job needs.
|
|
25
|
+
*
|
|
26
|
+
* This is the same text MongoDB turns into index bounds, so deriving the prefix
|
|
27
|
+
* here keeps the two in step: the caller states the pattern once, and the prefix
|
|
28
|
+
* cannot drift from it. An escaped character counts as literal, so `\.` yields `.`
|
|
29
|
+
* and the host survives. The walk stops at the first metacharacter.
|
|
30
|
+
*/
|
|
31
|
+
export function derivePrefix(pattern) {
|
|
32
|
+
if (!pattern.startsWith("^")) {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
let prefix = "";
|
|
36
|
+
for (let index = 1; index < pattern.length; index++) {
|
|
37
|
+
const character = pattern[index];
|
|
38
|
+
if (character === "\\") {
|
|
39
|
+
const escaped = pattern[index + 1];
|
|
40
|
+
if (escaped === undefined)
|
|
41
|
+
break;
|
|
42
|
+
prefix += escaped;
|
|
43
|
+
index++;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (REGEXP_METACHARACTERS.has(character))
|
|
47
|
+
break;
|
|
48
|
+
prefix += character;
|
|
49
|
+
}
|
|
50
|
+
return prefix;
|
|
51
|
+
}
|
|
52
|
+
export class RevalidateService {
|
|
53
|
+
configApi;
|
|
54
|
+
cli;
|
|
55
|
+
app;
|
|
56
|
+
constructor(configApi, cli, app) {
|
|
57
|
+
this.configApi = configApi;
|
|
58
|
+
this.cli = cli;
|
|
59
|
+
this.app = app;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Builds the filter the API expects from the patterns the caller stated, and
|
|
63
|
+
* derives the mandatory prefixes from them.
|
|
64
|
+
*/
|
|
65
|
+
buildFilter(patterns, explicitPrefixes) {
|
|
66
|
+
if (!patterns.length) {
|
|
67
|
+
return { prefixes: explicitPrefixes };
|
|
68
|
+
}
|
|
69
|
+
for (const pattern of patterns) {
|
|
70
|
+
const derived = derivePrefix(pattern);
|
|
71
|
+
if (!derived.startsWith("http")) {
|
|
72
|
+
throw new ApplicationError(`cannot derive a prefix from the pattern "${pattern}"`, [
|
|
73
|
+
"Anchor the pattern at the scheme, so its literal head is a URL prefix.",
|
|
74
|
+
'Example: "^https://www.example.com/en/.*-c1234$".',
|
|
75
|
+
"MongoDB reads that same literal as index bounds, so an unanchored pattern would also scan the table.",
|
|
76
|
+
"Pass --prefix yourself if the pattern genuinely cannot carry the host.",
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const clauses = patterns.map((pattern) => ({ url: { $regex: pattern } }));
|
|
81
|
+
return {
|
|
82
|
+
prefixes: explicitPrefixes.length
|
|
83
|
+
? explicitPrefixes
|
|
84
|
+
: patterns.map((pattern) => derivePrefix(pattern)),
|
|
85
|
+
query: clauses.length === 1 ? clauses[0] : { $or: clauses },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Rejects a filter the backend would reject, before the request goes out. The
|
|
90
|
+
* check is cheap here and the server-side error does not say which field is at
|
|
91
|
+
* fault.
|
|
92
|
+
*/
|
|
93
|
+
assertFilter(filter) {
|
|
94
|
+
if (!filter.prefixes?.length) {
|
|
95
|
+
throw new ApplicationError("a revalidation filter needs at least one prefix", [
|
|
96
|
+
"The backend rejects a filter without prefixes.",
|
|
97
|
+
"Pass --pattern, which derives the prefix, or --prefix to state one.",
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
for (const prefix of filter.prefixes) {
|
|
101
|
+
if (!prefix.startsWith("http")) {
|
|
102
|
+
throw new ApplicationError(`prefix "${prefix}" is not a full URL`, [
|
|
103
|
+
"A prefix matches the stored URL, so it carries the scheme and the host.",
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Reports what the job would take. The Refresh API has no preview mode, so this
|
|
110
|
+
* runs the job's own filter against the asset table instead.
|
|
111
|
+
*/
|
|
112
|
+
async preview(filter, sampleSize) {
|
|
113
|
+
const entityManager = await this.configApi.client.getEntityManager();
|
|
114
|
+
const mongoFilter = this.toMongoFilter(filter);
|
|
115
|
+
const count = await entityManager[ASSET_TABLE].find()
|
|
116
|
+
.where(mongoFilter)
|
|
117
|
+
.count();
|
|
118
|
+
const sample = await entityManager[ASSET_TABLE].find()
|
|
119
|
+
.where(mongoFilter)
|
|
120
|
+
.limit(sampleSize)
|
|
121
|
+
.resultList();
|
|
122
|
+
return {
|
|
123
|
+
count,
|
|
124
|
+
sample: sample.map((asset) => asset.url),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Asks before a purge that takes more assets than the threshold allows. */
|
|
128
|
+
async confirmLargeJob(count, type) {
|
|
129
|
+
return this.cli.confirm(`${type} ${count} assets? They stay out of the cache until traffic fills it again.`, false);
|
|
130
|
+
}
|
|
131
|
+
/** Submits the job and returns its status id. */
|
|
132
|
+
async submit(filter, type) {
|
|
133
|
+
const response = await this.request(`https://${this.app}.app.baqend.com/v1/asset/revalidate`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
body: JSON.stringify({ type, filter, triggeredBy: "sk-cli" }),
|
|
136
|
+
});
|
|
137
|
+
return response.statusId;
|
|
138
|
+
}
|
|
139
|
+
async status(statusId) {
|
|
140
|
+
return this.request(`https://${this.app}.app.baqend.com/v1/asset/revalidate/${statusId}`);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Issues the request itself instead of going through the shared client. A
|
|
144
|
+
* submitted job answers 202, and the shared client reports every status other
|
|
145
|
+
* than 200 as an error — which turns an accepted job into "Error: Accepted".
|
|
146
|
+
*/
|
|
147
|
+
async request(url, extension = {}) {
|
|
148
|
+
const requestInit = await this.configApi.client.createRequest(extension);
|
|
149
|
+
const response = await fetch(url, requestInit);
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
const body = await response.text();
|
|
152
|
+
throw new ApplicationError(`the revalidation API answered ${response.status} ${response.statusText}`, [body.slice(0, 400)]);
|
|
153
|
+
}
|
|
154
|
+
const body = await response.text();
|
|
155
|
+
return (body ? JSON.parse(body) : {});
|
|
156
|
+
}
|
|
157
|
+
/** Polls until the job reaches a terminal state, and reports each state change. */
|
|
158
|
+
async waitFor(statusId) {
|
|
159
|
+
let lastState = "";
|
|
160
|
+
for (;;) {
|
|
161
|
+
const status = await this.status(statusId);
|
|
162
|
+
if (status.state !== lastState) {
|
|
163
|
+
this.cli.write(`revalidation job ${statusId}: ${status.state}`);
|
|
164
|
+
lastState = status.state;
|
|
165
|
+
}
|
|
166
|
+
if (TERMINAL_STATES.has(status.state)) {
|
|
167
|
+
return status;
|
|
168
|
+
}
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Mirrors the job's filter as one MongoDB query, so the dry run reports the set
|
|
174
|
+
* the job takes and not every URL the query alone would match.
|
|
175
|
+
*
|
|
176
|
+
* Each prefix becomes its own anchored expression, and they combine with `$or`.
|
|
177
|
+
* One alternation would read `^(?:a|b)`, whose first character after the anchor
|
|
178
|
+
* is a metacharacter — MongoDB then extracts no literal prefix and scans the
|
|
179
|
+
* collection. Some asset tables are far too large for that. `^literal` keeps
|
|
180
|
+
* each branch on the index.
|
|
181
|
+
*/
|
|
182
|
+
toMongoFilter(filter) {
|
|
183
|
+
const prefixClauses = filter.prefixes.map((prefix) => ({
|
|
184
|
+
url: { $regex: `^${escapeRegExp(prefix)}` },
|
|
185
|
+
}));
|
|
186
|
+
const prefixClause = prefixClauses.length === 1 ? prefixClauses[0] : { $or: prefixClauses };
|
|
187
|
+
return filter.query ? { $and: [prefixClause, filter.query] } : prefixClause;
|
|
188
|
+
}
|
|
189
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -871,6 +871,100 @@
|
|
|
871
871
|
"pull.js"
|
|
872
872
|
]
|
|
873
873
|
},
|
|
874
|
+
"revalidate": {
|
|
875
|
+
"aliases": [],
|
|
876
|
+
"args": {
|
|
877
|
+
"app": {
|
|
878
|
+
"description": "The customer app name",
|
|
879
|
+
"name": "app",
|
|
880
|
+
"required": true
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
"description": "Purge or refresh cached assets by prefix and query. Run it after a config that narrows scope goes live, so returning visitors stop receiving the cached HTML.",
|
|
884
|
+
"examples": [
|
|
885
|
+
"$ sk revalidate <app> --pattern <regex> [--pattern <regex>] [--dryRun] [--wait]",
|
|
886
|
+
"$ sk revalidate decathlon --pattern '^https://www\\\\.decathlon\\\\.de/damen/.*-c(?:1234|5678)(?:/|\\\\?|$)' --dryRun",
|
|
887
|
+
"$ sk revalidate decathlon --pattern '^https://www\\\\.decathlon\\\\.de/damen/.*-c1234(?:/|\\\\?|$)' --wait",
|
|
888
|
+
"$ sk revalidate decathlon --prefix https://www.decathlon.de/damen/ --dryRun"
|
|
889
|
+
],
|
|
890
|
+
"flags": {
|
|
891
|
+
"pattern": {
|
|
892
|
+
"description": "URL pattern to purge, as a regex anchored at the scheme — mirror what the config excludes, e.g. \"^https://www.example.com/en/.*-c1234$\". Repeatable. The command derives the prefix the API requires from the pattern's literal head, and MongoDB reads that same literal as index bounds.",
|
|
893
|
+
"name": "pattern",
|
|
894
|
+
"hasDynamicHelp": false,
|
|
895
|
+
"multiple": true,
|
|
896
|
+
"type": "option"
|
|
897
|
+
},
|
|
898
|
+
"prefix": {
|
|
899
|
+
"description": "URL prefix, matched against the start of the cached URL. Derived from --pattern by default; state it only to override that, or to purge a whole path without a pattern.",
|
|
900
|
+
"name": "prefix",
|
|
901
|
+
"hasDynamicHelp": false,
|
|
902
|
+
"multiple": true,
|
|
903
|
+
"type": "option"
|
|
904
|
+
},
|
|
905
|
+
"query": {
|
|
906
|
+
"description": "Raw MongoDB query on the asset fields, as JSON. An escape hatch for a rule --pattern cannot express, such as a condition on another field. It replaces the query --pattern would build.",
|
|
907
|
+
"name": "query",
|
|
908
|
+
"hasDynamicHelp": false,
|
|
909
|
+
"multiple": false,
|
|
910
|
+
"type": "option"
|
|
911
|
+
},
|
|
912
|
+
"type": {
|
|
913
|
+
"description": "PURGE drops the entries and does not rebuild them, which is what an out-of-scope URL needs.",
|
|
914
|
+
"name": "type",
|
|
915
|
+
"default": "PURGE",
|
|
916
|
+
"hasDynamicHelp": false,
|
|
917
|
+
"multiple": false,
|
|
918
|
+
"options": [
|
|
919
|
+
"REFRESH",
|
|
920
|
+
"INSTANT",
|
|
921
|
+
"DEPLOYMENT",
|
|
922
|
+
"PURGE"
|
|
923
|
+
],
|
|
924
|
+
"type": "option"
|
|
925
|
+
},
|
|
926
|
+
"dryRun": {
|
|
927
|
+
"char": "d",
|
|
928
|
+
"description": "Report the assets the filter selects and exit. Changes nothing.",
|
|
929
|
+
"name": "dryRun",
|
|
930
|
+
"allowNo": false,
|
|
931
|
+
"type": "boolean"
|
|
932
|
+
},
|
|
933
|
+
"wait": {
|
|
934
|
+
"description": "Poll the job until it finishes.",
|
|
935
|
+
"name": "wait",
|
|
936
|
+
"allowNo": false,
|
|
937
|
+
"type": "boolean"
|
|
938
|
+
},
|
|
939
|
+
"yes": {
|
|
940
|
+
"description": "Skip the confirmation a job above 100 assets asks for. Required in a non-interactive shell.",
|
|
941
|
+
"name": "yes",
|
|
942
|
+
"allowNo": false,
|
|
943
|
+
"type": "boolean"
|
|
944
|
+
},
|
|
945
|
+
"quiet": {
|
|
946
|
+
"char": "q",
|
|
947
|
+
"description": "less output non interactive",
|
|
948
|
+
"name": "quiet",
|
|
949
|
+
"allowNo": false,
|
|
950
|
+
"type": "boolean"
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
"hasDynamicHelp": false,
|
|
954
|
+
"hiddenAliases": [],
|
|
955
|
+
"id": "revalidate",
|
|
956
|
+
"pluginAlias": "@speedkit/cli",
|
|
957
|
+
"pluginName": "@speedkit/cli",
|
|
958
|
+
"pluginType": "core",
|
|
959
|
+
"strict": true,
|
|
960
|
+
"enableJsonFlag": false,
|
|
961
|
+
"isESM": true,
|
|
962
|
+
"relativePath": [
|
|
963
|
+
"dist",
|
|
964
|
+
"commands",
|
|
965
|
+
"revalidate.js"
|
|
966
|
+
]
|
|
967
|
+
},
|
|
874
968
|
"wpt-launcher": {
|
|
875
969
|
"aliases": [],
|
|
876
970
|
"args": {
|
|
@@ -1220,5 +1314,5 @@
|
|
|
1220
1314
|
]
|
|
1221
1315
|
}
|
|
1222
1316
|
},
|
|
1223
|
-
"version": "4.
|
|
1317
|
+
"version": "4.25.0"
|
|
1224
1318
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@speedkit/cli",
|
|
3
3
|
"description": "Speed Kit CLI",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.25.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Baqend.com",
|
|
7
7
|
"email": "info@baqend.com"
|
|
@@ -91,6 +91,8 @@
|
|
|
91
91
|
"@eslint/eslintrc": "^3.3.5",
|
|
92
92
|
"@eslint/js": "^10.0.1",
|
|
93
93
|
"@inquirer/prompts": "^8.4.2",
|
|
94
|
+
"@nats-io/nats-core": "^3.2.0",
|
|
95
|
+
"@nats-io/transport-node": "^3.2.0",
|
|
94
96
|
"@oclif/core": "^4.10.6",
|
|
95
97
|
"@oclif/errors": "^1.3.6",
|
|
96
98
|
"@oclif/plugin-autocomplete": "^3.2.46",
|
|
@@ -102,8 +104,13 @@
|
|
|
102
104
|
"chokidar": "^5.0.0",
|
|
103
105
|
"cli-progress": "^3.12.0",
|
|
104
106
|
"clipboardy": "^5.3.1",
|
|
107
|
+
"css-select": "^5.1.0",
|
|
105
108
|
"deepmerge": "^4.3.1",
|
|
106
109
|
"diff": "^9.0.0",
|
|
110
|
+
"dom-serializer": "^2.0.0",
|
|
111
|
+
"domelementtype": "^2.3.0",
|
|
112
|
+
"domhandler": "^5.0.3",
|
|
113
|
+
"domutils": "^3.1.0",
|
|
107
114
|
"dotenv": "^17.4.2",
|
|
108
115
|
"encoding-japanese": "^2.2.0",
|
|
109
116
|
"esbuild": "^0.27.3",
|
|
@@ -115,8 +122,11 @@
|
|
|
115
122
|
"iconv-lite": "^0.7.2",
|
|
116
123
|
"json2csv": "^6.0.0-alpha.2",
|
|
117
124
|
"node-fetch": "^3.3.2",
|
|
125
|
+
"node-fetch-native": "^1.6.7",
|
|
118
126
|
"nunjucks": "^3.2.4",
|
|
119
127
|
"nypm": "^0.6.6",
|
|
128
|
+
"parse5": "^7.1.2",
|
|
129
|
+
"parse5-htmlparser2-tree-adapter": "^7.0.0",
|
|
120
130
|
"puppeteer": "^24.41.0",
|
|
121
131
|
"puppeteer-extra": "^3.3.6",
|
|
122
132
|
"semver": "^7.7.4",
|