nicoespeon / abracadabra

Automated refactorings for VS Code (JS & TS) ✨ It's magic ✨
https://marketplace.visualstudio.com/items?itemName=nicoespeon.abracadabra
MIT License
805 stars 48 forks source link

Destructure Object is too slow #416

Closed nicoespeon closed 1 year ago

nicoespeon commented 3 years ago

See https://github.com/nicoespeon/abracadabra/issues/415 for context

We had to disable the Action Provider for this refactoring because it would cause VS Code to hang.

The thing is:

Solutions to fix:

  1. Don't create a new TS program on every Action Provider run (similar optimization we did for parsing the AST only once)
  2. Probably better => don't create a new TS program at all but tap into VS Code's one instead

Going with 2) will also unlock multi-files refactoring. Definitely the path I want to explore.

QA

Monitor performance when running the extension on a large TS file. This snippet should do:

This snippet should do: ```tsx interface GitgraphOptions { template?: TemplateName | Template; orientation?: Orientation; reverseArrow?: boolean; initCommitOffsetX?: number; initCommitOffsetY?: number; mode?: Mode; author?: string; branchLabelOnEveryCommit?: boolean; commitMessage?: string; generateCommitHash?: () => Commit["hash"]; compareBranchesOrder?: CompareBranchesOrder; } interface RenderedData { commits: Array>; branchesPaths: BranchesPaths; commitMessagesX: number; } class GitgraphCore { public orientation?: Orientation; public get isHorizontal(): boolean { return ( this.orientation === Orientation.Horizontal || this.orientation === Orientation.HorizontalReverse ); } public get isVertical(): boolean { return !this.isHorizontal; } public get isReverse(): boolean { return ( this.orientation === Orientation.HorizontalReverse || this.orientation === Orientation.VerticalReverse ); } public get shouldDisplayCommitMessage(): boolean { return !this.isHorizontal && this.mode !== Mode.Compact; } public reverseArrow: boolean; public initCommitOffsetX: number; public initCommitOffsetY: number; public mode?: Mode; public author: string; public commitMessage: string; public generateCommitHash: () => Commit["hash"] | undefined; public branchesOrderFunction: CompareBranchesOrder | undefined; public template: Template; public branchLabelOnEveryCommit: boolean; public refs = new Refs(); public tags = new Refs(); public tagStyles: { [name: string]: TemplateOptions["tag"] } = {}; public tagRenders: { [name: string]: GitgraphTagOptions["render"]; } = {}; public commits: Array> = []; public branches: Map> = new Map(); public currentBranch: Branch; private listeners: Array<(data: RenderedData) => void> = []; private nextTimeoutId: number | null = null; constructor(options: GitgraphOptions = {}) { this.template = getTemplate(options.template); // Set a default `master` branch this.currentBranch = this.createBranch("master"); // Set all options with default values this.orientation = options.orientation; this.reverseArrow = booleanOptionOr(options.reverseArrow, false); this.initCommitOffsetX = numberOptionOr(options.initCommitOffsetX, 0); this.initCommitOffsetY = numberOptionOr(options.initCommitOffsetY, 0); this.mode = options.mode; this.author = options.author || "Sergio Flores "; this.commitMessage = options.commitMessage || "He doesn't like George Michael! Boooo!"; this.generateCommitHash = typeof options.generateCommitHash === "function" ? options.generateCommitHash : () => undefined; this.branchesOrderFunction = typeof options.compareBranchesOrder === "function" ? options.compareBranchesOrder : undefined; this.branchLabelOnEveryCommit = booleanOptionOr( options.branchLabelOnEveryCommit, false ); } /** * Return the API to manipulate Gitgraph as a user. * Rendering library should give that API to their consumer. */ public getUserApi(): GitgraphUserApi { return new GitgraphUserApi(this, () => this.next()); } /** * Add a change listener. * It will be called any time the graph have changed (commit, merge…). * * @param listener A callback to be invoked on every change. * @returns A function to remove this change listener. */ public subscribe(listener: (data: RenderedData) => void): () => void { this.listeners.push(listener); let isSubscribed = true; return () => { if (!isSubscribed) return; isSubscribed = false; const index = this.listeners.indexOf(listener); this.listeners.splice(index, 1); }; } /** * Return all data required for rendering. * Rendering libraries will use this to implement their rendering strategy. */ public getRenderedData(): RenderedData { const commits = this.computeRenderedCommits(); const branchesPaths = this.computeRenderedBranchesPaths(commits); const commitMessagesX = this.computeCommitMessagesX(branchesPaths); this.computeBranchesColor(commits, branchesPaths); return { commits, branchesPaths, commitMessagesX }; } /** * Create a new branch. * * @param options Options of the branch */ public createBranch(options: GitgraphBranchOptions): Branch; /** * Create a new branch. (as `git branch`) * * @param name Name of the created branch */ public createBranch(name: string): Branch; public createBranch(args: any): Branch { const defaultParentBranchName = "HEAD"; let options = { gitgraph: this, name: "", parentCommitHash: this.refs.getCommit(defaultParentBranchName), style: this.template.branch, onGraphUpdate: () => this.next(), }; if (typeof args === "string") { options.name = args; options.parentCommitHash = this.refs.getCommit(defaultParentBranchName); } else { const parentBranchName = args.from ? args.from.name : defaultParentBranchName; const parentCommitHash = this.refs.getCommit(parentBranchName) || (this.refs.hasCommit(args.from) ? args.from : undefined); args.style = args.style || {}; options = { ...options, ...args, parentCommitHash, style: { ...options.style, ...args.style, label: { ...options.style.label, ...args.style.label, }, }, }; } const branch = new Branch(options); this.branches.set(branch.name, branch); return branch; } /** * Return commits with data for rendering. */ private computeRenderedCommits(): Array> { const branches = this.getBranches(); // Commits that are not associated to a branch in `branches` // were in a deleted branch. If the latter was merged beforehand // they are reachable and are rendered. Others are not const reachableUnassociatedCommits = (() => { const unassociatedCommits = new Set( this.commits.reduce( (commits: Commit["hash"][], { hash }: { hash: Commit["hash"] }) => !branches.has(hash) ? [...commits, hash] : commits, [] ) ); const tipsOfMergedBranches = this.commits.reduce( (tipsOfMergedBranches: Commit[], commit: Commit) => commit.parents.length > 1 ? [ ...tipsOfMergedBranches, ...commit.parents .slice(1) .map( (parentHash) => this.commits.find(({ hash }) => parentHash === hash)! ), ] : tipsOfMergedBranches, [] ); const reachableCommits = new Set(); tipsOfMergedBranches.forEach((tip) => { let currentCommit: Commit | undefined = tip; while (currentCommit && unassociatedCommits.has(currentCommit.hash)) { reachableCommits.add(currentCommit.hash); currentCommit = currentCommit.parents.length > 0 ? this.commits.find( ({ hash }) => currentCommit!.parents[0] === hash ) : undefined; } }); return reachableCommits; })(); const commitsToRender = this.commits.filter( ({ hash }) => branches.has(hash) || reachableUnassociatedCommits.has(hash) ); const commitsWithBranches = commitsToRender.map((commit) => this.withBranches(branches, commit) ); const rows = createGraphRows(this.mode, commitsToRender); const branchesOrder = new BranchesOrder( commitsWithBranches, this.template.colors, this.branchesOrderFunction ); return ( commitsWithBranches .map((commit) => commit.setRefs(this.refs)) .map((commit) => this.withPosition(rows, branchesOrder, commit)) // Fallback commit computed color on branch color. .map((commit) => commit.withDefaultColor( this.getBranchDefaultColor(branchesOrder, commit.branchToDisplay) ) ) // Tags need commit style to be computed (with default color). .map((commit) => commit.setTags( this.tags, (name) => Object.assign({}, this.tagStyles[name], this.template.tag), (name) => this.tagRenders[name] ) ) ); } /** * Return branches paths with all data required for rendering. * * @param commits List of commits with rendering data computed */ private computeRenderedBranchesPaths( commits: Array> ): BranchesPaths { return new BranchesPathsCalculator( commits, this.branches, this.template.commit.spacing, this.isVertical, this.isReverse, () => createDeletedBranch(this, this.template.branch, () => this.next()) ).execute(); } /** * Set branches colors based on branches paths. * * @param commits List of graph commits * @param branchesPaths Branches paths to be rendered */ private computeBranchesColor( commits: Array>, branchesPaths: BranchesPaths ): void { const branchesOrder = new BranchesOrder( commits, this.template.colors, this.branchesOrderFunction ); Array.from(branchesPaths).forEach(([branch]) => { branch.computedColor = branch.style.color || this.getBranchDefaultColor(branchesOrder, branch.name); }); } /** * Return commit messages X position for rendering. * * @param branchesPaths Branches paths to be rendered */ private computeCommitMessagesX(branchesPaths: BranchesPaths): number { const numberOfColumns = Array.from(branchesPaths).length; return numberOfColumns * this.template.branch.spacing; } /** * Add `branches` property to commit. * * @param branches All branches mapped by commit hash * @param commit Commit */ private withBranches( branches: Map>, commit: Commit ): Commit { let commitBranches = Array.from( (branches.get(commit.hash) || new Set()).values() ); if (commitBranches.length === 0) { // No branch => branch has been deleted. commitBranches = [DELETED_BRANCH_NAME]; } return commit.setBranches(commitBranches); } /** * Get all branches from current commits. */ private getBranches(): Map> { const result = new Map>(); const queue: Array = []; const branches = this.refs.getAllNames().filter((name) => name !== "HEAD"); branches.forEach((branch) => { const commitHash = this.refs.getCommit(branch); if (commitHash) { queue.push(commitHash); } while (queue.length > 0) { const currentHash = queue.pop() as Commit["hash"]; const current = this.commits.find( ({ hash }) => hash === currentHash ) as Commit | null; const prevBranches = result.get(currentHash) || new Set(); prevBranches.add(branch); result.set(currentHash, prevBranches); if (current && current.parents && current.parents.length > 0) { queue.push(current.parents[0]); } } }); return result; } /** * Add position to given commit. * * @param rows Graph rows * @param branchesOrder Computed order of branches * @param commit Commit to position */ private withPosition( rows: GraphRows, branchesOrder: BranchesOrder, commit: Commit ): Commit { const row = rows.getRowOf(commit.hash); const maxRow = rows.getMaxRow(); const order = branchesOrder.get(commit.branchToDisplay); switch (this.orientation) { default: return commit.setPosition({ x: this.initCommitOffsetX + this.template.branch.spacing * order, y: this.initCommitOffsetY + this.template.commit.spacing * (maxRow - row), }); case Orientation.VerticalReverse: return commit.setPosition({ x: this.initCommitOffsetX + this.template.branch.spacing * order, y: this.initCommitOffsetY + this.template.commit.spacing * row, }); case Orientation.Horizontal: return commit.setPosition({ x: this.initCommitOffsetX + this.template.commit.spacing * row, y: this.initCommitOffsetY + this.template.branch.spacing * order, }); case Orientation.HorizontalReverse: return commit.setPosition({ x: this.initCommitOffsetX + this.template.commit.spacing * (maxRow - row), y: this.initCommitOffsetY + this.template.branch.spacing * order, }); } } /** * Return the default color for given branch. * * @param branchesOrder Computed order of branches * @param branchName Name of the branch */ private getBranchDefaultColor( branchesOrder: BranchesOrder, branchName: Branch["name"] ): string { return branchesOrder.getColorOf(branchName); } /** * Tell each listener something new happened. * E.g. a rendering library will know it needs to re-render the graph. */ private next() { if (this.nextTimeoutId) { window.clearTimeout(this.nextTimeoutId); } // Use setTimeout() with `0` to debounce call to next tick. this.nextTimeoutId = window.setTimeout(() => { this.listeners.forEach((listener) => listener(this.getRenderedData())); }, 0); } } interface CommitRenderOptions { renderDot?: (commit: Commit) => TNode; renderMessage?: (commit: Commit) => TNode; renderTooltip?: (commit: Commit) => TNode; } interface CommitOptions extends CommitRenderOptions { author: string; subject: string; style: CommitStyle; body?: string; hash?: string; parents?: string[]; dotText?: string; onClick?: (commit: Commit) => void; onMessageClick?: (commit: Commit) => void; onMouseOver?: (commit: Commit) => void; onMouseOut?: (commit: Commit) => void; } /** * Generate a random hash. * * @return hex string with 40 chars */ const getRandomHash = () => ( Math.random().toString(16).substring(3) + Math.random().toString(16).substring(3) + Math.random().toString(16).substring(3) + Math.random().toString(16).substring(3) ).substring(0, 40); class Commit { /** * Ref names */ public refs: Array = []; /** * Commit x position */ public x = 0; /** * Commit y position */ public y = 0; /** * Commit hash */ public hash: string; /** * Abbreviated commit hash */ public hashAbbrev: string; /** * Parent hashes */ public parents: Array["hash"]>; /** * Abbreviated parent hashed */ public parentsAbbrev: Array["hashAbbrev"]>; /** * Author */ public author: { /** * Author name */ name: string; /** * Author email */ email: string; /** * Author date */ timestamp: number; }; /** * Committer */ public committer: { /** * Commiter name */ name: string; /** * Commiter email */ email: string; /** * Commiter date */ timestamp: number; }; /** * Subject */ public subject: string; /** * Body */ public body: string; /** * Message */ public get message() { let message = ""; if (this.style.message.displayHash) { message += `${this.hashAbbrev} `; } message += this.subject; if (this.style.message.displayAuthor) { message += ` - ${this.author.name} <${this.author.email}>`; } return message; } /** * Style */ public style: CommitStyle; /** * Text inside commit dot */ public dotText?: string; /** * List of branches attached */ public branches?: Array; /** * Branch that should be rendered */ public get branchToDisplay(): Branch["name"] { return this.branches ? this.branches[0] : ""; } /** * List of tags attached */ public tags?: Array>; /** * Callback to execute on click. */ public onClick: () => void; /** * Callback to execute on click on the commit message. */ public onMessageClick: () => void; /** * Callback to execute on mouse over. */ public onMouseOver: () => void; /** * Callback to execute on mouse out. */ public onMouseOut: () => void; /** * Custom dot render */ public renderDot?: (commit: Commit) => TNode; /** * Custom message render */ public renderMessage?: (commit: Commit) => TNode; /** * Custom tooltip render */ public renderTooltip?: (commit: Commit) => TNode; constructor(options: CommitOptions) { // Set author & committer let name, email; try { [, name, email] = options.author.match(/(.*) <(.*)>/) as RegExpExecArray; } catch (e) { [name, email] = [options.author, ""]; } this.author = { name, email, timestamp: Date.now() }; this.committer = { name, email, timestamp: Date.now() }; // Set commit message this.subject = options.subject; this.body = options.body || ""; // Set commit hash this.hash = options.hash || getRandomHash(); this.hashAbbrev = this.hash.substring(0, 7); // Set parent hash this.parents = options.parents ? options.parents : []; this.parentsAbbrev = this.parents.map((commit) => commit.substring(0, 7)); // Set style this.style = { ...options.style, message: { ...options.style.message }, dot: { ...options.style.dot }, }; this.dotText = options.dotText; // Set callbacks this.onClick = () => (options.onClick ? options.onClick(this) : undefined); this.onMessageClick = () => options.onMessageClick ? options.onMessageClick(this) : undefined; this.onMouseOver = () => options.onMouseOver ? options.onMouseOver(this) : undefined; this.onMouseOut = () => options.onMouseOut ? options.onMouseOut(this) : undefined; // Set custom renders this.renderDot = options.renderDot; this.renderMessage = options.renderMessage; this.renderTooltip = options.renderTooltip; } public setRefs(refs: Refs): this { this.refs = refs.getNames(this.hash); return this; } public setTags( tags: Refs, getTagStyle: (name: Tag["name"]) => Partial, getTagRender: ( name: Tag["name"] ) => GitgraphTagOptions["render"] ): this { this.tags = tags .getNames(this.hash) .map( (name) => new Tag(name, getTagStyle(name), getTagRender(name), this.style) ); return this; } public setBranches(branches: Array): this { this.branches = branches; return this; } public setPosition({ x, y }: { x: number; y: number }): this { this.x = x; this.y = y; return this; } public withDefaultColor(color: string): Commit { const newStyle = { ...this.style, dot: { ...this.style.dot }, message: { ...this.style.message }, }; if (!newStyle.color) newStyle.color = color; if (!newStyle.dot.color) newStyle.dot.color = color; if (!newStyle.message.color) newStyle.message.color = color; const commit = this.cloneCommit(); commit.style = newStyle; return commit; } /** * Ideally, we want Commit to be a [Value Object](https://martinfowler.com/bliki/ValueObject.html). * We started with a mutable class. So we'll refactor that little by little. * This private function is a helper to create a new Commit from existing one. */ private cloneCommit() { const commit = new Commit({ author: `${this.author.name} <${this.author.email}>`, subject: this.subject, style: this.style, body: this.body, hash: this.hash, parents: this.parents, dotText: this.dotText, onClick: this.onClick, onMessageClick: this.onMessageClick, onMouseOver: this.onMouseOver, onMouseOut: this.onMouseOut, renderDot: this.renderDot, renderMessage: this.renderMessage, renderTooltip: this.renderTooltip, }); commit.refs = this.refs; commit.branches = this.branches; commit.tags = this.tags; commit.x = this.x; commit.y = this.y; return commit; } } interface BranchCommitDefaultOptions extends CommitRenderOptions { author?: string; subject?: string; style?: TemplateOptions["commit"]; } interface BranchRenderOptions { renderLabel?: (branch: Branch) => TNode; } interface BranchOptions extends BranchRenderOptions { /** * Gitgraph constructor */ gitgraph: GitgraphCore; /** * Branch name */ name: string; /** * Branch style */ style: BranchStyle; /** * Parent commit */ parentCommitHash?: Commit["hash"]; /** * Default options for commits */ commitDefaultOptions?: BranchCommitDefaultOptions; /** * On graph update. */ onGraphUpdate: () => void; } const DELETED_BRANCH_NAME = ""; class Branch { public name: BranchOptions["name"]; public style: BranchStyle; public computedColor?: BranchStyle["color"]; public parentCommitHash: BranchOptions["parentCommitHash"]; public commitDefaultOptions: BranchCommitDefaultOptions; public renderLabel: BranchOptions["renderLabel"]; private gitgraph: GitgraphCore; private onGraphUpdate: () => void; constructor(options: BranchOptions) { this.gitgraph = options.gitgraph; this.name = options.name; this.style = options.style; this.parentCommitHash = options.parentCommitHash; this.commitDefaultOptions = options.commitDefaultOptions || { style: {} }; this.onGraphUpdate = options.onGraphUpdate; this.renderLabel = options.renderLabel; } /** * Return the API to manipulate Gitgraph branch as a user. */ public getUserApi(): BranchUserApi { return new BranchUserApi(this, this.gitgraph, this.onGraphUpdate); } /** * Return true if branch was deleted. */ public isDeleted(): boolean { return this.name === DELETED_BRANCH_NAME; } } function createDeletedBranch( gitgraph: GitgraphCore, style: BranchStyle, onGraphUpdate: () => void ): Branch { return new Branch({ name: DELETED_BRANCH_NAME, gitgraph, style, onGraphUpdate, }); } /** * Branch merge style enum */ enum MergeStyle { Bezier = "bezier", Straight = "straight", } /** * Arrow style */ interface ArrowStyle { /** * Arrow color */ color: string | null; /** * Arrow size in pixel */ size: number | null; /** * Arrow offset in pixel */ offset: number; } type ArrowStyleOptions = Partial; interface BranchStyle { /** * Branch color */ color?: string; /** * Branch line width in pixel */ lineWidth: number; /** * Branch merge style */ mergeStyle: MergeStyle; /** * Space between branches */ spacing: number; /** * Branch label style */ label: BranchLabelStyleOptions; } type BranchStyleOptions = Partial; interface BranchLabelStyle { /** * Branch label visibility */ display: boolean; /** * Branch label text color */ color: string; /** * Branch label stroke color */ strokeColor: string; /** * Branch label background color */ bgColor: string; /** * Branch label font */ font: string; /** * Branch label border radius */ borderRadius: number; } type BranchLabelStyleOptions = Partial; export interface TagStyle { /** * Tag text color */ color: string; /** * Tag stroke color */ strokeColor?: string; /** * Tag background color */ bgColor?: string; /** * Tag font */ font: string; /** * Tag border radius */ borderRadius: number; /** * Width of the tag pointer */ pointerWidth: number; } type TagStyleOptions = Partial; interface CommitDotStyle { /** * Commit dot color */ color?: string; /** * Commit dot size in pixel */ size: number; /** * Commit dot stroke width */ strokeWidth?: number; /** * Commit dot stroke color */ strokeColor?: string; /** * Commit dot font */ font: string; } type CommitDotStyleOptions = Partial; interface CommitMessageStyle { /** * Commit message color */ color?: string; /** * Commit message display policy */ display: boolean; /** * Commit message author display policy */ displayAuthor: boolean; /** * Commit message hash display policy */ displayHash: boolean; /** * Commit message font */ font: string; } type CommitMessageStyleOptions = Partial; interface CommitStyleBase { /** * Spacing between commits */ spacing: number; /** * Commit color (dot & message) */ color?: string; /** * Tooltips policy */ hasTooltipInCompactMode: boolean; } interface CommitStyle extends CommitStyleBase { /** * Commit message style */ message: CommitMessageStyle; /** * Commit dot style */ dot: CommitDotStyle; } interface CommitStyleOptions extends Partial { /** * Commit message style */ message?: CommitMessageStyleOptions; /** * Commit dot style */ dot?: CommitDotStyleOptions; } interface TemplateOptions { /** * Colors scheme: One color for each column */ colors?: string[]; /** * Arrow style */ arrow?: ArrowStyleOptions; /** * Branch style */ branch?: BranchStyleOptions; /** * Commit style */ commit?: CommitStyleOptions; /** * Tag style */ tag?: TagStyleOptions; } export const DEFAULT_FONT = "normal 12pt Calibri"; /** * Gitgraph template * * Set of design rules for the rendering. */ class Template { /** * Colors scheme: One color for each column */ public colors: string[]; /** * Arrow style */ public arrow: ArrowStyle; /** * Branch style */ public branch: BranchStyle; /** * Commit style */ public commit: CommitStyle; /** * Tag style */ public tag: TagStyleOptions; constructor(options: TemplateOptions) { // Options options.branch = options.branch || {}; options.branch.label = options.branch.label || {}; options.arrow = options.arrow || {}; options.commit = options.commit || {}; options.commit.dot = options.commit.dot || {}; options.commit.message = options.commit.message || {}; // One color per column this.colors = options.colors || ["#000000"]; // Branch style this.branch = { color: options.branch.color, lineWidth: options.branch.lineWidth || 2, mergeStyle: options.branch.mergeStyle || MergeStyle.Bezier, spacing: numberOptionOr(options.branch.spacing, 20), label: { display: booleanOptionOr(options.branch.label.display, true), color: options.branch.label.color || options.commit.color, strokeColor: options.branch.label.strokeColor || options.commit.color, bgColor: options.branch.label.bgColor || "white", font: options.branch.label.font || options.commit.message.font || DEFAULT_FONT, borderRadius: numberOptionOr(options.branch.label.borderRadius, 10), }, }; // Arrow style this.arrow = { size: options.arrow.size || null, color: options.arrow.color || null, offset: options.arrow.offset || 2, }; // Commit style this.commit = { color: options.commit.color, spacing: numberOptionOr(options.commit.spacing, 25), hasTooltipInCompactMode: booleanOptionOr( options.commit.hasTooltipInCompactMode, true ), dot: { color: options.commit.dot.color || options.commit.color, size: options.commit.dot.size || 3, strokeWidth: numberOptionOr(options.commit.dot.strokeWidth, 0), strokeColor: options.commit.dot.strokeColor, font: options.commit.dot.font || options.commit.message.font || "normal 10pt Calibri", }, message: { display: booleanOptionOr(options.commit.message.display, true), displayAuthor: booleanOptionOr( options.commit.message.displayAuthor, true ), displayHash: booleanOptionOr(options.commit.message.displayHash, true), color: options.commit.message.color || options.commit.color, font: options.commit.message.font || DEFAULT_FONT, }, }; // Tag style // This one is computed in the Tag instance. It needs Commit style // that is partially computed at runtime (for colors). this.tag = options.tag || {}; } } /** * Black arrow template */ const blackArrowTemplate = new Template({ colors: ["#6963FF", "#47E8D4", "#6BDB52", "#E84BA5", "#FFA657"], branch: { color: "#000000", lineWidth: 4, spacing: 50, mergeStyle: MergeStyle.Straight, }, commit: { spacing: 60, dot: { size: 16, strokeColor: "#000000", strokeWidth: 4, }, message: { color: "black", }, }, arrow: { size: 16, offset: -1.5, }, }); /** * Metro template */ const metroTemplate = new Template({ colors: ["#979797", "#008fb5", "#f1c109"], branch: { lineWidth: 10, spacing: 50, }, commit: { spacing: 80, dot: { size: 14, }, message: { font: "normal 14pt Arial", }, }, }); enum TemplateName { Metro = "metro", BlackArrow = "blackarrow", } /** * Extend an existing template with new options. * * @param selectedTemplate Template to extend * @param options Template options */ function templateExtend( selectedTemplate: TemplateName, options: TemplateOptions ): Template { const template = getTemplate(selectedTemplate); if (!options.branch) options.branch = {}; if (!options.commit) options.commit = {}; // This is tedious, but it seems acceptable so we don't need lodash // as we want to keep bundlesize small. return { colors: options.colors || template.colors, arrow: { ...template.arrow, ...options.arrow, }, branch: { ...template.branch, ...options.branch, label: { ...template.branch.label, ...options.branch.label, }, }, commit: { ...template.commit, ...options.commit, dot: { ...template.commit.dot, ...options.commit.dot, }, message: { ...template.commit.message, ...options.commit.message, }, }, tag: { ...template.tag, ...options.tag, }, }; } /** * Resolve the template to use regarding given `template` value. * * @param template Selected template name, or instance. */ function getTemplate(template?: TemplateName | Template): Template { if (!template) return metroTemplate; if (typeof template === "string") { return { [TemplateName.BlackArrow]: blackArrowTemplate, [TemplateName.Metro]: metroTemplate, }[template]; } return template as Template; } ```
nicoespeon commented 1 year ago

The refactoring as it was implemented was removed so we don't include the whole TS library in the bundle anymore: https://github.com/nicoespeon/abracadabra/pull/934

I plan to eventually get back to the whiteboard and take another approach to make the extension lean on the TS compiler (as a TS plugin) while controlling the UX in VS Code.