@loopress/cli 0.32.2 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +549 -179
  2. package/dist/commands/acf/list.d.ts +1 -1
  3. package/dist/commands/acf/list.js +3 -4
  4. package/dist/commands/acf/pull.d.ts +1 -1
  5. package/dist/commands/acf/pull.js +9 -6
  6. package/dist/commands/acf/push.d.ts +1 -1
  7. package/dist/commands/acf/push.js +5 -4
  8. package/dist/commands/api/publish.js +2 -1
  9. package/dist/commands/app/pull.js +2 -1
  10. package/dist/commands/app/push.js +7 -5
  11. package/dist/commands/app/{remove.d.ts → rm.d.ts} +1 -0
  12. package/dist/commands/app/{remove.js → rm.js} +2 -1
  13. package/dist/commands/block/diff.d.ts +2 -0
  14. package/dist/commands/block/diff.js +5 -0
  15. package/dist/commands/block/list.d.ts +8 -0
  16. package/dist/commands/block/list.js +23 -0
  17. package/dist/commands/block/pull.d.ts +21 -0
  18. package/dist/commands/block/pull.js +67 -0
  19. package/dist/commands/block/push.d.ts +20 -0
  20. package/dist/commands/block/push.js +53 -0
  21. package/dist/commands/block/rm.d.ts +23 -0
  22. package/dist/commands/block/rm.js +69 -0
  23. package/dist/commands/block/rollback.d.ts +2 -0
  24. package/dist/commands/block/rollback.js +5 -0
  25. package/dist/commands/composer/init.js +1 -0
  26. package/dist/commands/composer/push.js +5 -4
  27. package/dist/commands/content/copy.d.ts +3 -0
  28. package/dist/commands/content/copy.js +79 -2
  29. package/dist/commands/dev.d.ts +2 -2
  30. package/dist/commands/dev.js +13 -18
  31. package/dist/commands/diff.d.ts +6 -3
  32. package/dist/commands/diff.js +63 -13
  33. package/dist/commands/doctor.d.ts +4 -2
  34. package/dist/commands/doctor.js +49 -25
  35. package/dist/commands/form/pull.js +7 -5
  36. package/dist/commands/form/push.js +4 -3
  37. package/dist/commands/init.js +4 -3
  38. package/dist/commands/menu/push.js +8 -8
  39. package/dist/commands/option/list.js +5 -2
  40. package/dist/commands/option/push.js +3 -1
  41. package/dist/commands/option/{remove.d.ts → rm.d.ts} +1 -0
  42. package/dist/commands/option/{remove.js → rm.js} +2 -1
  43. package/dist/commands/page/push.js +3 -1
  44. package/dist/commands/plugin/add.d.ts +1 -0
  45. package/dist/commands/plugin/add.js +50 -9
  46. package/dist/commands/plugin/audit.d.ts +4 -4
  47. package/dist/commands/plugin/audit.js +35 -15
  48. package/dist/commands/plugin/pull.js +3 -2
  49. package/dist/commands/plugin/push.js +41 -6
  50. package/dist/commands/plugin/status.js +3 -1
  51. package/dist/commands/project/config.js +7 -7
  52. package/dist/commands/project/pull.js +3 -1
  53. package/dist/commands/project/push.js +6 -4
  54. package/dist/commands/project/{remove.d.ts → rm.d.ts} +1 -0
  55. package/dist/commands/project/{remove.js → rm.js} +3 -2
  56. package/dist/commands/project/switch.js +6 -4
  57. package/dist/commands/promote.d.ts +1 -0
  58. package/dist/commands/promote.js +15 -6
  59. package/dist/commands/pull.d.ts +7 -0
  60. package/dist/commands/pull.js +67 -18
  61. package/dist/commands/push.d.ts +11 -1
  62. package/dist/commands/push.js +157 -48
  63. package/dist/commands/snippet/publish.js +9 -5
  64. package/dist/commands/snippet/push.d.ts +1 -1
  65. package/dist/commands/snippet/push.js +15 -12
  66. package/dist/commands/status.js +1 -0
  67. package/dist/commands/theme/activate.d.ts +21 -0
  68. package/dist/commands/theme/activate.js +44 -0
  69. package/dist/commands/theme/pull.js +2 -1
  70. package/dist/commands/theme/push.js +7 -3
  71. package/dist/commands/theme/style/diff.js +1 -1
  72. package/dist/commands/theme/style/push.js +8 -3
  73. package/dist/commands/theme/style/rollback.js +1 -1
  74. package/dist/commands/theme/template/push.d.ts +2 -0
  75. package/dist/commands/theme/template/push.js +66 -18
  76. package/dist/commands/validate.js +3 -0
  77. package/dist/config/auth.manager.d.ts +10 -7
  78. package/dist/config/auth.manager.js +5 -1
  79. package/dist/config/json-file.d.ts +1 -1
  80. package/dist/config/project-config.manager.d.ts +2 -2
  81. package/dist/config/project-config.manager.js +13 -21
  82. package/dist/lib/api-client.js +16 -21
  83. package/dist/lib/app-build.js +6 -2
  84. package/dist/lib/app-manifest.js +6 -6
  85. package/dist/lib/base.d.ts +8 -1
  86. package/dist/lib/base.js +43 -9
  87. package/dist/lib/bootstrap-full-install.js +8 -6
  88. package/dist/lib/capabilities.d.ts +44 -0
  89. package/dist/lib/capabilities.js +85 -0
  90. package/dist/lib/declared-plugins.d.ts +9 -0
  91. package/dist/lib/declared-plugins.js +21 -0
  92. package/dist/lib/declared-type-command.js +7 -2
  93. package/dist/lib/dev-watch.d.ts +1 -1
  94. package/dist/lib/dev-watch.js +13 -15
  95. package/dist/lib/diff-command.d.ts +19 -3
  96. package/dist/lib/diff-command.js +132 -14
  97. package/dist/lib/diff-state.js +2 -3
  98. package/dist/lib/environment-urls.d.ts +6 -0
  99. package/dist/lib/environment-urls.js +59 -0
  100. package/dist/lib/exclude.d.ts +16 -0
  101. package/dist/lib/exclude.js +102 -0
  102. package/dist/lib/load-files.js +2 -1
  103. package/dist/lib/load-snippets.js +39 -43
  104. package/dist/lib/local-callback-server.js +7 -1
  105. package/dist/lib/narrow.d.ts +5 -0
  106. package/dist/lib/narrow.js +26 -0
  107. package/dist/lib/php-files-command.js +13 -8
  108. package/dist/lib/plugin-detection.d.ts +0 -1
  109. package/dist/lib/plugin-detection.js +2 -4
  110. package/dist/lib/push-command.d.ts +5 -1
  111. package/dist/lib/push-command.js +48 -7
  112. package/dist/lib/put-or-create.d.ts +2 -2
  113. package/dist/lib/readdir-tolerant.js +9 -4
  114. package/dist/lib/resource-selection.d.ts +9 -0
  115. package/dist/lib/resource-selection.js +49 -0
  116. package/dist/lib/resource-state.d.ts +2 -0
  117. package/dist/lib/resource-state.js +60 -14
  118. package/dist/lib/rollback-command.d.ts +1 -1
  119. package/dist/lib/rollback-command.js +4 -3
  120. package/dist/lib/rollback-materialize.js +29 -9
  121. package/dist/lib/rotate-app-password.js +1 -0
  122. package/dist/lib/sentry.js +2 -2
  123. package/dist/lib/snapshot-store.d.ts +10 -7
  124. package/dist/lib/snapshot-store.js +21 -5
  125. package/dist/lib/spinner.js +4 -0
  126. package/dist/lib/validate-local.d.ts +1 -0
  127. package/dist/lib/validate-local.js +102 -28
  128. package/dist/lib/wp-client.d.ts +7 -1
  129. package/dist/lib/wp-client.js +60 -69
  130. package/dist/lib/wp-site-diagnostic.js +3 -5
  131. package/dist/types/global-config.generated.d.ts +1 -1
  132. package/dist/types/plugin.d.ts +7 -5
  133. package/dist/types/plugin.js +6 -1
  134. package/dist/types/project-config.generated.d.ts +24 -5
  135. package/dist/types/snippet.d.ts +2 -2
  136. package/dist/types/snippet.js +1 -1
  137. package/dist/utils/acf-format.js +6 -3
  138. package/dist/utils/block-format.d.ts +29 -0
  139. package/dist/utils/block-format.js +109 -0
  140. package/dist/utils/composer.d.ts +7 -15
  141. package/dist/utils/composer.js +43 -16
  142. package/dist/utils/declared-type-format.js +26 -19
  143. package/dist/utils/form-format.js +2 -2
  144. package/dist/utils/loopress-config.js +9 -1
  145. package/dist/utils/menu-format.d.ts +3 -1
  146. package/dist/utils/menu-format.js +5 -0
  147. package/dist/utils/option-format.d.ts +2 -2
  148. package/dist/utils/option-format.js +15 -25
  149. package/dist/utils/page-format.js +9 -7
  150. package/dist/utils/php-top-level.js +15 -4
  151. package/dist/utils/plugin-sync.d.ts +7 -7
  152. package/dist/utils/plugin-sync.js +11 -11
  153. package/dist/utils/plugins.js +9 -6
  154. package/dist/utils/resource-dirs.d.ts +2 -0
  155. package/dist/utils/resource-dirs.js +13 -5
  156. package/dist/utils/snippet-format.d.ts +1 -1
  157. package/dist/utils/snippet-format.js +5 -3
  158. package/dist/utils/template-format.js +2 -2
  159. package/dist/utils/theme-css.js +14 -6
  160. package/dist/utils/theme-styles-format.d.ts +3 -0
  161. package/dist/utils/theme-styles-format.js +15 -5
  162. package/dist/utils/version.js +1 -1
  163. package/oclif.manifest.json +2661 -1235
  164. package/package.json +45 -20
  165. package/schemas/global-config.schema.json +1 -1
  166. package/schemas/project-config.schema.json +32 -5
@@ -1,11 +1,13 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
+ import {} from './loopress-config.js';
3
4
  // The default subdirectory for each file-backed resource, relative to rootDir. Used whenever
4
5
  // loopress.json has no `<kind>Dir` key, which is the case for a fresh `lps init`.
5
6
  export const RESOURCE_DIR_DEFAULTS = {
6
7
  acf: 'acf',
7
8
  api: 'api',
8
9
  apps: 'apps',
10
+ block: 'blocks',
9
11
  blockCss: 'theme/blocks',
10
12
  cpt: 'cpt',
11
13
  css: 'theme/css',
@@ -26,15 +28,16 @@ export const RESOURCE_DIR_DEFAULTS = {
26
28
  const CONFIG_KEY = {
27
29
  acf: 'acfDir',
28
30
  api: 'apiDir',
29
- apps: 'appsDir',
31
+ apps: 'appDir',
32
+ block: 'blockDir',
30
33
  blockCss: 'blockCssDir',
31
34
  cpt: 'cptDir',
32
35
  css: 'cssDir',
33
36
  form: 'formDir',
34
- hooks: 'hooksDir',
37
+ hooks: 'hookDir',
35
38
  menu: 'menuDir',
36
39
  meta: 'metaDir',
37
- options: 'optionsDir',
40
+ options: 'optionDir',
38
41
  page: 'pageDir',
39
42
  part: 'partDir',
40
43
  pattern: 'patternDir',
@@ -42,7 +45,7 @@ const CONFIG_KEY = {
42
45
  snippets: 'snippetsDir',
43
46
  taxonomy: 'taxonomyDir',
44
47
  template: 'templateDir',
45
- themeStyles: 'themeStylesDir',
48
+ themeStyles: 'themeStyleDir',
46
49
  };
47
50
  // Resolves where a resource's files live: an explicit override wins, otherwise the
48
51
  // loopress.json `<kind>Dir` setting, otherwise the built-in default, all under `rootDir`.
@@ -53,10 +56,15 @@ export function resolveResourceDir(kind, localConfig, override) {
53
56
  return override;
54
57
  const rootDir = localConfig.rootDir ?? '.';
55
58
  const configured = localConfig[CONFIG_KEY[kind]];
56
- return join(rootDir, configured ?? RESOURCE_DIR_DEFAULTS[kind]);
59
+ return join(rootDir, typeof configured === 'string' ? configured : RESOURCE_DIR_DEFAULTS[kind]);
57
60
  }
58
61
  // Whether the project uses a resource at all: its directory exists. What the aggregate `lps push`,
59
62
  // `lps pull` and `lps diff` go by for a resource backed by a plugin the site may not have.
60
63
  export function projectUses(kind, localConfig) {
61
64
  return existsSync(resolve(resolveResourceDir(kind, localConfig)));
62
65
  }
66
+ // The project has files for the Loopress child theme to hold: templates, parts or CSS. Without
67
+ // them the child would be empty, activateChildTheme or not: `lps theme activate` covers that.
68
+ export function projectHasChildThemeFiles(localConfig) {
69
+ return ['template', 'part', 'css', 'blockCss'].some((kind) => projectUses(kind, localConfig));
70
+ }
@@ -11,7 +11,7 @@ export type NormalizedSnippet = {
11
11
  location: SnippetLocation;
12
12
  name: string;
13
13
  priority: number;
14
- revision?: string;
14
+ revision?: string | undefined;
15
15
  shortcodeAttributes: string[];
16
16
  tags: string[];
17
17
  type: SnippetType;
@@ -1,3 +1,5 @@
1
+ import { isOneOf } from '../lib/narrow.js';
2
+ import {} from '../types/snippet.generated.js';
1
3
  export const SNIPPETS_ENDPOINT = 'loopress/v1/snippets';
2
4
  // Coerces server JSON that should be a string but, being unknown, could in principle be an
3
5
  // object; a plain String() on that would print "[object Object]" instead of falling back.
@@ -7,12 +9,12 @@ function coerceString(value, fallback = '') {
7
9
  export function parseType(raw) {
8
10
  const valid = ['css', 'html', 'js', 'php', 'text'];
9
11
  const value = coerceString(raw).toLowerCase();
10
- return valid.includes(value) ? value : null;
12
+ return isOneOf(valid, value) ? value : null;
11
13
  }
12
14
  const VALID_LOCATIONS = new Set(['admin', 'body', 'everywhere', 'footer', 'frontend', 'header', 'once']);
13
15
  export function parseLocation(raw) {
14
16
  const value = coerceString(raw).toLowerCase();
15
- return VALID_LOCATIONS.has(value) ? value : null;
17
+ return isOneOf(VALID_LOCATIONS, value) ? value : null;
16
18
  }
17
19
  export function parseInsertMethod(raw) {
18
20
  return raw === 'auto' || raw === 'shortcode' ? raw : null;
@@ -83,7 +85,7 @@ export function normalizeSnippet(data) {
83
85
  priority: resolvePriority(data.priority),
84
86
  revision: typeof data.revision === 'string' ? data.revision : undefined,
85
87
  shortcodeAttributes: Array.isArray(data.shortcodeAttributes) ? data.shortcodeAttributes.map(String) : [],
86
- tags: Array.isArray(data.tags) ? data.tags : [],
88
+ tags: Array.isArray(data.tags) ? data.tags.map(String) : [],
87
89
  type: parseType(data.type) ?? 'php',
88
90
  };
89
91
  }
@@ -32,8 +32,8 @@ export function parseTemplateFile(slug, raw) {
32
32
  // `area` defaults from the name, the way themes name their parts: header, header-large-title...
33
33
  export function parsePartFile(slug, raw) {
34
34
  const { body, meta } = parseFile(raw, ['title', 'area']);
35
- const nameArea = /^(header|footer)(-|$)/.exec(slug)?.[1] ?? 'uncategorized';
36
- return { area: meta.area || nameArea, html: body, slug, title: meta.title || titleFromSlug(slug) };
35
+ const nameArea = /^(header|footer)(?:-|$)/.exec(slug)?.[1] ?? 'uncategorized';
36
+ return { area: (meta.area ?? '') || nameArea, html: body, slug, title: (meta.title ?? '') || titleFromSlug(slug) };
37
37
  }
38
38
  // Same directory rules as pages/ (flat, `.html` only), with template slugs, every problem at once.
39
39
  export async function readLocalTemplates(templatesDir, partsDir) {
@@ -3,6 +3,9 @@ import { Buffer } from 'node:buffer';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { readdir } from 'node:fs/promises';
5
5
  import { extname, join, relative, resolve, sep } from 'node:path';
6
+ import { z } from 'zod';
7
+ import { errorCode } from '../lib/narrow.js';
8
+ import {} from './page-format.js';
6
9
  // The global stylesheet's entry: it @imports the rest of theme/css/ in cascade order.
7
10
  export const STYLE_ENTRY = 'style.css';
8
11
  // WordPress's own block name rule (register_block_type): lowercase letters, digits and dashes,
@@ -18,7 +21,7 @@ async function listFiles(dir) {
18
21
  .toSorted((a, b) => a.localeCompare(b));
19
22
  }
20
23
  catch (error) {
21
- if (error.code === 'ENOENT')
24
+ if (errorCode(error) === 'ENOENT')
22
25
  return [];
23
26
  throw error;
24
27
  }
@@ -27,8 +30,9 @@ async function readBlockEntries(blocksDir, problems) {
27
30
  const entries = [];
28
31
  for (const file of await listFiles(blocksDir)) {
29
32
  const segments = relative(blocksDir, file).split(sep);
30
- const [namespace, block] = [segments[0], segments[1]?.replace(/\.css$/, '')];
31
- if (segments.length !== 2 || extname(file) !== '.css' || !BLOCK_SEGMENT.test(namespace) || !BLOCK_SEGMENT.test(block)) {
33
+ const [namespace, fileName] = segments;
34
+ const block = fileName?.replace(/\.css$/, '');
35
+ if (namespace === undefined || block === undefined || segments.length !== 2 || extname(file) !== '.css' || !BLOCK_SEGMENT.test(namespace) || !BLOCK_SEGMENT.test(block)) {
32
36
  problems.push({ file, message: `must be <namespace>/<block>.css, named after a block (e.g. ${join(blocksDir, 'core', 'button.css')})` });
33
37
  continue;
34
38
  }
@@ -36,11 +40,15 @@ async function readBlockEntries(blocksDir, problems) {
36
40
  }
37
41
  return entries;
38
42
  }
43
+ const BUILD_LOCATION = z.object({ file: z.string(), line: z.number() });
44
+ const BUILD_MESSAGE = z.object({ location: BUILD_LOCATION.nullable(), text: z.string() });
45
+ const BUILD_FAILURE = z.object({ errors: z.array(BUILD_MESSAGE) });
39
46
  function buildProblems(error) {
40
- const { errors } = error;
41
- if (!Array.isArray(errors))
47
+ // esbuild's BuildFailure, the only part of it read here. Anything else is not a CSS problem.
48
+ const failure = BUILD_FAILURE.safeParse(error);
49
+ if (!failure.success)
42
50
  throw error;
43
- return errors.map((message) => ({
51
+ return failure.data.errors.map((message) => ({
44
52
  file: message.location ? `${message.location.file}:${message.location.line}` : 'esbuild',
45
53
  message: message.text,
46
54
  }));
@@ -8,8 +8,11 @@ export type WpThemeSupportInfo = {
8
8
  }>>;
9
9
  status: 'active' | 'inactive';
10
10
  stylesheet: string;
11
+ template?: string;
11
12
  theme_supports?: Record<string, unknown>;
12
13
  };
14
+ export declare const CHILD_THEME_SUFFIX = "-loopress";
15
+ export declare function projectStylesheet(theme: Pick<WpThemeSupportInfo, 'stylesheet' | 'template'>): string;
13
16
  export declare function isBlockTheme(theme: Pick<WpThemeSupportInfo, 'theme_supports'>): boolean;
14
17
  export declare function getUserGlobalStylesId(theme: Pick<WpThemeSupportInfo, '_links'>): null | number;
15
18
  export declare function globalStylesFiles(dir: string): Promise<string[]>;
@@ -1,5 +1,6 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { readdirTolerant } from '../lib/readdir-tolerant.js';
3
+ import {} from '../lib/wp-client.js';
3
4
  // Global Styles are the user's customizations from the Site Editor's "Styles" screen (colors,
4
5
  // typography, spacing), stored by WordPress core in a `wp_global_styles` post, one per theme.
5
6
  // This is separate from the theme's own theme.json file, which lives in the theme's repo and is
@@ -11,6 +12,15 @@ export function themeStylesFileName(stylesheet) {
11
12
  export function globalStylesEndpoint(id) {
12
13
  return `wp/v2/global-styles/${id}`;
13
14
  }
15
+ // Appended to the parent's stylesheet by the plugin's ChildThemeController.
16
+ export const CHILD_THEME_SUFFIX = '-loopress';
17
+ // The stylesheet a project's Global Styles file is named after: the parent's when the Loopress
18
+ // child is active, so the tracked file stays the same whether or not the child was activated
19
+ // (the plugin copies the parent's Global Styles into the child when activating it).
20
+ export function projectStylesheet(theme) {
21
+ const { stylesheet, template } = theme;
22
+ return template && stylesheet === `${template}${CHILD_THEME_SUFFIX}` ? template : stylesheet;
23
+ }
14
24
  // WordPress core surfaces `wp_is_block_theme()` over the `wp/v2/themes` REST response as
15
25
  // `theme_supports['block-templates']` (see `_add_default_theme_supports()` in
16
26
  // wp-includes/theme.php): core adds that feature flag itself exactly when the theme has its own
@@ -56,13 +66,13 @@ export async function globalStylesSkipNote(wp, dir, action) {
56
66
  const active = await getActiveTheme(wp);
57
67
  if (!isBlockTheme(active))
58
68
  return `the active theme "${active.stylesheet}" is a classic theme, skipped`;
59
- if (action === 'push' && !files.includes(themeStylesFileName(active.stylesheet))) {
60
- return `no file for the active theme "${active.stylesheet}", skipped`;
69
+ if (action === 'push' && !files.includes(themeStylesFileName(projectStylesheet(active)))) {
70
+ return `no file for the active theme "${projectStylesheet(active)}", skipped`;
61
71
  }
62
72
  return undefined;
63
73
  }
64
- // Resolves the active theme's stylesheet slug and the id of its editable Global Styles post in
65
- // one call, failing clearly (rather than with a raw REST error later) when the active theme
74
+ // Resolves the active theme's stylesheet slug, as the project names it (see projectStylesheet),
75
+ // and the id of its editable Global Styles post in one call, failing clearly (rather than with a raw REST error later) when the active theme
66
76
  // isn't a block theme: classic themes have no Styles screen and no `wp_global_styles` post at all.
67
77
  export async function getActiveThemeGlobalStyles(wp) {
68
78
  const active = await getActiveTheme(wp);
@@ -73,7 +83,7 @@ export async function getActiveThemeGlobalStyles(wp) {
73
83
  if (id === null) {
74
84
  throw new Error(`Could not resolve the editable Global Styles post for "${active.stylesheet}". Requires an admin-capable account (edit_theme_options) on WordPress 5.9+.`);
75
85
  }
76
- return { id, stylesheet: active.stylesheet };
86
+ return { id, stylesheet: projectStylesheet(active) };
77
87
  }
78
88
  // The compared/stored value: `settings` and `styles` only. `id` and `_links` are WordPress
79
89
  // bookkeeping, not part of the tracked configuration (a fresh install resolves to a different
@@ -10,7 +10,7 @@ import { compareVersions as cmp, validate } from 'compare-versions';
10
10
  // shape here would let it slip past `isExactVersion` at `add` time, then have
11
11
  // `compareVersions`/`isDowngrade` silently treat it as unorderable, bypassing the --force
12
12
  // gate on a real downgrade.
13
- const EXACT_VERSION = /^\d+(\.\d+){0,3}$|^\d+(\.\d+){2,3}-[0-9A-Za-z.]+$/;
13
+ const EXACT_VERSION = /^\d+(?:\.\d+){0,3}$|^\d+(?:\.\d+){2,3}-[\d.a-z]+$/i;
14
14
  export function isExactVersion(value) {
15
15
  return EXACT_VERSION.test(value);
16
16
  }