Skip to content

Commit

Permalink
feat(npm): finish npm init/install/update/delete logic
Browse files Browse the repository at this point in the history
  • Loading branch information
JiyuShao committed Aug 9, 2024
1 parent b65facd commit 9d0edef
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 88 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Yet Another IDE: 基于浏览器的在线代码编辑器,欢迎各位大佬来
- [x] 实现 Ctrl+L/clear 清屏能力
- [x] 实现命令行历史记录能力
- [ ] JS 在线开发调试
- [ ] 支持 NPM 包
- [x] 支持 NPM 包
- [ ] 实现 JS 在线打包逻辑
- [ ] 实现 JS 在线运行/调试逻辑

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ export abstract class Client {
>;
abstract init(): void;
abstract install(args: {
packages: { name: string; version?: string }[];
isDev?: boolean;
packages: { name: string; version: string }[];
}): void;
abstract update(args: {
packages: { name: string; version: string }[];
}): void;
abstract update(args: { query: string }): void;
abstract remove(args: { packages: string[] }): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { injectable } from '../../../utils/webview';
import { FS_SCHEME } from '../../../config';
import { loadDependencies, Manifest } from '../../../utils/sandpack-core';
import { logger } from '../../../utils/logger';
import { writeFile } from '../../../utils/file';
import { writeFile, deleteFile, deleteDir } from '../../../utils/file';
// import { dirname } from '../../../utils/paths';

@injectable()
Expand Down Expand Up @@ -53,52 +53,70 @@ export class NpmClient extends Client {
}
async init() {
const key = this.#uri.path;
if (this.#projectsState[key]) {
if (this.#projectsState[key].status === 'installing') {
return;
} else if (this.#projectsState[key].status === 'success') {
// TODO
return;
}
if (!this.#projectsState[key]) {
this.#projectsState[key] = {
status: 'failed',
dependenciesState: {},
};
}

this.#projectsState[key] = {
status: 'installing',
dependenciesState: {},
};
const currentProject = this.#projectsState[key];

const dependencies = await this.getAllPackages();

if (currentProject.status === 'installing') {
return;
} else if (currentProject.status === 'success') {
let isInited = true;
for (const currentDep of dependencies) {
if (
!currentProject.dependenciesState[currentDep.name] ||
currentProject.dependenciesState[currentDep.name].version !==
currentDep.version ||
currentProject.dependenciesState[currentDep.name].status !== 'success'
) {
isInited = false;
}
}
if (isInited) {
return;
}
}
try {
currentProject.status = 'installing';
await deleteDir(vscode.Uri.parse(`${FS_SCHEME}:/node_modules`));
await this.install({ packages: dependencies });
currentProject.status = 'success';
} catch (_error) {
currentProject.status = 'failed';
}
}
async install(options: {
packages: { name: string; version: string }[];
isDev?: boolean;
}) {
async install(options: { packages: { name: string; version: string }[] }) {
const key = this.#uri.path;
if (!this.#projectsState[key]) {
return;
}
const currentProject = this.#projectsState[key];
const packagesWithoutTypings = options.packages.filter(currentPkg => {
return !currentPkg.name.includes('@types');
});
if (packagesWithoutTypings.length === 0) {
return;
}

await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
},
async (progress, token) => {
// The total number of steps, if known.
const totalSteps = options.packages.length;

for (const [currentIndex, currentPkg] of options.packages.entries()) {
const totalSteps = packagesWithoutTypings.length;
for (const [
currentIndex,
currentPkg,
] of packagesWithoutTypings.entries()) {
// Update the progress bar
progress.report({
increment: currentIndex,
message: `Installing ${options.isDev ? 'dev ' : ''}dependency ${currentPkg.name}@${currentPkg.version}`,
message: `Installing dependency ${currentPkg.name}@${currentPkg.version}`,
});

currentProject.dependenciesState[currentPkg.name] = {
Expand Down Expand Up @@ -147,7 +165,7 @@ export class NpmClient extends Client {
} catch (error) {
currentPkgState.status = 'failed';
logger.error(
`Install ${options.isDev ? 'dev ' : ''}dependency ${currentPkg.name}@${currentPkg.version} failed`,
`Install dependency ${currentPkg.name}@${currentPkg.version} failed`,
error
);
throw error;
Expand All @@ -160,24 +178,23 @@ export class NpmClient extends Client {
}
);
}
async update(_options: { query: string }) {
// const args = ['update', ...query.split(' ')];
// spawn.sync('npm', args, {
// stdio: 'inherit',
// cwd: this.#cwd,
// windowsHide: true,
// shell: false,
// });
this.updatePackageJSON();
async update(options: { packages: { name: string; version: string }[] }) {
await this.install({ packages: options.packages });
}
async remove(_options: { packages: string[] }) {
console.log(_options);
// spawn.sync('npm', ['remove', ...packages], {
// stdio: 'inherit',
// cwd: this.#cwd,
// windowsHide: true,
// shell: false,
// });
async remove(options: { packages: string[] }) {
const key = this.#uri.path;
if (!this.#projectsState[key]) {
return;
}
const currentProject = this.#projectsState[key];
for (const currentPkg of options.packages) {
const currentPkgState = currentProject.dependenciesState[currentPkg];
for (const currentContentUrl of currentPkgState.manifest?.contentUrls ||
[]) {
await deleteFile(vscode.Uri.parse(`${FS_SCHEME}:${currentContentUrl}`));
}
delete currentProject.dependenciesState[currentPkg];
}
this.updatePackageJSON();
}
private async updatePackageJSON() {
Expand All @@ -200,6 +217,7 @@ export class NpmClient extends Client {
currentProject.dependenciesState[currentPkgName].version,
};
}, {});
delete packageJson.devDependencies;
await vscode.workspace.fs.writeFile(
packageJsonUri,
new TextEncoder().encode(JSON.stringify(packageJson, null, 4))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export class PackageJsonController extends Controller {
const client = await this.clientManager.getClient(data.packageJSON);
client.install({
packages: data.packages,
isDev: data.dev,
});
return {};
}
Expand All @@ -44,16 +43,13 @@ export class PackageJsonController extends Controller {
packages: { name: string; maxSatisfyingVersion: string }[];
packageJSON: string;
}) {
const query = data.packages
.map(item => {
if (item.maxSatisfyingVersion) {
return `${item.name}@${item.maxSatisfyingVersion}`;
}
return item.name;
})
.join(' ');
const client = await this.clientManager.getClient(data.packageJSON);
client.update({ query });
client.update({
packages: data.packages.map(e => ({
name: e.name,
version: e.maxSatisfyingVersion,
})),
});
return {};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as vscode from 'vscode';
import 'reflect-metadata';
import { Container } from 'inversify';
import { bootstrap as webviewBootstrap } from '../../utils/webview/app/bootstrap';
import { Bus } from '../../utils/webview/bus/bus';
import routes from './routes';
Expand All @@ -10,45 +11,47 @@ import { PackageJsonController } from './controllers/package-json-controller';
import { logger } from '../../utils/logger';

export async function registerNpm(context: vscode.ExtensionContext) {
const initNpmProject = () => {
const packageJsonController = app.get(PackageJsonController);
packageJsonController.getPackageJSONFiles().then(([packageJsonFile]) => {
if (!packageJsonFile) {
logger.error('PackageJsonController packageJsonFile not found');
return;
}
packageJsonController.init({ packageJSON: packageJsonFile });
});
};
const app = webviewBootstrap({
context,
viewId: 'js-runner-and-debugger.npm',
initCallback: app => {
app.bind(NpmClient).toSelf();
app.bind(ClientManager).toSelf();
routes(app);
initNpmProject();
initNpmProject(app);
},
});
function initNpmProject(app: Container) {
const packageJsonController = app.get(PackageJsonController);
packageJsonController.getPackageJSONFiles().then(([packageJsonFile]) => {
if (!packageJsonFile) {
logger.error('PackageJsonController packageJsonFile not found');
return;
}
packageJsonController.init({ packageJSON: packageJsonFile });
});
}

let webviewView: vscode.WebviewView | null = null;
app
.get(Bus)
.on(WebviewProviderEvents.registered, (webviewView: vscode.WebviewView) => {
if (vscode.workspace.workspaceFolders) {
const watcher =
vscode.workspace.createFileSystemWatcher('/package.json');
const notify = () => {
webviewView.webview.postMessage({ type: 'PACKAGE_JSON_UPDATED' });
initNpmProject();
};
watcher.onDidChange(notify);
watcher.onDidDelete(notify);
watcher.onDidCreate(notify);
context.subscriptions.push(watcher);
}

vscode.workspace.onDidChangeConfiguration(() => {
webviewView.webview.postMessage({ type: 'CONFIG_UPDATED' });
});
.on(WebviewProviderEvents.registered, (webview: vscode.WebviewView) => {
webviewView = webview;
});

if (vscode.workspace.workspaceFolders) {
const watcher = vscode.workspace.createFileSystemWatcher('/package.json');
const notify = () => {
webviewView?.webview.postMessage({ type: 'PACKAGE_JSON_UPDATED' });
initNpmProject(app);
};
watcher.onDidChange(notify);
watcher.onDidDelete(notify);
watcher.onDidCreate(notify);
context.subscriptions.push(watcher);
}

vscode.workspace.onDidChangeConfiguration(() => {
webviewView?.webview.postMessage({ type: 'CONFIG_UPDATED' });
});
}
60 changes: 54 additions & 6 deletions packages/extensions/js-runner-and-debugger/src/web/utils/file.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import * as vscode from 'vscode';
import * as paths from './paths';
import { FS_SCHEME } from '../config';
import { logger } from './logger';

export async function importWorkspaceFromZip(): Promise<vscode.Uri | void> {
const uris = await vscode.window.showOpenDialog({
Expand Down Expand Up @@ -39,17 +42,17 @@ export async function exportWorkspaceToZip(

export async function fileExists(uri: vscode.Uri): Promise<boolean> {
try {
return (await vscode.workspace.fs.stat(uri)).type === vscode.FileType.File;
const stat = await vscode.workspace.fs.stat(uri);
return stat.type === vscode.FileType.File;
} catch (err) {
return false;
}
}

export async function dirExists(uri: vscode.Uri): Promise<boolean> {
try {
return (
(await vscode.workspace.fs.stat(uri)).type === vscode.FileType.Directory
);
const stat = await vscode.workspace.fs.stat(uri);
return stat.type === vscode.FileType.Directory;
} catch (err) {
return false;
}
Expand All @@ -60,12 +63,57 @@ export async function writeFile(
data: Uint8Array
): Promise<boolean> {
try {
if (!dirExists(uri)) {
await vscode.workspace.fs.createDirectory(uri);
if (!(await dirExists(uri))) {
await vscode.workspace.fs.createDirectory(
vscode.Uri.parse(`${FS_SCHEME}:${paths.dirname(uri.path)}`)
);
}
await vscode.workspace.fs.writeFile(uri, data);
return true;
} catch (error) {
return false;
}
}

export async function deleteFile(uri: vscode.Uri): Promise<boolean> {
try {
await vscode.workspace.fs.delete(uri);

let currentDirUri = vscode.Uri.parse(
`${FS_SCHEME}:${paths.dirname(uri.path)}`
);
let currentDirItems =
await vscode.workspace.fs.readDirectory(currentDirUri);
while (currentDirUri.path !== '/' && currentDirItems.length === 0) {
await vscode.workspace.fs.delete(currentDirUri);
currentDirUri = vscode.Uri.parse(
`${FS_SCHEME}:${paths.dirname(currentDirUri.path)}`
);
currentDirItems = await vscode.workspace.fs.readDirectory(currentDirUri);
}
return true;
} catch (error) {
return false;
}
}

export async function deleteDir(dirPath: vscode.Uri): Promise<void> {
try {
if (!(await dirExists(dirPath))) {
return;
}
const files = await vscode.workspace.fs.readDirectory(dirPath);
for (const [name, type] of files) {
const filePath = vscode.Uri.joinPath(dirPath, name);
if (type === vscode.FileType.File) {
await vscode.workspace.fs.delete(filePath);
} else if (type === vscode.FileType.Directory) {
await deleteDir(filePath);
}
}
await vscode.workspace.fs.delete(dirPath);
} catch (error) {
logger.error(`deleteDir error: ${error}`);
throw error;
}
}

0 comments on commit 9d0edef

Please # to comment.