Similar to this question, referencing Yarn workspaces, I have the following npm workspace structure:
package.json // root
packages
@myscope/a
package.json
tsconfig.json
@myscope/b
package.json
tsconfig.json
@myscope/c
package.json
tsconfig.json
These packages are referenced in the root package JSON as follows
{
"workspaces": [
"packages/*",
],
}
@myscope/c
depends on @myscope/b
, and @myscope/b
depends on @myscope/a
.
Each package has its own build command and its own tsconfig.json
:
{
"build": "tsc --build --verbose tsconfig.json",
}
The tsc
command builds types as well as the JS, which are critical for the imports to work when developing with local packages. I could switch these commands for a vite.config.ts
that builds the JS->TS but types are not emitted. I know I can use vite-plugin-dts for this purpose.
If I'm running @myscope/c
with the following command from within the @myscope/c
folder:
npx vite serve --mode=development --config vite.config.ts
I've created an example repository demonstrating the layout. Inside it, running @myscope/c
with vite will not rebuild @myscope/b
or @myscope/a
when they change despite having included preserveSymlinks
in my vite.config.ts.
...
resolve: {
preserveSymlinks: true // this is the fix from yargs question
}
})
How do I get my @myscope/c
vite command to rebuild @myscope/a
and @myscope/b
when they change?
Do I need to implement a vite.config.ts for @myscope/a
and @myscope/b
?
The answer for yarn workspaces, preserveSymlinks
, doesn't work for npm workspaces.
I had to write a Vite plugin to watch my local npm workspace packages and then handle updates to those files.
The gist of the plugin is:
export const VitePluginWatchWorkspace = async (config: VitePluginWatchExternalOptions): Promise<Plugin<any>> => {
// get a list of external files you want to watch
const externalFiles = await getExternalFileLists()
return {
name: 'vite-plugin-watch-workspace',
// on build start, add the external files to Vite's watch list
async buildStart() {
Object.keys(externalFiles).map((file) => {
this.addWatchFile(file)
})
},
// when the external files change, rebuild them with esbuild
async handleHotUpdate({ file, server }) {
log(`File', ${file}`)
const tsconfigPath = externalFiles[file]
if (!tsconfigPath) {
log(`tsconfigPath not found for file ${file}`)
return
}
const tsconfig = getTsConfigFollowExtends(tsconfigPath)
const fileExtension = path.extname(file)
const loader = getLoader(fileExtension)
const outdir = getOutDir(file, tsconfig)
const outfile = getOutFile(outdir, file, fileExtension)
log(`Outfile ${outfile}, loader ${loader}`)
const buildResult = await build({
tsconfig: tsconfigPath,
stdin: {
contents: fs.readFileSync(file, 'utf8'),
loader,
resolveDir: path.dirname(file),
},
outfile,
platform: config.format === 'cjs' ? 'node' : 'neutral',
format: config.format || 'esm',
})
log(`buildResult', ${JSON.stringify(buildResult)}`)
// tell the server that the file has updated
server.ws.send({
type: 'update',
updates: [
{
acceptedPath: file,
type: 'js-update',
path: file,
timestamp: Date.now(),
},
],
})
},
}
}
You can download the plugin here and view the source code here.