UNPKG

@jsenv/snapshot

Version:
336 lines (328 loc) 12 kB
import { setUrlBasename, startsWithWindowsDriveLetter, urlIsOrIsInsideOf, urlToRelativeUrl, } from "@jsenv/urls"; import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js"; import { pathToFileURL } from "node:url"; import { createWellKnown } from "../../filesystem_well_known_values.js"; import { renderFileContent } from "../render_side_effects.js"; import { groupFileSideEffectsPerDirectory } from "./group_file_side_effects_per_directory.js"; import { spyFilesystemCalls } from "./spy_filesystem_calls.js"; const filesystemSideEffectsOptionsDefault = { preserve: false, baseDirectory: "", textualFilesInline: false, }; const INLINE_MAX_LINES = 20; const INLINE_MAX_LENGTH = 2000; export const filesystemSideEffects = ( filesystemSideEffectsOptions, { sourceFileUrl, filesystemActions, replaceFilesystemWellKnownValues }, ) => { filesystemSideEffectsOptions = { ...filesystemSideEffectsOptionsDefault, ...filesystemSideEffectsOptions, }; let baseDirectory; let removeBaseDirectoryWellKnown = () => {}; const setBaseDirectory = (value) => { removeBaseDirectoryWellKnown(); baseDirectory = value; if (baseDirectory) { removeBaseDirectoryWellKnown = replaceFilesystemWellKnownValues.addWellKnownFileUrl( baseDirectory, createWellKnown("base"), { position: "start" }, ); } }; return { name: "filesystem", setBaseDirectory, install: (addSideEffect, { addSkippableHandler, addFinallyCallback }) => { let { preserve, textualFilesInline } = filesystemSideEffectsOptions; if (filesystemSideEffectsOptions.baseDirectory) { setBaseDirectory(filesystemSideEffectsOptions.baseDirectory); } else if (sourceFileUrl) { setBaseDirectory(new URL("./", sourceFileUrl)); } const getUrlRelativeToBase = (url) => { if (baseDirectory) { return urlToRelativeUrl(url, baseDirectory, { preferRelativeNotation: true, }); } return url; }; const getUrlInsideOutDirectory = (url, generateOutFileUrl) => { if (baseDirectory) { if (urlIsOrIsInsideOf(url, baseDirectory)) { const outRelativeUrl = urlToRelativeUrl(url, baseDirectory); return generateOutFileUrl(outRelativeUrl); } } // otherwise we replace the url with well known const toRelativeUrl = replaceFilesystemWellKnownValues(url); return generateOutFileUrl(toRelativeUrl); }; addSkippableHandler((sideEffect) => { // if directory ends up with something inside we'll not report // this side effect because: // - it was likely created to write the file // - the file creation will be reported and implies directory creation if (sideEffect.code === "write_directory") { return (nextSideEffect, { skip, stop }) => { if ( (nextSideEffect.code === "write_file" || nextSideEffect.code === "write_directory") && urlIsOrIsInsideOf(nextSideEffect.value.url, sideEffect.value.url) ) { skip(); stop(); return; } if ( nextSideEffect.code === "remove_directory" && nextSideEffect.value.url === sideEffect.value.url ) { stop(); return; } }; } return null; }); addSkippableHandler((sideEffect) => { // on llinux writing to a file with the same content // is ignored somehow // let's make this consistent on other platforms if (sideEffect.code === "write_file") { return (nextSideEffect, { skip, stop }) => { if ( nextSideEffect.code === "write_file" && nextSideEffect.value.url === sideEffect.value.url && !Buffer.compare( nextSideEffect.value.buffer, sideEffect.value.buffer, ) ) { skip(); } stop(); }; } return null; }); addFinallyCallback((sideEffects) => { // gather all file side effect next to each other // collapse them if they have a shared ancestor groupFileSideEffectsPerDirectory(sideEffects, { createWriteFileGroupSideEffect: (fileSideEffectArray, commonPath) => { let commonUrl; if ( process.platform === "win32" && startsWithWindowsDriveLetter(commonPath.slice(1)) ) { commonUrl = pathToFileURL(commonPath.slice("/C:".length)); } else { commonUrl = pathToFileURL(commonPath); } let commonDirectoryUrl; if (commonUrl.href.endsWith("/")) { commonDirectoryUrl = commonUrl; } else { commonDirectoryUrl = new URL("./", commonUrl); } return { code: "write_file_group", type: `write_file_group ${commonDirectoryUrl}`, value: {}, render: { md: (options) => { let allFilesInsideOutDirectory = true; let groupMd = ""; let numberOfFiles = 0; for (const fileSideEffect of fileSideEffectArray) { numberOfFiles++; if (groupMd) { groupMd += "\n\n"; } const { url, outDirectoryReason } = fileSideEffect.value; const { text } = fileSideEffect.render.md(options); const urlRelativeToCommonDirectory = urlToRelativeUrl( url, commonDirectoryUrl, ); if (outDirectoryReason) { const outUrlRelativeToCommonDirectory = urlToRelativeUrl( text.urlInsideOutDirectory, options.sideEffectMdFileUrl, ); groupMd += `${"#".repeat(2)} ${urlRelativeToCommonDirectory} ${renderFileContent( { ...text, relativeUrl: urlRelativeToCommonDirectory, outRelativeUrl: outUrlRelativeToCommonDirectory, }, { ...options, sideEffect: fileSideEffect, }, )}`; continue; } allFilesInsideOutDirectory = false; groupMd += `${"#".repeat(2)} ${urlRelativeToCommonDirectory} ${renderFileContent( { ...text, relativeUrl: urlRelativeToCommonDirectory, }, { ...options, sideEffect: fileSideEffect, }, )}`; } const commonDirectoryRelativeUrl = getUrlRelativeToBase(commonDirectoryUrl); if (allFilesInsideOutDirectory) { const commonDirectoryOutUrl = getUrlInsideOutDirectory( commonDirectoryUrl, options.generateOutFileUrl, ); const commonDirectoryOutRelativeUrl = urlToRelativeUrl( commonDirectoryOutUrl, options.sideEffectMdFileUrl, { preferRelativeNotation: true }, ); return { label: `write ${numberOfFiles} files into "${commonDirectoryRelativeUrl}"`, text: `see [${commonDirectoryOutRelativeUrl}](${commonDirectoryOutRelativeUrl})`, }; } return { label: `write ${numberOfFiles} files into "${commonDirectoryRelativeUrl}"`, text: groupMd, }; }, }, }; }, }); }); const filesystemSpy = spyFilesystemCalls( { onWriteFile: (url, buffer) => { const contentType = CONTENT_TYPE.fromUrlExtension(url); const isTextual = CONTENT_TYPE.isTextual(contentType); let outDirectoryReason; if (isTextual) { if (textualFilesInline) { if (String(buffer).split("\n").length > INLINE_MAX_LINES) { outDirectoryReason = "lot_of_lines"; } else if (buffer.size > INLINE_MAX_LENGTH) { outDirectoryReason = "lot_of_chars"; } } else { outDirectoryReason = "text"; } } else { outDirectoryReason = "binary"; } const writeFileSideEffect = { code: "write_file", type: `write_file:${url}`, value: { url, buffer, contentType, isTextual, outDirectoryReason, }, render: { md: ({ sideEffectMdFileUrl, generateOutFileUrl }) => { const urlRelativeToBase = getUrlRelativeToBase(url); if (outDirectoryReason) { let urlInsideOutDirectory = getUrlInsideOutDirectory( url, generateOutFileUrl, ); if (writeFileSideEffect.counter) { urlInsideOutDirectory = setUrlBasename( urlInsideOutDirectory, (basename) => `${basename}_${writeFileSideEffect.counter}`, ); } let textValue; if (outDirectoryReason === "lot_of_chars") { textValue = String(buffer.slice(0, INLINE_MAX_LENGTH)); } else if (outDirectoryReason === "lot_of_lines") { textValue = String(buffer) .split("\n") .slice(0, INLINE_MAX_LINES) .join("\n"); } else { textValue = buffer; } const outRelativeUrl = urlToRelativeUrl( urlInsideOutDirectory, sideEffectMdFileUrl, { preferRelativeNotation: true, }, ); return { label: `write file "${urlRelativeToBase}"`, text: { type: "file_content", value: textValue, relativeUrl: urlRelativeToBase, outRelativeUrl, urlInsideOutDirectory, }, }; } return { label: `write file "${urlRelativeToBase}"`, text: { type: "file_content", value: String(buffer), }, }; }, }, }; addSideEffect(writeFileSideEffect); }, onWriteDirectory: (url) => { addSideEffect({ code: "write_directory", type: `write_directory:${url}`, value: { url }, render: { md: () => { return { label: `write directory "${getUrlRelativeToBase(url)}"`, }; }, }, }); }, }, { include: filesystemActions, undoFilesystemSideEffects: !preserve, }, ); return () => { filesystemSpy.restore(); }; }, }; };