zuzu-js 0.4.0 → 0.6.0

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.
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { createBrowserRuntime } = require( './browser-runtime' );
4
+ const { addBrowserModule } = require( './host/browser-host' );
4
5
  const {
5
6
  createBrowserGuiBridge,
6
7
  createBrowserGuiRenderer,
@@ -147,6 +148,7 @@ if (
147
148
  }
148
149
 
149
150
  module.exports = {
151
+ addModule: addBrowserModule,
150
152
  createBrowserRuntime: createConfiguredBrowserRuntime,
151
153
  createBrowserGuiBridge,
152
154
  createBrowserGuiRenderer,
package/lib/cli.js CHANGED
@@ -254,7 +254,7 @@ function stripShebang( source ) {
254
254
  }
255
255
 
256
256
  function printVersion( runtime, verbose ) {
257
- process.stdout.write( 'zuzu-js version 0.4.0\n' );
257
+ process.stdout.write( 'zuzu-js version 0.6.0\n' );
258
258
  if ( verbose ) {
259
259
  process.stdout.write( '\nlib search paths:\n' );
260
260
  for ( const p of runtime.getModuleSearchRoots() ) {
@@ -24,6 +24,10 @@ function makeSet( values ) {
24
24
  return runtimeHelpers().makeSet( values );
25
25
  }
26
26
 
27
+ function operatorString( value ) {
28
+ return runtimeHelpers().operatorString( value );
29
+ }
30
+
27
31
  function stringSortComparator( a, b ) {
28
32
  return runtimeHelpers().stringSortComparator( a, b );
29
33
  }
@@ -68,7 +72,15 @@ class ZuzuBag {
68
72
  return this;
69
73
  }
70
74
  push( ...values ) { return this.add( ...values ); }
71
- remove( value ) { return this.remove_first( value ); }
75
+ remove( value ) {
76
+ for ( let idx = this.items.length - 1; idx >= 0; idx-- ) {
77
+ if ( this.items[idx] === value ) {
78
+ releaseCollectionValue( this, this.items[idx] );
79
+ this.items.splice( idx, 1 );
80
+ }
81
+ }
82
+ return this;
83
+ }
72
84
  remove_first( value ) {
73
85
  const idx = this.items.indexOf( value );
74
86
  if ( idx >= 0 ) {
@@ -87,7 +99,6 @@ class ZuzuBag {
87
99
  } );
88
100
  return this;
89
101
  }
90
- get( idx, fallback = null ) { return idx >= 0 && idx < this.items.length ? this.items[idx] : fallback; }
91
102
  map( fn ) { return new ZuzuBag( this.items.map( fn ) ); }
92
103
  grep( fn ) { return new ZuzuBag( this.items.filter( fn ) ); }
93
104
  any( fn ) { return this.items.some( fn ) ? 1 : 0; }
@@ -193,6 +204,7 @@ class PairList {
193
204
  length() { return this.list.length; }
194
205
  count() { return this.list.length; }
195
206
  empty() { return this.list.length === 0 ? 1 : 0; }
207
+ is_empty() { return this.empty(); }
196
208
  keys() { return this.list.map( (pair) => pair[0] ); }
197
209
  values() { return this.list.map( (pair) => pair[1] ); }
198
210
  enumerate() { return this.to_Array(); }
@@ -279,6 +291,7 @@ Object.defineProperty( PairList.prototype, '__zuzu_builtin_type', {
279
291
  });
280
292
 
281
293
  function withArrayMethods() {
294
+ const nativeJoin = Array.prototype.join;
282
295
  const define = ( name, fn ) => {
283
296
  if ( !Object.prototype.hasOwnProperty.call( Array.prototype, name ) ) {
284
297
  const desc = Object.create( null );
@@ -287,6 +300,40 @@ function withArrayMethods() {
287
300
  Object.defineProperty( Array.prototype, name, desc );
288
301
  }
289
302
  };
303
+ if ( Array.prototype.join.__zuzu_array_join !== true ) {
304
+ const desc = Object.create( null );
305
+ desc.value = function _join( separator, fallback ) {
306
+ const hasFallback = arguments.length > 1;
307
+ let fallbackString;
308
+ const sep = operatorString( separator );
309
+ const out = [];
310
+ for ( const value of this ) {
311
+ try {
312
+ out.push( operatorString( value ) );
313
+ }
314
+ catch ( err ) {
315
+ if ( !hasFallback ) {
316
+ throw err;
317
+ }
318
+ if ( typeof fallback === 'function' ) {
319
+ out.push( operatorString( fallback( value ) ) );
320
+ }
321
+ else {
322
+ if ( fallbackString === undefined ) {
323
+ fallbackString = operatorString( fallback );
324
+ }
325
+ out.push( fallbackString );
326
+ }
327
+ }
328
+ }
329
+ return nativeJoin.call( out, sep );
330
+ };
331
+ desc.value.__zuzu_array_join = true;
332
+ desc.enumerable = false;
333
+ desc.configurable = true;
334
+ desc.writable = true;
335
+ Object.defineProperty( Array.prototype, 'join', desc );
336
+ }
290
337
  define( 'count', function _count() { return this.length; } );
291
338
  define( 'empty', function _empty() { return this.length === 0 ? 1 : 0; } );
292
339
  define( 'is_empty', function _is_empty() { return this.empty(); } );
@@ -48,6 +48,43 @@ function _sourceUrlComment( runOptions = {} ) {
48
48
  return `\n//# sourceURL=${filename.replace( /[\r\n]/gu, '_' )}`;
49
49
  }
50
50
 
51
+ const browserModuleUrls = new Map();
52
+
53
+ function _normalizeModuleName( value ) {
54
+ const name = String( value ?? '' ).trim().replace( /\\/gu, '/' );
55
+ if (
56
+ name === ''
57
+ || name.startsWith( '/' )
58
+ || name.endsWith( '/' )
59
+ || /(^|\/)\.\.?(\/|$)/u.test( name )
60
+ ) {
61
+ throw new Error( `Invalid browser module name: ${value}` );
62
+ }
63
+ return name.replace( /\.zzm$/u, '' );
64
+ }
65
+
66
+ function _moduleNameFromPath( filename ) {
67
+ const path = _resolvePath( filename );
68
+ if ( !path.startsWith( '/modules/' ) || !path.endsWith( '.zzm' ) ) {
69
+ return null;
70
+ }
71
+ return path.slice( '/modules/'.length, -'.zzm'.length );
72
+ }
73
+
74
+ function addBrowserModule( moduleName, url ) {
75
+ const logicalName = _normalizeModuleName( moduleName );
76
+ const moduleUrl = String( url ?? '' ).trim();
77
+ if ( moduleUrl === '' ) {
78
+ throw new Error( `Invalid URL for browser module '${logicalName}'` );
79
+ }
80
+ browserModuleUrls.set( logicalName, moduleUrl );
81
+ return logicalName;
82
+ }
83
+
84
+ function hasBrowserModules() {
85
+ return browserModuleUrls.size > 0;
86
+ }
87
+
51
88
  function _browserEval( source, context, runOptions = {} ) {
52
89
  context.globalThis = context;
53
90
  const sourceUrl = _sourceUrlComment( runOptions );
@@ -153,6 +190,18 @@ function createBrowserHost( options = {} ) {
153
190
  const jsModules = new Map( Object.entries( options.jsModules || {} )
154
191
  .map( ( [ filename, loaded ] ) => [ _resolvePath( filename ), loaded ] ) );
155
192
  const fetchedFiles = new Map();
193
+ const fetchingFiles = new Map();
194
+ const optionModuleUrls = new Map(
195
+ Object.entries( options.remoteModules || {} )
196
+ .map( ( [ name, url ] ) => [ _normalizeModuleName( name ), String( url ) ] )
197
+ );
198
+ const fetchText = typeof options.fetch === 'function'
199
+ ? options.fetch
200
+ : (
201
+ typeof fetch === 'function'
202
+ ? fetch
203
+ : null
204
+ );
156
205
  const fetchModule = typeof options.fetchModule === 'function'
157
206
  ? options.fetchModule
158
207
  : null;
@@ -193,6 +242,58 @@ function createBrowserHost( options = {} ) {
193
242
  capabilities.add( 'worker' );
194
243
  }
195
244
  const gui = _defaultGuiBridge( options );
245
+ function browserModuleUrlForPath( filename ) {
246
+ const logicalName = _moduleNameFromPath( filename );
247
+ if ( !logicalName ) {
248
+ return null;
249
+ }
250
+ if ( optionModuleUrls.has( logicalName ) ) {
251
+ return optionModuleUrls.get( logicalName );
252
+ }
253
+ return browserModuleUrls.get( logicalName ) || null;
254
+ }
255
+ function hasAsyncModules() {
256
+ return optionModuleUrls.size > 0 || browserModuleUrls.size > 0;
257
+ }
258
+ async function fetchBrowserModule( key, url ) {
259
+ if ( fetchedFiles.has( key ) ) {
260
+ return fetchedFiles.get( key );
261
+ }
262
+ if ( fetchingFiles.has( key ) ) {
263
+ return fetchingFiles.get( key );
264
+ }
265
+ if ( !fetchText ) {
266
+ throw new Error( `Exception: Browser host cannot fetch module '${key}'` );
267
+ }
268
+ const pending = Promise.resolve( fetchText( url ) )
269
+ .then( async (response) => {
270
+ if ( typeof response === 'string' ) {
271
+ return response;
272
+ }
273
+ if ( response && response.ok === false ) {
274
+ throw new Error(
275
+ `Exception: Fetch failed for browser module '${key}' (${response.status})`
276
+ );
277
+ }
278
+ if ( !response || typeof response.text !== 'function' ) {
279
+ throw new Error(
280
+ `Exception: Fetch did not return text for browser module '${key}'`
281
+ );
282
+ }
283
+ return response.text();
284
+ } )
285
+ .then( (source) => {
286
+ const text = String( source );
287
+ fetchedFiles.set( key, text );
288
+ fetchingFiles.delete( key );
289
+ return text;
290
+ }, (err) => {
291
+ fetchingFiles.delete( key );
292
+ throw err;
293
+ } );
294
+ fetchingFiles.set( key, pending );
295
+ return pending;
296
+ }
196
297
  return {
197
298
  name: 'browser',
198
299
  repoRoot,
@@ -229,13 +330,33 @@ function createBrowserHost( options = {} ) {
229
330
  return fetched;
230
331
  }
231
332
  }
333
+ if ( browserModuleUrlForPath( key ) ) {
334
+ throw new Error( `Exception: Browser module '${filename}' requires asynchronous loading` );
335
+ }
232
336
  throw new Error( `Exception: Browser host cannot read file '${filename}'` );
233
337
  },
338
+ async readFileTextAsync( filename ) {
339
+ const key = _resolvePath( filename );
340
+ if ( virtualFiles.has( key ) ) {
341
+ return virtualFiles.get( key );
342
+ }
343
+ if ( fetchedFiles.has( key ) ) {
344
+ return fetchedFiles.get( key );
345
+ }
346
+ const browserModuleUrl = browserModuleUrlForPath( key );
347
+ if ( browserModuleUrl ) {
348
+ return fetchBrowserModule( key, browserModuleUrl );
349
+ }
350
+ return this.readFileText( key );
351
+ },
234
352
  fileExists( filename ) {
235
353
  const key = _resolvePath( filename );
236
354
  if ( virtualFiles.has( key ) || jsModules.has( key ) || fetchedFiles.has( key ) ) {
237
355
  return true;
238
356
  }
357
+ if ( browserModuleUrlForPath( key ) ) {
358
+ return true;
359
+ }
239
360
  if ( fetchModule ) {
240
361
  const fetched = fetchModule( key );
241
362
  if ( typeof fetched === 'string' ) {
@@ -270,9 +391,12 @@ function createBrowserHost( options = {} ) {
270
391
  }
271
392
  return jsModules.get( key );
272
393
  },
394
+ hasAsyncModules,
273
395
  };
274
396
  }
275
397
 
276
398
  module.exports = {
399
+ addBrowserModule,
277
400
  createBrowserHost,
401
+ hasBrowserModules,
278
402
  };
@@ -143,6 +143,15 @@ function retainValue( value ) {
143
143
  return resolved;
144
144
  }
145
145
 
146
+ function strongReferenceCount( value ) {
147
+ const resolved = resolveWeakValue( value );
148
+ if ( !isWeakableValue( resolved ) ) {
149
+ return 0;
150
+ }
151
+ const state = weakStates.get( resolved );
152
+ return state ? state.strong : 0;
153
+ }
154
+
146
155
  function retainedValuesForCollection( collection, create = false ) {
147
156
  const resolved = resolveWeakValue( collection );
148
157
  if ( !isWeakableValue( resolved ) ) {
@@ -198,7 +207,7 @@ function releaseCollectionValues( collection ) {
198
207
  return null;
199
208
  }
200
209
 
201
- function releaseValue( value ) {
210
+ function releaseValue( value, options = {} ) {
202
211
  if ( isWeakCell( value ) ) {
203
212
  return null;
204
213
  }
@@ -209,7 +218,15 @@ function releaseValue( value ) {
209
218
  const wasStrong = state.strong;
210
219
  state.strong = Math.max( 0, state.strong - 1 );
211
220
  if ( wasStrong > 0 && state.strong === 0 ) {
212
- releaseCollectionValues( resolved );
221
+ if ( !options.preserveCollectionValues ) {
222
+ releaseCollectionValues( resolved );
223
+ }
224
+ if (
225
+ !options.suppressDemolish
226
+ && typeof resolved.__demolish__ === 'function'
227
+ ) {
228
+ resolved.__demolish__();
229
+ }
213
230
  }
214
231
  }
215
232
  }
@@ -571,6 +588,9 @@ function buildComparator( name ) {
571
588
  if ( name === 'eq' ) {
572
589
  return ( left, right ) => String( left ) === String( right );
573
590
  }
591
+ if ( name === 'eqi' ) {
592
+ return ( left, right ) => String( left ).toLowerCase() === String( right ).toLowerCase();
593
+ }
574
594
  if ( name === '~' ) {
575
595
  return ( left, right ) => {
576
596
  if ( right && typeof right.test === 'function' ) {
@@ -582,11 +602,21 @@ function buildComparator( name ) {
582
602
  return ( left, right ) => left == right;
583
603
  }
584
604
 
605
+ function switchTruthy( value ) {
606
+ return !!value;
607
+ }
608
+
585
609
  function runSwitch( value, cmpName, cases, defaultBody ) {
586
610
  const cmp = buildComparator( cmpName );
587
611
  let runNext = false;
588
612
  for ( const section of cases ) {
589
- const matched = section.values.some( (item) => cmp( value, item ) );
613
+ let matched = false;
614
+ if ( Array.isArray( section.tests ) ) {
615
+ matched = section.tests.some( (test) => switchTruthy( test( value ) ) );
616
+ }
617
+ else {
618
+ matched = section.values.some( (item) => cmp( value, item ) );
619
+ }
590
620
  if ( matched || runNext ) {
591
621
  const result = section.body();
592
622
  runNext = result === true;
@@ -961,6 +991,7 @@ module.exports = {
961
991
  resolveWeakValue,
962
992
  retainValue,
963
993
  releaseValue,
994
+ strongReferenceCount,
964
995
  retainCollectionValue,
965
996
  releaseCollectionValue,
966
997
  releaseCollectionValues,