alchemymvc 1.4.3 → 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -285,6 +285,16 @@ Revision.setMethod(function afterSave(record, options, created) {
285
285
  // Find the complete saved item
286
286
  Model.get(this.model.model_name).findByPk(doc.$pk, async function gotRecord(err, result) {
287
287
 
288
+ try {
289
+ await createRevision(result);
290
+ pledge.resolve();
291
+ } catch (err) {
292
+ pledge.reject(err);
293
+ }
294
+ });
295
+
296
+ async function createRevision(result) {
297
+
288
298
  if (result) {
289
299
 
290
300
  // Get the new data
@@ -320,13 +330,13 @@ Revision.setMethod(function afterSave(record, options, created) {
320
330
  [that.revision_model.model_name] : revision_data
321
331
  };
322
332
 
323
- // Save the data (but do not wait for it)
324
- that.revision_model.save(revision_data, {allowFields: true});
333
+ // Save the revision. This IS awaited: a `revert()` right after
334
+ // a save has to see this revision, and a fire-and-forget save
335
+ // would also swallow any insert error.
336
+ await that.revision_model.save(revision_data, {allowFields: true});
325
337
  }
326
338
  }
327
-
328
- pledge.resolve();
329
- });
339
+ }
330
340
 
331
341
  return pledge;
332
342
  });
@@ -722,64 +722,87 @@ Mongo.setMethod(function _remove(context) {
722
722
  */
723
723
  Mongo.setMethod(function _ensureIndex(model, index, callback) {
724
724
 
725
- this.collection(model.table, async function gotCollection(err, collection) {
725
+ // `collection()` returns a promise since 1.4 (its callback form was removed),
726
+ // so this must await it - the old callback form silently never fired, leaving
727
+ // indexes added via `addIndex` uncreated.
728
+ let pledge = Swift.waterfall(
729
+ this.collection(model.table),
730
+ async (collection) => {
726
731
 
727
- if (err != null) {
728
- return callback(err);
729
- }
732
+ let options = {
733
+ name : index.options.name,
734
+ unique : index.options.unique ? true : false,
735
+ sparse : index.options.sparse ? true : false,
736
+ };
730
737
 
731
- let options = {
732
- name : index.options.name,
733
- unique : index.options.unique ? true : false,
734
- sparse : index.options.sparse ? true : false,
735
- };
738
+ let index_specs;
736
739
 
737
- let index_specs;
740
+ // Hack in the text indexes
741
+ if (options.name == 'text') {
742
+ index_specs = {};
738
743
 
739
- // Hack in the text indexes
740
- if (options.name == 'text') {
741
- let key;
744
+ for (let key in index.fields) {
745
+ index_specs[key] = 'text';
746
+ }
747
+ } else {
748
+ index_specs = index.fields;
749
+ }
742
750
 
743
- index_specs = {};
751
+ // Reconcile any conflicting index to the schema's definition. A single
752
+ // drop can expose a second conflict (the wanted key may already exist
753
+ // under a different name), and concurrent boot-time ensures can drop an
754
+ // index from under us - so loop, dropping the conflicting index each
755
+ // round, until the create succeeds. Bounded, so a genuine problem still
756
+ // surfaces instead of spinning.
757
+ let attempts = 0;
744
758
 
745
- for (key in index.fields) {
746
- index_specs[key] = 'text';
747
- }
748
- } else {
749
- index_specs = index.fields;
750
- }
759
+ while (true) {
751
760
 
752
- try {
753
- await collection.createIndex(index_specs, options);
754
- } catch (err) {
761
+ try {
762
+ await collection.createIndex(index_specs, options);
763
+ return;
764
+ } catch (err) {
755
765
 
756
- // Check for IndexOptionsConflict
757
- if (err.code === 85) {
758
- let index_to_drop;
766
+ // 85 = IndexOptionsConflict (our key held under a different name),
767
+ // 86 = IndexKeySpecsConflict (our name held by a different key).
768
+ if ((err.code !== 85 && err.code !== 86) || attempts >= 5) {
769
+ throw err;
770
+ }
759
771
 
760
- if (err.message.includes('already exists with a different name:')) {
761
- index_to_drop = err.message.after('different name:').trim();
762
- }
772
+ attempts++;
763
773
 
764
- if (!index_to_drop) {
765
- index_to_drop = options.name;
766
- }
774
+ // Find the conflicting index(es) by inspecting the collection -
775
+ // the one holding our name, or one holding our exact key - rather
776
+ // than parsing the version-specific error message.
777
+ let specs_json = JSON.stringify(index_specs),
778
+ dropped = false;
767
779
 
768
- try {
780
+ for (let existing of await collection.indexes()) {
769
781
 
770
- // Index already exists, drop it
771
- await collection.dropIndex(index_to_drop);
782
+ if (existing.name === '_id_') {
783
+ continue;
784
+ }
772
785
 
773
- // Try again
774
- await collection.createIndex(index_specs, options);
775
- } catch (second_err) {
776
- return callback(second_err);
786
+ if (existing.name === options.name || JSON.stringify(existing.key) === specs_json) {
787
+ try {
788
+ await collection.dropIndex(existing.name);
789
+ dropped = true;
790
+ } catch (drop_err) {
791
+ // Already gone (a concurrent ensure dropped it).
792
+ }
793
+ }
794
+ }
795
+
796
+ // Nothing identifiable to drop: surface the error, don't spin.
797
+ if (!dropped) {
798
+ throw err;
799
+ }
777
800
  }
778
- } else {
779
- return callback(err);
780
801
  }
781
802
  }
803
+ );
782
804
 
783
- callback();
784
- });
805
+ pledge.done(callback);
806
+
807
+ return pledge;
785
808
  });
@@ -165,7 +165,7 @@ Fallback.setMethod(function storeInUpperDatasource(model, data, options) {
165
165
  *
166
166
  * @author Jelle De Loecker <jelle@elevenways.be>
167
167
  * @since 1.1.0
168
- * @version 1.4.2
168
+ * @version 1.4.5
169
169
  *
170
170
  * @param {Alchemy.OperationalContext.ReadDocumentFromDatasource} context
171
171
  *
@@ -183,7 +183,28 @@ Fallback.setMethod(function read(context) {
183
183
  let lower_context = context.createChild();
184
184
  lower_context.setCriteria(lower_criteria);
185
185
 
186
- tasks.push(() => this.lower.read(lower_context));
186
+ // A rejected lower (remote) read must not reject the whole query:
187
+ // resolve to null instead, so the waterfall falls through to the upper
188
+ // (local cache) read. Without this, being offline made every find fail
189
+ // even though the cache had the data.
190
+ tasks.push(() => {
191
+ let attempt = new Swift();
192
+
193
+ Swift.done(this.lower.read(lower_context), (err, result) => {
194
+
195
+ if (err) {
196
+ if (Blast.isBrowser && typeof alchemy != 'undefined' && alchemy.distinctProblem) {
197
+ alchemy.distinctProblem('fallback-lower-read', 'Remote read failed, using local cache', {repeat_after: 60000});
198
+ }
199
+
200
+ return attempt.resolve(null);
201
+ }
202
+
203
+ attempt.resolve(result);
204
+ });
205
+
206
+ return attempt;
207
+ });
187
208
  }
188
209
 
189
210
  let upper_criteria = criteria.clone();
@@ -202,7 +223,7 @@ Fallback.setMethod(function read(context) {
202
223
  *
203
224
  * @author Jelle De Loecker <jelle@elevenways.be>
204
225
  * @since 1.1.0
205
- * @version 1.4.2
226
+ * @version 1.4.5
206
227
  *
207
228
  * @param {Model} model
208
229
  *
@@ -220,7 +241,13 @@ Fallback.setMethod(function getRecordsToSync(model) {
220
241
  context.setModel(model);
221
242
  context.setCriteria(criteria);
222
243
 
223
- return this.upper.read(context);
244
+ // Datasource#read resolves with `{items, available}` since 1.4.2, but the
245
+ // consumers of this method (getRecordsToBeSavedRemotely) iterate the result
246
+ // directly - so unwrap to the items array, or offline saves never sync.
247
+ return Swift.waterfall(
248
+ this.upper.read(context),
249
+ result => (result && result.items) ? result.items : (result || [])
250
+ );
224
251
  });
225
252
 
226
253
  /**
@@ -81,7 +81,7 @@ Remote.setMethod(async function doServerCommand(action, model, data, callback) {
81
81
  let fetch_options = {
82
82
  post : data,
83
83
  headers : {'content-type': 'application/json-dry'},
84
- max_timeout : this.options.max_timeout ?? 3500 // Configurable timeout, default 3.5s
84
+ max_timeout : this.options.max_timeout ?? 15000 // Configurable timeout, default 15s
85
85
  };
86
86
 
87
87
  alchemy.fetch(route_name, fetch_options, function gotResult(err, result) {
@@ -1170,6 +1170,31 @@ Document.setMethod(function save(data, options, callback) {
1170
1170
 
1171
1171
  save_result = save_result[0];
1172
1172
 
1173
+ // Fields edited while the save was in flight would be silently
1174
+ // reverted by adopting the response wholesale (the response only
1175
+ // reflects the record as it was sent), so detect and keep them.
1176
+ let kept_fields;
1177
+
1178
+ if (in_flight_snapshot && that.$main) {
1179
+ let key;
1180
+
1181
+ for (key in that.$main) {
1182
+
1183
+ if (key == pk_name) {
1184
+ continue;
1185
+ }
1186
+
1187
+ if (!that.alikeWhenStored(that.$main[key], in_flight_snapshot[key])) {
1188
+
1189
+ if (!kept_fields) {
1190
+ kept_fields = {};
1191
+ }
1192
+
1193
+ kept_fields[key] = that.$main[key];
1194
+ }
1195
+ }
1196
+ }
1197
+
1173
1198
  // Use the saved data from now on
1174
1199
  that.$main = save_result.$main;
1175
1200
 
@@ -1177,10 +1202,21 @@ Document.setMethod(function save(data, options, callback) {
1177
1202
  that.$attributes.original_record = undefined;
1178
1203
  that.markUnchanged();
1179
1204
 
1180
- if (that.hasObjectFields()) {
1205
+ if (kept_fields || that.hasObjectFields()) {
1181
1206
  that.storeCurrentDataAsOriginalRecord();
1182
1207
  }
1183
1208
 
1209
+ if (kept_fields) {
1210
+ let key;
1211
+
1212
+ // Put the newer, in-flight edits back on top of the saved state
1213
+ // and mark them as changed, so a follow-up save persists them.
1214
+ for (key in kept_fields) {
1215
+ that.markChangedField(key, kept_fields[key]);
1216
+ that.$main[key] = kept_fields[key];
1217
+ }
1218
+ }
1219
+
1184
1220
  pledge.resolve(that);
1185
1221
  }
1186
1222
 
@@ -1218,6 +1254,16 @@ Document.setMethod(function save(data, options, callback) {
1218
1254
  }
1219
1255
  }
1220
1256
 
1257
+ // Snapshot the record as it is being sent, so `updateDoc` can tell which
1258
+ // fields were edited while the save was in flight
1259
+ let in_flight_snapshot = null;
1260
+
1261
+ try {
1262
+ in_flight_snapshot = JSON.clone(main);
1263
+ } catch (err) {
1264
+ // Without a snapshot the response is simply adopted as-is
1265
+ }
1266
+
1221
1267
  sub_pledge = this.$model.save(this, options, updateDoc);
1222
1268
 
1223
1269
  return pledge;
@@ -1478,12 +1524,45 @@ Document.setMethod(function hasFieldValue(name) {
1478
1524
  return Object.hasProperty(this.$main, name);
1479
1525
  });
1480
1526
 
1527
+ /**
1528
+ * Compare two field values, treating them as equal when their datasource
1529
+ * (stored) form matches.
1530
+ *
1531
+ * `hasChanged()` compares the live, normalized field value (a cast object such
1532
+ * as a TagTree, an ObjectId instance, ...) against the original record, which
1533
+ * holds the raw stored form. Without this, a normalized value always looked
1534
+ * different from its own unchanged stored counterpart, so such fields reported
1535
+ * a change on every load. If both sides serialize to the same datasource value,
1536
+ * saving either would write identical data, so the field has not changed.
1537
+ *
1538
+ * @author Jelle De Loecker <jelle@elevenways.be>
1539
+ * @since 1.4.4
1540
+ * @version 1.4.4
1541
+ *
1542
+ * @param {Mixed} a
1543
+ * @param {Mixed} b
1544
+ *
1545
+ * @return {boolean}
1546
+ */
1547
+ Document.setMethod(function alikeWhenStored(a, b) {
1548
+
1549
+ if (Object.alike(a, b)) {
1550
+ return true;
1551
+ }
1552
+
1553
+ try {
1554
+ return Object.alike(JSON.clone(a, 'toDatasource'), JSON.clone(b, 'toDatasource'));
1555
+ } catch (err) {
1556
+ return false;
1557
+ }
1558
+ });
1559
+
1481
1560
  /**
1482
1561
  * Has this document changed since it was created?
1483
1562
  *
1484
1563
  * @author Jelle De Loecker <jelle@elevenways.be>
1485
1564
  * @since 1.0.4
1486
- * @version 1.3.0
1565
+ * @version 1.4.4
1487
1566
  *
1488
1567
  * @param {string} name The optional field name
1489
1568
  *
@@ -1505,27 +1584,47 @@ Document.setMethod(function hasChanged(name) {
1505
1584
 
1506
1585
  let result;
1507
1586
 
1587
+ // Fields excluded from change-detection (computed fields, or `track_changes:
1588
+ // false`) never count as changed and are never compared. The `$model` getter
1589
+ // throws for an unresolvable model (a detached document, or a client document
1590
+ // whose model is not registered), so guard against that and compare every
1591
+ // field when no schema is available.
1592
+ let untracked;
1593
+ try {
1594
+ let schema = this.$model && this.$model.schema;
1595
+ untracked = schema ? schema.getUntrackedFieldNames() : null;
1596
+ } catch (err) {
1597
+ untracked = null;
1598
+ }
1599
+
1508
1600
  // If we only want to check a single field
1509
1601
  if (name) {
1510
1602
  let current_value,
1511
1603
  old_value;
1512
-
1513
- if (name.includes('.')) {
1514
- current_value = Object.path(this, name);
1515
- old_value = Object.path(this.$attributes.original_record, name);
1604
+
1605
+ if (untracked && untracked.has(name)) {
1606
+ result = false;
1516
1607
  } else {
1517
- current_value = this[name];
1518
- old_value = this.$attributes.original_record[name];
1519
- }
1520
1608
 
1521
- result = !Object.alike(old_value, current_value);
1609
+ if (name.includes('.')) {
1610
+ current_value = Object.path(this, name);
1611
+ old_value = Object.path(this.$attributes.original_record, name);
1612
+ } else {
1613
+ current_value = this[name];
1614
+ old_value = this.$attributes.original_record[name];
1615
+ }
1616
+
1617
+ result = !this.alikeWhenStored(old_value, current_value);
1618
+ }
1522
1619
  } else {
1523
1620
 
1524
1621
  let key;
1525
1622
 
1526
1623
  for (key in this.$attributes.original_record) {
1527
- if (!Object.alike(this.$attributes.original_record[key], this[key])) {
1528
- // @TODO: some special fields always end up being different
1624
+ if (untracked && untracked.has(key)) {
1625
+ continue;
1626
+ }
1627
+ if (!this.alikeWhenStored(this.$attributes.original_record[key], this[key])) {
1529
1628
  result = true;
1530
1629
  break;
1531
1630
  }
@@ -1533,7 +1632,10 @@ Document.setMethod(function hasChanged(name) {
1533
1632
 
1534
1633
  if (!result) {
1535
1634
  for (key in this.$main) {
1536
- if (!Object.alike(this.$main[key], this.$attributes.original_record[key])) {
1635
+ if (untracked && untracked.has(key)) {
1636
+ continue;
1637
+ }
1638
+ if (!this.alikeWhenStored(this.$main[key], this.$attributes.original_record[key])) {
1537
1639
  result = true;
1538
1640
  break;
1539
1641
  }
@@ -1892,20 +1892,6 @@ Model.setMethod(function saveRecord(document, options, callback) {
1892
1892
 
1893
1893
  creating = options.create || document.$pk == null;
1894
1894
  next();
1895
-
1896
- return;
1897
-
1898
- // Look through unique indexes if no _id is present
1899
- that.auditRecord(document, options, function afterAudit(err, doc) {
1900
-
1901
- if (err) {
1902
- return next(err);
1903
- }
1904
-
1905
- // Is a new record being created?
1906
- creating = options.create || doc.$pk == null;
1907
- next();
1908
- });
1909
1895
  }, function doBeforeNormalize(next) {
1910
1896
  // @TODO: make "beforeSave" only use promises
1911
1897
  that.issueDataEvent('beforeNormalize', [document, options], next);
@@ -1402,7 +1402,7 @@ Model.setMethod(function remove(id, callback) {
1402
1402
  *
1403
1403
  * @author Jelle De Loecker <jelle@elevenways.be>
1404
1404
  * @since 0.5.0
1405
- * @version 1.2.0
1405
+ * @version 1.4.5
1406
1406
  *
1407
1407
  * @param {Object} options Find options
1408
1408
  * @param {Function} task Task to perform on each record
@@ -1425,7 +1425,12 @@ Model.setMethod(function eachRecord(options, task, callback) {
1425
1425
  options = {};
1426
1426
  }
1427
1427
 
1428
- if (!callback) {
1428
+ if (callback) {
1429
+ // Errors are also delivered through the callback, and callers that
1430
+ // pass one routinely discard the returned pledge - its rejection
1431
+ // must not be reported as an uncaught error on top of the callback
1432
+ pledge.warn_uncaught_errors = false;
1433
+ } else {
1429
1434
  callback = Function.thrower;
1430
1435
  }
1431
1436
 
@@ -862,6 +862,10 @@ Schema.setMethod(function addField(name, type, options) {
862
862
 
863
863
  this.set(name, field);
864
864
 
865
+ // A new field can change which fields are excluded from change-detection, so
866
+ // drop the cached set (rebuilt lazily by getUntrackedFieldNames).
867
+ this.untracked_field_names = null;
868
+
865
869
  if (options.rules) {
866
870
  let rules = Array.cast(options.rules),
867
871
  i;
@@ -1074,6 +1078,50 @@ Schema.setMethod(function getFieldNames() {
1074
1078
  return Object.keys(this.dict);
1075
1079
  });
1076
1080
 
1081
+ /**
1082
+ * The set of field names excluded from change-detection, computed once and
1083
+ * cached. A field is excluded when `options.track_changes === false`, or - when
1084
+ * `track_changes` is unset - when it is a computed field (`is_computed`), since
1085
+ * its stored value is regenerated from its inputs and so is never independent
1086
+ * state worth (deep-)comparing in `Document#hasChanged()`. Lazily computed and
1087
+ * cached; `addField` clears the cache, so a later field addition is reflected.
1088
+ *
1089
+ * @author Jelle De Loecker <jelle@elevenways.be>
1090
+ * @since 1.4.4
1091
+ * @version 1.4.4
1092
+ *
1093
+ * @return {Set<string>}
1094
+ */
1095
+ Schema.setMethod(function getUntrackedFieldNames() {
1096
+
1097
+ if (this.untracked_field_names) {
1098
+ return this.untracked_field_names;
1099
+ }
1100
+
1101
+ let names = new Set();
1102
+
1103
+ for (let name of this.getFieldNames()) {
1104
+
1105
+ let field = this.getField(name);
1106
+
1107
+ if (!field || !field.options) {
1108
+ continue;
1109
+ }
1110
+
1111
+ let skip = (field.options.track_changes != null)
1112
+ ? (field.options.track_changes === false)
1113
+ : !!field.options.is_computed;
1114
+
1115
+ if (skip) {
1116
+ names.add(name);
1117
+ }
1118
+ }
1119
+
1120
+ this.untracked_field_names = names;
1121
+
1122
+ return names;
1123
+ });
1124
+
1077
1125
  /**
1078
1126
  * Add an index
1079
1127
  *
@@ -2439,6 +2439,36 @@ Alchemy.setMethod(function start(options, callback) {
2439
2439
  // Indicate the server is starting
2440
2440
  starting = true;
2441
2441
 
2442
+ // `--migrate`: apply pending migrations (app/migrations/) and exit, without
2443
+ // starting the HTTP server - so it can run alongside a live instance.
2444
+ if (this.argv.migrate) {
2445
+
2446
+ STAGES.afterStages('settings', async () => {
2447
+
2448
+ let code = 0;
2449
+
2450
+ try {
2451
+ await Classes.Alchemy.Migration.start();
2452
+ } catch (err) {
2453
+ log.error('Migration run failed:', err);
2454
+ code = 1;
2455
+ }
2456
+
2457
+ process.exit(code);
2458
+ });
2459
+
2460
+ STAGES.launch([
2461
+ 'load_app',
2462
+ 'datasource',
2463
+ 'tasks',
2464
+ 'settings',
2465
+ ]);
2466
+
2467
+ Blast.doLoaded();
2468
+
2469
+ return this.ready(callback);
2470
+ }
2471
+
2442
2472
  // Start the stages
2443
2473
  STAGES.launch([
2444
2474
  'load_app',
@@ -291,6 +291,13 @@ ClientBase.setMethod(function issueEvent(name, args, next) {
291
291
  args = [];
292
292
  }
293
293
 
294
+ if (next) {
295
+ // Errors are also delivered through `next`, and callers that pass a
296
+ // callback routinely discard the returned pledge - its rejection
297
+ // must not be reported as an uncaught error on top of the callback.
298
+ pledge.warn_uncaught_errors = false;
299
+ }
300
+
294
301
  if (this.constructor.event_to_method_map) {
295
302
  method_name = this.constructor.event_to_method_map.get(name);
296
303
  }
@@ -118,7 +118,17 @@ const hawkejs = routes.createStage('hawkejs', () => {
118
118
  return req.conduit.notFound('Could not find any of the given templates');
119
119
  }
120
120
 
121
- req.conduit.setHeader('cache-control', 'public, max-age=3600, must-revalidate');
121
+ // Only allow long-lived caching when the request is versioned
122
+ // with the CURRENT app version: unversioned (or stale-versioned)
123
+ // urls would keep serving an old template for up to an hour
124
+ // after a deploy.
125
+ let requested_version = req.conduit.param('v');
126
+
127
+ if (requested_version && String(requested_version) === String(alchemy.hawkejs.app_version)) {
128
+ req.conduit.setHeader('cache-control', 'public, max-age=3600, must-revalidate');
129
+ } else {
130
+ req.conduit.setHeader('cache-control', 'no-cache');
131
+ }
122
132
 
123
133
  // Don't use json dry, hawkejs expects regular json
124
134
  req.conduit.json_dry = false;
@@ -24,6 +24,78 @@
24
24
  */
25
25
  'use strict';
26
26
 
27
+ const fs = require('fs'),
28
+ child_process = require('child_process');
29
+
30
+ /**
31
+ * Check if a browser executable actually works in headless mode.
32
+ * Puppeteer's own bundled-Chrome download can be missing/corrupt in some
33
+ * sandboxes (e.g. a pinned old revision missing its ICU data file). Such a
34
+ * binary still exits 0 for `--version`, but hangs and never emits a CDP
35
+ * handshake when actually launched headless - which is what makes
36
+ * `puppeteer.launch()` hang until its connect-timeout instead of failing
37
+ * fast. So a real headless run is checked synchronously up front instead.
38
+ *
39
+ * @author Jelle De Loecker <jelle@elevenways.be>
40
+ * @since 1.4.5
41
+ * @version 1.4.5
42
+ *
43
+ * @param {string} executable_path
44
+ *
45
+ * @return {boolean}
46
+ */
47
+ function isUsableBrowser(executable_path) {
48
+
49
+ if (!executable_path || !fs.existsSync(executable_path)) {
50
+ return false;
51
+ }
52
+
53
+ let result = child_process.spawnSync(executable_path, [
54
+ '--headless',
55
+ '--disable-gpu',
56
+ '--no-sandbox',
57
+ '--dump-dom',
58
+ 'about:blank',
59
+ ], {timeout: 8000});
60
+
61
+ return result.status === 0 && !result.error;
62
+ }
63
+
64
+ /**
65
+ * Resolve which Chrome/Chromium executable to launch.
66
+ * Prefers `PUPPETEER_EXECUTABLE_PATH` (puppeteer already honors it, but we
67
+ * validate it here too) and puppeteer's own bundled download; falls back to
68
+ * common system-installed locations when neither actually works.
69
+ *
70
+ * @author Jelle De Loecker <jelle@elevenways.be>
71
+ * @since 1.4.5
72
+ * @version 1.4.5
73
+ *
74
+ * @param {Object} puppeteer
75
+ *
76
+ * @return {string|undefined}
77
+ */
78
+ function resolveExecutablePath(puppeteer) {
79
+
80
+ let candidates = [
81
+ process.env.PUPPETEER_EXECUTABLE_PATH,
82
+ puppeteer.executablePath(),
83
+ '/usr/bin/chromium',
84
+ '/usr/bin/chromium-browser',
85
+ '/usr/bin/google-chrome',
86
+ ];
87
+
88
+ for (let candidate of candidates) {
89
+ if (isUsableBrowser(candidate)) {
90
+ return candidate;
91
+ }
92
+ }
93
+
94
+ // Nothing usable found: let puppeteer try its own default and produce
95
+ // its normal error/timeout, rather than silently launching with none
96
+ return undefined;
97
+ }
98
+
27
99
  /**
28
100
  * The BrowserHelper class
29
101
  *
@@ -133,7 +205,8 @@ BrowserHelper.setMethod(async function load() {
133
205
  } else {
134
206
  // Launch new browser
135
207
  this.browser = await puppeteer.launch({
136
- headless: this.options.headless,
208
+ headless : this.options.headless,
209
+ executablePath : resolveExecutablePath(puppeteer),
137
210
  });
138
211
  }
139
212
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "alchemymvc",
3
3
  "description": "MVC framework for Node.js",
4
- "version": "1.4.3",
4
+ "version": "1.4.5",
5
5
  "author": "Jelle De Loecker <jelle@elevenways.be>",
6
6
  "keywords": [
7
7
  "alchemy",
@@ -22,7 +22,7 @@
22
22
  "chokidar" : "~4.0.3",
23
23
  "formidable" : "~3.5.4",
24
24
  "graceful-fs" : "~4.2.11",
25
- "hawkejs" : "~2.4.2",
25
+ "hawkejs" : "~2.4.3",
26
26
  "jsondiffpatch" : "~0.5.0",
27
27
  "mime-types" : "~3.0.2",
28
28
  "minimist" : "~1.2.8",
@@ -32,7 +32,7 @@
32
32
  "ncp" : "~2.0.0",
33
33
  "postcss" : "~8.5.6",
34
34
  "postcss-prune-var": "~1.1.2",
35
- "protoblast" : "~0.9.8",
35
+ "protoblast" : "~0.9.9",
36
36
  "semver" : "~7.7.2",
37
37
  "socket.io" : "~4.7.5",
38
38
  "@11ways/socket.io-stream" : "~0.9.2",
@@ -66,7 +66,7 @@
66
66
  "mocha" : "^11.7.5",
67
67
  "mongo-unit" : "^3.4.0",
68
68
  "nyc" : "^15.1.0",
69
- "puppeteer" : "~21.3.6",
69
+ "puppeteer" : "^24.43.1",
70
70
  "source-map" : "~0.7.3"
71
71
  },
72
72
  "scripts": {