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