diff --git a/tools/tests/apps/client-refresh/.gitignore b/tools/tests/apps/client-refresh/.gitignore
new file mode 100644
index 0000000000..40b878db5b
--- /dev/null
+++ b/tools/tests/apps/client-refresh/.gitignore
@@ -0,0 +1 @@
+node_modules/
\ No newline at end of file
diff --git a/tools/tests/apps/client-refresh/.meteor/.finished-upgraders b/tools/tests/apps/client-refresh/.meteor/.finished-upgraders
new file mode 100644
index 0000000000..4538749ab8
--- /dev/null
+++ b/tools/tests/apps/client-refresh/.meteor/.finished-upgraders
@@ -0,0 +1,18 @@
+# This file contains information which helps Meteor properly upgrade your
+# app when you run 'meteor update'. You should check it into version control
+# with your project.
+
+notices-for-0.9.0
+notices-for-0.9.1
+0.9.4-platform-file
+notices-for-facebook-graph-api-2
+1.2.0-standard-minifiers-package
+1.2.0-meteor-platform-split
+1.2.0-cordova-changes
+1.2.0-breaking-changes
+1.3.0-split-minifiers-package
+1.4.0-remove-old-dev-bundle-link
+1.4.1-add-shell-server-package
+1.4.3-split-account-service-packages
+1.5-add-dynamic-import-package
+1.7-split-underscore-from-meteor-base
diff --git a/tools/tests/apps/client-refresh/.meteor/.gitignore b/tools/tests/apps/client-refresh/.meteor/.gitignore
new file mode 100644
index 0000000000..4083037423
--- /dev/null
+++ b/tools/tests/apps/client-refresh/.meteor/.gitignore
@@ -0,0 +1 @@
+local
diff --git a/tools/tests/apps/client-refresh/.meteor/.id b/tools/tests/apps/client-refresh/.meteor/.id
new file mode 100644
index 0000000000..bb9b430463
--- /dev/null
+++ b/tools/tests/apps/client-refresh/.meteor/.id
@@ -0,0 +1,7 @@
+# This file contains a token that is unique to your project.
+# Check it into your repository along with the rest of this directory.
+# It can be used for purposes such as:
+# - ensuring you don't accidentally deploy one app on top of another
+# - providing package authors with aggregated statistics
+
+hc942wajsktj.cez67ut1n9qo
diff --git a/tools/tests/apps/client-refresh/.meteor/packages b/tools/tests/apps/client-refresh/.meteor/packages
new file mode 100644
index 0000000000..3e7e57d3b9
--- /dev/null
+++ b/tools/tests/apps/client-refresh/.meteor/packages
@@ -0,0 +1,18 @@
+# Meteor packages used by this project, one per line.
+# Check this file (and the other files in this directory) into your repository.
+#
+# 'meteor add' and 'meteor remove' will edit this file for you,
+# but you can also edit it by hand.
+
+meteor # Shared foundation for all Meteor packages
+static-html # Define static page content in .html files
+standard-minifier-css # CSS minifier run for production mode
+standard-minifier-js # JS minifier run for production mode
+es5-shim # ECMAScript 5 compatibility for older browsers
+ecmascript # Enable ECMAScript2015+ syntax in app code
+typescript # Enable TypeScript syntax in .ts and .tsx modules
+shell-server # Server-side component of the `meteor shell` command
+webapp # Serves a Meteor app over HTTP
+server-render # Support for server-side rendering
+test-package
+autoupdate
diff --git a/tools/tests/apps/client-refresh/.meteor/platforms b/tools/tests/apps/client-refresh/.meteor/platforms
new file mode 100644
index 0000000000..efeba1b50c
--- /dev/null
+++ b/tools/tests/apps/client-refresh/.meteor/platforms
@@ -0,0 +1,2 @@
+server
+browser
diff --git a/tools/tests/apps/client-refresh/.meteor/release b/tools/tests/apps/client-refresh/.meteor/release
new file mode 100644
index 0000000000..621e94f0ec
--- /dev/null
+++ b/tools/tests/apps/client-refresh/.meteor/release
@@ -0,0 +1 @@
+none
diff --git a/tools/tests/apps/client-refresh/.meteor/versions b/tools/tests/apps/client-refresh/.meteor/versions
new file mode 100644
index 0000000000..087c94fa51
--- /dev/null
+++ b/tools/tests/apps/client-refresh/.meteor/versions
@@ -0,0 +1,54 @@
+autoupdate@1.6.0
+babel-compiler@7.4.0-beta182.17
+babel-runtime@1.4.0-beta182.17
+base64@1.0.12
+blaze-tools@1.0.10
+boilerplate-generator@1.6.0
+caching-compiler@1.2.1
+caching-html-compiler@1.1.3
+callback-hook@1.1.0
+check@1.3.1
+ddp@1.4.0
+ddp-client@2.3.3
+ddp-common@1.4.0
+ddp-server@2.3.0
+diff-sequence@1.1.1
+dynamic-import@0.5.1
+ecmascript@0.13.0-beta182.17
+ecmascript-runtime@0.7.0
+ecmascript-runtime-client@0.9.0-beta182.17
+ecmascript-runtime-server@0.8.0-beta182.17
+ejson@1.1.0
+es5-shim@4.8.0
+fetch@0.1.1
+html-tools@1.0.11
+htmljs@1.0.11
+id-map@1.1.0
+inter-process-messaging@0.1.0
+logging@1.1.20
+meteor@1.9.3
+minifier-css@1.4.2
+minifier-js@2.4.1
+modern-browsers@0.1.4
+modules@0.14.0-beta182.17
+modules-runtime@0.11.0-beta182.17
+mongo-id@1.0.7
+promise@0.11.2
+random@1.1.0
+reload@1.3.0
+retry@1.1.0
+routepolicy@1.1.0
+server-render@0.3.1
+shell-server@0.4.0
+socket-stream-client@0.2.2
+spacebars-compiler@1.1.3
+standard-minifier-css@1.5.3
+standard-minifier-js@2.4.1
+static-html@1.2.2
+templating-tools@1.1.2
+test-package@0.0.1
+tracker@1.2.0
+typescript@3.5.2-beta182.17
+underscore@1.0.10
+webapp@1.7.4
+webapp-hashing@1.0.9
diff --git a/tools/tests/apps/client-refresh/client/main.css b/tools/tests/apps/client-refresh/client/main.css
new file mode 100644
index 0000000000..7f354f0fa7
--- /dev/null
+++ b/tools/tests/apps/client-refresh/client/main.css
@@ -0,0 +1,4 @@
+body {
+ padding: 10px;
+ font-family: sans-serif;
+}
diff --git a/tools/tests/apps/client-refresh/client/main.html b/tools/tests/apps/client-refresh/client/main.html
new file mode 100644
index 0000000000..19789db4de
--- /dev/null
+++ b/tools/tests/apps/client-refresh/client/main.html
@@ -0,0 +1,21 @@
+
+ Minimal Meteor app
+
+
+
+ Minimal Meteor app
+
+ This Meteor app uses as few Meteor packages as possible, to keep the
+ client JavaScript bundle as small as possible.
+
+
+
+
+ Learn Meteor!
+
+
diff --git a/tools/tests/apps/client-refresh/client/main.js b/tools/tests/apps/client-refresh/client/main.js
new file mode 100644
index 0000000000..4d7e4c693d
--- /dev/null
+++ b/tools/tests/apps/client-refresh/client/main.js
@@ -0,0 +1,2 @@
+import "../imports/both";
+console.log(module.id, 0);
diff --git a/tools/tests/apps/client-refresh/imports/both.js b/tools/tests/apps/client-refresh/imports/both.js
new file mode 100644
index 0000000000..652a4168ea
--- /dev/null
+++ b/tools/tests/apps/client-refresh/imports/both.js
@@ -0,0 +1 @@
+console.log(module.id, 0);
diff --git a/tools/tests/apps/client-refresh/package.json b/tools/tests/apps/client-refresh/package.json
new file mode 100644
index 0000000000..480a02abdf
--- /dev/null
+++ b/tools/tests/apps/client-refresh/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "client-refresh",
+ "private": true,
+ "scripts": {
+ "start": "meteor run",
+ "test": "meteor test --once --driver-package meteortesting:mocha",
+ "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha",
+ "visualize": "meteor --production --extra-packages bundle-visualizer"
+ },
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "meteor-node-stubs": "^1.0.0"
+ },
+ "meteor": {
+ "mainModule": {
+ "client": "client/main.js",
+ "server": "server/main.js"
+ }
+ }
+}
diff --git a/tools/tests/apps/client-refresh/packages/test-package/README.md b/tools/tests/apps/client-refresh/packages/test-package/README.md
new file mode 100644
index 0000000000..8bb51bc3bf
--- /dev/null
+++ b/tools/tests/apps/client-refresh/packages/test-package/README.md
@@ -0,0 +1,2 @@
+Helper package for testing that changes to files that are not used by the
+server bundle do not trigger a server restart.
diff --git a/tools/tests/apps/client-refresh/packages/test-package/both.js b/tools/tests/apps/client-refresh/packages/test-package/both.js
new file mode 100644
index 0000000000..652a4168ea
--- /dev/null
+++ b/tools/tests/apps/client-refresh/packages/test-package/both.js
@@ -0,0 +1 @@
+console.log(module.id, 0);
diff --git a/tools/tests/apps/client-refresh/packages/test-package/client.js b/tools/tests/apps/client-refresh/packages/test-package/client.js
new file mode 100644
index 0000000000..8e148648bb
--- /dev/null
+++ b/tools/tests/apps/client-refresh/packages/test-package/client.js
@@ -0,0 +1,2 @@
+import "./both";
+console.log(module.id, 0);
diff --git a/tools/tests/apps/client-refresh/packages/test-package/package.js b/tools/tests/apps/client-refresh/packages/test-package/package.js
new file mode 100644
index 0000000000..d001a1fb7b
--- /dev/null
+++ b/tools/tests/apps/client-refresh/packages/test-package/package.js
@@ -0,0 +1,12 @@
+Package.describe({
+ name: 'test-package',
+ version: '0.0.1',
+ summary: '',
+ documentation: 'README.md'
+});
+
+Package.onUse(function(api) {
+ api.use('ecmascript');
+ api.mainModule('client.js', 'client');
+ api.mainModule('server.js', 'server');
+});
diff --git a/tools/tests/apps/client-refresh/packages/test-package/server.js b/tools/tests/apps/client-refresh/packages/test-package/server.js
new file mode 100644
index 0000000000..8e148648bb
--- /dev/null
+++ b/tools/tests/apps/client-refresh/packages/test-package/server.js
@@ -0,0 +1,2 @@
+import "./both";
+console.log(module.id, 0);
diff --git a/tools/tests/apps/client-refresh/server/main.js b/tools/tests/apps/client-refresh/server/main.js
new file mode 100644
index 0000000000..4d7e4c693d
--- /dev/null
+++ b/tools/tests/apps/client-refresh/server/main.js
@@ -0,0 +1,2 @@
+import "../imports/both";
+console.log(module.id, 0);
diff --git a/tools/tests/client-refresh.js b/tools/tests/client-refresh.js
new file mode 100644
index 0000000000..e9958141a6
--- /dev/null
+++ b/tools/tests/client-refresh.js
@@ -0,0 +1,129 @@
+import * as selftest from '../tool-testing/selftest.js';
+
+selftest.define("client refresh for package code", () => testHelper({
+ client: {
+ path: "packages/test-package/client.js",
+ id: "/node_modules/meteor/test-package/client.js",
+ },
+ server: {
+ path: "packages/test-package/server.js",
+ id: "/node_modules/meteor/test-package/server.js",
+ },
+ both: {
+ path: "packages/test-package/both.js",
+ id: "/node_modules/meteor/test-package/both.js",
+ },
+}));
+
+selftest.define("client refresh for application code", () => testHelper({
+ client: {
+ path: "client/main.js",
+ id: "/client/main.js",
+ },
+ server: {
+ path: "server/main.js",
+ id: "/server/main.js",
+ },
+ both: {
+ path: "imports/both.js",
+ id: "/imports/both.js",
+ },
+}));
+
+function testHelper(pathsAndIds) {
+ const s = new selftest.Sandbox();
+ s.createApp("myapp", "client-refresh");
+ s.cd("myapp");
+
+ let run = s.run();
+ run.match("Started proxy");
+ run.waitSecs(15);
+
+ run.match(pathsAndIds.both.id + " 0");
+ run.match(pathsAndIds.server.id + " 0");
+
+ function checkClientRefresh() {
+ run.match("Client modified -- refreshing");
+ }
+
+ function checkServerRestart(counts) {
+ run.match("Server modified -- restarting");
+ if (typeof counts.both === "number") {
+ run.match(pathsAndIds.both.id + " " + counts.both);
+ }
+ if (typeof counts.server === "number") {
+ run.match(pathsAndIds.server.id + " " + counts.server);
+ }
+ run.match("Meteor server restarted");
+ }
+
+ increment(s, pathsAndIds.client.path);
+ checkClientRefresh();
+
+ increment(s, pathsAndIds.server.path);
+ checkServerRestart({
+ both: 0,
+ server: 1,
+ });
+
+ increment(s, pathsAndIds.both.path);
+ checkServerRestart({
+ both: 1,
+ server: 1,
+ });
+
+ increment(s, pathsAndIds.client.path);
+ checkClientRefresh();
+
+ s.write(
+ pathsAndIds.server.path,
+ // Comment out the import of ./both in the server file:
+ s.read(pathsAndIds.server.path).replace(/\bimport\b/, '//import'),
+ );
+ checkServerRestart({
+ server: 1,
+ });
+
+ increment(s, pathsAndIds.server.path);
+ checkServerRestart({
+ server: 2,
+ });
+
+ increment(s, pathsAndIds.both.path);
+ checkClientRefresh();
+
+ increment(s, pathsAndIds.client.path);
+ checkClientRefresh();
+
+ s.write(
+ pathsAndIds.server.path,
+ // Uncomment the import of ./both in the server file:
+ s.read(pathsAndIds.server.path).replace(/\/\/import\b/, 'import'),
+ );
+ checkServerRestart({
+ both: 2,
+ server: 2,
+ });
+
+ increment(s, pathsAndIds.both.path);
+ checkServerRestart({
+ both: 3,
+ server: 2,
+ });
+
+ increment(s, pathsAndIds.server.path);
+ checkServerRestart({
+ both: 3,
+ server: 3,
+ });
+
+ increment(s, pathsAndIds.client.path);
+ checkClientRefresh();
+}
+
+function increment(s, path) {
+ s.write(path, s.read(path).replace(
+ /module.id, (\d+)/,
+ (match, n) => `module.id, ${ ++n }`,
+ ));
+}