Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto import and core refactoring #2216

Merged
merged 2 commits into from
Jul 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ function objectAssign (to, from) {
return to;
}

export var IMPORT_MAP = hasSymbol ? Symbol() : '#';

function resolveAndComposePackages (packages, outPackages, baseUrl, parentMap, parentUrl) {
for (var p in packages) {
var resolvedLhs = resolveIfNotPlainOrUrl(p, baseUrl) || p;
Expand Down
43 changes: 43 additions & 0 deletions src/features/auto-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Auto-import <script src="system-module.js"></script> registrations
* Allows the browser preloader to work directly with SystemJS optimization
*/
import { systemJSPrototype, REGISTRY } from '../system-core.js';
import { hasDocument } from '../common.js';

var autoImports = {};

var systemRegister = systemJSPrototype.register;
systemJSPrototype.register = function (deps, declare) {
if (hasDocument && document.readyState === 'loading' && typeof deps !== 'string') {
var scripts = document.getElementsByTagName('script');
var lastScript = scripts[scripts.length - 1];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, when using the condition document.readystate === 'loading', it is not guaranteed that the last script of the page is indeed the one who called register(). More specifically, this can happen when the first auto-imported script is calling a dynamic import (System.import()) while there are still some script tags being parsed in the page (either statically or dynamically)

I'll try to come up with a way to differentiate these dynamic imports.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh that makes sense. We should be able to specifically opt-out of SystemJS own-injected scripts from this mechanism somehow.

Copy link

@tmsns tmsns Jul 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this is very similar to the problem discussed below 🙈 , except that:

  • the user is dynamically injecting a script using a regular dynamic load (System.import) during load, which will cause a System.register later on ofc
  • the last script was not related to the System script, but another script at the end of the page

In other words, it doesn't seem such an edge case. 😊

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh weird, maybe that code path isn't working for some reason? I didn't fully test it yet - but yes that logic should be doing the avoidance here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure your race conditions aren't just the expected ordering ones?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, in my setup I have only one auto-imported script. In that script, there is a dynamic import to load the main chunk of the page. When System.register of the chunk is being called, I can see it takes the src of an unrelated script further down the page. So, in that sense, I don't expect any other ones. 😊

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok then it sounds like this top-out path is not working as it is supposed to.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect the issue is that lastScript is being checked in that dynamic import script, and not matching its own script as it is dynamic.

A timing-based fix with the registration system should be possible here... I will aim to take a look.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, indeed. I don't think there is a full proof way in IE to distinguish System.register's being called in the dynamic-import way vs the auto import way, as document.currentScript cannot exactly be faked.

I see a couple of workarounds though, but they are never ideal:

  • Disable auto-import once a first dynamic import is being called.
  • Add a second condition next to document.readyState === 'loading' to check a specific attribute of the lastScript.
  • Have dynamic imports only actually register (and not import) during the load phase of the element. This is a big change though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use the existing mechanism which is based on the sync timing of the register callback itself to register the script to the right URL. Integrating this with this code path will involve some timing tricks though and likely requires moving all this logic into an async step with checks.

if (lastScript && lastScript.src) {
// Only import if not already in the registry.
// This avoids re-importing an inline dynamic System.import dependency.
// There is still a risk if a user dynamically injects a custom System.register script during DOM load that does an
// anomymous registration that is able to execute before DOM load completion, and thus is incorrectly matched to the
// last script when it wasn't causing a System.import of that last script, but this is deemed an acceptable edge case
// since due to custom user registration injection.
// Not using document.currentScript is done to ensure IE11 equivalence.
if (!this[REGISTRY][lastScript.src]) {
autoImports[lastScript.src] = [deps, declare];
// Auto import = immediately import the registration
// It is up to the user to manage execution order for deep preloading.
this.import(lastScript.src);
}
return;
}
}
return systemRegister.call(this, deps, declare);
};

var systemInstantiate = systemJSPrototype.instantiate;
systemJSPrototype.instantiate = function (url) {
var autoImport = autoImports[url];
if (autoImport) {
delete autoImports[url];
return autoImport;
}
return systemInstantiate.apply(this, arguments);
};
61 changes: 61 additions & 0 deletions src/features/browser-scripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* SystemJS browser attachments for script and import map processing
*/
import { baseUrl, resolveAndComposeImportMap, hasDocument, IMPORT_MAP, resolveUrl } from '../common.js';
import { systemJSPrototype } from '../system-core.js';
import { errMsg } from '../err-msg.js';

var importMapPromise = Promise.resolve({ imports: {}, scopes: {} });

// Scripts are processed immediately, on the first System.import, and on DOMReady.
// Import map scripts are processed only once (by being marked) and in order for each phase.
// This is to avoid using DOM mutation observers in core, although that would be an alternative.
var processFirst = hasDocument;
systemJSPrototype.prepareImport = function () {
if (processFirst) {
processScripts();
processFirst = false;
}
var loader = this;
return importMapPromise.then(function (importMap) {
loader[IMPORT_MAP] = importMap;
});
};
if (hasDocument) {
processScripts();
window.addEventListener('DOMContentLoaded', processScripts);
}

function processScripts () {
[].forEach.call(document.querySelectorAll('script'), function (script) {
// TODO: deprecate systemjs-module in next major now that we have auto import
if (script.type === 'systemjs-module') {
if (!script.src)
return;
System.import(script.src.slice(0, 7) === 'import:' ? script.src.slice(7) : resolveUrl(script.src, baseUrl));
}
else if (script.type === 'systemjs-importmap') {
if (script.sp) // sp marker = systemjs processed
return;
script.sp = true;
importMapPromise = importMapPromise.then(function (importMap) {
if (script.src)
return fetch(script.src).then(function (res) {
return res.text();
}).then(function (text) {
return extendImportMap(importMap, text, script.src);
});
return extendImportMap(importMap, script.innerHTML, baseUrl);
});
}
});
}

function extendImportMap (importMap, newMapText, newMapUrl) {
try {
var newMap = JSON.parse(newMapText);
} catch (err) {
throw Error(process.env.SYSTEM_PRODUCTION ? errMsg(1) : errMsg(1, "systemjs-importmap contains invalid JSON"));
}
return resolveAndComposeImportMap(newMap, newMapUrl, importMap);
}
64 changes: 0 additions & 64 deletions src/features/import-map.js

This file was deleted.

12 changes: 12 additions & 0 deletions src/features/resolve.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { BASE_URL, baseUrl, resolveImportMap, resolveIfNotPlainOrUrl, IMPORT_MAP } from '../common.js';
import { systemJSPrototype } from '../system-core.js';
import { errMsg } from '../err-msg.js';

systemJSPrototype.resolve = function (id, parentUrl) {
parentUrl = parentUrl || !process.env.SYSTEM_BROWSER && this[BASE_URL] || baseUrl;
return resolveImportMap(this[IMPORT_MAP], resolveIfNotPlainOrUrl(id, parentUrl) || id, parentUrl) || throwUnresolved(id, parentUrl);
};

function throwUnresolved (id, parentUrl) {
throw Error(errMsg(8, process.env.SYSTEM_PRODUCTION ? [id, parentUrl].join(', ') : "Unable to resolve bare specifier '" + id + (parentUrl ? "' from " + parentUrl : "'")));
}
28 changes: 4 additions & 24 deletions src/features/script-load.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
/*
* Supports loading System.register via script tag injection
* Script instantiation loading
*/

import { systemJSPrototype } from '../system-core';
import { hasDocument, baseUrl, resolveUrl } from '../common';
import { hasDocument } from '../common.js';
import { systemJSPrototype } from '../system-core.js';
import { errMsg } from '../err-msg.js';

var systemRegister = systemJSPrototype.register;
systemJSPrototype.register = function (deps, declare) {
systemRegister.call(this, deps, declare);
};

systemJSPrototype.createScript = function (url) {
var script = document.createElement('script');
script.charset = 'utf-8';
Expand Down Expand Up @@ -43,22 +37,8 @@ systemJSPrototype.instantiate = function (url, firstParentUrl) {
});
};

if (hasDocument) {
if (hasDocument)
window.addEventListener('error', function (evt) {
lastWindowErrorUrl = evt.filename;
lastWindowError = evt.error;
});

window.addEventListener('DOMContentLoaded', loadScriptModules);
loadScriptModules();
}


function loadScriptModules() {
[].forEach.call(
document.querySelectorAll('script[type=systemjs-module]'), function (script) {
if (script.src) {
System.import(script.src.slice(0, 7) === 'import:' ? script.src.slice(7) : resolveUrl(script.src, baseUrl));
}
});
}
1 change: 0 additions & 1 deletion src/features/worker-load.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,3 @@ if (hasSelf && typeof importScripts === 'function')
return loader.getRegister();
});
};

4 changes: 3 additions & 1 deletion src/s.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import './features/import-map.js';
import './features/resolve.js';
import './features/browser-scripts.js';
import './features/script-load.js';
import './features/auto-import.js';
import './features/worker-load.js';
12 changes: 9 additions & 3 deletions src/system-node.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { REGISTRY, systemJSPrototype } from './system-core.js';
import { IMPORT_MAP, IMPORT_MAP_PROMISE } from './features/import-map.js';
import './features/resolve.js';
import './features/registry.js';
import './extras/global.js';
import './extras/module-types.js';
import './features/node-fetch.js';
import { BASE_URL, baseUrl, resolveAndComposeImportMap } from './common.js';
import { REGISTRY, systemJSPrototype } from './system-core.js';
import { BASE_URL, baseUrl, resolveAndComposeImportMap, IMPORT_MAP } from './common.js';

export const System = global.System;

const IMPORT_MAP_PROMISE = Symbol();

systemJSPrototype.prepareImport = function () {
return this[IMPORT_MAP_PROMISE];
};

const originalResolve = systemJSPrototype.resolve;
systemJSPrototype.resolve = function () {
if (!this[IMPORT_MAP]) {
Expand Down
6 changes: 4 additions & 2 deletions src/system.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import './features/import-map.js';
import './features/resolve.js';
import './features/browser-scripts.js';
import './features/script-load.js';
import './features/auto-import.js';
import './features/worker-load.js';
import './extras/global.js';
import './extras/module-types.js';
import './features/registry.js';
import './features/registry.js';
6 changes: 6 additions & 0 deletions test/browser/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,12 @@ suite('SystemJS Standard Tests', function() {
assert.equal(System.get(resolved).hi, 'bye');
});

test('should load auto import', function () {
const resolved = System.resolve('/test/fixtures/browser/auto-import.js');
assert.ok(System.has(resolved));
assert.equal(System.get(resolved).auto, 'import');
});

test('non-enumerable __esModule property export (issue 2090)', function () {
return System.import('fixtures/__esModule.js').then(function (m) {
// Even though __esModule is not enumerable on the exported object, it should be preserved on the systemjs namespace
Expand Down
7 changes: 7 additions & 0 deletions test/fixtures/browser/auto-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
System.register([], function(_export) {
return {
execute: function () {
_export('auto', 'import');
}
};
});
9 changes: 5 additions & 4 deletions test/system-core.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import { resolveIfNotPlainOrUrl } from '../src/common.js';
import { resolveIfNotPlainOrUrl, IMPORT_MAP } from '../src/common.js';
import '../src/features/registry.js';
import { REGISTRY } from '../src/system-core.js';
import '../src/features/import-map.js';
import '../src/features/resolve.js';

const SystemLoader = System.constructor;

SystemLoader.prototype[IMPORT_MAP] = { imports: {}, scopes: {} };
SystemLoader.prototype.prepareImport = () => {};

describe('Core API', function () {
const loader = new SystemLoader();
loader.resolve = x => x;

before(() => System.prepareImport());

it('Should be an instance of itself', function () {
assert(loader instanceof SystemLoader);
});
Expand Down
1 change: 1 addition & 0 deletions test/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<script type="systemjs-module" src="/test/fixtures/browser/systemjs-module-early.js"></script>
<script src="../dist/system.js"></script>
<script type="systemjs-module" src="/test/fixtures/browser/systemjs-module-script.js"></script>
<script src="/test/fixtures/browser/auto-import.js"></script>
<script type="systemjs-module" src="import:/test/fixtures/browser/systemjs-module-script2.js"></script>

<script>
Expand Down