Skip to content

Commit

Permalink
Add HMR support for linked & local npm packages (#470)
Browse files Browse the repository at this point in the history
  • Loading branch information
drwpow committed Jun 11, 2020
1 parent 4fb022c commit 3280de3
Show file tree
Hide file tree
Showing 27 changed files with 170 additions and 32 deletions.
1 change: 1 addition & 0 deletions .gitignore
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
Expand Up @@ -97,6 +97,15 @@ function getEncodingType(ext: string): 'utf-8' | 'binary' {
}
}

/** Find disk location from specifier string */
function getPackageNameFromSpecifier(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);
await 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 = getPackageNameFromSpecifier(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 = getPackageNameFromSpecifier(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
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
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
1 change: 1 addition & 0 deletions test/build/linked-pkg/.gitignore
@@ -0,0 +1 @@
node_modules
15 changes: 15 additions & 0 deletions test/build/linked-pkg/index.test.js
@@ -0,0 +1,15 @@
const fs = require('fs');
const path = require('path');
const execa = require('execa');

it('linked package install', () => {
// setup
const cwd = __dirname;
execa.sync('npm', ['link'], {cwd: path.resolve(__dirname, 'packages', 'test-link')});
execa.sync('npm', ['link', '@snowpack/test-link'], {cwd});
execa.sync('npm', ['install'], {cwd});

// test output (the assumption is if anything went wrong, these files wouldn’t be built)
execa.sync('npm', ['run', 'TEST'], {cwd});
expect(fs.existsSync(path.resolve(__dirname, 'build', '_dist_', 'index.js'))).toBe(true);
});
12 changes: 12 additions & 0 deletions test/build/linked-pkg/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions test/build/linked-pkg/package.json
@@ -0,0 +1,16 @@
{
"scripts": {
"start": "node ../../../pkg/dist-node/index.bin.js dev",
"TEST": "node ../../../pkg/dist-node/index.bin.js build"
},
"snowpack": {
"scripts": {
"mount:public": "mount public --to /",
"mount:src": "mount src --to /_dist_"
}
},
"dependencies": {
"@snowpack/test-link": "*",
"@snowpack/test-local": "file:./packages/test-local"
}
}
3 changes: 3 additions & 0 deletions test/build/linked-pkg/packages/test-link/deep/index.js
@@ -0,0 +1,3 @@
export default function test() {
console.log('deeplink-change');
}
3 changes: 3 additions & 0 deletions test/build/linked-pkg/packages/test-link/index.js
@@ -0,0 +1,3 @@
export default function test() {
console.log('link');
}
5 changes: 5 additions & 0 deletions test/build/linked-pkg/packages/test-link/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions test/build/linked-pkg/packages/test-link/package.json
@@ -0,0 +1,6 @@
{
"name": "@snowpack/test-link",
"main": "index.js",
"module": "index.js",
"version": "0.0.1-alpha.0"
}
3 changes: 3 additions & 0 deletions test/build/linked-pkg/packages/test-local/index.js
@@ -0,0 +1,3 @@
export default function test() {
console.log('local');
}
6 changes: 6 additions & 0 deletions test/build/linked-pkg/packages/test-local/package.json
@@ -0,0 +1,6 @@
{
"name": "@snowpack/test-local",
"main": "index.js",
"module": "index.js",
"version": "0.0.1-alpha.0"
}
23 changes: 23 additions & 0 deletions test/build/linked-pkg/public/index.html
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Web site created using create-snowpack-app" />
<title>Snowpack App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="/_dist_/index.js"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
7 changes: 7 additions & 0 deletions test/build/linked-pkg/src/index.js
@@ -0,0 +1,7 @@
import link from '@snowpack/test-link';
import deeplink from '@snowpack/test-link/deep';
import local from '@snowpack/test-local';

link();
deeplink();
local();
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.
@@ -1,12 +1,12 @@
{
"scripts": {
"TEST": "node ../../../pkg/dist-node/index.bin.js build"
},
"snowpack": {
"buildOptions": {
"metaDir": "/static/snowpack"
}
},
"scripts": {
"TEST": "../../../pkg/dist-node/index.bin.js build"
},
"dependencies": {
"shallow-equal": "^1.2.1"
}
Expand Down
1 change: 0 additions & 1 deletion test/build/metadata-dir/.gitignore

This file was deleted.

0 comments on commit 3280de3

Please sign in to comment.