-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathindex.ts
More file actions
346 lines (313 loc) · 10.7 KB
/
index.ts
File metadata and controls
346 lines (313 loc) · 10.7 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
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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
import MagicString, { type SourceMapOptions } from "magic-string";
import { type Template, type TemplatePart, type ParseLiteralsOptions, parseLiterals } from "parse-literals";
import { type Strategy, defaultMinifyOptions, defaultStrategy } from "./strategy.js";
/**
* Options for <code>minifyHTMLLiterals()</code>.
*/
export type Options = DefaultOptions | CustomOptions<any>;
/**
* Options for <code>minifyHTMLLiterals()</code>, using default html-minifier
* strategy.
*/
export interface DefaultOptions extends BaseOptions {
/**
* <code>html-minifier</code> options to use. Defaults to
* <code>defaultMinifyOptions</code>, for production-ready minification.
*/
minifyOptions?: Partial<typeof defaultMinifyOptions>;
}
/**
* Options for <code>minifyHTMLLiterals()</code>, using a custom strategy.
*/
export interface CustomOptions<S extends Strategy> extends BaseOptions {
/**
* HTML minification options.
*/
minifyOptions?: S extends Strategy<infer O> ? Partial<O> : never;
/**
* Override the default strategy for how to minify HTML. The default is to
* use <code>html-minifier</code>.
*/
strategy: S;
}
/**
* Options for <code>minifyHTMLLiterals()</code>.
*/
export interface BaseOptions {
/**
* The name of the file. This is used to determine how to parse the source
* code and for source map filenames. It may be a base name, relative, or
* absolute path.
*/
fileName?: string;
/**
* Override how source maps are generated. Set to false to disable source map
* generation.
*
* @param ms the MagicString instance with code modifications
* @param fileName the name or path of the file
* @returns a v3 SourceMap or undefined
*/
generateSourceMap?: ((ms: MagicStringLike, fileName: string) => SourceMap | undefined) | false;
/**
* The MagicString-like constructor to use. MagicString is used to replace
* strings and generate source maps.
*
* Override if you want to set your own version of MagicString or change how
* strings are overridden. Use <code>generateSourceMap</code> if you want to
* change how source maps are created.
*/
MagicString?: { new (source: string): MagicStringLike };
/**
* Override how template literals are parsed from a source string.
*/
parseLiterals?: typeof parseLiterals;
/**
* Options for <code>parseLiterals()</code>.
*/
parseLiteralsOptions?: Partial<ParseLiteralsOptions>;
/**
* Determines whether or not a template should be minified. The default is to
* minify all tagged template whose tag name contains "html" (case
* insensitive).
*
* @param template the template to check
* @returns true if the template should be minified
*/
shouldMinify?(template: Template): boolean;
/**
* Determines whether or not a CSS template should be minified. The default is
* to minify all tagged template whose tag name contains "css" (case
* insensitive).
*
* @param template the template to check
* @returns true if the template should be minified
*/
shouldMinifyCSS?(template: Template): boolean;
/**
* Override custom validation or set to false to disable validation. This is
* only useful when implementing your own strategy that may return
* unexpected results.
*/
validate?: Validation | false;
}
/**
* A MagicString-like instance. <code>minify-literals</code> only uses a
* subset of the MagicString API to overwrite the source code and generate
* source maps.
*/
export interface MagicStringLike {
generateMap(options?: Partial<SourceMapOptions>): SourceMap;
overwrite(start: number, end: number, content: string): any;
toString(): string;
}
/**
* A v3 SourceMap.
*
* <code>magic-string> incorrectly declares the SourceMap type with a version
* string instead of a number, so <code>minify-literals</code> declares
* its own type.
*/
export interface SourceMap {
version: number | string;
file: string | null;
sources: Array<string | null>;
sourcesContent: Array<string | null>;
names: string[];
mappings: string;
toString(): string;
toUrl(): string;
}
/**
* Validation that is executed when minifying HTML to ensure there are no
* unexpected errors. This is to alleviate hard-to-troubleshoot errors such as
* undefined errors.
*/
export interface Validation {
/**
* Throws an error if <code>strategy.getPlaceholder()</code> does not return
* a valid placeholder string.
*
* @param placeholder the placeholder to check
*/
ensurePlaceholderValid(placeholder: any): void;
/**
* Throws an error if <code>strategy.splitHTMLByPlaceholder()</code> does not
* return an HTML part string for each template part.
*
* @param parts the template parts that generated the strings
* @param htmlParts the split HTML strings
*/
ensureHTMLPartsValid(parts: TemplatePart[], htmlParts: string[]): void;
}
/**
* The result of a call to <code>minifyHTMLLiterals()</code>.
*/
export interface Result {
/**
* The minified code.
*/
code: string;
/**
* Optional v3 SourceMap for the code.
*/
map?: SourceMap | undefined;
}
/**
* The default method to generate a SourceMap. It will generate the SourceMap
* from the provided MagicString instance using "fileName.map" as the file and
* "fileName" as the source.
*
* @param ms the MagicString instance with code modifications
* @param fileName the name of the source file
* @returns a v3 SourceMap
*/
export function defaultGenerateSourceMap(ms: MagicStringLike, fileName: string) {
return ms.generateMap({
file: `${fileName}.map`,
source: fileName,
hires: true,
});
}
/**
* The default method to determine whether or not to minify a template. It will
* return true for all tagged templates whose tag name contains "html" (case
* insensitive).
*
* @param template the template to check
* @returns true if the template should be minified
*/
export function defaultShouldMinify(template: Template) {
const tag = template.tag?.toLowerCase();
return !!tag && (tag.includes("html") || tag.includes("svg"));
}
/**
* The default method to determine whether or not to minify a CSS template. It
* will return true for all tagged templates whose tag name contains "css" (case
* insensitive).
*
* @param template the template to check
* @returns true if the template should be minified
*/
export function defaultShouldMinifyCSS(template: Template) {
if (!template?.tag?.toLowerCase().includes("css")) return false;
return true;
}
/**
* The default validation.
*/
export const defaultValidation: Validation = {
ensurePlaceholderValid(placeholder) {
if (typeof placeholder === "string" && placeholder.length > 0) {
return;
}
if (Array.isArray(placeholder) && placeholder.every((ph) => ph.length > 0)) {
return;
}
throw new Error("getPlaceholder() must return a non-empty string | string[]");
},
ensureHTMLPartsValid(parts, htmlParts) {
if (parts.length !== htmlParts.length) {
throw new Error("splitHTMLByPlaceholder() must return same number of strings as template parts");
}
},
};
/**
* Minifies all HTML template literals in the provided source string.
*
* @param source the source code
* @param options minification options
* @returns the minified code, or null if no minification occurred.
*/
export async function minifyHTMLLiterals(source: string, options?: DefaultOptions): Promise<Result | null>;
/**
* Minifies all HTML template literals in the provided source string.
*
* @param source the source code
* @param options minification options
* @returns the minified code, or null if no minification occurred.
*/
export async function minifyHTMLLiterals<S extends Strategy>(
source: string,
options?: CustomOptions<S>,
): Promise<Result | null>;
export async function minifyHTMLLiterals(source: string, options: Options = {}): Promise<Result | null> {
options.MagicString = (options.MagicString || MagicString) as typeof options.MagicString;
options.parseLiterals = options.parseLiterals || parseLiterals;
options.shouldMinify = options.shouldMinify || defaultShouldMinify;
options.shouldMinifyCSS = options.shouldMinifyCSS || defaultShouldMinifyCSS;
options.minifyOptions = {
...defaultMinifyOptions,
...options.minifyOptions,
};
options.parseLiteralsOptions = {
fileName: options.fileName,
...options.parseLiteralsOptions,
};
const templates = options.parseLiterals(source, options.parseLiteralsOptions);
const strategy = <Strategy>(<CustomOptions<any>>options).strategy || defaultStrategy;
const { shouldMinify, shouldMinifyCSS } = options;
let validate: Validation | undefined;
if (options.validate !== false) {
validate = options.validate || defaultValidation;
}
let skipCSS = false;
let skipHTML = false;
if (strategy.minifyCSS && source.includes("unsafeCSS")) {
console.warn(
`minify-literals: unsafeCSS() detected in source. CSS minification will not be performed for this file.`,
);
skipCSS = true;
}
if (source.includes("unsafeHTML")) {
console.warn(
`minify-literals: unsafeHTML() detected in source. HTML minification will not be performed for this file.`,
);
skipHTML = true;
}
if (!options.MagicString) throw new Error("MagicString is required, this should never happen");
const ms = new options.MagicString(source);
const promises = templates.map(async (template) => {
const minifyHTML = !skipHTML && shouldMinify(template);
const minifyCSS = !skipCSS && strategy.minifyCSS && shouldMinifyCSS(template);
if (!(minifyHTML || minifyCSS)) return;
const placeholder = strategy.getPlaceholder(template.parts, template.tag);
if (validate) {
validate.ensurePlaceholderValid(placeholder);
}
const combined = strategy.combineHTMLStrings(template.parts, placeholder);
let min: string;
if (minifyCSS) {
const minifyCSSOptions = (options as DefaultOptions).minifyOptions?.minifyCSS;
if (typeof minifyCSSOptions === "function") {
min = minifyCSSOptions(combined);
} else if (minifyCSSOptions === false) {
min = combined;
} else {
const cssOptions = typeof minifyCSSOptions === "object" ? minifyCSSOptions : undefined;
min = (await strategy.minifyCSS?.(combined, cssOptions)) ?? combined;
}
} else {
min = await strategy.minifyHTML(combined, options.minifyOptions);
}
const minParts = strategy.splitHTMLByPlaceholder(min, placeholder);
if (validate) validate.ensureHTMLPartsValid(template.parts, minParts);
for (const [index, part] of template.parts.entries()) {
if (part.start < part.end)
// Only overwrite if the literal part has text content
ms.overwrite(part.start, part.end, minParts[index] ?? "");
}
});
await Promise.all(promises);
const sourceMin = ms.toString();
if (source === sourceMin) return null;
let map: SourceMap | undefined;
if (options.generateSourceMap !== false) {
const generateSourceMap = options.generateSourceMap || defaultGenerateSourceMap;
map = generateSourceMap(ms, options.fileName || "");
}
return {
map,
code: sourceMin,
};
}