Skip to content

Commit 7dcb3a5

Browse files
committed
feat(Bicons): Add Bicons and generation script
1 parent 36d86a3 commit 7dcb3a5

File tree

8 files changed

+11224
-7
lines changed

8 files changed

+11224
-7
lines changed

packages/bootstrap-vue-3-icons/package.json

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
"license": "MIT",
77
"main": "./dist/bootstrap-vue-3-icons.umd.js",
88
"module": "./dist/bootstrap-vue-3-icons.es.js",
9+
"type": "module",
910
"exports": {
1011
".": {
1112
"import": "./dist/bootstrap-vue-3-icons.es.js",
12-
"require": "./dist/bootstrap-vue-3-icons.umd.js"
13+
"require": "./dist/bootstrap-vue-3-icons.umd.js"
1314
},
1415
"./dist/bootstrap-vue-3-icons.css": "./dist/bootstrap-vue-3-icons.css",
1516
"./nuxt": "./nuxt.js",
@@ -20,6 +21,9 @@
2021
"src",
2122
"nuxt.js"
2223
],
24+
"bin":{
25+
"generate": "./scripts/generate.js"
26+
},
2327
"types": "./dist/BootstrapVueIcons.d.ts",
2428
"private": false,
2529
"scripts": {
@@ -29,7 +33,8 @@
2933
"lint": "eslint --ext .js,.vue --ignore-path ../../.gitignore --fix src",
3034
"format": "prettier . --write",
3135
"test": "pnpm run test:lint",
32-
"test:lint": "pnpm run lint"
36+
"test:lint": "pnpm run lint",
37+
"buildIcons" : "pnpm exec generate"
3338
},
3439
"peerDependencies": {
3540
"@popperjs/core": "^2.11.6",
@@ -38,7 +43,9 @@
3843
"vue": "^3.2.37"
3944
},
4045
"dependencies": {
41-
"@nuxt/kit": "3.0.0-rc.12"
46+
"@nuxt/kit": "3.0.0-rc.12",
47+
"bootstrap-vue-3": "workspace:*"
48+
4249
},
4350
"devDependencies": {
4451
"@popperjs/core": "^2.11.6",
@@ -62,7 +69,9 @@
6269
"vitest": "^0.23.2",
6370
"vue": "^3.2.37",
6471
"vue-router": "^4.1.3",
65-
"vue-tsc": "^0.38.4"
72+
"vue-tsc": "^0.38.4",
73+
"lodash": "^4.17.21"
74+
6675
},
6776
"repository": {
6877
"type": "git",
@@ -85,5 +94,6 @@
8594
"lint-staged": {
8695
"*.{js,vue}": "eslint --cache --fix",
8796
"*": "prettier --write --ignore-unknown"
88-
}
97+
},
98+
"sideEffects": false
8999
}
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
#!/usr/bin/env node
2+
3+
// NodeJS script to create the icon components modules:
4+
// src/icons/icons.js
5+
// src/icons/plugin.js
6+
// src/icons/icons.d.ts
7+
//
8+
// Source is bootstrap-icons/icons
9+
import {promises as fsPromises, readdirSync, writeFileSync} from 'fs'
10+
import {fileURLToPath} from 'url';
11+
import * as path from 'path'
12+
import lodash from 'lodash'
13+
// for now we will have duplicate utils until the main package
14+
// removes bootstrap as a JS dependency
15+
import { pascalCase} from 'bootstrap-vue-3-icons'
16+
17+
18+
// Bootstrap Icons package.json
19+
/* eslint-disable no-eval */
20+
import bsIconsPkg from '../package.json' assert {type: 'json'}
21+
22+
23+
// BootstrapVue icons package.json
24+
// import IconsPkg from '../src/components/Icons/package.json' assert {type: 'json'}
25+
26+
import { createRequire } from "module";
27+
28+
const require = createRequire(import.meta.url);
29+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
30+
31+
//Bootstrap icon asssets and SVGs
32+
const bootstrapIconsBase = path.dirname(require.resolve('bootstrap-icons/package.json'))
33+
const bootstrapIconsDir = path.join(bootstrapIconsBase, 'icons/')
34+
const bsIconsMetaFile = path.join(bootstrapIconsBase, 'package.json')
35+
36+
//Bootstrap vue Icons - where all output files will be relative too
37+
const bvBase = path.resolve(__dirname, '..')
38+
const bvIconsBase = path.join(bvBase, 'src', 'components','Icons')
39+
40+
const iconsFile = path.resolve(bvIconsBase, 'icons.ts')
41+
const pluginFile = path.resolve(bvIconsBase, 'plugin.js')
42+
const typesFile = path.resolve(bvIconsBase, 'icons.d.ts')
43+
const bvIconsPkgFile = path.resolve(bvIconsBase, 'package.json')
44+
45+
// // --- Constants ---
46+
47+
48+
// // BootstrapVue icons package.json
49+
// const bvIconsPkg = require(bvIconsPkgFile)
50+
51+
// if (bvIconsPkg.meta['bootstrap-icons-version'] === bsIconsPkg.version) {
52+
// // Exit early of no changes in bootstrap-icons version
53+
// // Should also test if `src/icons/helper/make-icons.js` has changed (i.e. new props)
54+
// // console.log(' No changes detected in bootstrap-icons version')
55+
// // Commented out until this build process is stabilized
56+
// // exit 0
57+
// }
58+
59+
// Template for `src/icons/icons.js`
60+
const iconsTemplateFn = lodash.template(`// --- BEGIN AUTO-GENERATED FILE ---
61+
//
62+
// @IconsVersion: <%= version %>
63+
// @Generated: <%= created %>
64+
//
65+
// This file is generated on each build. Do not edit this file!
66+
/*!
67+
* BootstrapVue Icons, generated from Bootstrap Icons <%= version %>
68+
*
69+
* @link <%= homepage %>
70+
* @license <%= license %>
71+
* https://github.com/twbs/icons/blob/master/LICENSE.md
72+
*/
73+
import { makeIcon } from './helpers/make-icon'
74+
// --- BootstrapVue custom icons ---
75+
export const BIconBlank = /*#__PURE__*/ makeIcon('Blank', '')
76+
// --- Bootstrap Icons ---
77+
<% componentNames.forEach(component => { %>
78+
// eslint-disable-next-line
79+
export const <%= component %> = /*#__PURE__*/ makeIcon(
80+
'<%= icons[component].name %>',
81+
'<%= icons[component].content %>'
82+
)
83+
<% }) %>
84+
// --- END AUTO-GENERATED FILE ---
85+
`)
86+
87+
// Template for `src/icons/plugin.js`
88+
const pluginTemplateFn = lodash.template(`// --- BEGIN AUTO-GENERATED FILE ---
89+
//
90+
// @IconsVersion: <%= version %>
91+
// @Generated: <%= created %>
92+
//
93+
// This file is generated on each build. Do not edit this file!
94+
// import { pluginFactoryNoConfig } from '../utils/plugins'
95+
// Icon helper component
96+
import BIcon from '../components/BIcon.vue'
97+
// Icon stacking component
98+
import BIconstack from '../components/BIconstack.vue'
99+
import {
100+
// BootstrapVue custom icons
101+
BIconBlank,
102+
// Bootstrap icons
103+
<%= componentNames.join(',\\n ') %>
104+
} from './icons'
105+
// Icon component names for used in the docs
106+
export const iconNames = [
107+
// BootstrapVue custom icon component names
108+
'BIconBlank',
109+
// Bootstrap icon component names
110+
<%= componentNames.map(n => ("'" + n + "'")).join(',\\n ') %>
111+
]
112+
// Export the icons plugin
113+
// export const IconsPlugin = /*#__PURE__*/ pluginFactoryNoConfig({
114+
// components: {
115+
// // Icon helper component
116+
// BIcon,
117+
// // Icon stacking component
118+
// BIconstack,
119+
// // BootstrapVue custom icon components
120+
// BIconBlank,
121+
// // Bootstrap icon components
122+
// <%= componentNames.join(',\\n ') %>
123+
// }
124+
// })
125+
// Export the BootstrapVueIcons plugin installer
126+
// Mainly for the stand-alone bootstrap-vue-icons.xxx.js builds
127+
export const BootstrapVueIcons = /*#__PURE__*/ pluginFactoryNoConfig(
128+
{ plugins: { IconsPlugin } },
129+
{ NAME: 'BootstrapVueIcons' }
130+
)
131+
// --- END AUTO-GENERATED FILE ---
132+
`)
133+
134+
// Template for `src/icons/icons.d.ts`
135+
const typesTemplateFn = lodash.template(`// --- BEGIN AUTO-GENERATED FILE ---
136+
//
137+
// @IconsVersion: <%= version %>
138+
// @Generated: <%= created %>
139+
//
140+
// This file is generated on each build. Do not edit this file!
141+
import Vue from 'vue'
142+
import { BvComponent } from '../'
143+
// --- BootstrapVue custom icons ---
144+
export declare class BIconBlank extends BvComponent {}
145+
// --- Bootstrap Icons ---
146+
<% componentNames.forEach(component => { %>
147+
export declare class <%= component %> extends BvComponent {}
148+
<% }) %>
149+
// --- END AUTO-GENERATED FILE ---
150+
`)
151+
152+
// // --- Utility methods ---
153+
154+
155+
156+
// Parses a single SVG File
157+
const processFile = (file, data) =>{
158+
return new Promise((resolve, reject) => {
159+
file = path.join(bootstrapIconsDir, file)
160+
if (path.extname(file) !== '.svg') {
161+
resolve()
162+
return
163+
}
164+
const name = pascalCase(path.basename(file, '.svg'))
165+
const componentName = `BIcon${name}`
166+
fsPromises.readFile(file, 'utf8')
167+
.then(svg => {
168+
const content = svg
169+
// Remove <svg ...> and </svg>
170+
.replace(/<svg[^>]+>/i, '')
171+
.replace(/<\/svg>/i, '')
172+
// Remove whitespace between elements
173+
.replace(/>\s+</g, '><')
174+
// Fix broken stroke colors in some components
175+
// Might be fixed in 1.0.0-alpha3 release
176+
.replace(' stroke="#000"', ' stroke="currentColor"')
177+
// Remove leading/trailing whitespace
178+
.trim()
179+
// Add to the iconsData object
180+
data.icons[componentName] = { name, content }
181+
data.componentNames.push(componentName)
182+
// Resolve
183+
resolve()
184+
})
185+
.catch(error => reject(error))
186+
})
187+
}
188+
189+
// Method to generate the updated `package.json` content
190+
const updatePkgMeta = data => {
191+
// Create a semi-deep clone of the current `package.json`
192+
const newPkg = { ...bvIconsPkg, meta: { ...bvIconsPkg.meta } }
193+
// Grab current component entries array and filter out auto-generated entries
194+
const metaComponents = bvIconsPkg.meta.components.filter(c => !c['auto-gen'])
195+
// Grab the props definition array from `BIcon` and filter out `icon` prop
196+
const iconProps = metaComponents
197+
.find(m => m.component === 'BIcon')
198+
.props.filter(p => p.prop !== 'icon')
199+
// Build the icon component entries
200+
const iconMeta = data.componentNames.map(name => {
201+
return {
202+
component: name,
203+
'auto-gen': `bootstrap-icons ${data.version}`,
204+
props: iconProps
205+
}
206+
})
207+
// Update the package components meta info
208+
newPkg.meta.components = [...metaComponents, ...iconMeta]
209+
// Update the bootstrap-icons-version reference
210+
newPkg.meta['bootstrap-icons-version'] = data.version
211+
// Return the updated `package.json` as a json string
212+
return `${JSON.stringify(newPkg, null, 2)}\n`
213+
}
214+
215+
// --- Main process ---
216+
const main = async () => {
217+
// Information needed in the templates
218+
const today = new Date()
219+
const data = {
220+
version: bsIconsPkg.version,
221+
license: bsIconsPkg.license,
222+
homepage: bsIconsPkg.homepage,
223+
created: today.toISOString(),
224+
componentNames: [],
225+
icons: {}
226+
}
227+
228+
console.log(` Reading SVGs from bootstrap-icons version ${data.version}`)
229+
230+
// Read in the list of SVG Files
231+
const files = readdirSync(bootstrapIconsDir)
232+
// Process the SVG Data for all files
233+
await Promise.all(files.map(file => processFile(file, data)))
234+
// Sort the icon component names
235+
data.componentNames = data.componentNames.sort()
236+
237+
console.log(` Read ${data.componentNames.length} SVGs...`)
238+
239+
// Write out the files
240+
console.log(' Creating icon components...')
241+
writeFileSync(iconsFile, iconsTemplateFn(data), 'utf8')
242+
console.log(` Wrote to ${iconsFile}`)
243+
console.log(' Creating icon plugin...')
244+
writeFileSync(pluginFile, pluginTemplateFn(data), 'utf8')
245+
console.log(` Wrote to ${pluginFile}`)
246+
console.log(' Creating type declarations...')
247+
writeFileSync(typesFile, typesTemplateFn(data), 'utf8')
248+
console.log(` Wrote to ${typesFile}`)
249+
console.log(' Updating icons meta info...')
250+
// writeFileSync(bvIconsPkgFile, updatePkgMeta(data), 'utf8')
251+
// console.log(` Wrote to ${bvIconsPkgFile}`)
252+
}
253+
254+
main()

packages/bootstrap-vue-3-icons/src/BootstrapVueIcons.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export {BIcon, BIconstack}
1313
// Export types
1414
export type {Animation, IconSize} from './types'
1515

16+
export {kebabCase, pascalCase} from './utils'
17+
18+
export * from './components/icons/icons'
19+
1620
// Inject all components into the global @vue/runtime-core
1721
// This allows intellisense in templates w/out direct importing
1822
declare module '@vue/runtime-core' {

packages/bootstrap-vue-3-icons/src/components/BIcon/BIconBase.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default /* #__PURE__ */ defineComponent({
3838
3939
const hasScale = computed(() => props.flipH || props.flipV || computedScale.value !== 1)
4040
const hasShift = computed(() => computedShiftH.value || computedShiftV.value)
41-
// const hasContent = computed(() => props.content !== null && props.content !== undefined)
41+
const hasContent = computed(() => props.content !== null && props.content !== undefined)
4242
const hasTransforms = computed(() => hasScale.value || props.rotate)
4343
4444
const transforms = computed(() =>
@@ -93,8 +93,9 @@ export default /* #__PURE__ */ defineComponent({
9393
'g',
9494
{
9595
transform: svgTransform.value,
96+
innerHTML: hasContent ? props.content || '' : {}
9697
},
97-
[props.content, normalizeSlot('default', {}, slots)]
98+
[normalizeSlot('default', {}, slots)]
9899
)
99100
100101
// If needed, we wrap in an additional `<g>` in order to handle the shifting
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { kebabCase, pascalCase } from '../../../utils/string'
2+
import BVIconBase from '../../BIcon/BIconBase.vue'
3+
import { defineComponent, h } from 'vue'
4+
5+
6+
/**
7+
* Icon component generator function
8+
*
9+
* @param {string} icon name (minus the leading `BIcon`)
10+
* @param {string} raw `innerHTML` for SVG
11+
* @return {VueComponent}
12+
*/
13+
export const makeIcon = (name : string, content : string) => {
14+
// For performance reason we pre-compute some values, so that
15+
// they are not computed on each render of the icon component
16+
17+
return defineComponent(
18+
{
19+
name: name,
20+
extends : BVIconBase,
21+
setup(props){
22+
23+
const kebabName = kebabCase(name)
24+
const iconName = `BIcon${pascalCase(name)}`
25+
const iconNameClass = `bi-${kebabName}`
26+
const iconTitle = kebabName.replace(/-/g, ' ')
27+
const svgContent = (content || '').trim()
28+
29+
return () =>{
30+
return h(BVIconBase,
31+
{title : iconTitle,
32+
'aria-label': iconTitle ,
33+
class: [iconNameClass],
34+
content: svgContent
35+
},
36+
)
37+
}
38+
39+
}
40+
}
41+
)
42+
43+
}

0 commit comments

Comments
 (0)