Skip to content

Commit aab483a

Browse files
committed
Merge branch 'v109'
# Conflicts: # composer.json
2 parents 8aa7249 + f468ec2 commit aab483a

3 files changed

Lines changed: 225 additions & 4 deletions

File tree

bin/storefront-hot-proxy/change-feedback-watcher.js

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ const {
1515
createLogger,
1616
} = require('./utils');
1717

18-
function createChangeFeedbackWatcher(projectRoot) {
18+
function createChangeFeedbackWatcher(projectRoot, options = {}) {
1919
const DUPLICATE_LOG_WINDOW_MS = 2000;
2020
const TWIG_DEBOUNCE_MS = 90;
21+
const TRANSLATION_DEBOUNCE_MS = 180;
2122
const rootPath = path.resolve(projectRoot);
2223
const storefrontApp = resolveStorefrontApp(rootPath);
2324
const storefrontRequire = createStorefrontRequire(rootPath);
@@ -26,7 +27,12 @@ function createChangeFeedbackWatcher(projectRoot) {
2627
const disableJsCompilation = process.env.SHOPWARE_STOREFRONT_DISABLE_JS === '1';
2728
const jsCompileFeedbackEnabled = process.env.SHOPWARE_STOREFRONT_JS_COMPILE_FEEDBACK !== '0';
2829
const disableTwigWatch = process.env.SHOPWARE_STOREFRONT_DISABLE_TWIG === '1';
30+
const disableTranslationWatch = process.env.SHOPWARE_STOREFRONT_DISABLE_TRANSLATION_WATCH === '1';
31+
const onTranslationChange = typeof options.onTranslationChange === 'function'
32+
? options.onTranslationChange
33+
: null;
2934
const twigLog = createLogger('TWIG');
35+
const translationLog = createLogger('I18N');
3036

3137
let watchpack = null;
3238
const recentlyLogged = new Map();
@@ -36,6 +42,14 @@ function createChangeFeedbackWatcher(projectRoot) {
3642
pendingEventType: '',
3743
pendingFiles: new Set(),
3844
};
45+
const translationState = {
46+
timer: null,
47+
inFlight: false,
48+
queued: false,
49+
waitLogged: false,
50+
pendingEventType: '',
51+
pendingFiles: new Set(),
52+
};
3953

4054
function logFileEvent(fileType, eventType, formattedFile, details = '') {
4155
const eventColor = eventType === 'remove' ? ANSI.yellow : ANSI.green;
@@ -142,6 +156,10 @@ function createChangeFeedbackWatcher(projectRoot) {
142156
return 'twig';
143157
}
144158

159+
if (extension === '.json' && isTranslationJsonFile(filePath)) {
160+
return 'translation';
161+
}
162+
145163
if (['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'].includes(extension)) {
146164
return 'js';
147165
}
@@ -157,6 +175,15 @@ function createChangeFeedbackWatcher(projectRoot) {
157175
return now - previous < DUPLICATE_LOG_WINDOW_MS;
158176
}
159177

178+
function isTranslationJsonFile(filePath) {
179+
const normalizedPath = String(filePath || '').replace(/\\/g, '/').toLowerCase();
180+
if (normalizedPath === '' || !normalizedPath.endsWith('.json')) {
181+
return false;
182+
}
183+
184+
return normalizedPath.includes('/snippet/') || normalizedPath.includes('/snippets/');
185+
}
186+
160187
function rememberTwigPending(eventType, formattedFile) {
161188
if (typeof eventType === 'string' && eventType !== '') {
162189
twigState.pendingEventType = eventType;
@@ -199,6 +226,80 @@ function createChangeFeedbackWatcher(projectRoot) {
199226
}, TWIG_DEBOUNCE_MS);
200227
}
201228

229+
function rememberTranslationPending(eventType, formattedFile) {
230+
if (typeof eventType === 'string' && eventType !== '') {
231+
translationState.pendingEventType = eventType;
232+
}
233+
234+
if (typeof formattedFile === 'string' && formattedFile !== '') {
235+
translationState.pendingFiles.add(formattedFile);
236+
}
237+
}
238+
239+
async function flushTranslationFeedback() {
240+
const pendingFiles = [...translationState.pendingFiles];
241+
const trigger = translationState.pendingEventType || 'change';
242+
const fileSummary = summarizeFiles(pendingFiles);
243+
const reasonLabel = fileSummary ? `${trigger}: ${fileSummary}` : trigger;
244+
245+
if (translationState.inFlight) {
246+
translationState.queued = true;
247+
if (!translationState.waitLogged) {
248+
translationLog.status('WAIT', `change queued while cache flush is running${fileSummary ? ` (${fileSummary})` : ''}`);
249+
translationState.waitLogged = true;
250+
}
251+
return;
252+
}
253+
254+
translationState.pendingEventType = '';
255+
translationState.pendingFiles.clear();
256+
translationState.waitLogged = false;
257+
translationState.inFlight = true;
258+
const startedAt = Date.now();
259+
translationLog.status('RUN', `flushing cache (${reasonLabel})`);
260+
261+
try {
262+
if (onTranslationChange) {
263+
await onTranslationChange({
264+
eventType: trigger,
265+
reasonLabel,
266+
files: pendingFiles,
267+
});
268+
}
269+
270+
translationLog.status('OK', `cache flushed + reload triggered (${reasonLabel}) in ${Date.now() - startedAt}ms`);
271+
} catch (error) {
272+
translationLog.status('ERR', `cache flush failed (${reasonLabel}) after ${Date.now() - startedAt}ms: ${error?.message || error}`, true);
273+
} finally {
274+
translationState.inFlight = false;
275+
276+
if (translationState.queued) {
277+
translationState.queued = false;
278+
setTimeout(() => {
279+
void flushTranslationFeedback();
280+
}, TRANSLATION_DEBOUNCE_MS);
281+
}
282+
}
283+
}
284+
285+
function scheduleTranslationFeedback(eventType, formattedFile) {
286+
rememberTranslationPending(eventType, formattedFile);
287+
288+
if (translationState.timer) {
289+
if (!translationState.waitLogged) {
290+
const queuedFiles = summarizeFiles([...translationState.pendingFiles]);
291+
translationLog.status('WAIT', `change queued while cache flush is running${queuedFiles ? ` (${queuedFiles})` : ''}`);
292+
translationState.waitLogged = true;
293+
}
294+
return;
295+
}
296+
297+
translationState.timer = setTimeout(() => {
298+
translationState.timer = null;
299+
void flushTranslationFeedback();
300+
}, TRANSLATION_DEBOUNCE_MS);
301+
}
302+
202303
function handleFileEvent(eventType, absoluteFilePath) {
203304
const fileType = classifyFile(absoluteFilePath);
204305
if (!fileType) {
@@ -244,6 +345,24 @@ function createChangeFeedbackWatcher(projectRoot) {
244345
}
245346

246347
scheduleTwigReloadFeedback(eventType, formattedFile);
348+
return;
349+
}
350+
351+
if (fileType === 'translation') {
352+
if (disableTranslationWatch) {
353+
if (shouldSkipDuplicate(eventType, formattedFile)) {
354+
return;
355+
}
356+
357+
logFileEvent('i18n', eventType, formattedFile, '(skipped: translation watch disabled)');
358+
return;
359+
}
360+
361+
if (shouldSkipDuplicate(eventType, formattedFile)) {
362+
return;
363+
}
364+
365+
scheduleTranslationFeedback(eventType, formattedFile);
247366
}
248367
}
249368

@@ -279,6 +398,11 @@ function createChangeFeedbackWatcher(projectRoot) {
279398
twigState.timer = null;
280399
}
281400

401+
if (translationState.timer) {
402+
clearTimeout(translationState.timer);
403+
translationState.timer = null;
404+
}
405+
282406
if (watchpack) {
283407
watchpack.close();
284408
watchpack = null;

bin/storefront-hot-proxy/start-hot-reload.js

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ const assetPort = Number(process.env.STOREFRONT_ASSETS_PORT) || 9999;
2828
const shouldOpenBrowser = process.env.SHOPWARE_STOREFRONT_OPEN_BROWSER !== '0';
2929
const scssEngine = String(process.env.SHOPWARE_STOREFRONT_SCSS_ENGINE || 'webpack').toLowerCase();
3030
const disableScss = process.env.SHOPWARE_STOREFRONT_DISABLE_SCSS === '1';
31+
const translationCacheFlushEnabled = process.env.SHOPWARE_STOREFRONT_TRANSLATION_CACHE_FLUSH !== '0';
32+
const translationCacheCommandParts = parseCommandParts(process.env.SHOPWARE_STOREFRONT_TRANSLATION_CACHE_COMMAND || 'cache:flush:all');
33+
const translationCacheFallbackCommandParts = parseCommandParts(process.env.SHOPWARE_STOREFRONT_TRANSLATION_CACHE_FALLBACK_COMMAND || 'cache:clear:all');
3134
const noOp = () => {};
35+
let liveReloadServerInstance = null;
36+
const pendingReloadReasons = [];
3237

3338
const themeFilesConfigPath = path.resolve(projectRootPath, 'var/theme-files.json');
3439
let themeFiles = {};
@@ -105,7 +110,15 @@ if (!disableScss && scssEngine === 'sass-cli') {
105110
scssSidecar = createScssSidecar(projectRootPath);
106111
}
107112

108-
const changeFeedbackWatcher = createChangeFeedbackWatcher(projectRootPath);
113+
const changeFeedbackWatcher = createChangeFeedbackWatcher(projectRootPath, {
114+
onTranslationChange: async ({ reasonLabel }) => {
115+
if (translationCacheFlushEnabled) {
116+
await runShopwareCacheFlush();
117+
}
118+
119+
requestLiveReload(reasonLabel || 'translation-json');
120+
},
121+
});
109122

110123
function onProxyReq(proxyReq, req) {
111124
const requestUrl = req.url || '';
@@ -240,7 +253,17 @@ if (scssSidecar) {
240253

241254
changeFeedbackWatcher.start();
242255

243-
server.then(() => {
256+
server.then((liveReloadServer) => {
257+
liveReloadServerInstance = liveReloadServer;
258+
259+
if (pendingReloadReasons.length > 0) {
260+
const pending = [...pendingReloadReasons];
261+
pendingReloadReasons.length = 0;
262+
for (const reason of pending) {
263+
requestLiveReload(reason);
264+
}
265+
}
266+
244267
if (proxyUrlEnv.protocol === 'https:' && skipSslCerts === false) {
245268
try {
246269
const httpsServer = nodeServerHttps.createServer(sslOptions, proxy);
@@ -313,6 +336,80 @@ function openBrowserWithUrl(url) {
313336
child.on('error', error => console.log('Unable to open browser! Details:', error));
314337
}
315338

339+
function parseCommandParts(commandString) {
340+
const normalized = String(commandString || '').trim();
341+
if (normalized === '') {
342+
return ['cache:clear'];
343+
}
344+
345+
return normalized.split(/\s+/).filter((part) => part !== '');
346+
}
347+
348+
function runShopwareCacheFlush() {
349+
return runShopwareConsoleCommand(translationCacheCommandParts).catch((error) => {
350+
if (translationCacheFallbackCommandParts.join(' ') === translationCacheCommandParts.join(' ')) {
351+
throw error;
352+
}
353+
354+
return runShopwareConsoleCommand(translationCacheFallbackCommandParts);
355+
});
356+
}
357+
358+
function runShopwareConsoleCommand(commandParts) {
359+
return new Promise((resolve, reject) => {
360+
const commandArgs = [...commandParts];
361+
if (!commandArgs.includes('--no-interaction') && !commandArgs.includes('-n')) {
362+
commandArgs.push('--no-interaction');
363+
}
364+
365+
const binConsole = path.resolve(projectRootPath, 'bin/console');
366+
const child = spawn('php', [binConsole, ...commandArgs], {
367+
cwd: projectRootPath,
368+
env: process.env,
369+
stdio: ['ignore', 'pipe', 'pipe'],
370+
});
371+
372+
let output = '';
373+
child.stdout.on('data', (chunk) => {
374+
output += chunk.toString();
375+
});
376+
child.stderr.on('data', (chunk) => {
377+
output += chunk.toString();
378+
});
379+
380+
child.on('error', (error) => {
381+
reject(error);
382+
});
383+
child.on('close', (code) => {
384+
if (code === 0) {
385+
resolve();
386+
return;
387+
}
388+
389+
const message = output.replace(/\s+/g, ' ').trim().slice(0, 320);
390+
reject(new Error(message || `cache flush command failed with exit code ${code}`));
391+
});
392+
});
393+
}
394+
395+
function requestLiveReload(reason) {
396+
const reloadReason = typeof reason === 'string' && reason !== '' ? reason : 'translation-json';
397+
const serverInstance = liveReloadServerInstance;
398+
399+
if (!serverInstance || !serverInstance.webSocketServer || !serverInstance.sendMessage) {
400+
pendingReloadReasons.push(reloadReason);
401+
return false;
402+
}
403+
404+
serverInstance.sendMessage(
405+
serverInstance.webSocketServer.clients,
406+
'static-changed',
407+
reloadReason,
408+
);
409+
410+
return true;
411+
}
412+
316413
function isLineItemRequest(requestUrl) {
317414
return (requestUrl || '').includes('/checkout/line-item/');
318415
}

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "Sidworks Developer tools for Shopware 6",
44
"type": "shopware-platform-plugin",
55
"license": "MIT",
6-
"version": "1.0.9",
6+
"version": "1.0.10",
77
"authors": [
88
{
99
"name": "Sidworks"

0 commit comments

Comments
 (0)