Comparing version 1.1.0 to 2.0.0-rc.1
@@ -21,2 +21,13 @@ /* | ||
globalThis.APPS = globalThis.APPS || new Map(); | ||
App.register = function register(name) { | ||
if (!globalThis.APPS.has(name)) { | ||
globalThis.APPS.set(name, { | ||
attached: false, | ||
initialState: null, | ||
}); | ||
} | ||
} | ||
function App(derby, name, filename, options) { | ||
@@ -37,2 +48,3 @@ EventEmitter.call(this); | ||
this._init(options); | ||
App.register(name); | ||
} | ||
@@ -63,2 +75,3 @@ | ||
}; | ||
App.prototype._finishInit = function() { | ||
@@ -78,3 +91,3 @@ var script = this._getScript(); | ||
// the page finished attaching | ||
if (this._cancelAttach) { | ||
if (this._cancelAttach || this._isAttached()) { | ||
this.history.refresh(); | ||
@@ -98,2 +111,23 @@ return; | ||
}; | ||
App.prototype._isAttached = function isInitialized() { | ||
const { attached } = globalThis.APPS.get(this.name); | ||
return attached; | ||
} | ||
App.prototype._persistInitialState = function persistInitialState(state) { | ||
if (this._isAttached()) { | ||
return; | ||
} | ||
globalThis.APPS.set(this.name, { | ||
attached: true, | ||
initialState: state, | ||
}); | ||
} | ||
App.prototype._initialState = function initialState() { | ||
const { initialState } = globalThis.APPS.get(this.name); | ||
return initialState; | ||
} | ||
// Modified from: https://github.com/addyosmani/jquery.parts/blob/master/jquery.documentReady.js | ||
@@ -174,4 +208,4 @@ App.prototype._contentReady = function() { | ||
App.prototype._getScript = function() { | ||
return document.querySelector('script[data-derby-app]'); | ||
App.prototype._getAppStateScript = function() { | ||
return document.querySelector('script[data-derby-app-state]'); | ||
}; | ||
@@ -178,0 +212,0 @@ |
@@ -29,4 +29,4 @@ /* | ||
var COMPILERS = { | ||
'.css': cssCompiler | ||
, '.html': htmlCompiler | ||
'.css': cssCompiler, | ||
'.html': htmlCompiler | ||
}; | ||
@@ -120,98 +120,17 @@ function cssCompiler(file, filename, options) { | ||
AppForServer.prototype.bundle = function(backend, options, cb) { | ||
var app = this; | ||
if (typeof options === 'function') { | ||
cb = options; | ||
options = null; | ||
} | ||
options || (options = {}); | ||
if (options.minify == null) options.minify = util.isProduction; | ||
// Turn all of the app's currently registered views into a javascript | ||
// function that can recreate them in the client | ||
var viewsSource = this._viewsSource(options); | ||
var bundleFiles = []; | ||
backend.once('bundle', function(bundle) { | ||
bundle.require(path.dirname(__dirname), {expose: 'derby'}); | ||
// Hack to inject the views script into the Browserify bundle by replacing | ||
// the empty _views.js file with the generated source | ||
var viewsFilename = require.resolve('./_views'); | ||
bundle.transform(function(filename) { | ||
if (filename !== viewsFilename) return through(); | ||
return through( | ||
function write() {} | ||
, function end() { | ||
this.queue(viewsSource); | ||
this.queue(null); | ||
} | ||
); | ||
}, {global: true}); | ||
bundle.on('file', function(filename) { | ||
bundleFiles.push(filename); | ||
}); | ||
app.emit('bundle', bundle); | ||
}); | ||
backend.bundle(app.filename, options, function(err, source, map) { | ||
if (err) return cb(err); | ||
app.scriptHash = crypto.createHash('md5').update(source).digest('hex'); | ||
source = source.replace('{{DERBY_SCRIPT_HASH}}', app.scriptHash); | ||
source = source.replace(/['"]{{DERBY_BUNDLED_AT}}['"]/, Date.now()); | ||
if (app.watchFiles) { | ||
app._autoRefresh(backend); | ||
app._watchBundle(bundleFiles); | ||
} | ||
cb(null, source, map); | ||
}); | ||
throw new Error( | ||
'bundle implementation missing; use racer-bundler for implementation, or remove call to this method and use another bundler', | ||
); | ||
}; | ||
AppForServer.prototype.writeScripts = function(backend, dir, options, cb) { | ||
var app = this; | ||
this.bundle(backend, options, function(err, source, map) { | ||
if (err) return cb(err); | ||
dir = path.join(dir, 'derby'); | ||
if (!fs.existsSync(dir)) fs.mkdirSync(dir); | ||
var filename = app.name + '-' + app.scriptHash; | ||
var base = path.join(dir, filename); | ||
app.scriptUrl = app.scriptBaseUrl + '/derby/' + filename + '.js'; | ||
// Write current map and bundle files | ||
if (!(options && options.disableScriptMap)) { | ||
app.scriptMapUrl = app.scriptMapBaseUrl + '/derby/' + filename + '.map.json'; | ||
source += '\n//# sourceMappingURL=' + app.scriptMapUrl; | ||
app.scriptMapFilename = base + '.map.json'; | ||
fs.writeFileSync(app.scriptMapFilename, map, 'utf8'); | ||
} | ||
app.scriptFilename = base + '.js'; | ||
fs.writeFileSync(app.scriptFilename, source, 'utf8'); | ||
// Delete app bundles with same name in development so files don't | ||
// accumulate. Don't do this automatically in production, since there could | ||
// be race conditions with multiple processes intentionally running | ||
// different versions of the app in parallel out of the same directory, | ||
// such as during a rolling restart. | ||
if (app.watchFiles) { | ||
var appPrefix = app.name + '-'; | ||
var currentBundlePrefix = appPrefix + app.scriptHash; | ||
var filenames = fs.readdirSync(dir); | ||
for (var i = 0; i < filenames.length; i++) { | ||
var filename = filenames[i]; | ||
if (filename.indexOf(appPrefix) !== 0) { | ||
// Not a bundle for this app, skip. | ||
continue; | ||
} | ||
if (filename.indexOf(currentBundlePrefix) === 0) { | ||
// Current (newly written) bundle for this app, skip. | ||
continue; | ||
} | ||
// Older bundle for this app, clean it up. | ||
var oldFilename = path.join(dir, filename); | ||
fs.unlinkSync(oldFilename); | ||
} | ||
} | ||
cb && cb(); | ||
}); | ||
throw new Error( | ||
'writeScripts implementation missing; use racer-bundler for implementation, or remove call to this method and use another bundler', | ||
); | ||
}; | ||
AppForServer.prototype._viewsSource = function(options) { | ||
return '/*DERBY_SERIALIZED_VIEWS*/' + | ||
'module.exports = ' + this.views.serialize(options) + ';' + | ||
'/*DERBY_SERIALIZED_VIEWS_END*/'; | ||
return `/*DERBY_SERIALIZED_VIEWS ${this.name}*/\n` + | ||
'module.exports = ' + this.views.serialize(options) + ';\n' + | ||
`/*DERBY_SERIALIZED_VIEWS_END ${this.name}*/\n`; | ||
}; | ||
@@ -234,6 +153,6 @@ | ||
var serialized = JSON.stringify({ | ||
scriptBaseUrl: this.scriptBaseUrl | ||
, scriptMapBaseUrl: this.scriptMapBaseUrl | ||
, scriptUrl: scriptUrl | ||
, scriptMapUrl: scriptMapUrl | ||
scriptBaseUrl: this.scriptBaseUrl, | ||
scriptMapBaseUrl: this.scriptMapBaseUrl, | ||
scriptUrl: scriptUrl, | ||
scriptMapUrl: scriptMapUrl | ||
}); | ||
@@ -240,0 +159,0 @@ fs.writeFileSync(this.serializedBase + '.json', serialized, 'utf8'); |
@@ -38,3 +38,3 @@ module.exports = Dom; | ||
(type === 'destroy') ? new DestroyListener(target, listener) : | ||
new DomListener(type, target, listener, useCapture); | ||
new DomListener(type, target, listener, useCapture); | ||
if (-1 === this._listenerIndex(domListener)) { | ||
@@ -41,0 +41,0 @@ var listeners = this._listeners || this._initListeners(); |
@@ -259,4 +259,9 @@ var expressions = require('derby-templates').expressions; | ||
EventModel.prototype._addBinding = function(binding) { | ||
var bindings = this.bindings || (this.bindings = new BindingsMap()); | ||
binding.eventModels || (binding.eventModels = new EventModelsMap()); | ||
if (this.bindings == null) { | ||
this.bindings = new BindingsMap(); | ||
} | ||
var bindings = this.bindings; | ||
if (binding.eventModels == null) { | ||
binding.eventModels = new EventModelsMap(); | ||
} | ||
bindings[binding.id] = binding; | ||
@@ -263,0 +268,0 @@ binding.eventModels[this.id] = this; |
@@ -57,3 +57,5 @@ /* | ||
function loadStylesSync(app, sourceFilename, options) { | ||
options || (options = {compress: util.isProduction}); | ||
if (options == null) { | ||
options = { compress: util.isProduction }; | ||
} | ||
var resolved = resolve.sync(sourceFilename, { | ||
@@ -60,0 +62,0 @@ extensions: app.styleExtensions, |
@@ -129,3 +129,5 @@ var derbyTemplates = require('derby-templates'); | ||
// Unfetch and unsubscribe from all queries and documents | ||
silentModel.unloadAll && silentModel.unloadAll(); | ||
if (silentModel.unloadAll) { | ||
silentModel.unloadAll(); | ||
} | ||
}; | ||
@@ -132,0 +134,0 @@ |
@@ -32,19 +32,5 @@ var Page = require('./Page'); | ||
this.res.write(pageHtml); | ||
this.app.emit('htmlDone', this); | ||
var bundleScriptTag = '<script async data-derby-app src="' + this.app.scriptUrl + '"'; | ||
if (this.app.scriptCrossOrigin) { | ||
// Scripts loaded from a different origin (such as a CDN) won't report | ||
// much information to the host page's window.onerror. Adding the | ||
// "crossorigin" attribute to the script tag allows reporting of detailed | ||
// error info to the host page. | ||
// HOWEVER - if the "crossorigin" attribute is present for a script tag | ||
// with a cross-origin "src", then the script's HTTP response MUST have | ||
// an appropriate "Access-Control-Allow-Origin" header set. Otherwise, | ||
// the browser will refuse to load the script. | ||
bundleScriptTag += ' crossorigin'; | ||
} | ||
bundleScriptTag += '></script>'; | ||
this.res.write(bundleScriptTag); | ||
this.res.write('<script type="application/json">'); | ||
this.res.write('<script data-derby-app-state type="application/json">'); | ||
var tailHtml = this.get('Tail', ns); | ||
@@ -100,5 +86,5 @@ | ||
var params = { | ||
url: req.url | ||
, body: req.body | ||
, query: req.query | ||
url: req.url, | ||
body: req.body, | ||
query: req.query, | ||
}; | ||
@@ -105,0 +91,0 @@ for (var key in req.params) { |
@@ -9,3 +9,5 @@ exports.onStringInsert = onStringInsert; | ||
} | ||
previous || (previous = ''); | ||
if (!previous) { | ||
previous = ''; | ||
} | ||
var newText = previous.slice(0, index) + text + previous.slice(index); | ||
@@ -19,3 +21,5 @@ replaceText(el, newText, transformCursor); | ||
} | ||
previous || (previous = ''); | ||
if (!previous) { | ||
previous = ''; | ||
} | ||
var newText = previous.slice(0, index) + previous.slice(index + howMany); | ||
@@ -22,0 +26,0 @@ replaceText(el, newText, transformCursor); |
{ | ||
"name": "derby", | ||
"description": "MVC framework making it easy to write realtime, collaborative applications that run in both Node.js and browsers.", | ||
"version": "1.1.0", | ||
"version": "2.0.0-rc.1", | ||
"homepage": "http://derbyjs.com/", | ||
@@ -12,24 +12,26 @@ "repository": { | ||
"scripts": { | ||
"test": "./node_modules/.bin/mocha test/all/*.mocha.js test/dom/*.mocha.js test/server/*.mocha.js; ./node_modules/.bin/jshint lib/*.js test/*.js test-utils/*.js", | ||
"checks": "npm run lint && npm test", | ||
"lint": "npx eslint *.js lib/**/*.js test/**/*.js test-utils/**/*.js", | ||
"test": "npx mocha test/all/*.mocha.js test/dom/*.mocha.js test/server/*.mocha.js", | ||
"test-browser": "node test/server.js" | ||
}, | ||
"dependencies": { | ||
"chokidar": "^3.1.1", | ||
"derby-parsing": "^0.7.6", | ||
"derby-templates": "^0.7.4", | ||
"html-util": "^0.2.1", | ||
"qs": "^6.0.2", | ||
"racer": "^1.0.0", | ||
"resolve": "^1.1.6", | ||
"through": "^2.3.4", | ||
"tracks": "^0.5.0" | ||
"chokidar": "^3.5.3", | ||
"derby-parsing": "^0.8.0", | ||
"derby-templates": "^0.8.1", | ||
"html-util": "^0.2.3", | ||
"qs": "^6.11.0", | ||
"racer": "^1.0.3", | ||
"resolve": "^1.22.1", | ||
"through": "^2.3.8", | ||
"tracks": "^0.5.8" | ||
}, | ||
"devDependencies": { | ||
"async": "^1.5.2", | ||
"browserify": "^16.2.2", | ||
"chai": "^4.2.0", | ||
"express": "^4.13.3", | ||
"jsdom": "^15.2.0", | ||
"jshint": "^2.9.5", | ||
"mocha": "^6.2.0" | ||
"async": "^3.2.4", | ||
"browserify": "^17.0.0", | ||
"chai": "^4.3.6", | ||
"express": "^4.18.1", | ||
"jsdom": "^20.0.1", | ||
"jshint": "^2.13.5", | ||
"mocha": "^10.0.0" | ||
}, | ||
@@ -36,0 +38,0 @@ "optionalDependencies": {}, |
@@ -21,3 +21,3 @@ var ComponentHarness = require('./ComponentHarness'); | ||
var toRemove = []; | ||
for (var item; item = treeWalker.nextNode();) { | ||
for (var item = treeWalker.nextNode(); item != null; item = treeWalker.nextNode()) { | ||
toRemove.push(item); | ||
@@ -24,0 +24,0 @@ } |
@@ -47,2 +47,19 @@ var expect = require('chai').expect; | ||
describe('Initial eventmodel', function() { | ||
it('should be empty', function() { | ||
const em = new EventModel(); | ||
expect(em.isEmpty()).eq(true); | ||
}); | ||
it('should lazily create bindings', function() { | ||
const em = new EventModel(); | ||
const binding = this.binding; | ||
expect(binding.eventModels).to.eq(undefined); | ||
em.addBinding(['x'], binding); | ||
expect(binding.eventModels).to.be.instanceOf(Object); | ||
console.log(em.at(['x'])); | ||
expect(em.at(['x'])).has.ownProperty('bindings').instanceOf(Object); | ||
}); | ||
}); | ||
it('updates any object references under a path when remove/insert/move happens'); | ||
@@ -49,0 +66,0 @@ |
@@ -391,2 +391,38 @@ var expect = require('chai').expect; | ||
}); | ||
it('array item binding with view function calls', function() { | ||
var app = runner.createHarness().app; | ||
app.views.register('Body', '<view is="box" list="{{_page.list}}"/ boxName="My box"/>'); | ||
function Box() {} | ||
Box.view = { | ||
is: 'box', | ||
source: | ||
'<index:>' + | ||
'{{each list.items as #item, #i}}' + | ||
'<view is="item-row" item="{{#item}}" index="{{#i}}"/>' + | ||
'{{/each}}' | ||
}; | ||
app.component(Box); | ||
// The second argument to `len(@item, boxName)` is important to the test, even though it's | ||
// unused by the function implementation. The presence of the second argument adds an additional | ||
// dependency in the function binding, subtly changing how `binding.eventModels` gets set up. | ||
app.views.register('item-row', '<div>{{@item}} <view is="item-len" len="{{len(@item, boxName)}}"/></div>'); | ||
app.views.register('item-len', '[L={{@len}}]'); | ||
app.proto.len = function(str) { | ||
if (str == null) { | ||
throw new Error('len(str) function param is null'); | ||
} | ||
return str.length; | ||
}; | ||
var page = app.createPage(); | ||
var $items = page.model.at('_page.list.items'); | ||
$items.set(['alpha', 'beta']); | ||
var fragment = page.getFragment('Body'); | ||
// When `items` gets set to an array of one, down from two, a possible bug is the `len` function | ||
// getting invoked again for the no-longer-existing second item, resulting in a thrown exception. | ||
page.model.set('_page.list.items', ['omega']); | ||
expect(fragment).html('<div>omega [L=5]</div>'); | ||
}); | ||
// Racer model listeners could mutate the model, causing changed mutations. | ||
@@ -421,3 +457,3 @@ // These events queue up in the model's mutator event queue. Derby knows | ||
page.model.on('insert', '_data.items', function(index, values) { | ||
if (values[0] == 'B') { | ||
if (values[0] === 'B') { | ||
page.model.insert('_data.items', 0, 'C'); | ||
@@ -424,0 +460,0 @@ } |
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
233345
50
6017
1
+ Addedderby-parsing@0.8.0(transitive)
+ Addedderby-templates@0.8.4(transitive)
- Removedderby-parsing@0.7.6(transitive)
- Removedderby-templates@0.7.4(transitive)
Updatedchokidar@^3.5.3
Updatedderby-parsing@^0.8.0
Updatedderby-templates@^0.8.1
Updatedhtml-util@^0.2.3
Updatedqs@^6.11.0
Updatedracer@^1.0.3
Updatedresolve@^1.22.1
Updatedthrough@^2.3.8
Updatedtracks@^0.5.8