diff --git a/packages/host/app/resources/file.ts b/packages/host/app/resources/file.ts index c7f0415d72..eea0862ee6 100644 --- a/packages/host/app/resources/file.ts +++ b/packages/host/app/resources/file.ts @@ -334,6 +334,11 @@ class _FileResource extends Resource { clientRequestId?: string; }, ) => { + // Capture before saveSource which may call resetLoader(), replacing + // the loader with a fresh clone that has no loaded modules. + let moduleWasLoaded = + opts?.flushLoader && + this.loaderService.loader.isModuleLoaded(this._url); let response = await this.cardService.saveSource( new URL(this._url), content, @@ -343,7 +348,7 @@ class _FileResource extends Resource { clientRequestId: opts?.clientRequestId, }, ); - if (opts?.flushLoader) { + if (moduleWasLoaded) { this.store.refreshReferencesForCodeChange('file write'); } if (this.innerState.state === 'not-found') { diff --git a/packages/host/app/services/card-service.ts b/packages/host/app/services/card-service.ts index 8038782ba4..91c445ef69 100644 --- a/packages/host/app/services/card-service.ts +++ b/packages/host/app/services/card-service.ts @@ -239,7 +239,10 @@ export default class CardService extends Service { } this.subscriber?.(url, content); - if (options?.resetLoader) { + if ( + options?.resetLoader && + this.loaderService.loader.isModuleLoaded(url.href) + ) { this.loaderService.resetLoader(); } diff --git a/packages/host/app/services/store.ts b/packages/host/app/services/store.ts index 2b08c526fc..e7e628ce70 100644 --- a/packages/host/app/services/store.ts +++ b/packages/host/app/services/store.ts @@ -1060,10 +1060,17 @@ export default class StoreService extends Service implements StoreInterface { } let invalidations = event.invalidations as string[]; - if (invalidations.find((i) => hasExecutableExtension(i))) { - // the invalidation included code changes too. in this case we - // need to flush the loader so that we can pick up any updated - // code before re-running the card + if ( + invalidations.find( + (i) => + hasExecutableExtension(i) && + this.loaderService.loader.isModuleLoaded(i), + ) + ) { + // the invalidation included code changes to modules that are already + // loaded. in this case we need to flush the loader so that we can pick + // up the updated code before re-running the card. net-new modules that + // have never been loaded don't require a loader reset. this.loaderService.resetLoader(); this.store.reset(); this.reestablishReferences.perform(); diff --git a/packages/host/tests/unit/loader-test.ts b/packages/host/tests/unit/loader-test.ts index 36decb917d..0f12048170 100644 --- a/packages/host/tests/unit/loader-test.ts +++ b/packages/host/tests/unit/loader-test.ts @@ -126,6 +126,9 @@ module('Unit | loader', function (hooks) { 'foo.js': ` export function checkImportMeta() { return import.meta.url; } export function myLoader() { return import.meta.loader; } + `, + 'reexporter.js': ` + export { g } from './g'; `, }, }), @@ -254,6 +257,81 @@ module('Unit | loader', function (hooks) { ); }); + test('isModuleLoaded returns false for a module that has not been imported', function (assert) { + assert.false( + loader.isModuleLoaded(`${testRealmURL}a`), + 'module a is not loaded before import', + ); + assert.false( + loader.isModuleLoaded(`${testRealmURL}nonexistent`), + 'nonexistent module is not loaded', + ); + }); + + test('isModuleLoaded returns true for a module that has been imported', async function (assert) { + assert.false( + loader.isModuleLoaded(`${testRealmURL}a`), + 'module a is not loaded before import', + ); + await loader.import(`${testRealmURL}a`); + assert.true( + loader.isModuleLoaded(`${testRealmURL}a`), + 'module a is loaded after import', + ); + }); + + test('isModuleLoaded returns true for dependencies of an imported module', async function (assert) { + assert.false( + loader.isModuleLoaded(`${testRealmURL}b`), + 'module b is not loaded before import', + ); + assert.false( + loader.isModuleLoaded(`${testRealmURL}c`), + 'module c is not loaded before import', + ); + await loader.import(`${testRealmURL}a`); + assert.true( + loader.isModuleLoaded(`${testRealmURL}b`), + 'module b is loaded as a dependency of a', + ); + assert.true( + loader.isModuleLoaded(`${testRealmURL}c`), + 'module c is loaded as a transitive dependency of a', + ); + }); + + test('isModuleLoaded works with executable extensions in the URL', async function (assert) { + await loader.import(`${testRealmURL}person`); + assert.true( + loader.isModuleLoaded(`${testRealmURL}person`), + 'loaded without extension', + ); + assert.true( + loader.isModuleLoaded(`${testRealmURL}person.gts`), + 'loaded with .gts extension', + ); + }); + + test('isModuleLoaded returns true for a re-exported module', async function (assert) { + assert.false( + loader.isModuleLoaded(`${testRealmURL}reexporter`), + 'reexporter is not loaded before import', + ); + assert.false( + loader.isModuleLoaded(`${testRealmURL}g`), + 're-exported module g is not loaded before import', + ); + await loader.import(`${testRealmURL}reexporter`); + assert.true( + loader.isModuleLoaded(`${testRealmURL}reexporter`), + 'reexporter is loaded after import', + ); + assert.true( + loader.isModuleLoaded(`${testRealmURL}g`), + 're-exported module g is loaded as a dependency of reexporter', + ); + }); + test('identify preserves original module for reexports', function (assert) { let throwIfFetch = new Loader(async () => { throw new Error( diff --git a/packages/runtime-common/loader.ts b/packages/runtime-common/loader.ts index fab0338d91..99504a1d96 100644 --- a/packages/runtime-common/loader.ts +++ b/packages/runtime-common/loader.ts @@ -266,6 +266,19 @@ export class Loader { } } + isModuleLoaded(moduleIdentifier: string): boolean { + try { + moduleIdentifier = this.resolveImport(moduleIdentifier); + let resolvedModuleIdentifier = new URL(moduleIdentifier).href; + return this.getModule(resolvedModuleIdentifier) !== undefined; + } catch (e) { + if (e instanceof TypeError) { + return false; + } + throw e; + } + } + getKnownConsumedModules(moduleIdentifier: string): string[] { let resolvedModuleIdentifier = this.resolveImport(moduleIdentifier); let knownDependencies = this.collectKnownModuleDependencies(