jalla 1.0.0-4 → 1.0.0-41

Sign up to get free protection for your applications and to get access to all the features.
package/README.md CHANGED
@@ -28,7 +28,7 @@ compiled using [Documentify][documentify]. And it's all configured for you.
28
28
  - [`ctx.assets`](#ctxassets)
29
29
  - [Assets](#assets)
30
30
  - [Manifest](#manifest)
31
- - [Service Workers](#service-workers)
31
+ - [Service Workers](#service-workers)
32
32
  - [Advanced Usage](#advanced-usage)
33
33
  - [Configuration](#configuration)
34
34
  - [JavaScript](#javascript)
@@ -70,6 +70,7 @@ $ NODE_ENV=production jalla index.js
70
70
  - __`--css`__ explicitly include a css file in the build
71
71
  - __`--service-worker, --sw`__ entry point for a service worker
72
72
  - __`--base, -b`__ base path where app will be served
73
+ - __`--skip, -s`__ skip transform for file/glob (excluding optimizations)
73
74
  - __`--watch, -w`__ watch files for changes (default in `development`)
74
75
  - __`--dir, -d`__ output directory, use with [build](#build) and [serve](#serve)
75
76
  - __`--quiet, -q`__ disable printing to console
@@ -178,11 +179,13 @@ before issuing another render pass using the state generated the first time.
178
179
 
179
180
  ```javascript
180
181
  // store.js
182
+ var fetch = require('node-fetch')
183
+
181
184
  module.exports = function (state, emitter) {
182
185
  state.data = state.data || null
183
186
 
184
187
  emitter.on('fetch', function () {
185
- var request = window.fetch('/my/api')
188
+ var request = fetch('/my/api')
186
189
  .then((res) => res.json())
187
190
  .then(function (data) {
188
191
  state.data = data
@@ -258,7 +261,7 @@ A bare-bones application manifest is generated based on the projects
258
261
  `package.json`. You can either place a custom `manifest.json` in the
259
262
  [assets](#assets) folder or you can generate one using a custom middleware.
260
263
 
261
- ### Service Workers
264
+ ## Service Workers
262
265
  By supplying the path to a service worker entry file with the `sw` option, jalla
263
266
  will build and serve it's bundle from that path.
264
267
 
@@ -313,7 +316,13 @@ self.addEventListener('install', function oninstall (event) {
313
316
 
314
317
  self.addEventListener('activate', function onactivate (event) {
315
318
  // clear old caches on activate
316
- event.waitUntil(clear().then(() => self.clients.claim()))
319
+ event.waitUntil(clear().then(function () {
320
+ if (!self.registration.navigationPreload) return self.clients.claim()
321
+ // enable navigation preload
322
+ return self.registration.navigationPreload.enable().then(function () {
323
+ return self.clients.claim()
324
+ })
325
+ }))
317
326
  })
318
327
 
319
328
  self.addEventListener('fetch', function onfetch (event) {
@@ -321,14 +330,14 @@ self.addEventListener('fetch', function onfetch (event) {
321
330
  event.respondWith(caches.open(CACHE_KEY).then(async function (cache) {
322
331
  try {
323
332
  var cached = await cache.match(req)
324
- var response = self.fetch(event.request)
325
- if (req.method.toUpperCase() === 'GET') {
333
+ var response = await (event.preloadResponse || self.fetch(event.request))
334
+ if (response.ok && req.method.toUpperCase() === 'GET') {
326
335
  await cache.put(req, response.clone())
327
336
  }
328
337
  return response
329
338
  } catch (err) {
330
339
  if (cached) return cached
331
- throw err
340
+ return err
332
341
  }
333
342
  }))
334
343
  })
@@ -369,24 +378,32 @@ is compiling the app. The pipline steps are called in series, and have access
369
378
  to the assets and dependencies of all prior steps.
370
379
 
371
380
  ```javascript
372
- var fs = require('fs')
381
+ var path = require('path')
373
382
  var jalla = require('jalla')
383
+ var csv = require('csvtojson')
374
384
  var app = jalla('index.js')
375
385
 
376
- // include data.csv as an asset
386
+ // convert and include data.csv as a JSON file
377
387
  app.pipeline.get('assets').push(function (state, emit) {
378
- return function (cb) {
379
- emit('progress', 'data.csv')
380
-
381
- fs.readFile('data.csv', function (err, buffer) {
382
- if (err) return cb(err)
383
- emit('asset', 'data.json', buffer)
384
- cb()
388
+ return async function (cb) {
389
+ if (state.assets.has('data.json')) return cb()
390
+ emit('progress', 'data.json')
391
+ var json = await csv.fromFile(path.resolve(state.entry, 'data.csv'))
392
+ emit('asset', 'data.json', Buffer.from(JSON.stringify(json)), {
393
+ mime: 'application/json
385
394
  })
395
+ cb()
386
396
  }
387
397
  })
388
398
 
389
- app.listen(8080)
399
+ if (process.env.BUILD) {
400
+ app.build(path.resolve(__dirname, 'dist'), function (err) {
401
+ if (err) console.error(err)
402
+ process.exit(err ? 1 : 0)
403
+ })
404
+ } else {
405
+ app.listen(8080)
406
+ }
390
407
  ```
391
408
 
392
409
  ## Configuration
package/bin.js CHANGED
@@ -8,7 +8,6 @@ var assert = require('assert')
8
8
  var dedent = require('dedent')
9
9
  var getPort = require('get-port')
10
10
  var minimist = require('minimist')
11
- var App = require('./lib/app')
12
11
  var jalla = require('./index')
13
12
 
14
13
  var COMMANDS = ['start', 'build', 'serve']
@@ -16,14 +15,15 @@ var COMMANDS = ['start', 'build', 'serve']
16
15
  var argv = minimist(process.argv.slice(2), {
17
16
  alias: {
18
17
  'service-worker': 'sw',
19
- 'dir': 'd',
20
- 'quiet': 'q',
21
- 'inspect': 'i',
22
- 'base': 'b',
23
- 'watch': 'w',
24
- 'port': 'p',
25
- 'help': 'h',
26
- 'version': 'v'
18
+ dir: 'd',
19
+ quiet: 'q',
20
+ inspect: 'i',
21
+ skip: 's',
22
+ base: 'b',
23
+ watch: 'w',
24
+ port: 'p',
25
+ help: 'h',
26
+ version: 'v'
27
27
  },
28
28
  default: {
29
29
  port: process.env.PORT || 8080
@@ -49,6 +49,7 @@ if (argv.help) {
49
49
  --css entry point for CSS
50
50
  --service-worker, --sw entry point for service worker
51
51
  --base, -b base path where app will be mounted
52
+ --skip, -s skip transform for file/glob (excluding optimizations)
52
53
  --watch, -w enable watch mode (default in development)
53
54
  --dir, -d output directory, use with ${chalk.bold('build')} and ${chalk.bold('serve')}
54
55
  --quiet, -q disable printing to console
@@ -95,14 +96,16 @@ if (typeof argv.watch !== 'undefined') opts.watch = Boolean(argv.watch)
95
96
 
96
97
  if (command === 'build') {
97
98
  opts.watch = false
98
- let app = new App(path.resolve(process.cwd(), entry), opts)
99
- let dir = typeof argv.dir === 'string' ? argv.dir : 'dist'
100
- app.build(path.resolve(process.cwd(), dir), function (err) {
101
- process.exit(err ? 1 : 0)
99
+ const app = jalla(path.resolve(process.cwd(), entry), opts)
100
+ const dir = typeof argv.dir === 'string' ? argv.dir : 'dist'
101
+ app.build(path.resolve(process.cwd(), dir)).then(function () {
102
+ process.exit(0)
103
+ }, function () {
104
+ process.exit(1)
102
105
  })
103
106
  } else {
104
- let app = jalla(path.resolve(process.cwd(), entry), opts)
105
- getPort({ port: argv.port }).then(function (port) {
107
+ const app = jalla(path.resolve(process.cwd(), entry), opts)
108
+ getPort({ port: argv.port || 8080 }).then(function (port) {
106
109
  app.listen(port)
107
110
  })
108
111
  }
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  var path = require('path')
2
2
  var assert = require('assert')
3
3
  var serve = require('koa-static')
4
+ var { Minimatch } = require('minimatch')
4
5
  var App = require('./lib/app')
5
6
  var render = require('./lib/render')
6
7
 
@@ -14,11 +15,24 @@ function start (entry, opts = {}) {
14
15
  var dist = opts.dist
15
16
  if (!dist) dist = typeof opts.serve === 'string' ? opts.serve : 'dist'
16
17
 
18
+ var swPath = opts.sw
19
+ ? path.resolve(dir, dist, 'public', path.relative(dir, opts.sw))
20
+ : null
21
+
22
+ if (opts.skip) {
23
+ const input = Array.isArray(opts.skip) ? opts.skip : [opts.skip]
24
+ var skip = input.map(normalizeSkip)
25
+ }
26
+
17
27
  opts = Object.assign({}, opts, {
18
28
  dist: absolute(dist, dir),
19
29
  serve: Boolean(opts.serve),
20
30
  sw: opts.sw && absolute(opts.sw, dir),
21
- css: opts.css && absolute(opts.css, dir)
31
+ css: opts.css && absolute(opts.css, dir),
32
+ skip (file) {
33
+ if (!skip) return false
34
+ return skip.reduce((res, test) => res || test(file), false)
35
+ }
22
36
  })
23
37
 
24
38
  var app = new App(entry, opts)
@@ -29,45 +43,44 @@ function start (entry, opts = {}) {
29
43
  app.emit('timing', Date.now() - start, ctx)
30
44
  })
31
45
 
32
- if (opts.serve) {
33
- let pub = path.resolve(opts.dist, 'public')
34
- app.use(serve(pub, { setHeaders }))
35
- } else {
36
- let state = Object.assign({
37
- env: app.env,
38
- base: opts.base || '',
39
- watch: typeof opts.watch === 'undefined'
40
- ? app.env === 'development'
41
- : opts.watch
42
- }, opts)
43
- let init = new Promise(function (resolve, reject) {
44
- app.pipeline.bundle(entry, state, resolve)
45
- })
46
-
47
- app.use((ctx, next) => init.then(next))
48
- app.use(app.pipeline.middleware())
49
- app.use(function (ctx, next) {
50
- if (ctx.body) {
51
- let cache = state.env !== 'development' && !state.watch
52
- let maxAge = cache ? 60 * 60 * 24 * 365 : 0
53
- let value = `${cache ? 'public, ' : ''}max-age=${maxAge}`
54
- ctx.set('Cache-Control', value)
55
- }
56
- return next()
57
- })
58
- }
59
-
60
46
  app.use(require('koa-conditional-get')())
61
47
  app.use(require('koa-etag')())
62
48
  app.use(render(app))
63
49
 
50
+ if (opts.serve) {
51
+ app.use(serve(path.resolve(opts.dist, 'public'), { setHeaders }))
52
+ } else {
53
+ app.use(app.pipeline.middleware(app.state))
54
+ }
55
+
64
56
  return app
57
+
58
+ // set static asset headers
59
+ // (obj, str, obj) -> void
60
+ function setHeaders (res, filepath, stats) {
61
+ if (filepath === swPath) {
62
+ res.setHeader('Cache-Control', 'max-age=0')
63
+ } else {
64
+ res.setHeader('Cache-Control', `public, max-age=${60 * 60 * 24 * 365}`)
65
+ }
66
+ }
65
67
  }
66
68
 
67
- // set static asset headers
68
- // (obj, str, obj) -> void
69
- function setHeaders (res, path, stats) {
70
- res.setHeader('Cache-Control', `public, maxage=${60 * 60 * 24 * 365}`)
69
+ // ensure skip input is a function
70
+ // any -> fn
71
+ function normalizeSkip (val) {
72
+ if (val instanceof RegExp) {
73
+ return val.test.bind(val)
74
+ } else if (typeof val === 'function') {
75
+ return val
76
+ } else if (typeof val === 'string') {
77
+ var minimatch = new Minimatch(val)
78
+ return function (str) {
79
+ return str.includes(val) || minimatch.match(str)
80
+ }
81
+ } else {
82
+ throw new Error('jalla: skip should be either RegExp, function or string')
83
+ }
71
84
  }
72
85
 
73
86
  // resolve file path (relative to dir) to absolute path
package/lib/app.js CHANGED
@@ -1,7 +1,8 @@
1
+ var fs = require('fs')
1
2
  var Koa = require('koa')
2
3
  var path = require('path')
3
4
  var assert = require('assert')
4
- var mkdirp = require('mkdirp')
5
+ var browserslist = require('browserslist')
5
6
  var ui = require('./ui')
6
7
  var build = require('./build')
7
8
  var style = require('./style')
@@ -12,28 +13,37 @@ var manifest = require('./manifest')
12
13
  var Pipeline = require('./pipeline')
13
14
  var serviceWorker = require('./service-worker')
14
15
 
16
+ var DEFAULT_BROWSERS = [
17
+ 'last 2 Chrome versions',
18
+ 'last 2 Firefox versions',
19
+ 'last 2 Safari versions',
20
+ 'last 2 Edge versions',
21
+ '> 1%'
22
+ ]
23
+
15
24
  module.exports = class App extends Koa {
16
25
  constructor (entry, opts) {
17
26
  super()
18
27
 
19
- this.base = opts.base || ''
20
-
21
28
  var bundled = []
22
29
  if (opts.serve) {
23
30
  try {
24
31
  // pick up stat of existing build
25
- let stat = require(path.resolve(opts.dist, 'stat.json'))
32
+ const stat = require(path.resolve(opts.dist, 'stat.json'))
26
33
  bundled = stat.assets.map(function (asset) {
27
34
  return Object.assign({}, asset, {
28
35
  hash: Buffer.from(asset.hash, 'hex'),
29
36
  file: path.resolve(opts.dist, 'public', asset.file)
30
37
  })
31
38
  })
32
- // use bundled entry for faster startup time
33
- entry = path.resolve(opts.dist, path.basename(entry))
39
+ this.browsers = stat.browsers
34
40
  } catch (err) {
35
41
  this.emit('error', Error('Failed to load stat from serve directory'))
36
42
  }
43
+ } else {
44
+ const dir = path.dirname(entry)
45
+ const browsers = browserslist.loadConfig({ path: dir, env: this.env })
46
+ this.browsers = browsers || DEFAULT_BROWSERS
37
47
  }
38
48
 
39
49
  var pipeline = new Pipeline([
@@ -45,6 +55,8 @@ module.exports = class App extends Koa {
45
55
  ['build']
46
56
  ], bundled)
47
57
 
58
+ this.bundled = false
59
+ this.base = opts.base || ''
48
60
  this.entry = entry
49
61
  this._opts = opts
50
62
  this.pipeline = pipeline
@@ -62,20 +74,26 @@ module.exports = class App extends Koa {
62
74
  else this.silent = true
63
75
  }
64
76
 
77
+ get state () {
78
+ return Object.assign({
79
+ browsers: this.browsers,
80
+ base: this.base,
81
+ env: this.env,
82
+ watch: this.env === 'development'
83
+ }, this._opts)
84
+ }
85
+
65
86
  // write assets to disk
66
87
  // (str, fn) -> void
67
88
  build (dir, state) {
68
89
  assert(typeof dir === 'string', 'jalla:build dir should be type string')
69
90
 
70
- state = Object.assign({
71
- base: this.base,
72
- env: this.env,
73
- watch: false,
74
- dist: dir
75
- }, this._opts, state)
91
+ if (!path.isAbsolute(dir)) dir = path.resolve(dir)
92
+
93
+ state = Object.assign({}, this.state, state, { dist: dir, watch: false })
76
94
 
77
95
  return new Promise((resolve, reject) => {
78
- mkdirp(dir, (err) => {
96
+ fs.mkdir(dir, { recursive: true }, (err) => {
79
97
  if (err) return reject(err)
80
98
  var index = this.pipeline.get('build').push(build)
81
99
  this.pipeline.bundle(this.entry, state, (err) => {
@@ -87,12 +105,24 @@ module.exports = class App extends Koa {
87
105
  })
88
106
  }
89
107
 
90
- // start server
91
- // (num, fn) -> void
92
- listen (port, cb) {
93
- super.listen(port, () => {
94
- this.emit('start', port)
95
- if (typeof cb === 'function') cb()
108
+ bundle () {
109
+ this.bundled = true
110
+ var init = new Promise((resolve, reject) => {
111
+ this.pipeline.bundle(this.entry, this.state, function (err) {
112
+ if (err) return reject(err)
113
+ resolve()
114
+ })
115
+ })
116
+ this.middleware.unshift((ctx, next) => init.then(next))
117
+ return init
118
+ }
119
+
120
+ listen (port = 8080, cb) {
121
+ var self = this
122
+ if (!this.state.serve && !this.bundled) this.bundle()
123
+ return super.listen(port, function () {
124
+ self.emit('start', port)
125
+ if (typeof cb === 'function') return cb.apply(this, arguments)
96
126
  })
97
127
  }
98
128
  }
package/lib/build.js CHANGED
@@ -1,86 +1,14 @@
1
1
  var fs = require('fs')
2
2
  var util = require('util')
3
3
  var path = require('path')
4
- var brfs = require('brfs')
5
- var mkdirp = require('mkdirp')
6
- var crypto = require('crypto')
7
- var tfilter = require('tfilter')
8
- var through = require('through2')
9
- var babelify = require('babelify')
10
- var exorcist = require('exorcist')
11
- var uglifyify = require('uglifyify')
12
- var browserify = require('browserify')
13
- var unassertify = require('unassertify')
14
- var shakeify = require('common-shakeify')
15
- var splitRequire = require('split-require')
16
- var babelPresetEnv = require('@babel/preset-env')
17
- var inject = require('./inject')
18
4
 
19
- var createDir = util.promisify(mkdirp)
5
+ var createDir = util.promisify(fs.mkdir)
20
6
  var writeFile = util.promisify(fs.writeFile)
21
7
 
22
- module.exports = ssr
23
-
24
- function ssr (state, emit) {
25
- var name = path.basename(state.entry)
26
- var b = browserify(state.entry, {
27
- node: true,
28
- debug: true,
29
- ignoreMissing: true,
30
- // preserve paths for split-require to resolve during runtime
31
- fullPaths: true,
32
- standalone: name
33
- })
34
-
35
- capture()
36
- b.on('reset', capture)
37
-
38
- b.plugin(splitRequire, {
39
- public: state.dist,
40
- filename: dynamicBundleName,
41
- output: bundleDynamicBundle
42
- })
43
-
44
- b.on('split.pipeline', function (pipeline, entry, name) {
45
- var map = path.resolve(state.dist, name + '.map')
46
- pipeline.get('wrap').push(exorcist(map))
47
- })
48
-
49
- b.transform(tfilter(babelify, { exclude: /node_modules/ }), {
50
- plugins: ['dynamic-import-split-require'],
51
- babelrc: false,
52
- presets: [
53
- [babelPresetEnv, {
54
- targets: { node: 'current' }
55
- }]
56
- ]
57
- })
58
-
59
- b.transform(brfs)
60
- b.transform(inject('source-map-support/register', state.entry))
61
-
62
- if (state.env !== 'development') {
63
- b.transform(unassertify, { global: true })
64
- b.transform(uglifyify, { global: true })
65
- b.plugin(shakeify)
66
- }
67
-
68
- b.on('reset', function restart () {
69
- emit('progress', name, 0)
70
- })
8
+ module.exports = build
71
9
 
10
+ function build (state, emit) {
72
11
  return function bundle (cb) {
73
- var map = path.resolve(state.dist, name + '.map')
74
- var ssr = new Promise(function (resolve, reject) {
75
- b.bundle().on('error', reject)
76
- // can't run terser in tandem due it generating octal escaped sequences
77
- // in template strings which it then can't parse
78
- // .pipe(state.env !== 'development' ? minify() : through())
79
- .pipe(state.env !== 'development' ? exorcist(map) : through())
80
- .pipe(fs.createWriteStream(path.resolve(state.dist, name), 'utf8'))
81
- .on('close', resolve)
82
- })
83
-
84
12
  var re = new RegExp(`^(?:${state.base.replace(/\//g, '\\/')})?\\/`)
85
13
  var assets = Array.from(state.assets.values(), function (asset) {
86
14
  asset.file = asset.url.replace(re, '')
@@ -88,70 +16,26 @@ function ssr (state, emit) {
88
16
  })
89
17
 
90
18
  var stat = JSON.stringify({
91
- entry: name,
19
+ browsers: state.browsers,
92
20
  assets: assets
93
21
  }, stringify, 2)
94
22
 
95
23
  emit('progress', 'stat.json', 0)
96
24
 
97
25
  Promise.all([
98
- ssr,
99
- template(),
100
- ...assets.map(async function (asset) {
101
- var dest = path.resolve(state.dist, 'public', asset.file)
26
+ Promise.all(assets.map(async function (asset) {
27
+ var file = path.resolve(state.dist, 'public', asset.file)
102
28
  emit('progress', asset.id, 0)
103
- await createDir(path.dirname(dest))
104
- return writeFile(dest, asset.buffer)
105
- }),
29
+ try {
30
+ await createDir(path.dirname(file), { recursive: true })
31
+ } catch (err) {
32
+ // Ignore failed `mkdir` and try writing file anyway
33
+ }
34
+ return writeFile(file, asset.buffer)
35
+ })),
106
36
  writeFile(path.resolve(state.dist, 'stat.json'), stat)
107
37
  ]).then(cb.bind(undefined, null), cb)
108
38
  }
109
-
110
- // copy template file to dist
111
- // () -> Promise
112
- function template () {
113
- return new Promise(function (resolve, reject) {
114
- var dir = path.join(path.dirname(state.entry), 'index')
115
- resolve('.', { basedir: dir, extensions: ['.html'] }, function (err, file) {
116
- if (err) return resolve()
117
- emit('progress', file, 0)
118
- fs.readFile(file, function (err, buf) {
119
- if (err) return reject(err)
120
- resolve(writeFile(path.resolve(state.dist, 'index.html'), buf))
121
- })
122
- })
123
- })
124
- }
125
-
126
- // capture bundle dependencies from pipeline
127
- // () -> void
128
- function capture () {
129
- emit('reset')
130
- b.pipeline.get('deps').push(through.obj(function (row, enc, next) {
131
- var file = row.expose ? b._expose[row.id] : row.file
132
- emit('dep', file)
133
- this.push(row)
134
- next()
135
- }))
136
- }
137
-
138
- // generate name for dýnamic bundle
139
- // obj -> str
140
- function dynamicBundleName (record) {
141
- var basename = path.basename(record.sourceFile, '.js')
142
- var isIndex = basename === 'index'
143
- var id = basename
144
- if (isIndex) id = path.dirname(record.sourceFile).split('/').slice(-1)[0]
145
- var buff = Buffer.from(record.source)
146
- var hash = crypto.createHash('sha512').update(buff).digest('buffer')
147
- return `${id}-${hash.toString('hex').slice(0, 16)}.js`
148
- }
149
-
150
- // handle dynamic bundle
151
- // str -> stream.Writable
152
- function bundleDynamicBundle (name) {
153
- return fs.createWriteStream(path.resolve(state.dist, name), 'utf8')
154
- }
155
39
  }
156
40
 
157
41
  // JSON.stringify replacer
package/lib/compile.js CHANGED
@@ -3,6 +3,7 @@ var { addHook } = require('pirates')
3
3
  var clearModule = require('clear-module')
4
4
 
5
5
  var SCRIPT = /\.js$/
6
+ var NODE_MODULES = /node_modules/
6
7
 
7
8
  module.exports = compile
8
9
 
@@ -10,14 +11,16 @@ function compile (state, emit) {
10
11
  addHook(hook, { matcher })
11
12
 
12
13
  return function (cb) {
13
- for (let dep of state.deps) {
14
- if (SCRIPT.test(dep)) clearModule(dep)
14
+ for (const dep of state.deps) {
15
+ if (SCRIPT.test(dep) && !NODE_MODULES.test(dep) && !state.skip(dep)) {
16
+ clearModule(dep)
17
+ }
15
18
  }
16
19
  cb()
17
20
  }
18
21
 
19
22
  function matcher (file) {
20
- if (/node_modules/.test(file)) return false
23
+ if (NODE_MODULES.test(file) || state.skip(file)) return false
21
24
  return state.deps.has(file)
22
25
  }
23
26