@loopress/cli 0.33.0 → 0.35.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 +236 -207
- package/dist/commands/app/push.js +2 -1
- package/dist/commands/block/push.js +9 -4
- package/dist/commands/dev.d.ts +2 -2
- package/dist/commands/dev.js +10 -16
- package/dist/commands/diff.d.ts +6 -4
- package/dist/commands/diff.js +49 -29
- package/dist/commands/doctor.js +13 -3
- package/dist/commands/form/push.js +3 -2
- package/dist/commands/option/list.js +6 -1
- package/dist/commands/option/push.js +1 -0
- package/dist/commands/page/push.js +1 -0
- package/dist/commands/plugin/add.d.ts +1 -0
- package/dist/commands/plugin/add.js +50 -9
- package/dist/commands/plugin/audit.d.ts +1 -1
- package/dist/commands/plugin/audit.js +45 -23
- package/dist/commands/plugin/outdated.d.ts +22 -0
- package/dist/commands/plugin/outdated.js +67 -0
- package/dist/commands/plugin/push.d.ts +2 -1
- package/dist/commands/plugin/push.js +109 -23
- package/dist/commands/plugin/update.d.ts +36 -0
- package/dist/commands/plugin/update.js +121 -0
- package/dist/commands/project/config.js +0 -1
- package/dist/commands/promote.d.ts +1 -0
- package/dist/commands/promote.js +8 -4
- package/dist/commands/pull.d.ts +5 -0
- package/dist/commands/pull.js +26 -1
- package/dist/commands/push.d.ts +9 -0
- package/dist/commands/push.js +116 -40
- package/dist/commands/snippet/push.js +3 -2
- package/dist/commands/theme/style/diff.js +1 -1
- package/dist/commands/theme/style/push.js +1 -1
- 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 +42 -13
- package/dist/commands/validate.js +3 -0
- package/dist/lib/base.d.ts +7 -0
- package/dist/lib/base.js +55 -2
- package/dist/lib/capabilities.d.ts +5 -0
- package/dist/lib/capabilities.js +16 -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 +3 -0
- package/dist/lib/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +11 -13
- package/dist/lib/diff-command.d.ts +17 -0
- package/dist/lib/diff-command.js +125 -9
- 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-snippets.js +3 -5
- package/dist/lib/php-files-command.js +5 -3
- package/dist/lib/push-command.d.ts +26 -0
- package/dist/lib/push-command.js +78 -12
- package/dist/lib/readdir-tolerant.js +6 -3
- 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 +25 -4
- package/dist/lib/rollback-command.js +1 -1
- package/dist/lib/rollback-materialize.js +1 -1
- package/dist/lib/validate-local.d.ts +1 -0
- package/dist/lib/validate-local.js +71 -12
- package/dist/lib/wp-client.d.ts +2 -0
- package/dist/lib/wp-client.js +36 -10
- package/dist/types/plugin.d.ts +2 -1
- package/dist/types/plugin.js +32 -1
- package/dist/types/project-config.generated.d.ts +15 -4
- package/dist/utils/block-format.d.ts +2 -0
- package/dist/utils/block-format.js +18 -2
- package/dist/utils/option-format.d.ts +1 -0
- package/dist/utils/option-format.js +14 -1
- package/dist/utils/page-format.js +2 -1
- package/dist/utils/plugins.d.ts +3 -0
- package/dist/utils/plugins.js +6 -1
- package/dist/utils/rename-retry.d.ts +1 -0
- package/dist/utils/rename-retry.js +20 -0
- package/dist/utils/resource-dirs.js +4 -4
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.js +21 -0
- package/dist/utils/wporg.d.ts +5 -0
- package/dist/utils/wporg.js +41 -0
- package/oclif.manifest.json +2131 -1150
- package/package.json +14 -14
- package/schemas/project-config.schema.json +21 -4
- package/dist/commands/api/publish.d.ts +0 -9
- package/dist/commands/api/publish.js +0 -45
- package/dist/commands/snippet/publish.d.ts +0 -10
- package/dist/commands/snippet/publish.js +0 -62
- package/dist/lib/resolve-linked-project.d.ts +0 -8
- package/dist/lib/resolve-linked-project.js +0 -27
package/dist/types/plugin.d.ts
CHANGED
|
@@ -16,7 +16,8 @@ export type WpNativePlugin = {
|
|
|
16
16
|
export declare const PLUGIN_PIN: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
17
17
|
active: z.ZodExactOptional<z.ZodBoolean>;
|
|
18
18
|
version: z.ZodString;
|
|
19
|
-
}, z.core.$
|
|
19
|
+
}, z.core.$strict>]>;
|
|
20
|
+
export declare function pinProblem(slug: string, pin: unknown): string | undefined;
|
|
20
21
|
export type PluginPin = z.infer<typeof PLUGIN_PIN>;
|
|
21
22
|
export type PluginManifest = Record<string, PluginPin>;
|
|
22
23
|
export declare const pinVersion: (pin: PluginPin) => string;
|
package/dist/types/plugin.js
CHANGED
|
@@ -1,7 +1,38 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
// A bare version string means "installed and active"; the object form records a plugin that
|
|
3
3
|
// must stay installed but inactive.
|
|
4
|
-
|
|
4
|
+
// Strict: an unknown key is a typo (`activate` for `active`) that would otherwise be dropped.
|
|
5
|
+
export const PLUGIN_PIN = z.union([z.string(), z.strictObject({ active: z.boolean().exactOptional(), version: z.string() })]);
|
|
6
|
+
const PIN_KEYS = ['active', 'version'];
|
|
7
|
+
// Levenshtein distance, enough to tell a typo of a pin key ("activate", "verison") from another key.
|
|
8
|
+
function editDistance(a, b) {
|
|
9
|
+
let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
10
|
+
for (let i = 1; i <= a.length; i++) {
|
|
11
|
+
const current = [i];
|
|
12
|
+
for (let j = 1; j <= b.length; j++) {
|
|
13
|
+
const substitution = (previous[j - 1] ?? 0) + (a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1);
|
|
14
|
+
current.push(Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, substitution));
|
|
15
|
+
}
|
|
16
|
+
previous = current;
|
|
17
|
+
}
|
|
18
|
+
return previous[b.length] ?? 0;
|
|
19
|
+
}
|
|
20
|
+
// Why a loopress.json plugin pin is invalid, or undefined when it is fine.
|
|
21
|
+
export function pinProblem(slug, pin) {
|
|
22
|
+
if (PLUGIN_PIN.safeParse(pin).success)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (pin !== null && typeof pin === 'object' && !Array.isArray(pin)) {
|
|
25
|
+
const unknown = Object.keys(pin).filter((key) => !PIN_KEYS.includes(key));
|
|
26
|
+
if (unknown.length > 0) {
|
|
27
|
+
const hints = unknown.map((key) => {
|
|
28
|
+
const near = PIN_KEYS.find((known) => editDistance(key.toLowerCase(), known) <= 2);
|
|
29
|
+
return near ? `"${key}" (did you mean "${near}"?)` : `"${key}"`;
|
|
30
|
+
});
|
|
31
|
+
return `plugin "${slug}" has unknown key(s) ${hints.join(', ')}: a pin is a version string or {version, active}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return `plugin "${slug}" must map to a version string or {version, active}`;
|
|
35
|
+
}
|
|
5
36
|
export const pinVersion = (pin) => (typeof pin === 'string' ? pin : pin.version);
|
|
6
37
|
// No pin at all (a slug loopress.json doesn't list) keeps the default: active.
|
|
7
38
|
export const pinActive = (pin) => typeof pin === 'string' || pin?.active !== false;
|
|
@@ -30,11 +30,11 @@ export interface LoopressProjectConfiguration {
|
|
|
30
30
|
/**
|
|
31
31
|
* Directory where hook files (WordPress actions, filters, and scheduled tasks) are read from and written to, relative to rootDir.
|
|
32
32
|
*/
|
|
33
|
-
|
|
33
|
+
hookDir?: string;
|
|
34
34
|
/**
|
|
35
35
|
* Directory where single-page app bundles are read from and written to, relative to rootDir. Each subdirectory is one app: a loopress.app.json plus a built dist/ folder.
|
|
36
36
|
*/
|
|
37
|
-
|
|
37
|
+
appDir?: string;
|
|
38
38
|
/**
|
|
39
39
|
* Directory where static HTML pages (one <slug>.html per WordPress page, managed by Loopress) are read from, relative to rootDir.
|
|
40
40
|
*/
|
|
@@ -58,7 +58,7 @@ export interface LoopressProjectConfiguration {
|
|
|
58
58
|
/**
|
|
59
59
|
* 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.
|
|
60
60
|
*/
|
|
61
|
-
|
|
61
|
+
optionDir?: string;
|
|
62
62
|
/**
|
|
63
63
|
* Directory where custom post types (one JSON file of register_post_type() arguments per post type, named <slug>.json) are read from and written to, relative to rootDir.
|
|
64
64
|
*/
|
|
@@ -86,7 +86,7 @@ export interface LoopressProjectConfiguration {
|
|
|
86
86
|
/**
|
|
87
87
|
* Directory where the active block theme's Global Styles customizations (Site Editor > Styles: colors, typography, spacing) are read from and written to, relative to rootDir. Classic (non-block) themes are not supported.
|
|
88
88
|
*/
|
|
89
|
-
|
|
89
|
+
themeStyleDir?: string;
|
|
90
90
|
/**
|
|
91
91
|
* Project identifier from the global Loopress config. When set, takes precedence over the currently active project in the global config.
|
|
92
92
|
*/
|
|
@@ -106,6 +106,17 @@ export interface LoopressProjectConfiguration {
|
|
|
106
106
|
active?: boolean;
|
|
107
107
|
};
|
|
108
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* Per-environment settings, keyed by environment name (as in `lps project config`).
|
|
111
|
+
*/
|
|
112
|
+
environments?: {
|
|
113
|
+
[k: string]: {
|
|
114
|
+
/**
|
|
115
|
+
* Globs relative to rootDir (`*`, `**`, `?`) of files this environment never receives. `lps push`, `lps diff` and `lps promote` leave them alone on it: never pushed, never pruned or mirrored away, reported as held back instead of drift. A glob matching a directory covers everything inside. `lps pull` and `lps dev` ignore it.
|
|
116
|
+
*/
|
|
117
|
+
exclude?: string[];
|
|
118
|
+
};
|
|
119
|
+
};
|
|
109
120
|
/**
|
|
110
121
|
* Keep the Loopress child theme (<parent>-loopress) active: `lps theme template push`, and so `lps push`, activates it when it isn't. Off by default, a theme switch changes the settings WordPress stores per theme.
|
|
111
122
|
*/
|
|
@@ -15,6 +15,8 @@ export declare function readLocalBlocks(dir: string): Promise<{
|
|
|
15
15
|
blocks: LocalBlock[];
|
|
16
16
|
problems: BlockProblem[];
|
|
17
17
|
}>;
|
|
18
|
+
export declare function excludedBlockFiles(dir: string, slug: string): string[];
|
|
19
|
+
export declare function blockFilesToPush(dir: string, block: LocalBlock, site: BlockFiles | undefined): BlockFiles;
|
|
18
20
|
export declare function formatBlockProblems(problems: BlockProblem[]): string;
|
|
19
21
|
export declare function assertSafeBlock(block: LocalBlock): void;
|
|
20
22
|
export declare function writeBlock(dir: string, block: LocalBlock): Promise<void>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { isExcluded, withoutExcluded } from '../lib/exclude.js';
|
|
3
4
|
import { errorCode, isRecord } from '../lib/narrow.js';
|
|
4
5
|
// A dynamic block: blocks/<slug>/ in WordPress's own block format, registered on the site as
|
|
5
6
|
// loopress/<slug> (the plugin's BlocksDirectory and BlockFilesController).
|
|
@@ -53,7 +54,7 @@ export function blockProblems(slug, files) {
|
|
|
53
54
|
export async function readLocalBlocks(dir) {
|
|
54
55
|
let entries;
|
|
55
56
|
try {
|
|
56
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
57
|
+
entries = withoutExcluded(dir, await readdir(dir, { withFileTypes: true }), (entry) => entry.name);
|
|
57
58
|
}
|
|
58
59
|
catch (error) {
|
|
59
60
|
if (errorCode(error) === 'ENOENT')
|
|
@@ -69,12 +70,27 @@ export async function readLocalBlocks(dir) {
|
|
|
69
70
|
if (file.isFile() && !file.name.startsWith('.'))
|
|
70
71
|
files[file.name] = await readFile(join(dir, slug, file.name), 'utf8');
|
|
71
72
|
}
|
|
73
|
+
// Checked whole, then the environment's excluded files are dropped: a push keeps the site's
|
|
74
|
+
// copy of those (see blockFilesToPush), a diff holds the block back.
|
|
72
75
|
for (const message of blockProblems(slug, files))
|
|
73
76
|
problems.push({ file: join(dir, slug), message });
|
|
74
|
-
|
|
77
|
+
const kept = new Set(withoutExcluded(join(dir, slug), Object.keys(files), (name) => name));
|
|
78
|
+
blocks.push({ files: Object.fromEntries(Object.entries(files).filter(([name]) => kept.has(name))), slug });
|
|
75
79
|
}
|
|
76
80
|
return { blocks, problems };
|
|
77
81
|
}
|
|
82
|
+
// Block files the environment excludes: never sent, so the site's copy of each one goes back in
|
|
83
|
+
// the PUT (a block is replaced as a whole).
|
|
84
|
+
export function excludedBlockFiles(dir, slug) {
|
|
85
|
+
return BLOCK_FILES.filter((name) => isExcluded(join(dir, slug, name)));
|
|
86
|
+
}
|
|
87
|
+
// What a push sends for `block`: its local files plus the site's copy of its excluded ones.
|
|
88
|
+
export function blockFilesToPush(dir, block, site) {
|
|
89
|
+
const excluded = new Set(excludedBlockFiles(dir, block.slug));
|
|
90
|
+
const kept = Object.entries(block.files).filter(([name]) => !excluded.has(name));
|
|
91
|
+
const fromSite = Object.entries(site ?? {}).filter(([name]) => excluded.has(name));
|
|
92
|
+
return Object.fromEntries([...kept, ...fromSite]);
|
|
93
|
+
}
|
|
78
94
|
export function formatBlockProblems(problems) {
|
|
79
95
|
return problems.map((problem) => `${problem.file}: ${problem.message}`).join('\n');
|
|
80
96
|
}
|
|
@@ -26,6 +26,7 @@ export type ListedOption = {
|
|
|
26
26
|
};
|
|
27
27
|
export declare function isReservedOptionName(name: string): boolean;
|
|
28
28
|
export declare function defaultReadonlyFor(name: string): boolean;
|
|
29
|
+
export declare function isWindowsReservedName(name: string): boolean;
|
|
29
30
|
export declare function optionFileName(name: string): string;
|
|
30
31
|
export declare function partitionByReadonly(tracked: LocalOption[]): {
|
|
31
32
|
skipped: LocalOption[];
|
|
@@ -38,7 +38,17 @@ export function isReservedOptionName(name) {
|
|
|
38
38
|
export function defaultReadonlyFor(name) {
|
|
39
39
|
return READONLY_BY_DEFAULT_OPTION_NAMES.includes(name);
|
|
40
40
|
}
|
|
41
|
+
// Windows refuses these device names as a file name, with or without an extension (`NUL.json`).
|
|
42
|
+
const WINDOWS_RESERVED_NAME = /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])$/i;
|
|
43
|
+
export function isWindowsReservedName(name) {
|
|
44
|
+
return WINDOWS_RESERVED_NAME.test(name.split('.', 1)[0]?.trimEnd() ?? '');
|
|
45
|
+
}
|
|
46
|
+
function reservedNameMessage(name) {
|
|
47
|
+
return `Option name "${name}" is a reserved Windows file name (CON, PRN, AUX, NUL, COM1-COM9, LPT1-LPT9) and cannot be tracked as ${name}.json`;
|
|
48
|
+
}
|
|
41
49
|
export function optionFileName(name) {
|
|
50
|
+
if (isWindowsReservedName(name))
|
|
51
|
+
throw new Error(reservedNameMessage(name));
|
|
42
52
|
return `${name}.json`;
|
|
43
53
|
}
|
|
44
54
|
// Splits tracked options into what `push` may write and what it must skip: readonly is a local
|
|
@@ -54,7 +64,10 @@ const REFS_ERROR = '"refs" must map a value path to a post type, e.g. {"purchase
|
|
|
54
64
|
// Each check's message is the error thrown for it, the first failing one wins. Loose: unknown
|
|
55
65
|
// keys (`readonly`, future ones) pass through untouched.
|
|
56
66
|
const LOCAL_OPTION = z.looseObject({
|
|
57
|
-
name: z
|
|
67
|
+
name: z
|
|
68
|
+
.string('missing a "name" string')
|
|
69
|
+
.min(1, 'missing a "name" string')
|
|
70
|
+
.refine((name) => !isWindowsReservedName(name), { error: (issue) => reservedNameMessage(String(issue.input)) }),
|
|
58
71
|
autoload: z.string('missing an "autoload" string'),
|
|
59
72
|
// `value` itself is deliberately untyped (a WP option can hold any JSON-safe value, including
|
|
60
73
|
// null); only the key's presence is checked, so a hand-edited file that dropped it entirely is
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, extname, join } from 'node:path';
|
|
3
|
+
import { withoutExcluded } from '../lib/exclude.js';
|
|
3
4
|
import { errorCode, errorMessage, isOneOf } from '../lib/narrow.js';
|
|
4
5
|
export const PAGES_ENDPOINT = 'loopress/v1/pages';
|
|
5
6
|
// Mirrored server-side in PagesController. No leading, trailing or doubled hyphen: WordPress's
|
|
@@ -90,7 +91,7 @@ export async function readLocalPages(dir) {
|
|
|
90
91
|
export async function readLocalHtmlFiles(dir, parse, slugRule = PAGE_SLUG_RULE) {
|
|
91
92
|
let entries;
|
|
92
93
|
try {
|
|
93
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
94
|
+
entries = withoutExcluded(dir, await readdir(dir, { withFileTypes: true }), (entry) => entry.name);
|
|
94
95
|
}
|
|
95
96
|
catch (error) {
|
|
96
97
|
if (errorCode(error) === 'ENOENT')
|
package/dist/utils/plugins.d.ts
CHANGED
|
@@ -37,5 +37,8 @@ export type MergeResult<P extends PluginPin = PluginPin> = {
|
|
|
37
37
|
export declare function describePin(pin: PluginPin): string;
|
|
38
38
|
export declare function mergePluginManifest<P extends PluginPin>(existing: Record<string, P>, incoming: Record<string, P>): MergeResult<P>;
|
|
39
39
|
export declare function parseInstalledPlugins(raw: WpNativePlugin[]): InstalledPlugin[];
|
|
40
|
+
export declare function preferredCopy<P extends {
|
|
41
|
+
file: string;
|
|
42
|
+
}>(copies: P[], slug: string): P | undefined;
|
|
40
43
|
export declare function diffPlugins(manifest: PluginManifest, installed: InstalledPlugin[], managedSlugs?: Set<string>): PluginDiff;
|
|
41
44
|
export declare function lockedWpackagistSlugs(composerLock: null | string, kind: 'plugin' | 'theme'): Set<string>;
|
package/dist/utils/plugins.js
CHANGED
|
@@ -46,12 +46,17 @@ export function parseInstalledPlugins(raw) {
|
|
|
46
46
|
}))
|
|
47
47
|
.filter((plugin) => !LOOPRESS_PLUGIN_SLUGS.has(plugin.slug));
|
|
48
48
|
}
|
|
49
|
+
// Composer installs a plugin into `<slug>/`; a site can also hold another copy under the same
|
|
50
|
+
// slug (core's `hello.php` is Hello Dolly). Prefer the one in the slug's folder.
|
|
51
|
+
export function preferredCopy(copies, slug) {
|
|
52
|
+
return copies.find((p) => p.file.startsWith(`${slug}/`)) ?? copies[0];
|
|
53
|
+
}
|
|
49
54
|
export function diffPlugins(manifest, installed,
|
|
50
55
|
// Slugs Loopress currently manages, from the local composer.lock's wpackagist-plugin/*
|
|
51
56
|
// entries. Empty for a loopress.json-only project that doesn't keep a lockfile in the repo:
|
|
52
57
|
// `toRemove` then stays empty (removals still happen server-side, they just aren't previewed).
|
|
53
58
|
managedSlugs = new Set()) {
|
|
54
|
-
const installedMap = new Map(installed.map((p) => [p.slug, p]));
|
|
59
|
+
const installedMap = new Map(installed.map((p) => [p.slug, preferredCopy(installed.filter((q) => q.slug === p.slug), p.slug) ?? p]));
|
|
55
60
|
const toInstall = [];
|
|
56
61
|
const toPin = [];
|
|
57
62
|
const toActivate = [];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renameWithRetry(from: string, to: string, attempts?: number, baseDelayMs?: number): Promise<void>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { rename } from 'node:fs/promises';
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
const RETRYABLE_CODES = new Set(['EBUSY', 'EPERM']);
|
|
4
|
+
// On Windows, an editor, an antivirus or the search indexer can briefly hold a file open and make
|
|
5
|
+
// rename() fail with EPERM/EBUSY. Those clear within milliseconds, so retry a few times with a
|
|
6
|
+
// growing delay before giving up. Other errors (ENOENT, EEXIST...) are thrown right away.
|
|
7
|
+
export async function renameWithRetry(from, to, attempts = 5, baseDelayMs = 50) {
|
|
8
|
+
for (let attempt = 1;; attempt++) {
|
|
9
|
+
try {
|
|
10
|
+
await rename(from, to);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
const code = error instanceof Error && 'code' in error ? String(error.code) : '';
|
|
15
|
+
if (!RETRYABLE_CODES.has(code) || attempt >= attempts)
|
|
16
|
+
throw error;
|
|
17
|
+
await sleep(baseDelayMs * attempt);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -28,16 +28,16 @@ export const RESOURCE_DIR_DEFAULTS = {
|
|
|
28
28
|
const CONFIG_KEY = {
|
|
29
29
|
acf: 'acfDir',
|
|
30
30
|
api: 'apiDir',
|
|
31
|
-
apps: '
|
|
31
|
+
apps: 'appDir',
|
|
32
32
|
block: 'blockDir',
|
|
33
33
|
blockCss: 'blockCssDir',
|
|
34
34
|
cpt: 'cptDir',
|
|
35
35
|
css: 'cssDir',
|
|
36
36
|
form: 'formDir',
|
|
37
|
-
hooks: '
|
|
37
|
+
hooks: 'hookDir',
|
|
38
38
|
menu: 'menuDir',
|
|
39
39
|
meta: 'metaDir',
|
|
40
|
-
options: '
|
|
40
|
+
options: 'optionDir',
|
|
41
41
|
page: 'pageDir',
|
|
42
42
|
part: 'partDir',
|
|
43
43
|
pattern: 'patternDir',
|
|
@@ -45,7 +45,7 @@ const CONFIG_KEY = {
|
|
|
45
45
|
snippets: 'snippetsDir',
|
|
46
46
|
taxonomy: 'taxonomyDir',
|
|
47
47
|
template: 'templateDir',
|
|
48
|
-
themeStyles: '
|
|
48
|
+
themeStyles: 'themeStyleDir',
|
|
49
49
|
};
|
|
50
50
|
// Resolves where a resource's files live: an explicit override wins, otherwise the
|
|
51
51
|
// loopress.json `<kind>Dir` setting, otherwise the built-in default, all under `rootDir`.
|
package/dist/utils/version.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export declare function isExactVersion(value: string): boolean;
|
|
2
2
|
export declare function compareVersions(a: string, b: string): null | number;
|
|
3
|
+
export type VersionJump = 'major' | 'minor' | 'patch';
|
|
4
|
+
export declare function versionJump(from: string, to: string): null | VersionJump;
|
package/dist/utils/version.js
CHANGED
|
@@ -19,3 +19,24 @@ export function isExactVersion(value) {
|
|
|
19
19
|
export function compareVersions(a, b) {
|
|
20
20
|
return validate(a) && validate(b) ? cmp(a, b) : null;
|
|
21
21
|
}
|
|
22
|
+
// How far `from` is behind `to`: the first numeric segment that differs, major, minor, then
|
|
23
|
+
// anything finer is a patch. Null when `from` is not behind `to` or either isn't comparable.
|
|
24
|
+
export function versionJump(from, to) {
|
|
25
|
+
const order = compareVersions(from, to);
|
|
26
|
+
if (order === null || order >= 0)
|
|
27
|
+
return null;
|
|
28
|
+
const a = numericSegments(from);
|
|
29
|
+
const b = numericSegments(to);
|
|
30
|
+
if (a[0] !== b[0])
|
|
31
|
+
return 'major';
|
|
32
|
+
if ((a[1] ?? 0) !== (b[1] ?? 0))
|
|
33
|
+
return 'minor';
|
|
34
|
+
return 'patch';
|
|
35
|
+
}
|
|
36
|
+
function numericSegments(version) {
|
|
37
|
+
const core = (version.split('-', 1)[0] ?? version).replace(/^v/i, '');
|
|
38
|
+
return core.split('.').map((part) => {
|
|
39
|
+
const n = Number(part);
|
|
40
|
+
return Number.isNaN(n) ? 0 : Math.trunc(n);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const WPORG_INFO = "https://api.wordpress.org/plugins/info/1.2/";
|
|
2
|
+
export declare function requestPluginInfo(slug: string, { versions }?: {
|
|
3
|
+
versions?: boolean;
|
|
4
|
+
}): Promise<null | Record<string, unknown>>;
|
|
5
|
+
export declare function fetchPluginInfo(slug: string): Promise<null | Record<string, unknown>>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import got from 'got';
|
|
2
|
+
export const WPORG_INFO = 'https://api.wordpress.org/plugins/info/1.2/';
|
|
3
|
+
// The WordPress.org plugin_information answer for one slug, or null when the plugin isn't
|
|
4
|
+
// listed there (zip, premium), or {error: 'closed'} for a closed one. Throws when the API can't be reached or answers with an error.
|
|
5
|
+
// `versions` adds every released version (`versions`: version -> download URL) to the answer.
|
|
6
|
+
export async function requestPluginInfo(slug, { versions = false } = {}) {
|
|
7
|
+
try {
|
|
8
|
+
const body = await got(WPORG_INFO, {
|
|
9
|
+
searchParams: {
|
|
10
|
+
action: 'plugin_information',
|
|
11
|
+
'request[slug]': slug,
|
|
12
|
+
...(versions && { 'request[fields][versions]': '1' }),
|
|
13
|
+
},
|
|
14
|
+
timeout: { request: 10_000 },
|
|
15
|
+
}).json();
|
|
16
|
+
// A removed plugin answers {error: 'closed'}: kept so callers can tell it from an unlisted one.
|
|
17
|
+
if (typeof body === 'object' && 'error' in body && body.error === 'closed')
|
|
18
|
+
return body;
|
|
19
|
+
return typeof body === 'object' && !('error' in body) ? body : null;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (isNotFound(error))
|
|
23
|
+
return null;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Same lookup, but an unreachable API is reported as null too.
|
|
28
|
+
export async function fetchPluginInfo(slug) {
|
|
29
|
+
try {
|
|
30
|
+
return await requestPluginInfo(slug);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function isNotFound(error) {
|
|
37
|
+
if (typeof error !== 'object' || error === null || !('response' in error))
|
|
38
|
+
return false;
|
|
39
|
+
const { response } = error;
|
|
40
|
+
return typeof response === 'object' && response !== null && 'statusCode' in response && response.statusCode === 404;
|
|
41
|
+
}
|