PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
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 0.5.0 All 128 releases
extendify / src / AutoLaunch / functions / plugins.js

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

231 lines 6.1 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: { source: 'auto-launch', caller: 'installPlugin' },
67 });
68 return null;
69 }
70 }
71 };
72
73 export const installPlugin = (slug) =>
74 enqueue(async () => {
75 const known = installed.get(slug);
76 if (known) return { plugin: known };
77
78 const plugin = await install(slug);
79 if (plugin?.plugin) installed.set(slug, plugin.plugin);
80 return plugin;
81 });
82
83 export const getPlugin = async (slug) => {
84 const response = await apiFetch({
85 path: addQueryArgs('/wp/v2/plugins', { search: slug }),
86 });
87 return response?.find((p) => p.plugin?.split('/')[0] === slug);
88 };
89
90 export const activatePlugin = async (slug) => {
91 const fn = (s) =>
92 apiFetch({
93 path: `/wp/v2/plugins/${s}`,
94 method: 'POST',
95 data: { status: 'active' },
96 });
97
98 try {
99 await fn(slug);
100 return true;
101 } catch (error) {
102 const report = (e) => {
103 digest({
104 error: e,
105 details: { source: 'auto-launch', caller: 'activatePlugin' },
106 });
107 return false;
108 };
109 const found = await getPlugin(slug).catch(() => undefined);
110 if (!found?.plugin) return report(error);
111 return fn(found.plugin).then(() => true, report);
112 }
113 };
114
115 // Exact match, not substring: woocommerce-payments would mask a missing woocommerce.
116 const notActive = (activePlugins, slugs) =>
117 slugs.filter(
118 (slug) => !activePlugins?.some((path) => path.split('/')[0] === slug),
119 );
120
121 export const ensurePluginsActive = async (slugs) => {
122 // Two callers don't await this; a throw here silently skips every install.
123 const active = await getActivePlugins().catch((error) => {
124 digest({
125 error,
126 details: { source: 'auto-launch', caller: 'ensurePluginsActive' },
127 });
128 return [];
129 });
130 const missing = notActive(active, slugs);
131 if (!missing.length) return { failed: [] };
132
133 const onDisk = window.extSharedData?.installedPluginsSlugs ?? [];
134 const failed = [];
135 for (const slug of missing) {
136 try {
137 const plugin = onDisk.includes(slug) ? null : await installPlugin(slug);
138 const activated = await activatePlugin(plugin?.plugin ?? slug);
139 if (!activated) failed.push(slug);
140 } catch (_) {
141 failed.push(slug);
142 }
143 }
144 return { failed };
145 };
146
147 export const reportInactivePlugins = async (slugs) => {
148 // Falling back to an empty list would name every plugin as inactive.
149 const active = await getActivePlugins().catch(() => null);
150 if (!active) return;
151
152 const inactive = notActive(active, slugs);
153 if (!inactive.length) return;
154
155 digest({
156 error: { message: `Plugins inactive after launch: ${inactive.join(', ')}` },
157 details: {
158 source: 'auto-launch',
159 caller: 'reportInactivePlugins',
160 inactive,
161 },
162 });
163 };
164
165 export const reportFailedDependencies = (pages) => {
166 const failed = [
167 ...new Set(pages.flatMap(({ patterns }) => failedDependencies(patterns))),
168 ];
169 if (!failed.length) return;
170
171 digest({
172 error: {
173 message: `Patterns left static after a failed dependency: ${failed.join(', ')}`,
174 },
175 details: {
176 source: 'auto-launch',
177 caller: 'reportFailedDependencies',
178 failed,
179 },
180 });
181 };
182
183 // Currently this only processes patterns with placeholders
184 // by swapping out the placeholders with the actual code
185 // returns the patterns as blocks with the placeholders replaced
186 export const replacePlaceholderPatterns = async (patterns) => {
187 // Directly replace "blog-section" patterns using their replacement code, skipping the API call
188 patterns = patterns.map((pattern) => {
189 if (
190 pattern.patternTypes.includes('blog-section') &&
191 pattern.patternReplacementCode
192 ) {
193 return {
194 ...pattern,
195 code: pattern.patternReplacementCode,
196 };
197 }
198 return pattern;
199 });
200
201 const hasPlaceholders = patterns.filter((p) => p.patternReplacementCode);
202 if (!hasPlaceholders?.length) return patterns;
203
204 const activePlugins =
205 (await getActivePlugins())?.data?.map((path) => path.split('/')[0]) || [];
206
207 const pluginsActivity = patterns
208 .filter((p) => p.pluginDependency)
209 .map((p) => p.pluginDependency)
210 .filter((p) => !activePlugins.includes(p));
211
212 for (const plugin of pluginsActivity) {
213 recordPluginActivity({
214 slug: plugin,
215 source: 'auto-launch',
216 });
217 }
218
219 return await processWithSecondPass(processPlaceholders, patterns);
220 };
221
222 // This endpoint installs pattern dependencies from PHP, outside the queue.
223 export const processPlaceholders = async (patterns) => {
224 await waitForInstalls();
225 return apiFetch({
226 path: '/extendify/v1/shared/process-placeholders',
227 method: 'POST',
228 data: { patterns },
229 });
230 };
231