@loopress/cli 0.27.0 → 0.29.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 +448 -189
- package/bin/dev.js +0 -0
- package/dist/commands/acf/pull.d.ts +9 -1
- package/dist/commands/acf/pull.js +2 -0
- package/dist/commands/acf/push.d.ts +9 -1
- package/dist/commands/acf/push.js +3 -1
- package/dist/commands/api/publish.js +1 -1
- package/dist/commands/app/pull.js +17 -4
- package/dist/commands/app/push.d.ts +3 -0
- package/dist/commands/app/push.js +30 -6
- package/dist/commands/composer/push.js +3 -21
- package/dist/commands/diff.js +1 -1
- package/dist/commands/form/pull.d.ts +9 -1
- package/dist/commands/form/pull.js +3 -1
- package/dist/commands/form/push.d.ts +7 -1
- package/dist/commands/form/push.js +3 -1
- package/dist/commands/init.js +6 -36
- package/dist/commands/menu/pull.d.ts +7 -1
- package/dist/commands/menu/pull.js +2 -0
- package/dist/commands/menu/push.d.ts +7 -1
- package/dist/commands/menu/push.js +3 -1
- package/dist/commands/option/pull.js +4 -4
- package/dist/commands/option/push.js +2 -0
- package/dist/commands/page/push.js +3 -0
- package/dist/commands/plugin/add.js +6 -3
- package/dist/commands/plugin/audit.js +4 -5
- package/dist/commands/plugin/pull.d.ts +7 -1
- package/dist/commands/plugin/pull.js +68 -14
- package/dist/commands/plugin/push.d.ts +5 -1
- package/dist/commands/plugin/push.js +92 -38
- package/dist/commands/plugin/status.d.ts +1 -0
- package/dist/commands/plugin/status.js +21 -2
- package/dist/commands/project/config.js +12 -8
- package/dist/commands/promote.d.ts +2 -0
- package/dist/commands/promote.js +69 -8
- package/dist/commands/pull.js +2 -2
- package/dist/commands/push.d.ts +1 -0
- package/dist/commands/push.js +40 -5
- package/dist/commands/seo/pull.d.ts +8 -1
- package/dist/commands/seo/pull.js +2 -0
- package/dist/commands/seo/push.d.ts +7 -1
- package/dist/commands/seo/push.js +3 -1
- package/dist/commands/snippet/publish.js +1 -1
- package/dist/commands/snippet/pull.js +1 -1
- package/dist/commands/snippet/push.js +1 -1
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +4 -4
- package/dist/commands/theme/pull.d.ts +6 -8
- package/dist/commands/theme/pull.js +30 -26
- package/dist/commands/theme/push.d.ts +8 -9
- package/dist/commands/theme/push.js +54 -111
- package/dist/commands/theme/style/diff.d.ts +2 -0
- package/dist/commands/{theme-styles → theme/style}/diff.js +2 -1
- package/dist/commands/{theme-styles → theme/style}/pull.d.ts +8 -2
- package/dist/commands/{theme-styles → theme/style}/pull.js +6 -4
- package/dist/commands/{theme-styles → theme/style}/push.d.ts +9 -2
- package/dist/commands/{theme-styles → theme/style}/push.js +9 -6
- package/dist/commands/theme/style/rollback.d.ts +2 -0
- package/dist/commands/theme/style/rollback.js +6 -0
- package/dist/commands/theme/template/diff.d.ts +10 -0
- package/dist/commands/theme/template/diff.js +13 -0
- package/dist/commands/theme/template/list.d.ts +13 -0
- package/dist/commands/theme/template/list.js +23 -0
- package/dist/commands/theme/template/push.d.ts +20 -0
- package/dist/commands/theme/template/push.js +44 -0
- package/dist/commands/theme/version/pull.d.ts +23 -0
- package/dist/commands/theme/version/pull.js +43 -0
- package/dist/commands/theme/version/push.d.ts +22 -0
- package/dist/commands/theme/version/push.js +123 -0
- package/dist/hooks/finally.d.ts +2 -1
- package/dist/hooks/finally.js +27 -4
- package/dist/lib/api-client.js +3 -1
- package/dist/lib/app-build.d.ts +5 -0
- package/dist/lib/app-build.js +69 -0
- package/dist/lib/app-manifest.d.ts +4 -0
- package/dist/lib/app-manifest.js +10 -2
- package/dist/lib/base.d.ts +1 -0
- package/dist/lib/base.js +14 -2
- package/dist/lib/bootstrap-full-install.js +45 -14
- package/dist/lib/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +3 -1
- package/dist/lib/diff-command.d.ts +1 -0
- package/dist/lib/diff-command.js +6 -4
- package/dist/lib/diff-state.js +1 -0
- package/dist/lib/github-release.js +0 -1
- package/dist/lib/local-callback-server.d.ts +12 -6
- package/dist/lib/local-callback-server.js +21 -22
- package/dist/lib/php-files-command.js +3 -1
- package/dist/lib/push-command.d.ts +5 -0
- package/dist/lib/push-command.js +30 -8
- package/dist/lib/resource-state.js +47 -4
- package/dist/lib/rollback-command.d.ts +1 -0
- package/dist/lib/rollback-command.js +8 -6
- package/dist/lib/rotate-app-password.js +2 -2
- package/dist/lib/snapshot-store.js +2 -2
- package/dist/lib/validate-local.js +6 -0
- package/dist/lib/wp-authorize-flow.d.ts +10 -17
- package/dist/lib/wp-authorize-flow.js +24 -28
- package/dist/lib/wp-client.d.ts +4 -1
- package/dist/lib/wp-client.js +25 -1
- package/dist/lib/wp-site-diagnostic.js +37 -0
- package/dist/types/plugin.d.ts +8 -1
- package/dist/types/plugin.js +4 -1
- package/dist/types/project-config.generated.d.ts +19 -2
- package/dist/utils/composer.d.ts +16 -0
- package/dist/utils/composer.js +144 -0
- package/dist/utils/option-format.d.ts +3 -1
- package/dist/utils/option-format.js +14 -3
- package/dist/utils/page-format.d.ts +12 -0
- package/dist/utils/page-format.js +40 -28
- package/dist/utils/plugin-sync.js +1 -1
- package/dist/utils/plugins.d.ts +9 -4
- package/dist/utils/plugins.js +16 -7
- package/dist/utils/resource-dirs.d.ts +2 -0
- package/dist/utils/resource-dirs.js +6 -2
- package/dist/utils/seo-format.d.ts +2 -2
- package/dist/utils/template-format.d.ts +31 -0
- package/dist/utils/template-format.js +43 -0
- package/oclif.manifest.json +841 -63
- package/package.json +24 -17
- package/schemas/project-config.schema.json +25 -4
- package/dist/commands/theme-styles/diff.d.ts +0 -2
- package/dist/commands/theme-styles/rollback.d.ts +0 -2
- package/dist/commands/theme-styles/rollback.js +0 -5
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import got from 'got';
|
|
2
2
|
export const REQUEST_TIMEOUT_MS = 10_000;
|
|
3
|
+
/**
|
|
4
|
+
* WordPress 7.0 is the first version that accepts a loopback `http://127.0.0.1` success_url on
|
|
5
|
+
* authorize-application.php whatever the site's environment type (wp_is_authorize_application_redirect_url_valid).
|
|
6
|
+
* Earlier versions only accept it when WP_ENVIRONMENT_TYPE is `local`, which isn't exposed publicly,
|
|
7
|
+
* so a 6.x site is sent to manual entry even though a local one would have worked.
|
|
8
|
+
*/
|
|
9
|
+
const MIN_BROWSER_AUTH_MAJOR = 7;
|
|
3
10
|
/**
|
|
4
11
|
* Pre-flight checks run before starting the browser authorization flow, so failures
|
|
5
12
|
* (unreachable site, blocked REST API, Application Passwords disabled) surface as an
|
|
@@ -29,8 +36,38 @@ export async function diagnoseWpSite(siteUrl) {
|
|
|
29
36
|
reason: `Application Passwords are not available on ${siteUrl}. The site may be older than WordPress 5.6, require HTTPS, or have the feature disabled by a plugin or filter.`,
|
|
30
37
|
};
|
|
31
38
|
}
|
|
39
|
+
const version = await detectWpVersion(siteUrl);
|
|
40
|
+
if (version && Number(version.split('.', 1)[0]) < MIN_BROWSER_AUTH_MAJOR) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
reason: `${siteUrl} runs WordPress ${version}. Before WordPress ${MIN_BROWSER_AUTH_MAJOR}.0, WordPress refuses to send the Application Password back to the CLI on this machine (http://127.0.0.1), so authorizing in the browser is not possible. Create an Application Password in wp-admin (Users > Profile) and enter it below.`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
32
46
|
return { ok: true };
|
|
33
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Best effort, from the public generator tag: the feed's `<generator>` first, then the home page
|
|
50
|
+
* `<meta name="generator">`. Security plugins often strip both, in which case this returns
|
|
51
|
+
* undefined and the browser flow is attempted anyway.
|
|
52
|
+
*/
|
|
53
|
+
async function detectWpVersion(siteUrl) {
|
|
54
|
+
const sources = [
|
|
55
|
+
{ pattern: /<generator>[^<]*[?&]v=(\d+(?:\.\d+)*)/, url: `${siteUrl}/feed/` },
|
|
56
|
+
{ pattern: /<meta name="generator" content="WordPress (\d+(?:\.\d+)*)/, url: `${siteUrl}/` },
|
|
57
|
+
];
|
|
58
|
+
for (const { pattern, url } of sources) {
|
|
59
|
+
try {
|
|
60
|
+
const body = await got.get(url, { timeout: { request: REQUEST_TIMEOUT_MS } }).text();
|
|
61
|
+
const version = pattern.exec(body)?.[1];
|
|
62
|
+
if (version)
|
|
63
|
+
return version;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Feed disabled or page unreachable: try the next source.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
34
71
|
function describe(error) {
|
|
35
72
|
return error instanceof Error ? error.message : String(error);
|
|
36
73
|
}
|
package/dist/types/plugin.d.ts
CHANGED
|
@@ -12,4 +12,11 @@ export type WpNativePlugin = {
|
|
|
12
12
|
status: 'active' | 'inactive' | 'network-active';
|
|
13
13
|
version: string;
|
|
14
14
|
};
|
|
15
|
-
export type
|
|
15
|
+
export type PluginPin = string | {
|
|
16
|
+
active?: boolean;
|
|
17
|
+
version: string;
|
|
18
|
+
};
|
|
19
|
+
export type PluginManifest = Record<string, PluginPin>;
|
|
20
|
+
export declare const pinVersion: (pin: PluginPin) => string;
|
|
21
|
+
export declare const pinActive: (pin: PluginPin) => boolean;
|
|
22
|
+
export declare const pinVersions: (manifest: PluginManifest) => Record<string, string>;
|
package/dist/types/plugin.js
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const pinVersion = (pin) => (typeof pin === 'string' ? pin : pin.version);
|
|
2
|
+
export const pinActive = (pin) => typeof pin === 'string' || pin.active !== false;
|
|
3
|
+
// The version-only view the Composer sync endpoint expects.
|
|
4
|
+
export const pinVersions = (manifest) => Object.fromEntries(Object.entries(manifest).map(([slug, pin]) => [slug, pinVersion(pin)]));
|
|
@@ -39,6 +39,14 @@ export interface LoopressProjectConfiguration {
|
|
|
39
39
|
* Directory where static HTML pages (one <slug>.html per WordPress page, managed by Loopress) are read from, relative to rootDir.
|
|
40
40
|
*/
|
|
41
41
|
pageDir?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Directory where block templates (one <slug>.html per template, written to the Loopress child theme of the active block theme) are read from, relative to rootDir. A static page uses one with its `template` header.
|
|
44
|
+
*/
|
|
45
|
+
templateDir?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Directory where block template parts (one <slug>.html per part, written to the Loopress child theme of the active block theme) are read from, relative to rootDir.
|
|
48
|
+
*/
|
|
49
|
+
partDir?: string;
|
|
42
50
|
/**
|
|
43
51
|
* Directory where explicitly tracked WordPress options (wp_options rows) are read from and written to, relative to rootDir. Only options added via `lps option add` live here, this is never the whole options table.
|
|
44
52
|
*/
|
|
@@ -56,10 +64,19 @@ export interface LoopressProjectConfiguration {
|
|
|
56
64
|
*/
|
|
57
65
|
projectId?: string;
|
|
58
66
|
/**
|
|
59
|
-
* WordPress.org plugins managed by Loopress, installed via Composer + WPackagist on the site. Keys are plugin slugs. A value is either an exact version to pin (e.g. "9.4.2") or "latest" to track the newest release on every push. Removing an entry uninstalls the plugin on the next push. Ignored when a composer.json exists in rootDir (that file becomes authoritative).
|
|
67
|
+
* WordPress.org plugins managed by Loopress, installed via Composer + WPackagist on the site. Keys are plugin slugs. A value is either an exact version to pin (e.g. "9.4.2") or "latest" to track the newest release on every push, and the plugin is kept active. An object with "active": false keeps the plugin installed but inactive. Removing an entry uninstalls the plugin on the next push. Ignored when a composer.json exists in rootDir (that file becomes authoritative).
|
|
60
68
|
*/
|
|
61
69
|
plugins?: {
|
|
62
|
-
[k: string]: string
|
|
70
|
+
[k: string]: string | {
|
|
71
|
+
/**
|
|
72
|
+
* Exact version to pin, or "latest".
|
|
73
|
+
*/
|
|
74
|
+
version: string;
|
|
75
|
+
/**
|
|
76
|
+
* false keeps the plugin installed but inactive. Defaults to true.
|
|
77
|
+
*/
|
|
78
|
+
active?: boolean;
|
|
79
|
+
};
|
|
63
80
|
};
|
|
64
81
|
/**
|
|
65
82
|
* WordPress.org themes managed by Loopress, installed via Composer + WPackagist on the site. Keys are theme slugs; values are an exact version or "latest". Loopress manages installed theme versions only, it never switches the active theme. Ignored when a composer.json exists in rootDir.
|
package/dist/utils/composer.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type SyncIntent } from './plugin-sync.js';
|
|
2
|
+
import { type MergeResult } from './plugins.js';
|
|
3
|
+
export declare const NOT_ON_WORDPRESS_ORG = "not on WordPress.org";
|
|
1
4
|
export type ComposerJson = {
|
|
2
5
|
config?: {
|
|
3
6
|
'allow-plugins'?: Record<string, boolean>;
|
|
@@ -16,3 +19,16 @@ export type ComposerJson = {
|
|
|
16
19
|
export declare function readComposerJson(): Promise<ComposerJson | null>;
|
|
17
20
|
export declare function readComposerLock(): Promise<null | string>;
|
|
18
21
|
export declare function getComposerManagedSlugs(composerJson: ComposerJson): string[];
|
|
22
|
+
export type ComposerPullResult = MergeResult<string> & {
|
|
23
|
+
skipped: Array<{
|
|
24
|
+
reason: string;
|
|
25
|
+
slug: string;
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
28
|
+
export declare function pullIntoComposerJson(path: string, kind: 'plugin' | 'theme', incoming: Record<string, string>, { dryRun, log }: {
|
|
29
|
+
dryRun: boolean;
|
|
30
|
+
log: (message: string) => void;
|
|
31
|
+
}): Promise<ComposerPullResult>;
|
|
32
|
+
export declare function toIntent(require: Record<string, string>): SyncIntent;
|
|
33
|
+
export declare function requireToInstall(composerJson: ComposerJson, includeDev: boolean): Record<string, string>;
|
|
34
|
+
export declare function wpackagistRequire(composerJson: ComposerJson, kind: 'plugin' | 'theme', includeDev?: boolean): Record<string, string>;
|
package/dist/utils/composer.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import writeFileAtomic from 'write-file-atomic';
|
|
5
|
+
import { isExactVersion } from './version.js';
|
|
6
|
+
// Why pullIntoComposerJson skips a plugin it can't install: premium or custom, not on WPackagist.
|
|
7
|
+
export const NOT_ON_WORDPRESS_ORG = 'not on WordPress.org';
|
|
4
8
|
export async function readComposerJson() {
|
|
5
9
|
const path = join(process.cwd(), 'composer.json');
|
|
6
10
|
if (!existsSync(path))
|
|
@@ -31,3 +35,143 @@ export function getComposerManagedSlugs(composerJson) {
|
|
|
31
35
|
.filter((pkg) => pkg.startsWith('wpackagist-plugin/'))
|
|
32
36
|
.map((pkg) => pkg.slice('wpackagist-plugin/'.length));
|
|
33
37
|
}
|
|
38
|
+
// A slug WPackagist can't serve makes `composer update` fail for the whole file, libraries
|
|
39
|
+
// included. WPackagist mirrors WordPress.org, whose API answers a clean 404 for premium and
|
|
40
|
+
// custom plugins/themes. Any other failure throws before composer.json is written.
|
|
41
|
+
async function isOnWordPressOrg(kind, slug) {
|
|
42
|
+
const url = `https://api.wordpress.org/${kind}s/info/1.2/?action=${kind}_information&slug=${encodeURIComponent(slug)}`;
|
|
43
|
+
const fail = (why) => new Error(`Could not check whether the ${kind} "${slug}" is on WordPress.org (${why}), so composer.json was not modified. ` +
|
|
44
|
+
'Loopress only adds packages WPackagist can serve. Retry once api.wordpress.org is reachable.');
|
|
45
|
+
let response;
|
|
46
|
+
try {
|
|
47
|
+
response = await fetch(url);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw fail(`network error: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
if (response.status === 404)
|
|
53
|
+
return false;
|
|
54
|
+
if (!response.ok)
|
|
55
|
+
throw fail(`HTTP ${response.status}`);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
// Rewrites a JSON file the way it was indented (Composer itself writes 4 spaces), so a pull only
|
|
59
|
+
// shows the entries it changed in the git diff. A file with no indentation to copy gets 4 spaces.
|
|
60
|
+
function stringifyLike(original, value) {
|
|
61
|
+
const indent = /^([ \t]+)"/m.exec(original)?.[1] ?? ' '.repeat(4);
|
|
62
|
+
return JSON.stringify(value, null, indent) + (original.endsWith('\n') ? '\n' : '');
|
|
63
|
+
}
|
|
64
|
+
// A package already declared in composer.json: `pin` means the pull may move it (an exact pin,
|
|
65
|
+
// or already live), `skip` gives the reason it must stay as is.
|
|
66
|
+
function classifyDeclared(declared, live, inRequire) {
|
|
67
|
+
if (!inRequire)
|
|
68
|
+
return { skip: 'declared in require-dev' };
|
|
69
|
+
if (declared === live || isExactVersion(declared))
|
|
70
|
+
return { pin: declared };
|
|
71
|
+
return { skip: `keeps constraint ${declared}, live ${live}` };
|
|
72
|
+
}
|
|
73
|
+
// Sorts every installed package into the result, and returns the undeclared slugs that still
|
|
74
|
+
// need a WordPress.org lookup before they can be added.
|
|
75
|
+
function planComposerPull(composerJson, prefix, incoming) {
|
|
76
|
+
const require = composerJson.require ?? {};
|
|
77
|
+
const allPackages = { ...require, ...composerJson['require-dev'] };
|
|
78
|
+
const providedBy = new Map(Object.keys(allPackages)
|
|
79
|
+
.filter((name) => !name.startsWith(prefix) && name.includes('/'))
|
|
80
|
+
.map((name) => [name.split('/', 2)[1], name]));
|
|
81
|
+
const result = { added: [], merged: {}, skipped: [], updated: [] };
|
|
82
|
+
const candidates = [];
|
|
83
|
+
for (const [slug, version] of Object.entries(incoming)) {
|
|
84
|
+
const name = prefix + slug;
|
|
85
|
+
const provider = providedBy.get(slug);
|
|
86
|
+
if (Object.hasOwn(allPackages, name)) {
|
|
87
|
+
const outcome = classifyDeclared(allPackages[name], version, Object.hasOwn(require, name));
|
|
88
|
+
if ('skip' in outcome) {
|
|
89
|
+
result.skipped.push({ reason: outcome.skip, slug });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
result.merged[slug] = version;
|
|
93
|
+
if (outcome.pin !== version)
|
|
94
|
+
result.updated.push({ from: outcome.pin, slug, to: version });
|
|
95
|
+
}
|
|
96
|
+
else if (provider) {
|
|
97
|
+
result.skipped.push({ reason: `provided by ${provider}`, slug });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
candidates.push(slug);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { candidates, result };
|
|
104
|
+
}
|
|
105
|
+
// `lps plugin pull` / `lps theme version pull` on a project that has a composer.json: pin installed
|
|
106
|
+
// packages to their live versions as `wpackagist-<kind>/<slug>` under `require`, leaving every
|
|
107
|
+
// other key untouched. The server adds the WPackagist repository and composer/installers on push.
|
|
108
|
+
// - A declared version constraint (^9.4, *, ...) is the user's intent: never overwritten, only an
|
|
109
|
+
// exact pin moves to the live version.
|
|
110
|
+
// - Slugs already declared in require-dev stay there instead of being duplicated into require.
|
|
111
|
+
// - A slug provided by another package (acme/<slug>) or absent from WordPress.org is skipped.
|
|
112
|
+
export async function pullIntoComposerJson(path, kind, incoming, { dryRun, log }) {
|
|
113
|
+
const raw = await readFile(path, 'utf8');
|
|
114
|
+
const composerJson = JSON.parse(raw);
|
|
115
|
+
const prefix = `wpackagist-${kind}/`;
|
|
116
|
+
const { candidates, result } = planComposerPull(composerJson, prefix, incoming);
|
|
117
|
+
const found = await Promise.all(candidates.map(async (slug) => isOnWordPressOrg(kind, slug)));
|
|
118
|
+
for (const [i, slug] of candidates.entries()) {
|
|
119
|
+
if (found[i]) {
|
|
120
|
+
result.added.push(slug);
|
|
121
|
+
result.merged[slug] = incoming[slug];
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
result.skipped.push({ reason: NOT_ON_WORDPRESS_ORG, slug });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!dryRun) {
|
|
128
|
+
const require = { ...composerJson.require };
|
|
129
|
+
for (const [slug, version] of Object.entries(result.merged))
|
|
130
|
+
require[prefix + slug] = version;
|
|
131
|
+
// Atomic: an interrupted write must never leave the user's committed composer.json truncated.
|
|
132
|
+
await writeFileAtomic(path, stringifyLike(raw, { ...composerJson, require }), 'utf8');
|
|
133
|
+
}
|
|
134
|
+
log(`${dryRun ? '[dry-run] Would pin' : 'Pinned'} ${Object.keys(result.merged).length} ${kind}s in composer.json`);
|
|
135
|
+
if (result.added.length > 0)
|
|
136
|
+
log(` + Added: ${result.added.join(', ')}`);
|
|
137
|
+
for (const u of result.updated)
|
|
138
|
+
log(` ~ Updated: ${u.slug} ${u.from} → ${u.to}`);
|
|
139
|
+
for (const s of result.skipped)
|
|
140
|
+
log(` - Skipped: ${s.slug} (${s.reason})`);
|
|
141
|
+
if (!dryRun && (result.added.length > 0 || result.updated.length > 0))
|
|
142
|
+
log('Run `lps composer push` to apply.');
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
// Split a composer.json `require` map into the three intent namespaces the sync endpoint
|
|
146
|
+
// understands. `composer/installers` is owned by the server scaffold, never sent.
|
|
147
|
+
export function toIntent(require) {
|
|
148
|
+
const intent = { libraries: {}, plugins: {}, themes: {} };
|
|
149
|
+
for (const [name, constraint] of Object.entries(require)) {
|
|
150
|
+
if (name === 'composer/installers')
|
|
151
|
+
continue;
|
|
152
|
+
if (name.startsWith('wpackagist-plugin/')) {
|
|
153
|
+
intent.plugins[name.slice('wpackagist-plugin/'.length)] = constraint;
|
|
154
|
+
}
|
|
155
|
+
else if (name.startsWith('wpackagist-theme/')) {
|
|
156
|
+
intent.themes[name.slice('wpackagist-theme/'.length)] = constraint;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
intent.libraries[name] = constraint;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return intent;
|
|
163
|
+
}
|
|
164
|
+
// What a push installs: `require`, plus `require-dev` on the local environment only (Query
|
|
165
|
+
// Monitor, or a premium plugin's free edition to develop against). A remote site never gets
|
|
166
|
+
// `require-dev`, the same split as `composer install --no-dev` on a production server.
|
|
167
|
+
export function requireToInstall(composerJson, includeDev) {
|
|
168
|
+
return includeDev ? { ...composerJson.require, ...composerJson['require-dev'] } : { ...composerJson.require };
|
|
169
|
+
}
|
|
170
|
+
// The `wpackagist-<kind>/<slug>` entries of what a push installs (see requireToInstall), keyed
|
|
171
|
+
// by slug, with the version or constraint as written.
|
|
172
|
+
export function wpackagistRequire(composerJson, kind, includeDev = false) {
|
|
173
|
+
const prefix = `wpackagist-${kind}/`;
|
|
174
|
+
return Object.fromEntries(Object.entries(requireToInstall(composerJson, includeDev))
|
|
175
|
+
.filter(([name]) => name.startsWith(prefix))
|
|
176
|
+
.map(([name, version]) => [name.slice(prefix.length), version]));
|
|
177
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export declare const OPTIONS_ENDPOINT = "loopress/v1/options";
|
|
2
|
-
export declare function optionEndpoint(name: string): string;
|
|
2
|
+
export declare function optionEndpoint(name: string, refs?: OptionRefs): string;
|
|
3
3
|
export declare const RESERVED_OPTION_NAMES: readonly ["active_plugins", "stylesheet", "template"];
|
|
4
4
|
export declare const READONLY_BY_DEFAULT_OPTION_NAMES: readonly ["siteurl", "home", "db_version", "initial_db_version", "cron", "rewrite_rules", "WPLANG", "default_role", "users_can_register", "uninstall_plugins", "mailserver_url", "mailserver_login", "mailserver_pass", "mailserver_port"];
|
|
5
|
+
export type OptionRefs = Record<string, string>;
|
|
5
6
|
export type LocalOption = {
|
|
6
7
|
autoload: string;
|
|
7
8
|
name: string;
|
|
8
9
|
readonly?: boolean;
|
|
10
|
+
refs?: OptionRefs;
|
|
9
11
|
value: unknown;
|
|
10
12
|
};
|
|
11
13
|
export type RemoteOption = {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export const OPTIONS_ENDPOINT = 'loopress/v1/options';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
// `refs` asks WordPress to return the declared post references as paths (see OptionRefs).
|
|
3
|
+
export function optionEndpoint(name, refs) {
|
|
4
|
+
const endpoint = `${OPTIONS_ENDPOINT}/${encodeURIComponent(name)}`;
|
|
5
|
+
return refs && Object.keys(refs).length > 0 ? `${endpoint}?refs=${encodeURIComponent(JSON.stringify(refs))}` : endpoint;
|
|
4
6
|
}
|
|
5
7
|
// Mirrors OptionsService::RESERVED_NAMES on the WordPress side (the actual, unbypassable
|
|
6
8
|
// enforcement lives there). Checked here too so `option add` fails fast with a clear pointer to
|
|
@@ -52,7 +54,7 @@ export function parseLocalOption(raw) {
|
|
|
52
54
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
53
55
|
throw new Error('not a JSON object');
|
|
54
56
|
}
|
|
55
|
-
const { autoload, name, value } = parsed;
|
|
57
|
+
const { autoload, name, refs, value } = parsed;
|
|
56
58
|
if (typeof name !== 'string' || name === '') {
|
|
57
59
|
throw new Error('missing a "name" string');
|
|
58
60
|
}
|
|
@@ -65,5 +67,14 @@ export function parseLocalOption(raw) {
|
|
|
65
67
|
if (value === undefined) {
|
|
66
68
|
throw new Error('missing a "value" field');
|
|
67
69
|
}
|
|
70
|
+
if (refs !== undefined && !isOptionRefs(refs)) {
|
|
71
|
+
throw new TypeError('"refs" must map a value path to a post type, e.g. {"purchase_page": "page"}');
|
|
72
|
+
}
|
|
68
73
|
return parsed;
|
|
69
74
|
}
|
|
75
|
+
function isOptionRefs(refs) {
|
|
76
|
+
return (typeof refs === 'object' &&
|
|
77
|
+
refs !== null &&
|
|
78
|
+
!Array.isArray(refs) &&
|
|
79
|
+
Object.entries(refs).every(([path, postType]) => path !== '' && typeof postType === 'string' && postType !== ''));
|
|
80
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export declare const PAGES_ENDPOINT = "loopress/v1/pages";
|
|
2
2
|
export declare const PAGE_SLUG_PATTERN: RegExp;
|
|
3
|
+
export type SlugRule = {
|
|
4
|
+
message: string;
|
|
5
|
+
pattern: RegExp;
|
|
6
|
+
};
|
|
3
7
|
export declare const PAGE_STATUSES: readonly ["draft", "publish"];
|
|
4
8
|
export type PageStatus = (typeof PAGE_STATUSES)[number];
|
|
5
9
|
export type Page = {
|
|
@@ -24,8 +28,16 @@ export type PageProblem = {
|
|
|
24
28
|
export declare function formatPageProblems(problems: PageProblem[]): string;
|
|
25
29
|
export declare function titleFromSlug(slug: string): string;
|
|
26
30
|
export declare function parsePageFile(slug: string, raw: string): Page;
|
|
31
|
+
export declare function parseHtmlHeader(raw: string, keys: readonly string[]): {
|
|
32
|
+
body: string;
|
|
33
|
+
meta: Record<string, string>;
|
|
34
|
+
};
|
|
27
35
|
export declare function readLocalPages(dir: string): Promise<{
|
|
28
36
|
pages: Page[];
|
|
29
37
|
problems: PageProblem[];
|
|
30
38
|
}>;
|
|
39
|
+
export declare function readLocalHtmlFiles<T>(dir: string, parse: (slug: string, raw: string) => T, slugRule?: SlugRule): Promise<{
|
|
40
|
+
files: T[];
|
|
41
|
+
problems: PageProblem[];
|
|
42
|
+
}>;
|
|
31
43
|
export declare function frontPageNote(page: PushedPage): string;
|
|
@@ -4,6 +4,10 @@ export const PAGES_ENDPOINT = 'loopress/v1/pages';
|
|
|
4
4
|
// Mirrored server-side in PagesController. No leading, trailing or doubled hyphen: WordPress's
|
|
5
5
|
// sanitize_title() would rewrite those, and the page would no longer match its file name.
|
|
6
6
|
export const PAGE_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
|
+
const PAGE_SLUG_RULE = {
|
|
8
|
+
message: 'the file name must be lowercase letters and digits separated by single hyphens (e.g. "legal-notice.html")',
|
|
9
|
+
pattern: PAGE_SLUG_PATTERN,
|
|
10
|
+
};
|
|
7
11
|
export const PAGE_STATUSES = ['draft', 'publish'];
|
|
8
12
|
const HEADER_KEYS = ['title', 'status', 'full-width', 'hide-title', 'template'];
|
|
9
13
|
function parseBoolean(key, value) {
|
|
@@ -35,27 +39,11 @@ export function titleFromSlug(slug) {
|
|
|
35
39
|
//
|
|
36
40
|
// A comment rather than YAML front matter so the file stays valid HTML, previewable as is in a
|
|
37
41
|
// browser. The header is stripped from what gets pushed. Throws on an unknown key or status so a
|
|
38
|
-
// typo never silently falls back to a default. `template` is the active theme
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// default template, the same silent no-op as `full-width`/`hide-title` on a theme that doesn't
|
|
42
|
-
// support them.
|
|
42
|
+
// typo never silently falls back to a default. `template` is a template slug the active theme
|
|
43
|
+
// offers for pages (one it ships, or a custom one from templates/); it isn't validated here,
|
|
44
|
+
// WordPress itself refuses an unknown one ("Invalid page template") and fails that page's push.
|
|
43
45
|
export function parsePageFile(slug, raw) {
|
|
44
|
-
const
|
|
45
|
-
const meta = {};
|
|
46
|
-
if (header) {
|
|
47
|
-
for (const line of header[1].split(/\r?\n/)) {
|
|
48
|
-
if (line.trim() === '')
|
|
49
|
-
continue;
|
|
50
|
-
const colon = line.indexOf(':');
|
|
51
|
-
if (colon === -1)
|
|
52
|
-
throw new Error(`header line "${line.trim()}" is not a "key: value" pair`);
|
|
53
|
-
const key = line.slice(0, colon).trim();
|
|
54
|
-
if (!HEADER_KEYS.includes(key))
|
|
55
|
-
throw new Error(`unknown header key "${key}" (allowed: ${HEADER_KEYS.join(', ')})`);
|
|
56
|
-
meta[key] = line.slice(colon + 1).trim();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
46
|
+
const { body, meta } = parseHtmlHeader(raw, HEADER_KEYS);
|
|
59
47
|
const status = meta.status ?? 'draft';
|
|
60
48
|
if (!PAGE_STATUSES.includes(status)) {
|
|
61
49
|
throw new Error(`status "${status}" is not one of ${PAGE_STATUSES.join(', ')}`);
|
|
@@ -63,28 +51,52 @@ export function parsePageFile(slug, raw) {
|
|
|
63
51
|
return {
|
|
64
52
|
fullWidth: parseBoolean('full-width', meta['full-width']),
|
|
65
53
|
hideTitle: parseBoolean('hide-title', meta['hide-title']),
|
|
66
|
-
html:
|
|
54
|
+
html: body,
|
|
67
55
|
slug,
|
|
68
56
|
status: status,
|
|
69
57
|
template: meta.template ?? '',
|
|
70
58
|
title: meta.title || titleFromSlug(slug),
|
|
71
59
|
};
|
|
72
60
|
}
|
|
61
|
+
// Splits the optional `key: value` HTML comment header off `raw`. Shared with templates/.
|
|
62
|
+
export function parseHtmlHeader(raw, keys) {
|
|
63
|
+
const header = /^\s*<!--([\s\S]*?)-->\r?\n?/.exec(raw);
|
|
64
|
+
const meta = {};
|
|
65
|
+
if (!header)
|
|
66
|
+
return { body: raw, meta };
|
|
67
|
+
for (const line of header[1].split(/\r?\n/)) {
|
|
68
|
+
if (line.trim() === '')
|
|
69
|
+
continue;
|
|
70
|
+
const colon = line.indexOf(':');
|
|
71
|
+
if (colon === -1)
|
|
72
|
+
throw new Error(`header line "${line.trim()}" is not a "key: value" pair`);
|
|
73
|
+
const key = line.slice(0, colon).trim();
|
|
74
|
+
if (!keys.includes(key))
|
|
75
|
+
throw new Error(`unknown header key "${key}" (allowed: ${keys.join(', ')})`);
|
|
76
|
+
meta[key] = line.slice(colon + 1).trim();
|
|
77
|
+
}
|
|
78
|
+
return { body: raw.slice(header[0].length), meta };
|
|
79
|
+
}
|
|
73
80
|
// Reads every page of `dir` and reports every problem at once instead of skipping bad files
|
|
74
81
|
// like the other resources do: a page push is declarative, so anything unexpected in the
|
|
75
82
|
// directory (a `.php`, a subdirectory, a bad header) must stop it before any network call.
|
|
76
83
|
// Dotfiles (.DS_Store, editor droppings) are ignored. A missing directory is "no pages".
|
|
77
84
|
export async function readLocalPages(dir) {
|
|
85
|
+
const { files, problems } = await readLocalHtmlFiles(dir, parsePageFile);
|
|
86
|
+
return { pages: files, problems };
|
|
87
|
+
}
|
|
88
|
+
// The directory rules of readLocalPages(), for any flat folder of `<slug>.html` files.
|
|
89
|
+
export async function readLocalHtmlFiles(dir, parse, slugRule = PAGE_SLUG_RULE) {
|
|
78
90
|
let entries;
|
|
79
91
|
try {
|
|
80
92
|
entries = await readdir(dir, { withFileTypes: true });
|
|
81
93
|
}
|
|
82
94
|
catch (error) {
|
|
83
95
|
if (error.code === 'ENOENT')
|
|
84
|
-
return {
|
|
96
|
+
return { files: [], problems: [] };
|
|
85
97
|
throw error;
|
|
86
98
|
}
|
|
87
|
-
const
|
|
99
|
+
const files = [];
|
|
88
100
|
const problems = [];
|
|
89
101
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
90
102
|
for (const entry of entries) {
|
|
@@ -92,7 +104,7 @@ export async function readLocalPages(dir) {
|
|
|
92
104
|
continue;
|
|
93
105
|
const filePath = join(dir, entry.name);
|
|
94
106
|
if (!entry.isFile()) {
|
|
95
|
-
problems.push({ file: filePath, message: `subdirectories are not supported, keep every
|
|
107
|
+
problems.push({ file: filePath, message: `subdirectories are not supported, keep every file at the top of ${dir}` });
|
|
96
108
|
continue;
|
|
97
109
|
}
|
|
98
110
|
if (extname(entry.name) !== '.html') {
|
|
@@ -100,18 +112,18 @@ export async function readLocalPages(dir) {
|
|
|
100
112
|
continue;
|
|
101
113
|
}
|
|
102
114
|
const slug = basename(entry.name, '.html');
|
|
103
|
-
if (!
|
|
104
|
-
problems.push({ file: filePath, message:
|
|
115
|
+
if (!slugRule.pattern.test(slug)) {
|
|
116
|
+
problems.push({ file: filePath, message: slugRule.message });
|
|
105
117
|
continue;
|
|
106
118
|
}
|
|
107
119
|
try {
|
|
108
|
-
|
|
120
|
+
files.push(parse(slug, await readFile(filePath, 'utf8')));
|
|
109
121
|
}
|
|
110
122
|
catch (error) {
|
|
111
123
|
problems.push({ file: filePath, message: error.message });
|
|
112
124
|
}
|
|
113
125
|
}
|
|
114
|
-
return {
|
|
126
|
+
return { files, problems };
|
|
115
127
|
}
|
|
116
128
|
export function frontPageNote(page) {
|
|
117
129
|
return page.frontPage === undefined ? '' : ` (${FRONT_PAGE_NOTES[page.frontPage]})`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Shared helpers for driving the server-side `loopress/v1/composer/sync` endpoint, used by
|
|
2
|
-
// `lps plugin push`, `lps theme push` and `lps composer push`. The endpoint runs Composer +
|
|
2
|
+
// `lps plugin push`, `lps theme version push` and `lps composer push`. The endpoint runs Composer +
|
|
3
3
|
// WPackagist inside WordPress; the CLI only ever sends intent (which slugs at which versions),
|
|
4
4
|
// never a composer.json, so the plugin stays the single authority on that file's shape.
|
|
5
5
|
import { compareVersions } from './version.js';
|
package/dist/utils/plugins.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type InstalledPlugin, type PluginManifest, type WpNativePlugin } from '../types/plugin.js';
|
|
1
|
+
import { type InstalledPlugin, type PluginManifest, type PluginPin, type WpNativePlugin } from '../types/plugin.js';
|
|
2
2
|
export type PluginDiff = {
|
|
3
3
|
collisions: Array<{
|
|
4
4
|
installedVersion: string;
|
|
@@ -9,6 +9,10 @@ export type PluginDiff = {
|
|
|
9
9
|
file: string;
|
|
10
10
|
slug: string;
|
|
11
11
|
}>;
|
|
12
|
+
toDeactivate: Array<{
|
|
13
|
+
file: string;
|
|
14
|
+
slug: string;
|
|
15
|
+
}>;
|
|
12
16
|
toInstall: Array<{
|
|
13
17
|
slug: string;
|
|
14
18
|
version: string;
|
|
@@ -21,16 +25,17 @@ export type PluginDiff = {
|
|
|
21
25
|
toRemove: string[];
|
|
22
26
|
untrackedActive: string[];
|
|
23
27
|
};
|
|
24
|
-
export type MergeResult = {
|
|
28
|
+
export type MergeResult<P extends PluginPin = PluginPin> = {
|
|
25
29
|
added: string[];
|
|
26
|
-
merged:
|
|
30
|
+
merged: Record<string, P>;
|
|
27
31
|
updated: Array<{
|
|
28
32
|
from: string;
|
|
29
33
|
slug: string;
|
|
30
34
|
to: string;
|
|
31
35
|
}>;
|
|
32
36
|
};
|
|
33
|
-
export declare function
|
|
37
|
+
export declare function describePin(pin: PluginPin): string;
|
|
38
|
+
export declare function mergePluginManifest<P extends PluginPin>(existing: Record<string, P>, incoming: Record<string, P>): MergeResult<P>;
|
|
34
39
|
export declare function parseInstalledPlugins(raw: WpNativePlugin[]): InstalledPlugin[];
|
|
35
40
|
export declare function diffPlugins(manifest: PluginManifest, installed: InstalledPlugin[], managedSlugs?: Set<string>): PluginDiff;
|
|
36
41
|
export declare function lockedWpackagistSlugs(composerLock: null | string, kind: 'plugin' | 'theme'): Set<string>;
|
package/dist/utils/plugins.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { pinActive, pinVersion, } from '../types/plugin.js';
|
|
2
|
+
import { isExactVersion } from './version.js';
|
|
3
|
+
// "1.7.2", or "1.7.2 (inactive)" for a pin that keeps the plugin inactive.
|
|
4
|
+
export function describePin(pin) {
|
|
5
|
+
return pinActive(pin) ? pinVersion(pin) : `${pinVersion(pin)} (inactive)`;
|
|
6
|
+
}
|
|
1
7
|
// Loopress must never manage itself: pulling it into loopress.json would make a later
|
|
2
8
|
// `plugin push` try to reinstall it from WordPress.org, where it doesn't exist, potentially
|
|
3
9
|
// clobbering the plugin's own directory in the process. Checked against every slug this
|
|
@@ -8,8 +14,8 @@ export function mergePluginManifest(existing, incoming) {
|
|
|
8
14
|
const merged = { ...existing, ...incoming };
|
|
9
15
|
const added = Object.keys(incoming).filter((s) => !Object.hasOwn(existing, s));
|
|
10
16
|
const updated = Object.keys(incoming)
|
|
11
|
-
.filter((s) => Object.hasOwn(existing, s) && existing[s] !== incoming[s])
|
|
12
|
-
.map((s) => ({ from: existing[s], slug: s, to: incoming[s] }));
|
|
17
|
+
.filter((s) => Object.hasOwn(existing, s) && describePin(existing[s]) !== describePin(incoming[s]))
|
|
18
|
+
.map((s) => ({ from: describePin(existing[s]), slug: s, to: describePin(incoming[s]) }));
|
|
13
19
|
return { added, merged, updated };
|
|
14
20
|
}
|
|
15
21
|
// WordPress core identifies each plugin by a `<folder>/<file>` id (or a bare `<file>` for a
|
|
@@ -47,9 +53,11 @@ managedSlugs = new Set()) {
|
|
|
47
53
|
const toInstall = [];
|
|
48
54
|
const toPin = [];
|
|
49
55
|
const toActivate = [];
|
|
56
|
+
const toDeactivate = [];
|
|
50
57
|
const collisions = [];
|
|
51
58
|
const inSync = [];
|
|
52
|
-
for (const [slug,
|
|
59
|
+
for (const [slug, pin] of Object.entries(manifest)) {
|
|
60
|
+
const wanted = pinVersion(pin);
|
|
53
61
|
const live = installedMap.get(slug);
|
|
54
62
|
if (!live) {
|
|
55
63
|
toInstall.push({ slug, version: wanted });
|
|
@@ -61,12 +69,13 @@ managedSlugs = new Set()) {
|
|
|
61
69
|
collisions.push({ installedVersion: live.version, slug });
|
|
62
70
|
continue;
|
|
63
71
|
}
|
|
64
|
-
if (wanted
|
|
72
|
+
if (isExactVersion(wanted) && live.version !== wanted) {
|
|
65
73
|
toPin.push({ from: live.version, slug, to: wanted });
|
|
66
74
|
continue;
|
|
67
75
|
}
|
|
68
|
-
if (
|
|
69
|
-
|
|
76
|
+
if (live.active !== pinActive(pin)) {
|
|
77
|
+
;
|
|
78
|
+
(live.active ? toDeactivate : toActivate).push({ file: live.file, slug });
|
|
70
79
|
continue;
|
|
71
80
|
}
|
|
72
81
|
inSync.push(slug);
|
|
@@ -75,7 +84,7 @@ managedSlugs = new Set()) {
|
|
|
75
84
|
const untrackedActive = installed
|
|
76
85
|
.filter((p) => p.active && !Object.hasOwn(manifest, p.slug) && !managedSlugs.has(p.slug))
|
|
77
86
|
.map((p) => p.slug);
|
|
78
|
-
return { collisions, inSync, toActivate, toInstall, toPin, toRemove, untrackedActive };
|
|
87
|
+
return { collisions, inSync, toActivate, toDeactivate, toInstall, toPin, toRemove, untrackedActive };
|
|
79
88
|
}
|
|
80
89
|
// wpackagist-plugin/<slug> or wpackagist-theme/<slug> entries from a composer.lock string.
|
|
81
90
|
export function lockedWpackagistSlugs(composerLock, kind) {
|
|
@@ -8,8 +8,10 @@ export declare const RESOURCE_DIR_DEFAULTS: {
|
|
|
8
8
|
readonly menu: "menus";
|
|
9
9
|
readonly options: "options";
|
|
10
10
|
readonly page: "pages";
|
|
11
|
+
readonly part: "parts";
|
|
11
12
|
readonly seo: "seo";
|
|
12
13
|
readonly snippets: "snippets";
|
|
14
|
+
readonly template: "templates";
|
|
13
15
|
readonly themeStyles: "theme";
|
|
14
16
|
};
|
|
15
17
|
export type ResourceDirKind = keyof typeof RESOURCE_DIR_DEFAULTS;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
// The default subdirectory for each file-backed resource, relative to rootDir.
|
|
3
|
-
//
|
|
2
|
+
// The default subdirectory for each file-backed resource, relative to rootDir. Used whenever
|
|
3
|
+
// loopress.json has no `<kind>Dir` key, which is the case for a fresh `lps init`.
|
|
4
4
|
export const RESOURCE_DIR_DEFAULTS = {
|
|
5
5
|
acf: 'acf',
|
|
6
6
|
api: 'api',
|
|
@@ -10,8 +10,10 @@ export const RESOURCE_DIR_DEFAULTS = {
|
|
|
10
10
|
menu: 'menus',
|
|
11
11
|
options: 'options',
|
|
12
12
|
page: 'pages',
|
|
13
|
+
part: 'parts',
|
|
13
14
|
seo: 'seo',
|
|
14
15
|
snippets: 'snippets',
|
|
16
|
+
template: 'templates',
|
|
15
17
|
themeStyles: 'theme',
|
|
16
18
|
};
|
|
17
19
|
const CONFIG_KEY = {
|
|
@@ -23,8 +25,10 @@ const CONFIG_KEY = {
|
|
|
23
25
|
menu: 'menuDir',
|
|
24
26
|
options: 'optionsDir',
|
|
25
27
|
page: 'pageDir',
|
|
28
|
+
part: 'partDir',
|
|
26
29
|
seo: 'seoDir',
|
|
27
30
|
snippets: 'snippetsDir',
|
|
31
|
+
template: 'templateDir',
|
|
28
32
|
themeStyles: 'themeStylesDir',
|
|
29
33
|
};
|
|
30
34
|
// Resolves where a resource's files live: an explicit override wins, otherwise the
|