@loopress/cli 0.24.1 → 0.26.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 +721 -90
- 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 +2 -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/page/diff.d.ts +2 -0
- package/dist/commands/page/diff.js +5 -0
- package/dist/commands/page/list.d.ts +8 -0
- package/dist/commands/page/list.js +22 -0
- package/dist/commands/page/push.d.ts +20 -0
- package/dist/commands/page/push.js +60 -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 +10 -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/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +2 -1
- 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 +116 -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 +7 -1
- package/dist/types/project-config.generated.d.ts +12 -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/page-format.d.ts +27 -0
- package/dist/utils/page-format.js +111 -0
- package/dist/utils/resource-dirs.d.ts +3 -0
- package/dist/utils/resource-dirs.js +6 -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 +1633 -168
- package/package.json +4 -2
- package/schemas/project-config.schema.json +15 -0
|
@@ -4,8 +4,10 @@ import { dirname, extname, join } from 'node:path';
|
|
|
4
4
|
import { PushCommand } from '../../lib/push-command.js';
|
|
5
5
|
import { putOrCreate } from '../../lib/put-or-create.js';
|
|
6
6
|
import { readdirTolerant } from '../../lib/readdir-tolerant.js';
|
|
7
|
+
import { getResourceStateProvider } from '../../lib/resource-state.js';
|
|
8
|
+
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
7
9
|
import { pluralize } from '../../utils/pluralize.js';
|
|
8
|
-
import { redirectFileBase, SEO_REDIRECTS_ENDPOINT, SEO_SETTINGS_ENDPOINT, seoPostMetaEndpoint, seoRedirectEndpoint, } from '../../utils/seo-format.js';
|
|
10
|
+
import { redirectFileBase, SEO_REDIRECTS_ENDPOINT, SEO_SETTINGS_ENDPOINT, seoPostMetaEndpoint, seoPostMetaItemEndpoint, seoRedirectEndpoint, } from '../../utils/seo-format.js';
|
|
9
11
|
export default class Push extends PushCommand {
|
|
10
12
|
static args = {
|
|
11
13
|
path: Args.string({ description: 'Path to SEO directory (overrides project config)' }),
|
|
@@ -28,9 +30,12 @@ export default class Push extends PushCommand {
|
|
|
28
30
|
this.allowExternalRedirects = flags['allow-external-redirects'];
|
|
29
31
|
this.log(`Pushing SEO configuration to ${url}`);
|
|
30
32
|
this.log(`SEO path: ${path}`);
|
|
33
|
+
const provider = getResourceStateProvider('seo');
|
|
34
|
+
const beforeState = await this.captureBeforePushState(provider, path);
|
|
31
35
|
await this.pushSettings(path);
|
|
32
36
|
await this.pushPostMeta(path);
|
|
33
37
|
await this.pushRedirects(path);
|
|
38
|
+
await this.writeAfterPushSnapshot(provider, path, beforeState);
|
|
34
39
|
if (this.failedCount > 0) {
|
|
35
40
|
this.error(`${pluralize(this.failedCount, 'SEO item')} failed to push.`);
|
|
36
41
|
}
|
|
@@ -39,6 +44,17 @@ export default class Push extends PushCommand {
|
|
|
39
44
|
await this.recordSuccess();
|
|
40
45
|
this.log('All SEO configuration pushed.');
|
|
41
46
|
}
|
|
47
|
+
async currentPostMetaRevision(postType, slug) {
|
|
48
|
+
try {
|
|
49
|
+
const current = await this.wp.get(seoPostMetaItemEndpoint(postType, slug));
|
|
50
|
+
return current.revision;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (isNotFoundError(error))
|
|
54
|
+
return undefined;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
42
58
|
async jsonFilesIn(dir) {
|
|
43
59
|
return (await readdirTolerant(dir)).filter((file) => extname(file) === '.json');
|
|
44
60
|
}
|
|
@@ -64,7 +80,16 @@ export default class Push extends PushCommand {
|
|
|
64
80
|
}
|
|
65
81
|
try {
|
|
66
82
|
const post = JSON.parse(await readFile(filePath, 'utf8'));
|
|
67
|
-
|
|
83
|
+
// Read the post's current SEO-meta revision right before writing it, and send it back as
|
|
84
|
+
// `expectedRevision`: WordPress refuses the write (412) if something else changed that
|
|
85
|
+
// post's meta in between, instead of this push silently overwriting it (#234). No revision
|
|
86
|
+
// to condition on for a post whose meta doesn't exist remotely yet (an upsert that will
|
|
87
|
+
// itself fail with a clearer "post not found" error): falls back to an unconditional write.
|
|
88
|
+
const expectedRevision = await this.currentPostMetaRevision(postType, post.slug);
|
|
89
|
+
const body = { meta: post.meta, slug: post.slug };
|
|
90
|
+
if (expectedRevision !== undefined)
|
|
91
|
+
body.expectedRevision = expectedRevision;
|
|
92
|
+
await this.wp.post(seoPostMetaEndpoint(postType), body);
|
|
68
93
|
if (task)
|
|
69
94
|
task.output = `Pushed: ${post.slug}`;
|
|
70
95
|
}
|
|
@@ -128,7 +153,12 @@ export default class Push extends PushCommand {
|
|
|
128
153
|
return;
|
|
129
154
|
}
|
|
130
155
|
try {
|
|
131
|
-
|
|
156
|
+
const settings = JSON.parse(raw);
|
|
157
|
+
// Read the settings' current revision right before writing them, and send it back as
|
|
158
|
+
// `expectedRevision`: WordPress refuses the write (412) if something else changed the
|
|
159
|
+
// settings in between, instead of this push silently overwriting it (#234).
|
|
160
|
+
const { revision } = await this.wp.get(SEO_SETTINGS_ENDPOINT);
|
|
161
|
+
await this.wp.put(SEO_SETTINGS_ENDPOINT, { expectedRevision: revision, settings });
|
|
132
162
|
this.log(`Pushed: ${file}`);
|
|
133
163
|
}
|
|
134
164
|
catch (error) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceRollbackCommand } from '../../lib/rollback-command.js';
|
|
2
|
+
export default resourceRollbackCommand('seo', {
|
|
3
|
+
description: 'Restore SEO settings, post meta, and redirects to a snapshot taken automatically before an earlier `lps seo push`',
|
|
4
|
+
pathNoun: 'SEO directory',
|
|
5
|
+
});
|
|
@@ -19,6 +19,7 @@ export default class Push extends PushCommand {
|
|
|
19
19
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
20
|
};
|
|
21
21
|
run(): Promise<PushResult>;
|
|
22
|
+
private currentRevision;
|
|
22
23
|
private ensureCanonicalFilename;
|
|
23
24
|
private loadSnippets;
|
|
24
25
|
private pushSnippet;
|
|
@@ -4,6 +4,8 @@ import { basename, dirname, extname, join } from 'node:path';
|
|
|
4
4
|
import { loadSnippets as loadSnippetsFromDisk } from '../../lib/load-snippets.js';
|
|
5
5
|
import { PushCommand } from '../../lib/push-command.js';
|
|
6
6
|
import { putOrCreate } from '../../lib/put-or-create.js';
|
|
7
|
+
import { getResourceStateProvider } from '../../lib/resource-state.js';
|
|
8
|
+
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
7
9
|
import { pluralize } from '../../utils/pluralize.js';
|
|
8
10
|
import { normalizeSnippet, SNIPPETS_ENDPOINT, stripPhpOpeningTag } from '../../utils/snippet-format.js';
|
|
9
11
|
import { toSlug } from '../../utils/to-slug.js';
|
|
@@ -24,6 +26,8 @@ export default class Push extends PushCommand {
|
|
|
24
26
|
const path = this.resolveSnippetsPath(args.path);
|
|
25
27
|
this.log(`Pushing snippets to ${url}`);
|
|
26
28
|
this.log(`Snippets path: ${path}`);
|
|
29
|
+
const provider = getResourceStateProvider('snippet');
|
|
30
|
+
const beforeState = await this.captureBeforePushState(provider, path);
|
|
27
31
|
const snippets = await this.loadSnippets(path);
|
|
28
32
|
this.log(`Found ${pluralize(snippets.length, 'snippet')} to push`);
|
|
29
33
|
const pushed = [];
|
|
@@ -31,6 +35,7 @@ export default class Push extends PushCommand {
|
|
|
31
35
|
const id = await this.pushSnippet(snippet, task);
|
|
32
36
|
pushed.push({ id, name: snippet.name });
|
|
33
37
|
});
|
|
38
|
+
await this.writeAfterPushSnapshot(provider, path, beforeState);
|
|
34
39
|
if (this.failedCount > 0) {
|
|
35
40
|
this.error(`${pluralize(this.failedCount, 'snippet')} failed to push.`);
|
|
36
41
|
}
|
|
@@ -40,6 +45,17 @@ export default class Push extends PushCommand {
|
|
|
40
45
|
this.log('All snippets pushed.');
|
|
41
46
|
return { pushed, status: 'success' };
|
|
42
47
|
}
|
|
48
|
+
async currentRevision(id) {
|
|
49
|
+
try {
|
|
50
|
+
const current = await this.wp.get(`${SNIPPETS_ENDPOINT}/${id}`);
|
|
51
|
+
return normalizeSnippet(current).revision;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (isNotFoundError(error))
|
|
55
|
+
return undefined;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
43
59
|
// Renames the local file pair to the `<id>-<slug>` convention used by `snippet pull` whenever
|
|
44
60
|
// it doesn't already match (e.g. a hand-created `demo.php` with no id, or a stale slug after a rename).
|
|
45
61
|
// This is a side effect of `push`: local files on disk are renamed, not just the remote snippet.
|
|
@@ -90,12 +106,26 @@ export default class Push extends PushCommand {
|
|
|
90
106
|
}
|
|
91
107
|
try {
|
|
92
108
|
const payload = this.toPayload(snippet);
|
|
109
|
+
// Read the snippet's current revision right before writing it, and send it back as
|
|
110
|
+
// `expectedRevision`: WordPress refuses the write (412) if something else changed the
|
|
111
|
+
// snippet in between, instead of this push silently overwriting it (#234). No revision to
|
|
112
|
+
// condition on when there's no known remote id yet (a first push): putPayload then equals
|
|
113
|
+
// payload, and putOrCreate falls back to POST below exactly as before this existed. Kept
|
|
114
|
+
// out of the POST body specifically (see postPayload): a spurious expectedRevision on a
|
|
115
|
+
// create would be meaningless there.
|
|
116
|
+
let putPayload = payload;
|
|
117
|
+
if (snippet.id !== undefined) {
|
|
118
|
+
const expectedRevision = await this.currentRevision(snippet.id);
|
|
119
|
+
if (expectedRevision !== undefined)
|
|
120
|
+
putPayload = { ...payload, expectedRevision };
|
|
121
|
+
}
|
|
93
122
|
// The id recorded locally may not exist on this site (e.g. a fresh install): putOrCreate
|
|
94
123
|
// falls back to POST instead of failing, adopting whatever id the site assigns.
|
|
95
124
|
const { body, created } = await putOrCreate(this.wp, {
|
|
96
125
|
id: snippet.id ?? null,
|
|
97
|
-
payload,
|
|
126
|
+
payload: putPayload,
|
|
98
127
|
postEndpoint: SNIPPETS_ENDPOINT,
|
|
128
|
+
postPayload: payload,
|
|
99
129
|
putEndpoint: (id) => `${SNIPPETS_ENDPOINT}/${id}`,
|
|
100
130
|
});
|
|
101
131
|
let { id } = snippet;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceRollbackCommand } from '../../lib/rollback-command.js';
|
|
2
|
+
export default resourceRollbackCommand('snippet', {
|
|
3
|
+
description: 'Restore snippets to a snapshot taken automatically before an earlier `lps snippet push`',
|
|
4
|
+
pathNoun: 'snippets directory',
|
|
5
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
+
export default resourceDiffCommand('theme-styles', {
|
|
3
|
+
description: "Show what differs in the active block theme's Global Styles (Site Editor > Styles) between your local file and a WordPress environment, or between two environments",
|
|
4
|
+
pathNoun: 'theme styles directory',
|
|
5
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
export default class Pull extends LoopressCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
5
|
+
import { canonicalGlobalStyles, getActiveThemeGlobalStyles, globalStylesEndpoint, themeStylesFileName, } from '../../utils/theme-styles-format.js';
|
|
6
|
+
export default class Pull extends LoopressCommand {
|
|
7
|
+
static args = {
|
|
8
|
+
path: Args.string({ description: 'Path to theme styles directory (overrides project config)' }),
|
|
9
|
+
};
|
|
10
|
+
static description = "Pull the active block theme's Global Styles customizations (Site Editor > Styles) from WordPress into a local file";
|
|
11
|
+
static examples = ['$ lps theme-styles pull'];
|
|
12
|
+
static flags = {
|
|
13
|
+
...LoopressCommand.dryRunFlag,
|
|
14
|
+
};
|
|
15
|
+
async run() {
|
|
16
|
+
const { args } = await this.parse(Pull);
|
|
17
|
+
const { url } = this.siteConfig;
|
|
18
|
+
const dir = this.resolveThemeStylesPath(args.path);
|
|
19
|
+
this.log(`Pulling Global Styles from ${url}`);
|
|
20
|
+
const { id, stylesheet } = await getActiveThemeGlobalStyles(this.wp);
|
|
21
|
+
const item = await this.wp.get(globalStylesEndpoint(id));
|
|
22
|
+
const file = join(dir, themeStylesFileName(stylesheet));
|
|
23
|
+
if (this.dryRun) {
|
|
24
|
+
this.log(`[dry-run] Would pull Global Styles for "${stylesheet}" to ${file}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await mkdir(dir, { recursive: true });
|
|
28
|
+
await writeFile(file, JSON.stringify(canonicalGlobalStyles(item), null, 2) + '\n');
|
|
29
|
+
this.log(`Pulled Global Styles for "${stylesheet}" to ${file}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PushCommand } from '../../lib/push-command.js';
|
|
2
|
+
export default class Push extends PushCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { PushCommand } from '../../lib/push-command.js';
|
|
5
|
+
import { getResourceStateProvider } from '../../lib/resource-state.js';
|
|
6
|
+
import { getActiveThemeGlobalStyles, globalStylesEndpoint, themeStylesFileName } from '../../utils/theme-styles-format.js';
|
|
7
|
+
export default class Push extends PushCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
path: Args.string({ description: 'Path to theme styles directory (overrides project config)' }),
|
|
10
|
+
};
|
|
11
|
+
static description = "Push the local Global Styles file to the active block theme's Site Editor > Styles on WordPress";
|
|
12
|
+
static examples = ['$ lps theme-styles push'];
|
|
13
|
+
static flags = {
|
|
14
|
+
...PushCommand.dryRunFlag,
|
|
15
|
+
};
|
|
16
|
+
async run() {
|
|
17
|
+
const { args } = await this.parse(Push);
|
|
18
|
+
const { url } = this.siteConfig;
|
|
19
|
+
const dir = this.resolveThemeStylesPath(args.path);
|
|
20
|
+
this.log(`Pushing Global Styles to ${url}`);
|
|
21
|
+
const { id, stylesheet } = await getActiveThemeGlobalStyles(this.wp);
|
|
22
|
+
const file = join(dir, themeStylesFileName(stylesheet));
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = await readFile(file, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code === 'ENOENT') {
|
|
29
|
+
this.error(`No local Global Styles file found for the active theme ("${file}"). Run \`lps theme-styles pull\` first.`);
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
if (this.dryRun) {
|
|
34
|
+
this.log(`[dry-run] Would push: ${file}`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const provider = getResourceStateProvider('theme-styles');
|
|
38
|
+
const beforeState = await this.captureBeforePushState(provider, dir);
|
|
39
|
+
const local = JSON.parse(raw);
|
|
40
|
+
await this.wp.post(globalStylesEndpoint(id), { settings: local.settings ?? {}, styles: local.styles ?? {} });
|
|
41
|
+
this.log(`Pushed: ${file}`);
|
|
42
|
+
await this.writeAfterPushSnapshot(provider, dir, beforeState);
|
|
43
|
+
await this.recordSuccess();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceRollbackCommand } from '../../lib/rollback-command.js';
|
|
2
|
+
export default resourceRollbackCommand('theme-styles', {
|
|
3
|
+
description: 'Restore the active theme’s Global Styles to a snapshot taken automatically before an earlier `lps theme-styles push`',
|
|
4
|
+
pathNoun: 'theme styles directory',
|
|
5
|
+
});
|
package/dist/lib/app-manifest.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
3
|
-
import { extname, join
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { extname, join } from 'node:path';
|
|
4
|
+
import { walkFiles } from './readdir-tolerant.js';
|
|
4
5
|
export const APP_CONFIG_FILENAME = 'loopress.app.json';
|
|
5
6
|
const DEFAULT_ASSETS_DIR = 'dist';
|
|
6
7
|
// Static assets a bundler emits, mirrors the server's AppsDirectory::ALLOWED_EXTENSIONS.
|
|
@@ -14,35 +15,11 @@ const ALLOWED_EXTENSIONS = new Set([
|
|
|
14
15
|
export function isAllowedAsset(relPath) {
|
|
15
16
|
return ALLOWED_EXTENSIONS.has(extname(relPath).toLowerCase());
|
|
16
17
|
}
|
|
17
|
-
async function walk(dir, root) {
|
|
18
|
-
let entries;
|
|
19
|
-
try {
|
|
20
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
21
|
-
}
|
|
22
|
-
catch (error) {
|
|
23
|
-
if (error.code === 'ENOENT')
|
|
24
|
-
return [];
|
|
25
|
-
throw error;
|
|
26
|
-
}
|
|
27
|
-
const out = [];
|
|
28
|
-
for (const entry of entries) {
|
|
29
|
-
const abs = join(dir, entry.name);
|
|
30
|
-
if (entry.isDirectory()) {
|
|
31
|
-
out.push(...(await walk(abs, root)));
|
|
32
|
-
}
|
|
33
|
-
else if (entry.isFile()) {
|
|
34
|
-
// Identity is always '/'-joined, never the OS separator: it travels over HTTP and is
|
|
35
|
-
// matched against the server's own '/'-joined paths.
|
|
36
|
-
out.push(relative(root, abs).replaceAll(/[\\/]/g, '/'));
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return out;
|
|
40
|
-
}
|
|
41
18
|
// Builds the sorted file list of a dist directory. Throws on a file whose extension the
|
|
42
19
|
// server would reject, so the push fails locally with a clear message instead of a 400
|
|
43
20
|
// halfway through the upload.
|
|
44
21
|
export async function buildFileList(distDir) {
|
|
45
|
-
const relPaths = (await
|
|
22
|
+
const relPaths = (await walkFiles(distDir)).sort((a, b) => a.localeCompare(b));
|
|
46
23
|
const files = [];
|
|
47
24
|
for (const path of relPaths) {
|
|
48
25
|
if (!isAllowedAsset(path)) {
|
package/dist/lib/base.d.ts
CHANGED
|
@@ -36,9 +36,11 @@ export declare abstract class LoopressCommand extends Command {
|
|
|
36
36
|
protected resolveAppsPath(override?: string): string;
|
|
37
37
|
protected resolveFormPath(override?: string): string;
|
|
38
38
|
protected resolveHooksPath(override?: string): string;
|
|
39
|
+
protected resolveMenuPath(override?: string): string;
|
|
39
40
|
protected resolveOptionsPath(override?: string): string;
|
|
40
41
|
protected resolveSeoPath(override?: string): string;
|
|
41
42
|
protected resolveSnippetsPath(override?: string): string;
|
|
43
|
+
protected resolveThemeStylesPath(override?: string): string;
|
|
42
44
|
private pickEnvironment;
|
|
43
45
|
private resolveEnvironment;
|
|
44
46
|
private resolveEnvironmentForConfiguredProject;
|
package/dist/lib/base.js
CHANGED
|
@@ -147,6 +147,9 @@ export class LoopressCommand extends Command {
|
|
|
147
147
|
resolveHooksPath(override) {
|
|
148
148
|
return resolveResourceDir('hooks', this.localConfig, override);
|
|
149
149
|
}
|
|
150
|
+
resolveMenuPath(override) {
|
|
151
|
+
return resolveResourceDir('menu', this.localConfig, override);
|
|
152
|
+
}
|
|
150
153
|
resolveOptionsPath(override) {
|
|
151
154
|
return resolveResourceDir('options', this.localConfig, override);
|
|
152
155
|
}
|
|
@@ -156,6 +159,9 @@ export class LoopressCommand extends Command {
|
|
|
156
159
|
resolveSnippetsPath(override) {
|
|
157
160
|
return resolveResourceDir('snippets', this.localConfig, override);
|
|
158
161
|
}
|
|
162
|
+
resolveThemeStylesPath(override) {
|
|
163
|
+
return resolveResourceDir('themeStyles', this.localConfig, override);
|
|
164
|
+
}
|
|
159
165
|
pickEnvironment(project, envName) {
|
|
160
166
|
const env = project.environments[envName];
|
|
161
167
|
if (!env) {
|
package/dist/lib/dev-watch.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type LoopressLocalConfig } from '../utils/loopress-config.js';
|
|
2
|
-
export type ResourceType = 'api' | 'plugins' | 'snippets';
|
|
2
|
+
export type ResourceType = 'api' | 'pages' | 'plugins' | 'snippets';
|
|
3
3
|
export declare const RESOURCE_TYPES: ResourceType[];
|
|
4
4
|
export type WatchTarget = {
|
|
5
5
|
commandId: string;
|
package/dist/lib/dev-watch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join, sep } from 'node:path';
|
|
3
|
-
export const RESOURCE_TYPES = ['snippets', 'api', 'plugins'];
|
|
3
|
+
export const RESOURCE_TYPES = ['snippets', 'pages', 'api', 'plugins'];
|
|
4
4
|
// `only` narrows the default set, `skip` then removes from whatever that set is, same
|
|
5
5
|
// precedence a combined `--only=a,b --skip=b` reads as: watch a, b, then drop b.
|
|
6
6
|
export function resolveResourceTypes(only, skip = []) {
|
|
@@ -15,6 +15,7 @@ export function buildWatchTargets(types, localConfig, cwd) {
|
|
|
15
15
|
const rootDir = join(cwd, localConfig.rootDir ?? '.');
|
|
16
16
|
const candidates = {
|
|
17
17
|
api: { commandId: 'api:push', path: join(rootDir, localConfig.apiDir ?? 'api'), type: 'api' },
|
|
18
|
+
pages: { commandId: 'page:push', path: join(rootDir, localConfig.pageDir ?? 'pages'), type: 'pages' },
|
|
18
19
|
plugins: { commandId: 'plugin:push', path: join(cwd, 'loopress.json'), type: 'plugins' },
|
|
19
20
|
snippets: { commandId: 'snippet:push', path: join(rootDir, localConfig.snippetsDir ?? 'snippets'), type: 'snippets' },
|
|
20
21
|
};
|
|
@@ -1,51 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
// Same shape as load-files.ts's own walker, kept separate rather than shared: this one
|
|
4
|
-
// filters against a list of extensions (snippet pull needs more than one), that one
|
|
5
|
-
// against a single extension, and reconciling the two would cost more than the ~15 duplicated
|
|
6
|
-
// lines it would save.
|
|
7
|
-
async function walk(dir, extensions) {
|
|
8
|
-
let entries;
|
|
9
|
-
try {
|
|
10
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
11
|
-
}
|
|
12
|
-
catch (error) {
|
|
13
|
-
if (error.code === 'ENOENT')
|
|
14
|
-
return [];
|
|
15
|
-
throw error;
|
|
16
|
-
}
|
|
17
|
-
const relativePaths = [];
|
|
18
|
-
for (const entry of entries) {
|
|
19
|
-
if (entry.isDirectory()) {
|
|
20
|
-
// join() builds the real OS path to actually walk, but the returned identity is always
|
|
21
|
-
// '/'-joined regardless of platform: it's matched against `keep`, filenames the server
|
|
22
|
-
// sends with '/', never the OS separator. join() here (backslash on Windows) would make
|
|
23
|
-
// every nested file's identity never match, findOrphanedFiles() would then report a
|
|
24
|
-
// live, kept file as orphaned and pull.ts would delete it.
|
|
25
|
-
const nested = await walk(join(dir, entry.name), extensions);
|
|
26
|
-
relativePaths.push(...nested.map((relativePath) => `${entry.name}/${relativePath}`));
|
|
27
|
-
}
|
|
28
|
-
else if (extensions.includes(extname(entry.name))) {
|
|
29
|
-
relativePaths.push(entry.name);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return relativePaths;
|
|
33
|
-
}
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { readdirTolerant, walkFiles } from './readdir-tolerant.js';
|
|
34
3
|
export async function findOrphanedFiles(dir, keep, matcher) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
try {
|
|
41
|
-
files = await readdir(dir);
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
if (error.code === 'ENOENT')
|
|
45
|
-
return [];
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
4
|
+
// walkFiles already returns '/'-joined identities regardless of platform: matched against
|
|
5
|
+
// `keep`, filenames the server sends with '/', never the OS separator.
|
|
6
|
+
const files = matcher.recursive ? await walkFiles(dir) : await readdirTolerant(dir);
|
|
49
7
|
return files.filter((file) => {
|
|
50
8
|
const ext = extname(file);
|
|
51
9
|
if (!matcher.extensions.includes(ext))
|
package/dist/lib/load-files.js
CHANGED
|
@@ -1,52 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { extname, join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
// subdirectories, e.g. api/invoice-pdf/[order_id].php); every other resource type stays
|
|
5
|
-
// flat. withFileTypes avoids a second stat() per entry to tell files from directories.
|
|
6
|
-
async function walk(dir, extension) {
|
|
7
|
-
let entries;
|
|
8
|
-
try {
|
|
9
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
10
|
-
}
|
|
11
|
-
catch (error) {
|
|
12
|
-
if (error.code === 'ENOENT')
|
|
13
|
-
return [];
|
|
14
|
-
throw error;
|
|
15
|
-
}
|
|
16
|
-
const filePaths = [];
|
|
17
|
-
for (const entry of entries) {
|
|
18
|
-
const entryPath = join(dir, entry.name);
|
|
19
|
-
if (entry.isDirectory()) {
|
|
20
|
-
filePaths.push(...(await walk(entryPath, extension)));
|
|
21
|
-
}
|
|
22
|
-
else if (extname(entry.name) === extension) {
|
|
23
|
-
filePaths.push(entryPath);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return filePaths;
|
|
27
|
-
}
|
|
3
|
+
import { readdirTolerant, walkFiles } from './readdir-tolerant.js';
|
|
28
4
|
// Loads every `<extension>` file of a directory through `parse`. One file is read in
|
|
29
5
|
// isolation: a corrupted or hand-broken file (unreadable, bad JSON, failed validation) must
|
|
30
6
|
// only skip that file, never abort loading the rest of the directory, so `parse` throwing is
|
|
31
7
|
// reported through `onSkip` and the loop moves on. ENOENT on the directory itself means
|
|
32
8
|
// "nothing to load", not an error.
|
|
33
9
|
export async function loadFiles(dir, options) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
filePaths = await walk(dir, options.extension);
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
let entries;
|
|
40
|
-
try {
|
|
41
|
-
entries = await readdir(dir);
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
if (error.code === 'ENOENT')
|
|
45
|
-
return [];
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
48
|
-
filePaths = entries.filter((entry) => extname(entry) === options.extension).map((entry) => join(dir, entry));
|
|
49
|
-
}
|
|
10
|
+
const entries = options.recursive ? await walkFiles(dir) : await readdirTolerant(dir);
|
|
11
|
+
const filePaths = entries.filter((entry) => extname(entry) === options.extension).map((entry) => join(dir, entry));
|
|
50
12
|
const items = [];
|
|
51
13
|
for (const filePath of filePaths) {
|
|
52
14
|
try {
|