diff --git a/CHANGELOG.md b/CHANGELOG.md index 79f9547..aeada58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- Watch local dependency source files in dev/watch mode — changes now trigger webpack rebuilds ([#70](https://github.com/studiometa/playground/pull/70), [0c08ef0](https://github.com/studiometa/playground/commit/0c08ef0)) + ## v0.3.7 - 2026.04.01 ### Added diff --git a/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.test.ts b/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.test.ts index 65226fe..d4f74f7 100644 --- a/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.test.ts +++ b/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.test.ts @@ -100,6 +100,7 @@ describe('PlaygroundDependenciesPlugin', () => { // Trigger the compilation hook with a minimal fake compilation compilationCallback?.({ + fileDependencies: new Set(), hooks: { processAssets: { tapAsync() {} }, }, @@ -203,6 +204,7 @@ describe('PlaygroundDependenciesPlugin', () => { // Should not throw compilationCallback?.({ + fileDependencies: new Set(), hooks: { processAssets: { tapAsync() {} }, }, @@ -211,4 +213,78 @@ describe('PlaygroundDependenciesPlugin', () => { expect(p.importMap).toBeUndefined(); }); }); + + describe('file watching for local sources', () => { + function applyAndGetFileDependencies( + deps: ResolvedDependency[], + configDir: string, + ): Set { + const p = new PlaygroundDependenciesPlugin(deps, configDir); + let compilationCallback: ((compilation: unknown) => void) | undefined; + const fakeCompiler = { + options: { output: { publicPath: 'auto' } }, + webpack: { Compilation: { PROCESS_ASSETS_STAGE_ADDITIONAL: 0 } }, + hooks: { + thisCompilation: { + tap(_name: string, cb: (compilation: unknown) => void) { + compilationCallback = cb; + }, + }, + }, + }; + + p.apply(fakeCompiler as any); + + const fileDependencies = new Set(); + compilationCallback?.({ + fileDependencies, + hooks: { + processAssets: { tapAsync() {} }, + }, + }); + + return fileDependencies; + } + + it('adds local source files to compilation fileDependencies', () => { + const deps: ResolvedDependency[] = [ + { + specifier: 'my-lib', + importMapValue: '/static/deps/my-lib/index.js', + type: 'bundle', + source: './src/index.ts', + }, + ]; + + const fileDeps = applyAndGetFileDependencies(deps, '/project'); + expect(fileDeps.size).toBe(1); + expect([...fileDeps][0]).toContain('src/index.ts'); + }); + + it('does not add esm-sh dependencies to fileDependencies', () => { + const deps: ResolvedDependency[] = [ + { + specifier: 'deepmerge', + importMapValue: 'https://esm.sh/deepmerge', + type: 'esm-sh', + }, + ]; + + const fileDeps = applyAndGetFileDependencies(deps, '/project'); + expect(fileDeps.size).toBe(0); + }); + + it('does not add bundle deps without source to fileDependencies', () => { + const deps: ResolvedDependency[] = [ + { + specifier: 'my-lib', + importMapValue: '/static/deps/my-lib/index.js', + type: 'bundle', + }, + ]; + + const fileDeps = applyAndGetFileDependencies(deps, '/project'); + expect(fileDeps.size).toBe(0); + }); + }); }); diff --git a/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.ts b/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.ts index 7283cbc..20d15f3 100644 --- a/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.ts +++ b/packages/playground/src/lib/plugins/PlaygroundDependenciesPlugin.ts @@ -61,6 +61,18 @@ export class PlaygroundDependenciesPlugin { } } + // Watch local source files so webpack rebuilds when they change + for (const dep of this.dependencies) { + if (dep.type === 'bundle' && dep.source && this.isLocalSource(dep.source)) { + const resolvedPattern = resolve(this.configDir, dep.source); + const isGlob = dep.source.includes('*'); + const sourceFiles = isGlob ? glob.globSync(resolvedPattern) : [resolvedPattern]; + for (const file of sourceFiles) { + compilation.fileDependencies.add(file); + } + } + } + compilation.hooks.processAssets.tapAsync( { name: pluginName,