@loopress/cli 0.32.0 → 0.32.2
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 +121 -121
- package/dist/lib/diff-command.js +1 -1
- package/dist/lib/push-command.js +10 -3
- package/dist/lib/resource-state.d.ts +1 -1
- package/dist/lib/resource-state.js +20 -6
- package/dist/utils/acf-format.d.ts +1 -0
- package/dist/utils/acf-format.js +5 -0
- package/dist/utils/php-top-level.js +9 -4
- package/dist/utils/theme-styles-format.js +29 -1
- package/oclif.manifest.json +436 -436
- package/package.json +1 -1
package/dist/lib/diff-command.js
CHANGED
|
@@ -40,7 +40,7 @@ export class DiffCommand extends LoopressCommand {
|
|
|
40
40
|
return {
|
|
41
41
|
left: async () => provider.remote(this.wp, sides.warn, dir),
|
|
42
42
|
resource: provider.resource,
|
|
43
|
-
right: sides.againstWp ? async () => provider.remote(sides.againstWp, sides.warn, dir) : async () => provider.local(dir, sides.warn),
|
|
43
|
+
right: sides.againstWp ? async () => provider.remote(sides.againstWp, sides.warn, dir) : async () => provider.local(dir, sides.warn, this.wp),
|
|
44
44
|
title: provider.title,
|
|
45
45
|
};
|
|
46
46
|
}
|
package/dist/lib/push-command.js
CHANGED
|
@@ -65,7 +65,7 @@ export class PushCommand extends LoopressCommand {
|
|
|
65
65
|
const remote = await this.previewRemote(provider, dir);
|
|
66
66
|
await this.guardOutsideEdits(provider, dir, remote, writes);
|
|
67
67
|
try {
|
|
68
|
-
const local = await provider.local(dir, () => { });
|
|
68
|
+
const local = await provider.local(dir, () => { }, this.wp);
|
|
69
69
|
this.previewStates = { local, remote };
|
|
70
70
|
this.previewChanges = pushChanges(local, remote, { writes });
|
|
71
71
|
}
|
|
@@ -236,12 +236,19 @@ export class PushCommand extends LoopressCommand {
|
|
|
236
236
|
const snapshot = await readSnapshot(resolve(process.cwd(), this.rootDir), provider.resource, this.siteConfig.name);
|
|
237
237
|
expected = recordToState(snapshot.afterState);
|
|
238
238
|
pushedAt = snapshot.createdAt;
|
|
239
|
-
local = await provider.local(dir, () => { });
|
|
240
239
|
}
|
|
241
240
|
catch {
|
|
242
|
-
// No earlier push from this machine
|
|
241
|
+
// No earlier push from this machine.
|
|
243
242
|
return;
|
|
244
243
|
}
|
|
244
|
+
// Unreadable files are skipped with a warning, so a failure here is the site's (ACF's
|
|
245
|
+
// normalize route): the check can't run, and a push must not go on unchecked.
|
|
246
|
+
try {
|
|
247
|
+
local = await provider.local(dir, () => { }, this.wp);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
this.error(`Could not check for changes made on ${this.siteConfig.url} outside Loopress: ${error.message}. Retry, or pass --overwrite.`);
|
|
251
|
+
}
|
|
245
252
|
const overwritten = [...local.keys()].filter((id) => (writes?.(id) ?? true) &&
|
|
246
253
|
remote.get(id) !== undefined &&
|
|
247
254
|
!sameEntry(remote, expected, id) &&
|
|
@@ -3,7 +3,7 @@ import { type ResourceState } from './diff-state.js';
|
|
|
3
3
|
import { type WpClient } from './wp-client.js';
|
|
4
4
|
export type ResourceStateProvider = {
|
|
5
5
|
dirKind: ResourceDirKind;
|
|
6
|
-
local(dir: string, onWarn: (message: string) => void): Promise<ResourceState>;
|
|
6
|
+
local(dir: string, onWarn: (message: string) => void, wp?: WpClient): Promise<ResourceState>;
|
|
7
7
|
remote(wp: WpClient, onWarn: (message: string) => void, dir: string): Promise<ResourceState>;
|
|
8
8
|
resource: string;
|
|
9
9
|
title: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, join, relative, sep } from 'node:path';
|
|
3
|
-
import { ACF_OBJECT_TYPES, acfEndpoint, getAcfKey } from '../utils/acf-format.js';
|
|
3
|
+
import { ACF_OBJECT_TYPES, acfEndpoint, acfNormalizeEndpoint, getAcfKey } from '../utils/acf-format.js';
|
|
4
4
|
import { CPT_SPEC, fileExtension, META_SPEC, parseDeclaredFile, PATTERN_SPEC, ROLE_SPEC, TAXONOMY_SPEC, } from '../utils/declared-type-format.js';
|
|
5
5
|
import { FORM_ENDPOINT, getFormId } from '../utils/form-format.js';
|
|
6
6
|
import { getMenuSlug, MENU_ENDPOINT, MENU_LOCATIONS_ENDPOINT } from '../utils/menu-format.js';
|
|
@@ -115,19 +115,33 @@ const formProvider = {
|
|
|
115
115
|
};
|
|
116
116
|
// ---- ACF --------------------------------------------------------------------------------
|
|
117
117
|
// ACF's export JSON is large, deeply nested, and versioned by ACF itself; `key` is the stable
|
|
118
|
-
// identity and the whole object (minus the `modified` timestamp)
|
|
119
|
-
//
|
|
120
|
-
//
|
|
118
|
+
// identity and the whole object (minus the `modified` timestamp) is compared. Ids are
|
|
119
|
+
// namespaced by object type (`field-groups/group_x`) since keys are only unique within a type.
|
|
120
|
+
// The site stores every setting, defaults included, while a hand- or agent-written file holds
|
|
121
|
+
// only what its author wrote: given the site, the local side is what the site would store
|
|
122
|
+
// (ACF's own normalization, nothing saved), or every such file would read as drift forever.
|
|
123
|
+
async function normalizeAcf(wp, type, objects) {
|
|
124
|
+
try {
|
|
125
|
+
return await wp.post(acfNormalizeEndpoint(type), { objects });
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
// A plugin older than the route: compared as written, as before it existed.
|
|
129
|
+
if (isNotFoundError(error))
|
|
130
|
+
return objects;
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
121
134
|
const acfProvider = {
|
|
122
135
|
dirKind: 'acf',
|
|
123
|
-
async local(dir, onWarn) {
|
|
136
|
+
async local(dir, onWarn, wp) {
|
|
124
137
|
const state = new Map();
|
|
125
138
|
for (const type of ACF_OBJECT_TYPES) {
|
|
126
|
-
const
|
|
139
|
+
const files = await loadFiles(join(dir, type), {
|
|
127
140
|
extension: '.json',
|
|
128
141
|
onSkip: onWarn,
|
|
129
142
|
parse: readJson,
|
|
130
143
|
});
|
|
144
|
+
const objects = wp && files.length > 0 ? await normalizeAcf(wp, type, files) : files;
|
|
131
145
|
for (const object of objects) {
|
|
132
146
|
const key = getAcfKey(object);
|
|
133
147
|
if (key !== null)
|
|
@@ -2,6 +2,7 @@ export declare const ACF_OBJECT_TYPES: readonly ["field-groups", "post-types", "
|
|
|
2
2
|
export type AcfObjectType = (typeof ACF_OBJECT_TYPES)[number];
|
|
3
3
|
export declare function acfEndpoint(type: AcfObjectType): string;
|
|
4
4
|
export declare function acfObjectEndpoint(type: AcfObjectType, key: string): string;
|
|
5
|
+
export declare function acfNormalizeEndpoint(type: AcfObjectType): string;
|
|
5
6
|
export declare function getAcfKey(data: Record<string, unknown>): null | string;
|
|
6
7
|
export declare function acfFieldChanges(before: unknown, after: unknown): {
|
|
7
8
|
added: string[];
|
package/dist/utils/acf-format.js
CHANGED
|
@@ -9,6 +9,11 @@ export function acfEndpoint(type) {
|
|
|
9
9
|
export function acfObjectEndpoint(type, key) {
|
|
10
10
|
return `${acfEndpoint(type)}/${key}`;
|
|
11
11
|
}
|
|
12
|
+
// POST route returning local objects the way the site would store them, defaults filled in,
|
|
13
|
+
// nothing saved (see resource-state.ts's acfProvider).
|
|
14
|
+
export function acfNormalizeEndpoint(type) {
|
|
15
|
+
return `loopress/v1/acf/normalize/${type}`;
|
|
16
|
+
}
|
|
12
17
|
// Deliberately loose: ACF's own export JSON is large, deeply nested, and versioned by ACF
|
|
13
18
|
// itself (new settings appear across ACF releases). We only need `key` for filenames/identity;
|
|
14
19
|
// everything else round-trips through pull/push untouched, so there's no generated type for it
|
|
@@ -34,10 +34,8 @@ export function startsWithStrictTypes(content) {
|
|
|
34
34
|
// Advances the brace depth and "a statement starts here" flag past one token; returns the token
|
|
35
35
|
// when it starts a top-level statement that isn't a declaration.
|
|
36
36
|
function step(state, token) {
|
|
37
|
-
if (token.type === 'html')
|
|
38
|
-
state
|
|
39
|
-
return token.text === '' ? null : 'inline HTML after ?>';
|
|
40
|
-
}
|
|
37
|
+
if (token.type === 'html')
|
|
38
|
+
return inlineHtml(state, token);
|
|
41
39
|
const statementStart = state.depth === 0 && state.atStart;
|
|
42
40
|
switch (token.text) {
|
|
43
41
|
case ';': {
|
|
@@ -66,6 +64,13 @@ function step(state, token) {
|
|
|
66
64
|
}
|
|
67
65
|
}
|
|
68
66
|
}
|
|
67
|
+
// A template inside a class or function body (`?> <p>...</p> <?php`) only runs when it is called.
|
|
68
|
+
function inlineHtml(state, token) {
|
|
69
|
+
if (state.depth > 0)
|
|
70
|
+
return null;
|
|
71
|
+
state.atStart = true;
|
|
72
|
+
return token.text === '' ? null : 'inline HTML after ?>';
|
|
73
|
+
}
|
|
69
74
|
function openBrace(state, statementStart) {
|
|
70
75
|
if (state.namespaceHeader) {
|
|
71
76
|
state.namespaceHeader = false;
|
|
@@ -78,6 +78,34 @@ export async function getActiveThemeGlobalStyles(wp) {
|
|
|
78
78
|
// The compared/stored value: `settings` and `styles` only. `id` and `_links` are WordPress
|
|
79
79
|
// bookkeeping, not part of the tracked configuration (a fresh install resolves to a different
|
|
80
80
|
// id for the same content), so they're dropped rather than becoming spurious drift.
|
|
81
|
+
// WordPress also rewrites two things on save, so both sides take its stored form, or a file
|
|
82
|
+
// pushed as is would read as drift right after its push:
|
|
83
|
+
// - a preset list (`palette: [...]`) is keyed by its origin, `palette: {custom: [...]}`, and
|
|
84
|
+
// - a `var:preset|color|base` reference becomes `var(--wp--preset--color--base)`.
|
|
81
85
|
export function canonicalGlobalStyles(record) {
|
|
82
|
-
return { settings: record.settings ?? {}, styles: record.styles ?? {} };
|
|
86
|
+
return { settings: expandVarReferences(unwrapPresets(record.settings ?? {})), styles: expandVarReferences(record.styles ?? {}) };
|
|
87
|
+
}
|
|
88
|
+
const PRESET_KEYS = new Set(['aspectRatios', 'duotone', 'fontFamilies', 'fontSizes', 'gradients', 'palette', 'presets', 'radiusSizes', 'spacingSizes']);
|
|
89
|
+
function unwrapPresets(value) {
|
|
90
|
+
if (typeof value !== 'object' || value === null)
|
|
91
|
+
return value;
|
|
92
|
+
if (Array.isArray(value))
|
|
93
|
+
return value.map((item) => unwrapPresets(item));
|
|
94
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
|
95
|
+
const custom = PRESET_KEYS.has(key) && isOnlyCustom(item) ? item.custom : item;
|
|
96
|
+
return [key, unwrapPresets(custom)];
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
function isOnlyCustom(value) {
|
|
100
|
+
return typeof value === 'object' && value !== null && Object.keys(value).length === 1 && Array.isArray(value.custom);
|
|
101
|
+
}
|
|
102
|
+
// WP_Theme_JSON::convert_custom_properties()'s rule.
|
|
103
|
+
function expandVarReferences(value) {
|
|
104
|
+
if (typeof value === 'string')
|
|
105
|
+
return value.startsWith('var:') ? `var(--wp--${value.slice(4).replaceAll('|', '--')})` : value;
|
|
106
|
+
if (typeof value !== 'object' || value === null)
|
|
107
|
+
return value;
|
|
108
|
+
if (Array.isArray(value))
|
|
109
|
+
return value.map((item) => expandVarReferences(item));
|
|
110
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, expandVarReferences(item)]));
|
|
83
111
|
}
|