@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/commands/push.js
CHANGED
|
@@ -6,8 +6,10 @@ import { integrationFix, missingIntegration, missingModule } from '../lib/capabi
|
|
|
6
6
|
import { guardProductionPush } from '../lib/guard-production-push.js';
|
|
7
7
|
import { stdoutToStderr } from '../lib/json-delegation.js';
|
|
8
8
|
import { errorMessage } from '../lib/narrow.js';
|
|
9
|
-
import { PushCommand } from '../lib/push-command.js';
|
|
9
|
+
import { postDeployment, PushCommand, startDeploymentCollector, stopDeploymentCollector } from '../lib/push-command.js';
|
|
10
|
+
import { parseSelectionFlag, selectionFlags, selectResources } from '../lib/resource-selection.js';
|
|
10
11
|
import { isLocalEnvironment } from '../lib/wp-client.js';
|
|
12
|
+
import { pinActive } from '../types/plugin.js';
|
|
11
13
|
import { parseComposerJson, wpackagistRequire } from '../utils/composer.js';
|
|
12
14
|
import { projectHasChildThemeFiles, projectUses, resolveResourceDir } from '../utils/resource-dirs.js';
|
|
13
15
|
import { globalStylesSkipNote } from '../utils/theme-styles-format.js';
|
|
@@ -16,55 +18,77 @@ import { globalStylesSkipNote } from '../utils/theme-styles-format.js';
|
|
|
16
18
|
// site's 409 or the missing directory.
|
|
17
19
|
const PLUGIN_BACKED = { 'acf:push': 'acf', 'form:push': 'form', 'snippet:push': 'snippets' };
|
|
18
20
|
// Features of Loopress Full only: on Light, skipped while the project doesn't use them, refused
|
|
19
|
-
// upfront when it does.
|
|
21
|
+
// upfront when it does. Like every other resource, skipped when the project doesn't use them.
|
|
20
22
|
const FULL_ONLY = { 'api:push': 'api', 'block:push': 'block', 'hook:push': 'hooks', 'page:push': 'page', 'pattern:push': 'pattern' };
|
|
23
|
+
// Always available on the site: nothing to refuse, but a project without the resource's
|
|
24
|
+
// directory has nothing to push, so the step is skipped instead of running on an empty directory.
|
|
25
|
+
const CORE_BACKED = { 'cpt:push': 'cpt', 'menu:push': 'menu', 'meta:push': 'meta', 'option:push': 'options', 'role:push': 'role', 'taxonomy:push': 'taxonomy' };
|
|
21
26
|
// Dependency-ish order: code first (plugins, composer), then the ACF definitions content
|
|
22
27
|
// relies on, then content itself.
|
|
23
28
|
const PUSH_TARGETS = [
|
|
24
|
-
{ commandId: 'plugin:push', label: 'plugins' },
|
|
25
|
-
{ commandId: 'composer:push', label: 'composer' },
|
|
29
|
+
{ commandId: 'plugin:push', id: 'plugin', label: 'plugins' },
|
|
30
|
+
{ commandId: 'composer:push', id: 'composer', label: 'composer' },
|
|
26
31
|
// Before ACF: a field group's location rule can target a Loopress post type or taxonomy.
|
|
27
|
-
{ commandId: 'cpt:push', label: 'post types' },
|
|
28
|
-
{ commandId: 'taxonomy:push', label: 'taxonomies' },
|
|
29
|
-
{ commandId: 'meta:push', label: 'post meta' },
|
|
30
|
-
{ commandId: 'role:push', label: 'roles' },
|
|
31
|
-
{ commandId: 'acf:push', label: 'ACF' },
|
|
32
|
-
{ commandId: 'api:push', label: 'API routes' },
|
|
33
|
-
{ commandId: 'hook:push', label: 'hooks' },
|
|
32
|
+
{ commandId: 'cpt:push', id: 'cpt', label: 'post types' },
|
|
33
|
+
{ commandId: 'taxonomy:push', id: 'taxonomy', label: 'taxonomies' },
|
|
34
|
+
{ commandId: 'meta:push', id: 'meta', label: 'post meta' },
|
|
35
|
+
{ commandId: 'role:push', id: 'role', label: 'roles' },
|
|
36
|
+
{ commandId: 'acf:push', id: 'acf', label: 'ACF' },
|
|
37
|
+
{ commandId: 'api:push', id: 'api', label: 'API routes' },
|
|
38
|
+
{ commandId: 'hook:push', id: 'hook', label: 'hooks' },
|
|
34
39
|
// Before templates and pages, which place them (`wp:loopress/<slug>`).
|
|
35
|
-
{ commandId: 'block:push', label: 'dynamic blocks' },
|
|
36
|
-
{ commandId: 'form:push', label: 'forms' },
|
|
40
|
+
{ commandId: 'block:push', id: 'block', label: 'dynamic blocks' },
|
|
41
|
+
{ commandId: 'form:push', id: 'form', label: 'forms' },
|
|
37
42
|
// Before templates and pages, which can reference them (`wp:pattern`).
|
|
38
|
-
{ commandId: 'pattern:push', label: 'patterns' },
|
|
43
|
+
{ commandId: 'pattern:push', id: 'pattern', label: 'patterns' },
|
|
39
44
|
// Before pages: WordPress refuses a page whose `template` it doesn't know yet.
|
|
40
|
-
{ commandId: 'theme:template:push', label: 'templates and parts' },
|
|
45
|
+
{ commandId: 'theme:template:push', id: 'template', label: 'templates and parts' },
|
|
41
46
|
// Belong to whichever theme is active, so right after its templates. Theme versions stay out:
|
|
42
47
|
// installing or removing themes is `lps theme push`'s call.
|
|
43
|
-
{ commandId: 'theme:style:push', label: 'Global Styles' },
|
|
44
|
-
{ commandId: 'page:push', label: 'pages' },
|
|
45
|
-
{ commandId: 'menu:push', label: 'menus' },
|
|
46
|
-
{ commandId: 'option:push', label: 'options' },
|
|
47
|
-
{ commandId: 'snippet:push', label: 'snippets' },
|
|
48
|
+
{ commandId: 'theme:style:push', id: 'theme-style', label: 'Global Styles' },
|
|
49
|
+
{ commandId: 'page:push', id: 'page', label: 'pages' },
|
|
50
|
+
{ commandId: 'menu:push', id: 'menu', label: 'menus' },
|
|
51
|
+
{ commandId: 'option:push', id: 'option', label: 'options' },
|
|
52
|
+
{ commandId: 'snippet:push', id: 'snippet', label: 'snippets' },
|
|
48
53
|
];
|
|
54
|
+
const PUSH_IDS = PUSH_TARGETS.map((target) => target.id);
|
|
49
55
|
export default class Push extends LoopressCommand {
|
|
50
56
|
static description = 'Push every local resource to WordPress in one run: plugins, composer dependencies, post types, taxonomies, post meta, roles, ACF, API routes, hooks, dynamic blocks, forms, patterns, templates and parts, Global Styles, pages, menus, options, and snippets';
|
|
51
57
|
static enableJsonFlag = true;
|
|
52
|
-
static examples = ['$ lps push', '$ lps push --env staging', '$ lps push --dry-run'];
|
|
58
|
+
static examples = ['$ lps push', '$ lps push --env staging', '$ lps push --dry-run', '$ lps push --only acf,page', '$ lps push --skip plugin --skip composer'];
|
|
53
59
|
static flags = {
|
|
54
60
|
...LoopressCommand.dryRunFlag,
|
|
55
61
|
...LoopressCommand.yesFlag,
|
|
62
|
+
...selectionFlags('push', PUSH_IDS),
|
|
63
|
+
...LoopressCommand.skipUnsupportedFlag,
|
|
56
64
|
overwrite: PushCommand.baseFlags.overwrite,
|
|
57
65
|
};
|
|
66
|
+
skipUnsupported = false;
|
|
67
|
+
// What --skip-unsupported held back, for the final warning.
|
|
68
|
+
unsupported = [];
|
|
58
69
|
async run() {
|
|
59
70
|
await this.guardProductionPush();
|
|
71
|
+
const { flags } = await this.parse(Push);
|
|
72
|
+
this.skipUnsupported = flags['skip-unsupported'];
|
|
73
|
+
const skipFlag = parseSelectionFlag(flags.skip, 'skip', PUSH_IDS, (message) => this.error(message));
|
|
74
|
+
const selected = new Set(selectResources(PUSH_IDS, parseSelectionFlag(flags.only, 'only', PUSH_IDS, (message) => this.error(message)), skipFlag));
|
|
60
75
|
const argv = this.buildArgv();
|
|
61
76
|
const results = [];
|
|
62
|
-
const skips = await this.skippedTargets();
|
|
77
|
+
const skips = await this.skippedTargets(selected, skipFlag.includes('composer'));
|
|
78
|
+
// The steps run in process and would each post their own record: collect, and post once.
|
|
79
|
+
const collected = startDeploymentCollector();
|
|
80
|
+
const record = async (status) => {
|
|
81
|
+
stopDeploymentCollector();
|
|
82
|
+
if (this.dryRun)
|
|
83
|
+
return;
|
|
84
|
+
const steps = results.map((result, index) => ({ command: PUSH_TARGETS[index]?.commandId ?? '', status: result.status }));
|
|
85
|
+
await postDeployment({ command: 'push', status, steps, url: this.siteConfig.url, ...(collected.plugins && { plugins: collected.plugins }) });
|
|
86
|
+
};
|
|
63
87
|
for (const target of PUSH_TARGETS) {
|
|
64
88
|
const skip = skips.get(target.commandId);
|
|
65
89
|
if (skip) {
|
|
66
90
|
this.log(`\n→ ${target.label}: ${skip.note}`);
|
|
67
|
-
results.push({ label: target.label, status: skip.status });
|
|
91
|
+
results.push({ label: target.label, ...(skip.reason && { reason: skip.reason }), status: skip.status });
|
|
68
92
|
continue;
|
|
69
93
|
}
|
|
70
94
|
this.log(`\n→ Pushing ${target.label}...`);
|
|
@@ -79,12 +103,18 @@ export default class Push extends LoopressCommand {
|
|
|
79
103
|
results.push({ error: message, label: target.label, status: 'failed' });
|
|
80
104
|
// Stop here: later targets depend on earlier ones (hooks use what composer installs,
|
|
81
105
|
// pages need their templates), so pushing them anyway can ship a silently broken site.
|
|
82
|
-
const skipped = PUSH_TARGETS.slice(PUSH_TARGETS.indexOf(target) + 1)
|
|
106
|
+
const skipped = PUSH_TARGETS.slice(PUSH_TARGETS.indexOf(target) + 1)
|
|
107
|
+
.filter((next) => !skips.has(next.commandId))
|
|
108
|
+
.map((next) => next.label);
|
|
83
109
|
const skippedNote = skipped.length > 0 ? ` Not attempted: ${skipped.join(', ')}.` : '';
|
|
110
|
+
this.warnUnsupported();
|
|
111
|
+
await record('failure');
|
|
84
112
|
this.error(`${target.label} failed to push: ${message}.${skippedNote}`);
|
|
85
113
|
}
|
|
86
114
|
}
|
|
87
|
-
|
|
115
|
+
await record('success');
|
|
116
|
+
this.log(this.doneMessage(selected.size < PUSH_IDS.length));
|
|
117
|
+
this.warnUnsupported();
|
|
88
118
|
return { results };
|
|
89
119
|
}
|
|
90
120
|
// A step the project uses on a site that can't hold it would only fail at its turn, after
|
|
@@ -94,10 +124,13 @@ export default class Push extends LoopressCommand {
|
|
|
94
124
|
const blockers = [];
|
|
95
125
|
for (const { commandId, missing, skipUnused, used } of steps) {
|
|
96
126
|
if (used && missing) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
127
|
+
const line = this.unsupportedLine(commandId, missing, declared);
|
|
128
|
+
if (this.skipUnsupported) {
|
|
129
|
+
skips.set(commandId, { note: `${missing.reason}, skipped (--skip-unsupported)`, reason: `${missing.reason} on ${this.siteConfig.name}`, status: 'skipped' });
|
|
130
|
+
this.unsupported.push(line);
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
blockers.push(line);
|
|
101
134
|
}
|
|
102
135
|
else if (!used && missing?.needsFull) {
|
|
103
136
|
skips.set(commandId, { note: `${missing.reason}, skipped`, status: 'skipped' });
|
|
@@ -137,6 +170,13 @@ export default class Push extends LoopressCommand {
|
|
|
137
170
|
return [];
|
|
138
171
|
}
|
|
139
172
|
}
|
|
173
|
+
// A run narrowed by --only / --skip must not read as a full push. Unused resources skipped
|
|
174
|
+
// on a plain run don't count: that run still pushes everything the project has.
|
|
175
|
+
doneMessage(narrowed) {
|
|
176
|
+
if (narrowed)
|
|
177
|
+
return this.dryRun ? '\nEvery selected resource would push, the others are skipped.' : '\nSelected resources pushed, the others are skipped.';
|
|
178
|
+
return this.dryRun ? '\nEvery resource would push.' : '\nAll resources pushed.';
|
|
179
|
+
}
|
|
140
180
|
// The site unreachable or the theme list refused: let the step run and report it itself.
|
|
141
181
|
async globalStylesSkipNote() {
|
|
142
182
|
try {
|
|
@@ -158,14 +198,20 @@ export default class Push extends LoopressCommand {
|
|
|
158
198
|
}
|
|
159
199
|
// Unconfigured is not a failure: a project that doesn't manage plugins or composer through
|
|
160
200
|
// Loopress must not stop the run before anything else gets pushed.
|
|
161
|
-
|
|
201
|
+
// `composerSkipped`: `composer` was named in --skip, as opposed to just left out of --only.
|
|
202
|
+
async skippedTargets(selected, composerSkipped) {
|
|
162
203
|
const skips = new Map();
|
|
204
|
+
const isSelected = (commandId) => selected.has(PUSH_TARGETS.find((target) => target.commandId === commandId)?.id ?? '');
|
|
163
205
|
const notConfigured = { note: 'not configured, skipped', status: 'skipped' };
|
|
164
206
|
const hasComposerJson = existsSync(this.composerJsonPath());
|
|
165
207
|
const declaredPlugins = await this.declaredPlugins(hasComposerJson);
|
|
166
208
|
// A composer.json that declares plugins is pushed whole by `plugin:push` (with its activation
|
|
167
209
|
// safety), so `composer:push` would only run Composer on the server a second time.
|
|
168
|
-
const composerPushedByPlugins = hasComposerJson && declaredPlugins.length > 0;
|
|
210
|
+
const composerPushedByPlugins = hasComposerJson && declaredPlugins.length > 0 && isSelected('plugin:push');
|
|
211
|
+
// That push runs Composer on the server, so a run told to skip Composer can't push plugins.
|
|
212
|
+
if (composerPushedByPlugins && composerSkipped) {
|
|
213
|
+
this.error('--skip composer conflicts with pushing plugins: composer.json declares them, so plugin push runs Composer on the server. Skip plugin too, or drop --skip composer.');
|
|
214
|
+
}
|
|
169
215
|
if (composerPushedByPlugins)
|
|
170
216
|
skips.set('composer:push', { note: 'already pushed whole by plugins', status: 'pushed' });
|
|
171
217
|
else if (!hasComposerJson)
|
|
@@ -174,28 +220,58 @@ export default class Push extends LoopressCommand {
|
|
|
174
220
|
if (declaredPlugins.length === 0)
|
|
175
221
|
skips.set('plugin:push', notConfigured);
|
|
176
222
|
const capabilities = await this.capabilities();
|
|
223
|
+
// Installed and activated by this run's first step, before the steps that need them: a fresh
|
|
224
|
+
// site (CI, a new staging) gets ACF from loopress.json and its field groups in one push.
|
|
225
|
+
// composer.json records no active state, so its plugins stay inactive until --activate.
|
|
226
|
+
const activatedByPush = hasComposerJson || !isSelected('plugin:push') ? [] : declaredPlugins.filter((slug) => pinActive(this.localConfig.plugins?.[slug]));
|
|
177
227
|
const missingDependencies = missingModule('dependencies', 'plugins and Composer', capabilities);
|
|
178
|
-
|
|
228
|
+
// A target left out by --only / --skip is reported skipped and never reaches the site checks,
|
|
229
|
+
// so it can't block the run.
|
|
230
|
+
const allSteps = [
|
|
179
231
|
...['plugin:push', 'composer:push'].map((commandId) => ({ commandId, missing: missingDependencies, skipUnused: false, used: !skips.has(commandId) })),
|
|
180
|
-
...Object.entries(PLUGIN_BACKED).map(([commandId, kind]) =>
|
|
181
|
-
|
|
182
|
-
missing
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
232
|
+
...Object.entries(PLUGIN_BACKED).map(([commandId, kind]) => {
|
|
233
|
+
const missing = missingIntegration(kind, capabilities);
|
|
234
|
+
// Not missing when one of the plugins that would provide it is activated earlier in the run.
|
|
235
|
+
const activated = missing?.plugins?.some((slug) => activatedByPush.includes(slug)) ?? false;
|
|
236
|
+
// A dry run activates nothing, so the site can't preview this step yet.
|
|
237
|
+
if (activated && this.dryRun)
|
|
238
|
+
skips.set(commandId, { note: `${missing?.reason} until this push activates it, skipped`, status: 'skipped' });
|
|
239
|
+
return { commandId, missing: activated ? undefined : missing, skipUnused: true, used: projectUses(kind, this.localConfig) };
|
|
240
|
+
}),
|
|
186
241
|
...Object.entries(FULL_ONLY).map(([commandId, kind]) => ({
|
|
187
242
|
commandId,
|
|
188
243
|
missing: missingIntegration(kind, capabilities),
|
|
189
|
-
skipUnused:
|
|
244
|
+
skipUnused: true,
|
|
190
245
|
used: projectUses(kind, this.localConfig),
|
|
191
246
|
})),
|
|
247
|
+
...Object.entries(CORE_BACKED).map(([commandId, kind]) => ({ commandId, missing: undefined, skipUnused: true, used: projectUses(kind, this.localConfig) })),
|
|
192
248
|
// Without templates, parts or CSS, the step would only write an empty child theme.
|
|
193
249
|
{ commandId: 'theme:template:push', missing: missingIntegration('template', capabilities), skipUnused: true, used: projectHasChildThemeFiles(this.localConfig) },
|
|
194
250
|
];
|
|
251
|
+
const steps = allSteps.filter((step) => isSelected(step.commandId));
|
|
195
252
|
this.applySiteSteps(steps, skips, { fromComposer: hasComposerJson, plugins: declaredPlugins });
|
|
196
|
-
const stylesNote = await this.globalStylesSkipNote();
|
|
253
|
+
const stylesNote = isSelected('theme:style:push') ? await this.globalStylesSkipNote() : undefined;
|
|
197
254
|
if (stylesNote)
|
|
198
255
|
skips.set('theme:style:push', { note: stylesNote, status: 'skipped' });
|
|
256
|
+
for (const target of PUSH_TARGETS) {
|
|
257
|
+
// Composer left out of --only still runs when plugin push syncs composer.json: say so.
|
|
258
|
+
if (composerPushedByPlugins && target.commandId === 'composer:push')
|
|
259
|
+
continue;
|
|
260
|
+
if (!selected.has(target.id))
|
|
261
|
+
skips.set(target.commandId, { note: 'not selected, skipped', status: 'skipped' });
|
|
262
|
+
}
|
|
199
263
|
return skips;
|
|
200
264
|
}
|
|
265
|
+
// "<resources>: <why> on <site>. <fix>", for the refusal and for the --skip-unsupported warning.
|
|
266
|
+
unsupportedLine(commandId, missing, declared) {
|
|
267
|
+
const label = PUSH_TARGETS.find((target) => target.commandId === commandId)?.label;
|
|
268
|
+
const fix = integrationFix(missing, declared);
|
|
269
|
+
const fixSentence = fix ? ` ${fix}.` : '';
|
|
270
|
+
return `${label}: ${missing.reason} on ${this.siteConfig.name}.${fixSentence}`;
|
|
271
|
+
}
|
|
272
|
+
warnUnsupported() {
|
|
273
|
+
if (this.unsupported.length > 0) {
|
|
274
|
+
this.warn(`Skipped with --skip-unsupported, this site can't hold them:\n ${this.unsupported.join('\n ')}\nResources that depend on them (a page placing a block, code calling a hook) may not work until they are pushed.`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
201
277
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
|
-
import { readFile,
|
|
2
|
+
import { readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { basename, dirname, extname, join } from 'node:path';
|
|
4
4
|
import { loadSnippets as loadSnippetsFromDisk } from '../../lib/load-snippets.js';
|
|
5
5
|
import { errorCode, errorMessage, isRecord } from '../../lib/narrow.js';
|
|
@@ -10,6 +10,7 @@ import { isNotFoundError } from '../../lib/wp-client.js';
|
|
|
10
10
|
import {} from '../../types/snippet.generated.js';
|
|
11
11
|
import {} from '../../types/snippet.js';
|
|
12
12
|
import { pluralize } from '../../utils/pluralize.js';
|
|
13
|
+
import { renameWithRetry } from '../../utils/rename-retry.js';
|
|
13
14
|
import { normalizeSnippet, SNIPPETS_ENDPOINT, stripPhpOpeningTag } from '../../utils/snippet-format.js';
|
|
14
15
|
import { toSlug } from '../../utils/to-slug.js';
|
|
15
16
|
export default class Push extends PushCommand {
|
|
@@ -89,7 +90,7 @@ export default class Push extends PushCommand {
|
|
|
89
90
|
return;
|
|
90
91
|
const newPath = join(dir, `${canonicalBase}${ext}`);
|
|
91
92
|
const newMetaPath = join(dir, `${canonicalBase}.json`);
|
|
92
|
-
await
|
|
93
|
+
await renameWithRetry(snippet.path, newPath);
|
|
93
94
|
await writeFile(newMetaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
94
95
|
await rm(oldMetaPath, { force: true });
|
|
95
96
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resourceDiffCommand } from '../../../lib/diff-command.js';
|
|
2
|
-
export default resourceDiffCommand('theme-
|
|
2
|
+
export default resourceDiffCommand('theme-style', {
|
|
3
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
4
|
command: 'theme style',
|
|
5
5
|
pathNoun: 'theme styles directory',
|
|
@@ -34,7 +34,7 @@ export default class Push extends PushCommand {
|
|
|
34
34
|
throw error;
|
|
35
35
|
}
|
|
36
36
|
// Only the active theme's file is pushed, whatever else the directory holds.
|
|
37
|
-
const provider = getResourceStateProvider('theme-
|
|
37
|
+
const provider = getResourceStateProvider('theme-style');
|
|
38
38
|
const beforeState = await this.captureBeforePushState(provider, dir, (name) => name === stylesheet);
|
|
39
39
|
if (this.dryRun)
|
|
40
40
|
return { changes: this.previewChanges, file, status: 'dry-run' };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resourceRollbackCommand } from '../../../lib/rollback-command.js';
|
|
2
|
-
export default resourceRollbackCommand('theme-
|
|
2
|
+
export default resourceRollbackCommand('theme-style', {
|
|
3
3
|
description: "Restore the active theme's Global Styles to a snapshot taken automatically before an earlier `lps theme style push`",
|
|
4
4
|
command: 'theme style',
|
|
5
5
|
pathNoun: 'theme styles directory',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
2
3
|
import { activateTheme } from '../../../lib/capabilities.js';
|
|
3
4
|
import {} from '../../../lib/diff-state.js';
|
|
5
|
+
import { hasExclusions, isExcluded } from '../../../lib/exclude.js';
|
|
4
6
|
import { errorMessage } from '../../../lib/narrow.js';
|
|
5
7
|
import { logChanges, pushChanges, PushCommand } from '../../../lib/push-command.js';
|
|
6
8
|
import { formatPageProblems } from '../../../utils/page-format.js';
|
|
@@ -39,8 +41,15 @@ export default class Push extends PushCommand {
|
|
|
39
41
|
};
|
|
40
42
|
this.log(`Found ${pluralize(templates.length, 'template')}, ${pluralize(parts.length, 'part')}, ` +
|
|
41
43
|
`${css.style ? 'a global stylesheet' : 'no global stylesheet'} and ${pluralize(css.blocks.length, 'block stylesheet')}`);
|
|
44
|
+
this.warnEnvironmentUrls([
|
|
45
|
+
...templates.map((template) => ({ id: `templates/${template.slug}`, value: template.html })),
|
|
46
|
+
...parts.map((part) => ({ id: `parts/${part.slug}`, value: part.html })),
|
|
47
|
+
]);
|
|
48
|
+
// The PUT replaces every file of the child theme: what the site has under a path this
|
|
49
|
+
// environment excludes (loopress.json) is sent back as it is, so it is left alone.
|
|
50
|
+
const kept = await this.keptFromSite(templatesPath, partsPath, templates, parts);
|
|
42
51
|
if (this.dryRun) {
|
|
43
|
-
const changes = await this.previewChildTheme(templates, parts, css);
|
|
52
|
+
const changes = await this.previewChildTheme(templates, parts, css, kept);
|
|
44
53
|
logChanges((message) => { this.log(message); }, changes.templates, 'templates/');
|
|
45
54
|
logChanges((message) => { this.log(message); }, changes.parts, 'parts/');
|
|
46
55
|
logChanges((message) => { this.log(message); }, changes.css, 'css/');
|
|
@@ -51,7 +60,7 @@ export default class Push extends PushCommand {
|
|
|
51
60
|
}
|
|
52
61
|
return { ...names, changes, status: 'dry-run' };
|
|
53
62
|
}
|
|
54
|
-
const child = await this.wp.put(CHILD_THEME_ENDPOINT, { ...css, parts, templates });
|
|
63
|
+
const child = await this.wp.put(CHILD_THEME_ENDPOINT, { ...css, parts: [...parts, ...kept.parts], templates: [...templates, ...kept.templates] });
|
|
55
64
|
await this.recordSuccess();
|
|
56
65
|
this.log(`Child theme ${child.stylesheet} written.`);
|
|
57
66
|
// Activated only when the project asks for it: a theme switch changes every per-theme setting.
|
|
@@ -83,15 +92,23 @@ export default class Push extends PushCommand {
|
|
|
83
92
|
unknownBlocks,
|
|
84
93
|
};
|
|
85
94
|
}
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
// The site's templates and parts under an excluded path the project doesn't push. Refuses when
|
|
96
|
+
// the site can't be read: pushing blind would remove them.
|
|
97
|
+
async keptFromSite(templatesPath, partsPath, templates, parts) {
|
|
98
|
+
if (!hasExclusions())
|
|
99
|
+
return { parts: [], templates: [] };
|
|
100
|
+
const child = await this.readChildTheme();
|
|
101
|
+
const localTemplates = new Set(templates.map((template) => template.slug));
|
|
102
|
+
const localParts = new Set(parts.map((part) => part.slug));
|
|
103
|
+
return {
|
|
104
|
+
parts: child.parts.filter((part) => !localParts.has(part.slug) && isExcluded(join(partsPath, `${part.slug}.html`))),
|
|
105
|
+
templates: child.templates.filter((template) => !localTemplates.has(template.slug) && isExcluded(join(templatesPath, `${template.slug}.html`))),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Same read and same refusal as previewRemote, one GET for templates, parts and CSS. Kept
|
|
109
|
+
// files are held back, they are neither changes nor removals.
|
|
110
|
+
async previewChildTheme(templates, parts, css, kept) {
|
|
111
|
+
const child = await this.readChildTheme();
|
|
95
112
|
const localCss = new Map(css.blocks.map((block) => [`blocks/${block.name}.css`, sha256(block.css)]));
|
|
96
113
|
if (css.style !== '')
|
|
97
114
|
localCss.set('style.css', sha256(css.style));
|
|
@@ -100,14 +117,26 @@ export default class Push extends PushCommand {
|
|
|
100
117
|
: [...localCss.keys()].toSorted((a, b) => a.localeCompare(b)).map((id) => ({ id, status: 'unknown' }));
|
|
101
118
|
return {
|
|
102
119
|
css: cssChanges,
|
|
103
|
-
parts: pushChanges(bySlug(parts), bySlug(child.parts), { mirror: true }),
|
|
104
|
-
templates: pushChanges(bySlug(templates), bySlug(child.templates), { mirror: true }),
|
|
120
|
+
parts: withoutKept(pushChanges(bySlug([...parts, ...kept.parts]), bySlug(child.parts), { mirror: true }), kept.parts),
|
|
121
|
+
templates: withoutKept(pushChanges(bySlug([...templates, ...kept.templates]), bySlug(child.templates), { mirror: true }), kept.templates),
|
|
105
122
|
};
|
|
106
123
|
}
|
|
124
|
+
async readChildTheme() {
|
|
125
|
+
try {
|
|
126
|
+
return await this.wp.get(CHILD_THEME_ENDPOINT);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
this.error(`The site would refuse this push: ${errorMessage(error)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
107
132
|
}
|
|
108
133
|
function bySlug(files) {
|
|
109
134
|
return new Map(files.map(({ slug, ...rest }) => [slug, rest]));
|
|
110
135
|
}
|
|
136
|
+
function withoutKept(changes, kept) {
|
|
137
|
+
const slugs = new Set(kept.map((file) => file.slug));
|
|
138
|
+
return changes.filter((change) => !slugs.has(change.id));
|
|
139
|
+
}
|
|
111
140
|
function sha256(text) {
|
|
112
141
|
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
113
142
|
}
|
|
@@ -11,6 +11,9 @@ export default class Validate extends Command {
|
|
|
11
11
|
for (const problem of result.problems) {
|
|
12
12
|
this.log(` ✗ ${problem.file}: ${problem.message}`);
|
|
13
13
|
}
|
|
14
|
+
for (const warning of result.warnings) {
|
|
15
|
+
this.log(` ! ${warning.file}: ${warning.message}`);
|
|
16
|
+
}
|
|
14
17
|
this.log('');
|
|
15
18
|
this.log(result.valid
|
|
16
19
|
? `${result.checked} file(s) checked, no problems found.`
|
package/dist/lib/base.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type EnvironmentConfig } from '../types/config.js';
|
|
|
3
3
|
import { type LoopressLocalConfig } from '../utils/loopress-config.js';
|
|
4
4
|
import { type SiteCapabilities } from './capabilities.js';
|
|
5
5
|
import { WpClient } from './wp-client.js';
|
|
6
|
+
export declare function listrRenderer(silent: boolean): 'default' | 'silent' | 'simple';
|
|
6
7
|
export declare abstract class LoopressCommand extends Command {
|
|
7
8
|
static baseFlags: {
|
|
8
9
|
env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -10,6 +11,10 @@ export declare abstract class LoopressCommand extends Command {
|
|
|
10
11
|
static dryRunFlag: {
|
|
11
12
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
};
|
|
14
|
+
static honorsExclude: boolean;
|
|
15
|
+
static skipUnsupportedFlag: {
|
|
16
|
+
'skip-unsupported': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
13
18
|
static yesFlag: {
|
|
14
19
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
20
|
};
|
|
@@ -17,6 +22,7 @@ export declare abstract class LoopressCommand extends Command {
|
|
|
17
22
|
protected localConfig: LoopressLocalConfig;
|
|
18
23
|
protected overwrite: boolean;
|
|
19
24
|
protected productionConfirmed: boolean;
|
|
25
|
+
protected projectEnvironments: EnvironmentConfig[];
|
|
20
26
|
protected projectId: string;
|
|
21
27
|
protected siteConfig: EnvironmentConfig;
|
|
22
28
|
protected yes: boolean;
|
|
@@ -44,6 +50,7 @@ export declare abstract class LoopressCommand extends Command {
|
|
|
44
50
|
protected resolveOptionsPath(override?: string): string;
|
|
45
51
|
protected resolveSnippetsPath(override?: string): string;
|
|
46
52
|
protected resolveThemeStylesPath(override?: string): string;
|
|
53
|
+
private assertSiteSupportsResource;
|
|
47
54
|
private pickEnvironment;
|
|
48
55
|
private resolveEnvironment;
|
|
49
56
|
}
|
package/dist/lib/base.js
CHANGED
|
@@ -8,11 +8,21 @@ import {} from '../types/config.js';
|
|
|
8
8
|
import { readLocalConfig } from '../utils/loopress-config.js';
|
|
9
9
|
import { pluralize } from '../utils/pluralize.js';
|
|
10
10
|
import { resolveResourceDir } from '../utils/resource-dirs.js';
|
|
11
|
-
import { getCapabilities } from './capabilities.js';
|
|
11
|
+
import { commandResourceKind, getCapabilities, missingIntegration, unsupportedMessage } from './capabilities.js';
|
|
12
|
+
import { declaredPlugins } from './declared-plugins.js';
|
|
13
|
+
import { activateExclusions } from './exclude.js';
|
|
12
14
|
import { isInteractive } from './interactive.js';
|
|
13
15
|
import { isAppPasswordStale, rotateAppPassword } from './rotate-app-password.js';
|
|
14
16
|
import { withoutSpinner } from './spinner.js';
|
|
15
17
|
import { WpClient } from './wp-client.js';
|
|
18
|
+
// Listr's default renderer wraps every label to the terminal width, so a terminal reporting no
|
|
19
|
+
// width (columns 0 or undefined, e.g. `script` without a size) renders one letter per line. The
|
|
20
|
+
// simple renderer prints plain lines and does not wrap.
|
|
21
|
+
export function listrRenderer(silent) {
|
|
22
|
+
if (silent)
|
|
23
|
+
return 'silent';
|
|
24
|
+
return process.stdout.columns > 0 ? 'default' : 'simple';
|
|
25
|
+
}
|
|
16
26
|
export class LoopressCommand extends Command {
|
|
17
27
|
// On every subclass without opt-in. Without it, commands target the "local" environment, so
|
|
18
28
|
// reaching a remote site is always spelled out in the command itself.
|
|
@@ -24,6 +34,16 @@ export class LoopressCommand extends Command {
|
|
|
24
34
|
static dryRunFlag = {
|
|
25
35
|
'dry-run': Flags.boolean({ char: 'd', description: 'Show what would change without making changes' }),
|
|
26
36
|
};
|
|
37
|
+
// Whether the environment's `exclude` globs in loopress.json apply (lib/exclude.ts): push and
|
|
38
|
+
// diff commands only, never pull.
|
|
39
|
+
static honorsExclude = false;
|
|
40
|
+
// Opt-in on push, pull, diff and promote: a resource the project uses but the site can't hold
|
|
41
|
+
// (see lib/capabilities.ts) is reported skipped, with a final warning, instead of failing the run.
|
|
42
|
+
static skipUnsupportedFlag = {
|
|
43
|
+
'skip-unsupported': Flags.boolean({
|
|
44
|
+
description: "Skip the resources the project uses but this site can't hold (a missing plugin, a Full feature on Light) instead of failing, and warn about each",
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
27
47
|
static yesFlag = {
|
|
28
48
|
yes: Flags.boolean({ char: 'y', description: 'Answer yes to confirmation prompts' }),
|
|
29
49
|
};
|
|
@@ -33,6 +53,8 @@ export class LoopressCommand extends Command {
|
|
|
33
53
|
overwrite = false;
|
|
34
54
|
// Set by PushCommand's hidden --production-confirmed, see there.
|
|
35
55
|
productionConfirmed = false;
|
|
56
|
+
// Every environment of the project, the target included (see environment-urls.ts).
|
|
57
|
+
projectEnvironments = [];
|
|
36
58
|
projectId;
|
|
37
59
|
siteConfig;
|
|
38
60
|
yes = false;
|
|
@@ -75,7 +97,15 @@ export class LoopressCommand extends Command {
|
|
|
75
97
|
const resolved = this.resolveEnvironment(flags.env);
|
|
76
98
|
this.siteConfig = resolved.env;
|
|
77
99
|
this.projectId = resolved.projectId;
|
|
100
|
+
activateExclusions({
|
|
101
|
+
config: this.localConfig,
|
|
102
|
+
cwd: process.cwd(),
|
|
103
|
+
envName: this.siteConfig.name,
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- see above
|
|
105
|
+
honors: this.ctor.honorsExclude,
|
|
106
|
+
});
|
|
78
107
|
await this.maybeAutoRotate();
|
|
108
|
+
await this.assertSiteSupportsResource();
|
|
79
109
|
}
|
|
80
110
|
// Silent, best-effort: a stale app password still works, so a failed rotation attempt
|
|
81
111
|
// (offline, site down) just tries again on the next command instead of blocking this one.
|
|
@@ -128,7 +158,10 @@ export class LoopressCommand extends Command {
|
|
|
128
158
|
task.output = `Pulled: ${options.title(item)}`;
|
|
129
159
|
},
|
|
130
160
|
title: `Pull ${options.title(item)}`,
|
|
131
|
-
})),
|
|
161
|
+
})),
|
|
162
|
+
// Listr types the task wrapper per renderer, a union of renderer names does not fit it.
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Listr ties the task wrapper type to one renderer
|
|
164
|
+
{ renderer: listrRenderer(this.jsonEnabled()) }).run();
|
|
132
165
|
});
|
|
133
166
|
await this.removeOrphanedFiles(dir, orphans, options.orphanReason);
|
|
134
167
|
this.log(options.pulledMessage);
|
|
@@ -187,6 +220,25 @@ export class LoopressCommand extends Command {
|
|
|
187
220
|
resolveThemeStylesPath(override) {
|
|
188
221
|
return resolveResourceDir('themeStyles', this.localConfig, override);
|
|
189
222
|
}
|
|
223
|
+
// A command on a single kind of resource (`lps hook push`) says what `lps push` says when the
|
|
224
|
+
// site can't hold it, instead of failing on the 404. A site whose capabilities can't be read
|
|
225
|
+
// (too old a plugin, unreachable) lets the command run and report its own error.
|
|
226
|
+
async assertSiteSupportsResource() {
|
|
227
|
+
const kind = commandResourceKind(this.id);
|
|
228
|
+
if (!kind)
|
|
229
|
+
return;
|
|
230
|
+
let missing;
|
|
231
|
+
try {
|
|
232
|
+
missing = missingIntegration(kind, await this.capabilities());
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!missing)
|
|
238
|
+
return;
|
|
239
|
+
const declared = await declaredPlugins({ localConfig: this.localConfig, rootDir: this.rootDir, siteName: this.siteConfig.name });
|
|
240
|
+
this.error(unsupportedMessage(missing, this.siteConfig.name, declared));
|
|
241
|
+
}
|
|
190
242
|
pickEnvironment(project, envName) {
|
|
191
243
|
const env = project.environments[envName];
|
|
192
244
|
if (!env) {
|
|
@@ -204,6 +256,7 @@ export class LoopressCommand extends Command {
|
|
|
204
256
|
if (!project) {
|
|
205
257
|
this.error(`Project "${projectId}" (from loopress.json) not found. Run \`lps project config\` to configure it.`);
|
|
206
258
|
}
|
|
259
|
+
this.projectEnvironments = Object.values(project.environments);
|
|
207
260
|
const envNames = Object.keys(project.environments);
|
|
208
261
|
if (envNames.length === 0) {
|
|
209
262
|
this.error(`Project "${project.name}" has no environments configured. Run \`lps project config\` to add one.`);
|
|
@@ -36,6 +36,11 @@ export declare function integrationFix(missing: MissingIntegration, declared: {
|
|
|
36
36
|
fromComposer: boolean;
|
|
37
37
|
plugins: string[];
|
|
38
38
|
}): string | undefined;
|
|
39
|
+
export declare function commandResourceKind(commandId: string | undefined): ResourceDirKind | undefined;
|
|
40
|
+
export declare function unsupportedMessage(missing: MissingIntegration, site: string, declared: {
|
|
41
|
+
fromComposer: boolean;
|
|
42
|
+
plugins: string[];
|
|
43
|
+
}): string;
|
|
39
44
|
export type ThemeActivation = {
|
|
40
45
|
active: string;
|
|
41
46
|
changed: boolean;
|
package/dist/lib/capabilities.js
CHANGED
|
@@ -78,6 +78,22 @@ export function integrationFix(missing, declared) {
|
|
|
78
78
|
const where = declared.fromComposer ? `wpackagist-plugin/${first} in composer.json` : `"${first}" in loopress.json plugins`;
|
|
79
79
|
return `Declare ${first}${or} (${where}), then activate it: ${activate(first)}`;
|
|
80
80
|
}
|
|
81
|
+
// The command topics that act on one kind of resource the site may be unable to hold
|
|
82
|
+
// (`lps hook push`, `lps acf list`...), so they can say so instead of surfacing the 404. `publish`
|
|
83
|
+
// goes through the console, not the site. `scf` is the alias of `acf`.
|
|
84
|
+
const TOPIC_KINDS = { acf: 'acf', api: 'api', block: 'block', form: 'form', hook: 'hooks', page: 'page', pattern: 'pattern', scf: 'acf', snippet: 'snippets' };
|
|
85
|
+
export function commandResourceKind(commandId) {
|
|
86
|
+
const [topic, action] = (commandId ?? '').split(':', 2);
|
|
87
|
+
if (!topic || !action || action === 'publish')
|
|
88
|
+
return undefined;
|
|
89
|
+
return TOPIC_KINDS[topic];
|
|
90
|
+
}
|
|
91
|
+
// "Not supported by <site>: <reason>. <fix>.", for the single-resource commands.
|
|
92
|
+
export function unsupportedMessage(missing, site, declared) {
|
|
93
|
+
const fix = integrationFix(missing, declared);
|
|
94
|
+
const fixSentence = fix ? ` ${fix}.` : '';
|
|
95
|
+
return `Not supported by ${site}: ${missing.reason}.${fixSentence}`;
|
|
96
|
+
}
|
|
81
97
|
export async function activateTheme(wp, stylesheet) {
|
|
82
98
|
const activation = await wp.post(THEME_ACTIVATE_ENDPOINT, { stylesheet });
|
|
83
99
|
cache.delete(wp);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type LoopressLocalConfig } from '../utils/loopress-config.js';
|
|
2
|
+
export declare function declaredPlugins(options: {
|
|
3
|
+
localConfig: LoopressLocalConfig;
|
|
4
|
+
rootDir: string;
|
|
5
|
+
siteName: string;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
fromComposer: boolean;
|
|
8
|
+
plugins: string[];
|
|
9
|
+
}>;
|