@nubisco/ui 4.1.0 → 4.1.2

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.
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import Components from 'unplugin-vue-components/vite';
5
5
  import { COMPONENT_MANIFEST } from '../../components/manifest.js';
6
6
  import { nubiscoGlyphs } from './glyphs.js';
7
+ import { nubiscoImports } from './imports.js';
7
8
  /** dist/plugins/vite/index.js → dist */
8
9
  const defaultDistRoot = () => path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
9
10
  /** Which stylesheets each component needs, recorded by the library's build. */
@@ -19,6 +20,7 @@ export function nubiscoUI(options = {}) {
19
20
  const { components = true, glyphs = true, styles = true } = options;
20
21
  const componentOptions = typeof components === 'object' ? components : {};
21
22
  const plugins = [];
23
+ let resolvedComponents = 0;
22
24
  if (glyphs !== false) {
23
25
  plugins.push(nubiscoGlyphs(typeof glyphs === 'object' ? glyphs : {}));
24
26
  }
@@ -27,6 +29,15 @@ export function nubiscoUI(options = {}) {
27
29
  const styleManifest = styles
28
30
  ? loadStyleManifest(options.distRoot ?? defaultDistRoot())
29
31
  : {};
32
+ plugins.push(nubiscoImports({
33
+ manifest: lookup,
34
+ styleManifest,
35
+ styles,
36
+ packageName: '@nubisco/ui',
37
+ onResolve: (count) => {
38
+ resolvedComponents += count;
39
+ },
40
+ }));
30
41
  plugins.push(Components({
31
42
  dirs: componentOptions.dirs ?? [],
32
43
  dts: componentOptions.dts ?? 'components.d.ts',
@@ -35,6 +46,7 @@ export function nubiscoUI(options = {}) {
35
46
  const file = lookup.get(name);
36
47
  if (!file)
37
48
  return;
49
+ resolvedComponents += 1;
38
50
  const sheets = styleManifest[file] ?? [];
39
51
  return {
40
52
  name,
@@ -44,6 +56,29 @@ export function nubiscoUI(options = {}) {
44
56
  },
45
57
  ],
46
58
  }));
59
+ // A build that linked no component at all cannot have linked any component
60
+ // CSS either, and `styles: true` says the app is relying on this plugin
61
+ // for it. That combination ships an unstyled app and fails nothing, so it
62
+ // is worth a line. It is reachable when every usage goes through a
63
+ // namespace import (`import * as UI from '@nubisco/ui'`), or a dynamic
64
+ // one, neither of which names the components it uses.
65
+ if (styles) {
66
+ plugins.push({
67
+ name: 'nubisco-ui:styles-check',
68
+ apply: 'build',
69
+ closeBundle() {
70
+ if (resolvedComponents)
71
+ return;
72
+ console.warn(`[nubisco-ui] no components were linked in this build, so none of ` +
73
+ `the component stylesheets were either. If this app renders ` +
74
+ `Nubisco components, it will render unstyled. Import them by ` +
75
+ `name (\`import { NbButton } from '@nubisco/ui'\`) or use the ` +
76
+ `tags directly so they can be resolved; or pass ` +
77
+ `\`nubiscoUI({ styles: false })\` and import ` +
78
+ `'@nubisco/ui/css' yourself.`);
79
+ },
80
+ });
81
+ }
47
82
  }
48
83
  return plugins;
49
84
  }
@@ -1 +1 @@
1
- .nb-info-hint[data-v-f6f2eaf3]{vertical-align:middle;align-items:center;display:inline-flex}.nb-info-hint--trigger[data-v-f6f2eaf3]{color:var(--nb-c-text-subtle);cursor:help;border-radius:var(--nb-radius-pill);transition:color var(--nb-animation-fast) ease;background:0 0;border:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.nb-info-hint--trigger[data-v-f6f2eaf3]:hover,.nb-info-hint--trigger.nb-info-hint--trigger-open[data-v-f6f2eaf3]{color:var(--nb-c-text)}.nb-info-hint--trigger[data-v-f6f2eaf3]:focus-visible{outline:2px solid var(--nb-c-focus-ring);outline-offset:2px;color:var(--nb-c-text)}.nb-info-hint--trigger[data-v-f6f2eaf3]:disabled{cursor:default;color:var(--nb-c-component-disabled)}.nb-info-hint--popover[data-v-f6f2eaf3]{z-index:var(--nb-zindex-tooltip);max-width:calc(var(--nb-base-unit) * 34);padding:calc(var(--nb-base-unit) * 1.25) calc(var(--nb-base-unit) * 1.5);border:1px solid var(--nb-c-border);border-radius:var(--nb-radius-md);background:var(--nb-c-surface);color:var(--nb-c-text);box-shadow:0 calc(var(--nb-base-unit) * .5) calc(var(--nb-base-unit) * 2) var(--nb-c-scrim);text-align:left;animation:nb-info-hint-in-f6f2eaf3 var(--nb-animation-fast) ease-out;position:fixed}.nb-info-hint--title[data-v-f6f2eaf3]{margin:0 0 calc(var(--nb-base-unit) * .5);font-family:var(--nb-type-label-md-family);font-size:var(--nb-type-label-md-size);font-weight:var(--nb-type-label-md-weight);line-height:var(--nb-type-label-md-line-height);color:var(--nb-c-text)}.nb-info-hint--body[data-v-f6f2eaf3]{font-family:var(--nb-type-body-sm-family);font-size:var(--nb-type-body-sm-size);font-weight:var(--nb-type-body-sm-weight);line-height:var(--nb-type-body-sm-line-height);color:var(--nb-c-text-muted)}.nb-info-hint--body[data-v-f6f2eaf3] p{margin:0 0 calc(var(--nb-base-unit) * .5)}.nb-info-hint--body[data-v-f6f2eaf3] p:last-child{margin-bottom:0}.nb-info-hint--body[data-v-f6f2eaf3] code{font-family:var(--nb-font-family-mono);font-size:var(--nb-font-size-11)}.nb-info-hint--popover[data-v-f6f2eaf3]:after{content:"";width:var(--nb-base-unit);height:var(--nb-base-unit);background:var(--nb-c-surface);border:1px solid var(--nb-c-border);position:absolute;transform:rotate(45deg)}.nb-info-hint--popover-top[data-v-f6f2eaf3]:after{bottom:calc(var(--nb-base-unit) / -2 - 1px);left:var(--nb-info-hint-arrow-offset,50%);margin-left:calc(var(--nb-base-unit) / -2);border-top:none;border-left:none}.nb-info-hint--popover-bottom[data-v-f6f2eaf3]:after{top:calc(var(--nb-base-unit) / -2 - 1px);left:var(--nb-info-hint-arrow-offset,50%);margin-left:calc(var(--nb-base-unit) / -2);border-bottom:none;border-right:none}.nb-info-hint--popover-left[data-v-f6f2eaf3]:after{right:calc(var(--nb-base-unit) / -2 - 1px);top:var(--nb-info-hint-arrow-offset,50%);margin-top:calc(var(--nb-base-unit) / -2);border-bottom:none;border-left:none}.nb-info-hint--popover-right[data-v-f6f2eaf3]:after{left:calc(var(--nb-base-unit) / -2 - 1px);top:var(--nb-info-hint-arrow-offset,50%);margin-top:calc(var(--nb-base-unit) / -2);border-top:none;border-right:none}@keyframes nb-info-hint-in-f6f2eaf3{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}@media (prefers-reduced-motion:reduce){.nb-info-hint--popover[data-v-f6f2eaf3]{animation:none}.nb-info-hint--trigger[data-v-f6f2eaf3]{transition:none}}
1
+ .nb-info-hint[data-v-e7ec3f8e]{vertical-align:middle;align-items:center;display:inline-flex}.nb-info-hint--trigger[data-v-e7ec3f8e]{color:var(--nb-c-text-subtle);cursor:help;border-radius:var(--nb-radius-pill);transition:color var(--nb-animation-fast) ease;background:0 0;border:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.nb-info-hint--trigger[data-v-e7ec3f8e]:hover,.nb-info-hint--trigger.nb-info-hint--trigger-open[data-v-e7ec3f8e]{color:var(--nb-c-text)}.nb-info-hint--trigger[data-v-e7ec3f8e]:focus-visible{outline:2px solid var(--nb-c-focus-ring);outline-offset:2px;color:var(--nb-c-text)}.nb-info-hint--trigger[data-v-e7ec3f8e]:disabled{cursor:default;color:var(--nb-c-component-disabled)}.nb-info-hint--popover[data-v-e7ec3f8e]{z-index:var(--nb-zindex-tooltip);max-width:calc(var(--nb-base-unit) * 34);padding:calc(var(--nb-base-unit) * 1.25) calc(var(--nb-base-unit) * 1.5);border:1px solid var(--nb-c-border);border-radius:var(--nb-radius-md);background:var(--nb-c-surface);color:var(--nb-c-text);box-shadow:0 calc(var(--nb-base-unit) * .5) calc(var(--nb-base-unit) * 2) var(--nb-c-scrim);text-align:left;animation:nb-info-hint-in-e7ec3f8e var(--nb-animation-fast) ease-out;position:fixed}.nb-info-hint--title[data-v-e7ec3f8e]{margin:0 0 calc(var(--nb-base-unit) * .5);font-family:var(--nb-type-label-md-family);font-size:var(--nb-type-label-md-size);font-weight:var(--nb-type-label-md-weight);line-height:var(--nb-type-label-md-line-height);color:var(--nb-c-text)}.nb-info-hint--body[data-v-e7ec3f8e]{font-family:var(--nb-type-body-sm-family);font-size:var(--nb-type-body-sm-size);font-weight:var(--nb-type-body-sm-weight);line-height:var(--nb-type-body-sm-line-height);color:var(--nb-c-text-muted)}.nb-info-hint--body[data-v-e7ec3f8e] p{margin:0 0 calc(var(--nb-base-unit) * .5)}.nb-info-hint--body[data-v-e7ec3f8e] p:last-child{margin-bottom:0}.nb-info-hint--body[data-v-e7ec3f8e] code{font-family:var(--nb-font-family-mono);font-size:var(--nb-font-size-11)}.nb-info-hint--popover[data-v-e7ec3f8e]:after{content:"";width:var(--nb-base-unit);height:var(--nb-base-unit);background:var(--nb-c-surface);border:1px solid var(--nb-c-border);position:absolute;transform:rotate(45deg)}.nb-info-hint--popover-top[data-v-e7ec3f8e]:after{bottom:calc(var(--nb-base-unit) / -2 - 1px);left:var(--nb-info-hint-arrow-offset,50%);margin-left:calc(var(--nb-base-unit) / -2);border-top:none;border-left:none}.nb-info-hint--popover-bottom[data-v-e7ec3f8e]:after{top:calc(var(--nb-base-unit) / -2 - 1px);left:var(--nb-info-hint-arrow-offset,50%);margin-left:calc(var(--nb-base-unit) / -2);border-bottom:none;border-right:none}.nb-info-hint--popover-left[data-v-e7ec3f8e]:after{right:calc(var(--nb-base-unit) / -2 - 1px);top:var(--nb-info-hint-arrow-offset,50%);margin-top:calc(var(--nb-base-unit) / -2);border-bottom:none;border-left:none}.nb-info-hint--popover-right[data-v-e7ec3f8e]:after{left:calc(var(--nb-base-unit) / -2 - 1px);top:var(--nb-info-hint-arrow-offset,50%);margin-top:calc(var(--nb-base-unit) / -2);border-top:none;border-right:none}@keyframes nb-info-hint-in-e7ec3f8e{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}@media (prefers-reduced-motion:reduce){.nb-info-hint--popover[data-v-e7ec3f8e]{animation:none}.nb-info-hint--trigger[data-v-e7ec3f8e]{transition:none}}
package/dist/ui.css CHANGED
@@ -41,7 +41,7 @@
41
41
  .nb-file-uploader[data-v-1adb0140]{flex-direction:column;gap:.75rem;display:flex}.nb-file-uploader__heading[data-v-1adb0140]{color:var(--nb-c-text);margin:0;font-size:1rem;font-weight:600}.nb-file-uploader__description[data-v-1adb0140]{color:var(--nb-c-text-muted);border-left:3px solid var(--nb-c-border);margin:0;padding-left:.75rem;font-size:.875rem;line-height:1.5}.nb-file-uploader__input[data-v-1adb0140]{display:none}.nb-file-uploader__btn[data-v-1adb0140]{align-self:flex-start}.nb-file-uploader__dropzone[data-v-1adb0140]{border:2px dashed var(--nb-c-border);cursor:pointer;text-align:center;border-radius:6px;justify-content:center;align-items:center;padding:2rem 1.5rem;transition:border-color .15s,background .15s;display:flex}.nb-file-uploader__dropzone--primary[data-v-1adb0140]{background:color-mix(in srgb, var(--nb-c-primary) 6%, transparent);border-color:color-mix(in srgb, var(--nb-c-primary) 60%, transparent)}.nb-file-uploader__dropzone--primary .nb-file-uploader__drop-label[data-v-1adb0140]{color:var(--nb-c-primary)}.nb-file-uploader__dropzone--secondary[data-v-1adb0140]{background:color-mix(in srgb, var(--nb-c-secondary) 6%, transparent);border-color:color-mix(in srgb, var(--nb-c-secondary) 60%, transparent)}.nb-file-uploader__dropzone--secondary .nb-file-uploader__drop-label[data-v-1adb0140]{color:var(--nb-c-secondary)}.nb-file-uploader__dropzone--danger[data-v-1adb0140]{background:color-mix(in srgb, var(--nb-c-danger) 6%, transparent);border-color:color-mix(in srgb, var(--nb-c-danger) 60%, transparent)}.nb-file-uploader__dropzone--danger .nb-file-uploader__drop-label[data-v-1adb0140]{color:var(--nb-c-danger)}.nb-file-uploader__dropzone--success[data-v-1adb0140]{background:color-mix(in srgb, var(--nb-c-success) 6%, transparent);border-color:color-mix(in srgb, var(--nb-c-success) 60%, transparent)}.nb-file-uploader__dropzone--success .nb-file-uploader__drop-label[data-v-1adb0140]{color:var(--nb-c-success)}.nb-file-uploader__dropzone--warning[data-v-1adb0140]{background:color-mix(in srgb, var(--nb-c-warning) 6%, transparent);border-color:color-mix(in srgb, var(--nb-c-warning) 60%, transparent)}.nb-file-uploader__dropzone--warning .nb-file-uploader__drop-label[data-v-1adb0140]{color:var(--nb-c-warning)}.nb-file-uploader__drop-label[data-v-1adb0140]{pointer-events:none;font-size:.9rem;line-height:1.6}.nb-file-uploader__list[data-v-1adb0140]{flex-direction:column;gap:.5rem;margin:0;padding:0;list-style:none;display:flex}.nb-file-uploader__item[data-v-1adb0140]{background:var(--nb-c-surface);color:var(--nb-c-text);border:1px solid #0000;border-radius:4px;grid-template-rows:auto auto;grid-template-columns:1fr auto;align-items:center;padding:.6rem .875rem;font-size:.9rem;transition:border-color .15s;display:grid}.nb-file-uploader__item--error[data-v-1adb0140]{border-color:var(--nb-c-danger)}.nb-file-uploader__name[data-v-1adb0140]{white-space:nowrap;text-overflow:ellipsis;grid-area:1/1;padding-right:.5rem;overflow:hidden}.nb-file-uploader__status[data-v-1adb0140]{grid-area:1/2;align-items:center;gap:.25rem;display:flex}.nb-file-uploader__error[data-v-1adb0140]{color:var(--nb-c-danger);border-top:1px solid var(--nb-c-border);grid-area:2/1/auto/-1;margin-top:.35rem;padding-top:.35rem;font-size:.8125rem}.nb-file-uploader__remove[data-v-1adb0140]{cursor:pointer;color:var(--nb-c-text-muted);background:0 0;border:none;align-items:center;padding:0;line-height:1;display:flex}.nb-file-uploader__remove[data-v-1adb0140]:hover{color:var(--nb-c-text)}.nb-file-uploader__spinner[data-v-1adb0140]{color:var(--nb-c-primary);animation:.8s linear infinite nb-spin-1adb0140}.nb-file-uploader__check[data-v-1adb0140]{color:var(--nb-c-primary)}.nb-file-uploader__warn[data-v-1adb0140]{color:var(--nb-c-danger)}@keyframes nb-spin-1adb0140{to{transform:rotate(360deg)}}
42
42
  .nb-flag[data-v-7bdc2000]{height:fit-content;line-height:1em;display:flex;position:relative}.nb-flag[data-v-7bdc2000]:not(.box-clickable){pointer-events:none}.nb-flag--lg[data-v-7bdc2000]{width:32px;max-width:32px;max-height:32px}.nb-flag--md[data-v-7bdc2000]{width:20px;max-width:20px;max-height:20px}.nb-flag--sm[data-v-7bdc2000]{width:16px;max-width:16px;max-height:16px}
43
43
  .nb-image-cropper[data-v-29cde507]{-webkit-user-select:none;user-select:none;max-width:100%;height:auto;display:inline-block;position:relative;overflow:hidden}.nb-image-cropper .canvas-container[data-v-29cde507]{width:100%;height:auto;display:inline-block;position:relative}.nb-image-cropper .canvas-container canvas[data-v-29cde507]{max-width:100%;height:auto;display:block}.nb-image-cropper .resize-handle[data-v-29cde507]{background:var(--nb-c-surface);border:1px solid var(--nb-c-border);z-index:1;width:10px;height:10px;position:absolute}.nb-image-cropper .resize-handle.top-left[data-v-29cde507]{cursor:nwse-resize}.nb-image-cropper .resize-handle.top[data-v-29cde507]{cursor:ns-resize}.nb-image-cropper .resize-handle.top-right[data-v-29cde507]{cursor:nesw-resize}.nb-image-cropper .resize-handle.right[data-v-29cde507]{cursor:ew-resize}.nb-image-cropper .resize-handle.bottom-right[data-v-29cde507]{cursor:nwse-resize}.nb-image-cropper .resize-handle.bottom[data-v-29cde507]{cursor:ns-resize}.nb-image-cropper .resize-handle.bottom-left[data-v-29cde507]{cursor:nesw-resize}.nb-image-cropper .resize-handle.left[data-v-29cde507]{cursor:ew-resize}.nb-image-cropper .cropped-image-preview[data-v-29cde507]{right:calc(var(--nb-base-unit,8px) * 2);bottom:calc(var(--nb-base-unit,8px) * 2);border-radius:var(--nb-border-radius-sm,6px);height:calc(var(--nb-base-unit,8px) * 14);border:dotted 1px var(--nb-c-border);background:var(--nb-c-surface);-webkit-user-select:none;user-select:none;pointer-events:none;margin-top:20px;position:absolute;overflow:hidden}.nb-image-cropper .cropped-image-preview ul[data-v-29cde507]{min-width:calc(var(--nb-base-unit,8px) * 9);background:var(--nb-c-contrast);color:var(--nb-c-surface);font-size:var(--nb-font-size-10,10px);padding:calc(var(--nb-base-unit,8px) / 2);line-height:var(--nb-font-size-14,14px)}.nb-image-cropper .cropped-image-preview img[data-v-29cde507]{object-fit:contain;object-position:center;width:100%;height:100%}
44
- .nb-info-hint[data-v-f6f2eaf3]{vertical-align:middle;align-items:center;display:inline-flex}.nb-info-hint--trigger[data-v-f6f2eaf3]{color:var(--nb-c-text-subtle);cursor:help;border-radius:var(--nb-radius-pill);transition:color var(--nb-animation-fast) ease;background:0 0;border:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.nb-info-hint--trigger[data-v-f6f2eaf3]:hover,.nb-info-hint--trigger.nb-info-hint--trigger-open[data-v-f6f2eaf3]{color:var(--nb-c-text)}.nb-info-hint--trigger[data-v-f6f2eaf3]:focus-visible{outline:2px solid var(--nb-c-focus-ring);outline-offset:2px;color:var(--nb-c-text)}.nb-info-hint--trigger[data-v-f6f2eaf3]:disabled{cursor:default;color:var(--nb-c-component-disabled)}.nb-info-hint--popover[data-v-f6f2eaf3]{z-index:var(--nb-zindex-tooltip);max-width:calc(var(--nb-base-unit) * 34);padding:calc(var(--nb-base-unit) * 1.25) calc(var(--nb-base-unit) * 1.5);border:1px solid var(--nb-c-border);border-radius:var(--nb-radius-md);background:var(--nb-c-surface);color:var(--nb-c-text);box-shadow:0 calc(var(--nb-base-unit) * .5) calc(var(--nb-base-unit) * 2) var(--nb-c-scrim);text-align:left;animation:nb-info-hint-in-f6f2eaf3 var(--nb-animation-fast) ease-out;position:fixed}.nb-info-hint--title[data-v-f6f2eaf3]{margin:0 0 calc(var(--nb-base-unit) * .5);font-family:var(--nb-type-label-md-family);font-size:var(--nb-type-label-md-size);font-weight:var(--nb-type-label-md-weight);line-height:var(--nb-type-label-md-line-height);color:var(--nb-c-text)}.nb-info-hint--body[data-v-f6f2eaf3]{font-family:var(--nb-type-body-sm-family);font-size:var(--nb-type-body-sm-size);font-weight:var(--nb-type-body-sm-weight);line-height:var(--nb-type-body-sm-line-height);color:var(--nb-c-text-muted)}.nb-info-hint--body[data-v-f6f2eaf3] p{margin:0 0 calc(var(--nb-base-unit) * .5)}.nb-info-hint--body[data-v-f6f2eaf3] p:last-child{margin-bottom:0}.nb-info-hint--body[data-v-f6f2eaf3] code{font-family:var(--nb-font-family-mono);font-size:var(--nb-font-size-11)}.nb-info-hint--popover[data-v-f6f2eaf3]:after{content:"";width:var(--nb-base-unit);height:var(--nb-base-unit);background:var(--nb-c-surface);border:1px solid var(--nb-c-border);position:absolute;transform:rotate(45deg)}.nb-info-hint--popover-top[data-v-f6f2eaf3]:after{bottom:calc(var(--nb-base-unit) / -2 - 1px);left:var(--nb-info-hint-arrow-offset,50%);margin-left:calc(var(--nb-base-unit) / -2);border-top:none;border-left:none}.nb-info-hint--popover-bottom[data-v-f6f2eaf3]:after{top:calc(var(--nb-base-unit) / -2 - 1px);left:var(--nb-info-hint-arrow-offset,50%);margin-left:calc(var(--nb-base-unit) / -2);border-bottom:none;border-right:none}.nb-info-hint--popover-left[data-v-f6f2eaf3]:after{right:calc(var(--nb-base-unit) / -2 - 1px);top:var(--nb-info-hint-arrow-offset,50%);margin-top:calc(var(--nb-base-unit) / -2);border-bottom:none;border-left:none}.nb-info-hint--popover-right[data-v-f6f2eaf3]:after{left:calc(var(--nb-base-unit) / -2 - 1px);top:var(--nb-info-hint-arrow-offset,50%);margin-top:calc(var(--nb-base-unit) / -2);border-top:none;border-right:none}@keyframes nb-info-hint-in-f6f2eaf3{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}@media (prefers-reduced-motion:reduce){.nb-info-hint--popover[data-v-f6f2eaf3]{animation:none}.nb-info-hint--trigger[data-v-f6f2eaf3]{transition:none}}
44
+ .nb-info-hint[data-v-e7ec3f8e]{vertical-align:middle;align-items:center;display:inline-flex}.nb-info-hint--trigger[data-v-e7ec3f8e]{color:var(--nb-c-text-subtle);cursor:help;border-radius:var(--nb-radius-pill);transition:color var(--nb-animation-fast) ease;background:0 0;border:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.nb-info-hint--trigger[data-v-e7ec3f8e]:hover,.nb-info-hint--trigger.nb-info-hint--trigger-open[data-v-e7ec3f8e]{color:var(--nb-c-text)}.nb-info-hint--trigger[data-v-e7ec3f8e]:focus-visible{outline:2px solid var(--nb-c-focus-ring);outline-offset:2px;color:var(--nb-c-text)}.nb-info-hint--trigger[data-v-e7ec3f8e]:disabled{cursor:default;color:var(--nb-c-component-disabled)}.nb-info-hint--popover[data-v-e7ec3f8e]{z-index:var(--nb-zindex-tooltip);max-width:calc(var(--nb-base-unit) * 34);padding:calc(var(--nb-base-unit) * 1.25) calc(var(--nb-base-unit) * 1.5);border:1px solid var(--nb-c-border);border-radius:var(--nb-radius-md);background:var(--nb-c-surface);color:var(--nb-c-text);box-shadow:0 calc(var(--nb-base-unit) * .5) calc(var(--nb-base-unit) * 2) var(--nb-c-scrim);text-align:left;animation:nb-info-hint-in-e7ec3f8e var(--nb-animation-fast) ease-out;position:fixed}.nb-info-hint--title[data-v-e7ec3f8e]{margin:0 0 calc(var(--nb-base-unit) * .5);font-family:var(--nb-type-label-md-family);font-size:var(--nb-type-label-md-size);font-weight:var(--nb-type-label-md-weight);line-height:var(--nb-type-label-md-line-height);color:var(--nb-c-text)}.nb-info-hint--body[data-v-e7ec3f8e]{font-family:var(--nb-type-body-sm-family);font-size:var(--nb-type-body-sm-size);font-weight:var(--nb-type-body-sm-weight);line-height:var(--nb-type-body-sm-line-height);color:var(--nb-c-text-muted)}.nb-info-hint--body[data-v-e7ec3f8e] p{margin:0 0 calc(var(--nb-base-unit) * .5)}.nb-info-hint--body[data-v-e7ec3f8e] p:last-child{margin-bottom:0}.nb-info-hint--body[data-v-e7ec3f8e] code{font-family:var(--nb-font-family-mono);font-size:var(--nb-font-size-11)}.nb-info-hint--popover[data-v-e7ec3f8e]:after{content:"";width:var(--nb-base-unit);height:var(--nb-base-unit);background:var(--nb-c-surface);border:1px solid var(--nb-c-border);position:absolute;transform:rotate(45deg)}.nb-info-hint--popover-top[data-v-e7ec3f8e]:after{bottom:calc(var(--nb-base-unit) / -2 - 1px);left:var(--nb-info-hint-arrow-offset,50%);margin-left:calc(var(--nb-base-unit) / -2);border-top:none;border-left:none}.nb-info-hint--popover-bottom[data-v-e7ec3f8e]:after{top:calc(var(--nb-base-unit) / -2 - 1px);left:var(--nb-info-hint-arrow-offset,50%);margin-left:calc(var(--nb-base-unit) / -2);border-bottom:none;border-right:none}.nb-info-hint--popover-left[data-v-e7ec3f8e]:after{right:calc(var(--nb-base-unit) / -2 - 1px);top:var(--nb-info-hint-arrow-offset,50%);margin-top:calc(var(--nb-base-unit) / -2);border-bottom:none;border-left:none}.nb-info-hint--popover-right[data-v-e7ec3f8e]:after{left:calc(var(--nb-base-unit) / -2 - 1px);top:var(--nb-info-hint-arrow-offset,50%);margin-top:calc(var(--nb-base-unit) / -2);border-top:none;border-right:none}@keyframes nb-info-hint-in-e7ec3f8e{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}@media (prefers-reduced-motion:reduce){.nb-info-hint--popover[data-v-e7ec3f8e]{animation:none}.nb-info-hint--trigger[data-v-e7ec3f8e]{transition:none}}
45
45
  .nb-inline-edit__text[data-v-d602ce76]{align-items:baseline;gap:calc(var(--nb-base-unit) * .75);max-width:100%;padding:calc(var(--nb-base-unit) * .25) calc(var(--nb-base-unit) * .75);margin:0;border-radius:var(--nb-radius-sm);color:inherit;text-align:start;cursor:text;background:0 0;border:0;margin-inline-start:calc(var(--nb-base-unit) * -.75);display:inline-flex}.nb-inline-edit__text span[data-v-d602ce76]{overflow-wrap:anywhere}.nb-inline-edit__text .nb-inline-edit__pencil[data-v-d602ce76]{color:var(--nb-c-text-subtle);opacity:0;flex:none;align-self:center;transition:opacity .12s}.nb-inline-edit__text[data-v-d602ce76]:hover:not(.is-disabled){background:var(--nb-c-surface-hover)}.nb-inline-edit__text:hover:not(.is-disabled) .nb-inline-edit__pencil[data-v-d602ce76]{opacity:1}.nb-inline-edit__text[data-v-d602ce76]:focus-visible{outline:2px solid var(--nb-c-focus-ring,var(--nb-c-primary));outline-offset:1px}.nb-inline-edit__text:focus-visible .nb-inline-edit__pencil[data-v-d602ce76]{opacity:1}.nb-inline-edit__text.is-disabled[data-v-d602ce76]{cursor:default}.nb-inline-edit__placeholder[data-v-d602ce76]{color:var(--nb-c-text-subtle)}.nb-inline-edit__input[data-v-d602ce76]{width:100%;padding:calc(var(--nb-base-unit) * .25) calc(var(--nb-base-unit) * .75);border:1px solid var(--nb-c-primary);border-radius:var(--nb-radius-sm);background:var(--nb-c-surface);color:inherit;margin:0;margin-inline-start:calc(var(--nb-base-unit) * -.75)}.nb-inline-edit__input[data-v-d602ce76]:focus{box-shadow:0 0 0 2px color-mix(in srgb, var(--nb-c-primary) 30%, transparent);outline:none}.nb-inline-edit__text--md[data-v-d602ce76],.nb-inline-edit__input--md[data-v-d602ce76]{font-family:var(--nb-type-body-md-family,var(--nb-font-family-sans));font-size:var(--nb-type-body-md-size);font-weight:var(--nb-type-body-md-weight);line-height:var(--nb-type-body-md-line-height);letter-spacing:var(--nb-type-body-md-letter-spacing,normal)}.nb-inline-edit__text--lg[data-v-d602ce76],.nb-inline-edit__input--lg[data-v-d602ce76]{font-family:var(--nb-type-heading-01-family,var(--nb-font-family-sans));font-size:var(--nb-type-heading-01-size);font-weight:var(--nb-type-heading-01-weight);line-height:var(--nb-type-heading-01-line-height);letter-spacing:var(--nb-type-heading-01-letter-spacing,normal)}.nb-inline-edit__text--xl[data-v-d602ce76],.nb-inline-edit__input--xl[data-v-d602ce76]{font-family:var(--nb-type-heading-02-family,var(--nb-font-family-sans));font-size:var(--nb-type-heading-02-size);font-weight:var(--nb-type-heading-02-weight);line-height:var(--nb-type-heading-02-line-height);letter-spacing:var(--nb-type-heading-02-letter-spacing,normal)}
46
46
  .nb-spinner[data-v-55d1147d]{align-items:center;gap:var(--nb-base-unit);font-family:var(--nb-font-family-sans);color:var(--nb-c-primary);--nb-spinner-diameter:calc(var(--nb-field-height-md) / 2);display:inline-flex}.nb-spinner--xs[data-v-55d1147d]{--nb-spinner-diameter:calc(var(--nb-field-height-xs) / 2)}.nb-spinner--sm[data-v-55d1147d]{--nb-spinner-diameter:calc(var(--nb-field-height-sm) / 2)}.nb-spinner--md[data-v-55d1147d]{--nb-spinner-diameter:calc(var(--nb-field-height-md) / 2)}.nb-spinner--lg[data-v-55d1147d]{--nb-spinner-diameter:calc(var(--nb-field-height-lg) / 2)}.nb-spinner--tone-inherit[data-v-55d1147d]{color:inherit}.nb-spinner__ring[data-v-55d1147d]{width:var(--nb-spinner-diameter);height:var(--nb-spinner-diameter);flex-shrink:0;display:block}.nb-spinner__track[data-v-55d1147d],.nb-spinner__arc[data-v-55d1147d]{fill:none;stroke-width:4px}.nb-spinner__track[data-v-55d1147d]{stroke:var(--nb-c-discrete)}.nb-spinner__arc[data-v-55d1147d]{stroke:currentColor;stroke-linecap:round;stroke-dasharray:22 66;transform-origin:50%;animation:.9s linear infinite nb-spinner-spin-55d1147d}.nb-spinner__actions[data-v-55d1147d]{gap:var(--nb-base-unit);margin-top:var(--nb-base-unit);align-items:center;display:flex}.nb-spinner__label[data-v-55d1147d]{font-size:var(--nb-font-size-12);color:var(--nb-c-text-muted);line-height:1.4}.nb-spinner__announcement[data-v-55d1147d]{white-space:nowrap;clip-path:inset(50%);border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.nb-spinner--overlay-container[data-v-55d1147d],.nb-spinner--overlay-page[data-v-55d1147d]{gap:var(--nb-base-unit);background:var(--nb-c-scrim);flex-direction:column;justify-content:center;align-items:center;display:flex}.nb-spinner--overlay-container .nb-spinner__label[data-v-55d1147d],.nb-spinner--overlay-page .nb-spinner__label[data-v-55d1147d]{color:var(--nb-c-white)}.nb-spinner--overlay-container[data-v-55d1147d]{z-index:var(--nb-zindex-pageheader);position:absolute;inset:0}.nb-spinner--overlay-page[data-v-55d1147d]{z-index:var(--nb-zindex-modal);position:fixed;inset:0}.nb-spinner--overlay-page.nb-spinner--overlay-stacked[data-v-55d1147d]{background:0 0}.nb-spinner--overlay-page[data-v-55d1147d]:focus{outline:none}@keyframes nb-spinner-spin-55d1147d{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.nb-spinner .nb-spinner__arc[data-v-55d1147d]{stroke-dasharray:none;animation:1.6s ease-in-out infinite nb-spinner-breathe-55d1147d}}.nb-spinner--reduced-motion .nb-spinner__arc[data-v-55d1147d]{stroke-dasharray:none;animation:1.6s ease-in-out infinite nb-spinner-breathe-55d1147d}@keyframes nb-spinner-breathe-55d1147d{0%,to{stroke-opacity:.25}50%{stroke-opacity:1}}@media (forced-colors:active){.nb-spinner[data-v-55d1147d]{color:canvastext}.nb-spinner .nb-spinner__arc[data-v-55d1147d]{forced-color-adjust:none;stroke:canvastext}.nb-spinner .nb-spinner__track[data-v-55d1147d]{forced-color-adjust:none;stroke:canvas}.nb-spinner.nb-spinner--overlay-container[data-v-55d1147d],.nb-spinner.nb-spinner--overlay-page[data-v-55d1147d]{outline-offset:-1px;background:canvas;outline:1px solid canvastext}.nb-spinner .nb-spinner__label[data-v-55d1147d]{color:canvastext}}
47
47
  .nb-inline-loading[data-v-077014e8]{gap:calc(var(--nb-base-unit) / 2);min-height:var(--nb-field-height-sm);font-family:var(--nb-font-family-sans);font-size:var(--nb-font-size-12);color:var(--nb-c-text-muted);align-items:center;line-height:1.4;display:inline-flex}.nb-inline-loading__spinner[data-v-077014e8],.nb-inline-loading__icon[data-v-077014e8]{flex-shrink:0}.nb-inline-loading__text[data-v-077014e8]{min-width:var(--nb-inline-loading-reserve,0)}.nb-inline-loading--finished[data-v-077014e8]{color:var(--nb-c-success)}.nb-inline-loading--error[data-v-077014e8]{color:var(--nb-c-danger)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubisco/ui",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "Vue 3 UI component library",
5
5
  "packageManager": "pnpm@11.0.8",
6
6
  "repository": {
@@ -140,7 +140,7 @@
140
140
  "build:lib": "vite build",
141
141
  "build:types": "vue-tsc --noEmit false --declaration --emitDeclarationOnly --outDir dist && node scripts/copy-type-modules.mjs && node scripts/rewrite-dist-aliases.mjs dist",
142
142
  "build:plugins": "tsc --project tsconfig.plugins.json",
143
- "verify:types": "pnpm run verify:dist-types && pnpm run verify:consumer-types && pnpm run verify:consumer-globals && pnpm run verify:resolution && pnpm run verify:styles",
143
+ "verify:types": "pnpm run verify:dist-types && pnpm run verify:consumer-types && pnpm run verify:consumer-globals && pnpm run verify:resolution && pnpm run verify:styles && pnpm run verify:glyph-defaults",
144
144
  "verify:dist-types": "node scripts/verify-dist-types.mjs dist",
145
145
  "verify:consumer-types": "node scripts/verify-consumer-types.mjs",
146
146
  "verify:consumer-globals": "node scripts/verify-consumer-globals.mjs",
@@ -161,7 +161,8 @@
161
161
  "verify:marks": "node scripts/verify-inlined-marks.mjs",
162
162
  "generate": "node scripts/generate-component-manifest.mjs && node scripts/generate-assets.mjs && node scripts/generate-entries.mjs",
163
163
  "verify:resolution": "node scripts/verify-compile-time-resolution.mjs dist",
164
- "verify:styles": "node scripts/verify-style-split.mjs dist"
164
+ "verify:styles": "node scripts/verify-style-split.mjs dist",
165
+ "verify:glyph-defaults": "node scripts/verify-default-glyphs.mjs dist"
165
166
  },
166
167
  "simple-git-hooks": {
167
168
  "pre-commit": "npx lint-staged && pnpm run quality:check",
@@ -3,7 +3,7 @@ import { existsSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import MagicString from 'magic-string'
6
- import { parse } from 'vue/compiler-sfc'
6
+ import { parse, babelParse } from 'vue/compiler-sfc'
7
7
 
8
8
  /**
9
9
  * Resolves icon and flag artwork at compile time.
@@ -63,6 +63,15 @@ export interface IGlyphOptions {
63
63
  packageName?: string
64
64
  /** Log every rewrite. Useful when auditing what a page actually links. */
65
65
  verbose?: boolean
66
+ /**
67
+ * Warn when a glyph forwarded through another component (`<NbButton
68
+ * :icon="…">`) is a value this plugin cannot see through, so no artwork was
69
+ * linked for it. On by default, once per file, because the alternative is
70
+ * the silence that lets an icon reach `NbIcon` with nothing to resolve it
71
+ * against. Turn it off in a codebase where those bindings are deliberate
72
+ * pass-through props, resolved by whoever passes them in.
73
+ */
74
+ warnUnresolved?: boolean
66
75
  }
67
76
 
68
77
  type TKind = 'icon' | 'flag'
@@ -136,6 +145,99 @@ function staticWeight(element: any): string | undefined {
136
145
  return 'regular'
137
146
  }
138
147
 
148
+ /**
149
+ * Parse one template expression on its own. `babelParse` wants a program, so
150
+ * the expression is wrapped; a binding Vue accepts but Babel cannot read is
151
+ * simply unknown, which the caller treats as unresolved.
152
+ */
153
+ function parseExpression(expression: string): any {
154
+ try {
155
+ const { program } = babelParse(`(${expression})`, {
156
+ sourceType: 'module',
157
+ plugins: ['typescript'],
158
+ })
159
+ const statement: any = program.body[0]
160
+ return statement?.type === 'ExpressionStatement'
161
+ ? statement.expression
162
+ : undefined
163
+ } catch {
164
+ return undefined
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Whether every value this expression can hand to the component is artwork the
170
+ * bundle already links: a literal this plugin just rewrote, or a glyph module
171
+ * the file imported itself.
172
+ *
173
+ * The point is what it refuses. `open ? 'caret-up' : 'caret-down'` produces one
174
+ * of two rewritten literals and nothing else, so it is resolved. `block?.icon
175
+ * || 'cube'` also contains a rewritten literal, but it can just as easily
176
+ * produce `block.icon`, which is a runtime value and needs the catalogue.
177
+ * Judging by "a rewritten literal appears and no quoted string survives" called
178
+ * that second one resolved, which meant adding a literal fallback silently
179
+ * removed the coverage the same expression had without one.
180
+ */
181
+ function producesOnlyLinkedGlyphs(
182
+ node: any,
183
+ isLinked: (name: string) => boolean,
184
+ ): boolean {
185
+ if (!node) return false
186
+ const recurse = (child: any) => producesOnlyLinkedGlyphs(child, isLinked)
187
+ switch (node.type) {
188
+ case 'Identifier':
189
+ return isLinked(node.name)
190
+ // The test is not a value the prop receives; the two arms are.
191
+ case 'ConditionalExpression':
192
+ return recurse(node.consequent) && recurse(node.alternate)
193
+ // `a || b`, `a ?? b` and `a && b` can all produce either side.
194
+ case 'LogicalExpression':
195
+ return recurse(node.left) && recurse(node.right)
196
+ case 'SequenceExpression':
197
+ return recurse(node.expressions[node.expressions.length - 1])
198
+ case 'TSAsExpression':
199
+ case 'TSNonNullExpression':
200
+ case 'TSSatisfiesExpression':
201
+ case 'ParenthesizedExpression':
202
+ return recurse(node.expression)
203
+ default:
204
+ return false
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Local names bound to glyph modules by the file's own imports, so that
210
+ * `<NbIcon :name="GithubLogo" />` is understood as already-linked artwork
211
+ * rather than as a runtime name needing the whole catalogue.
212
+ */
213
+ function glyphImportLocals(script: string, packageName: string): Set<string> {
214
+ const locals = new Set<string>()
215
+ const from = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
216
+ const pattern = new RegExp(
217
+ `import\\s+([^;'"\n]+?)\\s+from\\s*['"]${from}/(?:icons|flags)/[^'"]+['"]`,
218
+ 'g',
219
+ )
220
+ for (const match of script.matchAll(pattern)) {
221
+ const clause = match[1]
222
+ const namespace = clause.match(/\*\s+as\s+(\w+)/)
223
+ if (namespace) locals.add(namespace[1])
224
+ const defaultImport = clause.match(/^\s*(\w+)/)
225
+ if (defaultImport) locals.add(defaultImport[1])
226
+ const named = clause.match(/\{([^}]*)\}/)
227
+ if (named) {
228
+ for (const part of named[1].split(',')) {
229
+ const local = part
230
+ .trim()
231
+ .split(/\s+as\s+/)
232
+ .pop()
233
+ ?.trim()
234
+ if (local) locals.add(local)
235
+ }
236
+ }
237
+ }
238
+ return locals
239
+ }
240
+
139
241
  function staticPropKind(
140
242
  name: string,
141
243
  isIcon: boolean,
@@ -157,6 +259,7 @@ export function nubiscoGlyphs(options: IGlyphOptions = {}) {
157
259
  flags = true,
158
260
  packageName = '@nubisco/ui',
159
261
  verbose = false,
262
+ warnUnresolved = true,
160
263
  } = options
161
264
 
162
265
  const glyphRoot = options.glyphRoot ?? defaultGlyphRoot()
@@ -186,6 +289,16 @@ export function nubiscoGlyphs(options: IGlyphOptions = {}) {
186
289
  const edits: IEdit[] = []
187
290
  const used = new Map<string, IGlyphImport>()
188
291
  const catalogsNeeded = new Set<TKind>()
292
+ const unresolvedForwards: { tag: string; prop: string; exp: string }[] =
293
+ []
294
+
295
+ // Artwork the file already imports by hand counts as linked: binding an
296
+ // imported module is the documented no-plugin path, and pulling a
297
+ // catalogue in on top of it would be pure waste.
298
+ const glyphLocals = glyphImportLocals(
299
+ `${descriptor.scriptSetup?.content ?? ''}\n${descriptor.script?.content ?? ''}`,
300
+ packageName,
301
+ )
189
302
 
190
303
  const use = (glyph: IGlyphImport) => {
191
304
  const variable = identifierFor(glyph)
@@ -229,17 +342,18 @@ export function nubiscoGlyphs(options: IGlyphOptions = {}) {
229
342
  continue
230
343
  }
231
344
 
232
- /* ---- bound: :name="…" on NbIcon / NbFlag --------------------- */
345
+ /* ---- bound: :name="…", :icon="…", :flag="…" ------------------ */
233
346
  if (
234
347
  prop.type === NODE_DIRECTIVE &&
235
348
  prop.name === 'bind' &&
236
349
  prop.arg?.type === EXPR_SIMPLE &&
237
- prop.exp?.type === EXPR_SIMPLE &&
238
- (isIcon || isFlag)
350
+ prop.exp?.type === EXPR_SIMPLE
239
351
  ) {
240
- const kind: TKind = isIcon ? 'icon' : 'flag'
241
- if (!enabled(kind)) continue
242
- if (!['name', 'icon', 'flag'].includes(prop.arg.content)) continue
352
+ // Same rule as the static branch, so `:icon="a ? 'check' : 'copy'"`
353
+ // links the two icons whether it is written on NbIcon or forwarded
354
+ // through NbButton. Only the tag decides the kind.
355
+ const kind = staticPropKind(prop.arg.content, isIcon, isFlag)
356
+ if (!kind || !enabled(kind)) continue
243
357
 
244
358
  const expression: string = prop.exp.content
245
359
  const rewritten = expression.replace(
@@ -262,16 +376,52 @@ export function nubiscoGlyphs(options: IGlyphOptions = {}) {
262
376
  })
263
377
  }
264
378
 
265
- // Considered resolved when every value the expression can produce
266
- // is one of the modules just imported: at least one appears, and
267
- // no string literal survived that could not be accounted for.
268
- const resolved =
269
- rewritten.includes('__nb_') && !/['"]/.test(rewritten)
270
- if (!resolved) catalogsNeeded.add(kind)
379
+ const resolved = producesOnlyLinkedGlyphs(
380
+ parseExpression(rewritten),
381
+ (identifier) =>
382
+ used.has(identifier) || glyphLocals.has(identifier),
383
+ )
384
+ if (resolved) continue
385
+
386
+ // On NbIcon or NbFlag an unresolvable name is fatal at first
387
+ // render, so the catalogue is the only thing that can save it. On
388
+ // a forwarding component the same expression may just as well be
389
+ // a module arriving as a prop, and linking 1,500 icons on that
390
+ // guess would make the common wrapper the most expensive file in
391
+ // the bundle. So that case is reported rather than paid for.
392
+ if (isIcon || isFlag) catalogsNeeded.add(kind)
393
+ else
394
+ unresolvedForwards.push({
395
+ tag,
396
+ prop: prop.arg.content,
397
+ exp: expression,
398
+ })
271
399
  }
272
400
  }
273
401
  })
274
402
 
403
+ // One line per file, however many bindings it has: this is a note about
404
+ // the file, and repeating the same advice per expression would train
405
+ // people to skip it.
406
+ if (warnUnresolved && unresolvedForwards.length) {
407
+ const kinds = new Set(unresolvedForwards.map((f) => f.prop))
408
+ const register = kinds.has('flag')
409
+ ? 'registerFlags()'
410
+ : 'registerIcons()'
411
+ const catalogue = `${packageName}/${kinds.has('flag') ? 'flags' : 'icons'}/all`
412
+ console.warn(
413
+ `[nubisco-ui] ${path.relative(process.cwd(), file)}: no artwork ` +
414
+ `linked for ` +
415
+ unresolvedForwards
416
+ .map((f) => `<${f.tag} :${f.prop}="${f.exp}">`)
417
+ .join(', ') +
418
+ `. The value is only known at runtime, so it will throw on first ` +
419
+ `render unless something resolves it: ${register} for a known ` +
420
+ `set, or import '${catalogue}' in this file. Nothing to do if ` +
421
+ `these are pass-through props (set warnUnresolved: false).`,
422
+ )
423
+ }
424
+
275
425
  if (!used.size && !catalogsNeeded.size) return
276
426
 
277
427
  const imports: string[] = []
@@ -0,0 +1,192 @@
1
+ import MagicString from 'magic-string'
2
+ import { parse, babelParse } from 'vue/compiler-sfc'
3
+
4
+ /**
5
+ * Makes a hand-written component import behave like an auto-imported one.
6
+ *
7
+ * The library ships one stylesheet per chunk, and the auto-import resolver
8
+ * declares each component's sheets as side effects so the bundler links them
9
+ * next to the component. That only happens for tags the resolver saw. An app
10
+ * that writes its imports by hand:
11
+ *
12
+ * ```ts
13
+ * import { NbButton, NbPanel } from '@nubisco/ui'
14
+ * ```
15
+ *
16
+ * never went through the resolver, so it got no stylesheets at all. With
17
+ * `styles: true` (the default) and the `@nubisco/ui/css` import dropped, as
18
+ * the upgrade guide said to, that shipped an app with no component CSS: every
19
+ * page rendered unstyled, and nothing failed. The CSS bundle still looked
20
+ * plausible, because the app's own styles and the design tokens were still in
21
+ * it.
22
+ *
23
+ * So this plugin gives the same treatment to imports the app wrote itself:
24
+ *
25
+ * 1. A named component imported from the barrel is rewritten to its own entry
26
+ * point, which is what the resolver would have emitted, and which also
27
+ * stops the barrel from pulling in every other component.
28
+ * 2. Either way, that component's stylesheets are imported alongside it.
29
+ *
30
+ * Names the manifest does not know (composables, types, `registerIcons`) stay
31
+ * on the barrel import untouched, and a namespace or default import is left
32
+ * alone entirely, since there is no way to tell from it which components the
33
+ * file actually uses.
34
+ */
35
+
36
+ interface INubiscoImportOptions {
37
+ /** `Nb*` name to entry file, from the component manifest. */
38
+ manifest: Map<string, string>
39
+ /** Which stylesheets each component entry needs. */
40
+ styleManifest: Record<string, string[]>
41
+ /** Inject stylesheets. When false this plugin only rewrites imports. */
42
+ styles: boolean
43
+ packageName: string
44
+ /** Called with the number of components resolved in a file. */
45
+ onResolve: (count: number) => void
46
+ }
47
+
48
+ interface IScriptBlock {
49
+ content: string
50
+ offset: number
51
+ }
52
+
53
+ /** The script of a `.vue` file, or the whole module for a plain script. */
54
+ function scriptBlocks(code: string, file: string): IScriptBlock[] {
55
+ if (!file.endsWith('.vue')) return [{ content: code, offset: 0 }]
56
+ const { descriptor, errors } = parse(code, { filename: file })
57
+ if (errors.length) return []
58
+ return [descriptor.script, descriptor.scriptSetup]
59
+ .filter((block): block is NonNullable<typeof block> => Boolean(block))
60
+ .map((block) => ({
61
+ content: block.content,
62
+ offset: block.loc.start.offset,
63
+ }))
64
+ }
65
+
66
+ interface ISpecifier {
67
+ imported: string
68
+ local: string
69
+ }
70
+
71
+ export function nubiscoImports(options: INubiscoImportOptions) {
72
+ const { manifest, styleManifest, styles, packageName, onResolve } = options
73
+ const componentPrefix = `${packageName}/components/`
74
+
75
+ return {
76
+ name: 'nubisco-ui:imports',
77
+ enforce: 'pre' as const,
78
+
79
+ transform(code: string, id: string) {
80
+ const file = id.split('?')[0]
81
+ if (file.includes('/node_modules/')) return
82
+ if (!/\.(vue|[cm]?[jt]sx?)$/.test(file)) return
83
+ if (!code.includes(packageName)) return
84
+
85
+ const magic = new MagicString(code)
86
+ const sheetsSeen = new Set<string>()
87
+ let resolved = 0
88
+ let changed = false
89
+
90
+ // Stylesheets are appended after the import they belong to, so they
91
+ // land in the consumer's module graph in the order the resolver would
92
+ // have produced.
93
+ const sheetsFor = (entry: string) => {
94
+ if (!styles) return ''
95
+ const sheets = (styleManifest[entry] ?? []).filter(
96
+ (sheet) => !sheetsSeen.has(sheet),
97
+ )
98
+ sheets.forEach((sheet) => sheetsSeen.add(sheet))
99
+ return sheets
100
+ .map((sheet) => `\nimport '${packageName}/dist/${sheet}'`)
101
+ .join('')
102
+ }
103
+
104
+ for (const block of scriptBlocks(code, file)) {
105
+ let program
106
+ try {
107
+ program = babelParse(block.content, {
108
+ sourceType: 'module',
109
+ plugins: ['typescript', 'jsx'],
110
+ }).program
111
+ } catch {
112
+ // Not parseable on its own: the Vue plugin will report it.
113
+ continue
114
+ }
115
+
116
+ for (const node of program.body as any[]) {
117
+ if (node.type !== 'ImportDeclaration') continue
118
+ // `import type { … }` links nothing at runtime, so it needs no
119
+ // stylesheet and must not be rewritten into a value import.
120
+ if (node.importKind === 'type') continue
121
+ const source: string = node.source.value
122
+ const start = block.offset + node.start
123
+ const end = block.offset + node.end
124
+
125
+ /* An entry point the app already named: it only needs its styles. */
126
+ if (source.startsWith(componentPrefix)) {
127
+ const entry = source.slice(componentPrefix.length)
128
+ const sheets = sheetsFor(entry)
129
+ resolved += 1
130
+ if (sheets) {
131
+ magic.appendRight(end, sheets)
132
+ changed = true
133
+ }
134
+ continue
135
+ }
136
+
137
+ if (source !== packageName) continue
138
+
139
+ const components: ISpecifier[] = []
140
+ const rest: string[] = []
141
+ let splittable = true
142
+
143
+ for (const specifier of node.specifiers as any[]) {
144
+ if (specifier.type !== 'ImportSpecifier') {
145
+ // A default or namespace import says nothing about which
146
+ // components the file uses, so it stays as it is.
147
+ splittable = false
148
+ break
149
+ }
150
+ const imported: string =
151
+ specifier.imported.name ?? specifier.imported.value
152
+ const local: string = specifier.local.name
153
+ if (specifier.importKind === 'type' || !manifest.has(imported)) {
154
+ rest.push(
155
+ block.content.slice(specifier.start, specifier.end).trim(),
156
+ )
157
+ continue
158
+ }
159
+ components.push({ imported, local })
160
+ }
161
+
162
+ if (!splittable || !components.length) continue
163
+
164
+ const lines = components.map(({ imported, local }) => {
165
+ const entry = manifest.get(imported) as string
166
+ const binding =
167
+ imported === local ? imported : `${imported} as ${local}`
168
+ return (
169
+ `import { ${binding} } from '${componentPrefix}${entry}'` +
170
+ sheetsFor(entry)
171
+ )
172
+ })
173
+ if (rest.length) {
174
+ lines.push(`import { ${rest.join(', ')} } from '${source}'`)
175
+ }
176
+
177
+ resolved += components.length
178
+ magic.overwrite(start, end, lines.join('\n'))
179
+ changed = true
180
+ }
181
+ }
182
+
183
+ onResolve(resolved)
184
+ if (!changed) return
185
+
186
+ return {
187
+ code: magic.toString(),
188
+ map: magic.generateMap({ hires: true, source: file }),
189
+ }
190
+ },
191
+ }
192
+ }