diff --git a/README.md b/README.md index 307d933..599b6cf 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - 🏗️ 支持一键部署 - 🛠️ 支持二次集成开发,支持任意 npm registry - 🚀 基于 [Next.js](https://nextjs.org/docs/app/building-your-application/data-fetching) 纯静态部署 +- 🎉 使用 [CodeBlitz](https://github.com/opensumi/codeblitz) 进行代码浏览 [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/cnpm/cnpmweb) diff --git a/next.config.js b/next.config.js index 518a521..9e992e0 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,178 @@ +// const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +// const styleLoader = require('style-loader'); + +const path = require('path'); + +const nextConfig = { + transpilePackages: [ + '@codeblitzjs/ide-core', + '@codeblitzjs/ide-common', + '@codeblitzjs/ide-registry', + '@codeblitzjs/ide-sumi-core', + '@codeblitzjs/ide-plugin', + '@codeblitzjs/ide-i18n', + + ], + env: {}, + // https://github.com/vercel/next.js/issues/36251#issuecomment-1212900096 + httpAgentOptions: { + keepAlive: false, + }, + output: 'export', + lessLoaderOptions: { + lessOptions: { + javascriptEnabled: true, + paths: [ + path.resolve(__dirname, 'node_modules', '@opensumi'), + path.resolve(__dirname, 'node_modules', '@codeblitzjs'), + ], + }, + }, + + webpack: (config, options) => { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + net: false, + child_process: false, + http: false, + https: false, + path: require.resolve('path-browserify'), + os: require.resolve('os-browserify/browser'), + }; + /** + * 配置 webpack 以支持服务端渲染 CodeBlitz + * 使用动态引入组件 vercel 预览失败 https://github.com/cnpm/cnpmweb/pull/64#issuecomment-1925669277 + * TODO 目前 + */ + config.module.rules.push( + { + test: /\.module.less$/, + include: /@opensumi[\\/]ide|@codeblitzjs[\\/]ide/, + use: [ + { + loader: 'style-loader', + options: { + esModule: false, + }, + }, + { + loader: 'css-loader', + options: { + importLoaders: 1, + sourceMap: true, + esModule: false, + modules: { + mode: 'local', + localIdentName: '[local]___[hash:base64:5]', + }, + }, + }, + { + loader: 'less-loader', + options: { + lessOptions: { + javascriptEnabled: true, + }, + }, + }, + ], + }, + { + test: /\.less$/, + include: /@opensumi[\\/]ide|@codeblitzjs[\\/]ide/, + exclude: /\.module.less$/, + use: [ + { + loader: 'style-loader', + options: { + esModule: false, + }, + }, + { + loader: 'css-loader', + options: { + importLoaders: 1, + sourceMap: true, + esModule: false, + modules: { + mode: 'local', + localIdentName: '[local]___[hash:base64:5]', + }, + }, + }, + { + loader: 'less-loader', + options: { + lessOptions: { + javascriptEnabled: true, + }, + }, + }, + ], + }, + { + test: /\.css$/, + use: [ + { + loader: 'style-loader', + options: { + esModule: false, + }, + }, + { + loader: 'css-loader', + options: { + esModule: false, + }, + }, + ], + }, + { + test: /\.(png|jpe?g|gif|webp|ico|svg)(\?.*)?$/, + use: [ + { + loader: 'url-loader', + options: { + limit: 10000, + name: '[name].[ext]', + // require 图片的时候不用加 .default + esModule: false, + fallback: { + loader: 'file-loader', + options: { + name: '[name].[ext]', + esModule: false, + }, + }, + }, + }, + ], + }, + { + test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, + use: [ + { + loader: 'file-loader', + options: { + name: '[name].[ext]', + esModule: false, + publicPath: './', + }, + }, + ], + }, + { + test: /\.(txt|text|md)$/, + use: 'raw-loader', + }, + ); + return config; + }, +}; + +// module.exports = nextConfig; + module.exports = { env: {}, // https://github.com/vercel/next.js/issues/36251#issuecomment-1212900096 @@ -5,4 +180,7 @@ module.exports = { keepAlive: false, }, output: 'export', + typescript: { + ignoreBuildErrors: true + } }; diff --git a/package.json b/package.json index cb2bbdb..79b75b7 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@ant-design/cssinjs": "^1.11.1", "@ant-design/icons": "^5.2.5", + "@codeblitzjs/ide-core": "1.1.1-next-1710748243.0", "@gravatar/js": "^1.1.1", "@monaco-editor/loader": "^1.3.3", "@monaco-editor/react": "^4.4.2", @@ -28,13 +29,18 @@ "lodash": "^4.17.21", "marked": "^1.1.0", "next": "13.4.7", + "next-with-less": "^3.0.1", + "less": "^4.2.0", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", "npm-package-arg": "^11.0.1", "react": "18.2.0", "react-chartjs-2": "^5.2.0", "react-dom": "18.2.0", "react-icons": "^4.3.1", "semver": "^7.5.4", - "swr": "^2.2.0" + "swr": "^2.2.0", + "rc-overflow": "^1.3.2" }, "repository": "https://github.com/cnpm/cnpmweb.git", "devDependencies": { @@ -43,7 +49,14 @@ "@types/npm-package-arg": "^6.1.3", "@types/react": "18.2.14", "@types/semver": "^7.5.0", + "mini-css-extract-plugin": "^2.8.0", "prettier": "^3.0.3", - "typescript": "5.3.3" + "typescript": "5.3.3", + "raw-loader": "^4.0.2", + "file-loader": "^6.2.0", + "less-loader": "^7.1.0", + "style-loader": "^3.3.4", + "css-loader": "^5.0.1", + "url-loader": "^4.1.1" } } diff --git a/src/components/CustomTabs.tsx b/src/components/CustomTabs.tsx index 555578a..8159095 100644 --- a/src/components/CustomTabs.tsx +++ b/src/components/CustomTabs.tsx @@ -1,9 +1,10 @@ 'use client'; -import { Tabs } from 'antd'; +import { Segmented, Space, Tabs } from 'antd'; import Link from 'next/link'; import NPMVersionSelect from './NPMVersionSelect'; import { PackageManifest } from '@/hooks/useManifest'; import { useRouter } from 'next/router'; +import { IDEModeName } from '@/hooks/useCodeBlitz'; const presetTabs = [ { @@ -27,9 +28,13 @@ const presetTabs = [ export default function CustomTabs({ activateKey, pkg, + IDEMode, + setIDEMode, }: { activateKey: string; pkg: PackageManifest; + IDEMode: IDEModeName; + setIDEMode: (v: IDEModeName) => void; }) { const { push, query } = useRouter(); const routerVersion = query.version as string; @@ -40,20 +45,38 @@ export default function CustomTabs({ activeKey={activateKey} type={'line'} tabBarExtraContent={ - { - if (v === pkg?.['dist-tags']?.latest) { - push(`/package/${pkg.name}/${activateKey}`, undefined, { shallow: true }); - } else { - push(`/package/${pkg.name}/${activateKey}?version=${v}`, undefined, { - shallow: true, - }); - } - }} - /> +
+ {activateKey === 'files' && ( + + { + setIDEMode(v as IDEModeName); + }} + /> + + )} + + { + if (v === pkg?.['dist-tags']?.latest) { + push(`/package/${pkg.name}/${activateKey}`, undefined, { shallow: true }); + } else { + push(`/package/${pkg.name}/${activateKey}?version=${v}`, undefined, { + shallow: true, + }); + } + }} + /> +
} items={presetTabs.map((tab) => { return { diff --git a/src/components/IDE/DynamicIDEComponent.tsx b/src/components/IDE/DynamicIDEComponent.tsx new file mode 100644 index 0000000..d8ae8ce --- /dev/null +++ b/src/components/IDE/DynamicIDEComponent.tsx @@ -0,0 +1,17 @@ +import dynamic from 'next/dynamic'; + +export const DynamicIDEComponent = dynamic( + { + loader: async () => { + console.log('loading start IDE'); + const IDEModule = await import('./IDE'); + console.log('loading end IDE'); + return IDEModule.IDE; + }, + ssr: false, + }, + { + loading: () =>
loading...
, + ssr: false, + }, +); diff --git a/src/components/IDE/IDE.tsx b/src/components/IDE/IDE.tsx new file mode 100644 index 0000000..01d5bfc --- /dev/null +++ b/src/components/IDE/IDE.tsx @@ -0,0 +1,165 @@ +import { getFileContent, Directory } from '@/hooks/useFile'; +import { + AppRenderer, + BrowserFSFileType as FileType, + SlotLocation, +} from '@codeblitzjs/ide-core/bundle/index.min'; +import '@codeblitzjs/ide-core/bundle/codeblitz.css'; +import '@codeblitzjs/ide-core/languages'; +import { useEffect, useMemo, useState } from 'react'; +import { useThemeMode } from 'antd-style'; +import * as IDEPlugin from './ide.plugin'; +import IDEStyle from './ide.module.css'; +import { RegisterMenuModule } from './module'; + +const layoutConfig = () => ({ + // [SlotLocation.top]: { + // modules: ['@opensumi/ide-menu-bar'], + // }, + [SlotLocation.action]: { + modules: [''], + }, + [SlotLocation.left]: { + modules: ['@opensumi/ide-explorer', '@opensumi/ide-search'], + }, + [SlotLocation.main]: { + modules: ['@opensumi/ide-editor'], + }, + // [SlotLocation.bottom]: { + // modules: ['@opensumi/ide-output', '@opensumi/ide-markers'], + // }, + // [SlotLocation.statusBar]: { + // modules: ['@opensumi/ide-status-bar'], + // }, + [SlotLocation.extra]: { + modules: ['breadcrumb-menu'], + }, +}); + +function recursiveFind(path: string, treeNode: Directory): Directory { + if (treeNode.path === path) { + return treeNode; + } else { + const currentTree = treeNode.files!.find((item) => item.path === path); + if (currentTree) { + return currentTree as Directory; + } else { + let dir: Directory; + for (let i = 0; i < treeNode.files.length; i++) { + const item = treeNode.files[i]; + if (item.type === 'directory') { + const res = recursiveFind(path, item as Directory); + if (res?.path) { + dir = res; + break; + } + } + } + return dir!; + } + } +} + +function recursivePush(rootDir: Directory): string[] { + const allFiles: string[] = []; + rootDir.files?.forEach((file) => { + if (file.type === 'file') { + let path = file.path; + if (file.path.startsWith('/')) { + path = path.slice(1); + } + allFiles.push(path); + } else if (file.type === 'directory') { + allFiles.push(...recursivePush(file as Directory)); + } + }); + return allFiles; +} + +export const IDE = ({ + pkgName, + spec, + rootDir, +}: { + rootDir: Directory; + pkgName: string; + spec?: string; +}) => { + const { themeMode: theme } = useThemeMode(); + useEffect(() => { + IDEPlugin.api.commands?.executeCommand( + 'alex.setDefaultPreference', + 'general.theme', + theme === 'light' ? 'opensumi-light' : 'opensumi-dark', + ); + }, [theme]); + + const files = useMemo(() => { + const res = recursivePush(rootDir); + return res; + }, [rootDir]); + + return ( +
+ [ + file.path.split('/').pop() as string, + file.type === 'directory' ? FileType.DIRECTORY : FileType.FILE, + ]) || [] + ); + }, + async readFile(p) { + const res = await getFileContent({ fullname: pkgName, spec }, p || ''); + return new TextEncoder().encode(res); + }, + }, + }, + }, + textSearch: { + config: { + replace: false, + wordMatch: 'local', + include: 'local', + exclude: 'local', + }, + provideResults(query, opt, progress) { + // TODO 全局搜索 只能搜索到已经打开过的文件 依赖服务端能力 + return Promise.resolve(); + }, + }, + fileSearch: { + config: { + include: 'local', + exclude: 'local', + }, + provideResults() { + return files; + }, + }, + }} + /> +
+ ); +}; diff --git a/src/components/IDE/ide.module.css b/src/components/IDE/ide.module.css new file mode 100644 index 0000000..020bf30 --- /dev/null +++ b/src/components/IDE/ide.module.css @@ -0,0 +1,4 @@ +.ide-container { + width: '100%'; + height: 'calc(100vh - 120px)'; +} \ No newline at end of file diff --git a/src/components/IDE/ide.plugin.ts b/src/components/IDE/ide.plugin.ts new file mode 100644 index 0000000..a2e108b --- /dev/null +++ b/src/components/IDE/ide.plugin.ts @@ -0,0 +1,18 @@ +import type { IPluginAPI } from '@codeblitzjs/ide-core'; + +let _commands: IPluginAPI['commands'] | null = null; + +export const PLUGIN_ID = 'IDE_PLUGIN'; +export const api = { + get commands() { + return _commands; + }, +}; + +export const activate = ({ commands }: IPluginAPI) => { + _commands = commands; +}; + +export const deactivate = () => { + _commands = null +} diff --git a/src/components/IDE/module/index.ts b/src/components/IDE/module/index.ts new file mode 100644 index 0000000..a96388b --- /dev/null +++ b/src/components/IDE/module/index.ts @@ -0,0 +1,19 @@ +import { requireModule } from "@codeblitzjs/ide-core/bundle/index.min"; +const CommpnDI = requireModule("@opensumi/di"); +const CoreBrowser = requireModule("@opensumi/ide-core-browser"); + +const { Injectable } = CommpnDI; +const { Domain, BrowserModule, MenuId, MenuContribution } = CoreBrowser; + +@Domain(MenuContribution) +class RegisterMenuContribution { + registerMenus(menus: any) { + menus.unregisterMenuId(MenuId.SettingsIconMenu); + } +} + + +@Injectable() +export class RegisterMenuModule extends BrowserModule { + providers = [RegisterMenuContribution]; +} \ No newline at end of file diff --git a/src/hooks/useCodeBlitz.ts b/src/hooks/useCodeBlitz.ts new file mode 100644 index 0000000..b6ebe59 --- /dev/null +++ b/src/hooks/useCodeBlitz.ts @@ -0,0 +1,30 @@ +import { useEffect, useState, createContext } from 'react'; + +const LOCAL_STORAGE_IDE_MODE = 'ideMode'; + + +export enum IDEModeName { + IDE = 'ide', + NATIVE = 'native' +} + +export function useIDE(): [IDEModeName, (v: IDEModeName)=> void] { + const [IDEMode, setIDEMode] = useState(IDEModeName.IDE); + useEffect(() => { + const ideMode = localStorage.getItem(LOCAL_STORAGE_IDE_MODE) as IDEModeName; + if (ideMode) { + setIDEMode(ideMode); + } + }, []); + + return [ + IDEMode, + (v: IDEModeName) => { + localStorage.setItem(LOCAL_STORAGE_IDE_MODE, v); + setIDEMode(v); + }, + ]; +} + + +export const IDEModeContext = createContext(IDEModeName.IDE); diff --git a/src/hooks/useFile.ts b/src/hooks/useFile.ts index 851998b..3acf0ff 100644 --- a/src/hooks/useFile.ts +++ b/src/hooks/useFile.ts @@ -20,6 +20,7 @@ export interface File { type PkgInfo = { fullname: string; spec?: string; + path?: string; }; function sortFiles(files: (File | Directory)[]) { @@ -58,3 +59,15 @@ export const useFileContent = (info: PkgInfo, path: string) => { ); }); }; + +export const getDir = (info: PkgInfo): Promise => { + return fetch(`${REGISTRY}/${info.fullname}/${info.spec}/files`) + .then((res) => res.json()) + .then((res) => { + return Promise.resolve(res); + }); +}; + +export const getFileContent = (info: PkgInfo, path: string) => { + return fetch(`${REGISTRY}/${info.fullname}/${info.spec}/files${path}`).then((res) => res.text()); +}; diff --git a/src/pages/package/[...slug]/index.tsx b/src/pages/package/[...slug]/index.tsx index a120ef0..a417232 100644 --- a/src/pages/package/[...slug]/index.tsx +++ b/src/pages/package/[...slug]/index.tsx @@ -13,6 +13,7 @@ import { Result, Spin } from 'antd'; import Header from '@/components/Header'; import { useTheme } from '@/hooks/useTheme'; import AdHire from '@/components/AdHire'; +import { useIDE, IDEModeName } from '@/hooks/useCodeBlitz'; const DEFAULT_TYPE = 'home'; const ThemeProvider = _ThemeProvider as any; @@ -21,6 +22,7 @@ export type PageProps = { manifest: PackageManifest; version?: string; additionalInfo?: any; + IDEMode?: IDEModeName; }; function getPkgName(pathGroups: string[]) { @@ -71,6 +73,7 @@ const PageMap: Record JSX.Element> = { // 需要在页面中自行解析 export default function PackagePage({}: {}) { const router = useRouter(); + const [IDEMode, setIDEMode] = useIDE(); const [themeMode, setThemeMode] = useTheme(); @@ -143,10 +146,15 @@ export default function PackagePage({}: {}) { setThemeMode={setThemeMode} />
- +
- +