importmap-rails 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b87e83df8630c47a117ee7a9a854a8d34bd65112e547c27a5ae9133fb62f3c8a
4
- data.tar.gz: b6c77a31b534440c7a6a3132e1f88d14f658ed350fcd95d446bba9d58df11bc5
3
+ metadata.gz: 8e3127a8154739158fd8b7feea069b5ae6e8d353d36ba16fa446095f26b70ae0
4
+ data.tar.gz: c6d7e65df38a98e5c31b5195ee298651c6550cb97e3c438ebf46c8a5290a17b6
5
5
  SHA512:
6
- metadata.gz: 40f0955bde9ab6792c5b1e0b5f0fa4eed8701aa09f2b71c69dd1fe1f6b088131d2dd0089e4bd68b14172e4042a49959a7de609adc9f711bbd6e819552ec6fb11
7
- data.tar.gz: 471bc782818dc139c3a46fb8c1896784069b8d8c8ff777cdc2880d462f5efa207021292079d7b9de127af58cd916901153f8a7981944fc52e81ad80b418907fb
6
+ metadata.gz: 2e186a8e9785fc7da247ddf1c459d005977a8ce21fb2403aa3e3b63c2b938b9ffa682df1742292633dd9afcae0f7a7f68582af2cafc642afd04195564fd5c2a1
7
+ data.tar.gz: fc85e86b9d747e9e84e8e009bcca8e4746f6477f25d6355b705958fb917818872005127c5ad9ac8e9f9f7594d1ac06b68013038ab351cd072d45c0d4dc76b7ce
data/README.md CHANGED
@@ -22,30 +22,49 @@ Note: In order to use JavaScript from Rails frameworks like Action Cable, Action
22
22
 
23
23
  The import map is configured programmatically through the `Rails.application.config.importmap` assignment, which by default is setup in `config/importmap.rb` after running the installer. This file is automatically reloaded in development upon changes, but note that you must restart the server if you remove pins and need them gone from the rendered importmap or list of preloads.
24
24
 
25
- This programmatically configured import map is inlined in the `<head>` of your application layout using `<%= javascript_importmap_tags %>`, which will setup the JSON configuration inside a `<script type="importmap">` tag. After that, the [es-module-shim](https://github.com/guybedford/es-module-shims) is loaded, and then finally the application entrypoint is imported via `<script type="module">import "application"</script>`. That logical entrypoint, `application`, is mapped in the importmap script tag to the file `app/assets/javascripts/application.js`, which is copied and digested by the asset pipeline.
25
+ This programmatically configured import map is inlined in the `<head>` of your application layout using `<%= javascript_importmap_tags %>`, which will setup the JSON configuration inside a `<script type="importmap">` tag. After that, the [es-module-shim](https://github.com/guybedford/es-module-shims) is loaded, and then finally the application entrypoint is imported via `<script type="module">import "application"</script>`. That logical entrypoint, `application`, is mapped in the importmap script tag to the file `app/javascript/application.js`, which is copied and digested by the asset pipeline.
26
26
 
27
- It's in `app/assets/javascripts/application.js` you setup your application by importing any of the modules that have been defined in the import map. You can use the full ESM functionality of importing any particular export of the modules or everything.
27
+ It's in `app/javascript/application.js` you setup your application by importing any of the modules that have been defined in the import map. You can use the full ESM functionality of importing any particular export of the modules or everything.
28
28
 
29
29
  It makes sense to use logical names that match the package names used by NPM, such that if you later want to start transpiling or bundling your code, you'll not have to change any module imports.
30
30
 
31
31
 
32
- ## Use with Skypack (and other CDNs)
32
+ ## Using node modules via JavaScript CDNs
33
33
 
34
- Instead of mapping JavaScript modules to files in your application's path, you can also reference them directly from JavaScript CDNs like Skypack. Simply add them to the `config/importmap.rb` with the URL instead of the local path:
34
+ Importmap for Rails is designed to be used with JavaScript CDNs for your node package dependencies. The CDNs provide pre-compiled distribution versions ready to use, and offer a fast, efficient way of serving them.
35
35
 
36
- ```ruby
37
- Rails.application.config.importmap.draw do
38
- pin "trix", to: "https://cdn.skypack.dev/trix"
39
- pin "md5", to: "https://cdn.skypack.dev/md5"
40
- end
36
+ You can use the bin/importmap command that's added as part of the install to pin additional packages to your importmap. This command uses an API from JSPM.org to resolve your package dependencies most efficiently, and then add the pins to your config/importmap.rb file. It can resolve these dependencies from JSPM itself, but also from other CDNs, like unpkg.com, jsdelivr.com, skypack.dev, etc.
37
+
38
+ It works like so:
39
+
40
+ ```bash
41
+ ./bin/importmap pin react react-dom
42
+
43
+ Pinned 'react' to https://ga.jspm.io/npm:react@17.0.2/index.js
44
+ Pinned 'react-dom' to https://ga.jspm.io/npm:react-dom@17.0.2/index.js
45
+ Pinned 'object-assign' to https://ga.jspm.io/npm:object-assign@4.1.1/index.js
46
+ Pinned 'scheduler' to https://ga.jspm.io/npm:scheduler@0.20.2/index.js
47
+
48
+ ./bin/importmap json
49
+
50
+ {
51
+ "imports": {
52
+ "application": "/application.js",
53
+ "react": "https://ga.jspm.io/npm:react@17.0.2/index.js",
54
+ "react-dom": "https://ga.jspm.io/npm:react-dom@17.0.2/index.js",
55
+ "object-assign": "https://ga.jspm.io/npm:object-assign@4.1.1/index.js",
56
+ "scheduler": "https://ga.jspm.io/npm:scheduler@0.20.2/index.js"
57
+ }
58
+ }
41
59
  ```
42
60
 
61
+ As you can see, the two packages react and react-dom resolve to a total of four dependencies, when resolved via the jspm default.
62
+
43
63
  Now you can use these in your application.js entrypoint like you would any other module:
44
64
 
45
65
  ```js
46
- import "trix"
47
- import md5 from "md5"
48
- console.log(md5("Hash it out"))
66
+ import React from "react"
67
+ import ReactDOM from "react-dom"
49
68
  ```
50
69
 
51
70
 
@@ -57,16 +76,14 @@ Example:
57
76
 
58
77
  ```ruby
59
78
  # config/importmap.rb
60
- Rails.application.config.importmap.draw do
61
- pin "trix", to: "https://cdn.skypack.dev/trix"
62
- pin "md5", to: "https://cdn.skypack.dev/md5", preload: false
63
- end
79
+ pin "@github/hotkey", to: "https://ga.jspm.io/npm:@github/hotkey@1.4.4/dist/index.js"
80
+ pin "md5", to: "https://cdn.jsdelivr.net/npm/md5@2.3.0/md5.js", preload: false
64
81
 
65
82
  # app/views/layouts/application.html.erb
66
83
  <%= javascript_importmap_tags %>
67
84
 
68
85
  # will include the following link before the importmap is setup:
69
- <link rel="modulepreload" href="https://cdn.skypack.dev/trix">
86
+ <link rel="modulepreload" href="https://ga.jspm.io/npm:@github/hotkey@1.4.4/dist/index.js">
70
87
  ...
71
88
  ```
72
89
 
@@ -1 +1 @@
1
- //= require ./es-module-shims@0.12.2.js
1
+ //= require ./es-module-shims@0.12.8.js
@@ -1,6 +1,6 @@
1
- /* ES Module Shims 0.12.2 */
1
+ /* ES Module Shims 0.12.8 */
2
2
  (function () {
3
- // Temporary hack around https://github.com/guybedford/es-module-shims/issues/148
3
+ // Bail on all shimming for Chrome until https://github.com/guybedford/es-module-shims/issues/150
4
4
  if (navigator.userAgent.match("Chrome")) return
5
5
 
6
6
  const resolvedPromise = Promise.resolve();
@@ -16,6 +16,7 @@
16
16
  // support browsers without dynamic import support (eg Firefox 6x)
17
17
  let supportsDynamicImport = false;
18
18
  let supportsJsonAssertions = false;
19
+ let supportsCssAssertions = false;
19
20
  let dynamicImport;
20
21
  try {
21
22
  dynamicImport = (0, eval)('u=>import(u)');
@@ -53,6 +54,7 @@
53
54
  let supportsImportMaps = false;
54
55
 
55
56
  const featureDetectionPromise = Promise.all([
57
+ dynamicImport(createBlob('import"data:text/css,{}"assert{type:"css"}')).then(() => supportsCssAssertions = true, () => {}),
56
58
  dynamicImport(createBlob('import"data:text/json,{}"assert{type:"json"}')).then(() => supportsJsonAssertions = true, () => {}),
57
59
  dynamicImport(createBlob('import.meta')).then(() => supportsImportMeta = true, () => {}),
58
60
  supportsDynamicImport && hasDocument && new Promise(resolve => {
@@ -263,9 +265,13 @@
263
265
  load.n = load.d.some(dep => dep.n);
264
266
  }
265
267
 
268
+ let importMap = { imports: {}, scopes: {} };
269
+ let importMapSrcOrLazy = false;
270
+ let importMapPromise = resolvedPromise;
271
+
266
272
  let waitingForImportMapsInterval;
267
273
  let firstTopLevelProcess = true;
268
- async function topLevelLoad (url, fetchOpts, source, nativelyLoaded) {
274
+ async function topLevelLoad (url, fetchOpts, source, nativelyLoaded, lastStaticLoadPromise) {
269
275
  // no need to even fetch if we have feature support
270
276
  await featureDetectionPromise;
271
277
  if (waitingForImportMapsInterval > 0) {
@@ -278,8 +284,8 @@
278
284
  }
279
285
  await importMapPromise;
280
286
  // early analysis opt-out
281
- if (nativelyLoaded && supportsDynamicImport && supportsImportMeta && supportsImportMaps && !importMapSrcOrLazy) {
282
- // dont reexec inline for polyfills -> just return null
287
+ if (nativelyLoaded && supportsDynamicImport && supportsImportMeta && supportsImportMaps && supportsJsonAssertions && supportsCssAssertions && !importMapSrcOrLazy) {
288
+ // dont reexec inline for polyfills -> just return null (since no module id for executed inline module scripts)
283
289
  return source && nativelyLoaded ? null : dynamicImport(source ? createBlob(source) : url);
284
290
  }
285
291
  await init;
@@ -288,17 +294,30 @@
288
294
  await loadAll(load, seen);
289
295
  lastLoad = undefined;
290
296
  resolveDeps(load, seen);
291
- if (source && !nativelyLoaded && !shimMode && !load.n) {
297
+ await lastStaticLoadPromise;
298
+ if (source && !shimMode && !load.n) {
299
+ if (lastStaticLoadPromise) {
300
+ didExecForReadyPromise = true;
301
+ if (domContentLoaded)
302
+ didExecForDomContentLoaded = true;
303
+ }
292
304
  const module = dynamicImport(createBlob(source));
293
305
  if (shouldRevokeBlobURLs) revokeObjectURLs(Object.keys(seen));
294
306
  return module;
295
307
  }
296
308
  const module = await dynamicImport(load.b);
309
+ if (lastStaticLoadPromise && (!nativelyLoaded || load.b !== load.u)) {
310
+ didExecForReadyPromise = true;
311
+ if (domContentLoaded)
312
+ didExecForDomContentLoaded = true;
313
+ }
297
314
  // if the top-level load is a shell, run its update function
298
315
  if (load.s) {
299
316
  (await dynamicImport(load.s)).u$_(module);
300
317
  }
301
318
  if (shouldRevokeBlobURLs) revokeObjectURLs(Object.keys(seen));
319
+ // when tla is supported, this should return the tla promise as an actual handle
320
+ // so readystate can still correspond to the sync subgraph exec completions
302
321
  return module;
303
322
  }
304
323
 
@@ -323,7 +342,8 @@
323
342
  await featureDetectionPromise;
324
343
  // Make sure all the "in-flight" import maps are loaded and applied.
325
344
  await importMapPromise;
326
- return topLevelLoad(resolve(id, parentUrl).r || throwUnresolved(id, parentUrl), { credentials: 'same-origin' });
345
+ const resolved = await resolve(id, parentUrl);
346
+ return topLevelLoad(resolved.r || throwUnresolved(id, parentUrl), { credentials: 'same-origin' });
327
347
  }
328
348
 
329
349
  self.importShim = importShim;
@@ -334,7 +354,8 @@
334
354
 
335
355
  async function importMetaResolve (id, parentUrl = this.url) {
336
356
  await importMapPromise;
337
- return resolve(id, `${parentUrl}`).r || throwUnresolved(id, parentUrl);
357
+ const resolved = await resolve(id, `${parentUrl}`);
358
+ return resolved.r || throwUnresolved(id, parentUrl);
338
359
  }
339
360
 
340
361
  self._esmsm = meta;
@@ -346,6 +367,7 @@
346
367
  const skip = esmsInitOptions.skip || /^https?:\/\/(cdn\.skypack\.dev|jspm\.dev)\//;
347
368
  const onerror = esmsInitOptions.onerror || ((e) => { throw e; });
348
369
  const shouldRevokeBlobURLs = esmsInitOptions.revokeBlobURLs;
370
+ const noLoadEventRetriggers = esmsInitOptions.noLoadEventRetriggers;
349
371
 
350
372
  function urlJsString (url) {
351
373
  return `'${url.replace(/'/g, "\\'")}'`;
@@ -374,7 +396,7 @@
374
396
  const source = load.S;
375
397
 
376
398
  // edge doesnt execute sibling in order, so we fix this up by ensuring all previous executions are explicit dependencies
377
- let resolvedSource = edge && lastLoad ? `import '${lastLoad}';` : '';
399
+ let resolvedSource = edge && lastLoad ? `import '${lastLoad}';` : '';
378
400
 
379
401
  if (!imports.length) {
380
402
  resolvedSource += source;
@@ -449,7 +471,26 @@
449
471
  const cssContentType = /^text\/css(;|$)/;
450
472
  const wasmContentType = /^application\/wasm(;|$)/;
451
473
 
452
- const fetchOptsMap = new Map();
474
+ const cssUrlRegEx = /url\(\s*(?:(["'])((?:\\.|[^\n\\"'])+)\1|((?:\\.|[^\s,"'()\\])+))\s*\)/g;
475
+
476
+ async function doFetch (url, fetchOpts) {
477
+ const res = await fetchHook(url, fetchOpts);
478
+ if (!res.ok)
479
+ throw new Error(`${res.status} ${res.statusText} ${res.url}`);
480
+ const contentType = res.headers.get('content-type');
481
+ if (jsContentType.test(contentType))
482
+ return { r: res.url, s: await res.text(), t: 'js' };
483
+ else if (jsonContentType.test(contentType))
484
+ return { r: res.url, s: `export default ${await res.text()}`, t: 'json' };
485
+ else if (cssContentType.test(contentType))
486
+ return { r: res.url, s: `var s=new CSSStyleSheet();s.replaceSync(${
487
+ JSON.stringify((await res.text()).replace(cssUrlRegEx, (_match, quotes, relUrl1, relUrl2) => `url(${quotes}${resolveUrl(relUrl1 || relUrl2, url)}${quotes})`))
488
+ });export default s;`, t: 'css' };
489
+ else if (wasmContentType.test(contentType))
490
+ throw new Error('WASM modules not yet supported');
491
+ else
492
+ throw new Error(`Unknown Content-Type "${contentType}"`);
493
+ }
453
494
 
454
495
  function getOrCreateLoad (url, fetchOpts, source) {
455
496
  let load = registry[url];
@@ -476,27 +517,18 @@
476
517
  // shellUrl
477
518
  s: undefined,
478
519
  // needsShim
479
- n: false
520
+ n: false,
521
+ // type
522
+ t: null
480
523
  };
481
524
 
482
525
  load.f = (async () => {
483
526
  if (!source) {
484
527
  // preload fetch options override fetch options (race)
485
- const res = await fetchHook(url, fetchOptsMap.get(url) || fetchOpts);
486
- if (!res.ok)
487
- throw new Error(`${res.status} ${res.statusText} ${res.url}`);
488
- load.r = res.url;
489
- const contentType = res.headers.get('content-type');
490
- if (jsContentType.test(contentType))
491
- source = await res.text();
492
- else if (jsonContentType.test(contentType))
493
- source = `export default ${await res.text()}`;
494
- else if (cssContentType.test(contentType))
495
- throw new Error('CSS modules not yet supported');
496
- else if (wasmContentType.test(contentType))
497
- throw new Error('WASM modules not yet supported');
498
- else
499
- throw new Error(`Unknown Content-Type "${contentType}"`);
528
+ let t;
529
+ ({ r: load.r, s: source, t } = await (fetchCache[url] || doFetch(url, fetchOpts)));
530
+ if (t === 'css' && !supportsCssAssertions || t === 'json' && !supportsJsonAssertions)
531
+ load.n = true;
500
532
  }
501
533
  try {
502
534
  load.a = parse(source, load.u);
@@ -511,13 +543,12 @@
511
543
 
512
544
  load.L = load.f.then(async () => {
513
545
  let childFetchOpts = fetchOpts;
514
- load.d = await Promise.all(load.a[0].map(({ n, d, a }) => {
546
+ load.d = (await Promise.all(load.a[0].map(async ({ n, d, a }) => {
515
547
  if (d >= 0 && !supportsDynamicImport ||
516
- d === 2 && (!supportsImportMeta || source.slice(end, end + 8) === '.resolve') ||
517
- a && !supportsJsonAssertions)
548
+ d === 2 && (!supportsImportMeta || source.slice(end, end + 8) === '.resolve'))
518
549
  load.n = true;
519
550
  if (!n) return;
520
- const { r, m } = resolve(n, load.r || load.u);
551
+ const { r, m } = await resolve(n, load.r || load.u);
521
552
  if (m && (!supportsImportMaps || importMapSrcOrLazy))
522
553
  load.n = true;
523
554
  if (d !== -1) return;
@@ -527,22 +558,13 @@
527
558
  if (childFetchOpts.integrity)
528
559
  childFetchOpts = Object.assign({}, childFetchOpts, { integrity: undefined });
529
560
  return getOrCreateLoad(r, childFetchOpts).f;
530
- }).filter(l => l));
561
+ }))).filter(l => l);
531
562
  });
532
563
 
533
564
  return load;
534
565
  }
535
566
 
536
- let importMap = { imports: {}, scopes: {} };
537
- let importMapSrcOrLazy = false;
538
- let importMapPromise = resolvedPromise;
539
-
540
- if (hasDocument) {
541
- processScripts();
542
- waitingForImportMapsInterval = setInterval(processScripts, 20);
543
- }
544
-
545
- async function processScripts () {
567
+ function processScripts () {
546
568
  if (waitingForImportMapsInterval > 0 && document.readyState !== 'loading') {
547
569
  clearTimeout(waitingForImportMapsInterval);
548
570
  waitingForImportMapsInterval = 0;
@@ -550,29 +572,9 @@
550
572
  for (const link of document.querySelectorAll('link[rel="modulepreload"]'))
551
573
  processPreload(link);
552
574
  for (const script of document.querySelectorAll('script[type="module-shim"],script[type="importmap-shim"],script[type="module"],script[type="importmap"]'))
553
- await processScript(script);
575
+ processScript(script);
554
576
  }
555
577
 
556
- new MutationObserver(mutations => {
557
- for (const mutation of mutations) {
558
- if (mutation.type !== 'childList') continue;
559
- for (const node of mutation.addedNodes) {
560
- if (node.tagName === 'SCRIPT' && node.type)
561
- processScript(node, !firstTopLevelProcess);
562
- else if (node.tagName === 'LINK' && node.rel === 'modulepreload')
563
- processPreload(node);
564
- else if (node.querySelectorAll) {
565
- for (const script of node.querySelectorAll('script[type="module-shim"],script[type="importmap-shim"],script[type="module"],script[type="importmap"]')) {
566
- processScript(script, !firstTopLevelProcess);
567
- }
568
- for (const link of node.querySelectorAll('link[rel=modulepreload]')) {
569
- processPreload(link);
570
- }
571
- }
572
- }
573
- }
574
- }).observe(document, { childList: true, subtree: true });
575
-
576
578
  function getFetchOpts (script) {
577
579
  const fetchOpts = {};
578
580
  if (script.integrity)
@@ -588,12 +590,29 @@
588
590
  return fetchOpts;
589
591
  }
590
592
 
591
- async function processScript (script, dynamic) {
593
+ let staticLoadCnt = 0;
594
+ let didExecForReadyPromise = false;
595
+ let didExecForDomContentLoaded = false;
596
+ let lastStaticLoadPromise = Promise.resolve();
597
+ let domContentLoaded = false;
598
+ document.addEventListener('DOMContentLoaded', () => domContentLoaded = true);
599
+ function staticLoadCheck () {
600
+ staticLoadCnt--;
601
+ if (staticLoadCnt === 0 && !noLoadEventRetriggers) {
602
+ if (didExecForDomContentLoaded)
603
+ document.dispatchEvent(new Event('DOMContentLoaded'));
604
+ if (didExecForReadyPromise && document.readyState === 'complete')
605
+ document.dispatchEvent(new Event('readystatechange'));
606
+ }
607
+ }
608
+
609
+ function processScript (script, dynamic) {
592
610
  if (script.ep) // ep marker = script processed
593
611
  return;
594
612
  const shim = script.type.endsWith('-shim');
595
613
  if (shim) shimMode = true;
596
- const type = shim ? script.type.slice(0, -5) : script.type;
614
+ const type = shimMode ? script.type.slice(0, -5) : script.type;
615
+ // dont process module scripts in shim mode or noshim module scripts in polyfill mode
597
616
  if (!shim && shimMode || script.getAttribute('noshim') !== null)
598
617
  return;
599
618
  // empty inline scripts sometimes show before domready
@@ -601,7 +620,14 @@
601
620
  return;
602
621
  script.ep = true;
603
622
  if (type === 'module') {
604
- await topLevelLoad(script.src || `${baseUrl}?${id++}`, getFetchOpts(script), !script.src && script.innerHTML, !shim).catch(onerror);
623
+ const isReadyScript = document.readyState !== 'complete';
624
+ if (isReadyScript) staticLoadCnt++;
625
+ const p = topLevelLoad(script.src || `${baseUrl}?${id++}`, getFetchOpts(script), !script.src && script.innerHTML, !shimMode, isReadyScript && lastStaticLoadPromise);
626
+ p.catch(onerror);
627
+ if (isReadyScript) {
628
+ lastStaticLoadPromise = p.catch(staticLoadCheck);
629
+ p.then(staticLoadCheck);
630
+ }
605
631
  }
606
632
  else if (type === 'importmap') {
607
633
  importMapPromise = importMapPromise.then(async () => {
@@ -612,20 +638,51 @@
612
638
  }
613
639
  }
614
640
 
641
+ const fetchCache = {};
615
642
  function processPreload (link) {
616
643
  if (link.ep) // ep marker = processed
617
644
  return;
618
645
  link.ep = true;
619
- // prepopulate the load record
620
- const fetchOpts = getFetchOpts(link);
621
- // save preloaded fetch options for later load
622
- fetchOptsMap.set(link.href, fetchOpts);
623
- fetch(link.href, fetchOpts);
646
+ if (fetchCache[link.href])
647
+ return;
648
+ fetchCache[link.href] = doFetch(link.href, getFetchOpts(link));
624
649
  }
625
650
 
626
- function resolve (id, parentUrl) {
627
- const urlResolved = resolveIfNotPlainOrUrl(id, parentUrl);
628
- const resolved = resolveImportMap(importMap, urlResolved || id, parentUrl);
651
+ new MutationObserver(mutations => {
652
+ for (const mutation of mutations) {
653
+ if (mutation.type !== 'childList') continue;
654
+ for (const node of mutation.addedNodes) {
655
+ if (node.tagName === 'SCRIPT' && node.type)
656
+ processScript(node, !firstTopLevelProcess);
657
+ else if (node.tagName === 'LINK' && node.rel === 'modulepreload')
658
+ processPreload(node);
659
+ else if (node.querySelectorAll) {
660
+ for (const script of node.querySelectorAll('script[type="module-shim"],script[type="importmap-shim"],script[type="module"],script[type="importmap"]')) {
661
+ processScript(script, !firstTopLevelProcess);
662
+ }
663
+ for (const link of node.querySelectorAll('link[rel=modulepreload]')) {
664
+ processPreload(link);
665
+ }
666
+ }
667
+ }
668
+ }
669
+ }).observe(document, { childList: true, subtree: true });
670
+
671
+ async function defaultResolve (id, parentUrl) {
672
+ return resolveImportMap(importMap, resolveIfNotPlainOrUrl(id, parentUrl) || id, parentUrl);
673
+ }
674
+
675
+ async function resolve (id, parentUrl) {
676
+ let urlResolved = resolveIfNotPlainOrUrl(id, parentUrl);
677
+
678
+ let resolved;
679
+ if (esmsInitOptions.resolve) {
680
+ resolved = await esmsInitOptions.resolve(id, parentUrl, defaultResolve);
681
+ }
682
+ else {
683
+ resolved = resolveImportMap(importMap, urlResolved || id, parentUrl);
684
+ }
685
+
629
686
  return { r: resolved, m: urlResolved !== resolved };
630
687
  }
631
688
 
@@ -633,4 +690,9 @@
633
690
  throw Error("Unable to resolve specifier '" + id + (parentUrl ? "' from " + parentUrl : "'"));
634
691
  }
635
692
 
693
+ if (hasDocument) {
694
+ processScripts();
695
+ waitingForImportMapsInterval = setInterval(processScripts, 20);
696
+ }
697
+
636
698
  }());
@@ -0,0 +1,24 @@
1
+ require "thor"
2
+ require "importmap/pinner"
3
+
4
+ class Importmap::Commands < Thor
5
+ desc "pin [*PACKAGES]", "Pin new packages"
6
+ option :env, type: :string, aliases: :e, default: "production"
7
+ option :from, type: :string, aliases: :f, default: "jspm"
8
+ def pin(*packages)
9
+ if imports = Importmap::Pinner.new.pin(*packages, env: options[:env], from: options[:from])
10
+ imports.each do |package, url|
11
+ puts "Pinned '#{package}' to #{url}"
12
+ end
13
+ else
14
+ puts "Couldn't find any packages in #{packages.inspect} on #{options[:provider]}"
15
+ end
16
+ end
17
+
18
+ desc "json", "Show the full importmap in json"
19
+ def json
20
+ puts Rails.application.config.importmap.to_json(resolver: ActionController::Base.helpers)
21
+ end
22
+ end
23
+
24
+ Importmap::Commands.start(ARGV)
@@ -2,19 +2,16 @@ require "importmap/map"
2
2
 
3
3
  module Importmap
4
4
  class Engine < ::Rails::Engine
5
- config.importmap = Importmap::Map.new
6
-
5
+ config.importmap = Importmap::Map.new.draw("config/importmap.rb")
7
6
  config.autoload_once_paths = %W( #{root}/app/helpers )
8
7
 
9
8
  initializer "importmap.reloader" do |app|
10
9
  app.config.paths.add "config/importmap.rb"
11
10
 
12
- reloader = Importmap::Reloader.new
13
-
14
- reloader.execute
15
- app.reloaders << reloader
16
- app.reloader.to_run do
11
+ Importmap::Reloader.new.tap do |reloader|
17
12
  reloader.execute
13
+ app.reloaders << reloader
14
+ app.reloader.to_run { reloader.execute }
18
15
  end
19
16
  end
20
17
 
data/lib/importmap/map.rb CHANGED
@@ -1,17 +1,30 @@
1
+ require "pathname"
2
+
1
3
  class Importmap::Map
2
- attr_reader :files, :directories
4
+ attr_reader :packages, :directories
3
5
  attr_accessor :cached
4
6
 
5
7
  def initialize
6
- @files, @directories = {}, {}
8
+ @packages, @directories = {}, {}
7
9
  end
8
10
 
9
- def draw(&block)
10
- instance_eval(&block)
11
+ def draw(path = nil, &block)
12
+ if path
13
+ begin
14
+ instance_eval(File.read(path))
15
+ rescue Exception => e
16
+ Rails.logger.error "Unable to parse import map from #{path}: #{e.message}"
17
+ raise "Unable to parse import map from #{path}: #{e.message}"
18
+ end
19
+ else
20
+ instance_eval(&block)
21
+ end
22
+
23
+ self
11
24
  end
12
25
 
13
26
  def pin(name, to: nil, preload: true)
14
- @files[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload)
27
+ @packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload)
15
28
  end
16
29
 
17
30
  def pin_all_from(dir, under: nil, to: nil, preload: true)
@@ -20,19 +33,19 @@ class Importmap::Map
20
33
 
21
34
  def preloaded_module_paths(resolver:)
22
35
  cache_as(:preloaded_module_paths) do
23
- resolve_asset_paths(expanded_preloading_files_and_directories, resolver: resolver).values
36
+ resolve_asset_paths(expanded_preloading_packages_and_directories, resolver: resolver).values
24
37
  end
25
38
  end
26
39
 
27
40
  def to_json(resolver:)
28
41
  cache_as(:json) do
29
- { "imports" => resolve_asset_paths(expanded_files_and_directories, resolver: resolver) }.to_json
42
+ JSON.pretty_generate({ "imports" => resolve_asset_paths(expanded_packages_and_directories, resolver: resolver) })
30
43
  end
31
44
  end
32
45
 
33
46
  private
34
- MappedFile = Struct.new(:name, :path, :preload, keyword_init: true)
35
47
  MappedDir = Struct.new(:dir, :path, :under, :preload, keyword_init: true)
48
+ MappedFile = Struct.new(:name, :path, :preload, keyword_init: true)
36
49
 
37
50
  def cache_as(name)
38
51
  if (cached && result = instance_variable_get("@cached_#{name}"))
@@ -53,12 +66,12 @@ class Importmap::Map
53
66
  end.compact
54
67
  end
55
68
 
56
- def expanded_preloading_files_and_directories
57
- expanded_files_and_directories.select { |name, mapping| mapping.preload }
69
+ def expanded_preloading_packages_and_directories
70
+ expanded_packages_and_directories.select { |name, mapping| mapping.preload }
58
71
  end
59
72
 
60
- def expanded_files_and_directories
61
- @files.dup.tap { |expanded| expand_directories_into expanded }
73
+ def expanded_packages_and_directories
74
+ @packages.dup.tap { |expanded| expand_directories_into expanded }
62
75
  end
63
76
 
64
77
  def expand_directories_into(paths)
@@ -80,7 +93,7 @@ class Importmap::Map
80
93
  end
81
94
 
82
95
  def module_path_from(filename, mapping)
83
- [ mapping.path || mapping.under, filename.to_s ].join("/")
96
+ [ mapping.path || mapping.under, filename.to_s ].compact.join("/")
84
97
  end
85
98
 
86
99
  def find_javascript_files_in_tree(path)
@@ -0,0 +1,32 @@
1
+ require "httparty"
2
+
3
+ class Importmap::Pinner
4
+ include HTTParty
5
+ base_uri "https://api.jspm.io"
6
+
7
+ def pin(*packages, env: "production", from: "jspm")
8
+ fetch_imports(*packages, env: env, provider: from)&.tap do |imports|
9
+ imports.each do |package, url|
10
+ append_to_importmap package, url
11
+ end
12
+ end
13
+ end
14
+
15
+ private
16
+ def append_to_importmap(package, url)
17
+ Rails.root.join("config/importmap.rb").open("a") do |config|
18
+ config.puts %(pin "#{package}", to: "#{url}")
19
+ end
20
+ end
21
+
22
+ def fetch_imports(*packages, env:, provider:)
23
+ response = self.class.post("/generate", body: {
24
+ "install" => Array(packages),
25
+ "flattenScope" => true,
26
+ "env" => [ "browser", env ],
27
+ "provider" => provider.to_s
28
+ }.to_json)
29
+
30
+ response.dig("map", "imports")
31
+ end
32
+ end
@@ -2,7 +2,7 @@ class Importmap::Reloader
2
2
  delegate :execute_if_updated, :execute, :updated?, to: :updater
3
3
 
4
4
  def reload!
5
- import_map_paths.each { |path| load path }
5
+ import_map_paths.each { |path| config.importmap.draw(path) }
6
6
  end
7
7
 
8
8
  private
@@ -1,3 +1,3 @@
1
1
  module Importmap
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../config/application"
4
+ require "importmap/commands"
@@ -0,0 +1,20 @@
1
+ # Use Action Cable channels (remember to import "@rails/actionable" in your application.js)
2
+ # pin "@rails/actioncable", to: "actioncable.esm.js"
3
+ # pin_all_from "app/javascript/channels", under: "channels"
4
+
5
+ # Use direct uploads for Active Storage (remember to import "@rails/activestorage" in your application.js)
6
+ # pin "@rails/activestorage", to: "activestorage.esm.js"
7
+
8
+ # Pin vendored modules by first adding the following to app/assets/config/manifest.js:
9
+ # //= link_tree ../../../vendor/assets/javascripts .js
10
+ # pin_all_from "vendor/assets/javascripts"
11
+
12
+ # JavaScript CDN provider. Also available: :jsdelivr, :esmsh, :unpkg, :skypack
13
+ provider :jspm
14
+
15
+ # Use NPM libraries from CDN
16
+ # pin "local-time", version: "2.1.0", file: "app/assets/javascripts/local-time.js"
17
+ # pin "vue", version: "2.6.14", file: "dist/vue.esm.browser.js", from: :jsdelivr
18
+ # pin "d3", version: "7.0.1", file: "?bundle", from: :esmsh
19
+
20
+ pin "application"
@@ -10,10 +10,7 @@ end
10
10
 
11
11
  say "Create application.js module as entrypoint"
12
12
  create_file Rails.root.join("app/javascript/application.js") do <<-JS
13
- // Configure your import map in config/importmap.rb
14
-
15
- // import "@rails/actioncable"
16
- // import "@rails/activestorage"
13
+ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
17
14
  JS
18
15
  end
19
16
 
@@ -21,21 +18,8 @@ say "Ensure JavaScript files are in the asset pipeline manifest"
21
18
  append_to_file Rails.root.join("app/assets/config/manifest.js"), %(//= link_tree ../../javascript .js\n)
22
19
 
23
20
  say "Configure importmap paths in config/importmap.rb"
24
- create_file Rails.root.join("config/importmap.rb") do <<-RUBY
25
- Rails.application.config.importmap.draw do
26
- pin "application"
27
-
28
- # Use libraries available via the asset pipeline (locally or via gems).
29
- # pin "@rails/actioncable", to: "actioncable.esm.js"
30
- # pin "@rails/activestorage", to: "activestorage.esm.js"
31
-
32
- # Use libraries directly from JavaScript CDNs (see https://www.skypack.dev, https://esm.sh, https://www.jsdelivr.com/esm)
33
- # pin "vue", to: "https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js"
34
- # pin "d3", to: "https://esm.sh/d3?bundle"
21
+ copy_file "#{__dir__}/config/importmap.rb", "config/importmap.rb"
35
22
 
36
- # Pin vendored modules by first adding the following to app/assets/config/manifest.js:
37
- # //= link_tree ../../../vendor/assets/javascripts .js
38
- # pin_all_from "vendor/assets/javascripts"
39
- end
40
- RUBY
41
- end
23
+ say "Copying binstub"
24
+ copy_file "#{__dir__}/bin/importmap", "bin/importmap"
25
+ chmod "bin", 0755 & ~File.umask, verbose: false
@@ -3,4 +3,10 @@ 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.config.importmap.to_json(resolver: ActionController::Base.helpers)
11
+ end
6
12
  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.3.1
4
+ version: 0.4.0
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-08-22 00:00:00.000000000 Z
11
+ date: 2021-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 6.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: httparty
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0.16'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0.16'
27
41
  description:
28
42
  email: david@loudthinking.com
29
43
  executables: []
@@ -34,13 +48,17 @@ files:
34
48
  - README.md
35
49
  - Rakefile
36
50
  - app/assets/javascripts/es-module-shims.js
37
- - app/assets/javascripts/es-module-shims@0.12.2.js
51
+ - app/assets/javascripts/es-module-shims@0.12.8.js
38
52
  - app/helpers/importmap/importmap_tags_helper.rb
39
53
  - lib/importmap-rails.rb
54
+ - lib/importmap/commands.rb
40
55
  - lib/importmap/engine.rb
41
56
  - lib/importmap/map.rb
57
+ - lib/importmap/pinner.rb
42
58
  - lib/importmap/reloader.rb
43
59
  - lib/importmap/version.rb
60
+ - lib/install/bin/importmap
61
+ - lib/install/config/importmap.rb
44
62
  - lib/install/install.rb
45
63
  - lib/shim.js
46
64
  - lib/tasks/importmap_tasks.rake