@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.
Files changed (84) hide show
  1. package/README.md +128 -62
  2. package/dist/commands/acf/list.js +2 -1
  3. package/dist/commands/acf/push.js +1 -1
  4. package/dist/commands/api/list.d.ts +7 -4
  5. package/dist/commands/api/list.js +3 -10
  6. package/dist/commands/api/publish.js +1 -1
  7. package/dist/commands/api/pull.d.ts +8 -1
  8. package/dist/commands/api/pull.js +13 -4
  9. package/dist/commands/api/push.d.ts +7 -1
  10. package/dist/commands/api/push.js +48 -18
  11. package/dist/commands/composer/init.js +6 -6
  12. package/dist/commands/composer/pull.d.ts +7 -1
  13. package/dist/commands/composer/pull.js +41 -4
  14. package/dist/commands/composer/push.d.ts +8 -1
  15. package/dist/commands/composer/push.js +3 -1
  16. package/dist/commands/dev.d.ts +13 -0
  17. package/dist/commands/dev.js +97 -0
  18. package/dist/commands/doctor.js +4 -4
  19. package/dist/commands/init.js +7 -7
  20. package/dist/commands/login.js +3 -3
  21. package/dist/commands/page/list.d.ts +2 -4
  22. package/dist/commands/page/list.js +3 -10
  23. package/dist/commands/page/pull.d.ts +13 -1
  24. package/dist/commands/page/pull.js +5 -2
  25. package/dist/commands/page/push.d.ts +11 -1
  26. package/dist/commands/page/push.js +25 -13
  27. package/dist/commands/plugin/add.js +3 -3
  28. package/dist/commands/plugin/pull.d.ts +7 -1
  29. package/dist/commands/plugin/pull.js +3 -1
  30. package/dist/commands/plugin/push.d.ts +9 -1
  31. package/dist/commands/plugin/push.js +13 -9
  32. package/dist/commands/project/config.js +8 -8
  33. package/dist/commands/project/list.js +2 -2
  34. package/dist/commands/project/push.js +5 -5
  35. package/dist/commands/project/switch.js +2 -2
  36. package/dist/commands/snippet/list.d.ts +3 -4
  37. package/dist/commands/snippet/list.js +3 -10
  38. package/dist/commands/snippet/pull.d.ts +13 -1
  39. package/dist/commands/snippet/pull.js +5 -2
  40. package/dist/commands/snippet/push.d.ts +11 -1
  41. package/dist/commands/snippet/push.js +15 -5
  42. package/dist/commands/status.d.ts +13 -1
  43. package/dist/commands/status.js +25 -18
  44. package/dist/config/auth.manager.d.ts +2 -2
  45. package/dist/config/project-config.manager.d.ts +1 -1
  46. package/dist/config/project-config.manager.js +5 -6
  47. package/dist/lib/base.d.ts +2 -2
  48. package/dist/lib/base.js +3 -2
  49. package/dist/lib/bootstrap-full-install.d.ts +1 -1
  50. package/dist/lib/dev-watch.d.ts +16 -0
  51. package/dist/lib/dev-watch.js +71 -0
  52. package/dist/lib/find-orphaned-files.d.ts +3 -2
  53. package/dist/lib/find-orphaned-files.js +40 -4
  54. package/dist/lib/github-release.js +1 -1
  55. package/dist/lib/interactive.js +1 -1
  56. package/dist/lib/load-files.d.ts +1 -0
  57. package/dist/lib/load-files.js +40 -11
  58. package/dist/lib/load-snippets.d.ts +1 -1
  59. package/dist/lib/load-snippets.js +11 -9
  60. package/dist/lib/local-callback-server.js +8 -6
  61. package/dist/lib/plugin-detection.d.ts +1 -1
  62. package/dist/lib/push-command.js +2 -2
  63. package/dist/lib/rotate-app-password.d.ts +1 -1
  64. package/dist/lib/rotate-app-password.js +1 -1
  65. package/dist/lib/temp-admin.d.ts +3 -3
  66. package/dist/lib/temp-admin.js +2 -2
  67. package/dist/lib/wp-authorize-flow.js +5 -1
  68. package/dist/lib/wp-client.d.ts +2 -2
  69. package/dist/lib/wp-client.js +11 -1
  70. package/dist/lib/wp-site-diagnostic.d.ts +9 -2
  71. package/dist/lib/wp-site-diagnostic.js +13 -23
  72. package/dist/types/plugin.d.ts +4 -4
  73. package/dist/types/snippet.d.ts +3 -3
  74. package/dist/utils/composer.d.ts +2 -2
  75. package/dist/utils/form-format.js +1 -1
  76. package/dist/utils/loopress-config.d.ts +1 -1
  77. package/dist/utils/page-format.js +2 -2
  78. package/dist/utils/plugins.d.ts +5 -5
  79. package/dist/utils/plugins.js +3 -3
  80. package/dist/utils/seo-format.d.ts +6 -6
  81. package/dist/utils/snippet-format.d.ts +2 -2
  82. package/dist/utils/snippet-format.js +10 -5
  83. package/oclif.manifest.json +153 -31
  84. 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
- const server = createServer(async (req, res) => {
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: (page, error) => finish(res, page, () => reject(error)),
32
- resolveWithPage: (page, value) => finish(res, page, () => resolve(value)),
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
@@ -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 proceed = await confirm({ default: true, message: `Push to production (${this.siteConfig.url})?` });
28
- if (!proceed) {
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' });
@@ -1,9 +1,9 @@
1
- import { WpClient } from './wp-client.js';
2
- export interface TempAdmin {
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
@@ -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 stillExists = await wp
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 (stillExists) {
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');
@@ -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
- interface RequestOptions {
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;
@@ -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
- return `Endpoint not found (404) on ${url}. Is the required plugin installed and up to date on the site?`;
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, WordPress too old) surface as an actionable
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, WordPress too old) surface as an actionable
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
- try {
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: `Could not reach ${siteUrl}/wp-admin/authorize-application.php. (${describe(error)})`,
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 };
@@ -1,14 +1,14 @@
1
- export interface InstalledPlugin {
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 interface WpNativePlugin {
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>;
@@ -1,5 +1,5 @@
1
- import { SnippetInsertMethod, SnippetLocation, SnippetType } from '../utils/snippet-format.js';
2
- export interface Snippet {
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
+ };
@@ -1,4 +1,4 @@
1
- export interface ComposerJson {
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.isInteger(id) && id > 0 ? id : null;
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.isInteger(id) && id > 0 ? id : null;
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 in data)
59
+ if (Object.hasOwn(data, field))
60
60
  meta[field] = data[field];
61
61
  }
62
62
  return meta;
@@ -1,5 +1,5 @@
1
- import { InstalledPlugin, PluginManifest, WpNativePlugin } from '../types/plugin.js';
2
- export interface PluginDiff {
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 interface MergeResult {
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;
@@ -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 in existing));
9
+ const added = Object.keys(incoming).filter((s) => !Object.hasOwn(existing, s));
10
10
  const updated = Object.keys(incoming)
11
- .filter((s) => s in existing && existing[s] !== incoming[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 interface SeoPostMeta {
6
+ export type SeoPostMeta = {
7
7
  meta: Record<string, unknown>;
8
8
  slug: string;
9
9
  title: string;
10
- }
11
- export interface SeoRedirect {
12
- createdAt: null | string;
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: null | string;
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 interface NormalizedSnippet {
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 = String(raw ?? '').toLowerCase();
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 = String(raw ?? '').toLowerCase();
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: String(data.code ?? ''),
73
- description: String(data.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: String(data.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 : [],