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
← All changes | src/AutoLaunch/functions/plugins.js +135 -23 3.0.63.2.1 View file →
@@ -1,5 +1,11 @@
1 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';
2 8 import apiFetch from '@wordpress/api-fetch';
3 9 import { addQueryArgs } from '@wordpress/url';
4 10
5 11 export const getActivePlugins = () =>
@@ -7,9 +13,34 @@
7 13
8 14 export const alreadyActive = (activePlugins, pluginSlug) =>
9 15 activePlugins?.filter((p) => p.includes(pluginSlug))?.length;
10 16
11 -export const installPlugin = async (slug) => {
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) => {
12 43 const fn = async () => {
13 44 const p = await apiFetch({
14 45 path: '/wp/v2/plugins',
15 46 method: 'POST',
@@ -14,9 +45,11 @@
14 45 path: '/wp/v2/plugins',
15 46 method: 'POST',
16 47 data: { slug },
17 48 });
18 - await recordPluginActivity({ slug, source: 'auto-launch' });
49 + // Unawaited: no timeout on these, and a stall would hold the queue.
50 + recordPluginActivity({ slug, source: 'auto-launch' });
51 + enableAutoUpdate(p?.plugin);
19 52 return p;
20 53 };
21 54 try {
22 55 return await fn();
@@ -21,28 +54,38 @@
21 54 try {
22 55 return await fn();
23 56 } catch (error) {
24 57 if (error?.code === 'folder_exists') {
25 - console.warn(
26 - `Plugin ${slug} already installed. Attempting to activate...`,
27 - );
28 - // Get the plugin info directly here
58 + // Already on disk — fetch its record so the caller can activate it.
29 59 return await getPlugin(slug);
30 60 }
31 - console.error(`Error installing ${slug}. Retrying...`, error);
32 61 try {
33 62 return await fn();
34 63 } catch (error) {
35 - console.error(`Failed ${slug} again. Giving up`, error);
64 + digest({
65 + error,
66 + details: { source: 'auto-launch', caller: 'installPlugin' },
67 + });
68 + return null;
36 69 }
37 70 }
38 71 };
39 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 +
40 83 export const getPlugin = async (slug) => {
41 84 const response = await apiFetch({
42 85 path: addQueryArgs('/wp/v2/plugins', { search: slug }),
43 86 });
44 - return response?.[0];
87 + return response?.find((p) => p.plugin?.split('/')[0] === slug);
45 88 };
46 89
47 90 export const activatePlugin = async (slug) => {
48 91 const fn = (s) =>
@@ -53,20 +96,93 @@
53 96 });
54 97
55 98 try {
56 99 await fn(slug);
100 + return true;
57 101 } catch (_) {
58 - console.warn(`Error activating ${slug}. Retrying with fresh data...`);
59 - // try once more but get the slug first
60 - const { plugin } = await getPlugin(slug);
61 102 try {
103 + // try once more but get the slug first
104 + const { plugin } = await getPlugin(slug);
62 105 await fn(plugin);
106 + return true;
63 107 } catch (error) {
64 - console.error(`Failed to activate ${slug} again. Giving up`, error);
108 + digest({
109 + error,
110 + details: { source: 'auto-launch', caller: 'activatePlugin' },
111 + });
112 + return false;
65 113 }
66 114 }
67 115 };
68 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 +
69 185 // Currently this only processes patterns with placeholders
70 186 // by swapping out the placeholders with the actual code
71 187 // returns the patterns as blocks with the placeholders replaced
72 188 export const replacePlaceholderPatterns = async (patterns) => {
@@ -101,20 +217,16 @@
101 217 source: 'auto-launch',
102 218 });
103 219 }
104 220
105 - try {
106 - return await processPlaceholders(patterns);
107 - } catch (_e) {
108 - // Try one more time (plugins installed may not be fully loaded)
109 - return await processPlaceholders(patterns)
110 - // If this fails, just return the original patterns
111 - .catch(() => patterns);
112 - }
221 + return await processWithSecondPass(processPlaceholders, patterns);
113 222 };
114 223
115 -export const processPlaceholders = (patterns) =>
116 - apiFetch({
224 +// This endpoint installs pattern dependencies from PHP, outside the queue.
225 +export const processPlaceholders = async (patterns) => {
226 + await waitForInstalls();
227 + return apiFetch({
117 228 path: '/extendify/v1/shared/process-placeholders',
118 229 method: 'POST',
119 230 data: { patterns },
120 231 });
232 +};