PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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 0.6.0 All 127 releases
extendify / src / AutoLaunch / functions / plugins.js

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

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