PluginProbe
Extendify / 3.2.3
Extendify v3.2.3
3.2.3 3.2.2 3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 All 129 releases
extendify / src / AutoLaunch / functions / plugins.js

plugins.js in Extendify 3.2.3, at src/AutoLaunch/functions/plugins.js

239 lines 6.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { recordPluginActivity } from '@shared/api/DataApi';
2 import { digest } from '@shared/api/digest';
3 import { enableAutoUpdate } from '@shared/api/wp';
4 import {
5 failedDependencies,
6 processWithSecondPass,
7 } from '@shared/lib/patterns';
8 import apiFetch from '@wordpress/api-fetch';
9 import { addQueryArgs } from '@wordpress/url';
10
11 export const getActivePlugins = () =>
12 apiFetch({ path: 'extendify/v1/auto-launch/active-plugins' });
13
14 export const alreadyActive = (activePlugins, pluginSlug) =>
15 activePlugins?.filter((p) => p.includes(pluginSlug))?.length;
16
17 // WP unpacks every install through one shared dir; two at once corrupt both.
18 let installQueue = Promise.resolve();
19
20 // Re-installing is not free: WP unpacks before it finds the folder exists.
21 const installed = new Map();
22
23 const enqueue = (task) => {
24 const result = installQueue.then(task);
25 installQueue = result.catch(() => {});
26 return result;
27 };
28
29 const INSTALL_DRAIN_MS = 60_000;
30
31 // The install POST has no timeout; uncapped, one stall parks the build.
32 const waitForInstalls = () => {
33 let timer;
34 return Promise.race([
35 installQueue,
36 new Promise((resolve) => {
37 timer = setTimeout(resolve, INSTALL_DRAIN_MS);
38 }),
39 ]).finally(() => clearTimeout(timer));
40 };
41
42 const install = async (slug) => {
43 const fn = async () => {
44 const p = await apiFetch({
45 path: '/wp/v2/plugins',
46 method: 'POST',
47 data: { slug },
48 });
49 // Unawaited: no timeout on these, and a stall would hold the queue.
50 recordPluginActivity({ slug, source: 'auto-launch' });
51 enableAutoUpdate(p?.plugin);
52 return p;
53 };
54 try {
55 return await fn();
56 } catch (error) {
57 if (error?.code === 'folder_exists') {
58 // Already on disk — fetch its record so the caller can activate it.
59 return await getPlugin(slug);
60 }
61 try {
62 return await fn();
63 } catch (error) {
64 digest({
65 error,
66 details: {
67 source: 'auto-launch',
68 caller: 'installPlugin',
69 plugin: slug,
70 },
71 });
72 return null;
73 }
74 }
75 };
76
77 export const installPlugin = (slug) =>
78 enqueue(async () => {
79 const known = installed.get(slug);
80 if (known) return { plugin: known };
81
82 const plugin = await install(slug);
83 if (plugin?.plugin) installed.set(slug, plugin.plugin);
84 return plugin;
85 });
86
87 export const getPlugin = async (slug) => {
88 const response = await apiFetch({
89 path: addQueryArgs('/wp/v2/plugins', { search: slug }),
90 });
91 return response?.find((p) => p.plugin?.split('/')[0] === slug);
92 };
93
94 export const activatePlugin = async (slug) => {
95 const fn = (s) =>
96 apiFetch({
97 path: `/wp/v2/plugins/${s}`,
98 method: 'POST',
99 data: { status: 'active' },
100 });
101
102 try {
103 await fn(slug);
104 return true;
105 } catch (error) {
106 const report = (e) => {
107 digest({
108 error: e,
109 details: {
110 source: 'auto-launch',
111 caller: 'activatePlugin',
112 plugin: slug,
113 },
114 });
115 return false;
116 };
117 const found = await getPlugin(slug).catch(() => undefined);
118 if (!found?.plugin) return report(error);
119 return fn(found.plugin).then(() => true, report);
120 }
121 };
122
123 // Exact match, not substring: woocommerce-payments would mask a missing woocommerce.
124 const notActive = (activePlugins, slugs) =>
125 slugs.filter(
126 (slug) => !activePlugins?.some((path) => path.split('/')[0] === slug),
127 );
128
129 export const ensurePluginsActive = async (slugs) => {
130 // Two callers don't await this; a throw here silently skips every install.
131 const active = await getActivePlugins().catch((error) => {
132 digest({
133 error,
134 details: { source: 'auto-launch', caller: 'ensurePluginsActive' },
135 });
136 return [];
137 });
138 const missing = notActive(active, slugs);
139 if (!missing.length) return { failed: [] };
140
141 const onDisk = window.extSharedData?.installedPluginsSlugs ?? [];
142 const failed = [];
143 for (const slug of missing) {
144 try {
145 const plugin = onDisk.includes(slug) ? null : await installPlugin(slug);
146 const activated = await activatePlugin(plugin?.plugin ?? slug);
147 if (!activated) failed.push(slug);
148 } catch (_) {
149 failed.push(slug);
150 }
151 }
152 return { failed };
153 };
154
155 export const reportInactivePlugins = async (slugs) => {
156 // Falling back to an empty list would name every plugin as inactive.
157 const active = await getActivePlugins().catch(() => null);
158 if (!active) return;
159
160 const inactive = notActive(active, slugs);
161 if (!inactive.length) return;
162
163 digest({
164 error: { message: `Plugins inactive after launch: ${inactive.join(', ')}` },
165 details: {
166 source: 'auto-launch',
167 caller: 'reportInactivePlugins',
168 inactive,
169 },
170 });
171 };
172
173 export const reportFailedDependencies = (pages) => {
174 const failed = [
175 ...new Set(pages.flatMap(({ patterns }) => failedDependencies(patterns))),
176 ];
177 if (!failed.length) return;
178
179 digest({
180 error: {
181 message: `Patterns left static after a failed dependency: ${failed.join(', ')}`,
182 },
183 details: {
184 source: 'auto-launch',
185 caller: 'reportFailedDependencies',
186 failed,
187 },
188 });
189 };
190
191 // Currently this only processes patterns with placeholders
192 // by swapping out the placeholders with the actual code
193 // returns the patterns as blocks with the placeholders replaced
194 export const replacePlaceholderPatterns = async (patterns) => {
195 // Directly replace "blog-section" patterns using their replacement code, skipping the API call
196 patterns = patterns.map((pattern) => {
197 if (
198 pattern.patternTypes.includes('blog-section') &&
199 pattern.patternReplacementCode
200 ) {
201 return {
202 ...pattern,
203 code: pattern.patternReplacementCode,
204 };
205 }
206 return pattern;
207 });
208
209 const hasPlaceholders = patterns.filter((p) => p.patternReplacementCode);
210 if (!hasPlaceholders?.length) return patterns;
211
212 const activePlugins =
213 (await getActivePlugins())?.data?.map((path) => path.split('/')[0]) || [];
214
215 const pluginsActivity = patterns
216 .filter((p) => p.pluginDependency)
217 .map((p) => p.pluginDependency)
218 .filter((p) => !activePlugins.includes(p));
219
220 for (const plugin of pluginsActivity) {
221 recordPluginActivity({
222 slug: plugin,
223 source: 'auto-launch',
224 });
225 }
226
227 return await processWithSecondPass(processPlaceholders, patterns);
228 };
229
230 // This endpoint installs pattern dependencies from PHP, outside the queue.
231 export const processPlaceholders = async (patterns) => {
232 await waitForInstalls();
233 return apiFetch({
234 path: '/extendify/v1/shared/process-placeholders',
235 method: 'POST',
236 data: { patterns },
237 });
238 };
239