@loopress/cli 0.32.2 → 0.34.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 +549 -179
- 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 +7 -5
- 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 +53 -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.d.ts +2 -2
- package/dist/commands/dev.js +13 -18
- package/dist/commands/diff.d.ts +6 -3
- package/dist/commands/diff.js +63 -13
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.js +49 -25
- 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 +3 -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 +3 -1
- package/dist/commands/plugin/add.d.ts +1 -0
- package/dist/commands/plugin/add.js +50 -9
- 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 +41 -6
- package/dist/commands/plugin/status.js +3 -1
- package/dist/commands/project/config.js +7 -7
- 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.d.ts +1 -0
- package/dist/commands/promote.js +15 -6
- package/dist/commands/pull.d.ts +7 -0
- package/dist/commands/pull.js +67 -18
- package/dist/commands/push.d.ts +11 -1
- package/dist/commands/push.js +157 -48
- 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/diff.js +1 -1
- package/dist/commands/theme/style/push.js +8 -3
- package/dist/commands/theme/style/rollback.js +1 -1
- package/dist/commands/theme/template/push.d.ts +2 -0
- package/dist/commands/theme/template/push.js +66 -18
- package/dist/commands/validate.js +3 -0
- 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 +8 -1
- package/dist/lib/base.js +43 -9
- 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-plugins.d.ts +9 -0
- package/dist/lib/declared-plugins.js +21 -0
- package/dist/lib/declared-type-command.js +7 -2
- package/dist/lib/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +13 -15
- package/dist/lib/diff-command.d.ts +19 -3
- package/dist/lib/diff-command.js +132 -14
- package/dist/lib/diff-state.js +2 -3
- package/dist/lib/environment-urls.d.ts +6 -0
- package/dist/lib/environment-urls.js +59 -0
- package/dist/lib/exclude.d.ts +16 -0
- package/dist/lib/exclude.js +102 -0
- package/dist/lib/load-files.js +2 -1
- package/dist/lib/load-snippets.js +39 -43
- 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 +13 -8
- package/dist/lib/plugin-detection.d.ts +0 -1
- package/dist/lib/plugin-detection.js +2 -4
- package/dist/lib/push-command.d.ts +5 -1
- package/dist/lib/push-command.js +48 -7
- package/dist/lib/put-or-create.d.ts +2 -2
- package/dist/lib/readdir-tolerant.js +9 -4
- package/dist/lib/resource-selection.d.ts +9 -0
- package/dist/lib/resource-selection.js +49 -0
- package/dist/lib/resource-state.d.ts +2 -0
- package/dist/lib/resource-state.js +60 -14
- package/dist/lib/rollback-command.d.ts +1 -1
- package/dist/lib/rollback-command.js +4 -3
- package/dist/lib/rollback-materialize.js +29 -9
- 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.d.ts +1 -0
- package/dist/lib/validate-local.js +102 -28
- package/dist/lib/wp-client.d.ts +7 -1
- package/dist/lib/wp-client.js +60 -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 +24 -5
- 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 +29 -0
- package/dist/utils/block-format.js +109 -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 +9 -7
- 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 +13 -5
- 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 +2661 -1235
- package/package.json +45 -20
- package/schemas/global-config.schema.json +1 -1
- package/schemas/project-config.schema.json +32 -5
|
@@ -4,14 +4,18 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { dirname, join, relative, sep } from 'node:path';
|
|
6
6
|
import { authManager } from '../config/auth.manager.js';
|
|
7
|
+
import {} from '../types/config.js';
|
|
7
8
|
import { firstTopLevelSideEffect, startsWithStrictTypes } from '../utils/php-top-level.js';
|
|
8
9
|
import { pluralize } from '../utils/pluralize.js';
|
|
9
10
|
import { resolveResourceDir, RESOURCE_DIR_DEFAULTS } from '../utils/resource-dirs.js';
|
|
10
11
|
import { ApiClient } from './api-client.js';
|
|
11
12
|
import { LoopressCommand } from './base.js';
|
|
13
|
+
import {} from './diff-state.js';
|
|
14
|
+
import { isExcluded } from './exclude.js';
|
|
12
15
|
import { basenameKey, findOrphanedFiles } from './find-orphaned-files.js';
|
|
13
16
|
import { isInteractive } from './interactive.js';
|
|
14
17
|
import { loadFiles as loadDirectoryFiles } from './load-files.js';
|
|
18
|
+
import { errorMessage } from './narrow.js';
|
|
15
19
|
import { PushCommand } from './push-command.js';
|
|
16
20
|
import { getResourceStateProvider } from './resource-state.js';
|
|
17
21
|
import { isApplicative404 } from './wp-client.js';
|
|
@@ -82,7 +86,7 @@ export function resourcePushCommand(spec) {
|
|
|
82
86
|
static enableJsonFlag = true;
|
|
83
87
|
static examples = [
|
|
84
88
|
`$ lps ${spec.cliName} push`,
|
|
85
|
-
`$ lps ${spec.cliName} push
|
|
89
|
+
`$ lps ${spec.cliName} push ./${RESOURCE_DIR_DEFAULTS[spec.dirKind]}`,
|
|
86
90
|
`$ lps ${spec.cliName} push --prune`,
|
|
87
91
|
];
|
|
88
92
|
static flags = {
|
|
@@ -116,7 +120,7 @@ export function resourcePushCommand(spec) {
|
|
|
116
120
|
// confirmation), but their removal itself happens inside the same atomic swap as the push
|
|
117
121
|
// below, not as separate DELETE calls, so a mid-push failure can't remove server-side
|
|
118
122
|
// files without also landing the new ones.
|
|
119
|
-
const pruneList = flags.prune ? await this.resolvePruneList(new Set(files.map((file) => file.filename))) : [];
|
|
123
|
+
const pruneList = flags.prune ? await this.resolvePruneList(new Set(files.map((file) => file.filename)), path) : [];
|
|
120
124
|
if (this.dryRun) {
|
|
121
125
|
await this.writeAfterPushSnapshot(provider, path, beforeState);
|
|
122
126
|
return { changes: this.previewChanges, pruned: pruneList, pushed: files.map((file) => file.filename), status: 'dry-run' };
|
|
@@ -158,14 +162,14 @@ export function resourcePushCommand(spec) {
|
|
|
158
162
|
result = await this.wp.post(`${spec.endpoint}/batch`, body);
|
|
159
163
|
}
|
|
160
164
|
catch (error) {
|
|
161
|
-
this.error(`Push failed, nothing was changed on ${this.siteConfig.url}: ${error
|
|
165
|
+
this.error(`Push failed, nothing was changed on ${this.siteConfig.url}: ${errorMessage(error)}`);
|
|
162
166
|
}
|
|
163
167
|
for (const file of result.files) {
|
|
164
168
|
this.log(file.syntax_check === 'skipped'
|
|
165
169
|
? `Pushed: ${file.filename} (syntax check skipped, unavailable on this host)`
|
|
166
170
|
: `Pushed: ${file.filename}`);
|
|
167
171
|
if (file.public) {
|
|
168
|
-
this.warn(`${file.filename}: PUBLIC route, no authentication required. Anyone on the internet can call it. See https://loopress.dev/api/routes/#authentication-and-permissions`);
|
|
172
|
+
this.warn(`${file.filename}: PUBLIC route, no authentication required. Anyone on the internet can call it. See https://docs.loopress.dev/api/routes/#authentication-and-permissions`);
|
|
169
173
|
}
|
|
170
174
|
}
|
|
171
175
|
for (const filename of result.pruned)
|
|
@@ -178,9 +182,10 @@ export function resourcePushCommand(spec) {
|
|
|
178
182
|
// local path in CI must not silently wipe production routes, and unlike `pull`'s local
|
|
179
183
|
// deletions these are not recoverable from the repo. Only decides *which* filenames to
|
|
180
184
|
// prune; pushBatch() is what actually removes them, atomically alongside the push.
|
|
181
|
-
|
|
185
|
+
// A file excluded from this environment (loopress.json) is left alone, never pruned.
|
|
186
|
+
async resolvePruneList(localFilenames, path) {
|
|
182
187
|
const remote = await this.wp.get(spec.endpoint);
|
|
183
|
-
const orphans = remote.map((file) => file.filename).filter((name) => !localFilenames.has(name));
|
|
188
|
+
const orphans = remote.map((file) => file.filename).filter((name) => !localFilenames.has(name) && !isExcluded(join(path, `${name}.php`)));
|
|
184
189
|
if (orphans.length === 0)
|
|
185
190
|
return [];
|
|
186
191
|
const summary = `${pluralize(orphans.length, spec.noun)} on ${this.siteConfig.url} not present locally: ${orphans.join(', ')}`;
|
|
@@ -216,7 +221,7 @@ export function resourcePullCommand(spec) {
|
|
|
216
221
|
static args = pathArg(spec.pathNoun);
|
|
217
222
|
static description = spec.pullDescription;
|
|
218
223
|
static enableJsonFlag = true;
|
|
219
|
-
static examples = [`$ lps ${spec.cliName} pull`, `$ lps ${spec.cliName} pull
|
|
224
|
+
static examples = [`$ lps ${spec.cliName} pull`, `$ lps ${spec.cliName} pull ./${RESOURCE_DIR_DEFAULTS[spec.dirKind]}`];
|
|
220
225
|
static flags = { ...LoopressCommand.dryRunFlag, ...LoopressCommand.yesFlag };
|
|
221
226
|
async run() {
|
|
222
227
|
const { args } = await this.parse(ResourcePull);
|
|
@@ -294,7 +299,7 @@ export function resourceListCommand(spec) {
|
|
|
294
299
|
}
|
|
295
300
|
export function resourceRmCommand(spec) {
|
|
296
301
|
class ResourceRm extends LoopressCommand {
|
|
297
|
-
// `
|
|
302
|
+
// `rm` is canonical everywhere, `remove` stays an alias for existing scripts.
|
|
298
303
|
static aliases = [`${spec.cliName}:remove`];
|
|
299
304
|
static args = {
|
|
300
305
|
filename: Args.string({
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {} from '../types/plugin.js';
|
|
2
|
+
import {} from './wp-client.js';
|
|
1
3
|
const FULL_PLUGIN_PREFIX = 'loopress-full/';
|
|
2
4
|
/**
|
|
3
5
|
* Checks `wp/v2/plugins` directly rather than through `parseInstalledPlugins`
|
|
@@ -9,7 +11,3 @@ export async function isLoopressFullActive(wp) {
|
|
|
9
11
|
const plugins = await wp.get('wp/v2/plugins');
|
|
10
12
|
return plugins.some((plugin) => plugin.plugin.startsWith(FULL_PLUGIN_PREFIX) && plugin.status === 'active');
|
|
11
13
|
}
|
|
12
|
-
// How the Loopress plugin reports a resource's integration missing: "ACF is not active", "No
|
|
13
|
-
// supported form plugin is active", "No supported snippet plugin is active". Global Styles say a
|
|
14
|
-
// classic theme is active. The aggregate commands skip such a resource the project doesn't use.
|
|
15
|
-
export const INTEGRATION_MISSING = /\bis not active\b|\bNo supported .+ plugin is active\b|\bis a classic theme\b/;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LoopressCommand } from './base.js';
|
|
2
2
|
import { type ResourceState } from './diff-state.js';
|
|
3
|
+
import { type ScannedItem } from './environment-urls.js';
|
|
3
4
|
import { type ResourceStateProvider } from './resource-state.js';
|
|
4
5
|
export type PushChange = {
|
|
5
6
|
id: string;
|
|
@@ -7,7 +8,7 @@ export type PushChange = {
|
|
|
7
8
|
};
|
|
8
9
|
export declare function pushChanges(local: ResourceState, remote: ResourceState, options?: {
|
|
9
10
|
mirror?: boolean;
|
|
10
|
-
writes?: (id: string) => boolean;
|
|
11
|
+
writes?: ((id: string) => boolean) | undefined;
|
|
11
12
|
}): PushChange[];
|
|
12
13
|
export declare function logChanges(log: (message: string) => void, changes: Array<{
|
|
13
14
|
id: string;
|
|
@@ -19,6 +20,7 @@ export declare abstract class PushCommand extends LoopressCommand {
|
|
|
19
20
|
'production-confirmed': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
21
|
env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
22
|
};
|
|
23
|
+
static honorsExclude: boolean;
|
|
22
24
|
protected failedCount: number;
|
|
23
25
|
protected previewChanges: PushChange[];
|
|
24
26
|
protected previewStates?: {
|
|
@@ -28,6 +30,7 @@ export declare abstract class PushCommand extends LoopressCommand {
|
|
|
28
30
|
private refusedByGuard;
|
|
29
31
|
protected captureBeforePushState(provider: ResourceStateProvider, dir: string, writes?: (id: string) => boolean): Promise<ResourceState | undefined>;
|
|
30
32
|
catch(err: Error): Promise<void>;
|
|
33
|
+
finally(error: Error | undefined): Promise<void>;
|
|
31
34
|
protected guardProductionPush(): Promise<void>;
|
|
32
35
|
init(): Promise<void>;
|
|
33
36
|
protected previewRemote(provider: ResourceStateProvider, dir: string): Promise<ResourceState>;
|
|
@@ -40,6 +43,7 @@ export declare abstract class PushCommand extends LoopressCommand {
|
|
|
40
43
|
protected runPushTasks<T>(items: T[], label: (item: T) => string, task: (item: T, task?: {
|
|
41
44
|
output: string;
|
|
42
45
|
}) => Promise<void>): Promise<void>;
|
|
46
|
+
protected warnEnvironmentUrls(items: ScannedItem[]): void;
|
|
43
47
|
protected writeAfterPushSnapshot(provider: ResourceStateProvider, dir: string, beforeState: ResourceState | undefined): Promise<void>;
|
|
44
48
|
private guardOutsideEdits;
|
|
45
49
|
}
|
package/dist/lib/push-command.js
CHANGED
|
@@ -2,15 +2,37 @@ import { confirm } from '@inquirer/prompts';
|
|
|
2
2
|
import { Flags } from '@oclif/core';
|
|
3
3
|
import got from 'got';
|
|
4
4
|
import { Listr } from 'listr2';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
5
6
|
import { resolve } from 'node:path';
|
|
6
7
|
import { isDeepStrictEqual } from 'node:util';
|
|
7
8
|
import { authManager } from '../config/auth.manager.js';
|
|
9
|
+
import {} from '../types/config.js';
|
|
8
10
|
import { API_URL } from './api-client.js';
|
|
9
11
|
import { LoopressCommand } from './base.js';
|
|
12
|
+
import {} from './diff-state.js';
|
|
13
|
+
import { environmentUrlWarnings } from './environment-urls.js';
|
|
14
|
+
import { heldBackPaths } from './exclude.js';
|
|
10
15
|
import { guardProductionPush } from './guard-production-push.js';
|
|
11
16
|
import { isInteractive } from './interactive.js';
|
|
17
|
+
import { errorMessage } from './narrow.js';
|
|
18
|
+
import {} from './resource-state.js';
|
|
12
19
|
import { readSnapshot, recordToState, writeSnapshot } from './snapshot-store.js';
|
|
13
20
|
import { withoutSpinner, withSpinner } from './spinner.js';
|
|
21
|
+
// The Git state the deployment record carries: the HEAD commit and whether the working tree has
|
|
22
|
+
// uncommitted changes. Empty outside a Git repository (or without git), so nothing is sent then.
|
|
23
|
+
function gitState() {
|
|
24
|
+
try {
|
|
25
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path -- git is resolved from PATH like in every dev tool
|
|
26
|
+
const git = (args) => execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }).trim();
|
|
27
|
+
const commit = git(['rev-parse', 'HEAD']);
|
|
28
|
+
if (!commit)
|
|
29
|
+
return {};
|
|
30
|
+
return { commit, dirty: git(['status', '--porcelain', '--untracked-files=normal']) !== '' };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
14
36
|
export function pushChanges(local, remote, options = {}) {
|
|
15
37
|
const changes = [...local.keys()]
|
|
16
38
|
.filter((id) => options.writes?.(id) ?? true)
|
|
@@ -40,6 +62,7 @@ export class PushCommand extends LoopressCommand {
|
|
|
40
62
|
}),
|
|
41
63
|
'production-confirmed': Flags.boolean({ hidden: true }),
|
|
42
64
|
};
|
|
65
|
+
static honorsExclude = true;
|
|
43
66
|
failedCount = 0;
|
|
44
67
|
// A dry run's per-item preview, set by captureBeforePushState: what the push would create,
|
|
45
68
|
// change or leave as it is on the site.
|
|
@@ -72,7 +95,7 @@ export class PushCommand extends LoopressCommand {
|
|
|
72
95
|
catch (error) {
|
|
73
96
|
// The push reports the unreadable files itself, but an empty `changes` must not read as
|
|
74
97
|
// "nothing changes".
|
|
75
|
-
this.warn(`Could not list what this push would change: ${error
|
|
98
|
+
this.warn(`Could not list what this push would change: ${errorMessage(error)}`);
|
|
76
99
|
}
|
|
77
100
|
logChanges((message) => { this.log(message); }, this.previewChanges);
|
|
78
101
|
return undefined;
|
|
@@ -84,18 +107,29 @@ export class PushCommand extends LoopressCommand {
|
|
|
84
107
|
}, dir);
|
|
85
108
|
}
|
|
86
109
|
catch (error) {
|
|
87
|
-
this.warn(`Could not read remote state for a rollback snapshot: ${error
|
|
110
|
+
this.warn(`Could not read remote state for a rollback snapshot: ${errorMessage(error)}`);
|
|
88
111
|
return undefined;
|
|
89
112
|
}
|
|
90
113
|
await this.guardOutsideEdits(provider, dir, remote, writes);
|
|
91
114
|
return remote;
|
|
92
115
|
}
|
|
93
116
|
async catch(err) {
|
|
117
|
+
// Unset when init() failed before resolving the environment, despite its declared type.
|
|
94
118
|
if (!this.dryRun && !this.refusedByGuard && this.siteConfig) {
|
|
95
119
|
await this.recordDeployment('failure');
|
|
96
120
|
}
|
|
97
121
|
await super.catch(err);
|
|
98
122
|
}
|
|
123
|
+
// Lists what the environment's `exclude` globs (loopress.json) kept out of this push, so a
|
|
124
|
+
// feature held back from it is never forgotten there.
|
|
125
|
+
async finally(error) {
|
|
126
|
+
const paths = error ? [] : heldBackPaths();
|
|
127
|
+
if (paths.length > 0) {
|
|
128
|
+
const prefix = this.dryRun ? '[dry-run] ' : '';
|
|
129
|
+
this.log(`${prefix}Held back from ${this.siteConfig.name} (exclude in loopress.json): ${paths.join(', ')}`);
|
|
130
|
+
}
|
|
131
|
+
await super.finally(error);
|
|
132
|
+
}
|
|
99
133
|
async guardProductionPush() {
|
|
100
134
|
await guardProductionPush({
|
|
101
135
|
dryRun: this.dryRun,
|
|
@@ -120,7 +154,7 @@ export class PushCommand extends LoopressCommand {
|
|
|
120
154
|
return await provider.remote(this.wp, () => { }, dir);
|
|
121
155
|
}
|
|
122
156
|
catch (error) {
|
|
123
|
-
this.error(`The site would refuse this push: ${error
|
|
157
|
+
this.error(`The site would refuse this push: ${errorMessage(error)}`);
|
|
124
158
|
}
|
|
125
159
|
}
|
|
126
160
|
async recordDeployment(status) {
|
|
@@ -130,7 +164,7 @@ export class PushCommand extends LoopressCommand {
|
|
|
130
164
|
try {
|
|
131
165
|
await withSpinner('Recording the deployment', async () => got.post(`${API_URL}/deployments`, {
|
|
132
166
|
headers: { Authorization: `Bearer ${token}` },
|
|
133
|
-
json: { status, url: this.siteConfig.url },
|
|
167
|
+
json: { status, url: this.siteConfig.url, command: this.id, ...gitState() },
|
|
134
168
|
timeout: { request: 3000 },
|
|
135
169
|
}));
|
|
136
170
|
}
|
|
@@ -186,12 +220,19 @@ export class PushCommand extends LoopressCommand {
|
|
|
186
220
|
})), { concurrent: false, exitOnError: false, renderer: this.jsonEnabled() ? 'silent' : 'default' }).run();
|
|
187
221
|
});
|
|
188
222
|
}
|
|
223
|
+
// Absolute URLs to another environment of the project in what this push sends: warned about,
|
|
224
|
+
// never rewritten, on a dry run too.
|
|
225
|
+
warnEnvironmentUrls(items) {
|
|
226
|
+
const prefix = this.dryRun ? '[dry-run] ' : '';
|
|
227
|
+
for (const warning of environmentUrlWarnings(items, this.siteConfig, this.projectEnvironments))
|
|
228
|
+
this.warn(`${prefix}${warning}`);
|
|
229
|
+
}
|
|
189
230
|
// Pairs `beforeState` (from captureBeforePushState, read before this push touched anything)
|
|
190
231
|
// with a fresh remote read taken right after the push, and writes both as one rollback
|
|
191
232
|
// snapshot. Reading the after-state from WordPress itself, rather than assuming it matches
|
|
192
233
|
// the local files that were pushed, is what keeps this correct for every resource's own
|
|
193
234
|
// partial-push semantics: a readonly option skipped by `option push`, a snippet or form
|
|
194
|
-
// that gets a server-assigned id on create, a theme-
|
|
235
|
+
// that gets a server-assigned id on create, a theme-style push that only touches the
|
|
195
236
|
// active stylesheet, an API/hook push that leaves server-side files alone unless `--prune`.
|
|
196
237
|
// `lps <resource> rollback`'s drift check compares the environment's state at rollback time
|
|
197
238
|
// against exactly this after-state, so it has to be what actually landed, not what was sent.
|
|
@@ -211,7 +252,7 @@ export class PushCommand extends LoopressCommand {
|
|
|
211
252
|
});
|
|
212
253
|
}
|
|
213
254
|
catch (error) {
|
|
214
|
-
this.warn(`Could not save a rollback snapshot after this push: ${error
|
|
255
|
+
this.warn(`Could not save a rollback snapshot after this push: ${errorMessage(error)}`);
|
|
215
256
|
}
|
|
216
257
|
}
|
|
217
258
|
// Refuses to silently overwrite a change made on the site outside Loopress (wp-admin, another
|
|
@@ -247,7 +288,7 @@ export class PushCommand extends LoopressCommand {
|
|
|
247
288
|
local = await provider.local(dir, () => { }, this.wp);
|
|
248
289
|
}
|
|
249
290
|
catch (error) {
|
|
250
|
-
this.error(`Could not check for changes made on ${this.siteConfig.url} outside Loopress: ${error
|
|
291
|
+
this.error(`Could not check for changes made on ${this.siteConfig.url} outside Loopress: ${errorMessage(error)}. Retry, or pass --overwrite.`);
|
|
251
292
|
}
|
|
252
293
|
const overwritten = [...local.keys()].filter((id) => (writes?.(id) ?? true) &&
|
|
253
294
|
remote.get(id) !== undefined &&
|
|
@@ -6,8 +6,8 @@ type PutOrCreateOptions = {
|
|
|
6
6
|
postPayload?: Record<string, unknown>;
|
|
7
7
|
putEndpoint: (id: number) => string;
|
|
8
8
|
};
|
|
9
|
-
export declare function putOrCreate
|
|
10
|
-
body:
|
|
9
|
+
export declare function putOrCreate(wp: WpClient, options: PutOrCreateOptions): Promise<{
|
|
10
|
+
body: Record<string, unknown>;
|
|
11
11
|
created: boolean;
|
|
12
12
|
}>;
|
|
13
13
|
export {};
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import {} from 'node:fs';
|
|
1
2
|
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
-
import { join, relative, sep } from 'node:path';
|
|
3
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { withoutExcluded } from './exclude.js';
|
|
5
|
+
import { errorCode } from './narrow.js';
|
|
3
6
|
export async function readdirTolerant(dir, options) {
|
|
4
7
|
try {
|
|
8
|
+
// Excluded paths (lib/exclude.ts) are hidden here, so every resource listing through this
|
|
9
|
+
// helper leaves them out of a push or a diff on the environment that excludes them.
|
|
5
10
|
return options?.withFileTypes
|
|
6
|
-
? await readdir(dir, { recursive: options.recursive, withFileTypes: true })
|
|
7
|
-
: await readdir(dir, { recursive: options?.recursive });
|
|
11
|
+
? withoutExcluded(dir, await readdir(dir, { recursive: options.recursive, withFileTypes: true }), (entry) => resolve(entry.parentPath, entry.name))
|
|
12
|
+
: withoutExcluded(dir, await readdir(dir, { recursive: options?.recursive }), (entry) => entry);
|
|
8
13
|
}
|
|
9
14
|
catch (error) {
|
|
10
|
-
if (error
|
|
15
|
+
if (errorCode(error) === 'ENOENT')
|
|
11
16
|
return [];
|
|
12
17
|
throw error;
|
|
13
18
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
export declare const RESOURCE_IDS: readonly ["plugin", "composer", "cpt", "taxonomy", "meta", "role", "acf", "api", "hook", "block", "form", "pattern", "template", "part", "css", "theme-style", "page", "menu", "option", "snippet"];
|
|
3
|
+
export type ResourceId = (typeof RESOURCE_IDS)[number];
|
|
4
|
+
export declare function selectionFlags(noun: string, valid: readonly string[]): {
|
|
5
|
+
only: ReturnType<typeof Flags.string>;
|
|
6
|
+
skip: ReturnType<typeof Flags.string>;
|
|
7
|
+
};
|
|
8
|
+
export declare function parseSelectionFlag(raw: string[] | undefined, flagName: 'only' | 'skip', valid: readonly string[], fail: (message: string) => never): string[];
|
|
9
|
+
export declare function selectResources<T extends string>(all: readonly T[], only?: readonly string[], skip?: readonly string[]): T[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
// One singular id per resource, shared by every command taking `--only` / `--skip`
|
|
3
|
+
// (`lps dev`, `lps diff`, `lps push`). Each command accepts the subset it can act on.
|
|
4
|
+
export const RESOURCE_IDS = [
|
|
5
|
+
'plugin',
|
|
6
|
+
'composer',
|
|
7
|
+
'cpt',
|
|
8
|
+
'taxonomy',
|
|
9
|
+
'meta',
|
|
10
|
+
'role',
|
|
11
|
+
'acf',
|
|
12
|
+
'api',
|
|
13
|
+
'hook',
|
|
14
|
+
'block',
|
|
15
|
+
'form',
|
|
16
|
+
'pattern',
|
|
17
|
+
'template',
|
|
18
|
+
'part',
|
|
19
|
+
'css',
|
|
20
|
+
'theme-style',
|
|
21
|
+
'page',
|
|
22
|
+
'menu',
|
|
23
|
+
'option',
|
|
24
|
+
'snippet',
|
|
25
|
+
];
|
|
26
|
+
// Comma-separated or repeated: `--only a,b`, `--only a --only b`, or both.
|
|
27
|
+
export function selectionFlags(noun, valid) {
|
|
28
|
+
return {
|
|
29
|
+
only: Flags.string({ description: `Only ${noun} these resources (comma-separated or repeated): ${valid.join(', ')}`, multiple: true }),
|
|
30
|
+
skip: Flags.string({ description: `${noun.charAt(0).toUpperCase()}${noun.slice(1)} every resource except these (comma-separated or repeated)`, multiple: true }),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// Splits the raw flag values and rejects any id outside `valid`.
|
|
34
|
+
export function parseSelectionFlag(raw, flagName, valid, fail) {
|
|
35
|
+
return (raw ?? [])
|
|
36
|
+
.flatMap((value) => value.split(','))
|
|
37
|
+
.map((value) => value.trim())
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map((value) => {
|
|
40
|
+
if (!valid.includes(value))
|
|
41
|
+
fail(`Unknown resource "${value}" in --${flagName}. Valid resources: ${valid.join(', ')}`);
|
|
42
|
+
return value;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// `only` narrows `all`, `skip` then removes from whatever that is, so a combined
|
|
46
|
+
// `--only=a,b --skip=b` reads as: a, b, then drop b. The result always follows `all`'s order.
|
|
47
|
+
export function selectResources(all, only = [], skip = []) {
|
|
48
|
+
return all.filter((id) => (only.length === 0 || only.includes(id)) && !skip.includes(id));
|
|
49
|
+
}
|
|
@@ -3,6 +3,8 @@ 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
|
+
fileExtension?: string;
|
|
7
|
+
fileName?(id: string): string | undefined;
|
|
6
8
|
local(dir: string, onWarn: (message: string) => void, wp?: WpClient): Promise<ResourceState>;
|
|
7
9
|
remote(wp: WpClient, onWarn: (message: string) => void, dir: string): Promise<ResourceState>;
|
|
8
10
|
resource: string;
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, join, relative, sep } from 'node:path';
|
|
3
3
|
import { ACF_OBJECT_TYPES, acfEndpoint, acfNormalizeEndpoint, getAcfKey } from '../utils/acf-format.js';
|
|
4
|
+
import { blockFilesToPush, BLOCKS_ENDPOINT, excludedBlockFiles, formatBlockProblems, readLocalBlocks } from '../utils/block-format.js';
|
|
4
5
|
import { CPT_SPEC, fileExtension, META_SPEC, parseDeclaredFile, PATTERN_SPEC, ROLE_SPEC, TAXONOMY_SPEC, } from '../utils/declared-type-format.js';
|
|
5
6
|
import { FORM_ENDPOINT, getFormId } from '../utils/form-format.js';
|
|
6
7
|
import { getMenuSlug, MENU_ENDPOINT, MENU_LOCATIONS_ENDPOINT } from '../utils/menu-format.js';
|
|
7
8
|
import { optionEndpoint, parseLocalOption } from '../utils/option-format.js';
|
|
8
9
|
import { formatPageProblems, PAGES_ENDPOINT, readLocalHtmlFiles, readLocalPages } from '../utils/page-format.js';
|
|
10
|
+
import {} from '../utils/resource-dirs.js';
|
|
9
11
|
import { normalizeSnippet, SNIPPETS_ENDPOINT, stripPhpOpeningTag } from '../utils/snippet-format.js';
|
|
10
12
|
import { CHILD_THEME_ENDPOINT, parsePartFile, parseTemplateFile, TEMPLATE_SLUG_RULE } from '../utils/template-format.js';
|
|
11
13
|
import { canonicalGlobalStyles, getActiveThemeGlobalStyles, globalStylesEndpoint, THEME_STYLES_FILE_SUFFIX, } from '../utils/theme-styles-format.js';
|
|
14
|
+
import {} from './diff-state.js';
|
|
12
15
|
import { loadFiles } from './load-files.js';
|
|
13
16
|
import { loadSnippets } from './load-snippets.js';
|
|
17
|
+
import { causeOf, errorCode, isRecord } from './narrow.js';
|
|
14
18
|
import { isApplicative404, isNotFoundError } from './wp-client.js';
|
|
15
19
|
function readJson(raw) {
|
|
16
20
|
const parsed = JSON.parse(raw);
|
|
17
|
-
if (
|
|
21
|
+
if (!isRecord(parsed) || Array.isArray(parsed)) {
|
|
18
22
|
throw new Error('not a JSON object');
|
|
19
23
|
}
|
|
20
24
|
return parsed;
|
|
@@ -59,7 +63,7 @@ const snippetProvider = {
|
|
|
59
63
|
catch (error) {
|
|
60
64
|
// A directory that was never created is "nothing tracked locally", the same tolerance
|
|
61
65
|
// loadFiles/readdirTolerant already have; loadSnippets wraps the ENOENT in its own Error.
|
|
62
|
-
if (error
|
|
66
|
+
if (errorCode(causeOf(error)) === 'ENOENT')
|
|
63
67
|
return state;
|
|
64
68
|
throw error;
|
|
65
69
|
}
|
|
@@ -87,6 +91,7 @@ const snippetProvider = {
|
|
|
87
91
|
// stance as form-format.ts.
|
|
88
92
|
const formProvider = {
|
|
89
93
|
dirKind: 'form',
|
|
94
|
+
fileExtension: '.json',
|
|
90
95
|
async local(dir, onWarn) {
|
|
91
96
|
const entries = await loadFiles(dir, {
|
|
92
97
|
extension: '.json',
|
|
@@ -133,6 +138,7 @@ async function normalizeAcf(wp, type, objects) {
|
|
|
133
138
|
}
|
|
134
139
|
const acfProvider = {
|
|
135
140
|
dirKind: 'acf',
|
|
141
|
+
fileExtension: '.json',
|
|
136
142
|
async local(dir, onWarn, wp) {
|
|
137
143
|
const state = new Map();
|
|
138
144
|
for (const type of ACF_OBJECT_TYPES) {
|
|
@@ -166,20 +172,21 @@ const acfProvider = {
|
|
|
166
172
|
title: 'ACF',
|
|
167
173
|
};
|
|
168
174
|
// ---- PHP file directories (API routes, hooks) -----------------------------------------
|
|
175
|
+
function filesToState(files) {
|
|
176
|
+
const state = new Map();
|
|
177
|
+
for (const file of files)
|
|
178
|
+
state.set(file.filename, file.content);
|
|
179
|
+
return state;
|
|
180
|
+
}
|
|
169
181
|
// api/ and hooks/ are both just a recursive directory of PHP files whose text is compared
|
|
170
182
|
// verbatim (line diff on change), no server-side normalization to undo. Only the directory
|
|
171
183
|
// kind, the endpoint and the labels differ.
|
|
172
184
|
function phpFilesProvider(spec) {
|
|
173
|
-
const toState = (files) => {
|
|
174
|
-
const state = new Map();
|
|
175
|
-
for (const file of files)
|
|
176
|
-
state.set(file.filename, file.content);
|
|
177
|
-
return state;
|
|
178
|
-
};
|
|
179
185
|
return {
|
|
180
186
|
dirKind: spec.dirKind,
|
|
187
|
+
fileExtension: '.php',
|
|
181
188
|
async local(dir, onWarn) {
|
|
182
|
-
return
|
|
189
|
+
return filesToState(await loadFiles(dir, {
|
|
183
190
|
extension: '.php',
|
|
184
191
|
onSkip: onWarn,
|
|
185
192
|
parse: (raw, filePath) => ({
|
|
@@ -190,7 +197,7 @@ function phpFilesProvider(spec) {
|
|
|
190
197
|
}));
|
|
191
198
|
},
|
|
192
199
|
async remote(wp) {
|
|
193
|
-
return
|
|
200
|
+
return filesToState(await wp.get(spec.endpoint));
|
|
194
201
|
},
|
|
195
202
|
resource: spec.resource,
|
|
196
203
|
title: spec.title,
|
|
@@ -208,6 +215,30 @@ const hookProvider = phpFilesProvider({
|
|
|
208
215
|
resource: 'hook',
|
|
209
216
|
title: 'Hooks',
|
|
210
217
|
});
|
|
218
|
+
// ---- Dynamic blocks -------------------------------------------------------------------
|
|
219
|
+
// Keyed by slug, compared on the whole folder (file name => text). A local problem fails the
|
|
220
|
+
// resource, the same stance as pages: pushing would stop on it anyway.
|
|
221
|
+
const blockProvider = {
|
|
222
|
+
dirKind: 'block',
|
|
223
|
+
// A block is a folder: only the bare `blocks/<slug>` path names a site-only block.
|
|
224
|
+
fileExtension: '',
|
|
225
|
+
// A file the environment excludes inside a block folder keeps the site's copy, as the push
|
|
226
|
+
// sends it back: without `wp`, that file is just left out.
|
|
227
|
+
async local(dir, _onWarn, wp) {
|
|
228
|
+
const { blocks, problems } = await readLocalBlocks(dir);
|
|
229
|
+
if (problems.length > 0)
|
|
230
|
+
throw new Error(formatBlockProblems(problems));
|
|
231
|
+
const withExcluded = wp !== undefined && blocks.some((block) => excludedBlockFiles(dir, block.slug).length > 0);
|
|
232
|
+
const site = withExcluded ? new Map((await wp.get(BLOCKS_ENDPOINT)).map((block) => [block.slug, block.files])) : undefined;
|
|
233
|
+
return new Map(blocks.map((block) => [block.slug, site ? blockFilesToPush(dir, block, site.get(block.slug)) : block.files]));
|
|
234
|
+
},
|
|
235
|
+
async remote(wp) {
|
|
236
|
+
const blocks = await wp.get(BLOCKS_ENDPOINT);
|
|
237
|
+
return new Map(blocks.map((block) => [block.slug, block.files]));
|
|
238
|
+
},
|
|
239
|
+
resource: 'block',
|
|
240
|
+
title: 'Dynamic blocks',
|
|
241
|
+
};
|
|
211
242
|
// ---- Pages ------------------------------------------------------------------------------
|
|
212
243
|
// Keyed by slug (the page's identity on both sides). `link` is server-derived and never
|
|
213
244
|
// compared. A local problem (a `.php` in pages/, a bad header) fails the whole resource rather
|
|
@@ -240,13 +271,16 @@ async function childThemeState(wp, kind) {
|
|
|
240
271
|
const state = new Map(files.map(({ slug, ...rest }) => [slug, rest]));
|
|
241
272
|
for (const entry of child.customized) {
|
|
242
273
|
const [dir, slug] = entry.split('/', 2);
|
|
243
|
-
if (dir === kind)
|
|
244
|
-
|
|
274
|
+
if (dir === kind && slug !== undefined) {
|
|
275
|
+
const existing = state.get(slug);
|
|
276
|
+
state.set(slug, { ...(isRecord(existing) && existing), siteEditor: 'customized' });
|
|
277
|
+
}
|
|
245
278
|
}
|
|
246
279
|
return state;
|
|
247
280
|
}
|
|
248
281
|
const templateProvider = {
|
|
249
282
|
dirKind: 'template',
|
|
283
|
+
fileExtension: '.html',
|
|
250
284
|
async local(dir) {
|
|
251
285
|
const { files, problems } = await readLocalHtmlFiles(dir, parseTemplateFile, TEMPLATE_SLUG_RULE);
|
|
252
286
|
if (problems.length > 0)
|
|
@@ -259,6 +293,7 @@ const templateProvider = {
|
|
|
259
293
|
};
|
|
260
294
|
const partProvider = {
|
|
261
295
|
dirKind: 'part',
|
|
296
|
+
fileExtension: '.html',
|
|
262
297
|
async local(dir) {
|
|
263
298
|
const { files, problems } = await readLocalHtmlFiles(dir, parsePartFile, TEMPLATE_SLUG_RULE);
|
|
264
299
|
if (problems.length > 0)
|
|
@@ -287,6 +322,12 @@ function canonicalMenu(menu) {
|
|
|
287
322
|
}
|
|
288
323
|
const menuProvider = {
|
|
289
324
|
dirKind: 'menu',
|
|
325
|
+
fileExtension: '.json',
|
|
326
|
+
fileName(id) {
|
|
327
|
+
if (id === MENU_LOCATIONS_FILE)
|
|
328
|
+
return `${id}.json`;
|
|
329
|
+
return id.startsWith('menu/') ? `${id.slice('menu/'.length)}.json` : undefined;
|
|
330
|
+
},
|
|
290
331
|
async local(dir, onWarn) {
|
|
291
332
|
const state = new Map();
|
|
292
333
|
const entries = await loadFiles(dir, {
|
|
@@ -324,6 +365,7 @@ const menuProvider = {
|
|
|
324
365
|
function declaredTypeProvider(spec, title) {
|
|
325
366
|
return {
|
|
326
367
|
dirKind: spec.dirKind,
|
|
368
|
+
fileExtension: fileExtension(spec),
|
|
327
369
|
async local(dir, onWarn) {
|
|
328
370
|
const entries = await loadFiles(dir, {
|
|
329
371
|
extension: fileExtension(spec),
|
|
@@ -353,6 +395,7 @@ const patternProvider = declaredTypeProvider(PATTERN_SPEC, 'Patterns');
|
|
|
353
395
|
// sides so it can never itself show up as drift.
|
|
354
396
|
const optionsProvider = {
|
|
355
397
|
dirKind: 'options',
|
|
398
|
+
fileExtension: '.json',
|
|
356
399
|
async local(dir, onWarn) {
|
|
357
400
|
const entries = await loadFiles(dir, {
|
|
358
401
|
extension: '.json',
|
|
@@ -398,6 +441,8 @@ const optionsProvider = {
|
|
|
398
441
|
// individually rather than colliding on a single key.
|
|
399
442
|
const themeStylesProvider = {
|
|
400
443
|
dirKind: 'themeStyles',
|
|
444
|
+
fileExtension: '.json',
|
|
445
|
+
fileName: (stylesheet) => `${stylesheet}${THEME_STYLES_FILE_SUFFIX}`,
|
|
401
446
|
async local(dir, onWarn) {
|
|
402
447
|
const entries = await loadFiles(dir, {
|
|
403
448
|
extension: '.json',
|
|
@@ -421,7 +466,7 @@ const themeStylesProvider = {
|
|
|
421
466
|
const item = await wp.get(globalStylesEndpoint(id));
|
|
422
467
|
return new Map([[stylesheet, canonicalGlobalStyles(item)]]);
|
|
423
468
|
},
|
|
424
|
-
resource: 'theme-
|
|
469
|
+
resource: 'theme-style',
|
|
425
470
|
title: 'Theme styles',
|
|
426
471
|
};
|
|
427
472
|
export const RESOURCE_STATE_PROVIDERS = [
|
|
@@ -430,6 +475,7 @@ export const RESOURCE_STATE_PROVIDERS = [
|
|
|
430
475
|
acfProvider,
|
|
431
476
|
apiProvider,
|
|
432
477
|
hookProvider,
|
|
478
|
+
blockProvider,
|
|
433
479
|
templateProvider,
|
|
434
480
|
partProvider,
|
|
435
481
|
pageProvider,
|
|
@@ -476,7 +522,7 @@ export async function composerLocalState(rootDir) {
|
|
|
476
522
|
state.set(file, await readFile(join(rootDir, file), 'utf8'));
|
|
477
523
|
}
|
|
478
524
|
catch (error) {
|
|
479
|
-
if (error
|
|
525
|
+
if (errorCode(error) !== 'ENOENT')
|
|
480
526
|
throw error;
|
|
481
527
|
}
|
|
482
528
|
}
|
|
@@ -9,6 +9,7 @@ import { LoopressCommand } from './base.js';
|
|
|
9
9
|
import { compareStates, isEmptyDiff } from './diff-state.js';
|
|
10
10
|
import { guardProductionPush } from './guard-production-push.js';
|
|
11
11
|
import { isInteractive } from './interactive.js';
|
|
12
|
+
import { errorMessage } from './narrow.js';
|
|
12
13
|
import { getResourceStateProvider } from './resource-state.js';
|
|
13
14
|
import { materializeSnapshot } from './rollback-materialize.js';
|
|
14
15
|
import { listSnapshots, readSnapshot, recordToState } from './snapshot-store.js';
|
|
@@ -33,7 +34,7 @@ function indentPatch(patch) {
|
|
|
33
34
|
// anything, this runs the equivalent of `lps <resource> diff` between the environment's current
|
|
34
35
|
// state and the state the original push expected to leave behind, and requires explicit
|
|
35
36
|
// confirmation (the same interactive/`--yes` shape as guardProductionPush) when they differ.
|
|
36
|
-
// `command` is the CLI topic when it isn't the resource key (e.g. `theme style` for theme-
|
|
37
|
+
// `command` is the CLI topic when it isn't the resource key (e.g. `theme style` for theme-style).
|
|
37
38
|
export function resourceRollbackCommand(resource, options) {
|
|
38
39
|
const command = options.command ?? resource;
|
|
39
40
|
class ResourceRollback extends LoopressCommand {
|
|
@@ -69,7 +70,7 @@ export function resourceRollbackCommand(resource, options) {
|
|
|
69
70
|
snapshot = await readSnapshot(rootDir, resource, this.siteConfig.name, flags.to);
|
|
70
71
|
}
|
|
71
72
|
catch (error) {
|
|
72
|
-
this.error(error
|
|
73
|
+
this.error(errorMessage(error));
|
|
73
74
|
}
|
|
74
75
|
await guardProductionPush({
|
|
75
76
|
dryRun: this.dryRun,
|
|
@@ -140,7 +141,7 @@ export function resourceRollbackCommand(resource, options) {
|
|
|
140
141
|
// A cleanup failure here must never hide a real error from the push above: warn about
|
|
141
142
|
// it and let the original error (if any) keep propagating instead of being replaced.
|
|
142
143
|
await rm(tmpDir, { force: true, recursive: true }).catch((error) => {
|
|
143
|
-
this.warn(`Could not remove the temporary directory "${tmpDir}": ${error
|
|
144
|
+
this.warn(`Could not remove the temporary directory "${tmpDir}": ${errorMessage(error)}`);
|
|
144
145
|
});
|
|
145
146
|
}
|
|
146
147
|
this.log(`Rolled back ${provider.title} to the snapshot from ${snapshot.createdAt}.`);
|