Skip to content

Commit

Permalink
Add HMR support for linked & local npm packages
Browse files Browse the repository at this point in the history
  • Loading branch information
drwpow committed Jun 10, 2020
1 parent 18a213b commit 5120b88
Show file tree
Hide file tree
Showing 15 changed files with 67 additions and 29 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ www/index.md
www/dist
/node_modules
/web_modules
test/build/*/build
test/integration/*/web_modules
test/integration/*/node_modules/.cache
.DS_Store
Expand Down
86 changes: 62 additions & 24 deletions src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ function getEncodingType(ext: string): 'utf-8' | 'binary' {
}
}

/** Find disk location from specifier string */
function getPackageName(specifier: string): string {
let [packageName, ...deepPackagePathParts] = specifier.split('/');
if (packageName.startsWith('@')) {
packageName += '/' + deepPackagePathParts.shift();
}
return packageName;
}

const sendFile = (
req: http.IncomingMessage,
res: http.ServerResponse,
Expand Down Expand Up @@ -214,6 +223,28 @@ export async function command(commandOptions: CommandOptions) {
// no import-map found, safe to ignore
}

/** Rerun `snowpack install` while dev server is running */
async function reinstallDependencies() {
if (!currentlyRunningCommand) {
isLiveReloadPaused = true;
messageBus.emit('INSTALLING');
currentlyRunningCommand = installCommand(commandOptions);
currentlyRunningCommand.then(async () => {
dependencyImportMap = JSON.parse(
await fs
.readFile(dependencyImportMapLoc, {encoding: 'utf-8'})
.catch(() => `{"imports": {}}`),
);
await updateLockfileHash(DEV_DEPENDENCIES_DIR);
await cacache.rm.all(BUILD_CACHE);
inMemoryBuildCache.clear();
messageBus.emit('INSTALL_COMPLETE');
isLiveReloadPaused = false;
currentlyRunningCommand = null;
});
}
}

async function buildFile(
fileContents: string,
fileLoc: string,
Expand Down Expand Up @@ -297,33 +328,15 @@ export async function command(commandOptions: CommandOptions) {
}
return resolvedImport;
}
let [missingPackageName, ...deepPackagePathParts] = spec.split('/');
if (missingPackageName.startsWith('@')) {
missingPackageName += '/' + deepPackagePathParts.shift();
}
const [depManifestLoc] = resolveDependencyManifest(missingPackageName, cwd);
const packageName = getPackageName(spec);
const [depManifestLoc] = resolveDependencyManifest(packageName, cwd);
const doesPackageExist = !!depManifestLoc;
if (doesPackageExist && !currentlyRunningCommand) {
isLiveReloadPaused = true;
messageBus.emit('INSTALLING');
currentlyRunningCommand = installCommand(commandOptions);
currentlyRunningCommand.then(async () => {
dependencyImportMap = JSON.parse(
await fs
.readFile(dependencyImportMapLoc, {encoding: 'utf-8'})
.catch(() => `{"imports": {}}`),
);
await updateLockfileHash(DEV_DEPENDENCIES_DIR);
await cacache.rm.all(BUILD_CACHE);
inMemoryBuildCache.clear();
messageBus.emit('INSTALL_COMPLETE');
isLiveReloadPaused = false;
currentlyRunningCommand = null;
});
} else if (!doesPackageExist) {
if (doesPackageExist) {
reinstallDependencies();
} else {
missingWebModule = {
spec: spec,
pkgName: missingPackageName,
pkgName: packageName,
};
}
const extName = path.extname(spec);
Expand Down Expand Up @@ -860,6 +873,8 @@ export async function command(commandOptions: CommandOptions) {
updateOrBubble(updateUrl, new Set());
}
}

// Watch src files
async function onWatchEvent(fileLoc) {
handleHmrUpdate(fileLoc);
inMemoryBuildCache.delete(fileLoc);
Expand All @@ -880,6 +895,29 @@ export async function command(commandOptions: CommandOptions) {
watcher.on('change', (fileLoc) => onWatchEvent(fileLoc));
watcher.on('unlink', (fileLoc) => onWatchEvent(fileLoc));

// Watch node_modules & rerun snowpack install if symlinked dep updates
const symlinkedFileLocs = new Set(
Object.keys(dependencyImportMap.imports)
.map((specifier) => {
const packageName = getPackageName(specifier);
return resolveDependencyManifest(packageName, cwd);
}) // resolve symlink src location
.filter(([_, packageManifest]) => packageManifest && !packageManifest['_id']) // only watch symlinked deps for now
.map(([fileLoc]) => `${path.dirname(fileLoc!)}/**`),
);
function onDepWatchEvent() {
reinstallDependencies().then(() => hmrEngine.broadcastMessage({type: 'reload'}));
}
const depWatcher = chokidar.watch([...symlinkedFileLocs], {
cwd: '/', // we’re using absolute paths, so watch from root
persistent: true,
ignoreInitial: true,
disableGlobbing: false,
});
depWatcher.on('add', onDepWatchEvent);
depWatcher.on('change', onDepWatchEvent);
depWatcher.on('unlink', onDepWatchEvent);

onProcessExit(() => {
hmrEngine.disconnectAllClients();
});
Expand Down
4 changes: 2 additions & 2 deletions src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ function resolveWebDependency(dep: string, isExplicit: boolean): DependencyLoc {
if (!depManifest) {
throw new ErrorWithHint(
`Package "${dep}" not found. Have you installed it?`,
depManifestLoc && chalk.italic(depManifestLoc),
depManifestLoc ? chalk.italic(depManifestLoc) : '',
);
}
if (
Expand Down Expand Up @@ -198,7 +198,7 @@ function resolveWebDependency(dep: string, isExplicit: boolean): DependencyLoc {
}
return {
type: 'JS',
loc: path.join(depManifestLoc, '..', foundEntrypoint),
loc: path.join(depManifestLoc || '', '..', foundEntrypoint),
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function isTruthy<T>(item: T | false | null | undefined): item is T {
* NOTE: You used to be able to require() a package.json file directly,
* but now with export map support in Node v13 that's no longer possible.
*/
export function resolveDependencyManifest(dep: string, cwd: string) {
export function resolveDependencyManifest(dep: string, cwd: string): [string | null, any | null] {
// Attempt #1: Resolve the dependency manifest normally. This works for most
// packages, but fails when the package defines an export map that doesn't
// include a package.json. If we detect that to be the reason for failure,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const path = require('path');
const execa = require('execa');

it('buildOptions.metaDir', () => {
execa('node', ['npm', 'run', 'TEST']);
execa.commandSync('npm run TEST', {cwd: __dirname});
// expect dir in package.json to exist
expect(fs.existsSync(path.resolve(__dirname, 'build', 'static', 'snowpack')));
});
File renamed without changes.
File renamed without changes.
1 change: 0 additions & 1 deletion test/build/metadata-dir/.gitignore

This file was deleted.

0 comments on commit 5120b88

Please sign in to comment.