@loopress/cli 0.32.2 → 0.34.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/README.md +549 -179
- package/dist/commands/acf/list.d.ts +1 -1
- package/dist/commands/acf/list.js +3 -4
- package/dist/commands/acf/pull.d.ts +1 -1
- package/dist/commands/acf/pull.js +9 -6
- package/dist/commands/acf/push.d.ts +1 -1
- package/dist/commands/acf/push.js +5 -4
- package/dist/commands/api/publish.js +2 -1
- package/dist/commands/app/pull.js +2 -1
- package/dist/commands/app/push.js +7 -5
- package/dist/commands/app/{remove.d.ts → rm.d.ts} +1 -0
- package/dist/commands/app/{remove.js → rm.js} +2 -1
- package/dist/commands/block/diff.d.ts +2 -0
- package/dist/commands/block/diff.js +5 -0
- package/dist/commands/block/list.d.ts +8 -0
- package/dist/commands/block/list.js +23 -0
- package/dist/commands/block/pull.d.ts +21 -0
- package/dist/commands/block/pull.js +67 -0
- package/dist/commands/block/push.d.ts +20 -0
- package/dist/commands/block/push.js +53 -0
- package/dist/commands/block/rm.d.ts +23 -0
- package/dist/commands/block/rm.js +69 -0
- package/dist/commands/block/rollback.d.ts +2 -0
- package/dist/commands/block/rollback.js +5 -0
- package/dist/commands/composer/init.js +1 -0
- package/dist/commands/composer/push.js +5 -4
- package/dist/commands/content/copy.d.ts +3 -0
- package/dist/commands/content/copy.js +79 -2
- package/dist/commands/dev.d.ts +2 -2
- package/dist/commands/dev.js +13 -18
- package/dist/commands/diff.d.ts +6 -3
- package/dist/commands/diff.js +63 -13
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.js +49 -25
- package/dist/commands/form/pull.js +7 -5
- package/dist/commands/form/push.js +4 -3
- package/dist/commands/init.js +4 -3
- package/dist/commands/menu/push.js +8 -8
- package/dist/commands/option/list.js +5 -2
- package/dist/commands/option/push.js +3 -1
- package/dist/commands/option/{remove.d.ts → rm.d.ts} +1 -0
- package/dist/commands/option/{remove.js → rm.js} +2 -1
- package/dist/commands/page/push.js +3 -1
- package/dist/commands/plugin/add.d.ts +1 -0
- package/dist/commands/plugin/add.js +50 -9
- package/dist/commands/plugin/audit.d.ts +4 -4
- package/dist/commands/plugin/audit.js +35 -15
- package/dist/commands/plugin/pull.js +3 -2
- package/dist/commands/plugin/push.js +41 -6
- package/dist/commands/plugin/status.js +3 -1
- package/dist/commands/project/config.js +7 -7
- package/dist/commands/project/pull.js +3 -1
- package/dist/commands/project/push.js +6 -4
- package/dist/commands/project/{remove.d.ts → rm.d.ts} +1 -0
- package/dist/commands/project/{remove.js → rm.js} +3 -2
- package/dist/commands/project/switch.js +6 -4
- package/dist/commands/promote.d.ts +1 -0
- package/dist/commands/promote.js +15 -6
- package/dist/commands/pull.d.ts +7 -0
- package/dist/commands/pull.js +67 -18
- package/dist/commands/push.d.ts +11 -1
- package/dist/commands/push.js +157 -48
- package/dist/commands/snippet/publish.js +9 -5
- package/dist/commands/snippet/push.d.ts +1 -1
- package/dist/commands/snippet/push.js +15 -12
- package/dist/commands/status.js +1 -0
- package/dist/commands/theme/activate.d.ts +21 -0
- package/dist/commands/theme/activate.js +44 -0
- package/dist/commands/theme/pull.js +2 -1
- package/dist/commands/theme/push.js +7 -3
- package/dist/commands/theme/style/diff.js +1 -1
- package/dist/commands/theme/style/push.js +8 -3
- package/dist/commands/theme/style/rollback.js +1 -1
- package/dist/commands/theme/template/push.d.ts +2 -0
- package/dist/commands/theme/template/push.js +66 -18
- package/dist/commands/validate.js +3 -0
- package/dist/config/auth.manager.d.ts +10 -7
- package/dist/config/auth.manager.js +5 -1
- package/dist/config/json-file.d.ts +1 -1
- package/dist/config/project-config.manager.d.ts +2 -2
- package/dist/config/project-config.manager.js +13 -21
- package/dist/lib/api-client.js +16 -21
- package/dist/lib/app-build.js +6 -2
- package/dist/lib/app-manifest.js +6 -6
- package/dist/lib/base.d.ts +8 -1
- package/dist/lib/base.js +43 -9
- package/dist/lib/bootstrap-full-install.js +8 -6
- package/dist/lib/capabilities.d.ts +44 -0
- package/dist/lib/capabilities.js +85 -0
- package/dist/lib/declared-plugins.d.ts +9 -0
- package/dist/lib/declared-plugins.js +21 -0
- package/dist/lib/declared-type-command.js +7 -2
- package/dist/lib/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +13 -15
- package/dist/lib/diff-command.d.ts +19 -3
- package/dist/lib/diff-command.js +132 -14
- package/dist/lib/diff-state.js +2 -3
- package/dist/lib/environment-urls.d.ts +6 -0
- package/dist/lib/environment-urls.js +59 -0
- package/dist/lib/exclude.d.ts +16 -0
- package/dist/lib/exclude.js +102 -0
- package/dist/lib/load-files.js +2 -1
- package/dist/lib/load-snippets.js +39 -43
- package/dist/lib/local-callback-server.js +7 -1
- package/dist/lib/narrow.d.ts +5 -0
- package/dist/lib/narrow.js +26 -0
- package/dist/lib/php-files-command.js +13 -8
- package/dist/lib/plugin-detection.d.ts +0 -1
- package/dist/lib/plugin-detection.js +2 -4
- package/dist/lib/push-command.d.ts +5 -1
- package/dist/lib/push-command.js +48 -7
- package/dist/lib/put-or-create.d.ts +2 -2
- package/dist/lib/readdir-tolerant.js +9 -4
- package/dist/lib/resource-selection.d.ts +9 -0
- package/dist/lib/resource-selection.js +49 -0
- package/dist/lib/resource-state.d.ts +2 -0
- package/dist/lib/resource-state.js +60 -14
- package/dist/lib/rollback-command.d.ts +1 -1
- package/dist/lib/rollback-command.js +4 -3
- package/dist/lib/rollback-materialize.js +29 -9
- package/dist/lib/rotate-app-password.js +1 -0
- package/dist/lib/sentry.js +2 -2
- package/dist/lib/snapshot-store.d.ts +10 -7
- package/dist/lib/snapshot-store.js +21 -5
- package/dist/lib/spinner.js +4 -0
- package/dist/lib/validate-local.d.ts +1 -0
- package/dist/lib/validate-local.js +102 -28
- package/dist/lib/wp-client.d.ts +7 -1
- package/dist/lib/wp-client.js +60 -69
- package/dist/lib/wp-site-diagnostic.js +3 -5
- package/dist/types/global-config.generated.d.ts +1 -1
- package/dist/types/plugin.d.ts +7 -5
- package/dist/types/plugin.js +6 -1
- package/dist/types/project-config.generated.d.ts +24 -5
- package/dist/types/snippet.d.ts +2 -2
- package/dist/types/snippet.js +1 -1
- package/dist/utils/acf-format.js +6 -3
- package/dist/utils/block-format.d.ts +29 -0
- package/dist/utils/block-format.js +109 -0
- package/dist/utils/composer.d.ts +7 -15
- package/dist/utils/composer.js +43 -16
- package/dist/utils/declared-type-format.js +26 -19
- package/dist/utils/form-format.js +2 -2
- package/dist/utils/loopress-config.js +9 -1
- package/dist/utils/menu-format.d.ts +3 -1
- package/dist/utils/menu-format.js +5 -0
- package/dist/utils/option-format.d.ts +2 -2
- package/dist/utils/option-format.js +15 -25
- package/dist/utils/page-format.js +9 -7
- package/dist/utils/php-top-level.js +15 -4
- package/dist/utils/plugin-sync.d.ts +7 -7
- package/dist/utils/plugin-sync.js +11 -11
- package/dist/utils/plugins.js +9 -6
- package/dist/utils/resource-dirs.d.ts +2 -0
- package/dist/utils/resource-dirs.js +13 -5
- package/dist/utils/snippet-format.d.ts +1 -1
- package/dist/utils/snippet-format.js +5 -3
- package/dist/utils/template-format.js +2 -2
- package/dist/utils/theme-css.js +14 -6
- package/dist/utils/theme-styles-format.d.ts +3 -0
- package/dist/utils/theme-styles-format.js +15 -5
- package/dist/utils/version.js +1 -1
- package/oclif.manifest.json +2661 -1235
- package/package.json +45 -20
- package/schemas/global-config.schema.json +1 -1
- package/schemas/project-config.schema.json +32 -5
|
@@ -2,10 +2,13 @@ import { Args } from '@oclif/core';
|
|
|
2
2
|
import { readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { basename, dirname, extname, join } from 'node:path';
|
|
4
4
|
import { loadSnippets as loadSnippetsFromDisk } from '../../lib/load-snippets.js';
|
|
5
|
+
import { errorCode, errorMessage, isRecord } from '../../lib/narrow.js';
|
|
5
6
|
import { PushCommand } from '../../lib/push-command.js';
|
|
6
7
|
import { putOrCreate } from '../../lib/put-or-create.js';
|
|
7
8
|
import { getResourceStateProvider } from '../../lib/resource-state.js';
|
|
8
9
|
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
10
|
+
import {} from '../../types/snippet.generated.js';
|
|
11
|
+
import {} from '../../types/snippet.js';
|
|
9
12
|
import { pluralize } from '../../utils/pluralize.js';
|
|
10
13
|
import { normalizeSnippet, SNIPPETS_ENDPOINT, stripPhpOpeningTag } from '../../utils/snippet-format.js';
|
|
11
14
|
import { toSlug } from '../../utils/to-slug.js';
|
|
@@ -65,18 +68,18 @@ export default class Push extends PushCommand {
|
|
|
65
68
|
const currentBase = basename(snippet.path, ext);
|
|
66
69
|
const canonicalBase = `${id}-${toSlug(name)}`;
|
|
67
70
|
const oldMetaPath = join(dir, `${currentBase}.json`);
|
|
68
|
-
let
|
|
71
|
+
let existing = {};
|
|
69
72
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
73
|
+
const parsed = JSON.parse(await readFile(oldMetaPath, 'utf8'));
|
|
74
|
+
if (isRecord(parsed))
|
|
75
|
+
existing = parsed;
|
|
72
76
|
}
|
|
73
77
|
catch (error) {
|
|
74
|
-
if (error
|
|
78
|
+
if (errorCode(error) !== 'ENOENT')
|
|
75
79
|
throw error;
|
|
76
80
|
}
|
|
77
|
-
|
|
78
|
-
meta
|
|
79
|
-
meta.type = snippet.type;
|
|
81
|
+
// Spread, not rebuilt: keys the CLI doesn't know about survive, in their original order.
|
|
82
|
+
const meta = { ...existing, id, name, type: snippet.type };
|
|
80
83
|
// Persist the id against the *current* file pairing before renaming anything, so a
|
|
81
84
|
// crash between the rename and the sidecar write still leaves a valid `<name>.<ext>` /
|
|
82
85
|
// `<name>.json` pair with the id on disk, and a retried `snippet push` won't re-create
|
|
@@ -95,7 +98,7 @@ export default class Push extends PushCommand {
|
|
|
95
98
|
return await loadSnippetsFromDisk(path, (message) => { this.warn(message); });
|
|
96
99
|
}
|
|
97
100
|
catch (error) {
|
|
98
|
-
this.error(error
|
|
101
|
+
this.error(errorMessage(error));
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
async pushSnippet(snippet, task) {
|
|
@@ -134,15 +137,15 @@ export default class Push extends PushCommand {
|
|
|
134
137
|
id = createdSnippet.id;
|
|
135
138
|
await this.ensureCanonicalFilename(snippet, createdSnippet.id, createdSnippet.name);
|
|
136
139
|
}
|
|
137
|
-
else {
|
|
138
|
-
await this.ensureCanonicalFilename(snippet,
|
|
140
|
+
else if (id !== undefined) {
|
|
141
|
+
await this.ensureCanonicalFilename(snippet, id, snippet.name);
|
|
139
142
|
}
|
|
140
143
|
if (task)
|
|
141
144
|
task.output = `Pushed: ${snippet.name}`;
|
|
142
145
|
return id;
|
|
143
146
|
}
|
|
144
147
|
catch (error) {
|
|
145
|
-
this.reportTaskFailure(`Failed to push ${snippet.name}: ${error
|
|
148
|
+
this.reportTaskFailure(`Failed to push ${snippet.name}: ${errorMessage(error)}`, error, task);
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
toPayload(snippet) {
|
|
@@ -172,7 +175,7 @@ export default class Push extends PushCommand {
|
|
|
172
175
|
pathsById.set(id, [...(pathsById.get(id) ?? []), path]);
|
|
173
176
|
}
|
|
174
177
|
return snippets.filter((snippet) => {
|
|
175
|
-
const paths = snippet.id === undefined ? [] : pathsById.get(snippet.id);
|
|
178
|
+
const paths = snippet.id === undefined ? [] : (pathsById.get(snippet.id) ?? []);
|
|
176
179
|
if (paths.length < 2)
|
|
177
180
|
return true;
|
|
178
181
|
this.failedCount++;
|
package/dist/commands/status.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command, Flags, ux } from '@oclif/core';
|
|
2
2
|
import { configManager } from '../config/project-config.manager.js';
|
|
3
|
+
import {} from '../types/config.js';
|
|
3
4
|
import { readLocalConfig } from '../utils/loopress-config.js';
|
|
4
5
|
const c = ux.colorize;
|
|
5
6
|
export default class Status extends Command {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
type ActivateResult = {
|
|
3
|
+
active: string;
|
|
4
|
+
previous: string;
|
|
5
|
+
status: 'activated' | 'dry-run' | 'unchanged';
|
|
6
|
+
};
|
|
7
|
+
export default class Activate extends LoopressCommand {
|
|
8
|
+
static args: {
|
|
9
|
+
stylesheet: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
10
|
+
};
|
|
11
|
+
static description: string;
|
|
12
|
+
static enableJsonFlag: boolean;
|
|
13
|
+
static examples: string[];
|
|
14
|
+
static flags: {
|
|
15
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<ActivateResult>;
|
|
19
|
+
private childStylesheet;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
3
|
+
import { activateTheme } from '../../lib/capabilities.js';
|
|
4
|
+
import { guardProductionPush } from '../../lib/guard-production-push.js';
|
|
5
|
+
export default class Activate extends LoopressCommand {
|
|
6
|
+
static args = {
|
|
7
|
+
stylesheet: Args.string({ description: 'Theme to activate (default: the Loopress child theme, <parent>-loopress)' }),
|
|
8
|
+
};
|
|
9
|
+
static description = 'Activate a theme on WordPress, the Loopress child theme by default. A child theme without settings of its own starts from a copy of its parent\'s theme mods and Global Styles. Set "activateChildTheme": true in loopress.json to have `lps theme template push` keep the child active.';
|
|
10
|
+
static enableJsonFlag = true;
|
|
11
|
+
static examples = ['$ lps theme activate', '$ lps theme activate twentytwentyfive --env staging', '$ lps theme activate --dry-run'];
|
|
12
|
+
static flags = {
|
|
13
|
+
...LoopressCommand.dryRunFlag,
|
|
14
|
+
...LoopressCommand.yesFlag,
|
|
15
|
+
};
|
|
16
|
+
async run() {
|
|
17
|
+
const { args } = await this.parse(Activate);
|
|
18
|
+
const { theme } = await this.capabilities();
|
|
19
|
+
const stylesheet = args.stylesheet ?? this.childStylesheet(theme);
|
|
20
|
+
if (stylesheet === theme.active) {
|
|
21
|
+
this.log(`${stylesheet} is already the active theme.`);
|
|
22
|
+
return { active: stylesheet, previous: theme.active, status: 'unchanged' };
|
|
23
|
+
}
|
|
24
|
+
if (this.dryRun) {
|
|
25
|
+
this.log(`[dry-run] Would activate ${stylesheet} instead of ${theme.active}.`);
|
|
26
|
+
return { active: stylesheet, previous: theme.active, status: 'dry-run' };
|
|
27
|
+
}
|
|
28
|
+
await guardProductionPush({ dryRun: this.dryRun, error: (message) => this.error(message), siteConfig: this.siteConfig, yes: this.yes });
|
|
29
|
+
const activation = await activateTheme(this.wp, stylesheet);
|
|
30
|
+
this.log(`Activated ${activation.active} (was ${activation.previous}).`);
|
|
31
|
+
return { active: activation.active, previous: activation.previous, status: 'activated' };
|
|
32
|
+
}
|
|
33
|
+
childStylesheet(theme) {
|
|
34
|
+
if (theme.child === undefined) {
|
|
35
|
+
this.error('The Loopress child theme needs Loopress Full. Name the theme to activate instead.');
|
|
36
|
+
}
|
|
37
|
+
else if (theme.child === null) {
|
|
38
|
+
this.error(`The active theme "${theme.active}" can't have a Loopress child theme: it needs a block theme that isn't already a child theme.`);
|
|
39
|
+
}
|
|
40
|
+
if (!theme.child.exists)
|
|
41
|
+
this.error(`${theme.child.stylesheet} doesn't exist yet. Run \`lps theme template push\` first.`);
|
|
42
|
+
return theme.child.stylesheet;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
2
|
import { stdoutToStderr } from '../../lib/json-delegation.js';
|
|
3
|
+
import { errorMessage } from '../../lib/narrow.js';
|
|
3
4
|
import { pluralize } from '../../utils/pluralize.js';
|
|
4
5
|
// Templates and parts have no pull: the child theme is written from the project, never read back.
|
|
5
6
|
const PULL_TARGETS = [
|
|
@@ -26,7 +27,7 @@ export default class Pull extends LoopressCommand {
|
|
|
26
27
|
results.push({ label: target.label, status: 'pulled' });
|
|
27
28
|
}
|
|
28
29
|
catch (error) {
|
|
29
|
-
const
|
|
30
|
+
const message = errorMessage(error);
|
|
30
31
|
this.log(`✗ ${target.label} failed: ${message}`);
|
|
31
32
|
results.push({ error: message, label: target.label, status: 'failed' });
|
|
32
33
|
}
|
|
@@ -4,7 +4,9 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { LoopressCommand } from '../../lib/base.js';
|
|
5
5
|
import { guardProductionPush } from '../../lib/guard-production-push.js';
|
|
6
6
|
import { stdoutToStderr } from '../../lib/json-delegation.js';
|
|
7
|
+
import { errorMessage } from '../../lib/narrow.js';
|
|
7
8
|
import { pluralize } from '../../utils/pluralize.js';
|
|
9
|
+
import { projectHasChildThemeFiles } from '../../utils/resource-dirs.js';
|
|
8
10
|
// The parent theme must be installed before its child theme is written, and Global Styles
|
|
9
11
|
// belong to whichever theme is active, so they go last.
|
|
10
12
|
const PUSH_TARGETS = [
|
|
@@ -13,7 +15,7 @@ const PUSH_TARGETS = [
|
|
|
13
15
|
{ commandId: 'theme:style:push', label: 'Global Styles' },
|
|
14
16
|
];
|
|
15
17
|
export default class Push extends LoopressCommand {
|
|
16
|
-
static description = 'Push everything theme related to WordPress: theme versions from loopress.json, then block templates and parts, then Global Styles.
|
|
18
|
+
static description = 'Push everything theme related to WordPress: theme versions from loopress.json, then block templates and parts, then Global Styles. Switches the active theme only to the Loopress child, when loopress.json sets "activateChildTheme": true.';
|
|
17
19
|
static enableJsonFlag = true;
|
|
18
20
|
static examples = ['$ lps theme push', '$ lps theme push --env staging', '$ lps theme push --dry-run'];
|
|
19
21
|
static flags = {
|
|
@@ -34,7 +36,9 @@ export default class Push extends LoopressCommand {
|
|
|
34
36
|
// Nothing to install and no composer.json: the versions step would only fail on an empty
|
|
35
37
|
// manifest, which must not fail a templates or styles only project.
|
|
36
38
|
const hasVersions = Object.keys(this.localConfig.themes ?? {}).length > 0 || existsSync(join(process.cwd(), this.rootDir, 'composer.json'));
|
|
37
|
-
|
|
39
|
+
// Same for templates: without templates, parts or CSS the step would refuse (nothing to push).
|
|
40
|
+
const hasChildTheme = projectHasChildThemeFiles(this.localConfig);
|
|
41
|
+
const targets = PUSH_TARGETS.filter((target) => (hasVersions || target.commandId !== 'theme:version:push') && (hasChildTheme || target.commandId !== 'theme:template:push'));
|
|
38
42
|
const results = [];
|
|
39
43
|
for (const target of targets) {
|
|
40
44
|
const argv = ['--env', this.siteConfig.name, '--production-confirmed'];
|
|
@@ -51,7 +55,7 @@ export default class Push extends LoopressCommand {
|
|
|
51
55
|
results.push({ label: target.label, result, status: 'pushed' });
|
|
52
56
|
}
|
|
53
57
|
catch (error) {
|
|
54
|
-
const
|
|
58
|
+
const message = errorMessage(error);
|
|
55
59
|
this.log(`✗ ${target.label} failed: ${message}`);
|
|
56
60
|
results.push({ error: message, label: target.label, status: 'failed' });
|
|
57
61
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resourceDiffCommand } from '../../../lib/diff-command.js';
|
|
2
|
-
export default resourceDiffCommand('theme-
|
|
2
|
+
export default resourceDiffCommand('theme-style', {
|
|
3
3
|
description: "Show what differs in the active block theme's Global Styles (Site Editor > Styles) between your local file and a WordPress environment, or between two environments",
|
|
4
4
|
command: 'theme style',
|
|
5
5
|
pathNoun: 'theme styles directory',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { errorCode, isRecord } from '../../../lib/narrow.js';
|
|
4
5
|
import { PushCommand } from '../../../lib/push-command.js';
|
|
5
6
|
import { getResourceStateProvider } from '../../../lib/resource-state.js';
|
|
6
7
|
import { getActiveThemeGlobalStyles, globalStylesEndpoint, themeStylesFileName } from '../../../utils/theme-styles-format.js';
|
|
@@ -27,18 +28,22 @@ export default class Push extends PushCommand {
|
|
|
27
28
|
raw = await readFile(file, 'utf8');
|
|
28
29
|
}
|
|
29
30
|
catch (error) {
|
|
30
|
-
if (error
|
|
31
|
+
if (errorCode(error) === 'ENOENT') {
|
|
31
32
|
this.error(`No local Global Styles file found for the active theme ("${file}"). Run \`lps theme style pull\` first.`);
|
|
32
33
|
}
|
|
33
34
|
throw error;
|
|
34
35
|
}
|
|
35
36
|
// Only the active theme's file is pushed, whatever else the directory holds.
|
|
36
|
-
const provider = getResourceStateProvider('theme-
|
|
37
|
+
const provider = getResourceStateProvider('theme-style');
|
|
37
38
|
const beforeState = await this.captureBeforePushState(provider, dir, (name) => name === stylesheet);
|
|
38
39
|
if (this.dryRun)
|
|
39
40
|
return { changes: this.previewChanges, file, status: 'dry-run' };
|
|
40
41
|
const local = JSON.parse(raw);
|
|
41
|
-
|
|
42
|
+
// Never fall back to empty settings/styles: that push would wipe the site's customizations.
|
|
43
|
+
if (!isRecord(local))
|
|
44
|
+
throw new Error(`${file} is not a JSON object`);
|
|
45
|
+
const { settings, styles } = local;
|
|
46
|
+
await this.wp.post(globalStylesEndpoint(id), { settings: settings ?? {}, styles: styles ?? {} });
|
|
42
47
|
this.log(`Pushed: ${file}`);
|
|
43
48
|
await this.writeAfterPushSnapshot(provider, dir, beforeState);
|
|
44
49
|
await this.recordSuccess();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resourceRollbackCommand } from '../../../lib/rollback-command.js';
|
|
2
|
-
export default resourceRollbackCommand('theme-
|
|
2
|
+
export default resourceRollbackCommand('theme-style', {
|
|
3
3
|
description: "Restore the active theme's Global Styles to a snapshot taken automatically before an earlier `lps theme style push`",
|
|
4
4
|
command: 'theme style',
|
|
5
5
|
pathNoun: 'theme styles directory',
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { activateTheme } from '../../../lib/capabilities.js';
|
|
4
|
+
import {} from '../../../lib/diff-state.js';
|
|
5
|
+
import { hasExclusions, isExcluded } from '../../../lib/exclude.js';
|
|
6
|
+
import { errorMessage } from '../../../lib/narrow.js';
|
|
2
7
|
import { logChanges, pushChanges, PushCommand } from '../../../lib/push-command.js';
|
|
3
8
|
import { formatPageProblems } from '../../../utils/page-format.js';
|
|
4
9
|
import { pluralize } from '../../../utils/pluralize.js';
|
|
5
|
-
import { resolveResourceDir } from '../../../utils/resource-dirs.js';
|
|
10
|
+
import { projectHasChildThemeFiles, resolveResourceDir } from '../../../utils/resource-dirs.js';
|
|
6
11
|
import { CHILD_THEME_ENDPOINT, readLocalTemplates } from '../../../utils/template-format.js';
|
|
7
12
|
import { bundleThemeCss } from '../../../utils/theme-css.js';
|
|
8
13
|
export default class Push extends PushCommand {
|
|
9
|
-
static description = 'Push block templates (theme/templates/<slug>.html), template parts (theme/parts/<slug>.html) and the theme CSS (theme/css/style.css, theme/blocks/<namespace>/<block>.css, bundled with esbuild) as the files of a child theme of the active block theme, <parent>-loopress. The child mirrors the project: files removed locally are removed from it. It is
|
|
14
|
+
static description = 'Push block templates (theme/templates/<slug>.html), template parts (theme/parts/<slug>.html) and the theme CSS (theme/css/style.css, theme/blocks/<namespace>/<block>.css, bundled with esbuild) as the files of a child theme of the active block theme, <parent>-loopress. The child mirrors the project: files removed locally are removed from it. It is activated only when loopress.json sets "activateChildTheme": true, see `lps theme activate`.';
|
|
10
15
|
static enableJsonFlag = true;
|
|
11
16
|
static examples = ['$ lps theme template push', '$ lps theme template push --dry-run'];
|
|
12
17
|
static flags = {
|
|
@@ -17,6 +22,10 @@ export default class Push extends PushCommand {
|
|
|
17
22
|
await this.parse(Push);
|
|
18
23
|
const templatesPath = resolveResourceDir('template', this.localConfig);
|
|
19
24
|
const partsPath = resolveResourceDir('part', this.localConfig);
|
|
25
|
+
// Pushing would only create an empty child theme as a side effect.
|
|
26
|
+
if (!projectHasChildThemeFiles(this.localConfig)) {
|
|
27
|
+
this.error(`Nothing to push: no ${templatesPath}, ${partsPath}, ${resolveResourceDir('css', this.localConfig)} or ${resolveResourceDir('blockCss', this.localConfig)} directory.`);
|
|
28
|
+
}
|
|
20
29
|
this.log(`Pushing templates and parts to ${this.siteConfig.url}`);
|
|
21
30
|
this.log(`Templates path: ${templatesPath}, parts path: ${partsPath}`);
|
|
22
31
|
const { parts, problems, templates } = await readLocalTemplates(templatesPath, partsPath);
|
|
@@ -32,18 +41,37 @@ export default class Push extends PushCommand {
|
|
|
32
41
|
};
|
|
33
42
|
this.log(`Found ${pluralize(templates.length, 'template')}, ${pluralize(parts.length, 'part')}, ` +
|
|
34
43
|
`${css.style ? 'a global stylesheet' : 'no global stylesheet'} and ${pluralize(css.blocks.length, 'block stylesheet')}`);
|
|
44
|
+
this.warnEnvironmentUrls([
|
|
45
|
+
...templates.map((template) => ({ id: `templates/${template.slug}`, value: template.html })),
|
|
46
|
+
...parts.map((part) => ({ id: `parts/${part.slug}`, value: part.html })),
|
|
47
|
+
]);
|
|
48
|
+
// The PUT replaces every file of the child theme: what the site has under a path this
|
|
49
|
+
// environment excludes (loopress.json) is sent back as it is, so it is left alone.
|
|
50
|
+
const kept = await this.keptFromSite(templatesPath, partsPath, templates, parts);
|
|
35
51
|
if (this.dryRun) {
|
|
36
|
-
const changes = await this.previewChildTheme(templates, parts, css);
|
|
52
|
+
const changes = await this.previewChildTheme(templates, parts, css, kept);
|
|
37
53
|
logChanges((message) => { this.log(message); }, changes.templates, 'templates/');
|
|
38
54
|
logChanges((message) => { this.log(message); }, changes.parts, 'parts/');
|
|
39
55
|
logChanges((message) => { this.log(message); }, changes.css, 'css/');
|
|
56
|
+
if (this.localConfig.activateChildTheme) {
|
|
57
|
+
const { child } = (await this.capabilities()).theme;
|
|
58
|
+
if (child && !child.active)
|
|
59
|
+
this.log(`[dry-run] Would activate ${child.stylesheet}.`);
|
|
60
|
+
}
|
|
40
61
|
return { ...names, changes, status: 'dry-run' };
|
|
41
62
|
}
|
|
42
|
-
const child = await this.wp.put(CHILD_THEME_ENDPOINT, { ...css, parts, templates });
|
|
63
|
+
const child = await this.wp.put(CHILD_THEME_ENDPOINT, { ...css, parts: [...parts, ...kept.parts], templates: [...templates, ...kept.templates] });
|
|
43
64
|
await this.recordSuccess();
|
|
44
65
|
this.log(`Child theme ${child.stylesheet} written.`);
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
// Activated only when the project asks for it: a theme switch changes every per-theme setting.
|
|
67
|
+
let { active } = child;
|
|
68
|
+
if (!active && this.localConfig.activateChildTheme) {
|
|
69
|
+
const activation = await activateTheme(this.wp, child.stylesheet);
|
|
70
|
+
active = true;
|
|
71
|
+
this.log(`Activated ${child.stylesheet} (was ${activation.previous}), as loopress.json's activateChildTheme asks.`);
|
|
72
|
+
}
|
|
73
|
+
if (!active) {
|
|
74
|
+
this.warn(`${child.stylesheet} is not the active theme, so nothing changed on the site yet. Run \`lps theme activate\`, or set "activateChildTheme": true in loopress.json.`);
|
|
47
75
|
}
|
|
48
76
|
if (child.customized.length > 0) {
|
|
49
77
|
this.warn(`Edited in the Site Editor, their database copy still wins over the pushed file: ${child.customized.join(', ')}. ` +
|
|
@@ -57,22 +85,30 @@ export default class Push extends PushCommand {
|
|
|
57
85
|
}
|
|
58
86
|
return {
|
|
59
87
|
...names,
|
|
60
|
-
active
|
|
88
|
+
active,
|
|
61
89
|
customized: child.customized,
|
|
62
90
|
status: 'success',
|
|
63
91
|
stylesheet: child.stylesheet,
|
|
64
92
|
unknownBlocks,
|
|
65
93
|
};
|
|
66
94
|
}
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
// The site's templates and parts under an excluded path the project doesn't push. Refuses when
|
|
96
|
+
// the site can't be read: pushing blind would remove them.
|
|
97
|
+
async keptFromSite(templatesPath, partsPath, templates, parts) {
|
|
98
|
+
if (!hasExclusions())
|
|
99
|
+
return { parts: [], templates: [] };
|
|
100
|
+
const child = await this.readChildTheme();
|
|
101
|
+
const localTemplates = new Set(templates.map((template) => template.slug));
|
|
102
|
+
const localParts = new Set(parts.map((part) => part.slug));
|
|
103
|
+
return {
|
|
104
|
+
parts: child.parts.filter((part) => !localParts.has(part.slug) && isExcluded(join(partsPath, `${part.slug}.html`))),
|
|
105
|
+
templates: child.templates.filter((template) => !localTemplates.has(template.slug) && isExcluded(join(templatesPath, `${template.slug}.html`))),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Same read and same refusal as previewRemote, one GET for templates, parts and CSS. Kept
|
|
109
|
+
// files are held back, they are neither changes nor removals.
|
|
110
|
+
async previewChildTheme(templates, parts, css, kept) {
|
|
111
|
+
const child = await this.readChildTheme();
|
|
76
112
|
const localCss = new Map(css.blocks.map((block) => [`blocks/${block.name}.css`, sha256(block.css)]));
|
|
77
113
|
if (css.style !== '')
|
|
78
114
|
localCss.set('style.css', sha256(css.style));
|
|
@@ -81,14 +117,26 @@ export default class Push extends PushCommand {
|
|
|
81
117
|
: [...localCss.keys()].toSorted((a, b) => a.localeCompare(b)).map((id) => ({ id, status: 'unknown' }));
|
|
82
118
|
return {
|
|
83
119
|
css: cssChanges,
|
|
84
|
-
parts: pushChanges(bySlug(parts), bySlug(child.parts), { mirror: true }),
|
|
85
|
-
templates: pushChanges(bySlug(templates), bySlug(child.templates), { mirror: true }),
|
|
120
|
+
parts: withoutKept(pushChanges(bySlug([...parts, ...kept.parts]), bySlug(child.parts), { mirror: true }), kept.parts),
|
|
121
|
+
templates: withoutKept(pushChanges(bySlug([...templates, ...kept.templates]), bySlug(child.templates), { mirror: true }), kept.templates),
|
|
86
122
|
};
|
|
87
123
|
}
|
|
124
|
+
async readChildTheme() {
|
|
125
|
+
try {
|
|
126
|
+
return await this.wp.get(CHILD_THEME_ENDPOINT);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
this.error(`The site would refuse this push: ${errorMessage(error)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
88
132
|
}
|
|
89
133
|
function bySlug(files) {
|
|
90
134
|
return new Map(files.map(({ slug, ...rest }) => [slug, rest]));
|
|
91
135
|
}
|
|
136
|
+
function withoutKept(changes, kept) {
|
|
137
|
+
const slugs = new Set(kept.map((file) => file.slug));
|
|
138
|
+
return changes.filter((change) => !slugs.has(change.id));
|
|
139
|
+
}
|
|
92
140
|
function sha256(text) {
|
|
93
141
|
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
94
142
|
}
|
|
@@ -11,6 +11,9 @@ export default class Validate extends Command {
|
|
|
11
11
|
for (const problem of result.problems) {
|
|
12
12
|
this.log(` ✗ ${problem.file}: ${problem.message}`);
|
|
13
13
|
}
|
|
14
|
+
for (const warning of result.warnings) {
|
|
15
|
+
this.log(` ! ${warning.file}: ${warning.message}`);
|
|
16
|
+
}
|
|
14
17
|
this.log('');
|
|
15
18
|
this.log(result.valid
|
|
16
19
|
? `${result.checked} file(s) checked, no problems found.`
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const CONSOLE_AUTH: z.ZodObject<{
|
|
3
|
+
email: z.ZodOptional<z.ZodString>;
|
|
4
|
+
savedAt: z.ZodString;
|
|
5
|
+
token: z.ZodString;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export type ConsoleAuth = z.infer<typeof CONSOLE_AUTH>;
|
|
6
8
|
export declare class AuthManager {
|
|
7
|
-
private dataDir
|
|
8
|
-
constructor(dataDir?: string
|
|
9
|
+
private dataDir;
|
|
10
|
+
constructor(dataDir?: string);
|
|
9
11
|
clearAuth(): void;
|
|
10
12
|
getAuth(): ConsoleAuth | null;
|
|
11
13
|
getAuthFilePath(): string;
|
|
@@ -14,3 +16,4 @@ export declare class AuthManager {
|
|
|
14
16
|
private requireDataDir;
|
|
15
17
|
}
|
|
16
18
|
export declare const authManager: AuthManager;
|
|
19
|
+
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import { readJsonFile, writeJsonFileAtomic } from './json-file.js';
|
|
5
|
+
const CONSOLE_AUTH = z.object({ email: z.string().optional(), savedAt: z.string(), token: z.string() });
|
|
4
6
|
export class AuthManager {
|
|
5
7
|
dataDir;
|
|
6
8
|
constructor(dataDir) {
|
|
@@ -12,7 +14,9 @@ export class AuthManager {
|
|
|
12
14
|
unlinkSync(filePath);
|
|
13
15
|
}
|
|
14
16
|
getAuth() {
|
|
15
|
-
|
|
17
|
+
// A hand-edited or truncated auth.json reads as logged out, not as a token-less session.
|
|
18
|
+
const result = CONSOLE_AUTH.safeParse(readJsonFile(this.getAuthFilePath()));
|
|
19
|
+
return result.success ? result.data : null;
|
|
16
20
|
}
|
|
17
21
|
getAuthFilePath() {
|
|
18
22
|
return join(this.requireDataDir(), 'auth.json');
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function readJsonFile
|
|
1
|
+
export declare function readJsonFile(filePath: string): unknown;
|
|
2
2
|
export declare function writeJsonFileAtomic(filePath: string, data: unknown): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type EnvironmentConfig, type LoopressConfig, type ProjectConfig } from '../types/config.js';
|
|
2
2
|
export declare class ProjectConfigManager {
|
|
3
|
-
private configDir
|
|
4
|
-
constructor(configDir?: string
|
|
3
|
+
private configDir;
|
|
4
|
+
constructor(configDir?: string);
|
|
5
5
|
createProjectId(name: string): string;
|
|
6
6
|
ensureConfigDir(): void;
|
|
7
7
|
findProjectByApiId(apiProjectId: string): null | (ProjectConfig & {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { isRecord } from '../lib/narrow.js';
|
|
3
4
|
import { LOCAL_ENVIRONMENT_NAME } from '../lib/wp-client.js';
|
|
5
|
+
import {} from '../types/config.js';
|
|
4
6
|
import { toSlug } from '../utils/to-slug.js';
|
|
5
7
|
import { readJsonFile, writeJsonFileAtomic } from './json-file.js';
|
|
6
8
|
export class ProjectConfigManager {
|
|
@@ -55,10 +57,10 @@ export class ProjectConfigManager {
|
|
|
55
57
|
// remembered choice is exactly the hidden state that sends a push to the wrong site.
|
|
56
58
|
getDefaultEnvironment(projectId) {
|
|
57
59
|
const environments = this.getProject(projectId)?.environments ?? {};
|
|
58
|
-
const names = Object.keys(environments);
|
|
59
60
|
if (Object.hasOwn(environments, LOCAL_ENVIRONMENT_NAME))
|
|
60
|
-
return environments[LOCAL_ENVIRONMENT_NAME];
|
|
61
|
-
|
|
61
|
+
return environments[LOCAL_ENVIRONMENT_NAME] ?? null;
|
|
62
|
+
const [only, ...others] = Object.values(environments);
|
|
63
|
+
return others.length === 0 ? (only ?? null) : null;
|
|
62
64
|
}
|
|
63
65
|
getEnvironment(projectId, envName) {
|
|
64
66
|
const project = this.getProject(projectId);
|
|
@@ -165,10 +167,7 @@ export class ProjectConfigManager {
|
|
|
165
167
|
writeJsonFileAtomic(this.getConfigFilePath(), config);
|
|
166
168
|
}
|
|
167
169
|
isProjectConfig(value) {
|
|
168
|
-
|
|
169
|
-
return false;
|
|
170
|
-
const candidate = value;
|
|
171
|
-
return typeof candidate.name === 'string' && typeof candidate.environments === 'object' && candidate.environments !== null;
|
|
170
|
+
return isRecord(value) && typeof value.name === 'string' && isRecord(value.environments);
|
|
172
171
|
}
|
|
173
172
|
// Real CLI runs get this from the `init` hook (src/hooks/init.ts) before any command runs.
|
|
174
173
|
// Throwing when it's unset (rather than falling back to a hardcoded path) surfaces tests that
|
|
@@ -179,25 +178,21 @@ export class ProjectConfigManager {
|
|
|
179
178
|
return this.configDir;
|
|
180
179
|
}
|
|
181
180
|
sanitizeConfig(value) {
|
|
182
|
-
if (
|
|
181
|
+
if (!isRecord(value))
|
|
183
182
|
return { currentProject: null, projects: {} };
|
|
184
|
-
const
|
|
185
|
-
const telemetry = this.sanitizeTelemetry(candidate.telemetry);
|
|
183
|
+
const telemetry = this.sanitizeTelemetry(value.telemetry);
|
|
186
184
|
return {
|
|
187
|
-
currentProject: this.sanitizeCurrentProject(
|
|
188
|
-
projects: this.sanitizeProjects(
|
|
185
|
+
currentProject: this.sanitizeCurrentProject(value.currentProject),
|
|
186
|
+
projects: this.sanitizeProjects(value.projects),
|
|
189
187
|
...(telemetry && { telemetry }),
|
|
190
188
|
};
|
|
191
189
|
}
|
|
192
190
|
sanitizeCurrentProject(value) {
|
|
193
|
-
if (value === null || typeof value !== 'object')
|
|
194
|
-
return null;
|
|
195
|
-
const pointer = value;
|
|
196
191
|
// A legacy `env` field, from when switch also picked the environment, is dropped here.
|
|
197
|
-
return typeof
|
|
192
|
+
return isRecord(value) && typeof value.id === 'string' ? { id: value.id } : null;
|
|
198
193
|
}
|
|
199
194
|
sanitizeProjects(value) {
|
|
200
|
-
if (
|
|
195
|
+
if (!isRecord(value))
|
|
201
196
|
return {};
|
|
202
197
|
const projects = {};
|
|
203
198
|
for (const [id, project] of Object.entries(value)) {
|
|
@@ -207,10 +202,7 @@ export class ProjectConfigManager {
|
|
|
207
202
|
return projects;
|
|
208
203
|
}
|
|
209
204
|
sanitizeTelemetry(value) {
|
|
210
|
-
|
|
211
|
-
return undefined;
|
|
212
|
-
const candidate = value;
|
|
213
|
-
return typeof candidate.disabled === 'boolean' ? { disabled: candidate.disabled } : undefined;
|
|
205
|
+
return isRecord(value) && typeof value.disabled === 'boolean' ? { disabled: value.disabled } : undefined;
|
|
214
206
|
}
|
|
215
207
|
}
|
|
216
208
|
export const configManager = new ProjectConfigManager();
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import got from 'got';
|
|
1
|
+
import got, {} from 'got';
|
|
2
|
+
import { errorMessage, isRecord } from './narrow.js';
|
|
2
3
|
import { withSpinner } from './spinner.js';
|
|
3
|
-
import { REQUEST_TIMEOUT_MS } from './wp-client.js';
|
|
4
|
+
import { parseBody, REQUEST_TIMEOUT_MS, responseOf } from './wp-client.js';
|
|
4
5
|
export const API_URL = process.env.LPS_API_URL ?? 'https://api.loopress.dev';
|
|
5
6
|
/**
|
|
6
7
|
* HTTP client for the Loopress cloud API (projects, environments, credentials).
|
|
@@ -31,6 +32,8 @@ export class ApiClient {
|
|
|
31
32
|
async request(method, path, json) {
|
|
32
33
|
try {
|
|
33
34
|
const response = await withSpinner('Waiting for the Loopress API', async () => this.client(path, { json, method }));
|
|
35
|
+
// The API contract boundary: each endpoint's shape is the caller's `T`, not checked here.
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- see above
|
|
34
37
|
return (response.body ? JSON.parse(response.body) : undefined);
|
|
35
38
|
}
|
|
36
39
|
catch (error) {
|
|
@@ -39,41 +42,33 @@ export class ApiClient {
|
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
function formatApiError(error, url, timeoutMs) {
|
|
42
|
-
const
|
|
43
|
-
const status = err.response?.statusCode;
|
|
45
|
+
const { body, statusCode: status } = responseOf(error);
|
|
44
46
|
if (status === 401) {
|
|
45
47
|
return `Not logged in or session expired on ${url}. Run \`lps login\` again.`;
|
|
46
48
|
}
|
|
47
|
-
const reason = nestErrorMessage(
|
|
49
|
+
const reason = nestErrorMessage(body);
|
|
48
50
|
if (status === 403) {
|
|
49
51
|
// An empty body (not just a missing one) falls back to got's message too, never a bare ": ".
|
|
50
|
-
|
|
51
|
-
return `Request rejected (${status}) on ${url}: ${reason ?? body ?? err.message}`;
|
|
52
|
+
return `Request rejected (${status}) on ${url}: ${reason ?? (body === '' ? undefined : body) ?? errorMessage(error)}`;
|
|
52
53
|
}
|
|
53
54
|
// Any other API refusal (a 400 validation error, a 409 conflict) carries its explanation in
|
|
54
55
|
// the body; got's own message ("Request failed with status code 400") never does.
|
|
55
56
|
if (status !== undefined && reason) {
|
|
56
57
|
return `Request failed (${status}) on ${url}: ${reason}`;
|
|
57
58
|
}
|
|
58
|
-
if (
|
|
59
|
+
if (isRecord(error) && error.name === 'TimeoutError') {
|
|
59
60
|
return `Request timed out after ${timeoutMs / 1000}s on ${url}. Check your network connection, or the Loopress API may be temporarily unreachable.`;
|
|
60
61
|
}
|
|
61
|
-
return
|
|
62
|
+
return errorMessage(error);
|
|
62
63
|
}
|
|
63
64
|
// NestJS's HttpException body: `{statusCode, message, error}`, where `message` is a list for a
|
|
64
65
|
// ValidationPipe failure (one entry per invalid field).
|
|
65
66
|
function nestErrorMessage(body) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (Array.isArray(message) && message.length > 0)
|
|
73
|
-
return message.join('; ');
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
// not JSON (a proxy's HTML error page): nothing to extract
|
|
77
|
-
}
|
|
67
|
+
// Not JSON (a proxy's HTML error page) reads as no fields: nothing to extract.
|
|
68
|
+
const { message } = parseBody(body);
|
|
69
|
+
if (typeof message === 'string' && message.trim())
|
|
70
|
+
return message;
|
|
71
|
+
if (Array.isArray(message) && message.length > 0)
|
|
72
|
+
return message.join('; ');
|
|
78
73
|
return undefined;
|
|
79
74
|
}
|