Skip to content
This repository has been archived by the owner on Jan 4, 2021. It is now read-only.

Commit

Permalink
fix: improve bundling
Browse files Browse the repository at this point in the history
- Bundle#valid is replaced with Bundle#status
- Bundle.Status has been added

A manual `read` call is required after `unload` is called or an unexpected error occurs while bundling.

Rebuilds are more reliable.

Package watchers are properly destroyed by `unload` calls.
  • Loading branch information
aleclarson committed Jul 22, 2018
1 parent 4b6dc7c commit 07f9592
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 67 deletions.
8 changes: 4 additions & 4 deletions src/Bundle/Package.coffee
Expand Up @@ -142,7 +142,7 @@ class Package
try
data = evalFile @resolve('package.json')
if (name is data.name) and (version is data.version)
@bundle._rebuild() if @missedPackage
@bundle._invalidate() if @missedPackage
@data = data
return true
return false
Expand All @@ -169,7 +169,7 @@ class Package
if /^node_modules\//.test evt.name
# Skip new packages.
if evt.new
@bundle._rebuild() if @missedPackage
@bundle._invalidate() if @missedPackage
return

if /\.json$/.test evt.name
Expand All @@ -190,15 +190,15 @@ class Package

if evt.new
@assets[evt.name] = true
@bundle._rebuild() if @missedAsset
@bundle._invalidate() if @missedAsset
return

# Packages without a parent must reload their own data.
@_read() if @owner is null and evt.name is 'package.json'

asset = @assets[evt.name]
if isObject asset
@bundle._rebuild()
@bundle._invalidate()

if asset is @main
@main = null
Expand Down
20 changes: 9 additions & 11 deletions src/Bundle/build.coffee
Expand Up @@ -7,8 +7,6 @@ cush = require 'cush'
resolved = Promise.resolve()

build = (bundle, state) ->
timestamp = Date.now()

assets = [] # ordered assets
loaded = [] # sparse asset map for deduping
packages = [] # ordered packages
Expand Down Expand Up @@ -59,22 +57,22 @@ build = (bundle, state) ->
# Load the main module.
await loadAsset bundle.main

{IDLE, BUSY} = require('../Bundle').Status

# Keep loading modules until stopped or finished.
while bundle.valid and queue.length
while bundle.status == BUSY and queue.length
await mapFlush queue, loadAsset

# The bundle is invalid if dependencies are missing.
# Exit early for cancelled builds.
if bundle.status != BUSY
return null

# Cancel the build if dependencies are missing.
if missing.length
state.missing = missing
bundle._invalidate()

# Exit early for invalid bundles.
if !bundle.valid
bundle.status = IDLE
return null

# Update the build time.
bundle.time = timestamp

readTimer.print 'loaded %n assets in %t'
resolveTimer.print 'resolved %O dependencies in %t', resolvedCount
printStats bundle
Expand Down
109 changes: 59 additions & 50 deletions src/Bundle/index.coffee
Expand Up @@ -12,11 +12,20 @@ log = require('lodge').debug('cush')
fs = require 'saxon/sync'

empty = []
resolved = Promise.resolve null
nodeModulesRE = /\/node_modules\//

INIT = 1 # never built before
LAZY = 2 # no automatic rebuilds
IDLE = 3 # waiting for changes
REDO = 4 # scheduled to rebuild
BUSY = 5 # build in progress
DONE = 6 # build complete

class Bundle extends Emitter
@Asset: Asset
@Package: Package
@Status: {INIT, LAZY, IDLE, REDO, BUSY, DONE}

constructor: (opts) ->
super()
Expand All @@ -30,18 +39,18 @@ class Bundle extends Emitter
@plugins = opts.plugins or []
@parsers = opts.parsers or []
@project = null
@valid = false
@status = INIT
@state = null
@time = 0
@_result = null
@_init = opts.init
@_extRE = null
@_config = null
@_events = null
@_workers = []
@_loading = null
@_loadedPlugins = new Set
@_nextAssetId = 1
@_workers = []
@_config = null
@_events = null
@_extRE = null
@_init = opts.init
@_result = null

relative: (absolutePath) ->
absolutePath.slice @root.path.length + 1
Expand All @@ -50,7 +59,11 @@ class Bundle extends Emitter
path.resolve @root.path, relativePath

read: ->
@_result or= @_build()
if @status <= IDLE
if @_result is null
then @_result = @_build()
else @_result = @_result.then @_build.bind this
else @_result

use: (plugins) ->
if !Array.isArray plugins
Expand Down Expand Up @@ -111,25 +124,20 @@ class Bundle extends Emitter
arg.toUrl()

unload: ->
@_unload()
@_result = null
@status = LAZY
if @status > IDLE
then @_result.then @_unload.bind this
else @_unload()
return

destroy: ->
@_result = Promise.resolve null

@main = null
@assets = null

@packages.forEach (pack) ->
pack.watcher?.destroy()
@packages = null
@emitAsync 'destroy'

@project.drop this
@project = null

dropBundle this
@emitAsync 'destroy'
@unload()
@_result = resolved
return

_parseExt: (name) ->
Expand Down Expand Up @@ -168,42 +176,42 @@ class Bundle extends Emitter
if @plugins.length
return @use @plugins

_build: -> try
@valid = true
_build: ->
if @status isnt INIT
@emitSync 'rebuild'

@state = {}
@status = BUSY
try
await @_loading or= @_configure()
return null if @status isnt BUSY

await @_loading or= @_configure()
return null if !@valid
time = Date.now()
bundle = await build this, @state
return null if @status isnt BUSY

time = process.hrtime()
bundle = await build this, @state
return null if !@valid
@time = time
@status = DONE
return bundle

time = process.hrtime time
@state.elapsed = Math.ceil time[0] * 1e3 + time[1] * 1e-6
return bundle
catch err
return null if @status isnt BUSY

catch err
# Errors are ignored when the next build is automatic.
if @valid
@_result = null
@_invalidate()
@status = LAZY
@_result = resolved
throw err

# Invalidate the current build, and disable automatic rebuilds
# until the next build is triggered manually.
_invalidate: ->
@valid = false
@emitAsync 'invalidate'
return

# Schedule an automatic rebuild.
_rebuild: ->
if @_result
@_invalidate() if @valid
_invalidate: (reload) ->
if @status > REDO or @status == IDLE
@status = REDO
@emitAsync 'invalidate'
@_result = @_result
.then noEarlier 200 + Date.now()
.then @_build.bind this
.then noEarlier Date.now() + 100
.then =>
@_unload() if reload
@_build()
return

_unload: ->
dropBundle this
Expand Down Expand Up @@ -283,9 +291,10 @@ module.exports = Bundle

# Create a function that enforces a minimum delay.
noEarlier = (time) ->
return -> new Promise (resolve) ->
delay = Math.max 0, time - Date.now()
delay and setTimeout(resolve, delay) or resolve()
return -> wait time - Date.now()

wait = (ms) -> new Promise (resolve) ->
if ms > 0 then setTimeout(resolve, ms) else resolve()

resolvePlugin = (name, main) ->
if name.indexOf('cush-') is -1
Expand Down
3 changes: 1 addition & 2 deletions src/projects.coffee
Expand Up @@ -27,8 +27,7 @@ class Project extends Emitter
.on 'data', (evt) =>
@config = evalFile(evt.path) or {}
@bundles.forEach (bundle) ->
bundle._unload()
bundle._rebuild()
bundle._invalidate true
@emit 'config'
return this

Expand Down

0 comments on commit 07f9592

Please sign in to comment.