1 | #!/usr/bin/env node
|
2 | 'use strict';
|
3 |
|
4 | function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
|
5 |
|
6 | var browserify = _interopDefault(require('browserify'));
|
7 | var chalk = _interopDefault(require('chalk'));
|
8 | var exorcist = _interopDefault(require('exorcist'));
|
9 | var fs = _interopDefault(require('fs'));
|
10 | var mkdirp = _interopDefault(require('mkdirp'));
|
11 | var rollupify = _interopDefault(require('rollupify'));
|
12 | var uglifyJs = _interopDefault(require('uglify-js'));
|
13 | var path = require('path');
|
14 | var isSudo = _interopDefault(require('is-sudo'));
|
15 | var tls = require('tls');
|
16 | var url = require('url');
|
17 | var send = _interopDefault(require('send'));
|
18 | var http = _interopDefault(require('http'));
|
19 | var https = _interopDefault(require('https'));
|
20 | var watchify = _interopDefault(require('watchify'));
|
21 |
|
22 | function bundle(opts) {
|
23 | const domain = chalk.dim(`[${opts.domain}]`)
|
24 |
|
25 | console.log(domain, `PacPan is getting your Panels app "${opts.expose}" ready to go :)`)
|
26 | console.time('pacpan-bundle')
|
27 |
|
28 | const b = browserify({
|
29 | debug: true,
|
30 | entries: [opts.entry]
|
31 | })
|
32 |
|
33 |
|
34 | b.require(opts.entry, {expose: opts.expose})
|
35 |
|
36 |
|
37 | b.require(opts.requires)
|
38 |
|
39 |
|
40 | b.transform(rollupify, {config: opts.rollupConfig})
|
41 |
|
42 |
|
43 | opts.externals.forEach(dep => b.external(dep))
|
44 |
|
45 |
|
46 | mkdirp.sync(opts.bundle)
|
47 |
|
48 |
|
49 | const out = `${opts.expose.replace(/[@\/]/g, '')}-${opts.version}`
|
50 |
|
51 | const outJs = `${out}.js`
|
52 | const outJsMap = `${outJs}.map`
|
53 |
|
54 | const outJsMin = `${out}.min.js`
|
55 | const outJsMinMap = `${outJsMin}.map`
|
56 |
|
57 | function minify() {
|
58 | const minified = uglifyJs.minify(`${opts.bundle}/${outJs}`, {
|
59 | compress: {
|
60 | screw_ie8: true
|
61 | },
|
62 | inSourceMap: `${opts.bundle}/${outJsMap}`,
|
63 | mangle: {
|
64 | screw_ie8: true
|
65 | },
|
66 | outSourceMap: outJsMinMap
|
67 | })
|
68 |
|
69 |
|
70 | const codeStream = fs.createWriteStream(`${opts.bundle}/${outJsMin}`, 'utf8')
|
71 | codeStream.write(minified.code, () => codeStream.end())
|
72 |
|
73 |
|
74 | const mapStream = fs.createWriteStream(`${opts.bundle}/${outJsMinMap}`, 'utf8')
|
75 | mapStream.write(minified.map, () => mapStream.end())
|
76 | }
|
77 |
|
78 | function buildIndexHtml() {
|
79 | const out = fs.createWriteStream(`${opts.bundle}/index.html`, 'utf8')
|
80 |
|
81 | const html = fs.readFileSync(`${__dirname}/playground.html`).toString()
|
82 | .replace(
|
83 | '<script src=/panels.js></script>\n',
|
84 | `<script src=/${outJsMin}></script>\n<script src=https://cdn.uxtemple.com/panels.js></script>\n`
|
85 | )
|
86 |
|
87 | out.write(html, () => out.end())
|
88 | }
|
89 |
|
90 | function buildPanelsJson() {
|
91 | const out = fs.createWriteStream(`${opts.bundle}/panels.json`, 'utf8')
|
92 |
|
93 | const json = fs.readFileSync(`${__dirname}/panels.json`).toString().replace('app.js', outJsMin)
|
94 |
|
95 | out.write(json)
|
96 | out.end()
|
97 | }
|
98 |
|
99 | b.bundle()
|
100 | .pipe(exorcist(`${opts.bundle}/${outJsMap}`, outJsMap))
|
101 | .pipe(fs.createWriteStream(`${opts.bundle}/${outJs}`), 'utf8')
|
102 | .on('finish', () => {
|
103 |
|
104 |
|
105 |
|
106 |
|
107 | console.timeEnd('pacpan-bundle')
|
108 | console.log(domain, `PacPan just finished. Your bundle is at ${opts.bundle}:`)
|
109 | console.log(fs.readdirSync(opts.bundle).join(', '))
|
110 | })
|
111 | }
|
112 |
|
113 | function getConfig(raw) {
|
114 | const path$$ = path.resolve(raw)
|
115 |
|
116 | const configFile = `${path$$}/panels.config.js`
|
117 | const pkg = require(`${path$$}/package.json`)
|
118 | const rollupConfig = require(`${__dirname}/rollup.config.js`)(path$$)
|
119 | const version = pkg.version || 'dev'
|
120 |
|
121 | const defaultOpts = {
|
122 |
|
123 | assets: `${path$$}/public/`,
|
124 |
|
125 |
|
126 | bundle: `${path$$}/bundle/${version}`,
|
127 |
|
128 |
|
129 | entry: `${path$$}/${pkg.main}`,
|
130 |
|
131 |
|
132 | externals: Object.keys(
|
133 | require('panels/package.json').dependencies
|
134 | ).concat([
|
135 | 'redux-promise',
|
136 | 'panels'
|
137 | ]),
|
138 |
|
139 |
|
140 | expose: pkg.name,
|
141 |
|
142 |
|
143 | domain: pkg.name,
|
144 |
|
145 |
|
146 | handler: (req, res, next) => next(),
|
147 |
|
148 |
|
149 | host: '0.0.0.0',
|
150 |
|
151 |
|
152 | requires: [],
|
153 |
|
154 |
|
155 | rollupConfig,
|
156 |
|
157 |
|
158 | root: '/',
|
159 |
|
160 | secure: false,
|
161 |
|
162 |
|
163 | serveAsIs: [],
|
164 |
|
165 |
|
166 | tmp: `${path$$}/panels.app.tmp.js`,
|
167 |
|
168 |
|
169 | version: version
|
170 | }
|
171 |
|
172 |
|
173 | return fs.existsSync(configFile) ?
|
174 | Object.assign(defaultOpts, require(configFile)) :
|
175 | defaultOpts
|
176 | }
|
177 |
|
178 | var HELP = `
|
179 | Pacpan helps you package and run panels apps with as little configuration as needed.
|
180 |
|
181 | Development is the default mode of operation. It is what happens when you run "pacpan".
|
182 |
|
183 | You can target multiple apps at once, just run: "pacpan . ../path/to/another/app".
|
184 | Notice the first ".", it means that you want to target this directory as an app too.
|
185 |
|
186 | "pacpan bundle" bundles the assets for you.`
|
187 |
|
188 | const panelsVersion = require('panels/package.json').version
|
189 | const panelsJs = require.resolve(`panels/bundle/panels-${panelsVersion}.js`)
|
190 | const panelsJsonFile = `${__dirname}/panels.json`
|
191 | const playgroundFile = `${__dirname}/playground.html`
|
192 |
|
193 | const FILES = {
|
194 |
|
195 | 'panels.js': panelsJs,
|
196 |
|
197 | 'panels.json': panelsJsonFile,
|
198 |
|
199 | [`panels-${panelsVersion}.js.map`]: `${panelsJs}.map`
|
200 | }
|
201 |
|
202 | function isFile(file) {
|
203 | try {
|
204 | const stat = fs.statSync(file)
|
205 | return stat.isFile()
|
206 | } catch(err) {
|
207 | return false
|
208 | }
|
209 | }
|
210 |
|
211 | var createHandler = app => (req, res) => {
|
212 | try {
|
213 | const pathname = url.parse(req.url).pathname.replace(app.root, '')
|
214 | const assetFile = `${app.assets}${pathname}`
|
215 | let file
|
216 |
|
217 | if (pathname === 'app.js') {
|
218 |
|
219 | file = app.tmp
|
220 | } else if (isFile(assetFile)) {
|
221 |
|
222 | file = assetFile
|
223 | } else if (FILES[pathname]) {
|
224 | file = FILES[pathname]
|
225 | } else if (app.serveAsIs.find(regex => regex.test(pathname))) {
|
226 |
|
227 | if (isFile(assetFile)) {
|
228 | file = assetFile
|
229 | }
|
230 | } else {
|
231 |
|
232 | const customIndexFile = `${app.assets}/index.html`
|
233 |
|
234 | file = fs.existsSync(customIndexFile) ? customIndexFile : playgroundFile
|
235 | }
|
236 |
|
237 | res.setHeader('Access-Control-Allow-Origin', '*')
|
238 | res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
|
239 | send(req, file).pipe(res)
|
240 | } catch(err) {
|
241 | res.writeHead(404)
|
242 | res.end()
|
243 | }
|
244 | }
|
245 |
|
246 | const HOST = '0.0.0.0'
|
247 |
|
248 | function serve(apps) {
|
249 | const appsByRoot = {}
|
250 | const roots = apps.map(app => {
|
251 | const root = `${app.domain}${app.root}`
|
252 | appsByRoot[root] = app
|
253 | if (app.secure) {
|
254 | app.secureContext = tls.createSecureContext({
|
255 | cert: fs.readFileSync(app.secure.cert),
|
256 | key: fs.readFileSync(app.secure.key)
|
257 | })
|
258 | }
|
259 | return root
|
260 | })
|
261 |
|
262 | const findApp = req => {
|
263 | const fullUrl = `${req.headers.host}${req.url}`
|
264 | const root = roots.filter(r => fullUrl.indexOf(r.replace(/\/$/, '')) === 0).sort()[0]
|
265 | return appsByRoot[root] || apps[0]
|
266 | }
|
267 |
|
268 | let warnedAboutSecure = false
|
269 |
|
270 | const SNICallback = (domain, cb) => {
|
271 | try {
|
272 |
|
273 | const app = apps.find(a => a.domain === domain)
|
274 |
|
275 | if (!app.secure && !warnedAboutSecure) {
|
276 | warnedAboutSecure = true
|
277 | setTimeout(() => warnedAboutSecure = false, 100)
|
278 |
|
279 | console.log(chalk.red(`You tried to access ${domain} through https but we don't have its certificate and key.`),
|
280 | `Does the app's panels.config.js include a secure key like?:
|
281 |
|
282 | secure: {
|
283 | cert: '/path/to/file.cert',
|
284 | key: '/path/to/file.key'
|
285 | }
|
286 |
|
287 | If you don't care about https, you can always access http://${domain}`)
|
288 | }
|
289 |
|
290 | cb(null, app.secureContext)
|
291 | } catch(err) {
|
292 | console.log(chalk.red(domain), err)
|
293 | }
|
294 | }
|
295 |
|
296 | const handler = (req, res) => {
|
297 | const app = findApp(req)
|
298 | app.handler(req, res, () => createHandler(app)(req, res))
|
299 | }
|
300 |
|
301 | const s = https.createServer({ SNICallback }, handler)
|
302 | s.on('error', console.error.bind(console))
|
303 | s.on('listening', () => {
|
304 | const list = apps.filter(a => a.secure)
|
305 | if (list.length) {
|
306 | console.log(chalk.green('secure apps'))
|
307 | console.log(list.map(app => ` https://${app.domain}${app.root}`).join('\n'))
|
308 | }
|
309 | })
|
310 | s.listen(443, HOST)
|
311 |
|
312 | const s2 = http.createServer((req, res) => {
|
313 | const app = findApp(req)
|
314 |
|
315 | if (app.secure) {
|
316 | res.writeHead(302, { Location: `https://${req.headers.host}${app.root}` })
|
317 | res.end()
|
318 | } else {
|
319 | handler(req, res)
|
320 | }
|
321 | })
|
322 | s2.on('error', console.error.bind(console))
|
323 | s2.on('listening', () => {
|
324 | const list = apps.filter(a => !a.secure)
|
325 |
|
326 | if (list.length) {
|
327 | console.log(chalk.yellow('insecure apps'))
|
328 | console.log(list.map(app => ` http://${app.domain}${app.root}`).join('\n'))
|
329 | }
|
330 | })
|
331 | s2.listen(80, HOST)
|
332 | }
|
333 |
|
334 | let watchError
|
335 | function watch(opts) {
|
336 | const b = browserify({
|
337 | cache: {},
|
338 |
|
339 | entries: [opts.entry],
|
340 | packageCache: {},
|
341 | plugin: [watchify]
|
342 | })
|
343 |
|
344 | const domain = chalk.dim(`[${opts.domain}${opts.root}]`)
|
345 |
|
346 |
|
347 | b.require(opts.entry, {expose: opts.expose})
|
348 |
|
349 |
|
350 | b.require(opts.requires)
|
351 |
|
352 |
|
353 | b.transform(rollupify, {config: opts.rollupConfig, sourceMaps: false})
|
354 |
|
355 |
|
356 | opts.externals.forEach(dep => b.external(dep))
|
357 |
|
358 |
|
359 | function bundle() {
|
360 | b.bundle().pipe(fs.createWriteStream(opts.tmp))
|
361 | }
|
362 | bundle()
|
363 |
|
364 | b.on('update', bundle)
|
365 | b.on('log', msg => {
|
366 | console.log(domain, msg)
|
367 | })
|
368 |
|
369 | b.on('bundle', theBundle => {
|
370 | theBundle.on('error', error => {
|
371 | if (watchError !== error.stack) {
|
372 | if (error.codeFrame) {
|
373 | console.error(domain, chalk.red(`${error.constructor.name} at ${error.id}`))
|
374 | console.error(domain, error.codeFrame)
|
375 | } else {
|
376 | const match = error.stack.match(/Error: Could not resolve (.+?) from (.+?) while/)
|
377 | if (match) {
|
378 | console.error(domain, chalk.red(`ImportError at ${match[2]}`))
|
379 | console.error(domain, 'Does', chalk.blue(match[1]), 'exist? Check that import statement.')
|
380 | } else {
|
381 | console.error(domain, error.stack)
|
382 | }
|
383 | }
|
384 | watchError = error.stack
|
385 | }
|
386 | b.removeAllListeners()
|
387 | b.close()
|
388 | setTimeout(() => watch(opts), 1000)
|
389 | })
|
390 | })
|
391 |
|
392 | return function cleanup() {
|
393 | try {
|
394 | fs.unlinkSync(opts.tmp)
|
395 | } catch(err) {
|
396 | }
|
397 |
|
398 | try {
|
399 | fs.unlinkSync(`${opts.entry}.tmp`)
|
400 | } catch(err) {
|
401 | }
|
402 | process.exit()
|
403 | }
|
404 | }
|
405 |
|
406 | process.on('uncaughtException', err => {
|
407 | console.error('Uncaught exception:', err.stack)
|
408 | })
|
409 |
|
410 | const getApps = () => (
|
411 | process.argv[3] ? process.argv.slice(3, process.argv.length) : [process.cwd()]
|
412 | ).map(getConfig)
|
413 |
|
414 | switch(process.argv[2]) {
|
415 | case 'bundle':
|
416 | getApps().forEach(bundle)
|
417 | break
|
418 |
|
419 | case 'start':
|
420 | isSudo(is => {
|
421 | if (!is) {
|
422 | console.error(chalk.red(`Please run "sudo ${process.argv.join(' ')}" instead`))
|
423 | return
|
424 | }
|
425 |
|
426 | console.log(chalk.blue('PacPan is starting'))
|
427 | console.log(chalk.gray('To exit it, press ctrl+c'))
|
428 | const apps = getApps()
|
429 | serve(apps)
|
430 | const watchers = apps.map(watch)
|
431 | const cleanup = () => watchers.forEach(w => w())
|
432 |
|
433 |
|
434 | process.on('exit', cleanup)
|
435 | process.on('SIGINT', cleanup)
|
436 | })
|
437 | break
|
438 |
|
439 | case 'help':
|
440 | default:
|
441 | console.log(HELP)
|
442 | break
|
443 | } |
\ | No newline at end of file |