UNPKG

vite-plugin-google-apps-script

Version:

Vite plugin for HtmlService on GoogleAppsScript via @google/clasp

158 lines (152 loc) 4.75 kB
import { generate } from '@babel/generator'; import { parse } from '@babel/parser'; import _traverse from '@babel/traverse'; import { createFilter } from 'vite'; function enforceTerser({ useTerserMinify } = { useTerserMinify: true }) { return { name: "vite:enforce-terser", apply: "build", config(config) { if (config.build == null) config.build = {}; if (config.build.minify === "esbuild") { this.warn( `[plugin vite:enforce-terser] The plugin will override the "esbuild" minify option to use "terser" or disable minification.` ); } config.build.minify = useTerserMinify ? "terser" : false; } }; } const presetReplaceRules = [ // for jsDoc comments { from: /\n*\/\*\*[\n\s\S]*?\*\/\n*/g, to: "" }, // for scriptlet of apps script { from: /"(<\?!{0,1}={0,1}.+?\?>)"/g, to: "'$1'" } ]; const defaultOptions = { useDefault: true, replaceRules: [] }; const pluginName = "vite:replace-particular-expression"; const replaceParticularExpression = (options) => { const mergedOptions = { ...defaultOptions, ...options }; return { name: pluginName, apply: "build", enforce: "post", generateBundle(_outputOptions, outputBundle) { const chunkNames = Object.keys(outputBundle); chunkNames.forEach((chunkName) => { const chunk = outputBundle[chunkName]; const configs = [...mergedOptions.replaceRules]; mergedOptions.useDefault !== false && configs.push(...presetReplaceRules); configs.forEach(({ from, to, replacer }) => { const isMatch = typeof from === "string" ? chunk.code.indexOf(from) !== -1 : from.test(chunk.code); if (isMatch) { if (replacer != null) chunk.code = chunk.code.replace(from, replacer); else if (to != null) chunk.code = chunk.code.replace(from, to); } }); outputBundle[chunkName] = chunk; }); } }; }; const traverse = _traverse.default; const DEFAULT_URL_REGEX = /\b(?:https?:\/\/|ftp:\/\/|blob:|data:[^'")\s]+|\/\/)[\w/:%#$&?()~.=+\-{}]+/gi; function sanitizeString(input, pattern) { if (!pattern) return input.replace(DEFAULT_URL_REGEX, ""); if (pattern instanceof RegExp) return input.replace(pattern, ""); return pattern(input); } function toTemplateRaw(cooked) { return cooked.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${"); } function stripUrlsInTemplates(options = {}) { const { includes = [/\.([cm]?js|[cm]?ts|jsx|tsx)$/], excludes, urlPattern, parserPlugins = ["jsx", "typescript"] } = options; const filter = createFilter(includes, excludes); return { name: "vite:strip-urls-in-templates", apply: "build", transform(code, id, _options) { if (!filter(id)) return null; let ast; try { ast = parse(code, { sourceType: "module", plugins: parserPlugins, allowReturnOutsideFunction: true, allowAwaitOutsideFunction: true }); } catch { return null; } let templateDepth = 0; let mutated = false; const enterTemplate = () => { templateDepth += 1; }; const exitTemplate = () => { templateDepth -= 1; }; traverse(ast, { TemplateLiteral: { enter(path) { enterTemplate(); for (const elem of path.node.quasis) { const cooked = elem.value.cooked ?? elem.value.raw; const sanitized = sanitizeString(String(cooked), urlPattern); if (sanitized !== cooked) { mutated = true; elem.value.cooked = sanitized; elem.value.raw = toTemplateRaw(sanitized); } } }, exit() { exitTemplate(); } }, StringLiteral(path) { if (templateDepth <= 0) return; const original = path.node.value; const sanitized = sanitizeString(original, urlPattern); if (sanitized !== original) { mutated = true; path.node.value = sanitized; if (path.node.extra) { path.node.extra.raw = JSON.stringify(sanitized); path.node.extra.rawValue = sanitized; } } } }); if (!mutated) return null; const out = generate(ast, { sourceMaps: true, sourceFileName: id }, code); return { code: out.code, map: out.map || null }; } }; } const gas = ({ minify, url, replace } = {}) => { return [ enforceTerser(minify), stripUrlsInTemplates(url), replaceParticularExpression(replace) ]; }; export { gas };