-
-
Notifications
You must be signed in to change notification settings - Fork 2k
/
Copy pathbuild-templates.js
262 lines (215 loc) · 7.37 KB
/
build-templates.js
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
import fs from 'fs';
import path from 'path';
import parser from 'gitignore-parser';
import prettier from 'prettier';
import { transform } from 'sucrase';
import glob from 'tiny-glob/sync.js';
import { mkdirp, rimraf } from '../utils.js';
/** @param {string} content */
function convert_typescript(content) {
const transformed = transform(content, {
transforms: ['typescript']
});
return prettier.format(transformed.code, {
parser: 'babel',
useTabs: true,
singleQuote: true,
trailingComma: 'none',
printWidth: 100
});
}
/** @param {string} content */
function strip_jsdoc(content) {
return content.replace(/\/\*\*[\s\S]+?\*\/[\s\n]+/g, '');
}
/** @param {Set<string>} shared */
async function generate_templates(shared) {
const templates = fs.readdirSync('templates');
for (const template of templates) {
const dir = `dist/templates/${template}`;
const assets = `${dir}/assets`;
mkdirp(assets);
const cwd = path.resolve('templates', template);
const gitignore_file = path.join(cwd, '.gitignore');
if (!fs.existsSync(gitignore_file)) throw new Error('Template must have a .gitignore file');
const gitignore = parser.compile(fs.readFileSync(gitignore_file, 'utf-8'));
const ignore_file = path.join(cwd, '.ignore');
if (!fs.existsSync(ignore_file)) throw new Error('Template must have a .ignore file');
const ignore = parser.compile(fs.readFileSync(ignore_file, 'utf-8'));
const meta_file = path.join(cwd, '.meta.json');
if (!fs.existsSync(meta_file)) throw new Error('Template must have a .meta.json file');
/** @type {Record<string, import('../types/internal.js').File[]>} */
const types = {
typescript: [],
checkjs: [],
null: []
};
glob('**/*', { cwd, filesOnly: true, dot: true }).forEach((name) => {
// the package.template.json thing is a bit annoying — basically we want
// to be able to develop and deploy the app from here, but have a different
// package.json in newly created projects (based on package.template.json)
if (name === 'package.template.json') {
let contents = fs.readFileSync(path.join(cwd, name), 'utf8');
// TODO package-specific versions
contents = contents.replace(/workspace:\*/g, 'next');
fs.writeFileSync(`${dir}/package.json`, contents);
return;
}
// ignore files that are written conditionally
if (shared.has(name)) return;
// ignore contents of .gitignore or .ignore
if (!gitignore.accepts(name) || !ignore.accepts(name) || name === '.ignore') return;
if (/\.(ts|svelte)$/.test(name)) {
const contents = fs.readFileSync(path.join(cwd, name), 'utf8');
if (name.endsWith('.d.ts')) {
if (name.endsWith('app.d.ts')) types.checkjs.push({ name, contents });
types.typescript.push({ name, contents });
} else if (name.endsWith('.ts')) {
const js = convert_typescript(contents);
types.typescript.push({
name,
contents: strip_jsdoc(contents)
});
types.checkjs.push({
name: name.replace(/\.ts$/, '.js'),
contents: js
});
types.null.push({
name: name.replace(/\.ts$/, '.js'),
contents: strip_jsdoc(js)
});
} else {
// we jump through some hoops, rather than just using svelte.preprocess,
// so that the output preserves the original formatting to the extent
// possible (e.g. preserving double line breaks). Sucrase is the best
// tool for the job because it just removes the types; Prettier then
// tidies up the end result
const js_contents = contents.replace(
/<script([^>]+)>([\s\S]+?)<\/script>/g,
(m, attrs, typescript) => {
// Sucrase assumes 'unused' imports (which _are_ used, but only
// in the markup) are type imports, and strips them. This step
// prevents it from drawing that conclusion
const imports = [];
const import_pattern = /import (.+?) from/g;
let import_match;
while ((import_match = import_pattern.exec(typescript))) {
const word_pattern = /[a-z_$][a-z0-9_$]*/gi;
let word_match;
while ((word_match = word_pattern.exec(import_match[1]))) {
imports.push(word_match[0]);
}
}
const suffix = `\n${imports.join(',')}`;
const transformed = transform(typescript + suffix, {
transforms: ['typescript']
}).code.slice(0, -suffix.length);
const contents = prettier
.format(transformed, {
parser: 'babel',
useTabs: true,
singleQuote: true,
trailingComma: 'none',
printWidth: 100
})
.trim()
.replace(/^(.)/gm, '\t$1');
return `<script${attrs.replace(' lang="ts"', '')}>\n${contents}\n</script>`;
}
);
types.typescript.push({
name,
contents: strip_jsdoc(contents)
});
types.checkjs.push({
name,
contents: js_contents
});
types.null.push({
name,
contents: strip_jsdoc(js_contents)
});
}
} else {
const dest = path.join(assets, name.replace(/^\./, 'DOT-'));
mkdirp(path.dirname(dest));
fs.copyFileSync(path.join(cwd, name), dest);
}
});
fs.copyFileSync(meta_file, `${dir}/meta.json`);
fs.writeFileSync(
`${dir}/files.types=typescript.json`,
JSON.stringify(types.typescript, null, '\t')
);
fs.writeFileSync(`${dir}/files.types=checkjs.json`, JSON.stringify(types.checkjs, null, '\t'));
fs.writeFileSync(`${dir}/files.types=null.json`, JSON.stringify(types.null, null, '\t'));
}
}
async function generate_shared() {
const cwd = path.resolve('shared');
/** @type {Set<string>} */
const shared = new Set();
/** @type {Array<{ name: string, include: string[], exclude: string[], contents: string }>} */
const files = [];
glob('**/*', { cwd, filesOnly: true, dot: true }).forEach((file) => {
const contents = fs.readFileSync(path.join(cwd, file), 'utf8');
/** @type {string[]} */
const include = [];
/** @type {string[]} */
const exclude = [];
let name = file;
if (file.startsWith('+') || file.startsWith('-')) {
const [conditions, ...rest] = file.split(path.sep);
const pattern = /([+-])([a-z]+)/g;
let match;
while ((match = pattern.exec(conditions))) {
const set = match[1] === '+' ? include : exclude;
set.push(match[2]);
}
name = rest.join('/');
}
if (name.endsWith('.ts') && !include.includes('typescript')) {
// file includes types in TypeScript and JSDoc —
// create .js file, with and without JSDoc
const js = convert_typescript(contents);
const js_name = name.replace(/\.ts$/, '.js');
// typescript
files.push({
name,
include: [...include, 'typescript'],
exclude,
contents: strip_jsdoc(contents)
});
// checkjs
files.push({
name: js_name,
include: [...include, 'checkjs'],
exclude,
contents: js
});
// no typechecking
files.push({
name: js_name,
include,
exclude: [...exclude, 'typescript', 'checkjs'],
contents: strip_jsdoc(js)
});
shared.add(name);
shared.add(js_name);
} else {
shared.add(name);
files.push({ name, include, exclude, contents });
}
});
files.sort((a, b) => a.include.length + a.exclude.length - (b.include.length + b.exclude.length));
fs.writeFileSync('dist/shared.json', JSON.stringify({ files }, null, '\t'));
shared.delete('package.json');
return shared;
}
async function main() {
rimraf('dist');
mkdirp('dist');
const shared = await generate_shared();
await generate_templates(shared);
}
main();