-
Notifications
You must be signed in to change notification settings - Fork 10
/
packager.coffee
286 lines (218 loc) · 7.45 KB
/
packager.coffee
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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
###
Packager
========
The main responsibilities of the packager are bundling dependencies, and
creating the package.
Specification
-------------
A package is a json object with the following properties:
`dependencies` an object whose keys are names of dependencies within our context
and whose values are packages.
`distribution` an object whose keys are extensionless file paths and whose
values are executable code compiled from the source files that exist at those paths.
`source` an object whose keys are file paths and whose values are source code.
The `source` can be loaded and modified in an editor to recreate the compiled
package.
If the environment or dependecies contain all the tools required to build the
package then theoretically `distribution` may be omitted as it can be recreated
from `source`.
For a "production" distribution `source` may be omitted, but that will greatly
limit adaptability of packages.
The package specification is closely tied to the `require` method. This allows
us to use a simplified Node.js style `require` from the browser.
[Require Docs](http://distri.github.io/require/docs)
###
MemoizePromise = require "./util/memoize_promise"
# The path to the published package json. This is the primary build product and is
# used when requiring in other packages and running standalone html.
jsonPath = ({repository:{branch}}) ->
"#{branch}.json"
isDefault = (pkg) ->
{repository} = pkg
{branch} = repository
branch is repository.default_branch
relativePackagePath = (pkg) ->
if isDefault(pkg)
jsonPath(pkg)
else
"../#{jsonPath(pkg)}"
launcherScript = (pkg) ->
"""
xhr = new XMLHttpRequest;
url = #{JSON.stringify(relativePackagePath(pkg))};
xhr.open("GET", url, true);
xhr.responseType = "json";
xhr.onload = function() {
(function(PACKAGE) {
var src = #{JSON.stringify(PACKAGE.dependencies.require.distribution.main.content)};
var Require = new Function("PACKAGE", "return " + src)({distribution: {main: {content: src}}});
var require = Require.generateFor(PACKAGE);
require('./' + PACKAGE.entryPoint);
})(xhr.response)
};
xhr.send();
"""
startsWith = (string, prefix) ->
string.match RegExp "^#{prefix}"
reject = (message) ->
Promise.reject new Error message
metaTag = (name, content) ->
"<meta name=#{JSON.stringify(name)} content=#{JSON.stringify(content)}>"
html = (pkg, launcherScript=Packager.launcherScript) ->
metas = [
'<meta charset="utf-8">'
'<link rel="Help" href="./docs/">'
]
{config} = pkg
if config
{appCache} = config
try
jsonString = pkg.distribution.pixie.content.slice(17, -1)
info = JSON.parse jsonString
{title, description} = info
if title
metas.push "<title>#{title}</title>"
if description
metas.push metaTag "description", description.replace("\n", " ")
url = pkg.progenitor?.url
if url
metas.push "<link rel=\"Progenitor\" href=#{JSON.stringify(url)}>"
catch e
console.error e
if appCache
appCache = " manifest=\"manifest.appcache?#{+new Date}\""
else
appCache = ""
"""
<!DOCTYPE html>
<html#{appCache}>
<head>
#{metas.join("\n ")}
#{dependencyScripts(pkg.remoteDependencies)}
</head>
<body>
<script>
#{launcherScript(pkg)}
<\/script>
</body>
</html>
"""
cacheManifest = (pkg) ->
# TODO: Add js file
"""
CACHE MANIFEST
# #{+ new Date}
CACHE:
index.html
#{relativePackagePath(pkg)}
#{(pkg.remoteDependencies or []).join("\n")}
NETWORK:
https://*
http://*
*
"""
# `makeScript` returns a string representation of a script tag that has a src
# attribute.
makeScript = (src) ->
"<script src=#{JSON.stringify(src)}><\/script>"
# `dependencyScripts` returns a string containing the script tags that are
# the remote script dependencies of this build.
dependencyScripts = (remoteDependencies=[]) ->
remoteDependencies.map(makeScript).join("\n")
###
If our string is an absolute URL then we assume that the server is CORS enabled
and we can make a cross origin request to collect the JSON data.
We also handle a Github repo dependency such as `STRd6/issues:master`.
This loads the package from the published gh-pages branch of the given repo.
`STRd6/issues:master` will be accessible at `http://strd6.github.io/issues/master.json`.
###
fetchDependency = MemoizePromise (path) ->
if typeof path is "string"
if startsWith(path, "http")
ajax.getJSON(path)
.catch ({status, response}) ->
switch status
when 0
message = "Aborted"
when 404
message = "Not Found"
else
throw new Error response
throw "#{status} #{message}: #{path}"
else
if (match = path.match(/([^\/]*)\/([^\:]*)\:(.*)/))
[callback, user, repo, branch] = match
url = "https://#{user}.github.io/#{repo}/#{branch}.json"
ajax.getJSON(url)
.catch ->
throw new Error "Failed to load package '#{path}' from #{url}"
else
reject """
Failed to parse repository info string #{path}, be sure it's in the
form `<user>/<repo>:<ref>` for example: `STRd6/issues:master`
or `STRd6/editor:v0.9.1`
"""
else
reject "Can only handle url string dependencies right now"
Ajax = require "ajax"
ajax = Ajax()
Packager =
html: html
launcherScript: launcherScript
collectDependencies: (dependencies) ->
names = Object.keys(dependencies)
Promise.all(names.map (name) ->
value = dependencies[name]
fetchDependency value
).then (results) ->
bundledDependencies = {}
names.forEach (name, i) ->
bundledDependencies[name] = results[i]
return bundledDependencies
###
Create the standalone components of this package. An html page that loads the
main entry point for demonstration purposes and a json package that can be
used as a dependency in other packages.
The html page is named `index.html` and is in the folder of the ref, or the root
if our ref is the default branch.
Docs are generated and placed in `docs` directory as a sibling to `index.html`.
An application manifest is served up as a sibling to `index.html` as well.
The `.json` build product is placed into the root level, as siblings to the
folder containing `index.html`. If this branch is the default then these build
products are placed as siblings to `index.html`
The optional second argument is an array of files to be added to the final
package.
###
standAlone: (pkg, files=[]) ->
repository = pkg.repository
branch = repository.branch
if isDefault(pkg)
base = ""
else
base = "#{branch}/"
add = (path, content) ->
files.push
content: content
mode: "100644"
path: path
type: "blob"
add "#{base}index.html", html(pkg)
add "#{base}manifest.appcache", cacheManifest(pkg) if pkg.config?.appcache
json = JSON.stringify(pkg, null, 2)
add jsonPath(pkg), json
return files
testScripts: (pkg) ->
{distribution} = pkg
testProgram = Object.keys(distribution).filter (path) ->
path.match /test\//
.map (testPath) ->
"require('./#{testPath}')"
.join "\n"
"""
#{dependencyScripts(pkg.remoteDependencies)}
<script>
#{require('require').packageWrapper(pkg, testProgram)}
<\/script>
"""
relativePackagePath: relativePackagePath
module.exports = Packager