@loopress/cli 0.32.2 → 0.33.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 +511 -154
- 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 +5 -4
- 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 +48 -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.js +4 -3
- package/dist/commands/diff.d.ts +1 -0
- package/dist/commands/diff.js +33 -3
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.js +37 -23
- 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 +2 -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 +2 -1
- 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 +2 -2
- package/dist/commands/plugin/status.js +3 -1
- package/dist/commands/project/config.js +7 -6
- 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.js +7 -2
- package/dist/commands/pull.d.ts +2 -0
- package/dist/commands/pull.js +42 -18
- package/dist/commands/push.d.ts +2 -1
- package/dist/commands/push.js +69 -25
- 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/push.js +7 -2
- package/dist/commands/theme/template/push.js +25 -6
- 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 +2 -1
- package/dist/lib/base.js +10 -8
- 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-type-command.js +4 -2
- package/dist/lib/dev-watch.js +5 -5
- package/dist/lib/diff-command.d.ts +2 -3
- package/dist/lib/diff-command.js +9 -7
- package/dist/lib/diff-state.js +2 -3
- package/dist/lib/load-files.js +2 -1
- package/dist/lib/load-snippets.js +37 -42
- 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 +8 -5
- package/dist/lib/plugin-detection.d.ts +0 -1
- package/dist/lib/plugin-detection.js +2 -4
- package/dist/lib/push-command.d.ts +1 -1
- package/dist/lib/push-command.js +10 -5
- package/dist/lib/put-or-create.d.ts +2 -2
- package/dist/lib/readdir-tolerant.js +3 -1
- package/dist/lib/resource-state.js +38 -13
- package/dist/lib/rollback-command.d.ts +1 -1
- package/dist/lib/rollback-command.js +3 -2
- package/dist/lib/rollback-materialize.js +28 -8
- 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.js +35 -20
- package/dist/lib/wp-client.d.ts +6 -1
- package/dist/lib/wp-client.js +54 -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 +9 -1
- 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 +27 -0
- package/dist/utils/block-format.js +93 -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 +7 -6
- 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 +9 -1
- 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 +1938 -1393
- package/package.json +42 -17
- package/schemas/global-config.schema.json +1 -1
- package/schemas/project-config.schema.json +11 -1
|
@@ -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
|
}
|
package/dist/lib/app-build.js
CHANGED
|
@@ -2,6 +2,8 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { isOneOf } from './narrow.js';
|
|
5
7
|
import { withSpinner } from './spinner.js';
|
|
6
8
|
// The package managers `lps app push` may spawn. An allowlist, not the raw `packageManager`
|
|
7
9
|
// field: the name ends up as a process to run.
|
|
@@ -13,6 +15,8 @@ const LOCKFILES = [
|
|
|
13
15
|
['bun.lockb', 'bun'],
|
|
14
16
|
['package-lock.json', 'npm'],
|
|
15
17
|
];
|
|
18
|
+
// Only the fields read here; the rest of package.json is the app's own business.
|
|
19
|
+
const PACKAGE_JSON = z.looseObject({ packageManager: z.string().optional(), scripts: z.record(z.string(), z.string()).optional() });
|
|
16
20
|
// How many trailing lines of build output a failure message carries.
|
|
17
21
|
const OUTPUT_TAIL_LINES = 20;
|
|
18
22
|
// `packageManager` field first (corepack's own source of truth), then the nearest lockfile
|
|
@@ -20,7 +24,7 @@ const OUTPUT_TAIL_LINES = 20;
|
|
|
20
24
|
// then npm.
|
|
21
25
|
export function detectPackageManager(appDir, packageManagerField) {
|
|
22
26
|
const fromField = packageManagerField?.split('@', 1)[0];
|
|
23
|
-
if (PACKAGE_MANAGERS
|
|
27
|
+
if (isOneOf(PACKAGE_MANAGERS, fromField))
|
|
24
28
|
return fromField;
|
|
25
29
|
for (let dir = appDir;; dir = dirname(dir)) {
|
|
26
30
|
const lockfile = LOCKFILES.find(([file]) => existsSync(join(dir, file)));
|
|
@@ -38,7 +42,7 @@ export async function buildApp(appDir, onStart) {
|
|
|
38
42
|
const packageJsonPath = join(appDir, 'package.json');
|
|
39
43
|
if (!existsSync(packageJsonPath))
|
|
40
44
|
return undefined;
|
|
41
|
-
const pkg = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
45
|
+
const pkg = PACKAGE_JSON.parse(JSON.parse(await readFile(packageJsonPath, 'utf8')));
|
|
42
46
|
if (!pkg.scripts?.build)
|
|
43
47
|
return undefined;
|
|
44
48
|
const pm = detectPackageManager(appDir, pkg.packageManager);
|
package/dist/lib/app-manifest.js
CHANGED
|
@@ -44,9 +44,9 @@ export function computeBuildId(files) {
|
|
|
44
44
|
}
|
|
45
45
|
function normalizeAssetUrl(url) {
|
|
46
46
|
let path = url.trim();
|
|
47
|
-
const schemeMatch = /^(?:[a-z]+:)?\/\/[^/]+(\/.*)$/i.exec(path);
|
|
47
|
+
const schemeMatch = /^(?:[a-z]+:)?\/\/[^/]+(\/.*)$/i.exec(path)?.[1];
|
|
48
48
|
if (schemeMatch)
|
|
49
|
-
path = schemeMatch
|
|
49
|
+
path = schemeMatch;
|
|
50
50
|
path = path.replace(/^\.?\//, '');
|
|
51
51
|
return path;
|
|
52
52
|
}
|
|
@@ -57,17 +57,17 @@ export function deriveEntry(indexHtml) {
|
|
|
57
57
|
for (const tag of indexHtml.match(/<script\b[^>]*>/gi) ?? []) {
|
|
58
58
|
if (!/\btype=["']module["']/i.test(tag))
|
|
59
59
|
continue;
|
|
60
|
-
const src = /\bsrc=["']([^"']+)["']/i.exec(tag);
|
|
60
|
+
const src = /\bsrc=["']([^"']+)["']/i.exec(tag)?.[1];
|
|
61
61
|
if (src)
|
|
62
|
-
scripts.push(normalizeAssetUrl(src
|
|
62
|
+
scripts.push(normalizeAssetUrl(src));
|
|
63
63
|
}
|
|
64
64
|
const styles = [];
|
|
65
65
|
for (const tag of indexHtml.match(/<link\b[^>]*>/gi) ?? []) {
|
|
66
66
|
if (!/\brel=["']stylesheet["']/i.test(tag))
|
|
67
67
|
continue;
|
|
68
|
-
const href = /\bhref=["']([^"']+)["']/i.exec(tag);
|
|
68
|
+
const href = /\bhref=["']([^"']+)["']/i.exec(tag)?.[1];
|
|
69
69
|
if (href)
|
|
70
|
-
styles.push(normalizeAssetUrl(href
|
|
70
|
+
styles.push(normalizeAssetUrl(href));
|
|
71
71
|
}
|
|
72
72
|
return { scripts, styles };
|
|
73
73
|
}
|
package/dist/lib/base.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
import { type EnvironmentConfig } from '../types/config.js';
|
|
3
3
|
import { type LoopressLocalConfig } from '../utils/loopress-config.js';
|
|
4
|
+
import { type SiteCapabilities } from './capabilities.js';
|
|
4
5
|
import { WpClient } from './wp-client.js';
|
|
5
6
|
export declare abstract class LoopressCommand extends Command {
|
|
6
7
|
static baseFlags: {
|
|
@@ -22,6 +23,7 @@ export declare abstract class LoopressCommand extends Command {
|
|
|
22
23
|
private wpClient?;
|
|
23
24
|
protected get rootDir(): string;
|
|
24
25
|
protected get wp(): WpClient;
|
|
26
|
+
protected capabilities(): Promise<SiteCapabilities>;
|
|
25
27
|
init(): Promise<void>;
|
|
26
28
|
protected maybeAutoRotate(): Promise<void>;
|
|
27
29
|
protected pullDirectory<T>(dir: string, items: T[], orphans: string[], options: {
|
|
@@ -42,7 +44,6 @@ export declare abstract class LoopressCommand extends Command {
|
|
|
42
44
|
protected resolveOptionsPath(override?: string): string;
|
|
43
45
|
protected resolveSnippetsPath(override?: string): string;
|
|
44
46
|
protected resolveThemeStylesPath(override?: string): string;
|
|
45
|
-
protected warnRemovedSeoModule(): void;
|
|
46
47
|
private pickEnvironment;
|
|
47
48
|
private resolveEnvironment;
|
|
48
49
|
}
|
package/dist/lib/base.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { confirm } from '@inquirer/prompts';
|
|
2
2
|
import { Command, Flags } from '@oclif/core';
|
|
3
3
|
import { Listr } from 'listr2';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
5
4
|
import { mkdir, rm } from 'node:fs/promises';
|
|
6
5
|
import { join } from 'node:path';
|
|
7
6
|
import { configManager } from '../config/project-config.manager.js';
|
|
7
|
+
import {} from '../types/config.js';
|
|
8
8
|
import { readLocalConfig } from '../utils/loopress-config.js';
|
|
9
9
|
import { pluralize } from '../utils/pluralize.js';
|
|
10
10
|
import { resolveResourceDir } from '../utils/resource-dirs.js';
|
|
11
|
+
import { getCapabilities } from './capabilities.js';
|
|
11
12
|
import { isInteractive } from './interactive.js';
|
|
12
13
|
import { isAppPasswordStale, rotateAppPassword } from './rotate-app-password.js';
|
|
13
14
|
import { withoutSpinner } from './spinner.js';
|
|
@@ -49,10 +50,18 @@ export class LoopressCommand extends Command {
|
|
|
49
50
|
}
|
|
50
51
|
return this.wpClient;
|
|
51
52
|
}
|
|
53
|
+
// What the targeted site can do, read once per process (see lib/capabilities.ts).
|
|
54
|
+
async capabilities() {
|
|
55
|
+
return getCapabilities(this.wp, this.siteConfig.name);
|
|
56
|
+
}
|
|
52
57
|
async init() {
|
|
53
58
|
await super.init();
|
|
59
|
+
// oclif types `this.ctor` as the base `typeof Command`, and parse() can't infer flags from a
|
|
60
|
+
// runtime-chosen subclass: both casts state what every LoopressCommand subclass declares.
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- see above
|
|
54
62
|
const { flags } = (await this.parse({
|
|
55
63
|
args: this.ctor.args,
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- see above
|
|
56
65
|
baseFlags: this.ctor.baseFlags,
|
|
57
66
|
enableJsonFlag: this.ctor.enableJsonFlag,
|
|
58
67
|
flags: this.ctor.flags,
|
|
@@ -178,13 +187,6 @@ export class LoopressCommand extends Command {
|
|
|
178
187
|
resolveThemeStylesPath(override) {
|
|
179
188
|
return resolveResourceDir('themeStyles', this.localConfig, override);
|
|
180
189
|
}
|
|
181
|
-
// `lps seo` was removed: a leftover seo/ folder or seoDir key would otherwise be silently
|
|
182
|
-
// ignored by `lps pull` / `lps push`. Remove once projects have had a release to migrate.
|
|
183
|
-
warnRemovedSeoModule() {
|
|
184
|
-
if (!('seoDir' in this.localConfig) && !existsSync(join(this.rootDir, 'seo')))
|
|
185
|
-
return;
|
|
186
|
-
this.warn('The SEO module was removed: seo/ is no longer pulled or pushed. Track SEO plugin settings with `lps option add rank-math-options-titles` (or `wpseo_titles`), then delete seo/ and any seoDir key from loopress.json.');
|
|
187
|
-
}
|
|
188
190
|
pickEnvironment(project, envName) {
|
|
189
191
|
const env = project.environments[envName];
|
|
190
192
|
if (!env) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { launchLocalBrowser } from './browser-launch.js';
|
|
2
2
|
import { downloadLatestFullZip } from './github-release.js';
|
|
3
|
+
import { errorMessage } from './narrow.js';
|
|
3
4
|
import { createTempAdmin, deleteTempAdmin } from './temp-admin.js';
|
|
5
|
+
import {} from './wp-client.js';
|
|
4
6
|
/**
|
|
5
7
|
* Installs and activates Loopress Full on a site that doesn't have it yet: downloads the
|
|
6
8
|
* latest release zip, creates a temporary admin (the only way to get `install_plugins` from
|
|
@@ -29,7 +31,7 @@ export async function bootstrapLoopressFull(wp, siteUrl, log) {
|
|
|
29
31
|
// default termination runs with the right exit code.
|
|
30
32
|
removeTempAdmin()
|
|
31
33
|
.catch((error) => {
|
|
32
|
-
log(`Could not remove the temporary admin account "${admin.username}"; remove it manually in wp-admin. (${error
|
|
34
|
+
log(`Could not remove the temporary admin account "${admin.username}"; remove it manually in wp-admin. (${errorMessage(error)})`);
|
|
33
35
|
})
|
|
34
36
|
.finally(() => {
|
|
35
37
|
process.removeListener('SIGINT', onSignal);
|
|
@@ -65,12 +67,12 @@ export async function bootstrapLoopressFull(wp, siteUrl, log) {
|
|
|
65
67
|
process.removeListener('SIGTERM', onSignal);
|
|
66
68
|
}
|
|
67
69
|
const manualFallback = `Install it manually: upload ${zipPath} at ${siteUrl}/wp-admin/plugin-install.php?tab=upload`;
|
|
68
|
-
if (installError && cleanupError) {
|
|
69
|
-
throw new Error(`Could not install Loopress Full automatically, and the temporary admin account could not be removed (${cleanupError
|
|
70
|
+
if (installError !== undefined && cleanupError !== undefined) {
|
|
71
|
+
throw new Error(`Could not install Loopress Full automatically, and the temporary admin account could not be removed (${errorMessage(cleanupError)}). ${manualFallback}`, { cause: installError });
|
|
70
72
|
}
|
|
71
|
-
if (cleanupError)
|
|
72
|
-
throw cleanupError;
|
|
73
|
-
if (installError) {
|
|
73
|
+
if (cleanupError !== undefined)
|
|
74
|
+
throw cleanupError instanceof Error ? cleanupError : new Error(errorMessage(cleanupError));
|
|
75
|
+
if (installError !== undefined) {
|
|
74
76
|
throw new Error(`Could not install Loopress Full automatically. ${manualFallback}`, { cause: installError });
|
|
75
77
|
}
|
|
76
78
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ResourceDirKind } from '../utils/resource-dirs.js';
|
|
2
|
+
import { type WpClient } from './wp-client.js';
|
|
3
|
+
export type SiteCapabilities = {
|
|
4
|
+
edition: 'full' | 'light';
|
|
5
|
+
environmentType: string;
|
|
6
|
+
integrations: {
|
|
7
|
+
acf: boolean;
|
|
8
|
+
form: boolean;
|
|
9
|
+
snippets?: boolean | undefined;
|
|
10
|
+
};
|
|
11
|
+
modules: string[];
|
|
12
|
+
theme: {
|
|
13
|
+
active: string;
|
|
14
|
+
child?: null | undefined | {
|
|
15
|
+
active: boolean;
|
|
16
|
+
exists: boolean;
|
|
17
|
+
stylesheet: string;
|
|
18
|
+
};
|
|
19
|
+
isBlock: boolean;
|
|
20
|
+
template: string;
|
|
21
|
+
};
|
|
22
|
+
version: string;
|
|
23
|
+
};
|
|
24
|
+
export declare const CAPABILITIES_ENDPOINT = "loopress/v1/capabilities";
|
|
25
|
+
export declare const THEME_ACTIVATE_ENDPOINT = "loopress/v1/theme/activate";
|
|
26
|
+
export declare function getCapabilities(wp: WpClient, site: string): Promise<SiteCapabilities>;
|
|
27
|
+
export type MissingIntegration = {
|
|
28
|
+
fix?: string;
|
|
29
|
+
needsFull?: true;
|
|
30
|
+
plugins?: string[];
|
|
31
|
+
reason: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function missingModule(module: string, what: string, { modules }: SiteCapabilities): MissingIntegration | undefined;
|
|
34
|
+
export declare function missingIntegration(kind: ResourceDirKind, capabilities: SiteCapabilities): MissingIntegration | undefined;
|
|
35
|
+
export declare function integrationFix(missing: MissingIntegration, declared: {
|
|
36
|
+
fromComposer: boolean;
|
|
37
|
+
plugins: string[];
|
|
38
|
+
}): string | undefined;
|
|
39
|
+
export type ThemeActivation = {
|
|
40
|
+
active: string;
|
|
41
|
+
changed: boolean;
|
|
42
|
+
previous: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function activateTheme(wp: WpClient, stylesheet: string): Promise<ThemeActivation>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {} from '../utils/resource-dirs.js';
|
|
2
|
+
import { isNotFoundError } from './wp-client.js';
|
|
3
|
+
export const CAPABILITIES_ENDPOINT = 'loopress/v1/capabilities';
|
|
4
|
+
export const THEME_ACTIVATE_ENDPOINT = 'loopress/v1/theme/activate';
|
|
5
|
+
// One request per site and per process: the aggregate commands ask about every resource.
|
|
6
|
+
const cache = new WeakMap();
|
|
7
|
+
// `site` names it in the error, the environment name or its URL.
|
|
8
|
+
export async function getCapabilities(wp, site) {
|
|
9
|
+
let capabilities = cache.get(wp);
|
|
10
|
+
if (!capabilities) {
|
|
11
|
+
const tooOld = `The Loopress plugin on ${site} is too old for this CLI. Update it, then try again.`;
|
|
12
|
+
capabilities = wp
|
|
13
|
+
.get(CAPABILITIES_ENDPOINT)
|
|
14
|
+
.catch((error) => {
|
|
15
|
+
if (isNotFoundError(error))
|
|
16
|
+
throw new Error(tooOld, { cause: error });
|
|
17
|
+
throw error;
|
|
18
|
+
})
|
|
19
|
+
.then((result) => {
|
|
20
|
+
// A plugin from before `modules` answers without it.
|
|
21
|
+
if (!Array.isArray(result.modules))
|
|
22
|
+
throw new Error(tooOld);
|
|
23
|
+
return result;
|
|
24
|
+
});
|
|
25
|
+
cache.set(wp, capabilities);
|
|
26
|
+
}
|
|
27
|
+
return capabilities;
|
|
28
|
+
}
|
|
29
|
+
// `what` needs the `module` feature of Loopress Full.
|
|
30
|
+
export function missingModule(module, what, { modules }) {
|
|
31
|
+
return modules.includes(module) ? undefined : { fix: 'Install Loopress Full', needsFull: true, reason: `${what} need Loopress Full` };
|
|
32
|
+
}
|
|
33
|
+
const needsModule = (module, what) => (capabilities) => missingModule(module, what, capabilities);
|
|
34
|
+
// The kinds of resource a site may be unable to hold. Every other kind needs nothing from it.
|
|
35
|
+
const INTEGRATION_CHECKS = {
|
|
36
|
+
acf: ({ integrations }) => (integrations.acf ? undefined : { plugins: ['advanced-custom-fields'], reason: 'ACF is not active' }),
|
|
37
|
+
api: needsModule('api', 'API routes'),
|
|
38
|
+
block: needsModule('blocks', 'dynamic blocks'),
|
|
39
|
+
blockCss: needsModule('pages', 'templates and parts'),
|
|
40
|
+
css: needsModule('pages', 'templates and parts'),
|
|
41
|
+
form: ({ integrations }) => (integrations.form ? undefined : { plugins: ['wpforms-lite'], reason: 'no supported form plugin is active' }),
|
|
42
|
+
hooks: needsModule('hooks', 'hooks'),
|
|
43
|
+
page: needsModule('pages', 'pages'),
|
|
44
|
+
part: needsModule('pages', 'templates and parts'),
|
|
45
|
+
pattern: needsModule('pages', 'block patterns'),
|
|
46
|
+
snippets(capabilities) {
|
|
47
|
+
const missing = missingModule('snippets', 'snippets', capabilities);
|
|
48
|
+
if (missing)
|
|
49
|
+
return missing;
|
|
50
|
+
return capabilities.integrations.snippets === true
|
|
51
|
+
? undefined
|
|
52
|
+
: { plugins: ['code-snippets', 'insert-headers-and-footers'], reason: 'no supported snippet plugin is active' };
|
|
53
|
+
},
|
|
54
|
+
template: needsModule('pages', 'templates and parts'),
|
|
55
|
+
themeStyles: ({ theme }) => (theme.isBlock ? undefined : { reason: `the active theme "${theme.active}" is a classic theme` }),
|
|
56
|
+
};
|
|
57
|
+
// Why the site can't hold this kind of resource, or undefined when it can (or the kind needs
|
|
58
|
+
// nothing from the site).
|
|
59
|
+
export function missingIntegration(kind, capabilities) {
|
|
60
|
+
return INTEGRATION_CHECKS[kind]?.(capabilities);
|
|
61
|
+
}
|
|
62
|
+
// How to fix a missing integration, given the plugins the project declares (composer.json, or
|
|
63
|
+
// loopress.json without one): declared but inactive needs activating, otherwise declaring.
|
|
64
|
+
export function integrationFix(missing, declared) {
|
|
65
|
+
if (missing.fix)
|
|
66
|
+
return missing.fix;
|
|
67
|
+
if (!missing.plugins)
|
|
68
|
+
return undefined;
|
|
69
|
+
// composer.json records no active state, loopress.json pins it per plugin.
|
|
70
|
+
const activate = (plugin) => declared.fromComposer ? '`lps plugin push --activate`' : `"${plugin}": {"active": true, ...} in loopress.json, then \`lps plugin push\``;
|
|
71
|
+
const plugin = missing.plugins.find((candidate) => declared.plugins.includes(candidate));
|
|
72
|
+
if (plugin)
|
|
73
|
+
return `The project declares ${plugin}, activate it: ${activate(plugin)}`;
|
|
74
|
+
const [first, ...alternatives] = missing.plugins;
|
|
75
|
+
if (first === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
const or = alternatives.length > 0 ? ` (or ${alternatives.join(', ')})` : '';
|
|
78
|
+
const where = declared.fromComposer ? `wpackagist-plugin/${first} in composer.json` : `"${first}" in loopress.json plugins`;
|
|
79
|
+
return `Declare ${first}${or} (${where}), then activate it: ${activate(first)}`;
|
|
80
|
+
}
|
|
81
|
+
export async function activateTheme(wp, stylesheet) {
|
|
82
|
+
const activation = await wp.post(THEME_ACTIVATE_ENDPOINT, { stylesheet });
|
|
83
|
+
cache.delete(wp);
|
|
84
|
+
return activation;
|
|
85
|
+
}
|
|
@@ -9,6 +9,8 @@ import { LoopressCommand } from './base.js';
|
|
|
9
9
|
import { basenameKey, findOrphanedFiles } from './find-orphaned-files.js';
|
|
10
10
|
import { isInteractive } from './interactive.js';
|
|
11
11
|
import { loadFiles } from './load-files.js';
|
|
12
|
+
import { errorMessage } from './narrow.js';
|
|
13
|
+
import {} from './php-files-command.js';
|
|
12
14
|
import { PushCommand } from './push-command.js';
|
|
13
15
|
import { getResourceStateProvider } from './resource-state.js';
|
|
14
16
|
import { isApplicative404, isNotFoundError } from './wp-client.js';
|
|
@@ -87,7 +89,7 @@ export function declaredPushCommand(spec, description) {
|
|
|
87
89
|
task.output = `Pushed: ${slug}`;
|
|
88
90
|
}
|
|
89
91
|
catch (error) {
|
|
90
|
-
this.reportTaskFailure(`Failed to push ${slug}: ${error
|
|
92
|
+
this.reportTaskFailure(`Failed to push ${slug}: ${errorMessage(error)}`, error, task);
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
}
|
|
@@ -179,7 +181,7 @@ export function declaredRmCommand(spec, description, keptNote) {
|
|
|
179
181
|
await this.wp.delete(itemEndpoint(spec, slug));
|
|
180
182
|
}
|
|
181
183
|
catch (error) {
|
|
182
|
-
const notFound = `${spec.noun
|
|
184
|
+
const notFound = `${spec.noun.charAt(0).toUpperCase()}${spec.noun.slice(1)} not found`;
|
|
183
185
|
if (isApplicative404(error, notFound))
|
|
184
186
|
this.error(`${spec.noun} ${slug} is not managed by Loopress on ${url}.`);
|
|
185
187
|
throw error;
|
package/dist/lib/dev-watch.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join, sep } from 'node:path';
|
|
3
|
+
import {} from '../utils/loopress-config.js';
|
|
3
4
|
import { resolveResourceDir } from '../utils/resource-dirs.js';
|
|
4
5
|
export const RESOURCE_TYPES = ['snippets', 'templates', 'parts', 'css', 'blocks', 'pages', 'api', 'hooks', 'plugins'];
|
|
5
6
|
// `only` narrows the default set, `skip` then removes from whatever that set is, same
|
|
@@ -13,18 +14,17 @@ export function resolveResourceTypes(only, skip = []) {
|
|
|
13
14
|
// directories which are all optional. `plugins` watches the whole `loopress.json` lockfile since
|
|
14
15
|
// the plugin manifest is a field in it, not its own directory.
|
|
15
16
|
export function buildWatchTargets(types, localConfig, cwd) {
|
|
16
|
-
const rootDir = join(cwd, localConfig.rootDir ?? '.');
|
|
17
17
|
const candidates = {
|
|
18
|
-
api: { commandId: 'api:push', path: join(
|
|
18
|
+
api: { commandId: 'api:push', path: join(cwd, resolveResourceDir('api', localConfig)), type: 'api' },
|
|
19
19
|
// The theme CSS is bundled into the child theme with the templates, by the same push.
|
|
20
20
|
blocks: { commandId: 'theme:template:push', path: join(cwd, resolveResourceDir('blockCss', localConfig)), type: 'blocks' },
|
|
21
21
|
css: { commandId: 'theme:template:push', path: join(cwd, resolveResourceDir('css', localConfig)), type: 'css' },
|
|
22
|
-
hooks: { commandId: 'hook:push', path: join(
|
|
23
|
-
pages: { commandId: 'page:push', path: join(
|
|
22
|
+
hooks: { commandId: 'hook:push', path: join(cwd, resolveResourceDir('hooks', localConfig)), type: 'hooks' },
|
|
23
|
+
pages: { commandId: 'page:push', path: join(cwd, resolveResourceDir('page', localConfig)), type: 'pages' },
|
|
24
24
|
plugins: { commandId: 'plugin:push', path: join(cwd, 'loopress.json'), type: 'plugins' },
|
|
25
25
|
parts: { commandId: 'theme:template:push', path: join(cwd, resolveResourceDir('part', localConfig)), type: 'parts' },
|
|
26
26
|
templates: { commandId: 'theme:template:push', path: join(cwd, resolveResourceDir('template', localConfig)), type: 'templates' },
|
|
27
|
-
snippets: { commandId: 'snippet:push', path: join(
|
|
27
|
+
snippets: { commandId: 'snippet:push', path: join(cwd, resolveResourceDir('snippets', localConfig)), type: 'snippets' },
|
|
28
28
|
};
|
|
29
29
|
return types
|
|
30
30
|
.map((type) => candidates[type])
|
|
@@ -16,12 +16,12 @@ export type DiffJson = {
|
|
|
16
16
|
};
|
|
17
17
|
export type DiffTarget = {
|
|
18
18
|
left: () => Promise<ResourceState>;
|
|
19
|
-
optional?: boolean;
|
|
20
19
|
resource: string;
|
|
21
20
|
right: () => Promise<ResourceState>;
|
|
21
|
+
skip?: boolean;
|
|
22
22
|
title: string;
|
|
23
23
|
};
|
|
24
|
-
type DiffSides = {
|
|
24
|
+
export type DiffSides = {
|
|
25
25
|
againstWp: undefined | WpClient;
|
|
26
26
|
leftLabel: string;
|
|
27
27
|
rightHeading: string;
|
|
@@ -48,4 +48,3 @@ export declare function resourceDiffCommand(resource: string, options: {
|
|
|
48
48
|
description: string;
|
|
49
49
|
pathNoun: string;
|
|
50
50
|
}): typeof DiffCommand;
|
|
51
|
-
export {};
|
package/dist/lib/diff-command.js
CHANGED
|
@@ -4,7 +4,7 @@ import { configManager } from '../config/project-config.manager.js';
|
|
|
4
4
|
import { resolveResourceDir } from '../utils/resource-dirs.js';
|
|
5
5
|
import { LoopressCommand } from './base.js';
|
|
6
6
|
import { compareStates, isEmptyDiff } from './diff-state.js';
|
|
7
|
-
import {
|
|
7
|
+
import { errorMessage } from './narrow.js';
|
|
8
8
|
import { composerLocalState, composerRemoteState, getResourceStateProvider } from './resource-state.js';
|
|
9
9
|
import { WpClient } from './wp-client.js';
|
|
10
10
|
const c = ux.colorize;
|
|
@@ -23,10 +23,11 @@ export class DiffCommand extends LoopressCommand {
|
|
|
23
23
|
// than being prefixed with the cwd, so Composer is read from the same place as the
|
|
24
24
|
// file-backed resources.
|
|
25
25
|
const localRoot = resolve(process.cwd(), this.rootDir);
|
|
26
|
+
const { againstWp } = sides;
|
|
26
27
|
return {
|
|
27
28
|
left: async () => composerRemoteState(this.wp),
|
|
28
29
|
resource: 'composer',
|
|
29
|
-
right:
|
|
30
|
+
right: againstWp ? async () => composerRemoteState(againstWp) : async () => composerLocalState(localRoot),
|
|
30
31
|
title: 'Composer',
|
|
31
32
|
};
|
|
32
33
|
}
|
|
@@ -37,10 +38,11 @@ export class DiffCommand extends LoopressCommand {
|
|
|
37
38
|
this.error('A [PATH] argument cannot be combined with --against: --against compares two environments, not local files.');
|
|
38
39
|
}
|
|
39
40
|
const dir = resolveResourceDir(provider.dirKind, this.localConfig, dirOverride);
|
|
41
|
+
const { againstWp } = sides;
|
|
40
42
|
return {
|
|
41
43
|
left: async () => provider.remote(this.wp, sides.warn, dir),
|
|
42
44
|
resource: provider.resource,
|
|
43
|
-
right:
|
|
45
|
+
right: againstWp ? async () => provider.remote(againstWp, sides.warn, dir) : async () => provider.local(dir, sides.warn, this.wp),
|
|
44
46
|
title: provider.title,
|
|
45
47
|
};
|
|
46
48
|
}
|
|
@@ -113,6 +115,9 @@ export class DiffCommand extends LoopressCommand {
|
|
|
113
115
|
// Fetches and compares one target. No output (so targets can run concurrently); `report`
|
|
114
116
|
// renders `stateDiff` afterwards in a fixed order.
|
|
115
117
|
async computeResource(target, sides) {
|
|
118
|
+
const empty = { added: [], changed: [], removed: [] };
|
|
119
|
+
if (target.skip)
|
|
120
|
+
return { drift: false, json: empty, resource: target.resource, skipped: true, title: target.title };
|
|
116
121
|
try {
|
|
117
122
|
const [leftState, rightState] = await Promise.all([target.left(), target.right()]);
|
|
118
123
|
const stateDiff = compareStates(leftState, rightState, { left: sides.leftLabel, right: sides.rightLabel });
|
|
@@ -125,10 +130,7 @@ export class DiffCommand extends LoopressCommand {
|
|
|
125
130
|
};
|
|
126
131
|
}
|
|
127
132
|
catch (error) {
|
|
128
|
-
const
|
|
129
|
-
const empty = { added: [], changed: [], removed: [] };
|
|
130
|
-
if (target.optional && INTEGRATION_MISSING.test(message))
|
|
131
|
-
return { drift: false, json: empty, resource: target.resource, skipped: true, title: target.title };
|
|
133
|
+
const message = errorMessage(error);
|
|
132
134
|
return { drift: false, json: { ...empty, error: message }, resource: target.resource, title: target.title };
|
|
133
135
|
}
|
|
134
136
|
}
|
package/dist/lib/diff-state.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { createTwoFilesPatch } from 'diff';
|
|
2
2
|
import microdiff from 'microdiff';
|
|
3
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import { isRecord } from './narrow.js';
|
|
4
5
|
const MAX_VALUE_LENGTH = 200;
|
|
5
|
-
function isRecord(value) {
|
|
6
|
-
return typeof value === 'object' && value !== null;
|
|
7
|
-
}
|
|
8
6
|
function formatValue(value) {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typed as string, but JSON.stringify returns undefined for `undefined` (a removed field)
|
|
9
8
|
const text = JSON.stringify(value) ?? String(value);
|
|
10
9
|
return text.length > MAX_VALUE_LENGTH ? `${text.slice(0, MAX_VALUE_LENGTH - 1)}…` : text;
|
|
11
10
|
}
|
package/dist/lib/load-files.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { extname, join } from 'node:path';
|
|
3
|
+
import { errorMessage } from './narrow.js';
|
|
3
4
|
import { readdirTolerant, walkFiles } from './readdir-tolerant.js';
|
|
4
5
|
// Loads every `<extension>` file of a directory through `parse`. One file is read in
|
|
5
6
|
// isolation: a corrupted or hand-broken file (unreadable, bad JSON, failed validation) must
|
|
@@ -15,7 +16,7 @@ export async function loadFiles(dir, options) {
|
|
|
15
16
|
items.push(options.parse(await readFile(filePath, 'utf8'), filePath));
|
|
16
17
|
}
|
|
17
18
|
catch (error) {
|
|
18
|
-
options.onSkip(`Skipping "${filePath}": ${error
|
|
19
|
+
options.onSkip(`Skipping "${filePath}": ${errorMessage(error)}`);
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
return items;
|