importmap-rails 0.7.1 → 0.7.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +6 -6
- data/app/assets/javascripts/es-module-shims.js +106 -97
- data/lib/importmap/commands.rb +1 -0
- data/lib/importmap/map.rb +2 -3
- data/lib/importmap/version.rb +1 -1
- data/lib/tasks/importmap_tasks.rake +0 -6
- metadata +2 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38c3083184e97d6df1566c84b82011280e8ebc5b68d59daeac2eff60b8264bf6
|
4
|
+
data.tar.gz: 705e1989000dbae9d04e18555de12ad968f3fa557a65de7ca5eb87f6adbdf335
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9a7181a347eb789eb72d3f14d76a11004d1683c5e9dc7e8ddb9810a37445ad66f392105874c4a663ac939df55052f9184e33200d7ec27b458bf176de3ad225e5
|
7
|
+
data.tar.gz: 994fefc659e8b26a50ac3382f16c14436f2aa7343d4d9c6a7679b0044dafa5a3dac082a2818a4c9ee41ed6922b52a4d9e4668771d605763e34f92d8b6e687230
|
data/README.md
CHANGED
@@ -48,7 +48,7 @@ Pinning "scheduler" to https://ga.jspm.io/npm:scheduler@0.20.2/index.js
|
|
48
48
|
|
49
49
|
{
|
50
50
|
"imports": {
|
51
|
-
"application": "/application.js",
|
51
|
+
"application": "/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js",
|
52
52
|
"react": "https://ga.jspm.io/npm:react@17.0.2/index.js",
|
53
53
|
"react-dom": "https://ga.jspm.io/npm:react-dom@17.0.2/index.js",
|
54
54
|
"object-assign": "https://ga.jspm.io/npm:object-assign@4.1.1/index.js",
|
@@ -169,9 +169,9 @@ pin_all_from File.expand_path("../app/assets/javascripts", __dir__)
|
|
169
169
|
```
|
170
170
|
|
171
171
|
|
172
|
-
## Include a digest of the import map in your
|
172
|
+
## Include a digest of the import map in your ETag
|
173
173
|
|
174
|
-
If you're using
|
174
|
+
If you're using [ETags](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) generated by Rails helpers like `stale?` or `fresh_when`, you need to include the digest of the import map into this calculation. Otherwise your application will return 302 cache responses even when your JavaScript assets have changed. You can avoid this with something like:
|
175
175
|
|
176
176
|
```ruby
|
177
177
|
class ApplicationController < ActionController::Base
|
@@ -182,7 +182,7 @@ end
|
|
182
182
|
|
183
183
|
## Sweeping the cache in development and test
|
184
184
|
|
185
|
-
Generating the import map json and modulepreloads may require resolving hundreds of assets. This can take a while, so these operations are cached, but in development and test, we watch for changes to both `config/importmap.rb` and files in `app/javascript` to clear this cache. This feature can be controlled in an environment configuration file via the boolean `config.importmap.sweep_cache`. If you're pinning local files from outside of `app/javascript`, you'll need to restart your development server upon changes.
|
185
|
+
Generating the import map json and modulepreloads may require resolving hundreds of assets. This can take a while, so these operations are cached, but in development and test, we watch for changes to both `config/importmap.rb` and files in `app/javascript` to clear this cache. This feature can be controlled in an environment configuration file via the boolean `config.importmap.sweep_cache`. If you're pinning local files from outside of `app/javascript`, you'll need to restart your development server upon changes to those external files.
|
186
186
|
|
187
187
|
|
188
188
|
## Expected errors from using the es-module-shim
|
@@ -192,12 +192,12 @@ While import maps are native in Chrome and Edge, they need a shim in other brows
|
|
192
192
|
|
193
193
|
## Turning off the shim
|
194
194
|
|
195
|
-
Under certain circumstances, like running system tests using chromedriver under CI (which may be resource constrained and trigger errors in certain cases), you may want to explicitly turn off including the shim.
|
195
|
+
Under certain circumstances, like running system tests using chromedriver under CI (which may be resource constrained and trigger errors in certain cases), you may want to explicitly turn off including the shim. You can do this by calling the bulk tag helper with `javascript_importmap_tags("application", shim: false)`. Thus you can pass in something like `shim: !ENV["CI"]`. If you want, and are sure you're not doing any full-page caching, you can also connect this directive to a user agent check (using a gem like `useragent`) to check whether the browser is chrome/edge 89+. But you really shouldn't have to, as the shim is designed to gracefully work with natively compatible drivers.
|
196
196
|
|
197
197
|
|
198
198
|
## A note about browser extensions
|
199
199
|
|
200
|
-
Certain extensions that also load
|
200
|
+
Certain extensions that also load JavaScript modules may block import maps from being loaded (for instance, the Apollo Client Devtools extension). If you see a console message like `An import map is added after module script load was triggered`, browser extensions are likely the culprit.
|
201
201
|
|
202
202
|
|
203
203
|
## License
|
@@ -1,8 +1,6 @@
|
|
1
|
-
/* ES Module Shims
|
1
|
+
/* ES Module Shims 1.0.4 */
|
2
2
|
(function () {
|
3
3
|
|
4
|
-
Promise.resolve();
|
5
|
-
|
6
4
|
const edge = navigator.userAgent.match(/Edge\/\d\d\.\d+$/);
|
7
5
|
|
8
6
|
let baseUrl;
|
@@ -203,7 +201,7 @@
|
|
203
201
|
let shimMode = !!esmsInitOptions$1.shimMode;
|
204
202
|
const resolveHook = shimMode && esmsInitOptions$1.resolve;
|
205
203
|
|
206
|
-
const skip = esmsInitOptions$1.skip ? new RegExp(esmsInitOptions$1.skip) :
|
204
|
+
const skip = esmsInitOptions$1.skip ? new RegExp(esmsInitOptions$1.skip) : null;
|
207
205
|
|
208
206
|
let nonce = esmsInitOptions$1.nonce;
|
209
207
|
|
@@ -331,7 +329,35 @@
|
|
331
329
|
|
332
330
|
let importMap = { imports: {}, scopes: {} };
|
333
331
|
let importMapSrcOrLazy = false;
|
334
|
-
let
|
332
|
+
let baselinePassthrough;
|
333
|
+
|
334
|
+
const initPromise = featureDetectionPromise.then(() => {
|
335
|
+
baselinePassthrough = supportsDynamicImport && supportsImportMeta && supportsImportMaps && (!jsonModulesEnabled || supportsJsonAssertions) && (!cssModulesEnabled || supportsCssAssertions) && !importMapSrcOrLazy && !false;
|
336
|
+
// shim mode is determined on initialization, no late shim mode
|
337
|
+
if (!shimMode && document.querySelectorAll('script[type="module-shim"],script[type="importmap-shim"]').length)
|
338
|
+
setShimMode();
|
339
|
+
if (shimMode || !baselinePassthrough) {
|
340
|
+
new MutationObserver(mutations => {
|
341
|
+
for (const mutation of mutations) {
|
342
|
+
if (mutation.type !== 'childList') continue;
|
343
|
+
for (const node of mutation.addedNodes) {
|
344
|
+
if (node.tagName === 'SCRIPT') {
|
345
|
+
if (!shimMode && node.type === 'module' || shimMode && node.type === 'module-shim')
|
346
|
+
processScript(node);
|
347
|
+
if (!shimMode && node.type === 'importmap' || shimMode && node.type === 'importmap-shim')
|
348
|
+
processImportMap(node);
|
349
|
+
}
|
350
|
+
else if (node.tagName === 'LINK' && node.rel === 'modulepreload')
|
351
|
+
processPreload(node);
|
352
|
+
}
|
353
|
+
}
|
354
|
+
}).observe(document, { childList: true, subtree: true });
|
355
|
+
processImportMaps();
|
356
|
+
processScriptsAndPreloads();
|
357
|
+
return undefined;
|
358
|
+
}
|
359
|
+
});
|
360
|
+
let importMapPromise = initPromise;
|
335
361
|
|
336
362
|
let acceptingImportMaps = true;
|
337
363
|
let nativeAcceptingImportMaps = true;
|
@@ -346,21 +372,20 @@
|
|
346
372
|
}
|
347
373
|
await importMapPromise;
|
348
374
|
// early analysis opt-out - no need to even fetch if we have feature support
|
349
|
-
if (!shimMode &&
|
375
|
+
if (!shimMode && baselinePassthrough) {
|
350
376
|
// for polyfill case, only dynamic import needs a return value here, and dynamic import will never pass nativelyLoaded
|
351
377
|
if (nativelyLoaded)
|
352
378
|
return null;
|
353
379
|
await lastStaticLoadPromise;
|
354
380
|
return dynamicImport(source ? createBlob(source) : url, { errUrl: url || source });
|
355
381
|
}
|
356
|
-
await undefined;
|
357
382
|
const load = getOrCreateLoad(url, fetchOpts, source);
|
358
383
|
const seen = {};
|
359
384
|
await loadAll(load, seen);
|
360
385
|
lastLoad = undefined;
|
361
386
|
resolveDeps(load, seen);
|
362
387
|
await lastStaticLoadPromise;
|
363
|
-
if (source && !shimMode && !load.n) {
|
388
|
+
if (source && !shimMode && !load.n && !false) {
|
364
389
|
const module = await dynamicImport(createBlob(source), { errUrl: source });
|
365
390
|
if (revokeBlobURLs) revokeObjectURLs(Object.keys(seen));
|
366
391
|
return module;
|
@@ -393,7 +418,17 @@
|
|
393
418
|
}
|
394
419
|
|
395
420
|
async function importShim (id, parentUrl = baseUrl, _assertion) {
|
396
|
-
|
421
|
+
// needed for shim check
|
422
|
+
await initPromise;
|
423
|
+
if (acceptingImportMaps || shimMode || !baselinePassthrough) {
|
424
|
+
processImportMaps();
|
425
|
+
if (!shimMode) {
|
426
|
+
acceptingImportMaps = false;
|
427
|
+
}
|
428
|
+
else {
|
429
|
+
nativeAcceptingImportMaps = false;
|
430
|
+
}
|
431
|
+
}
|
397
432
|
await importMapPromise;
|
398
433
|
return topLevelLoad((await resolve(id, parentUrl)).r || throwUnresolved(id, parentUrl), { credentials: 'same-origin' });
|
399
434
|
}
|
@@ -403,7 +438,6 @@
|
|
403
438
|
const meta = {};
|
404
439
|
|
405
440
|
async function importMetaResolve (id, parentUrl = this.url) {
|
406
|
-
await importMapPromise;
|
407
441
|
return (await resolve(id, `${parentUrl}`)).r || throwUnresolved(id, parentUrl);
|
408
442
|
}
|
409
443
|
|
@@ -490,17 +524,11 @@
|
|
490
524
|
resolvedSource += source.slice(lastIndex);
|
491
525
|
}
|
492
526
|
|
493
|
-
resolvedSource = resolvedSource.replace(/\/\/# sourceMappingURL=(.*)\s*$/, (match, url) =>
|
494
|
-
return match.replace(url, new URL(url, load.r));
|
495
|
-
});
|
527
|
+
resolvedSource = resolvedSource.replace(/\/\/# sourceMappingURL=(.*)\s*$/, (match, url) => match.replace(url, () => new URL(url, load.r)));
|
496
528
|
let hasSourceURL = false;
|
497
|
-
resolvedSource = resolvedSource.replace(/\/\/# sourceURL=(.*)\s*$/, (match, url) =>
|
498
|
-
|
499
|
-
return match.replace(url, new URL(url, load.r));
|
500
|
-
});
|
501
|
-
if (!hasSourceURL) {
|
529
|
+
resolvedSource = resolvedSource.replace(/\/\/# sourceURL=(.*)\s*$/, (match, url) => (hasSourceURL = true, match.replace(url, () => new URL(url, load.r))));
|
530
|
+
if (!hasSourceURL)
|
502
531
|
resolvedSource += '\n//# sourceURL=' + load.r;
|
503
|
-
}
|
504
532
|
|
505
533
|
load.b = lastLoad = createBlob(resolvedSource);
|
506
534
|
load.S = undefined;
|
@@ -617,7 +645,7 @@
|
|
617
645
|
if (d !== -1) return;
|
618
646
|
if (!r)
|
619
647
|
throwUnresolved(n, load.r || load.u);
|
620
|
-
if (skip.test(r)) return { b: r };
|
648
|
+
if (skip && skip.test(r)) return { b: r };
|
621
649
|
if (childFetchOpts.integrity)
|
622
650
|
childFetchOpts = Object.assign({}, childFetchOpts, { integrity: undefined });
|
623
651
|
return getOrCreateLoad(r, childFetchOpts).f;
|
@@ -627,22 +655,16 @@
|
|
627
655
|
return load;
|
628
656
|
}
|
629
657
|
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
function processScripts () {
|
634
|
-
for (const link of document.querySelectorAll(preloadQuery))
|
635
|
-
processPreload(link);
|
636
|
-
const scripts = document.querySelectorAll(scriptQuery);
|
637
|
-
// early shim mode opt-in
|
638
|
-
if (!shimMode) {
|
639
|
-
for (const script of scripts) {
|
640
|
-
if (script.type.endsWith('-shim'))
|
641
|
-
setShimMode();
|
642
|
-
}
|
643
|
-
}
|
644
|
-
for (const script of scripts)
|
658
|
+
function processScriptsAndPreloads () {
|
659
|
+
for (const script of document.querySelectorAll(shimMode ? 'script[type="module-shim"]' : 'script[type="module"]'))
|
645
660
|
processScript(script);
|
661
|
+
for (const link of document.querySelectorAll('link[rel="modulepreload"]'))
|
662
|
+
processPreload(link);
|
663
|
+
}
|
664
|
+
|
665
|
+
function processImportMaps () {
|
666
|
+
for (const script of document.querySelectorAll(shimMode ? 'script[type="importmap-shim"]' : 'script[type="importmap"]'))
|
667
|
+
processImportMap(script);
|
646
668
|
}
|
647
669
|
|
648
670
|
function getFetchOpts (script) {
|
@@ -668,73 +690,74 @@
|
|
668
690
|
document.dispatchEvent(new Event('DOMContentLoaded'));
|
669
691
|
}
|
670
692
|
// this should always trigger because we assume es-module-shims is itself a domcontentloaded requirement
|
671
|
-
document.addEventListener('DOMContentLoaded',
|
693
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
694
|
+
await initPromise;
|
695
|
+
domContentLoadedCheck();
|
696
|
+
if (shimMode || !baselinePassthrough) {
|
697
|
+
processImportMaps();
|
698
|
+
processScriptsAndPreloads();
|
699
|
+
}
|
700
|
+
});
|
672
701
|
|
673
702
|
let readyStateCompleteCnt = 1;
|
674
703
|
if (document.readyState === 'complete')
|
675
704
|
readyStateCompleteCheck();
|
676
705
|
else
|
677
|
-
document.addEventListener('readystatechange',
|
706
|
+
document.addEventListener('readystatechange', async () => {
|
707
|
+
await initPromise;
|
708
|
+
readyStateCompleteCheck();
|
709
|
+
});
|
678
710
|
function readyStateCompleteCheck () {
|
679
711
|
if (--readyStateCompleteCnt === 0 && !noLoadEventRetriggers)
|
680
712
|
document.dispatchEvent(new Event('readystatechange'));
|
681
713
|
}
|
682
714
|
|
683
|
-
function
|
684
|
-
if (
|
715
|
+
function processImportMap (script) {
|
716
|
+
if (!acceptingImportMaps)
|
685
717
|
return;
|
686
|
-
|
687
|
-
if (shim && !shimMode) setShimMode();
|
688
|
-
const type = shimMode ? script.type.slice(0, -5) : script.type;
|
689
|
-
// dont process module scripts in shim mode or noshim module scripts in polyfill mode
|
690
|
-
if (!shim && shimMode || script.getAttribute('noshim') !== null)
|
718
|
+
if (script.ep) // ep marker = script processed
|
691
719
|
return;
|
692
720
|
// empty inline scripts sometimes show before domready
|
693
721
|
if (!script.src && !script.innerHTML)
|
694
722
|
return;
|
695
723
|
script.ep = true;
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
if (isReadyScript) readyStateCompleteCnt++;
|
702
|
-
if (isDomContentLoadedScript) domContentLoadedCnt++;
|
703
|
-
const loadPromise = topLevelLoad(script.src || `${baseUrl}?${id++}`, getFetchOpts(script), !script.src && script.innerHTML, !shimMode, isReadyScript && lastStaticLoadPromise).then(() => {
|
704
|
-
if (!noLoadEventRetriggers)
|
705
|
-
triggerLoadEvent(script);
|
706
|
-
}).catch(e => {
|
707
|
-
if (!noLoadEventRetriggers)
|
708
|
-
triggerLoadEvent(script);
|
709
|
-
// setTimeout(() => { throw e; });
|
710
|
-
onerror(e);
|
711
|
-
});
|
712
|
-
if (isReadyScript)
|
713
|
-
lastStaticLoadPromise = loadPromise.then(readyStateCompleteCheck);
|
714
|
-
if (isDomContentLoadedScript)
|
715
|
-
loadPromise.then(domContentLoadedCheck);
|
724
|
+
// we dont currently support multiple, external or dynamic imports maps in polyfill mode to match native
|
725
|
+
if (script.src || !nativeAcceptingImportMaps) {
|
726
|
+
if (!shimMode)
|
727
|
+
return;
|
728
|
+
importMapSrcOrLazy = true;
|
716
729
|
}
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
importMapSrcOrLazy = true;
|
723
|
-
}
|
724
|
-
if (!shimMode) {
|
725
|
-
acceptingImportMaps = false;
|
726
|
-
}
|
727
|
-
else {
|
728
|
-
nativeAcceptingImportMaps = false;
|
729
|
-
}
|
730
|
-
importMapPromise = importMapPromise.then(async () => {
|
731
|
-
importMap = resolveAndComposeImportMap(script.src ? await (await fetchHook(script.src)).json() : JSON.parse(script.innerHTML), script.src || baseUrl, importMap);
|
732
|
-
});
|
730
|
+
if (!shimMode) {
|
731
|
+
acceptingImportMaps = false;
|
732
|
+
}
|
733
|
+
else {
|
734
|
+
nativeAcceptingImportMaps = false;
|
733
735
|
}
|
736
|
+
importMapPromise = importMapPromise.then(async () => {
|
737
|
+
importMap = resolveAndComposeImportMap(script.src ? await (await fetchHook(script.src)).json() : JSON.parse(script.innerHTML), script.src || baseUrl, importMap);
|
738
|
+
});
|
734
739
|
}
|
735
740
|
|
736
|
-
function
|
737
|
-
script.
|
741
|
+
function processScript (script) {
|
742
|
+
if (script.ep) // ep marker = script processed
|
743
|
+
return;
|
744
|
+
if (script.getAttribute('noshim') !== null)
|
745
|
+
return;
|
746
|
+
// empty inline scripts sometimes show before domready
|
747
|
+
if (!script.src && !script.innerHTML)
|
748
|
+
return;
|
749
|
+
script.ep = true;
|
750
|
+
// does this load block readystate complete
|
751
|
+
const isReadyScript = readyStateCompleteCnt > 0;
|
752
|
+
// does this load block DOMContentLoaded
|
753
|
+
const isDomContentLoadedScript = domContentLoadedCnt > 0;
|
754
|
+
if (isReadyScript) readyStateCompleteCnt++;
|
755
|
+
if (isDomContentLoadedScript) domContentLoadedCnt++;
|
756
|
+
const loadPromise = topLevelLoad(script.src || `${baseUrl}?${id++}`, getFetchOpts(script), !script.src && script.innerHTML, !shimMode, isReadyScript && lastStaticLoadPromise).catch(onerror);
|
757
|
+
if (isReadyScript)
|
758
|
+
lastStaticLoadPromise = loadPromise.then(readyStateCompleteCheck);
|
759
|
+
if (isDomContentLoadedScript)
|
760
|
+
loadPromise.then(domContentLoadedCheck);
|
738
761
|
}
|
739
762
|
|
740
763
|
const fetchCache = {};
|
@@ -747,22 +770,8 @@
|
|
747
770
|
fetchCache[link.href] = doFetch(link.href, getFetchOpts(link));
|
748
771
|
}
|
749
772
|
|
750
|
-
new MutationObserver(mutations => {
|
751
|
-
for (const mutation of mutations) {
|
752
|
-
if (mutation.type !== 'childList') continue;
|
753
|
-
for (const node of mutation.addedNodes) {
|
754
|
-
if (node.tagName === 'SCRIPT' && node.type)
|
755
|
-
processScript(node);
|
756
|
-
else if (node.tagName === 'LINK' && node.rel === 'modulepreload')
|
757
|
-
processPreload(node);
|
758
|
-
}
|
759
|
-
}
|
760
|
-
}).observe(document, { childList: true, subtree: true });
|
761
|
-
|
762
773
|
function throwUnresolved (id, parentUrl) {
|
763
774
|
throw Error("Unable to resolve specifier '" + id + (parentUrl ? "' from " + parentUrl : "'"));
|
764
775
|
}
|
765
776
|
|
766
|
-
|
767
|
-
|
768
|
-
}());
|
777
|
+
}());
|
data/lib/importmap/commands.rb
CHANGED
data/lib/importmap/map.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require "pathname"
|
2
|
-
require "active_support/evented_file_update_checker"
|
3
2
|
|
4
3
|
class Importmap::Map
|
5
4
|
attr_reader :packages, :directories
|
@@ -63,11 +62,11 @@ class Importmap::Map
|
|
63
62
|
def cache_sweeper(watches: nil)
|
64
63
|
if watches
|
65
64
|
@cache_sweeper =
|
66
|
-
|
65
|
+
Rails.application.config.file_watcher.new([], Array(watches).collect { |dir| [ dir.to_s, "js"] }.to_h) do
|
67
66
|
clear_cache
|
68
67
|
end
|
69
68
|
else
|
70
|
-
@cache_sweeper
|
69
|
+
@cache_sweeper
|
71
70
|
end
|
72
71
|
end
|
73
72
|
|
data/lib/importmap/version.rb
CHANGED
@@ -3,10 +3,4 @@ namespace :importmap do
|
|
3
3
|
task :install do
|
4
4
|
system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../install/install.rb", __dir__)}"
|
5
5
|
end
|
6
|
-
|
7
|
-
desc "Show the importmap"
|
8
|
-
task :pins do
|
9
|
-
require Rails.root.join("config/environment")
|
10
|
-
puts Rails.application.importmap.to_json(resolver: ActionController::Base.helpers)
|
11
|
-
end
|
12
6
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: importmap-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Heinemeier Hansson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-09-
|
11
|
+
date: 2021-09-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -24,20 +24,6 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 6.0.0
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: listen
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '3.7'
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '3.7'
|
41
27
|
description:
|
42
28
|
email: david@loudthinking.com
|
43
29
|
executables: []
|