Skip to content

Commit a4752da

Browse files
Merge pull request #1 from sidworks-dev/file-watcher
Release 1.0.5
2 parents 0950a21 + a1dc3ba commit a4752da

14 files changed

Lines changed: 2763 additions & 3 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
bun.lock

README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Sidworks DevTools for Shopware 6
22

3-
Never hunt for a Twig file again. Sidworks DevTools reveals the exact template and block behind every Shopware 6 element and lets you jump straight into your IDE with a single click.
3+
Never hunt for a Twig file again. Sidworks DevTools reveals the exact template and block behind every Shopware 6 element and lets you jump straight into your IDE with a single click. It also includes a optimised storefront watcher.
44

55
![Screenshot](./docs/sw-devtools.png)
66

@@ -20,11 +20,19 @@ Never hunt for a Twig file again. Sidworks DevTools reveals the exact template a
2020
- **Intelligent Line Search**: Automatically finds the precise element line by searching for classes, IDs, and tags
2121
- **Multi-Editor Support**: Works with PHPStorm and VSCode
2222

23+
### Optimized Storefront Watcher
24+
- **One command start**: `bin/console sidworks:watch-storefront`
25+
- **Theme picker by default**: Choose theme + domain directly in the terminal
26+
- **Fast feedback**: Live SCSS updates with readable JS/Twig/SCSS logs
27+
- **Simple toggles**: `--no-js`, `--no-twig`, `--no-scss`
28+
2329
## Requirements
2430

2531
- Shopware 6.6.x or 6.7.x
2632
- PHP 8.1 or higher
2733
- Chrome or Edge browser (for extension)
34+
- Node.js 20+ (required for storefront watcher)
35+
- [Bun](https://bun.sh) optional (set `SHOPWARE_STOREFRONT_WATCH_PM=bun` to use it)
2836

2937
## Installation
3038

@@ -87,7 +95,38 @@ The plugin will automatically inject this path into the page, so you don't need
8795
8896
## Usage
8997
90-
### Basic Workflow
98+
### Storefront Watcher (`sidworks:watch-storefront`)
99+
100+
Run from your project root:
101+
102+
```bash
103+
bin/console sidworks:watch-storefront
104+
```
105+
106+
What this gives you:
107+
- Interactive theme/domain selection by default
108+
- Fast defaults for day-to-day storefront work
109+
- Clear terminal output with `[SCSS]`, `[TWIG]`, and `[JS]` log tags
110+
111+
Common toggles:
112+
113+
```bash
114+
bin/console sidworks:watch-storefront --no-js
115+
bin/console sidworks:watch-storefront --no-twig
116+
bin/console sidworks:watch-storefront --no-scss
117+
```
118+
119+
Theme selection shortcuts:
120+
121+
```bash
122+
bin/console sidworks:watch-storefront --theme-name=QsoTheme
123+
bin/console sidworks:watch-storefront --theme-id=018e94f67ba2719da036725041793f30 --domain-url=https://carclean.ddev.site/nl
124+
bin/console sidworks:watch-storefront --pick-theme
125+
```
126+
127+
`--domain-url` requires `--theme-id` (or use `--pick-theme` for interactive theme + domain selection).
128+
129+
### Template Inspector — Basic Workflow
91130

92131
1. **Enable debug mode** in Shopware (`.env`: `APP_ENV=dev`)
93132
2. **Install both** the plugin and [Chrome extension](https://github.com/sidworks-dev/sw-plugin-devtools-chrome-extension)
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/* eslint no-console: 0 */
2+
3+
const fs = require('node:fs');
4+
const path = require('node:path');
5+
6+
const {
7+
resolveStorefrontApp,
8+
createStorefrontRequire,
9+
} = require('./runtime-paths');
10+
11+
const ANSI = {
12+
reset: '\x1b[0m',
13+
gray: '\x1b[90m',
14+
green: '\x1b[32m',
15+
yellow: '\x1b[33m',
16+
cyan: '\x1b[36m',
17+
magenta: '\x1b[35m',
18+
};
19+
20+
function createChangeFeedbackWatcher(projectRoot) {
21+
const DUPLICATE_LOG_WINDOW_MS = 2000;
22+
const rootPath = path.resolve(projectRoot);
23+
const storefrontApp = resolveStorefrontApp(rootPath);
24+
const storefrontRequire = createStorefrontRequire(rootPath);
25+
const Watchpack = storefrontRequire('watchpack');
26+
const coreOnlyHotMode = process.env.SHOPWARE_STOREFRONT_HOT_CORE_ONLY === '1';
27+
const disableJsCompilation = process.env.SHOPWARE_STOREFRONT_DISABLE_JS === '1';
28+
const disableTwigWatch = process.env.SHOPWARE_STOREFRONT_DISABLE_TWIG === '1';
29+
const coreStorefrontJsRoot = path.resolve(storefrontApp, 'src');
30+
31+
let watchpack = null;
32+
const recentlyLogged = new Map();
33+
34+
function hasInteractiveTty() {
35+
return Boolean(process.stdout && process.stdout.isTTY);
36+
}
37+
38+
function colorize(text, colorCode) {
39+
if (!hasInteractiveTty()) {
40+
return text;
41+
}
42+
43+
return `${colorCode}${text}${ANSI.reset}`;
44+
}
45+
46+
function logFileEvent(fileType, eventType, formattedFile, details = '') {
47+
const typeColor = fileType === 'twig' ? ANSI.magenta : ANSI.cyan;
48+
const eventColor = eventType === 'remove' ? ANSI.yellow : ANSI.green;
49+
const typeTag = colorize(`[${fileType.toUpperCase()}]`, typeColor);
50+
const eventTag = colorize(`[${eventType.toUpperCase()}]`, eventColor);
51+
const suffix = details ? ` ${colorize(details, ANSI.gray)}` : '';
52+
53+
console.log(`[SidworksDevTools] ${typeTag} ${eventTag} ${formattedFile}${suffix}`);
54+
}
55+
56+
function isExistingDirectory(directoryPath) {
57+
return fs.existsSync(directoryPath) && fs.statSync(directoryPath).isDirectory();
58+
}
59+
60+
function isPathInside(childPath, parentPath) {
61+
const normalizedChild = path.resolve(childPath);
62+
const normalizedParent = path.resolve(parentPath);
63+
64+
return normalizedChild === normalizedParent || normalizedChild.startsWith(normalizedParent + path.sep);
65+
}
66+
67+
function readPluginsConfig() {
68+
const pluginsConfigPath = path.resolve(rootPath, 'var/plugins.json');
69+
if (!fs.existsSync(pluginsConfigPath)) {
70+
return [];
71+
}
72+
73+
try {
74+
const parsed = JSON.parse(fs.readFileSync(pluginsConfigPath, 'utf8'));
75+
return typeof parsed === 'object' && parsed !== null
76+
? Object.values(parsed)
77+
: [];
78+
} catch (_error) {
79+
return [];
80+
}
81+
}
82+
83+
function resolvePluginBasePath(pluginConfig) {
84+
const basePath = typeof pluginConfig?.basePath === 'string' ? pluginConfig.basePath : '';
85+
if (basePath === '') {
86+
return rootPath;
87+
}
88+
89+
return path.isAbsolute(basePath)
90+
? basePath
91+
: path.resolve(rootPath, basePath);
92+
}
93+
94+
function collectWatchDirectories() {
95+
const directories = new Set();
96+
const storefrontViewsRoot = path.resolve(storefrontApp, '..', '..', 'views');
97+
98+
const baseDirectories = [
99+
path.resolve(storefrontApp, 'src'),
100+
path.resolve(rootPath, 'src/Resources/views'),
101+
path.resolve(rootPath, 'templates'),
102+
storefrontViewsRoot,
103+
path.resolve(rootPath, 'custom/plugins'),
104+
path.resolve(rootPath, 'custom/apps'),
105+
].filter(isExistingDirectory);
106+
107+
baseDirectories.forEach((directoryPath) => directories.add(directoryPath));
108+
109+
const pluginConfigs = readPluginsConfig();
110+
for (const pluginConfig of pluginConfigs) {
111+
const pluginBasePath = resolvePluginBasePath(pluginConfig);
112+
113+
const viewDirectories = Array.isArray(pluginConfig?.views) ? pluginConfig.views : [];
114+
for (const viewDirectory of viewDirectories) {
115+
if (typeof viewDirectory !== 'string' || viewDirectory === '') {
116+
continue;
117+
}
118+
119+
const resolvedViewDirectory = path.resolve(pluginBasePath, viewDirectory);
120+
if (
121+
isExistingDirectory(resolvedViewDirectory) &&
122+
![...directories].some((directoryPath) => isPathInside(resolvedViewDirectory, directoryPath))
123+
) {
124+
directories.add(resolvedViewDirectory);
125+
}
126+
}
127+
128+
const storefrontPath = typeof pluginConfig?.storefront?.path === 'string'
129+
? pluginConfig.storefront.path
130+
: '';
131+
if (storefrontPath !== '') {
132+
const resolvedStorefrontPath = path.resolve(pluginBasePath, storefrontPath);
133+
if (
134+
isExistingDirectory(resolvedStorefrontPath) &&
135+
![...directories].some((directoryPath) => isPathInside(resolvedStorefrontPath, directoryPath))
136+
) {
137+
directories.add(resolvedStorefrontPath);
138+
}
139+
}
140+
141+
const entryFilePath = typeof pluginConfig?.storefront?.entryFilePath === 'string'
142+
? pluginConfig.storefront.entryFilePath
143+
: '';
144+
if (entryFilePath !== '') {
145+
const resolvedEntryDirectory = path.dirname(path.resolve(pluginBasePath, entryFilePath));
146+
if (
147+
isExistingDirectory(resolvedEntryDirectory) &&
148+
![...directories].some((directoryPath) => isPathInside(resolvedEntryDirectory, directoryPath))
149+
) {
150+
directories.add(resolvedEntryDirectory);
151+
}
152+
}
153+
}
154+
155+
return [...directories];
156+
}
157+
158+
function formatFilePath(absoluteFilePath) {
159+
if (typeof absoluteFilePath !== 'string' || absoluteFilePath === '') {
160+
return '';
161+
}
162+
163+
const normalizedRoot = path.resolve(rootPath);
164+
const normalizedFile = path.resolve(absoluteFilePath);
165+
if (normalizedFile.startsWith(normalizedRoot + path.sep)) {
166+
return path.relative(normalizedRoot, normalizedFile).replace(/\\/g, '/');
167+
}
168+
169+
return absoluteFilePath.replace(/\\/g, '/');
170+
}
171+
172+
function classifyFile(filePath) {
173+
const extension = path.extname(filePath).toLowerCase();
174+
if (extension === '.twig') {
175+
return 'twig';
176+
}
177+
178+
if (['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'].includes(extension)) {
179+
return 'js';
180+
}
181+
182+
return '';
183+
}
184+
185+
function shouldSkipDuplicate(eventType, formattedFile) {
186+
const dedupeKey = `${eventType}:${formattedFile}`;
187+
const now = Date.now();
188+
const previous = recentlyLogged.get(dedupeKey) || 0;
189+
recentlyLogged.set(dedupeKey, now);
190+
return now - previous < DUPLICATE_LOG_WINDOW_MS;
191+
}
192+
193+
function isCoreStorefrontJsFile(filePath) {
194+
const normalizedFile = path.resolve(filePath);
195+
return normalizedFile.startsWith(coreStorefrontJsRoot + path.sep);
196+
}
197+
198+
function handleFileEvent(eventType, absoluteFilePath) {
199+
const fileType = classifyFile(absoluteFilePath);
200+
if (!fileType) {
201+
return;
202+
}
203+
204+
const formattedFile = formatFilePath(absoluteFilePath);
205+
if (!formattedFile || shouldSkipDuplicate(eventType, formattedFile)) {
206+
return;
207+
}
208+
209+
if (fileType === 'js') {
210+
if (isCoreStorefrontJsFile(absoluteFilePath) && !disableJsCompilation) {
211+
// Core storefront JS is already logged via webpack compiler hooks.
212+
return;
213+
}
214+
215+
if (disableJsCompilation) {
216+
logFileEvent('js', eventType, formattedFile, '(skipped: --no-js)');
217+
return;
218+
}
219+
220+
if (coreOnlyHotMode) {
221+
logFileEvent('js', eventType, formattedFile, '(skipped: core-only-hot mode)');
222+
return;
223+
}
224+
225+
logFileEvent('js', eventType, formattedFile);
226+
return;
227+
}
228+
229+
if (fileType === 'twig') {
230+
if (disableTwigWatch) {
231+
logFileEvent('twig', eventType, formattedFile, '(skipped: --no-twig)');
232+
return;
233+
}
234+
235+
logFileEvent('twig', eventType, formattedFile, '(live reload)');
236+
}
237+
}
238+
239+
function start() {
240+
if (watchpack) {
241+
return true;
242+
}
243+
244+
const directoriesToWatch = collectWatchDirectories();
245+
if (directoriesToWatch.length === 0) {
246+
return false;
247+
}
248+
249+
watchpack = new Watchpack({
250+
aggregateTimeout: 80,
251+
ignored: [
252+
'**/.git/**',
253+
'**/node_modules/**',
254+
'**/var/cache/**',
255+
'**/var/.sidworks-hot/**',
256+
],
257+
});
258+
259+
watchpack.on('change', (filePath) => handleFileEvent('change', filePath));
260+
watchpack.on('remove', (filePath) => handleFileEvent('remove', filePath));
261+
watchpack.watch([], directoriesToWatch, Date.now() - 1000);
262+
return true;
263+
}
264+
265+
function close() {
266+
if (watchpack) {
267+
watchpack.close();
268+
watchpack = null;
269+
}
270+
}
271+
272+
return {
273+
start,
274+
close,
275+
};
276+
}
277+
278+
module.exports = {
279+
createChangeFeedbackWatcher,
280+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// No-op JS entry used when --no-js is enabled.
2+
module.exports = {};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// SCSS sidecar mode: webpack no longer compiles theme-entry.scss.
2+
module.exports = {};

0 commit comments

Comments
 (0)