-
Notifications
You must be signed in to change notification settings - Fork 5
/
html.debug.js
265 lines (227 loc) · 9.22 KB
/
html.debug.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
// # Internal Helpers
;(function(scope) {
'use strict'
var _util = {}
scope._util = _util
// ## `flattenValueTuples`
// After collecting value tuples ([key, value, source]), we can flatten them
// into an value map, and also warn about any masked values.
scope._util.flattenValueTuples = function flattenValueTuples(valueTuples, ignore) {
var valueMap = {}
var masked = []
for (var i = 0, tuple; tuple = valueTuples[i]; i++) {
var key = tuple[0]
if (key in valueMap && (!ignore || ignore.indexOf(key) === -1)) {
masked.push(key)
}
valueMap[key] = tuple[1]
}
if (masked.length) {
masked.forEach(_warnMaskedValues.bind(null, valueTuples))
}
return valueMap
}
// It's important that we give authors as much info as possible to diagnose
// any problems with their source. So, we spend a bit of computational effort
// whenever values are masked (imports, exports, etc).
function _warnMaskedValues(valueTuples, key) {
var conflicting = valueTuples.filter(function(tuple) {
return tuple[0] === key
}).map(function(tuple) {
return tuple[2]
})
console.warn.apply(console,
['Multiple values named "' + key + '" were evaluated:'].concat(conflicting)
)
}
})(this.HTMLExports = this.HTMLExports || {})
// # LoaderHooks
// `HTMLExports.LoaderHooks` is a mixable map of loader hooks that provide the
// underlying behavior for loading HTML documents as modules.
//
// These hooks are designed to be consumed via various interfaces:
//
// * They are indirectly mixed into [`DocumentLoader`](documentloader.html).
//
// * They can be mixed into any existing loader via [`DocumentLoader.mixin`](documentloader.html#-documentloader-mixin-).
//
// * They can be used via a [SystemJS plugin](sysjs-plugin.html).
//
;(function(scope) {
'use strict'
var LoaderHooks = {}
scope.LoaderHooks = LoaderHooks
// ## `LoaderHooks.fetch`
// Documents are fetched via a dynamic HTML import. This ensures that the
// document's linked resources (stylesheets, scripts, etc) are also properly
// loaded.
//
// The alternative would be for us to fetch document source and construct/load
// HTML documents ourselves. This becomes rather complex, and would end up
// duplicating much of the logic expressed by the HTML Imports polyfill.
LoaderHooks.fetch = function fetchHTML(load) {
console.debug('HTMLExports.LoaderHooks.fetch(', load, ')')
return new Promise(function(resolve, reject) {
var link = _newDynamicImport(load.address)
link.addEventListener('error', function() {
reject(new Error('Unknown failure when fetching URL: ' + load.address))
})
link.addEventListener('load', function() {
// One problem with the module loader spec is that the `instantiate`
// step does not support asynchronous execution. We want that, so that
// we can ensure that any async-executed scripts in the document defer
// its load (also so we can extract exports from them).
//
// Thus, we perform any async logic during load to emulate that (if
// scoped scripts are enabled).
var runScripts = scope.runScopedScripts && scope.runScopedScripts(link.import) || Promise.resolve()
runScripts.then(function() {
// Another difficulty of the module loader spec is that it rightfully
// assumes that all sources are a `String`. Because we completely skip
// over raw source, we need to be a little more crafty by placing the
// real source in a side channel.
load.metadata.importedHTMLDocument = link.import
// And then, to appease the spec/SystemJS, we provide a dummy value for
// `source`.
resolve('')
})
})
})
}
// ## `LoaderHooks.instantiate`
// Once we have a document fetched via HTML imports, we can extract the
// its dependencies and exported values.
//
// However, it is worth noting that we gain the same document semantics as
// HTML imports: stylesheets are merged into the root document, scripts
// evaluate globally, etc. Good for simplifying code, not great for scoping.
//
// Furthermore, imports are not considered loaded until all of their linked
// dependencies (stylesheets, scripts, etc) have also loaded. This makes
// prefetching declared module dependencies difficult/impossible.
LoaderHooks.instantiate = function instantiateHTML(load) {
console.debug('HTMLExports.LoaderHooks.instantiate(', load, ')')
var doc = load.metadata.importedHTMLDocument
if (!doc) {
throw new Error('HTMLExports bug: Expected fetched Document in metadata')
}
return {
deps: scope.depsFor(doc).map(function(d) { return d.name }),
execute: function executeHTML() {
return this.newModule(scope.exportsFor(doc))
}.bind(this),
}
}
// ## Document Processing
// ### `HTMLExports.depsFor`
// HTML modules can declare dependencies on any other modules via the `import`
// element:
//
// ```html
// <import src="some-module">
// ```
scope.depsFor = function depsFor(document) {
var declaredDependencies = document.querySelectorAll('import[src]')
return Array.prototype.map.call(declaredDependencies, function(importNode) {
// Much like ES6's import syntax, you can also choose which exported
// values to bring into scope, and rename them.
var aliases = {}
// The default export can be imported via the `as` attribute:
//
// ```html
// <import src="jQuery" as="$">
// ```
if (importNode.hasAttribute('as')) {
aliases.default = importNode.getAttribute('as')
}
// Named exports can be imported via the `values` attribute (space
// separated).
//
// ```html
// <import src="lodash" values="compact">
// ```
if (importNode.hasAttribute('values')) {
importNode.getAttribute('values').split(/\s+/).forEach(function(key) {
if (key === '') return
aliases[key] = key
})
}
// Each dependency returned is an object with:
return {
// * `name`: The declared name; you may want to `normalize` it.
name: importNode.getAttribute('src'),
// * `aliases`: A map of exported values to the keys they should be
// exposed as.
aliases: aliases,
// * `source`: Source element, to aid in debugging. Expect a beating if
// you leak this!
source: importNode,
}
})
}
// ### `HTMLExports.exportsFor`
// HTML modules can export elements that are tagged with `export`.
scope._exportTuplesFor = function _exportTuplesFor(document) {
//- We collect [key, value, source] and then flatten at the end.
var valueTuples = []
// They can either be named elements (via `id`), such as:
//
// ```html
// <div export id="foo">...</div>
// ```
var exportedNodes = document.querySelectorAll('[export][id]')
for (var i = 0, node; node = exportedNodes[i]; i++) {
valueTuples.push([node.getAttribute('id'), node, node])
}
// Or they can be the default export when tagged with `default`:
//
// ```html
// <div export default>...</div>
// ```
var defaultNodes = document.querySelectorAll('[export][default]')
if (defaultNodes.length > 1) {
throw new Error('Only one default export is allowed per document')
} else if (defaultNodes.length === 1) {
valueTuples.push(['default', defaultNodes[0], defaultNodes[0]])
// Otherwise, the default export will be the document.
} else {
valueTuples.push(['default', document, document])
}
// Furthermore, values exported by `<script type="scoped">` blocks are also
// exported via the document. This depends on `HTMLExports.runScopedScripts`
// having been run already.
var scopedScripts = document.querySelectorAll('script[type="scoped"]')
for (i = 0; node = scopedScripts[i]; i++) {
if (!node.exports) continue;
var keys = Object.keys(node.exports)
for (var j = 0, key; key = keys[j]; j++) {
valueTuples.push([key, node.exports[key], node])
}
}
return valueTuples
}
scope.exportsFor = function exportsFor(document) {
return scope._util.flattenValueTuples(scope._exportTuplesFor(document), ['default'])
}
// ## Internal Implementation
function _newDynamicImport(address) {
var link = document.createElement('link')
link.setAttribute('rel', 'import')
link.setAttribute('href', address)
link.setAttribute('module', '') // Annotate the link for debugability.
document.head.appendChild(link)
return link
}
})(this.HTMLExports = this.HTMLExports || {})
// # SystemJS Plugin
// Note that there is an assumption that `HTMLExports.LoaderHooks` exists in the
// scope (which is taken care of by the build process).
var LoaderHooks = this.HTMLExports.LoaderHooks
Object.keys(LoaderHooks).forEach(function(hookName) {
exports[hookName] = LoaderHooks[hookName]
})
// SystemJS' plugin interface has a slightly different interface for the
// `instantiate` hook. It expects the module to be directly returned:
exports.instantiate = function instantiateHTML() {
return LoaderHooks.instantiate.apply(this, arguments).execute()
}