@loopress/cli 0.20.0 → 0.21.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 +128 -62
- package/dist/commands/acf/list.js +2 -1
- package/dist/commands/acf/push.js +1 -1
- package/dist/commands/api/list.d.ts +7 -4
- package/dist/commands/api/list.js +3 -10
- package/dist/commands/api/publish.js +1 -1
- package/dist/commands/api/pull.d.ts +8 -1
- package/dist/commands/api/pull.js +13 -4
- package/dist/commands/api/push.d.ts +7 -1
- package/dist/commands/api/push.js +48 -18
- package/dist/commands/composer/init.js +6 -6
- package/dist/commands/composer/pull.d.ts +7 -1
- package/dist/commands/composer/pull.js +41 -4
- package/dist/commands/composer/push.d.ts +8 -1
- package/dist/commands/composer/push.js +3 -1
- package/dist/commands/dev.d.ts +13 -0
- package/dist/commands/dev.js +97 -0
- package/dist/commands/doctor.js +4 -4
- package/dist/commands/init.js +7 -7
- package/dist/commands/login.js +3 -3
- package/dist/commands/page/list.d.ts +2 -4
- package/dist/commands/page/list.js +3 -10
- package/dist/commands/page/pull.d.ts +13 -1
- package/dist/commands/page/pull.js +5 -2
- package/dist/commands/page/push.d.ts +11 -1
- package/dist/commands/page/push.js +25 -13
- package/dist/commands/plugin/add.js +3 -3
- package/dist/commands/plugin/pull.d.ts +7 -1
- package/dist/commands/plugin/pull.js +3 -1
- package/dist/commands/plugin/push.d.ts +9 -1
- package/dist/commands/plugin/push.js +13 -9
- package/dist/commands/project/config.js +8 -8
- package/dist/commands/project/list.js +2 -2
- package/dist/commands/project/push.js +5 -5
- package/dist/commands/project/switch.js +2 -2
- package/dist/commands/snippet/list.d.ts +3 -4
- package/dist/commands/snippet/list.js +3 -10
- package/dist/commands/snippet/pull.d.ts +13 -1
- package/dist/commands/snippet/pull.js +5 -2
- package/dist/commands/snippet/push.d.ts +11 -1
- package/dist/commands/snippet/push.js +15 -5
- package/dist/commands/status.d.ts +13 -1
- package/dist/commands/status.js +25 -18
- package/dist/config/auth.manager.d.ts +2 -2
- package/dist/config/project-config.manager.d.ts +1 -1
- package/dist/config/project-config.manager.js +5 -6
- package/dist/lib/base.d.ts +2 -2
- package/dist/lib/base.js +3 -2
- package/dist/lib/bootstrap-full-install.d.ts +1 -1
- package/dist/lib/dev-watch.d.ts +16 -0
- package/dist/lib/dev-watch.js +71 -0
- package/dist/lib/find-orphaned-files.d.ts +3 -2
- package/dist/lib/find-orphaned-files.js +40 -4
- package/dist/lib/github-release.js +1 -1
- package/dist/lib/interactive.js +1 -1
- package/dist/lib/load-files.d.ts +1 -0
- package/dist/lib/load-files.js +40 -11
- package/dist/lib/load-snippets.d.ts +1 -1
- package/dist/lib/load-snippets.js +11 -9
- package/dist/lib/local-callback-server.js +8 -6
- package/dist/lib/plugin-detection.d.ts +1 -1
- package/dist/lib/push-command.js +2 -2
- package/dist/lib/rotate-app-password.d.ts +1 -1
- package/dist/lib/rotate-app-password.js +1 -1
- package/dist/lib/temp-admin.d.ts +3 -3
- package/dist/lib/temp-admin.js +2 -2
- package/dist/lib/wp-authorize-flow.js +5 -1
- package/dist/lib/wp-client.d.ts +2 -2
- package/dist/lib/wp-client.js +11 -1
- package/dist/lib/wp-site-diagnostic.d.ts +9 -2
- package/dist/lib/wp-site-diagnostic.js +13 -23
- package/dist/types/plugin.d.ts +4 -4
- package/dist/types/snippet.d.ts +3 -3
- package/dist/utils/composer.d.ts +2 -2
- package/dist/utils/form-format.js +1 -1
- package/dist/utils/loopress-config.d.ts +1 -1
- package/dist/utils/page-format.js +2 -2
- package/dist/utils/plugins.d.ts +5 -5
- package/dist/utils/plugins.js +3 -3
- package/dist/utils/seo-format.d.ts +6 -6
- package/dist/utils/snippet-format.d.ts +2 -2
- package/dist/utils/snippet-format.js +10 -5
- package/oclif.manifest.json +153 -31
- package/package.json +9 -8
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
1
2
|
import { createServer } from 'node:http';
|
|
2
3
|
import { openBrowser } from './open-browser.js';
|
|
3
4
|
async function readBody(req) {
|
|
@@ -13,7 +14,7 @@ function parseFormData(body) {
|
|
|
13
14
|
* Sends the user to a URL in their browser and catches the resulting redirect on a short-lived
|
|
14
15
|
* local server; this factors out the server setup, timeout, and browser-opening boilerplate.
|
|
15
16
|
*/
|
|
16
|
-
export function waitForLocalCallback(options) {
|
|
17
|
+
export async function waitForLocalCallback(options) {
|
|
17
18
|
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
|
|
18
19
|
return new Promise((resolve, reject) => {
|
|
19
20
|
function finish(res, page, settle) {
|
|
@@ -23,13 +24,13 @@ export function waitForLocalCallback(options) {
|
|
|
23
24
|
server.close();
|
|
24
25
|
settle();
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
async function handleIncoming(req, res) {
|
|
27
28
|
try {
|
|
28
29
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
29
30
|
const body = req.method === 'POST' ? parseFormData(await readBody(req)) : {};
|
|
30
31
|
options.handleRequest(url, {
|
|
31
|
-
rejectWithPage
|
|
32
|
-
resolveWithPage
|
|
32
|
+
rejectWithPage(page, error) { finish(res, page, () => { reject(error); }); },
|
|
33
|
+
resolveWithPage(page, value) { finish(res, page, () => { resolve(value); }); },
|
|
33
34
|
respondBadRequest(message) {
|
|
34
35
|
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
35
36
|
res.end(message);
|
|
@@ -41,9 +42,10 @@ export function waitForLocalCallback(options) {
|
|
|
41
42
|
res.writeHead(500);
|
|
42
43
|
res.end('Internal error');
|
|
43
44
|
server.close();
|
|
44
|
-
reject(error);
|
|
45
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
45
46
|
}
|
|
46
|
-
}
|
|
47
|
+
}
|
|
48
|
+
const server = createServer((req, res) => { void handleIncoming(req, res); });
|
|
47
49
|
server.on('error', (err) => {
|
|
48
50
|
clearTimeout(timer);
|
|
49
51
|
reject(err);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WpClient } from './wp-client.js';
|
|
1
|
+
import { type WpClient } from './wp-client.js';
|
|
2
2
|
/**
|
|
3
3
|
* Checks `wp/v2/plugins` directly rather than through `parseInstalledPlugins`
|
|
4
4
|
* (`src/utils/plugins.ts`), which deliberately filters Loopress's own plugin slugs out of its
|
package/dist/lib/push-command.js
CHANGED
|
@@ -24,8 +24,8 @@ export class PushCommand extends LoopressCommand {
|
|
|
24
24
|
this.refusedByGuard = true;
|
|
25
25
|
this.error('Target environment is "production". Pass --yes to confirm the push in a non-interactive run.');
|
|
26
26
|
}
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
27
|
+
const isProceed = await confirm({ default: true, message: `Push to production (${this.siteConfig.url})?` });
|
|
28
|
+
if (!isProceed) {
|
|
29
29
|
this.refusedByGuard = true;
|
|
30
30
|
this.error('Aborted.');
|
|
31
31
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EnvironmentConfig } from '../types/config.js';
|
|
1
|
+
import { type EnvironmentConfig } from '../types/config.js';
|
|
2
2
|
export declare const APP_PASSWORD_MAX_AGE_MS: number;
|
|
3
3
|
export declare function isAppPasswordStale(addedAt: string): boolean;
|
|
4
4
|
export declare function rotateAppPassword(env: EnvironmentConfig & {
|
|
@@ -7,7 +7,7 @@ export function isAppPasswordStale(addedAt) {
|
|
|
7
7
|
// Create the new credential, confirm it actually authenticates on its own, only then revoke
|
|
8
8
|
// the old one. Reversing this order risks locking the user out if the new one is somehow bad.
|
|
9
9
|
export async function rotateAppPassword(env) {
|
|
10
|
-
const [user] = env.token.split(':');
|
|
10
|
+
const [user] = env.token.split(':', 1);
|
|
11
11
|
const oldClient = new WpClient(env.url, env.token);
|
|
12
12
|
const old = await oldClient.get(`${APPLICATION_PASSWORDS_PATH}/introspect`);
|
|
13
13
|
const created = await oldClient.post(APPLICATION_PASSWORDS_PATH, { name: 'Loopress' });
|
package/dist/lib/temp-admin.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { WpClient } from './wp-client.js';
|
|
2
|
-
export
|
|
1
|
+
import { type WpClient } from './wp-client.js';
|
|
2
|
+
export type TempAdmin = {
|
|
3
3
|
id: number;
|
|
4
4
|
password: string;
|
|
5
5
|
username: string;
|
|
6
|
-
}
|
|
6
|
+
};
|
|
7
7
|
/**
|
|
8
8
|
* Creates a temporary WordPress administrator, the only way to obtain `install_plugins` when
|
|
9
9
|
* all the CLI has is an application password. `.invalid` is the RFC 2606 reserved TLD for
|
package/dist/lib/temp-admin.js
CHANGED
|
@@ -32,7 +32,7 @@ export async function deleteTempAdmin(wp, admin) {
|
|
|
32
32
|
catch (error) {
|
|
33
33
|
throw new Error(`Failed to remove the temporary admin account "${admin.username}" (id ${admin.id}) from the site. Remove it manually from wp-admin.`, { cause: error });
|
|
34
34
|
}
|
|
35
|
-
const
|
|
35
|
+
const isStillExists = await wp
|
|
36
36
|
.get(`wp/v2/users/${admin.id}`)
|
|
37
37
|
.then(() => true)
|
|
38
38
|
.catch((error) => {
|
|
@@ -40,7 +40,7 @@ export async function deleteTempAdmin(wp, admin) {
|
|
|
40
40
|
return false;
|
|
41
41
|
throw error;
|
|
42
42
|
});
|
|
43
|
-
if (
|
|
43
|
+
if (isStillExists) {
|
|
44
44
|
throw new Error(`Temporary admin account "${admin.username}" (id ${admin.id}) still exists after deletion. Remove it manually from wp-admin.`);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -16,7 +16,7 @@ import { renderResultPage, waitForLocalCallback } from './local-callback-server.
|
|
|
16
16
|
* Application Password to the local callback server.
|
|
17
17
|
* 6. The local server extracts the credentials and resolves the promise.
|
|
18
18
|
*/
|
|
19
|
-
export function authorizeWithBrowser(siteUrl, log) {
|
|
19
|
+
export async function authorizeWithBrowser(siteUrl, log) {
|
|
20
20
|
return waitForLocalCallback({
|
|
21
21
|
buildUrl(callbackBaseUrl) {
|
|
22
22
|
const relayUrl = 'https://api.loopress.dev/auth/wp-authorize';
|
|
@@ -31,7 +31,11 @@ export function authorizeWithBrowser(siteUrl, log) {
|
|
|
31
31
|
rejectWithPage(REJECTED_PAGE, new Error('Authorization rejected in WordPress.'));
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
+
// `||`, not `??`: an empty-string field counts as absent here, same as the `!password`
|
|
35
|
+
// check below, so falls through to the query-param fallback instead of stopping at "".
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
34
37
|
const password = body.password || url.searchParams.get('password') || '';
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
35
39
|
const userLogin = body.user_login || url.searchParams.get('user_login') || '';
|
|
36
40
|
if (!password || !userLogin) {
|
|
37
41
|
respondBadRequest('Missing password or user_login');
|
package/dist/lib/wp-client.d.ts
CHANGED
|
@@ -13,9 +13,9 @@ export declare class WpClient {
|
|
|
13
13
|
put<T = unknown>(path: string, json?: Record<string, unknown>, options?: RequestOptions): Promise<T>;
|
|
14
14
|
private request;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
type RequestOptions = {
|
|
17
17
|
timeoutMs?: number;
|
|
18
|
-
}
|
|
18
|
+
};
|
|
19
19
|
export declare function isNotFoundError(error: unknown): boolean;
|
|
20
20
|
export declare function isTimeoutError(error: unknown): boolean;
|
|
21
21
|
export declare function formatWpError(error: unknown, url: string, timeoutMs?: number): string;
|
package/dist/lib/wp-client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import got from 'got';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
2
3
|
export const REQUEST_TIMEOUT_MS = 30_000;
|
|
3
4
|
/**
|
|
4
5
|
* HTTP client for a WordPress site's REST API.
|
|
@@ -53,7 +54,10 @@ export function formatWpError(error, url, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
53
54
|
return `Authentication failed (${status}) on ${url}. Check your credentials with \`lps project config\`.`;
|
|
54
55
|
}
|
|
55
56
|
if (status === 404) {
|
|
56
|
-
|
|
57
|
+
const reason = extractServerErrorMessage(err.response?.body);
|
|
58
|
+
return reason
|
|
59
|
+
? `Request failed (404) on ${url}: ${reason}`
|
|
60
|
+
: `Endpoint not found (404) on ${url}. Is the required plugin installed and up to date on the site?`;
|
|
57
61
|
}
|
|
58
62
|
if (status !== undefined) {
|
|
59
63
|
const reason = extractServerErrorMessage(err.response?.body);
|
|
@@ -78,6 +82,12 @@ function extractServerErrorMessage(body) {
|
|
|
78
82
|
return undefined;
|
|
79
83
|
try {
|
|
80
84
|
const parsed = JSON.parse(body);
|
|
85
|
+
// WP core's own generic body for a route it never registered (e.g. a plugin too old to
|
|
86
|
+
// expose this endpoint yet) is never more useful than the "is the plugin up to date?"
|
|
87
|
+
// fallback below; surfacing it verbatim buries a version-skew mismatch behind text that
|
|
88
|
+
// reads like the endpoint doesn't exist at all, pointing the user in the wrong direction.
|
|
89
|
+
if (parsed.code === 'rest_no_route')
|
|
90
|
+
return undefined;
|
|
81
91
|
const reason = parsed.error ?? parsed.message;
|
|
82
92
|
const summary = typeof reason === 'string' && reason.trim() ? reason : undefined;
|
|
83
93
|
const detail = typeof parsed.output === 'string' && parsed.output.trim() ? parsed.output.trim() : undefined;
|
|
@@ -7,7 +7,14 @@ export type DiagnosticResult = {
|
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
9
|
* Pre-flight checks run before starting the browser authorization flow, so failures
|
|
10
|
-
* (unreachable site, blocked REST API,
|
|
11
|
-
* message instead of a confusing timeout once the browser is already open.
|
|
10
|
+
* (unreachable site, blocked REST API, Application Passwords disabled) surface as an
|
|
11
|
+
* actionable message instead of a confusing timeout once the browser is already open.
|
|
12
|
+
*
|
|
13
|
+
* Whether Application Passwords are enabled can only be read from the `wp-json/` index:
|
|
14
|
+
* WordPress adds `authentication['application-passwords']` there when the feature is on
|
|
15
|
+
* (WP core, rest_add_application_passwords_to_index). The authorize-application.php page
|
|
16
|
+
* can't be probed for this instead, it sits behind the admin login wall, so an unauthenticated
|
|
17
|
+
* request just gets redirected to wp-login.php and never reaches the check that would say
|
|
18
|
+
* whether the feature is disabled.
|
|
12
19
|
*/
|
|
13
20
|
export declare function diagnoseWpSite(siteUrl: string): Promise<DiagnosticResult>;
|
|
@@ -2,12 +2,20 @@ import got from 'got';
|
|
|
2
2
|
export const REQUEST_TIMEOUT_MS = 10_000;
|
|
3
3
|
/**
|
|
4
4
|
* Pre-flight checks run before starting the browser authorization flow, so failures
|
|
5
|
-
* (unreachable site, blocked REST API,
|
|
6
|
-
* message instead of a confusing timeout once the browser is already open.
|
|
5
|
+
* (unreachable site, blocked REST API, Application Passwords disabled) surface as an
|
|
6
|
+
* actionable message instead of a confusing timeout once the browser is already open.
|
|
7
|
+
*
|
|
8
|
+
* Whether Application Passwords are enabled can only be read from the `wp-json/` index:
|
|
9
|
+
* WordPress adds `authentication['application-passwords']` there when the feature is on
|
|
10
|
+
* (WP core, rest_add_application_passwords_to_index). The authorize-application.php page
|
|
11
|
+
* can't be probed for this instead, it sits behind the admin login wall, so an unauthenticated
|
|
12
|
+
* request just gets redirected to wp-login.php and never reaches the check that would say
|
|
13
|
+
* whether the feature is disabled.
|
|
7
14
|
*/
|
|
8
15
|
export async function diagnoseWpSite(siteUrl) {
|
|
16
|
+
let index;
|
|
9
17
|
try {
|
|
10
|
-
await got.get(`${siteUrl}/wp-json/`, { timeout: { request: REQUEST_TIMEOUT_MS } });
|
|
18
|
+
index = await got.get(`${siteUrl}/wp-json/`, { timeout: { request: REQUEST_TIMEOUT_MS } }).json();
|
|
11
19
|
}
|
|
12
20
|
catch (error) {
|
|
13
21
|
return {
|
|
@@ -15,28 +23,10 @@ export async function diagnoseWpSite(siteUrl) {
|
|
|
15
23
|
reason: `Could not reach the WordPress REST API at ${siteUrl}/wp-json/. The site may be unreachable, or a security plugin may be blocking it. (${describe(error)})`,
|
|
16
24
|
};
|
|
17
25
|
}
|
|
18
|
-
|
|
19
|
-
const response = await got.head(`${siteUrl}/wp-admin/authorize-application.php`, {
|
|
20
|
-
throwHttpErrors: false,
|
|
21
|
-
timeout: { request: REQUEST_TIMEOUT_MS },
|
|
22
|
-
});
|
|
23
|
-
if (response.statusCode === 404) {
|
|
24
|
-
return {
|
|
25
|
-
ok: false,
|
|
26
|
-
reason: `The application authorization page was not found on ${siteUrl}. This WordPress site may be older than 5.6, or the feature may be disabled by a plugin.`,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
if (response.statusCode >= 400) {
|
|
30
|
-
return {
|
|
31
|
-
ok: false,
|
|
32
|
-
reason: `The application authorization page at ${siteUrl}/wp-admin/authorize-application.php returned an error (HTTP ${response.statusCode}).`,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
26
|
+
if (!index?.authentication?.['application-passwords']) {
|
|
37
27
|
return {
|
|
38
28
|
ok: false,
|
|
39
|
-
reason: `
|
|
29
|
+
reason: `Application Passwords are not available on ${siteUrl}. The site may be older than WordPress 5.6, require HTTPS, or have the feature disabled by a plugin or filter.`,
|
|
40
30
|
};
|
|
41
31
|
}
|
|
42
32
|
return { ok: true };
|
package/dist/types/plugin.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type InstalledPlugin = {
|
|
2
2
|
active: boolean;
|
|
3
3
|
file: string;
|
|
4
4
|
name: string;
|
|
5
5
|
slug: string;
|
|
6
6
|
version: string;
|
|
7
|
-
}
|
|
8
|
-
export
|
|
7
|
+
};
|
|
8
|
+
export type WpNativePlugin = {
|
|
9
9
|
name: string;
|
|
10
10
|
plugin: string;
|
|
11
11
|
status: 'active' | 'inactive' | 'network-active';
|
|
12
12
|
version: string;
|
|
13
|
-
}
|
|
13
|
+
};
|
|
14
14
|
export type PluginManifest = Record<string, string>;
|
package/dist/types/snippet.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { SnippetInsertMethod, SnippetLocation, SnippetType } from '../utils/snippet-format.js';
|
|
2
|
-
export
|
|
1
|
+
import { type SnippetInsertMethod, type SnippetLocation, type SnippetType } from '../utils/snippet-format.js';
|
|
2
|
+
export type Snippet = {
|
|
3
3
|
active: boolean;
|
|
4
4
|
code: string;
|
|
5
5
|
id?: number;
|
|
@@ -11,4 +11,4 @@ export interface Snippet {
|
|
|
11
11
|
shortcodeAttributes: string[];
|
|
12
12
|
tags: string[];
|
|
13
13
|
type: SnippetType;
|
|
14
|
-
}
|
|
14
|
+
};
|
package/dist/utils/composer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type ComposerJson = {
|
|
2
2
|
config?: {
|
|
3
3
|
'allow-plugins'?: Record<string, boolean>;
|
|
4
4
|
};
|
|
@@ -12,7 +12,7 @@ export interface ComposerJson {
|
|
|
12
12
|
}>;
|
|
13
13
|
require?: Record<string, string>;
|
|
14
14
|
'require-dev'?: Record<string, string>;
|
|
15
|
-
}
|
|
15
|
+
};
|
|
16
16
|
export declare function readComposerJson(): Promise<ComposerJson | null>;
|
|
17
17
|
export declare function readComposerLock(): Promise<null | string>;
|
|
18
18
|
export declare function getComposerManagedSlugs(composerJson: ComposerJson): string[];
|
|
@@ -8,7 +8,7 @@ export const FORM_ENDPOINT = 'loopress/v1/forms';
|
|
|
8
8
|
// through pull/push untouched.
|
|
9
9
|
export function getFormId(data) {
|
|
10
10
|
const id = Number(data.id);
|
|
11
|
-
return Number.
|
|
11
|
+
return Number.isSafeInteger(id) && id > 0 ? id : null;
|
|
12
12
|
}
|
|
13
13
|
export function getFormTitle(data) {
|
|
14
14
|
const settings = data.settings;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LoopressProjectConfiguration } from '../types/project-config.generated.js';
|
|
1
|
+
import { type LoopressProjectConfiguration } from '../types/project-config.generated.js';
|
|
2
2
|
export type LoopressLocalConfig = LoopressProjectConfiguration;
|
|
3
3
|
export declare function readLocalConfig(): Promise<LoopressLocalConfig>;
|
|
4
4
|
export declare function writeLocalConfig(config: LoopressLocalConfig): Promise<void>;
|
|
@@ -7,7 +7,7 @@ export const PAGE_ENDPOINT = 'wp/v2/pages';
|
|
|
7
7
|
export const PAGE_LIST_QUERY = 'per_page=100';
|
|
8
8
|
export function getPageId(data) {
|
|
9
9
|
const id = Number(data.id);
|
|
10
|
-
return Number.
|
|
10
|
+
return Number.isSafeInteger(id) && id > 0 ? id : null;
|
|
11
11
|
}
|
|
12
12
|
// `title`/`content`/`excerpt` come back as `{raw, rendered}` from `?context=edit` (what `page
|
|
13
13
|
// pull` requests) but round-trip fine as plain strings too for a hand-created file, same
|
|
@@ -56,7 +56,7 @@ const PAGE_META_FIELDS = [
|
|
|
56
56
|
export function pickPageMeta(data) {
|
|
57
57
|
const meta = {};
|
|
58
58
|
for (const field of PAGE_META_FIELDS) {
|
|
59
|
-
if (field
|
|
59
|
+
if (Object.hasOwn(data, field))
|
|
60
60
|
meta[field] = data[field];
|
|
61
61
|
}
|
|
62
62
|
return meta;
|
package/dist/utils/plugins.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { InstalledPlugin, PluginManifest, WpNativePlugin } from '../types/plugin.js';
|
|
2
|
-
export
|
|
1
|
+
import { type InstalledPlugin, type PluginManifest, type WpNativePlugin } from '../types/plugin.js';
|
|
2
|
+
export type PluginDiff = {
|
|
3
3
|
toActivate: Array<{
|
|
4
4
|
file: string;
|
|
5
5
|
slug: string;
|
|
@@ -8,8 +8,8 @@ export interface PluginDiff {
|
|
|
8
8
|
slug: string;
|
|
9
9
|
}>;
|
|
10
10
|
upToDate: string[];
|
|
11
|
-
}
|
|
12
|
-
export
|
|
11
|
+
};
|
|
12
|
+
export type MergeResult = {
|
|
13
13
|
added: string[];
|
|
14
14
|
merged: PluginManifest;
|
|
15
15
|
updated: Array<{
|
|
@@ -17,7 +17,7 @@ export interface MergeResult {
|
|
|
17
17
|
slug: string;
|
|
18
18
|
to: string;
|
|
19
19
|
}>;
|
|
20
|
-
}
|
|
20
|
+
};
|
|
21
21
|
export declare function mergePluginManifest(existing: PluginManifest, incoming: PluginManifest): MergeResult;
|
|
22
22
|
export declare function parseInstalledPlugins(raw: WpNativePlugin[]): InstalledPlugin[];
|
|
23
23
|
export declare function diffPlugins(manifest: PluginManifest, installed: InstalledPlugin[]): PluginDiff;
|
package/dist/utils/plugins.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
const LOOPRESS_PLUGIN_SLUGS = new Set(['loopress', 'loopress-full', 'loopress-light']);
|
|
7
7
|
export function mergePluginManifest(existing, incoming) {
|
|
8
8
|
const merged = { ...existing, ...incoming };
|
|
9
|
-
const added = Object.keys(incoming).filter((s) => !(s
|
|
9
|
+
const added = Object.keys(incoming).filter((s) => !Object.hasOwn(existing, s));
|
|
10
10
|
const updated = Object.keys(incoming)
|
|
11
|
-
.filter((s) => s
|
|
11
|
+
.filter((s) => Object.hasOwn(existing, s) && existing[s] !== incoming[s])
|
|
12
12
|
.map((s) => ({ from: existing[s], slug: s, to: incoming[s] }));
|
|
13
13
|
return { added, merged, updated };
|
|
14
14
|
}
|
|
@@ -16,7 +16,7 @@ export function mergePluginManifest(existing, incoming) {
|
|
|
16
16
|
// single-file plugin) with the `.php` extension stripped; the WordPress.org slug is just the
|
|
17
17
|
// folder name (or the bare id itself for a single-file plugin).
|
|
18
18
|
function slugFromPluginFile(file) {
|
|
19
|
-
return file.split('/')[0];
|
|
19
|
+
return file.split('/', 1)[0];
|
|
20
20
|
}
|
|
21
21
|
export function parseInstalledPlugins(raw) {
|
|
22
22
|
return raw
|
|
@@ -3,19 +3,19 @@ export declare const SEO_SETTINGS_ENDPOINT = "loopress/v1/seo/settings";
|
|
|
3
3
|
export declare const SEO_REDIRECTS_ENDPOINT = "loopress/v1/seo/redirects";
|
|
4
4
|
export declare function seoRedirectEndpoint(id: number): string;
|
|
5
5
|
export declare function seoPostMetaEndpoint(postType: string): string;
|
|
6
|
-
export
|
|
6
|
+
export type SeoPostMeta = {
|
|
7
7
|
meta: Record<string, unknown>;
|
|
8
8
|
slug: string;
|
|
9
9
|
title: string;
|
|
10
|
-
}
|
|
11
|
-
export
|
|
12
|
-
createdAt:
|
|
10
|
+
};
|
|
11
|
+
export type SeoRedirect = {
|
|
12
|
+
createdAt: string | undefined;
|
|
13
13
|
headerCode: number;
|
|
14
14
|
hits: number;
|
|
15
15
|
id: number;
|
|
16
16
|
sources: unknown;
|
|
17
17
|
status: string;
|
|
18
|
-
updatedAt:
|
|
18
|
+
updatedAt: string | undefined;
|
|
19
19
|
urlTo: string;
|
|
20
|
-
}
|
|
20
|
+
};
|
|
21
21
|
export declare function redirectFileBase(redirect: SeoRedirect): string;
|
|
@@ -2,7 +2,7 @@ export declare const SNIPPETS_ENDPOINT = "loopress/v1/snippets";
|
|
|
2
2
|
export type SnippetType = 'css' | 'html' | 'js' | 'php' | 'text';
|
|
3
3
|
export type SnippetInsertMethod = 'auto' | 'shortcode';
|
|
4
4
|
export type SnippetLocation = 'admin' | 'body' | 'everywhere' | 'footer' | 'frontend' | 'header' | 'once';
|
|
5
|
-
export
|
|
5
|
+
export type NormalizedSnippet = {
|
|
6
6
|
active: boolean;
|
|
7
7
|
code: string;
|
|
8
8
|
description: string;
|
|
@@ -14,7 +14,7 @@ export interface NormalizedSnippet {
|
|
|
14
14
|
shortcodeAttributes: string[];
|
|
15
15
|
tags: string[];
|
|
16
16
|
type: SnippetType;
|
|
17
|
-
}
|
|
17
|
+
};
|
|
18
18
|
export declare function parseType(raw: unknown): null | SnippetType;
|
|
19
19
|
export declare function parseLocation(raw: unknown): null | SnippetLocation;
|
|
20
20
|
export declare function parseInsertMethod(raw: unknown): null | SnippetInsertMethod;
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
export const SNIPPETS_ENDPOINT = 'loopress/v1/snippets';
|
|
2
|
+
// Coerces server JSON that should be a string but, being unknown, could in principle be an
|
|
3
|
+
// object; a plain String() on that would print "[object Object]" instead of falling back.
|
|
4
|
+
function coerceString(value, fallback = '') {
|
|
5
|
+
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? String(value) : fallback;
|
|
6
|
+
}
|
|
2
7
|
export function parseType(raw) {
|
|
3
8
|
const valid = ['css', 'html', 'js', 'php', 'text'];
|
|
4
|
-
const value =
|
|
9
|
+
const value = coerceString(raw).toLowerCase();
|
|
5
10
|
return valid.includes(value) ? value : null;
|
|
6
11
|
}
|
|
7
12
|
const VALID_LOCATIONS = new Set(['admin', 'body', 'everywhere', 'footer', 'frontend', 'header', 'once']);
|
|
8
13
|
export function parseLocation(raw) {
|
|
9
|
-
const value =
|
|
14
|
+
const value = coerceString(raw).toLowerCase();
|
|
10
15
|
return VALID_LOCATIONS.has(value) ? value : null;
|
|
11
16
|
}
|
|
12
17
|
export function parseInsertMethod(raw) {
|
|
@@ -69,12 +74,12 @@ export function buildMetaFile(snippet) {
|
|
|
69
74
|
export function normalizeSnippet(data) {
|
|
70
75
|
return {
|
|
71
76
|
active: Boolean(data.active),
|
|
72
|
-
code:
|
|
73
|
-
description:
|
|
77
|
+
code: coerceString(data.code),
|
|
78
|
+
description: coerceString(data.description),
|
|
74
79
|
id: Number(data.id),
|
|
75
80
|
insertMethod: parseInsertMethod(data.insertMethod) ?? 'auto',
|
|
76
81
|
location: parseLocation(data.location) ?? defaultLocationForType(parseType(data.type) ?? 'php'),
|
|
77
|
-
name:
|
|
82
|
+
name: coerceString(data.name),
|
|
78
83
|
priority: resolvePriority(data.priority),
|
|
79
84
|
shortcodeAttributes: Array.isArray(data.shortcodeAttributes) ? data.shortcodeAttributes.map(String) : [],
|
|
80
85
|
tags: Array.isArray(data.tags) ? data.tags : [],
|