Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/host/app/resources/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,11 @@ class _FileResource extends Resource<Args> {
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,
Expand All @@ -343,7 +348,7 @@ class _FileResource extends Resource<Args> {
clientRequestId: opts?.clientRequestId,
},
);
if (opts?.flushLoader) {
if (moduleWasLoaded) {
this.store.refreshReferencesForCodeChange('file write');
}
if (this.innerState.state === 'not-found') {
Expand Down
5 changes: 4 additions & 1 deletion packages/host/app/services/card-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
15 changes: 11 additions & 4 deletions packages/host/app/services/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
78 changes: 78 additions & 0 deletions packages/host/tests/unit/loader-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
`,
},
}),
Expand Down Expand Up @@ -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(
Expand Down
13 changes: 13 additions & 0 deletions packages/runtime-common/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading