- Notifications
You must be signed in to change notification settings - Fork 253
/
Copy pathseparate-snippets.ts
292 lines (249 loc) · 8.5 KB
/
separate-snippets.ts
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
// [SNIPPET_REGISTRY disabled]
import*ascpfrom"child_process";
import*asfsfrom"fs";
import*aspathfrom"path";
// Regex for comment which must be included in a file for it to be separated
constRE_SNIPPETS_SEPARATION=/\[SNIPPETS_SEPARATION\s+enabled\]/;
// Regex for comment to control the separator suffix
constRE_SNIPPETS_SUFFIX=/\[SNIPPETS_SUFFIX\s+([A-Za-z0-9_]+)\]/;
// Regex for [START] and [END] snippet tags.
constRE_START_SNIPPET=/\[START\s+([A-Za-z_]+)\s*\]/;
constRE_END_SNIPPET=/\[END\s+([A-Za-z_]+)\s*\]/;
// Regex for const = require statements
// TODO: Handle multiline imports?
constRE_REQUIRE=/const{(.+?)}=require\((.+?)\)/;
// Regex for ref docs URLs
// eg. "https://firebase.google.com/docs/reference/js/v8/firebase.User"
constRE_REF_DOCS=/https:\/\/firebase\.google\.com\/docs\/reference\/js\/(.*)/;
// Maps v8 ref docs URLs to their v9 counterpart
constREF_DOCS_MAPPINGS: {[key: string]: string}={
"v8/firebase.User" : "auth.user"
};
typeSnippetsConfig={
enabled: boolean;
suffix: string;
map: Record<string,string[]>;
};
constDEFAULT_SUFFIX="_modular";
functionisBlank(line: string){
returnline.trim().length===0;
}
/**
* Replace all v8 ref doc urls with their v9 counterpart.
*/
functionreplaceRefDocsUrls(lines: string[]){
constoutputLines=[];
for(constlineoflines){
if(line.match(RE_REF_DOCS)){
outputLines.push(line.replace(RE_REF_DOCS,(match: string,p1?: string)=>{
returnp1 ? `https://firebase.google.com/docs/reference/js/${REF_DOCS_MAPPINGS[p1]}` : match;
}));
}else{
outputLines.push(line);
}
}
returnoutputLines;
}
/**
* Replace all const { foo } = require('bar') with import { foo } from 'bar';
*/
functionreplaceRequireWithImport(lines: string[]){
constoutputLines=[];
for(constlineoflines){
if(line.match(RE_REQUIRE)){
outputLines.push(line.replace(RE_REQUIRE,`import {$1} from $2`));
}else{
outputLines.push(line);
}
}
returnoutputLines;
}
/**
* Change all [START foo] and [END foo] to be [START foosuffix] and [END foosuffix]
*/
functionaddSuffixToSnippetNames(lines: string[],snippetSuffix: string){
constoutputLines=[];
for(constlineoflines){
if(line.match(RE_START_SNIPPET)){
outputLines.push(line.replace(RE_START_SNIPPET,`[START $1${snippetSuffix}]`));
}elseif(line.match(RE_END_SNIPPET)){
outputLines.push(
line.replace(RE_END_SNIPPET,`[END $1${snippetSuffix}]`)
);
}else{
outputLines.push(line);
}
}
returnoutputLines;
}
/**
* Remove all left-padding so that the least indented line is left-aligned.
*/
functionadjustIndentation(lines: string[]){
constnonBlankLines=lines.filter((l)=>!isBlank(l));
constindentSizes=nonBlankLines.map((l)=>l.length-l.trimLeft().length);
constminIndent=Math.min(...indentSizes);
constoutputLines=[];
for(constlineoflines){
if(isBlank(line)){
outputLines.push("");
}else{
outputLines.push(line.slice(minIndent));
}
}
returnoutputLines;
}
/**
* If the first line after leading comments is blank, remove it.
*/
functionremoveFirstLineAfterComments(lines: string[]){
constoutputLines=[...lines];
constfirstNonComment=outputLines.findIndex(
(l)=>!l.startsWith("//")
);
if(firstNonComment>=0&&isBlank(outputLines[firstNonComment])){
outputLines.splice(firstNonComment,1);
}
returnoutputLines;
}
/**
* Turns a series of source lines into a standalone snippet file by running
* a series of transformations.
*
* @param lines the lines containing the snippet (including START/END comments)
* @param sourceFile the source file where the original snippet lives (used in preamble)
* @param snippetSuffix the suffix (such as _modular)
*/
functionprocessSnippet(
lines: string[],
sourceFile: string,
snippetSuffix: string
): string{
letoutputLines=[...lines];
// Perform transformations individually, in order
outputLines=replaceRequireWithImport(outputLines);
outputLines=addSuffixToSnippetNames(outputLines,snippetSuffix);
outputLines=adjustIndentation(outputLines);
outputLines=removeFirstLineAfterComments(outputLines);
outputLines=replaceRefDocsUrls(outputLines);
// Add a preamble to every snippet
constpreambleLines=[
`// This snippet file was generated by processing the source file:`,
`// ${sourceFile}`,
`//`,
`// To update the snippets in this file, edit the source and then run`,
`// 'npm run snippets'.`,
``,
];
constcontent=[...preambleLines, ...outputLines].join("\n");
returncontent;
}
/**
* Lists all the files in this repository that should be checked for snippets
*/
functionlistSnippetFiles(): string[]{
constoutput=cp
.execSync(
'find . -type f -name "*.js" -not -path "*node_modules*" -not -path "./snippets*"'
)
.toString();
returnoutput.split("\n").filter((x)=>!isBlank(x));
}
/**
* Collect all the snippets from a file into a map of snippet name to lines.
* @param filePath the file path to read.
*/
functioncollectSnippets(filePath: string): SnippetsConfig{
constfileContents=fs.readFileSync(filePath).toString();
constlines=fileContents.split("\n");
constconfig: SnippetsConfig={
enabled: false,
suffix: DEFAULT_SUFFIX,
map: {},
};
// If a file does not have '// [SNIPPETS_SEPARATION enabled]' in it then
// we don't process it for this script.
config.enabled=lines.some((l)=>!!l.match(RE_SNIPPETS_SEPARATION));
if(!config.enabled){
returnconfig;
}
// If the file contains '// [SNIPPETS_SUFFIX _banana]' we use _banana (or whatever)
// as the suffix. Otherwise we default to _modular.
constsuffixLine=lines.find((l)=>!!l.match(RE_SNIPPETS_SUFFIX));
if(suffixLine){
constm=suffixLine.match(RE_SNIPPETS_SUFFIX);
if(m&&m[1]){
config.suffix=m[1];
}
}
// A temporary array holding the names of snippets we're currently within.
// This allows for handling nested snippets.
letinSnippetNames: string[]=[];
for(constlineoflines){
conststartMatch=line.match(RE_START_SNIPPET);
constendMatch=line.match(RE_END_SNIPPET);
if(startMatch){
// When we find a new [START foo] tag we are now inside snippet 'foo'.
// Until we find an [END foo] tag. All lines we see between now and then
// are part of the snippet content.
constsnippetName=startMatch[1];
if(config.map[snippetName]!==undefined){
thrownewError(`Detected more than one snippet with the tag ${snippetName}!`);
}
config.map[snippetName]=[line];
inSnippetNames.push(snippetName);
}elseif(endMatch){
// When we find a new [END foo] tag we are now exiting snippet 'foo'.
constsnippetName=endMatch[1];
// If we were not aware that we were inside this snippet (no previous START)
// then we hard throw.
if(!inSnippetNames.includes(snippetName)){
thrownewError(
`Unrecognized END tag ${snippetName} in ${filePath}.`
);
}
// Collect this line as the final line of the snippet and then
// remove this snippet name from the list we're tracking.
config.map[snippetName].push(line);
inSnippetNames.splice(inSnippetNames.indexOf(snippetName),1);
}elseif(inSnippetNames.length>0){
// Any line that is not START or END is appended to the list of
// lines for all the snippets we're currently tracking.
for(constsnippetNameofinSnippetNames){
config.map[snippetName].push(line);
}
}
}
returnconfig;
}
asyncfunctionmain(){
constfileNames=listSnippetFiles();
for(constfilePathoffileNames){
constconfig=collectSnippets(filePath);
if(!config.enabled){
continue;
}
constfileSlug=filePath
.replace(".js","")
.replace("./","")
.replace(/\./g,"-");
constsnippetDir=path.join("./snippets",fileSlug);
console.log(
`Processing: ${filePath} --> ${snippetDir} (suffix=${config.suffix})`
);
if(!fs.existsSync(snippetDir)){
fs.mkdirSync(snippetDir,{recursive: true});
}
for(constsnippetNameinconfig.map){
constnewFilePath=path.join(snippetDir,`${snippetName}.js`);
constsnippetLines=config.map[snippetName];
constcontent=processSnippet(
snippetLines,
filePath,
config.suffix
);
fs.writeFileSync(newFilePath,content);
}
}
}
main();