vsix-utils
Version:
utilities for working with VSIX packages
328 lines (324 loc) • 11.2 kB
JavaScript
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 };