ast.js (5073B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 import { parseScriptTags } from "./parse-script-tags"; 6 import * as babelParser from "@babel/parser"; 7 import * as t from "@babel/types"; 8 import { getSource } from "../sources"; 9 10 const ASTs = new Map(); 11 12 function _parse(code, opts) { 13 return babelParser.parse(code, { 14 ...opts, 15 tokens: true, 16 }); 17 } 18 19 const sourceOptions = { 20 generated: { 21 sourceType: "unambiguous", 22 tokens: true, 23 plugins: [ 24 "classStaticBlock", 25 "classPrivateProperties", 26 "classPrivateMethods", 27 "classProperties", 28 "explicitResourceManagement", 29 "importAttributes", 30 "objectRestSpread", 31 "optionalChaining", 32 "privateIn", 33 "nullishCoalescingOperator", 34 "regexpUnicodeSets", 35 ], 36 }, 37 original: { 38 sourceType: "unambiguous", 39 tokens: true, 40 plugins: [ 41 "asyncGenerators", 42 "classPrivateMethods", 43 "classPrivateProperties", 44 "classProperties", 45 "classStaticBlock", 46 "decorators-legacy", 47 "doExpressions", 48 "dynamicImport", 49 "explicitResourceManagement", 50 "exportDefaultFrom", 51 "exportNamespaceFrom", 52 "flow", 53 "functionBind", 54 "functionSent", 55 "importAttributes", 56 "jsx", 57 "nullishCoalescingOperator", 58 "objectRestSpread", 59 "optionalChaining", 60 "react-jsx", 61 "regexpUnicodeSets", 62 ], 63 }, 64 }; 65 66 export function parse(text, opts) { 67 let ast = {}; 68 if (!text) { 69 return ast; 70 } 71 72 try { 73 ast = _parse(text, opts); 74 } catch (error) { 75 console.error(error); 76 } 77 78 return ast; 79 } 80 81 // Custom parser for parse-script-tags that adapts its input structure to 82 // our parser's signature 83 function htmlParser({ source, line }) { 84 return parse(source, { startLine: line, ...sourceOptions.generated }); 85 } 86 87 const VUE_COMPONENT_START = /^\s*</; 88 function vueParser({ source, line }) { 89 return parse(source, { 90 startLine: line, 91 ...sourceOptions.original, 92 }); 93 } 94 function parseVueScript(code) { 95 if (typeof code !== "string") { 96 return {}; 97 } 98 99 let ast; 100 101 // .vue files go through several passes, so while there is a 102 // single-file-component Vue template, there are also generally .vue files 103 // that are still just JS as well. 104 if (code.match(VUE_COMPONENT_START)) { 105 ast = parseScriptTags(code, vueParser); 106 if (t.isFile(ast)) { 107 // parseScriptTags is currently hard-coded to return scripts, but Vue 108 // always expects ESM syntax, so we just hard-code it. 109 ast.program.sourceType = "module"; 110 } 111 } else { 112 ast = parse(code, sourceOptions.original); 113 } 114 return ast; 115 } 116 117 export function parseConsoleScript(text) { 118 try { 119 return _parse(text, { 120 plugins: [ 121 "classStaticBlock", 122 "classPrivateProperties", 123 "classPrivateMethods", 124 "explicitResourceManagement", 125 "objectRestSpread", 126 "dynamicImport", 127 "nullishCoalescingOperator", 128 "optionalChaining", 129 "regexpUnicodeSets", 130 "topLevelAwait", 131 ], 132 sourceType: "module", 133 strictMode: false, 134 }); 135 } catch (e) { 136 return null; 137 } 138 } 139 140 export function parseScript(text, opts) { 141 return _parse(text, opts); 142 } 143 144 export function getAst(sourceId) { 145 if (ASTs.has(sourceId)) { 146 return ASTs.get(sourceId); 147 } 148 149 const source = getSource(sourceId); 150 151 if (source.isWasm) { 152 return null; 153 } 154 155 let ast = {}; 156 const { contentType } = source; 157 if (contentType == "text/html") { 158 ast = parseScriptTags(source.text, htmlParser) || {}; 159 } else if (contentType && contentType === "text/vue") { 160 ast = parseVueScript(source.text) || {}; 161 } else if ( 162 contentType && 163 contentType.match(/(javascript|jsx)/) && 164 !contentType.match(/typescript-jsx/) 165 ) { 166 const type = source.id.includes("original") ? "original" : "generated"; 167 const options = sourceOptions[type]; 168 ast = parse(source.text, options); 169 } else if (contentType && contentType.match(/typescript/)) { 170 const options = { 171 ...sourceOptions.original, 172 plugins: [ 173 ...sourceOptions.original.plugins.filter( 174 p => 175 p !== "flow" && 176 p !== "decorators" && 177 p !== "decorators2" && 178 (p !== "jsx" || contentType.match(/typescript-jsx/)) 179 ), 180 "decorators-legacy", 181 "typescript", 182 ], 183 }; 184 ast = parse(source.text, options); 185 } 186 187 ASTs.set(source.id, ast); 188 return ast; 189 } 190 191 export function clearASTs(sourceIds) { 192 for (const sourceId of sourceIds) { 193 ASTs.delete(sourceId); 194 } 195 } 196 197 export function replaceNode(ancestors, node) { 198 const parent = ancestors[ancestors.length - 1]; 199 200 if (typeof parent.index === "number") { 201 if (Array.isArray(node)) { 202 parent.node[parent.key].splice(parent.index, 1, ...node); 203 } else { 204 parent.node[parent.key][parent.index] = node; 205 } 206 } else { 207 parent.node[parent.key] = node; 208 } 209 }