Skip to content

Commit 826b4e4

Browse files
authored
Merge pull request #1015 from microsoft/benibenj/scrawny-flyingfish
Support "ls --tree"
2 parents 7dc3477 + 4e81044 commit 826b4e4

File tree

3 files changed

+187
-73
lines changed

3 files changed

+187
-73
lines changed

src/main.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ module.exports = function (argv: string[]): void {
6161
program
6262
.command('ls')
6363
.description('Lists all the files that will be published/packaged')
64+
.option('--tree', 'Prints the files in a tree format', false)
6465
.option('--yarn', 'Use yarn instead of npm (default inferred from presence of yarn.lock or .yarnrc)')
6566
.option('--no-yarn', 'Use npm instead of yarn (default inferred from absence of yarn.lock or .yarnrc)')
6667
.option<string[]>(
@@ -73,8 +74,8 @@ module.exports = function (argv: string[]): void {
7374
// default must remain undefined for dependencies or we will fail to load defaults from package.json
7475
.option('--dependencies', 'Enable dependency detection via npm or yarn', undefined)
7576
.option('--no-dependencies', 'Disable dependency detection via npm or yarn', undefined)
76-
.action(({ yarn, packagedDependencies, ignoreFile, dependencies }) =>
77-
main(ls({ useYarn: yarn, packagedDependencies, ignoreFile, dependencies }))
77+
.action(({ tree, yarn, packagedDependencies, ignoreFile, dependencies }) =>
78+
main(ls({ tree, useYarn: yarn, packagedDependencies, ignoreFile, dependencies }))
7879
);
7980

8081
program

src/package.ts

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1828,7 +1828,7 @@ export async function pack(options: IPackageOptions = {}): Promise<IPackageResul
18281828
const manifest = await readManifest(cwd);
18291829
const files = await collect(manifest, options);
18301830

1831-
printPackagedFiles(files, cwd, manifest, options);
1831+
await printPackagedFiles(files, cwd, manifest, options);
18321832

18331833
if (options.version && !(options.updatePackageJson ?? true)) {
18341834
manifest.version = options.version;
@@ -1885,23 +1885,13 @@ export async function packageCommand(options: IPackageOptions = {}): Promise<any
18851885
}
18861886

18871887
const stats = await fs.promises.stat(packagePath);
1888-
1889-
let size = 0;
1890-
let unit = '';
1891-
1892-
if (stats.size > 1048576) {
1893-
size = Math.round(stats.size / 10485.76) / 100;
1894-
unit = 'MB';
1895-
} else {
1896-
size = Math.round(stats.size / 10.24) / 100;
1897-
unit = 'KB';
1898-
}
1899-
1900-
util.log.done(`Packaged: ${packagePath} (${files.length} files, ${size}${unit})`);
1888+
const packageSize = util.bytesToString(stats.size);
1889+
util.log.done(`Packaged: ${packagePath} ` + chalk.bold(`(${files.length} files, ${packageSize})`));
19011890
}
19021891

19031892
export interface IListFilesOptions {
19041893
readonly cwd?: string;
1894+
readonly manifest?: Manifest;
19051895
readonly useYarn?: boolean;
19061896
readonly packagedDependencies?: string[];
19071897
readonly ignoreFile?: string;
@@ -1914,7 +1904,7 @@ export interface IListFilesOptions {
19141904
*/
19151905
export async function listFiles(options: IListFilesOptions = {}): Promise<string[]> {
19161906
const cwd = options.cwd ?? process.cwd();
1917-
const manifest = await readManifest(cwd);
1907+
const manifest = options.manifest ?? await readManifest(cwd);
19181908

19191909
if (options.prepublish) {
19201910
await prepublish(cwd, manifest, options.useYarn);
@@ -1923,29 +1913,46 @@ export async function listFiles(options: IListFilesOptions = {}): Promise<string
19231913
return await collectFiles(cwd, getDependenciesOption(options), options.packagedDependencies, options.ignoreFile, manifest.files);
19241914
}
19251915

1916+
interface ILSOptions {
1917+
readonly tree?: boolean;
1918+
readonly useYarn?: boolean;
1919+
readonly packagedDependencies?: string[];
1920+
readonly ignoreFile?: string;
1921+
readonly dependencies?: boolean;
1922+
}
1923+
19261924
/**
1927-
* Lists the files included in the extension's package. Runs prepublish.
1925+
* Lists the files included in the extension's package.
19281926
*/
1929-
export async function ls(options: IListFilesOptions = {}): Promise<void> {
1930-
const files = await listFiles({ ...options, prepublish: true });
1927+
export async function ls(options: ILSOptions = {}): Promise<void> {
1928+
const cwd = process.cwd();
1929+
const manifest = await readManifest(cwd);
19311930

1932-
for (const file of files) {
1933-
console.log(`${file}`);
1931+
const files = await listFiles({ ...options, cwd, manifest });
1932+
1933+
if (options.tree) {
1934+
const printableFileStructure = await util.generateFileStructureTree(
1935+
getDefaultPackageName(manifest, options),
1936+
files.map(f => ({ origin: f, tree: f }))
1937+
);
1938+
console.log(printableFileStructure.join('\n'));
1939+
} else {
1940+
console.log(files.join('\n'));
19341941
}
19351942
}
19361943

19371944
/**
19381945
* Prints the packaged files of an extension.
19391946
*/
1940-
export function printPackagedFiles(files: IFile[], cwd: string, manifest: Manifest, options: IPackageOptions): void {
1947+
export async function printPackagedFiles(files: IFile[], cwd: string, manifest: Manifest, options: IPackageOptions): Promise<void> {
19411948
// Warn if the extension contains a lot of files
19421949
const jsFiles = files.filter(f => /\.js$/i.test(f.path));
19431950
if (files.length > 5000 || jsFiles.length > 100) {
1944-
let message = '\n';
1951+
let message = '';
19451952
message += `This extension consists of ${chalk.bold(String(files.length))} files, out of which ${chalk.bold(String(jsFiles.length))} are JavaScript files. `;
19461953
message += `For performance reasons, you should bundle your extension: ${chalk.underline('https://aka.ms/vscode-bundle-extension')}. `;
19471954
message += `You should also exclude unnecessary files by adding them to your .vscodeignore: ${chalk.underline('https://aka.ms/vscode-vscodeignore')}.\n`;
1948-
console.log(message);
1955+
util.log.warn(message);
19491956
}
19501957

19511958
// Warn if the extension does not have a .vscodeignore file or a files property in package.json
@@ -1954,23 +1961,33 @@ export function printPackagedFiles(files: IFile[], cwd: string, manifest: Manife
19541961
if (!hasDeaultIgnore) {
19551962
let message = '';
19561963
message += `Neither a ${chalk.bold('.vscodeignore')} file nor a ${chalk.bold('"files"')} property in package.json was found. `;
1957-
message += `To ensure only necessary files are included in your extension package, `;
1958-
message += `add a .vscodeignore file or specify the "files" property in package.json. More info: ${chalk.underline('https://aka.ms/vscode-vscodeignore')}`;
1964+
message += `To ensure only necessary files are included in your extension, `;
1965+
message += `add a .vscodeignore file or specify the "files" property in package.json. More info: ${chalk.underline('https://aka.ms/vscode-vscodeignore')}\n`;
19591966
util.log.warn(message);
19601967
}
19611968
}
19621969

19631970
// Print the files included in the package
1964-
const printableFileStructure = util.generateFileStructureTree(getDefaultPackageName(manifest, options), files.map(f => f.path), 35);
1971+
const printableFileStructure = await util.generateFileStructureTree(
1972+
getDefaultPackageName(manifest, options),
1973+
files.map(f => ({
1974+
// File path relative to the extension root
1975+
origin: f.path.startsWith('extension/') ? f.path.substring(10) : f.path,
1976+
// File path in the VSIX
1977+
tree: f.path
1978+
})),
1979+
35 // Print up to 35 files/folders
1980+
);
19651981

19661982
let message = '';
19671983
message += chalk.bold.blue(`Files included in the VSIX:\n`);
19681984
message += printableFileStructure.join('\n');
19691985

1986+
// If not all files have been printed, mention how all files can be printed
19701987
if (files.length + 1 > printableFileStructure.length) {
1971-
// If not all files have been printed, mention how all files can be printed
1972-
message += `\n\n=> Run ${chalk.bold('vsce ls')} to see a list of all included files.\n`;
1988+
message += `\n\n=> Run ${chalk.bold('vsce ls --tree')} to see all included files.`;
19731989
}
19741990

1991+
message += '\n';
19751992
util.log.info(message);
19761993
}

src/util.ts

Lines changed: 139 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { promisify } from 'util';
2+
import * as fs from 'fs';
23
import _read from 'read';
34
import { WebApi, getBasicHandler } from 'azure-devops-node-api/WebApi';
45
import { IGalleryApi, GalleryApi } from 'azure-devops-node-api/GalleryApi';
@@ -184,88 +185,183 @@ export function patchOptionsWithManifest(options: any, manifest: Manifest): void
184185
}
185186
}
186187

187-
export function generateFileStructureTree(rootFolder: string, filePaths: string[], maxPrint: number = Number.MAX_VALUE): string[] {
188+
export function bytesToString(bytes: number): string {
189+
let size = 0;
190+
let unit = '';
191+
192+
if (bytes > 1048576) {
193+
size = Math.round(bytes / 10485.76) / 100;
194+
unit = 'MB';
195+
} else {
196+
size = Math.round(bytes / 10.24) / 100;
197+
unit = 'KB';
198+
}
199+
return `${size} ${unit}`;
200+
}
201+
202+
const FOLDER_SIZE_KEY = "/__FOlDER_SIZE__\\";
203+
const FOLDER_FILES_TOTAL_KEY = "/__FOLDER_CHILDREN__\\";
204+
const FILE_SIZE_WARNING_THRESHOLD = 0.85;
205+
const FILE_SIZE_LARGE_THRESHOLD = 0.2;
206+
207+
export async function generateFileStructureTree(rootFolder: string, filePaths: { origin: string, tree: string }[], printLinesLimit: number = Number.MAX_VALUE): Promise<string[]> {
188208
const folderTree: any = {};
189209
const depthCounts: number[] = [];
190210

191211
// Build a tree structure from the file paths
192-
filePaths.forEach(filePath => {
193-
const parts = filePath.split('/');
212+
// Store the file size in the leaf node and the folder size in the folder node
213+
// Store the number of children in the folder node
214+
for (const filePath of filePaths) {
215+
const parts = filePath.tree.split('/');
194216
let currentLevel = folderTree;
195217

196218
parts.forEach((part, depth) => {
219+
const isFile = depth === parts.length - 1;
220+
221+
// Create the node if it doesn't exist
197222
if (!currentLevel[part]) {
198-
currentLevel[part] = depth === parts.length - 1 ? null : {};
223+
if (isFile) {
224+
// The file size is stored in the leaf node,
225+
currentLevel[part] = 0;
226+
} else {
227+
// The folder size is stored in the folder node
228+
currentLevel[part] = {};
229+
currentLevel[part][FOLDER_SIZE_KEY] = 0;
230+
currentLevel[part][FOLDER_FILES_TOTAL_KEY] = 0;
231+
}
232+
233+
// Count the number of items at each depth
199234
if (depthCounts.length <= depth) {
200235
depthCounts.push(0);
201236
}
202237
depthCounts[depth]++;
203238
}
239+
204240
currentLevel = currentLevel[part];
241+
242+
// Count the total number of children in the nested folders
243+
if (!isFile) {
244+
currentLevel[FOLDER_FILES_TOTAL_KEY]++;
245+
}
205246
});
206-
});
247+
};
207248

208-
// Get max depth
249+
// Get max depth depending on the maximum number of lines allowed to print
209250
let currentDepth = 0;
210-
let countUpToCurrentDepth = depthCounts[0];
251+
let countUpToCurrentDepth = depthCounts[0] + 1 /* root folder */;
211252
for (let i = 1; i < depthCounts.length; i++) {
212-
if (countUpToCurrentDepth + depthCounts[i] > maxPrint) {
253+
if (countUpToCurrentDepth + depthCounts[i] > printLinesLimit) {
213254
break;
214255
}
215256
currentDepth++;
216257
countUpToCurrentDepth += depthCounts[i];
217258
}
218-
219259
const maxDepth = currentDepth;
220-
let message: string[] = [];
221260

222-
// Helper function to print the tree
223-
const printTree = (tree: any, depth: number, prefix: string) => {
261+
// Get all file sizes
262+
const fileSizes: [number, string][] = await Promise.all(filePaths.map(async (filePath) => {
263+
try {
264+
const stats = await fs.promises.stat(filePath.origin);
265+
return [stats.size, filePath.tree];
266+
} catch (error) {
267+
return [0, filePath.origin];
268+
}
269+
}));
270+
271+
// Store all file sizes in the tree
272+
let totalFileSizes = 0;
273+
fileSizes.forEach(([size, filePath]) => {
274+
totalFileSizes += size;
275+
276+
const parts = filePath.split('/');
277+
let currentLevel = folderTree;
278+
parts.forEach(part => {
279+
if (typeof currentLevel[part] === 'number') {
280+
currentLevel[part] = size;
281+
} else if (currentLevel[part]) {
282+
currentLevel[part][FOLDER_SIZE_KEY] += size;
283+
}
284+
currentLevel = currentLevel[part];
285+
});
286+
});
287+
288+
let output: string[] = [];
289+
output.push(chalk.bold(rootFolder));
290+
output.push(...createTreeOutput(folderTree, maxDepth, totalFileSizes));
291+
292+
for (const [size, filePath] of fileSizes) {
293+
if (size > FILE_SIZE_WARNING_THRESHOLD * totalFileSizes) {
294+
output.push(`\nThe file ${filePath} is ${chalk.red('large')} (${bytesToString(size)})`);
295+
break;
296+
}
297+
}
298+
299+
return output;
300+
}
301+
302+
function createTreeOutput(fileSystem: any, maxDepth: number, totalFileSizes: number): string[] {
303+
304+
const getColorFromSize = (size: number) => {
305+
if (size > FILE_SIZE_WARNING_THRESHOLD * totalFileSizes) {
306+
return chalk.red;
307+
} else if (size > FILE_SIZE_LARGE_THRESHOLD * totalFileSizes) {
308+
return chalk.yellow;
309+
} else {
310+
return chalk.grey;
311+
}
312+
};
313+
314+
const createFileOutput = (prefix: string, fileName: string, fileSize: number) => {
315+
let fileSizeColored = '';
316+
if (fileSize > 0) {
317+
const fileSizeString = `[${bytesToString(fileSize)}]`;
318+
fileSizeColored = getColorFromSize(fileSize)(fileSizeString);
319+
}
320+
return `${prefix}${fileName} ${fileSizeColored}`;
321+
}
322+
323+
const createFolderOutput = (prefix: string, filesCount: number, folderSize: number, folderName: string, depth: number) => {
324+
if (depth < maxDepth) {
325+
// Max depth is not reached, print only the folder
326+
// as children will be printed
327+
return prefix + chalk.bold(`${folderName}/`);
328+
}
329+
330+
// Max depth is reached, print the folder name and additional metadata
331+
// as children will not be printed
332+
const folderSizeString = bytesToString(folderSize);
333+
const folder = chalk.bold(`${folderName}/`);
334+
const numFilesString = chalk.green(`(${filesCount} ${filesCount === 1 ? 'file' : 'files'})`);
335+
const folderSizeColored = getColorFromSize(folderSize)(`[${folderSizeString}]`);
336+
return `${prefix}${folder} ${numFilesString} ${folderSizeColored}`;
337+
}
338+
339+
const createTreeLayerOutput = (tree: any, depth: number, prefix: string, path: string) => {
224340
// Print all files before folders
225-
const sortedFolderKeys = Object.keys(tree).filter(key => tree[key] !== null).sort();
226-
const sortedFileKeys = Object.keys(tree).filter(key => tree[key] === null).sort();
227-
const sortedKeys = [...sortedFileKeys, ...sortedFolderKeys];
341+
const sortedFolderKeys = Object.keys(tree).filter(key => typeof tree[key] !== 'number').sort();
342+
const sortedFileKeys = Object.keys(tree).filter(key => typeof tree[key] === 'number').sort();
343+
const sortedKeys = [...sortedFileKeys, ...sortedFolderKeys].filter(key => key !== FOLDER_SIZE_KEY && key !== FOLDER_FILES_TOTAL_KEY);
228344

345+
const output: string[] = [];
229346
for (let i = 0; i < sortedKeys.length; i++) {
230-
231347
const key = sortedKeys[i];
232348
const isLast = i === sortedKeys.length - 1;
233349
const localPrefix = prefix + (isLast ? '└─ ' : '├─ ');
234350
const childPrefix = prefix + (isLast ? ' ' : '│ ');
235351

236-
if (tree[key] === null) {
352+
if (typeof tree[key] === 'number') {
237353
// It's a file
238-
message.push(localPrefix + key);
354+
output.push(createFileOutput(localPrefix, key, tree[key]));
239355
} else {
240356
// It's a folder
357+
output.push(createFolderOutput(localPrefix, tree[key][FOLDER_FILES_TOTAL_KEY], tree[key][FOLDER_SIZE_KEY], key, depth));
241358
if (depth < maxDepth) {
242-
// maxdepth is not reached, print the folder and its children
243-
message.push(localPrefix + chalk.bold(`${key}/`));
244-
printTree(tree[key], depth + 1, childPrefix);
245-
} else {
246-
// max depth is reached, print the folder but not its children
247-
const filesCount = countFiles(tree[key]);
248-
message.push(localPrefix + chalk.bold(`${key}/`) + chalk.green(` (${filesCount} ${filesCount === 1 ? 'file' : 'files'})`));
359+
output.push(...createTreeLayerOutput(tree[key], depth + 1, childPrefix, path + key + '/'));
249360
}
250361
}
251362
}
363+
return output;
252364
};
253365

254-
// Helper function to count the number of files in a tree
255-
const countFiles = (tree: any): number => {
256-
let filesCount = 0;
257-
for (const key in tree) {
258-
if (tree[key] === null) {
259-
filesCount++;
260-
} else {
261-
filesCount += countFiles(tree[key]);
262-
}
263-
}
264-
return filesCount;
265-
};
266-
267-
message.push(chalk.bold(rootFolder));
268-
printTree(folderTree, 0, '');
269-
270-
return message;
271-
}
366+
return createTreeLayerOutput(fileSystem, 0, '', '');
367+
}

0 commit comments

Comments
 (0)