UNPKG

12.5 kBJavaScriptView Raw
1#!/usr/bin/env node
2'use strict';
3
4function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
5
6var browserify = _interopDefault(require('browserify'));
7var chalk = _interopDefault(require('chalk'));
8var exorcist = _interopDefault(require('exorcist'));
9var fs = _interopDefault(require('fs'));
10var mkdirp = _interopDefault(require('mkdirp'));
11var rollupify = _interopDefault(require('rollupify'));
12var uglifyJs = _interopDefault(require('uglify-js'));
13var path = require('path');
14var isSudo = _interopDefault(require('is-sudo'));
15var tls = require('tls');
16var url = require('url');
17var send = _interopDefault(require('send'));
18var http = _interopDefault(require('http'));
19var https = _interopDefault(require('https'));
20var watchify = _interopDefault(require('watchify'));
21
22function 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 // entry point of our app, panels needs this to require it
34 b.require(opts.entry, {expose: opts.expose})
35
36 // expose the app dependencies
37 b.require(opts.requires)
38
39 // rollupify the bundle
40 b.transform(rollupify, {config: opts.rollupConfig})
41
42 // declare our build's externals
43 opts.externals.forEach(dep => b.external(dep))
44
45 // make sure the bundle directory exists
46 mkdirp.sync(opts.bundle)
47
48 // determine the bundle's full path
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 // write the minified code
70 const codeStream = fs.createWriteStream(`${opts.bundle}/${outJsMin}`, 'utf8')
71 codeStream.write(minified.code, () => codeStream.end())
72
73 // write the minified map code
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 // minify()
104 // buildIndexHtml()
105 // buildPanelsJson()
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
113function 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 // static assets path to put your images, files, etc.
123 assets: `${path$$}/public/`,
124
125 // the path to the bundleda pp
126 bundle: `${path$$}/bundle/${version}`,
127
128 // the app's entry point
129 entry: `${path$$}/${pkg.main}`,
130
131 // dependencies that panels already bundles for us and we can safely declare as externals
132 externals: Object.keys(
133 require('panels/package.json').dependencies
134 ).concat([
135 'redux-promise',
136 'panels'
137 ]),
138
139 // the app's name that panels will call it after, generally its the domain where it runs
140 expose: pkg.name,
141
142 // the domain to run on, defaults to the package name
143 domain: pkg.name,
144
145 // web handler for specific requests in dev mode
146 handler: (req, res, next) => next(),
147
148 // host to run the dev server at
149 host: '0.0.0.0',
150
151 // expose your own requires for your own use too
152 requires: [], // pkg.dependencies ? Object.keys(pkg.dependencies) : [],
153
154 // path to rollup.config.js used to transform the code
155 rollupConfig,
156
157 // the root to run on in that domain
158 root: '/',
159
160 secure: false,
161
162 // list of path regexes to serve as regular files and not to try to render them as panels paths
163 serveAsIs: [],
164
165 // path to the temporary bundle used when watching
166 tmp: `${path$$}/panels.app.tmp.js`,
167
168 // the version we're working on
169 version: version
170 }
171
172
173 return fs.existsSync(configFile) ?
174 Object.assign(defaultOpts, require(configFile)) :
175 defaultOpts
176}
177
178var HELP = `
179Pacpan helps you package and run panels apps with as little configuration as needed.
180
181Development is the default mode of operation. It is what happens when you run "pacpan".
182
183You can target multiple apps at once, just run: "pacpan . ../path/to/another/app".
184Notice 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
188const panelsVersion = require('panels/package.json').version
189const panelsJs = require.resolve(`panels/bundle/panels-${panelsVersion}.js`)
190const panelsJsonFile = `${__dirname}/panels.json`
191const playgroundFile = `${__dirname}/playground.html`
192
193const FILES = {
194 // serve the panels runtime
195 'panels.js': panelsJs,
196 // serve panels.json if it wasn't served from assets
197 'panels.json': panelsJsonFile,
198 // serve the panels runtime source map
199 [`panels-${panelsVersion}.js.map`]: `${panelsJs}.map`
200}
201
202function isFile(file) {
203 try {
204 const stat = fs.statSync(file)
205 return stat.isFile()
206 } catch(err) {
207 return false
208 }
209}
210
211var 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 // serve panels packaged app.js'
219 file = app.tmp
220 } else if (isFile(assetFile)) {
221 // serve static assets
222 file = assetFile
223 } else if (FILES[pathname]) {
224 file = FILES[pathname]
225 } else if (app.serveAsIs.find(regex => regex.test(pathname))) {
226 // serve files that the user defined they want them like that
227 if (isFile(assetFile)) {
228 file = assetFile
229 }
230 } else {
231 // catch all for index
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
246const HOST = '0.0.0.0'
247
248function 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 // any app on that domain will do as they should all have the same key/cert
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
287If 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
334let watchError
335function watch(opts) {
336 const b = browserify({
337 cache: {},
338 // debug: true,
339 entries: [opts.entry],
340 packageCache: {},
341 plugin: [watchify]
342 })
343
344 const domain = chalk.dim(`[${opts.domain}${opts.root}]`)
345
346 // entry point of our app, panels needs this to require it
347 b.require(opts.entry, {expose: opts.expose})
348
349 // expose the app dependencies
350 b.require(opts.requires)
351
352 // rollupify the bundle
353 b.transform(rollupify, {config: opts.rollupConfig, sourceMaps: false})
354
355 // declare our build's externals
356 opts.externals.forEach(dep => b.external(dep))
357
358 // run the bundle and output to the console
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
406process.on('uncaughtException', err => {
407 console.error('Uncaught exception:', err.stack)
408})
409
410const getApps = () => (
411 process.argv[3] ? process.argv.slice(3, process.argv.length) : [process.cwd()]
412).map(getConfig)
413
414switch(process.argv[2]) {
415case 'bundle':
416 getApps().forEach(bundle)
417 break
418
419case '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 // clean up the temp file on exit and ctrl+c event
434 process.on('exit', cleanup)
435 process.on('SIGINT', cleanup)
436 })
437 break
438
439case 'help':
440default:
441 console.log(HELP)
442 break
443}
\No newline at end of file