@loopress/cli 0.24.1 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +629 -88
- package/dist/commands/acf/push.d.ts +1 -0
- package/dist/commands/acf/push.js +25 -2
- package/dist/commands/acf/rollback.d.ts +2 -0
- package/dist/commands/acf/rollback.js +5 -0
- package/dist/commands/api/rollback.d.ts +2 -0
- package/dist/commands/api/rollback.js +5 -0
- package/dist/commands/diff.js +1 -1
- package/dist/commands/doctor.d.ts +17 -1
- package/dist/commands/doctor.js +55 -38
- package/dist/commands/form/pull.js +4 -1
- package/dist/commands/form/push.d.ts +1 -0
- package/dist/commands/form/push.js +29 -3
- package/dist/commands/form/rollback.d.ts +2 -0
- package/dist/commands/form/rollback.js +5 -0
- package/dist/commands/hook/rollback.d.ts +2 -0
- package/dist/commands/hook/rollback.js +5 -0
- package/dist/commands/init.js +1 -0
- package/dist/commands/menu/diff.d.ts +2 -0
- package/dist/commands/menu/diff.js +5 -0
- package/dist/commands/menu/list.d.ts +13 -0
- package/dist/commands/menu/list.js +32 -0
- package/dist/commands/menu/pull.d.ts +15 -0
- package/dist/commands/menu/pull.js +72 -0
- package/dist/commands/menu/push.d.ts +17 -0
- package/dist/commands/menu/push.js +120 -0
- package/dist/commands/menu/rollback.d.ts +2 -0
- package/dist/commands/menu/rollback.js +5 -0
- package/dist/commands/option/add.js +1 -1
- package/dist/commands/option/push.d.ts +1 -0
- package/dist/commands/option/push.js +26 -1
- package/dist/commands/option/rollback.d.ts +2 -0
- package/dist/commands/option/rollback.js +5 -0
- package/dist/commands/plugin/push.d.ts +1 -0
- package/dist/commands/plugin/push.js +14 -0
- package/dist/commands/promote.d.ts +8 -1
- package/dist/commands/promote.js +2 -0
- package/dist/commands/pull.d.ts +11 -1
- package/dist/commands/pull.js +9 -2
- package/dist/commands/push.d.ts +11 -1
- package/dist/commands/push.js +9 -2
- package/dist/commands/seo/pull.js +6 -2
- package/dist/commands/seo/push.d.ts +1 -0
- package/dist/commands/seo/push.js +33 -3
- package/dist/commands/seo/rollback.d.ts +2 -0
- package/dist/commands/seo/rollback.js +5 -0
- package/dist/commands/snippet/push.d.ts +1 -0
- package/dist/commands/snippet/push.js +31 -1
- package/dist/commands/snippet/rollback.d.ts +2 -0
- package/dist/commands/snippet/rollback.js +5 -0
- package/dist/commands/theme-styles/diff.d.ts +2 -0
- package/dist/commands/theme-styles/diff.js +5 -0
- package/dist/commands/theme-styles/pull.d.ts +12 -0
- package/dist/commands/theme-styles/pull.js +31 -0
- package/dist/commands/theme-styles/push.d.ts +12 -0
- package/dist/commands/theme-styles/push.js +45 -0
- package/dist/commands/theme-styles/rollback.d.ts +2 -0
- package/dist/commands/theme-styles/rollback.js +5 -0
- package/dist/lib/app-manifest.js +4 -27
- package/dist/lib/base.d.ts +2 -0
- package/dist/lib/base.js +6 -0
- package/dist/lib/find-orphaned-files.js +5 -47
- package/dist/lib/load-files.js +4 -42
- package/dist/lib/php-files-command.d.ts +1 -0
- package/dist/lib/php-files-command.js +84 -47
- package/dist/lib/push-command.d.ts +4 -0
- package/dist/lib/push-command.js +49 -0
- package/dist/lib/readdir-tolerant.d.ts +1 -0
- package/dist/lib/readdir-tolerant.js +12 -0
- package/dist/lib/resource-state.js +92 -3
- package/dist/lib/rollback-command.d.ts +21 -0
- package/dist/lib/rollback-command.js +180 -0
- package/dist/lib/rollback-materialize.d.ts +1 -0
- package/dist/lib/rollback-materialize.js +121 -0
- package/dist/lib/snapshot-store.d.ts +30 -0
- package/dist/lib/snapshot-store.js +133 -0
- package/dist/lib/validate-local.js +2 -1
- package/dist/types/project-config.generated.d.ts +8 -0
- package/dist/utils/acf-format.d.ts +1 -0
- package/dist/utils/acf-format.js +6 -0
- package/dist/utils/form-format.d.ts +4 -0
- package/dist/utils/form-format.js +3 -7
- package/dist/utils/menu-format.d.ts +24 -0
- package/dist/utils/menu-format.js +8 -0
- package/dist/utils/option-format.d.ts +1 -0
- package/dist/utils/resource-dirs.d.ts +2 -0
- package/dist/utils/resource-dirs.js +4 -0
- package/dist/utils/seo-format.d.ts +8 -0
- package/dist/utils/seo-format.js +3 -0
- package/dist/utils/snippet-format.d.ts +1 -0
- package/dist/utils/snippet-format.js +1 -0
- package/dist/utils/theme-styles-format.d.ts +26 -0
- package/dist/utils/theme-styles-format.js +55 -0
- package/oclif.manifest.json +1736 -465
- package/package.json +4 -2
- package/schemas/project-config.schema.json +10 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const ACF_OBJECT_TYPES: readonly ["field-groups", "post-types", "taxonomies", "options-pages"];
|
|
2
2
|
export type AcfObjectType = (typeof ACF_OBJECT_TYPES)[number];
|
|
3
3
|
export declare function acfEndpoint(type: AcfObjectType): string;
|
|
4
|
+
export declare function acfObjectEndpoint(type: AcfObjectType, key: string): string;
|
|
4
5
|
export declare function getAcfKey(data: Record<string, unknown>): null | string;
|
package/dist/utils/acf-format.js
CHANGED
|
@@ -2,6 +2,12 @@ export const ACF_OBJECT_TYPES = ['field-groups', 'post-types', 'taxonomies', 'op
|
|
|
2
2
|
export function acfEndpoint(type) {
|
|
3
3
|
return `loopress/v1/acf/${type}`;
|
|
4
4
|
}
|
|
5
|
+
// GET/DELETE route for a single object by key, used by `acf push` to read the object's current
|
|
6
|
+
// revision right before writing it (see acf/push.ts, #234). Create/update both go through
|
|
7
|
+
// acfEndpoint() (a POST), the controller resolves create-vs-update server-side via `key`.
|
|
8
|
+
export function acfObjectEndpoint(type, key) {
|
|
9
|
+
return `${acfEndpoint(type)}/${key}`;
|
|
10
|
+
}
|
|
5
11
|
// Deliberately loose: ACF's own export JSON is large, deeply nested, and versioned by ACF
|
|
6
12
|
// itself (new settings appear across ACF releases). We only need `key` for filenames/identity;
|
|
7
13
|
// everything else round-trips through pull/push untouched, so there's no generated type for it
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export declare const FORM_ENDPOINT = "loopress/v1/forms";
|
|
2
|
+
export declare function formEndpoint(id: number): string;
|
|
3
|
+
export type RemoteForm = Record<string, unknown> & {
|
|
4
|
+
revision: string;
|
|
5
|
+
};
|
|
2
6
|
export declare function getFormId(data: Record<string, unknown>): null | number;
|
|
3
7
|
export declare function getFormTitle(data: Record<string, unknown>): string;
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
export const FORM_ENDPOINT = 'loopress/v1/forms';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// Snippets/WPCode split) is large, deeply nested, and versioned by whichever plugin release
|
|
6
|
-
// wrote it. We only need `id` for filenames/identity and `settings.form_title` for display
|
|
7
|
-
// (the field WPForms itself uses; a future provider may differ), everything else round-trips
|
|
8
|
-
// through pull/push untouched.
|
|
2
|
+
export function formEndpoint(id) {
|
|
3
|
+
return `${FORM_ENDPOINT}/${id}`;
|
|
4
|
+
}
|
|
9
5
|
export function getFormId(data) {
|
|
10
6
|
const id = Number(data.id);
|
|
11
7
|
return Number.isSafeInteger(id) && id > 0 ? id : null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const MENU_ENDPOINT = "loopress/v1/menus";
|
|
2
|
+
export declare const MENU_LOCATIONS_ENDPOINT = "loopress/v1/menu-locations";
|
|
3
|
+
export declare function menuEndpoint(slug: string): string;
|
|
4
|
+
export type MenuItem = {
|
|
5
|
+
children: MenuItem[];
|
|
6
|
+
classes: string[];
|
|
7
|
+
description: string;
|
|
8
|
+
object: null | string;
|
|
9
|
+
objectSlug: null | string;
|
|
10
|
+
target: string;
|
|
11
|
+
title: string;
|
|
12
|
+
type: 'custom' | 'post_type' | 'taxonomy';
|
|
13
|
+
url: null | string;
|
|
14
|
+
xfn: string;
|
|
15
|
+
};
|
|
16
|
+
export type Menu = {
|
|
17
|
+
items: MenuItem[];
|
|
18
|
+
name: string;
|
|
19
|
+
revision: string;
|
|
20
|
+
slug: string;
|
|
21
|
+
warnings: string[];
|
|
22
|
+
};
|
|
23
|
+
export type MenuLocations = Record<string, null | string>;
|
|
24
|
+
export declare function getMenuSlug(data: Record<string, unknown>): null | string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const MENU_ENDPOINT = 'loopress/v1/menus';
|
|
2
|
+
export const MENU_LOCATIONS_ENDPOINT = 'loopress/v1/menu-locations';
|
|
3
|
+
export function menuEndpoint(slug) {
|
|
4
|
+
return `${MENU_ENDPOINT}/${encodeURIComponent(slug)}`;
|
|
5
|
+
}
|
|
6
|
+
export function getMenuSlug(data) {
|
|
7
|
+
return typeof data.slug === 'string' && data.slug.trim() !== '' ? data.slug : null;
|
|
8
|
+
}
|
|
@@ -5,9 +5,11 @@ export declare const RESOURCE_DIR_DEFAULTS: {
|
|
|
5
5
|
readonly apps: "apps";
|
|
6
6
|
readonly form: "forms";
|
|
7
7
|
readonly hooks: "hooks";
|
|
8
|
+
readonly menu: "menus";
|
|
8
9
|
readonly options: "options";
|
|
9
10
|
readonly seo: "seo";
|
|
10
11
|
readonly snippets: "snippets";
|
|
12
|
+
readonly themeStyles: "theme";
|
|
11
13
|
};
|
|
12
14
|
export type ResourceDirKind = keyof typeof RESOURCE_DIR_DEFAULTS;
|
|
13
15
|
export declare function resolveResourceDir(kind: ResourceDirKind, localConfig: LoopressLocalConfig, override?: string): string;
|
|
@@ -7,9 +7,11 @@ export const RESOURCE_DIR_DEFAULTS = {
|
|
|
7
7
|
apps: 'apps',
|
|
8
8
|
form: 'forms',
|
|
9
9
|
hooks: 'hooks',
|
|
10
|
+
menu: 'menus',
|
|
10
11
|
options: 'options',
|
|
11
12
|
seo: 'seo',
|
|
12
13
|
snippets: 'snippets',
|
|
14
|
+
themeStyles: 'theme',
|
|
13
15
|
};
|
|
14
16
|
const CONFIG_KEY = {
|
|
15
17
|
acf: 'acfDir',
|
|
@@ -17,9 +19,11 @@ const CONFIG_KEY = {
|
|
|
17
19
|
apps: 'appsDir',
|
|
18
20
|
form: 'formDir',
|
|
19
21
|
hooks: 'hooksDir',
|
|
22
|
+
menu: 'menuDir',
|
|
20
23
|
options: 'optionsDir',
|
|
21
24
|
seo: 'seoDir',
|
|
22
25
|
snippets: 'snippetsDir',
|
|
26
|
+
themeStyles: 'themeStylesDir',
|
|
23
27
|
};
|
|
24
28
|
// Resolves where a resource's files live: an explicit override wins, otherwise the
|
|
25
29
|
// loopress.json `<kind>Dir` setting, otherwise the built-in default, all under `rootDir`.
|
|
@@ -3,11 +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 declare function seoPostMetaItemEndpoint(postType: string, slug: string): string;
|
|
6
7
|
export type SeoPostMeta = {
|
|
7
8
|
meta: Record<string, unknown>;
|
|
8
9
|
slug: string;
|
|
9
10
|
title: string;
|
|
10
11
|
};
|
|
12
|
+
export type RemoteSeoPostMeta = SeoPostMeta & {
|
|
13
|
+
revision: string;
|
|
14
|
+
};
|
|
15
|
+
export type RemoteSeoSettings = {
|
|
16
|
+
revision: string;
|
|
17
|
+
settings: Record<string, unknown>;
|
|
18
|
+
};
|
|
11
19
|
export type SeoRedirect = {
|
|
12
20
|
createdAt: string | undefined;
|
|
13
21
|
headerCode: number;
|
package/dist/utils/seo-format.js
CHANGED
|
@@ -9,6 +9,9 @@ export function seoRedirectEndpoint(id) {
|
|
|
9
9
|
export function seoPostMetaEndpoint(postType) {
|
|
10
10
|
return `loopress/v1/seo/post-meta/${postType}`;
|
|
11
11
|
}
|
|
12
|
+
export function seoPostMetaItemEndpoint(postType, slug) {
|
|
13
|
+
return `${seoPostMetaEndpoint(postType)}/${encodeURIComponent(slug)}`;
|
|
14
|
+
}
|
|
12
15
|
// The `<id>-<slug>.json` on-disk convention shared by `seo pull` and `seo push`.
|
|
13
16
|
export function redirectFileBase(redirect) {
|
|
14
17
|
return `${redirect.id}-${toSlug(redirect.urlTo, 'redirect')}`;
|
|
@@ -81,6 +81,7 @@ export function normalizeSnippet(data) {
|
|
|
81
81
|
location: parseLocation(data.location) ?? defaultLocationForType(parseType(data.type) ?? 'php'),
|
|
82
82
|
name: coerceString(data.name),
|
|
83
83
|
priority: resolvePriority(data.priority),
|
|
84
|
+
revision: typeof data.revision === 'string' ? data.revision : undefined,
|
|
84
85
|
shortcodeAttributes: Array.isArray(data.shortcodeAttributes) ? data.shortcodeAttributes.map(String) : [],
|
|
85
86
|
tags: Array.isArray(data.tags) ? data.tags : [],
|
|
86
87
|
type: parseType(data.type) ?? 'php',
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type WpClient } from '../lib/wp-client.js';
|
|
2
|
+
export declare const THEME_STYLES_FILE_SUFFIX = "-global-styles.json";
|
|
3
|
+
export declare function themeStylesFileName(stylesheet: string): string;
|
|
4
|
+
export declare function globalStylesEndpoint(id: number): string;
|
|
5
|
+
export type WpThemeSupportInfo = {
|
|
6
|
+
_links?: Record<string, Array<{
|
|
7
|
+
href: string;
|
|
8
|
+
}>>;
|
|
9
|
+
status: 'active' | 'inactive';
|
|
10
|
+
stylesheet: string;
|
|
11
|
+
theme_supports?: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
export declare function isBlockTheme(theme: Pick<WpThemeSupportInfo, 'theme_supports'>): boolean;
|
|
14
|
+
export declare function getUserGlobalStylesId(theme: Pick<WpThemeSupportInfo, '_links'>): null | number;
|
|
15
|
+
export declare function getActiveThemeGlobalStyles(wp: WpClient): Promise<{
|
|
16
|
+
id: number;
|
|
17
|
+
stylesheet: string;
|
|
18
|
+
}>;
|
|
19
|
+
export type GlobalStylesRecord = {
|
|
20
|
+
settings?: Record<string, unknown>;
|
|
21
|
+
styles?: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
export declare function canonicalGlobalStyles(record: {
|
|
24
|
+
settings?: unknown;
|
|
25
|
+
styles?: unknown;
|
|
26
|
+
}): Record<string, unknown>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Global Styles are the user's customizations from the Site Editor's "Styles" screen (colors,
|
|
2
|
+
// typography, spacing), stored by WordPress core in a `wp_global_styles` post, one per theme.
|
|
3
|
+
// This is separate from the theme's own theme.json file, which lives in the theme's repo and is
|
|
4
|
+
// out of scope here. Only block themes (Full Site Editing) have a Styles screen at all.
|
|
5
|
+
export const THEME_STYLES_FILE_SUFFIX = '-global-styles.json';
|
|
6
|
+
export function themeStylesFileName(stylesheet) {
|
|
7
|
+
return `${stylesheet}${THEME_STYLES_FILE_SUFFIX}`;
|
|
8
|
+
}
|
|
9
|
+
export function globalStylesEndpoint(id) {
|
|
10
|
+
return `wp/v2/global-styles/${id}`;
|
|
11
|
+
}
|
|
12
|
+
// WordPress core surfaces `wp_is_block_theme()` over the `wp/v2/themes` REST response as
|
|
13
|
+
// `theme_supports['block-templates']` (see `_add_default_theme_supports()` in
|
|
14
|
+
// wp-includes/theme.php): core adds that feature flag itself exactly when the theme has its own
|
|
15
|
+
// block templates. No Loopress-side controller is needed to detect this.
|
|
16
|
+
export function isBlockTheme(theme) {
|
|
17
|
+
return theme.theme_supports?.['block-templates'] === true;
|
|
18
|
+
}
|
|
19
|
+
// The `wp/v2/themes` list item links to its theme's editable `wp_global_styles` post under this
|
|
20
|
+
// core-defined relation rather than exposing the id as a plain field (the same relation
|
|
21
|
+
// @wordpress/e2e-test-utils-playwright's `getCurrentThemeGlobalStylesPostId()` reads). The href
|
|
22
|
+
// itself varies with the site's permalink structure (`?rest_route=/wp/v2/global-styles/123` vs
|
|
23
|
+
// `/wp-json/wp/v2/global-styles/123`), so the id is pulled out with a regex rather than assumed
|
|
24
|
+
// to be the URL's last path segment.
|
|
25
|
+
const USER_GLOBAL_STYLES_REL = 'wp:user-global-styles';
|
|
26
|
+
export function getUserGlobalStylesId(theme) {
|
|
27
|
+
const href = theme._links?.[USER_GLOBAL_STYLES_REL]?.[0]?.href;
|
|
28
|
+
if (!href)
|
|
29
|
+
return null;
|
|
30
|
+
const match = /\/wp\/v2\/global-styles\/(\d+)/.exec(href);
|
|
31
|
+
return match ? Number(match[1]) : null;
|
|
32
|
+
}
|
|
33
|
+
// Resolves the active theme's stylesheet slug and the id of its editable Global Styles post in
|
|
34
|
+
// one call, failing clearly (rather than with a raw REST error later) when the active theme
|
|
35
|
+
// isn't a block theme: classic themes have no Styles screen and no `wp_global_styles` post at all.
|
|
36
|
+
export async function getActiveThemeGlobalStyles(wp) {
|
|
37
|
+
const themes = await wp.get('wp/v2/themes');
|
|
38
|
+
const active = themes.find((theme) => theme.status === 'active');
|
|
39
|
+
if (!active)
|
|
40
|
+
throw new Error('Could not determine the active theme on WordPress.');
|
|
41
|
+
if (!isBlockTheme(active)) {
|
|
42
|
+
throw new Error(`Global Styles sync only supports block themes (Full Site Editing). The active theme "${active.stylesheet}" is a classic theme.`);
|
|
43
|
+
}
|
|
44
|
+
const id = getUserGlobalStylesId(active);
|
|
45
|
+
if (id === null) {
|
|
46
|
+
throw new Error(`Could not resolve the editable Global Styles post for "${active.stylesheet}". Requires an admin-capable account (edit_theme_options) on WordPress 5.9+.`);
|
|
47
|
+
}
|
|
48
|
+
return { id, stylesheet: active.stylesheet };
|
|
49
|
+
}
|
|
50
|
+
// The compared/stored value: `settings` and `styles` only. `id` and `_links` are WordPress
|
|
51
|
+
// bookkeeping, not part of the tracked configuration (a fresh install resolves to a different
|
|
52
|
+
// id for the same content), so they're dropped rather than becoming spurious drift.
|
|
53
|
+
export function canonicalGlobalStyles(record) {
|
|
54
|
+
return { settings: record.settings ?? {}, styles: record.styles ?? {} };
|
|
55
|
+
}
|