InlineSnapshots.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. 'use strict';
  2. Object.defineProperty(exports, '__esModule', {
  3. value: true
  4. });
  5. exports.saveInlineSnapshots = saveInlineSnapshots;
  6. var path = _interopRequireWildcard(require('path'));
  7. var fs = _interopRequireWildcard(require('graceful-fs'));
  8. var _semver = _interopRequireDefault(require('semver'));
  9. var _utils = require('./utils');
  10. function _interopRequireDefault(obj) {
  11. return obj && obj.__esModule ? obj : {default: obj};
  12. }
  13. function _getRequireWildcardCache(nodeInterop) {
  14. if (typeof WeakMap !== 'function') return null;
  15. var cacheBabelInterop = new WeakMap();
  16. var cacheNodeInterop = new WeakMap();
  17. return (_getRequireWildcardCache = function (nodeInterop) {
  18. return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
  19. })(nodeInterop);
  20. }
  21. function _interopRequireWildcard(obj, nodeInterop) {
  22. if (!nodeInterop && obj && obj.__esModule) {
  23. return obj;
  24. }
  25. if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
  26. return {default: obj};
  27. }
  28. var cache = _getRequireWildcardCache(nodeInterop);
  29. if (cache && cache.has(obj)) {
  30. return cache.get(obj);
  31. }
  32. var newObj = {};
  33. var hasPropertyDescriptor =
  34. Object.defineProperty && Object.getOwnPropertyDescriptor;
  35. for (var key in obj) {
  36. if (key !== 'default' && Object.prototype.hasOwnProperty.call(obj, key)) {
  37. var desc = hasPropertyDescriptor
  38. ? Object.getOwnPropertyDescriptor(obj, key)
  39. : null;
  40. if (desc && (desc.get || desc.set)) {
  41. Object.defineProperty(newObj, key, desc);
  42. } else {
  43. newObj[key] = obj[key];
  44. }
  45. }
  46. }
  47. newObj.default = obj;
  48. if (cache) {
  49. cache.set(obj, newObj);
  50. }
  51. return newObj;
  52. }
  53. var global = (function () {
  54. if (typeof globalThis !== 'undefined') {
  55. return globalThis;
  56. } else if (typeof global !== 'undefined') {
  57. return global;
  58. } else if (typeof self !== 'undefined') {
  59. return self;
  60. } else if (typeof window !== 'undefined') {
  61. return window;
  62. } else {
  63. return Function('return this')();
  64. }
  65. })();
  66. var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol;
  67. var global = (function () {
  68. if (typeof globalThis !== 'undefined') {
  69. return globalThis;
  70. } else if (typeof global !== 'undefined') {
  71. return global;
  72. } else if (typeof self !== 'undefined') {
  73. return self;
  74. } else if (typeof window !== 'undefined') {
  75. return window;
  76. } else {
  77. return Function('return this')();
  78. }
  79. })();
  80. var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol;
  81. var global = (function () {
  82. if (typeof globalThis !== 'undefined') {
  83. return globalThis;
  84. } else if (typeof global !== 'undefined') {
  85. return global;
  86. } else if (typeof self !== 'undefined') {
  87. return self;
  88. } else if (typeof window !== 'undefined') {
  89. return window;
  90. } else {
  91. return Function('return this')();
  92. }
  93. })();
  94. var jestWriteFile =
  95. global[Symbol.for('jest-native-write-file')] || fs.writeFileSync;
  96. var global = (function () {
  97. if (typeof globalThis !== 'undefined') {
  98. return globalThis;
  99. } else if (typeof global !== 'undefined') {
  100. return global;
  101. } else if (typeof self !== 'undefined') {
  102. return self;
  103. } else if (typeof window !== 'undefined') {
  104. return window;
  105. } else {
  106. return Function('return this')();
  107. }
  108. })();
  109. var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol;
  110. var global = (function () {
  111. if (typeof globalThis !== 'undefined') {
  112. return globalThis;
  113. } else if (typeof global !== 'undefined') {
  114. return global;
  115. } else if (typeof self !== 'undefined') {
  116. return self;
  117. } else if (typeof window !== 'undefined') {
  118. return window;
  119. } else {
  120. return Function('return this')();
  121. }
  122. })();
  123. var jestReadFile =
  124. global[Symbol.for('jest-native-read-file')] || fs.readFileSync;
  125. // prettier-ignore
  126. const babelTraverse = // @ts-expect-error requireOutside Babel transform
  127. require(require.resolve('@babel/traverse', {
  128. [(global['jest-symbol-do-not-touch'] || global.Symbol).for('jest-resolve-outside-vm-option')]: true
  129. })).default; // prettier-ignore
  130. const generate = require(require.resolve('@babel/generator', { // @ts-expect-error requireOutside Babel transform
  131. [(global['jest-symbol-do-not-touch'] || global.Symbol).for(
  132. 'jest-resolve-outside-vm-option'
  133. )]: true
  134. })).default; // @ts-expect-error requireOutside Babel transform
  135. const {file, templateElement, templateLiteral} = require(require.resolve(
  136. '@babel/types',
  137. {
  138. [(global['jest-symbol-do-not-touch'] || global.Symbol).for(
  139. 'jest-resolve-outside-vm-option'
  140. )]: true
  141. }
  142. )); // @ts-expect-error requireOutside Babel transform
  143. const {parseSync} = require(require.resolve('@babel/core', {
  144. [(global['jest-symbol-do-not-touch'] || global.Symbol).for(
  145. 'jest-resolve-outside-vm-option'
  146. )]: true
  147. }));
  148. function saveInlineSnapshots(snapshots, prettierPath) {
  149. let prettier = null;
  150. if (prettierPath) {
  151. try {
  152. // @ts-expect-error requireOutside Babel transform
  153. prettier = require(require.resolve(prettierPath, {
  154. [(global['jest-symbol-do-not-touch'] || global.Symbol).for(
  155. 'jest-resolve-outside-vm-option'
  156. )]: true
  157. }));
  158. } catch {
  159. // Continue even if prettier is not installed.
  160. }
  161. }
  162. const snapshotsByFile = groupSnapshotsByFile(snapshots);
  163. for (const sourceFilePath of Object.keys(snapshotsByFile)) {
  164. saveSnapshotsForFile(
  165. snapshotsByFile[sourceFilePath],
  166. sourceFilePath,
  167. prettier && _semver.default.gte(prettier.version, '1.5.0')
  168. ? prettier
  169. : undefined
  170. );
  171. }
  172. }
  173. const saveSnapshotsForFile = (snapshots, sourceFilePath, prettier) => {
  174. const sourceFile = jestReadFile(sourceFilePath, 'utf8'); // TypeScript projects may not have a babel config; make sure they can be parsed anyway.
  175. const presets = [require.resolve('babel-preset-current-node-syntax')];
  176. const plugins = [];
  177. if (/\.tsx?$/.test(sourceFilePath)) {
  178. plugins.push([
  179. require.resolve('@babel/plugin-syntax-typescript'),
  180. {
  181. isTSX: sourceFilePath.endsWith('x')
  182. }, // unique name to make sure Babel does not complain about a possible duplicate plugin.
  183. 'TypeScript syntax plugin added by Jest snapshot'
  184. ]);
  185. } // Record the matcher names seen during traversal and pass them down one
  186. // by one to formatting parser.
  187. const snapshotMatcherNames = [];
  188. const ast = parseSync(sourceFile, {
  189. filename: sourceFilePath,
  190. plugins,
  191. presets,
  192. root: path.dirname(sourceFilePath)
  193. });
  194. if (!ast) {
  195. throw new Error(`jest-snapshot: Failed to parse ${sourceFilePath}`);
  196. }
  197. traverseAst(snapshots, ast, snapshotMatcherNames); // substitute in the snapshots in reverse order, so slice calculations aren't thrown off.
  198. const sourceFileWithSnapshots = snapshots.reduceRight(
  199. (sourceSoFar, nextSnapshot) => {
  200. if (
  201. !nextSnapshot.node ||
  202. typeof nextSnapshot.node.start !== 'number' ||
  203. typeof nextSnapshot.node.end !== 'number'
  204. ) {
  205. throw new Error('Jest: no snapshot insert location found');
  206. }
  207. return (
  208. sourceSoFar.slice(0, nextSnapshot.node.start) +
  209. generate(nextSnapshot.node, {
  210. retainLines: true
  211. }).code.trim() +
  212. sourceSoFar.slice(nextSnapshot.node.end)
  213. );
  214. },
  215. sourceFile
  216. );
  217. const newSourceFile = prettier
  218. ? runPrettier(
  219. prettier,
  220. sourceFilePath,
  221. sourceFileWithSnapshots,
  222. snapshotMatcherNames
  223. )
  224. : sourceFileWithSnapshots;
  225. if (newSourceFile !== sourceFile) {
  226. jestWriteFile(sourceFilePath, newSourceFile);
  227. }
  228. };
  229. const groupSnapshotsBy = createKey => snapshots =>
  230. snapshots.reduce((object, inlineSnapshot) => {
  231. const key = createKey(inlineSnapshot);
  232. return {...object, [key]: (object[key] || []).concat(inlineSnapshot)};
  233. }, {});
  234. const groupSnapshotsByFrame = groupSnapshotsBy(({frame: {line, column}}) =>
  235. typeof line === 'number' && typeof column === 'number'
  236. ? `${line}:${column - 1}`
  237. : ''
  238. );
  239. const groupSnapshotsByFile = groupSnapshotsBy(({frame: {file}}) => file);
  240. const indent = (snapshot, numIndents, indentation) => {
  241. const lines = snapshot.split('\n'); // Prevent re-indentation of inline snapshots.
  242. if (
  243. lines.length >= 2 &&
  244. lines[1].startsWith(indentation.repeat(numIndents + 1))
  245. ) {
  246. return snapshot;
  247. }
  248. return lines
  249. .map((line, index) => {
  250. if (index === 0) {
  251. // First line is either a 1-line snapshot or a blank line.
  252. return line;
  253. } else if (index !== lines.length - 1) {
  254. // Do not indent empty lines.
  255. if (line === '') {
  256. return line;
  257. } // Not last line, indent one level deeper than expect call.
  258. return indentation.repeat(numIndents + 1) + line;
  259. } else {
  260. // The last line should be placed on the same level as the expect call.
  261. return indentation.repeat(numIndents) + line;
  262. }
  263. })
  264. .join('\n');
  265. };
  266. const resolveAst = fileOrProgram => {
  267. // Flow uses a 'Program' parent node, babel expects a 'File'.
  268. let ast = fileOrProgram;
  269. if (ast.type !== 'File') {
  270. ast = file(ast, ast.comments, ast.tokens);
  271. delete ast.program.comments;
  272. }
  273. return ast;
  274. };
  275. const traverseAst = (snapshots, fileOrProgram, snapshotMatcherNames) => {
  276. const ast = resolveAst(fileOrProgram);
  277. const groupedSnapshots = groupSnapshotsByFrame(snapshots);
  278. const remainingSnapshots = new Set(snapshots.map(({snapshot}) => snapshot));
  279. babelTraverse(ast, {
  280. CallExpression({node}) {
  281. const {arguments: args, callee} = node;
  282. if (
  283. callee.type !== 'MemberExpression' ||
  284. callee.property.type !== 'Identifier' ||
  285. callee.property.loc == null
  286. ) {
  287. return;
  288. }
  289. const {line, column} = callee.property.loc.start;
  290. const snapshotsForFrame = groupedSnapshots[`${line}:${column}`];
  291. if (!snapshotsForFrame) {
  292. return;
  293. }
  294. if (snapshotsForFrame.length > 1) {
  295. throw new Error(
  296. 'Jest: Multiple inline snapshots for the same call are not supported.'
  297. );
  298. }
  299. snapshotMatcherNames.push(callee.property.name);
  300. const snapshotIndex = args.findIndex(
  301. ({type}) => type === 'TemplateLiteral'
  302. );
  303. const values = snapshotsForFrame.map(inlineSnapshot => {
  304. inlineSnapshot.node = node;
  305. const {snapshot} = inlineSnapshot;
  306. remainingSnapshots.delete(snapshot);
  307. return templateLiteral(
  308. [
  309. templateElement({
  310. raw: (0, _utils.escapeBacktickString)(snapshot)
  311. })
  312. ],
  313. []
  314. );
  315. });
  316. const replacementNode = values[0];
  317. if (snapshotIndex > -1) {
  318. args[snapshotIndex] = replacementNode;
  319. } else {
  320. args.push(replacementNode);
  321. }
  322. }
  323. });
  324. if (remainingSnapshots.size) {
  325. throw new Error("Jest: Couldn't locate all inline snapshots.");
  326. }
  327. };
  328. const runPrettier = (
  329. prettier,
  330. sourceFilePath,
  331. sourceFileWithSnapshots,
  332. snapshotMatcherNames
  333. ) => {
  334. // Resolve project configuration.
  335. // For older versions of Prettier, do not load configuration.
  336. const config = prettier.resolveConfig
  337. ? prettier.resolveConfig.sync(sourceFilePath, {
  338. editorconfig: true
  339. })
  340. : null; // Detect the parser for the test file.
  341. // For older versions of Prettier, fallback to a simple parser detection.
  342. // @ts-expect-error
  343. const inferredParser = prettier.getFileInfo
  344. ? prettier.getFileInfo.sync(sourceFilePath).inferredParser
  345. : (config && typeof config.parser === 'string' && config.parser) ||
  346. simpleDetectParser(sourceFilePath);
  347. if (!inferredParser) {
  348. throw new Error(
  349. `Could not infer Prettier parser for file ${sourceFilePath}`
  350. );
  351. } // Snapshots have now been inserted. Run prettier to make sure that the code is
  352. // formatted, except snapshot indentation. Snapshots cannot be formatted until
  353. // after the initial format because we don't know where the call expression
  354. // will be placed (specifically its indentation), so we have to do two
  355. // prettier.format calls back-to-back.
  356. return prettier.format(
  357. prettier.format(sourceFileWithSnapshots, {
  358. ...config,
  359. filepath: sourceFilePath
  360. }),
  361. {
  362. ...config,
  363. filepath: sourceFilePath,
  364. parser: createFormattingParser(snapshotMatcherNames, inferredParser)
  365. }
  366. );
  367. }; // This parser formats snapshots to the correct indentation.
  368. const createFormattingParser =
  369. (snapshotMatcherNames, inferredParser) => (text, parsers, options) => {
  370. // Workaround for https://github.com/prettier/prettier/issues/3150
  371. options.parser = inferredParser;
  372. const ast = resolveAst(parsers[inferredParser](text, options));
  373. babelTraverse(ast, {
  374. CallExpression({node: {arguments: args, callee}}) {
  375. var _options$tabWidth, _options$tabWidth2;
  376. if (
  377. callee.type !== 'MemberExpression' ||
  378. callee.property.type !== 'Identifier' ||
  379. !snapshotMatcherNames.includes(callee.property.name) ||
  380. !callee.loc ||
  381. callee.computed
  382. ) {
  383. return;
  384. }
  385. let snapshotIndex;
  386. let snapshot;
  387. for (let i = 0; i < args.length; i++) {
  388. const node = args[i];
  389. if (node.type === 'TemplateLiteral') {
  390. snapshotIndex = i;
  391. snapshot = node.quasis[0].value.raw;
  392. }
  393. }
  394. if (snapshot === undefined || snapshotIndex === undefined) {
  395. return;
  396. }
  397. const useSpaces = !options.useTabs;
  398. snapshot = indent(
  399. snapshot,
  400. Math.ceil(
  401. useSpaces
  402. ? callee.loc.start.column /
  403. ((_options$tabWidth = options.tabWidth) !== null &&
  404. _options$tabWidth !== void 0
  405. ? _options$tabWidth
  406. : 1)
  407. : callee.loc.start.column / 2 // Each tab is 2 characters.
  408. ),
  409. useSpaces
  410. ? ' '.repeat(
  411. (_options$tabWidth2 = options.tabWidth) !== null &&
  412. _options$tabWidth2 !== void 0
  413. ? _options$tabWidth2
  414. : 1
  415. )
  416. : '\t'
  417. );
  418. const replacementNode = templateLiteral(
  419. [
  420. templateElement({
  421. raw: snapshot
  422. })
  423. ],
  424. []
  425. );
  426. args[snapshotIndex] = replacementNode;
  427. }
  428. });
  429. return ast;
  430. };
  431. const simpleDetectParser = filePath => {
  432. const extname = path.extname(filePath);
  433. if (/\.tsx?$/.test(extname)) {
  434. return 'typescript';
  435. }
  436. return 'babel';
  437. };