@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.
@@ -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
+ }