@charcoal-ui/icons-cli 1.0.0-alpha.1
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.
- package/LICENSE +201 -0
- package/README.md +78 -0
- package/dist/FigmaFileClient.d.ts +23 -0
- package/dist/FigmaFileClient.d.ts.map +1 -0
- package/dist/GitHubClient.d.ts +1173 -0
- package/dist/GitHubClient.d.ts.map +1 -0
- package/dist/GitlabClient.d.ts +19 -0
- package/dist/GitlabClient.d.ts.map +1 -0
- package/dist/concurrently.d.ts +2 -0
- package/dist/concurrently.d.ts.map +1 -0
- package/dist/getChangedFiles.d.ts +10 -0
- package/dist/getChangedFiles.d.ts.map +1 -0
- package/dist/index.cjs +899 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.modern.js +891 -0
- package/dist/index.modern.js.map +1 -0
- package/dist/index.module.js +889 -0
- package/dist/index.module.js.map +1 -0
- package/dist/optimizeSvg.d.ts +3 -0
- package/dist/optimizeSvg.d.ts.map +1 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +54 -0
- package/src/FigmaFileClient.ts +228 -0
- package/src/GitHubClient.ts +161 -0
- package/src/GitlabClient.ts +95 -0
- package/src/concurrently.ts +10 -0
- package/src/getChangedFiles.ts +65 -0
- package/src/index.ts +115 -0
- package/src/optimizeSvg.ts +101 -0
- package/src/utils.ts +24 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { promises as fs } from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { execp } from './utils'
|
|
4
|
+
|
|
5
|
+
export const targetDir = path.resolve(process.cwd(), 'packages', 'icons', 'svg')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* dir 内で変更があったファイル情報を for await で回せるようにするやつ
|
|
9
|
+
*/
|
|
10
|
+
export async function* getChangedFiles(dir = targetDir) {
|
|
11
|
+
const directories = await fs.readdir(dir)
|
|
12
|
+
const gitStatus = await collectGitStatus()
|
|
13
|
+
|
|
14
|
+
for (const dir of directories) {
|
|
15
|
+
const directory = path.resolve(targetDir, dir)
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line no-await-in-loop
|
|
18
|
+
const stat = await fs.stat(directory)
|
|
19
|
+
if (!stat.isDirectory()) {
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line no-await-in-loop
|
|
24
|
+
const files = await fs.readdir(directory)
|
|
25
|
+
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
const fullpath = path.resolve(targetDir, dir, file)
|
|
28
|
+
const relativePath = path.relative(process.cwd(), fullpath)
|
|
29
|
+
|
|
30
|
+
const status = gitStatus[relativePath]
|
|
31
|
+
if (status == null) {
|
|
32
|
+
// Already up-to-date
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// eslint-disable-next-line no-await-in-loop
|
|
37
|
+
const content = await fs.readFile(fullpath, { encoding: 'utf-8' })
|
|
38
|
+
|
|
39
|
+
yield { relativePath, content, status }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function collectGitStatus() {
|
|
45
|
+
return Object.fromEntries(
|
|
46
|
+
/**
|
|
47
|
+
* @see https://git-scm.com/docs/git-status#_porcelain_format_version_1
|
|
48
|
+
*/
|
|
49
|
+
(await execp(`git status --porcelain`))
|
|
50
|
+
.split('\n')
|
|
51
|
+
.map(
|
|
52
|
+
(s) =>
|
|
53
|
+
[
|
|
54
|
+
s.slice(3),
|
|
55
|
+
s.startsWith(' M')
|
|
56
|
+
? 'modified'
|
|
57
|
+
: s.startsWith('??')
|
|
58
|
+
? 'untracked'
|
|
59
|
+
: s.startsWith(' D')
|
|
60
|
+
? 'deleted'
|
|
61
|
+
: null,
|
|
62
|
+
] as const
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'fs-extra'
|
|
2
|
+
import yargs from 'yargs'
|
|
3
|
+
import { FigmaFileClient } from './FigmaFileClient'
|
|
4
|
+
import { GithubClient } from './GitHubClient'
|
|
5
|
+
import { GitlabClient } from './GitlabClient'
|
|
6
|
+
import { DEFAULT_CURRENT_COLOR_TARGET, optimizeSvg } from './optimizeSvg'
|
|
7
|
+
import { mustBeDefined } from './utils'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Figma
|
|
11
|
+
*/
|
|
12
|
+
const FIGMA_TOKEN = process.env.FIGMA_TOKEN
|
|
13
|
+
const FIGMA_FILE_URL = process.env.FIGMA_FILE_URL
|
|
14
|
+
const OUTPUT_ROOT_DIR = process.env.OUTPUT_ROOT_DIR
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* GitLab
|
|
18
|
+
*/
|
|
19
|
+
const GITLAB_ACCESS_TOKEN = process.env.GITLAB_ACCESS_TOKEN
|
|
20
|
+
const GITLAB_DEFAULT_BRANCH = process.env.GITLAB_DEFAULT_BRANCH
|
|
21
|
+
const GITLAB_HOST = process.env.GITLAB_HOST
|
|
22
|
+
const GITLAB_PROJECT_ID = process.env.GITLAB_PROJECT_ID
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* GitHub
|
|
26
|
+
*/
|
|
27
|
+
const GITHUB_ACCESS_TOKEN = process.env.GITHUB_ACCESS_TOKEN
|
|
28
|
+
const GITHUB_REPO_OWNER = process.env.GITHUB_REPO_OWNER
|
|
29
|
+
const GITHUB_REPO_NAME = process.env.GITHUB_REPO_NAME
|
|
30
|
+
const GITHUB_DEFAULT_BRANCH = process.env.GITHUB_DEFAULT_BRANCH
|
|
31
|
+
|
|
32
|
+
void yargs
|
|
33
|
+
.scriptName('icons-cli')
|
|
34
|
+
.command(
|
|
35
|
+
'figma:export',
|
|
36
|
+
'Load all icons from Figma and save to files',
|
|
37
|
+
() => yargs.option('format', { default: 'svg', choices: ['svg', 'pdf'] }),
|
|
38
|
+
({ format }) => {
|
|
39
|
+
mustBeDefined(FIGMA_FILE_URL, 'FIGMA_FILE_URL')
|
|
40
|
+
mustBeDefined(FIGMA_TOKEN, 'FIGMA_TOKEN')
|
|
41
|
+
mustBeDefined(OUTPUT_ROOT_DIR, 'OUTPUT_ROOT_DIR')
|
|
42
|
+
|
|
43
|
+
void FigmaFileClient.runFromCli(
|
|
44
|
+
FIGMA_FILE_URL,
|
|
45
|
+
FIGMA_TOKEN,
|
|
46
|
+
OUTPUT_ROOT_DIR,
|
|
47
|
+
format as 'svg' | 'pdf'
|
|
48
|
+
).catch((e) => {
|
|
49
|
+
console.error(e)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
.command(
|
|
55
|
+
'svg:optimize',
|
|
56
|
+
'Optimize given .svg files and overwrite it',
|
|
57
|
+
() =>
|
|
58
|
+
yargs
|
|
59
|
+
.option('file', { demandOption: true, type: 'string' })
|
|
60
|
+
.option('color', {
|
|
61
|
+
default: DEFAULT_CURRENT_COLOR_TARGET,
|
|
62
|
+
type: 'string',
|
|
63
|
+
defaultDescription:
|
|
64
|
+
'Color code that should be converted into `currentColor`',
|
|
65
|
+
}),
|
|
66
|
+
({ file, color }) => {
|
|
67
|
+
void readFile(file, { encoding: 'utf-8' })
|
|
68
|
+
.then((content) => optimizeSvg(content, color))
|
|
69
|
+
.then((optimized) => writeFile(file, optimized))
|
|
70
|
+
.catch((e) => {
|
|
71
|
+
console.error(e)
|
|
72
|
+
process.exit(1)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
.command(
|
|
77
|
+
'gitlab:mr',
|
|
78
|
+
'Create a merge request in the name of icons-cli',
|
|
79
|
+
{},
|
|
80
|
+
() => {
|
|
81
|
+
mustBeDefined(GITLAB_PROJECT_ID, 'GITLAB_PROJECT_ID')
|
|
82
|
+
mustBeDefined(GITLAB_ACCESS_TOKEN, 'GITLAB_ACCESS_TOKEN')
|
|
83
|
+
|
|
84
|
+
void GitlabClient.runFromCli(
|
|
85
|
+
GITLAB_HOST ?? 'https://gitlab.com',
|
|
86
|
+
Number(GITLAB_PROJECT_ID),
|
|
87
|
+
GITLAB_ACCESS_TOKEN,
|
|
88
|
+
GITLAB_DEFAULT_BRANCH ?? 'main'
|
|
89
|
+
).catch((e) => {
|
|
90
|
+
console.error(e)
|
|
91
|
+
process.exit(1)
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
.command(
|
|
96
|
+
'github:pr',
|
|
97
|
+
'Create a pull request in the name of icons-cli',
|
|
98
|
+
{},
|
|
99
|
+
() => {
|
|
100
|
+
mustBeDefined(GITHUB_ACCESS_TOKEN, 'GITHUB_ACCESS_TOKEN')
|
|
101
|
+
|
|
102
|
+
void GithubClient.runFromCli(
|
|
103
|
+
GITHUB_REPO_OWNER ?? 'pixiv',
|
|
104
|
+
GITHUB_REPO_NAME ?? 'charcoal',
|
|
105
|
+
GITHUB_ACCESS_TOKEN,
|
|
106
|
+
GITHUB_DEFAULT_BRANCH ?? 'main'
|
|
107
|
+
).catch((e) => {
|
|
108
|
+
console.error(e)
|
|
109
|
+
process.exit(1)
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
.demandCommand()
|
|
114
|
+
.help()
|
|
115
|
+
.parse()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { JSDOM } from 'jsdom'
|
|
2
|
+
import { parseToRgb } from 'polished'
|
|
3
|
+
import type { RgbColor, RgbaColor } from 'polished/lib/types/color'
|
|
4
|
+
import Svgo from 'svgo'
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_CURRENT_COLOR_TARGET = '#858585'
|
|
7
|
+
|
|
8
|
+
const svgo = new Svgo({
|
|
9
|
+
plugins: [
|
|
10
|
+
// NOTICE: SVGO は「svg 内のすべての fill を currentColor に変える」機能しかない
|
|
11
|
+
// icons-cli に必要なのは「特定の黒っぽい色だけ currentColor に変える」機能
|
|
12
|
+
// なので、convertColors plugin は使わない
|
|
13
|
+
// { convertColors: { currentColor: true } },
|
|
14
|
+
{ removeViewBox: true },
|
|
15
|
+
{ removeAttrs: { attrs: ['stroke-opacity', 'fill-opacity'] } },
|
|
16
|
+
],
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export async function optimizeSvg(input: string, convertedColor: string) {
|
|
20
|
+
const { data } = await svgo.optimize(input)
|
|
21
|
+
|
|
22
|
+
const { document } = new JSDOM(data).window
|
|
23
|
+
const svg = document.querySelector('svg')
|
|
24
|
+
if (!svg) {
|
|
25
|
+
throw new Error('optimizeSvg: input string seems not to have <svg>')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
addViewboxToRootSvg(svg)
|
|
29
|
+
convertToCurrentColor(svg, convertedColor)
|
|
30
|
+
|
|
31
|
+
return svg.outerHTML
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TARGET_ATTRS = ['fill', 'stroke']
|
|
35
|
+
|
|
36
|
+
function convertToCurrentColor(svg: SVGSVGElement, convertedColor: string) {
|
|
37
|
+
const targetColor = parseColor(convertedColor)
|
|
38
|
+
if (!targetColor) {
|
|
39
|
+
throw new Error(`${convertedColor} is not a valid color`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const attr of TARGET_ATTRS) {
|
|
43
|
+
const targets = Array.from(svg.querySelectorAll<SVGElement>(`[${attr}]`))
|
|
44
|
+
|
|
45
|
+
for (const el of targets) {
|
|
46
|
+
const value = parseColor(el.getAttribute(attr))
|
|
47
|
+
if (!value) {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!colorEquals(value, targetColor)) {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
el.setAttribute(attr, 'currentColor')
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseColor(value: string | null) {
|
|
61
|
+
if (value == null) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
return parseToRgb(value)
|
|
67
|
+
} catch {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function colorEquals(self: RgbColor | RgbaColor, other: RgbColor | RgbaColor) {
|
|
73
|
+
if (self.red !== other.red) {
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (self.blue !== other.blue) {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (self.green !== other.green) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if ('alpha' in self) {
|
|
86
|
+
if ('alpha' in other) {
|
|
87
|
+
if (self.alpha !== other.alpha) {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function addViewboxToRootSvg(svg: SVGSVGElement) {
|
|
97
|
+
const width = svg.getAttribute('width')!
|
|
98
|
+
const height = svg.getAttribute('height')!
|
|
99
|
+
|
|
100
|
+
svg.setAttribute('viewBox', `0 0 ${width} ${height}`)
|
|
101
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { exec } from 'child_process'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FIXME: util.promisify を使うと node-libs-browser に入っている方が使われてしまい、壊れる
|
|
5
|
+
*/
|
|
6
|
+
export const execp = (command: string) =>
|
|
7
|
+
new Promise<string>((resolve, reject) => {
|
|
8
|
+
exec(command, (err, stdout) => {
|
|
9
|
+
if (err) {
|
|
10
|
+
return reject(err)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return resolve(stdout)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export function mustBeDefined<T>(
|
|
18
|
+
value: T,
|
|
19
|
+
name: string
|
|
20
|
+
): asserts value is NonNullable<T> {
|
|
21
|
+
if (typeof value === 'undefined') {
|
|
22
|
+
throw new TypeError(`${name} must be defined.`)
|
|
23
|
+
}
|
|
24
|
+
}
|