-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Expand file tree
/
Copy pathgenerate-library.js
More file actions
192 lines (164 loc) · 5.47 KB
/
generate-library.js
File metadata and controls
192 lines (164 loc) · 5.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
/**
* External dependencies
*/
const path = require( 'path' );
const { readdir, readFile, writeFile } = require( 'fs' ).promises;
const { execFile } = require( 'child_process' );
const { promisify } = require( 'util' );
const execFileAsync = promisify( execFile );
const ICON_LIBRARY_DIR = path.join( __dirname, '..', 'src', 'library' );
// - Find *.svg files in ./library
// - For each, generate a sibling .tsx file
// - Build an index of these at ./library/index.ts
//
// Note that the generated files are ignored by Git.
// The SOURCE OF TRUTH for this package's library of icons consists of the SVG
// files found under `src/library`. We must thus first generate the TSX files
// corresponding to each SVG file, as well as an index of imports at
// `src/library/index.ts`.
async function main() {
await ensureSvgFilesTracked();
await cleanup();
await generateTsxFiles();
await generateIndex();
}
// Before automatically generating TSX files from SVG ones, ensure that all
// SVGs found are intended to be processed. If they aren't under version
// control, there is a chance that their presence is accidental, so halt.
async function ensureSvgFilesTracked() {
let untrackedFiles;
try {
// Avoid invoking `ls-files` with a wildcard (`*.svg`) due to the
// variability of wildcard behaviour across shells.
const { stdout } = await execFileAsync( 'git', [
'ls-files',
'-o',
'--full-name',
ICON_LIBRARY_DIR,
] );
// Filtering with `grep` in a single `exec` call was tempting, but this
// manual filtering avoids any shell escaping weirdness.
untrackedFiles = stdout
.trim()
.split( '\n' )
.filter( ( f ) => f.endsWith( '.svg' ) );
} catch {
return;
}
if ( untrackedFiles.length > 0 ) {
throw new Error(
`The following SVG files are not under version control:\n\n${ untrackedFiles
.map( ( file ) => ` - ${ file }` )
.join(
'\n'
) }\n\nPlease either delete them or add them to Git first:\n\n\tgit add ${ untrackedFiles.join(
' '
) }\n`
);
}
}
async function cleanup() {
await execFileAsync( 'git', [ 'clean', '-Xfq', ICON_LIBRARY_DIR ] );
}
// Generate src/library/*.tsx based on the available SVG files.
async function generateTsxFiles() {
const svgFiles = ( await readdir( ICON_LIBRARY_DIR ) ).filter( ( file ) =>
// Stricter than just checking for SVG suffix, thereby avoiding hidden
// files and characters that would get in the way of camel-casing.
file.match( /^[a-z0-9--]+\.svg$/ )
);
await Promise.all(
svgFiles.map( async ( svgFile ) => {
const svgPath = path.join( ICON_LIBRARY_DIR, svgFile );
const svgContent = await readFile( svgPath, 'utf8' );
const componentContent = svgToTsx( svgContent );
if ( ! componentContent ) {
throw new Error(
`Could not generate icon element from ${ svgPath }`
);
}
const tsxPath = svgPath.replace( /\.svg$/, '.tsx' );
await writeFile( tsxPath, componentContent );
} )
);
}
// Generate src/library/index.ts as a list of exports of the library's modules.
async function generateIndex() {
const tsxFiles = ( await readdir( ICON_LIBRARY_DIR ) ).filter( ( file ) =>
file.endsWith( '.tsx' )
);
let indexTemplate = tsxFiles
.map( ( file ) => {
const importPath = path.basename( file, '.tsx' );
// Camel case, but retaining 'RTL' acronym in uppercase
const identifier = importPath
.replace( /-([0-9A-Za-z])/g, ( _, c ) => c.toUpperCase() )
.replace( /Rtl\b/, 'RTL' );
return `export { default as ${ identifier } } from './${ importPath }';`;
} )
.join( '\n' );
// Trailing newlines make ESLint happy
indexTemplate += '\n';
await writeFile( path.join( ICON_LIBRARY_DIR, 'index.ts' ), indexTemplate );
}
// "Transform" to TSX by interpolating the SVG source into a simple TS module
// with a single default export.
//
// Detect SVG tags like `<circle>` and promote them to WordPress primitives
// like `<Circle />`, taking care of importing those primitives first.
function svgToTsx( svgContent ) {
let jsxContent = svgContent.trim();
jsxContent = jsxContent.replace( /\sclass=/g, ' className=' );
// Tags that ought to be converted to WordPress primitives when converting
// SVGs to React elements
const primitives = {
circle: 'Circle',
clippath: 'ClipPath',
defs: 'Defs',
ellipse: 'Ellipse',
g: 'G',
line: 'Line',
path: 'Path',
polygon: 'Polygon',
polyline: 'Polyline',
rect: 'Rect',
svg: 'SVG',
};
// Prepare regular expressions to match opening tags and closing tags to
// transform to primitives: <circle ...>, </circle>, etc.
const tagsRe = Object.keys( primitives ).join( '|' );
const openRe = new RegExp( `<(${ tagsRe })\\b`, 'g' );
const closeRe = new RegExp( `<\/(${ tagsRe })>`, 'g' );
// Keep track of primitives used in the SVG body to later generate the
// imports statement
const usedPrimitives = new Set();
// Transform from <circle> to <Circle>, etc.
jsxContent = jsxContent
.replace( openRe, ( _, tagName ) => {
const primitive = primitives[ tagName ];
usedPrimitives.add( primitive );
return `<${ primitive }`;
} )
.replace( closeRe, ( _, tagName ) => {
const primitive = primitives[ tagName ];
return `</${ primitive }>`;
} );
// Indent by one level
jsxContent = jsxContent
.split( '\n' )
.map( ( line ) => '\t' + line )
.join( '\n' );
return `/* eslint-disable prettier/prettier */
/**
* WordPress dependencies
*/
import { ${ Array.from( usedPrimitives )
.sort()
.join( ', ' ) } } from '@wordpress/primitives';
export default (
${ jsxContent }
);
/* eslint-enable */
`;
}
main();