UNPKG

vsix-utils

Version:

utilities for working with VSIX packages

328 lines (324 loc) 11.2 kB
import { VSCE_DEFAULT_IGNORE } from './chunk-WRSU2XB2.mjs'; import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import path, { extname } from 'node:path'; import process from 'node:process'; import ignore from 'ignore'; import mime from 'mime'; import { detect } from 'package-manager-detector'; import { glob } from 'tinyglobby'; import { remark } from 'remark'; import remarkTransformLinks from 'remark-transform-links'; async function transformMarkdown(manifest, options) { const { content, rewrite = true, branch = "HEAD" } = options; if (!rewrite) { return content; } let { baseContentUrl, baseImagesUrl } = options; const baseUrls = inferBaseUrls(manifest, branch); if (baseUrls == null) { throw new Error("Couldn't detect the repository where this extension is published."); } if (baseContentUrl == null) { baseContentUrl = baseUrls.contentUrl; } if (baseImagesUrl == null) { baseImagesUrl = baseUrls.imagesUrl; } const detectedSyntax = detectMarkdownSyntax(content); const file = await remark().data("settings", { bullet: detectedSyntax.bullet, bulletOrdered: detectedSyntax.bulletOrdered, emphasis: detectedSyntax.emphasis, listItemIndent: detectedSyntax.listItemIndent, fence: detectedSyntax.fence, strong: detectedSyntax.strong, rule: detectedSyntax.rule }).use(remarkTransformLinks, { baseUrl(_, type) { if (type === "image" || type === "html_img" || type === "html_video") { return baseImagesUrl; } return baseContentUrl; } }).process(content); return String(file); } function inferBaseUrls(manifest, branch) { let repository = null; if (typeof manifest.repository === "string") { repository = manifest.repository; } else if (manifest.repository && typeof manifest.repository === "object" && "url" in manifest.repository && typeof manifest.repository.url === "string") { repository = manifest.repository.url; } if (!repository) { return null; } if (repository.startsWith("git@")) { repository = repository.replace(":", "/").replace("git@", "https://"); } let url; try { url = new URL(repository); } catch { return null; } const ownerWithRepo = url.pathname.split("/").slice(1, 3).join("/").replace(/\.git$/, ""); const branchName = branch ?? "HEAD"; if (url.hostname === "github.com") { return { contentUrl: `https://github.com/${ownerWithRepo}/blob/${branchName}`, imagesUrl: `https://github.com/${ownerWithRepo}/raw/${branchName}` }; } if (url.hostname === "gitlab.com") { return { contentUrl: `https://gitlab.com/${ownerWithRepo}/-/blob/${branchName}`, imagesUrl: `https://gitlab.com/${ownerWithRepo}/-/raw/${branchName}` }; } if (url.hostname === "gitea.com") { return { contentUrl: `https://gitea.com/${ownerWithRepo}/src/branch/${branchName}`, imagesUrl: `https://gitea.com/${ownerWithRepo}/raw/branch/${branchName}` }; } return null; } function detectMarkdownSyntax(content) { const result = {}; const hyphenBullets = content.match(/^[\t ]*-[\t ][^\n]+/gm); const asteriskBullets = content.match(/^[\t ]*\*[\t ][^\n]+/gm); const plusBullets = content.match(/^[\t ]*\+[\t ][^\n]+/gm); if (hyphenBullets?.length) result.bullet = "-"; else if (asteriskBullets?.length) result.bullet = "*"; else if (plusBullets?.length) result.bullet = "+"; const dotOrdered = content.match(/^\s*\d+\.[\t ][^\n]+/gm); const parenthesisOrdered = content.match(/^\s*\d+\)[\t ][^\n]+/gm); if (dotOrdered?.length) result.bulletOrdered = "."; else if (parenthesisOrdered?.length) result.bulletOrdered = ")"; const asteriskEmphasis = content.match(/(?<!\*)\*[^*\n]+\*(?!\*)/g); const underscoreEmphasis = content.match(/(?<!_)_[^_\n]+_(?!_)/g); if (asteriskEmphasis?.length) result.emphasis = "*"; else if (underscoreEmphasis?.length) result.emphasis = "_"; const asteriskStrong = content.match(/\*\*[^*\n]+\*\*/g); const underscoreStrong = content.match(/__[^_\n]+__/g); if (asteriskStrong?.length) result.strong = "*"; else if (underscoreStrong?.length) result.strong = "_"; const backtickFence = content.match(/^```[^`]*```/gm); const tildeFence = content.match(/^~~~[^~]*~~~/gm); if (backtickFence?.length) result.fence = "`"; else if (tildeFence?.length) result.fence = "~"; const listItems = content.match(/^[\t ]*([-+*]|\d+[.)])\s+[^\n]+/gm); if (listItems?.length) { const indentTypes = new Set( listItems.map((item) => { const leadingSpace = item.match(/^[\t ]*/)?.[0] || ""; if (leadingSpace.includes(" ")) return "tab"; return leadingSpace.length === 1 ? "one" : leadingSpace.length > 1 ? "mixed" : "one"; }) ); if (indentTypes.size === 1) { result.listItemIndent = indentTypes.values().next().value; } else { result.listItemIndent = "mixed"; } } const asteriskRule = content.match(/^[\t ]*(\*[\t ]*){3,}$/m); const hyphenRule = content.match(/^[\t ]*(-[\t ]*){3,}$/m); const underscoreRule = content.match(/^[\t ]*(_[\t ]*){3,}$/m); if (asteriskRule?.length) result.rule = "*"; else if (hyphenRule?.length) result.rule = "-"; else if (underscoreRule?.length) result.rule = "_"; return result; } // src/files.ts function isLocalFile(file) { return file.type === "local"; } function isInMemoryFile(file) { return file.type === "in-memory"; } async function collect(manifest, options) { const { cwd = process.cwd(), ignoreFile = ".vscodeignore", dependencies = [], readme = "README.md" } = options; const gitignorePath = path.join(cwd, ".gitignore"); const vscodeIgnorePath = path.join(cwd, ignoreFile); const ig = ignore(); if (existsSync(gitignorePath)) { const ignoreContent = await readFile(gitignorePath, "utf8"); ig.add(ignoreContent); } if (existsSync(vscodeIgnorePath)) { const vsceIgnoreContent = await readFile(vscodeIgnorePath, "utf8"); ig.add(vsceIgnoreContent); } const globbedFiles = await glob("**", { cwd, followSymbolicLinks: true, expandDirectories: true, ignore: [...VSCE_DEFAULT_IGNORE, "!package.json", `!${readme}`, "node_modules/**"], dot: true, onlyFiles: true }); const filteredFiles = globbedFiles.filter((file) => !ig.ignores(file)); const files = filteredFiles.map((file) => ({ type: "local", localPath: path.join(cwd, file), path: path.join("extension/", file) })); if (dependencies.length > 0) { for (const dep of dependencies) { files.push({ type: "local", path: path.join("extension/node_modules", dep.name), localPath: dep.path }); } } return files; } async function getExtensionPackageManager(cwd) { const result = await detect({ cwd }); if (result == null) { throw new Error("could not detect package manager"); } if (result.name === "deno" || result.name === "bun") { throw new Error(`unsupported package manager: ${result.name}`); } return result.name; } var DEFAULT_MIME_TYPES = /* @__PURE__ */ new Map([ [".json", "application/json"], [".vsixmanifest", "text/xml"], [".md", "text/markdown"], [".png", "image/png"], [".txt", "text/plain"], [".js", "application/javascript"], [".yml", "text/yaml"], [".html", "text/html"], [".markdown", "text/markdown"], [".css", "text/css"] ]); function getContentTypesForFiles(files) { const contentTypes = {}; for (const file of files) { const ext = path.extname(file.path).toLowerCase(); if (ext == null) continue; if (!DEFAULT_MIME_TYPES.has(ext)) { const contentType = mime.getType(ext); if (contentType == null) { throw new Error(`could not determine content type for file: ${file.path}`); } contentTypes[ext] = contentType; } else { contentTypes[ext] = DEFAULT_MIME_TYPES.get(ext); } } const xml = ( /* xml */ `<?xml version="1.0" encoding="utf-8"?> <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"> ${Object.entries(contentTypes).map(([ext, contentType]) => `<Default Extension="${ext}" ContentType="${contentType}" /> `).join("")} </Types> ` ); return { xml, contentTypes }; } async function transformFiles(options) { const { manifest, files, readme } = options; const assets = []; let license; let icon; const hasLicenseFile = hasExtensionFile(files, ["LICENSE", "LICENSE.md", "LICENSE.txt", "LICENSE.markdown"]); const hasIconFile = hasExtensionFile(files, [manifest.icon]); const hasReadmeFile = hasExtensionFile(files, [readme, "README.md"]); const hasChangelogFile = hasExtensionFile(files, ["CHANGELOG.md", "CHANGELOG.markdown", "CHANGELOG.txt"]); const hasTranslationsFiles = hasExtensionFile(files, ["package.nls.json"]); if (hasLicenseFile.found) { if (!extname(hasLicenseFile.path)) { const entryIndex = files.findIndex((f) => f.path === hasLicenseFile.path); if (entryIndex === -1) { throw new Error(`could not find license file: ${hasLicenseFile.path}`); } const entry = files[entryIndex]; files[entryIndex] = { ...entry, path: `${hasLicenseFile.path}.md` }; hasLicenseFile.path = files[entryIndex].path; } license = hasLicenseFile.path; assets.push({ type: "Microsoft.VisualStudio.Services.Content.License", path: license }); } if (hasIconFile.found) { icon = hasIconFile.path; assets.push({ type: "Microsoft.VisualStudio.Services.Icons.Default", path: icon }); } if (hasReadmeFile.found) { const entryIndex = files.findIndex((f) => f.path === hasReadmeFile.path); if (entryIndex === -1) { throw new Error("could not find readme file"); } const entry = files[entryIndex]; let contents = entry.type === "in-memory" ? entry.contents : await readFile(entry.localPath, "utf-8"); if (typeof contents !== "string") { contents = String(contents); } files[entryIndex] = { type: "in-memory", contents: await transformMarkdown(manifest, { content: contents, ...options.markdown }), path: hasReadmeFile.path }; assets.push({ type: "Microsoft.VisualStudio.Services.Content.Details", path: hasReadmeFile.path }); } if (hasChangelogFile.found) { assets.push({ type: "Microsoft.VisualStudio.Services.Content.Changelog", path: hasChangelogFile.path }); } if (hasTranslationsFiles.found) { assets.push({ type: "Microsoft.VisualStudio.Code.Translation.en", path: hasTranslationsFiles.path }); } return { assets, icon, license }; } function hasExtensionFile(files, fileNames) { for (const fileName of fileNames) { if (fileName == null) continue; const file = files.find((f) => f.path.endsWith(fileName)); if (file) { return { found: true, path: file.path }; } } return { found: false, path: undefined }; } export { collect, getContentTypesForFiles, getExtensionPackageManager, inferBaseUrls, isInMemoryFile, isLocalFile, transformFiles, transformMarkdown };