diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index ab62820d..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -module.exports = { - extends: [ - 'plugin:putout/safe+align', - ], - plugins: [ - 'putout', - 'n', - ], - rules: { - 'key-spacing': 'off', - 'n/prefer-node-protocol': 'error', - }, - overrides: [{ - files: ['bin/release.js'], - rules: { - 'no-console': 'off', - 'n/shebang': 'off', - }, - }, { - files: ['client/dom/index.js'], - rules: { - 'no-multi-spaces': 'off', - }, - }, { - files: ['bin/cloudcmd.js'], - rules: { - 'no-console': 'off', - }, - }, { - files: ['{client,common,static}/**/*.js'], - env: { - browser: true, - }, - }], -}; diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 93b856e8..9fdb6d3c 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,3 @@ github: coderaiser -patreon: coderaiser open_collective: cloudcmd ko_fi: coderaiser diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 45635a3c..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,13 +0,0 @@ - - -- **Version** (`cloudcmd -v`): -- **Node Version** `node -v`: -- **OS** (`uname -a` on Linux): -- **Browser name/version**: -- **Used Command Line Parameters**: -- **Changed Config**: -- [ ] **I'm ready to donate on [Patreon](https://patreon.com/coderaiser) 🎁** -- [ ] **I'm willing to work on this issue 💪** diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..5c1e7460 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,45 @@ +--- + +name: Bug report +about: Create a report to help us improve +title: '' +labels: needs clarification +assignees: coderaiser + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + +- **Version** (`cloudcmd -v`): +- **Node Version** `node -v`: +- **OS** (`uname -a` on Linux): +- **Browser name/version**: +- **Used Command Line Parameters**: +- **Changed Config**: + +```json +{} +``` +- [ ] 🎁 **I'm ready to donate on https://opencollective.com/cloudcmd** +- [ ] 🎁 **I'm ready to donate on https://ko-fi.com/coderaiser** +- [ ] 💪 **I'm willing to work on this issue** + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..5f41e73a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Stack Overflow + url: https://stackoverflow.com/search?q=cloudcmd + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..549a8874 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- + +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md new file mode 100644 index 00000000..17bf5831 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_template.md @@ -0,0 +1,24 @@ +*** + +name: Tracking issue +about: Create an issue with bug report or feature request. +title: "" +labels: needs triage +assignees: coderaiser + +*** + +- **Version** (`cloudcmd -v`): +- **Node Version** `node -v`: +- **OS** (`uname -a` on Linux): +- **Browser name/version**: +- **Used Command Line Parameters**: +- **Changed Config**: + +```json +{} +``` + +- [ ] 🎁 **I'm ready to donate on https://opencollective.com/cloudcmd** +- [ ] 🎁 **I'm ready to donate on https://ko-fi.com/coderaiser** +- [ ] 💪 **I'm willing to work on this issue** diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5a932ac7..376b0f08 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -6,29 +6,30 @@ on: jobs: buildx: runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - name: Checkout - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v1 + uses: actions/checkout@v5 + - uses: oven-sh/setup-bun@v2 with: bun-version: latest - - name: Use Node.js 18.x - uses: actions/setup-node@v4 + - name: Use Node.js 22.x + uses: actions/setup-node@v6 with: - node-version: 18.x + node-version: 22.x - name: Install Redrun - run: bun i yarn redrun -g --no-save + run: bun i redrun -g --no-save - name: NPM Install - run: yarn --no-lockfile + run: bun i --no-save - name: Lint run: redrun lint - name: Build id: build - run: > + run: | redrun build - - echo "::set-output name=version::$(grep '"version":' package.json -m1 - | cut -d\" -f4)" + echo "::set-output name=version::$(grep '"version":' package.json -m1 | cut -d\" -f4)" - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx @@ -38,6 +39,12 @@ jobs: with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push base-image uses: docker/build-push-action@v5 with: @@ -48,6 +55,8 @@ jobs: tags: | coderaiser/cloudcmd:latest coderaiser/cloudcmd:${{ steps.build.outputs.version }} + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }} - name: Build and push alpine-image uses: docker/build-push-action@v5 with: @@ -58,3 +67,5 @@ jobs: tags: | coderaiser/cloudcmd:latest-alpine coderaiser/cloudcmd:${{ steps.build.outputs.version }}-alpine + ghcr.io/${{ github.repository }}:latest-alpine + ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-alpine diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 9dc3035e..1b392900 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -9,21 +9,22 @@ jobs: strategy: matrix: node-version: - - 18.x - - 20.x + - 22.x + - 24.x + - 25.x steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v1 + - uses: actions/checkout@v5 + - uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Install Redrun - run: bun i yarn redrun -g --no-save + run: bun i redrun -g --no-save - name: Install - run: yarn --no-lockfile + run: bun i --no-save - name: Lint run: redrun fix:lint - uses: actions/cache@v4 diff --git a/.madrun.mjs b/.madrun.mjs index 428eec7a..c8779b5f 100644 --- a/.madrun.mjs +++ b/.madrun.mjs @@ -1,8 +1,7 @@ -import {run, cutEnv} from 'madrun'; import process from 'node:process'; +import {run, cutEnv} from 'madrun'; const testEnv = { - THREAD_IT_COUNT: 0, SUPERTAPE_TIMEOUT: 7000, }; @@ -16,6 +15,7 @@ const is20 = process.version.startsWith('v2'); // https://stackoverflow.com/a/69746937/4536327 const buildEnv = (is17 || is20) && { NODE_OPTIONS: '--openssl-legacy-provider', + NODE_ENV: 'production', }; export default { diff --git a/.npmignore b/.npmignore index de3559c7..b668cf7f 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,6 @@ .* -*.spec.js +*.spec.* +*.config.* *.fixture.js* manifest.yml docker @@ -13,13 +14,13 @@ html yarn-error.log yarn.lock now.json -cssnano.config.js app.json bower.json manifest.yml +deno.json -bin/release.js +bin/release.mjs client img/logo/cloudcmd-hq.png @@ -30,3 +31,4 @@ webpack.config.js *.cdr *.eps +*.config.* diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..1efe0ac6 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.15.1 diff --git a/.nycrc.json b/.nycrc.json index df171b24..2a772159 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -4,7 +4,9 @@ "exclude": [ "**/*.spec.js", "**/fixture", - "**/*.*.js" + "**/*.*.js", + "**/*.config.*", + "**/test/**" ], "branches": 100, "lines": 100, diff --git a/.putout.json b/.putout.json index 1d2b1379..b868f59d 100644 --- a/.putout.json +++ b/.putout.json @@ -4,20 +4,15 @@ "html", "fixture*", "app.json", - "fontello.json" + "fontello.json", + "*.md" ], "rules": { - "github/convert-npm-to-bun": "off", - "github/set-node-versions": ["on", { - "versions": [ - "18.x", - "20.x" - ] - }] + "package-json/add-type": "off" }, "match": { "base64": { - "convert-typeof-to-is-type": "off" + "types/convert-typeof-to-is-type": "off" }, "*.md": { "nodejs/convert-commonjs-to-esm": "on" @@ -34,9 +29,12 @@ "server/{server,exit,terminal,distribute/log}.{js,mjs}": { "remove-console": "off" }, - "client/{client,cloudcmd,load-module}.js": { + "client/{client,cloudcmd,load-module}.{js,mjs}": { "remove-console": "off" }, + "client": { + "nodejs": "off" + }, "client/sw": { "remove-console": "off" }, @@ -51,6 +49,9 @@ }, "vim.js": { "merge-duplicate-functions": "off" + }, + "common": { + "nodejs/declare": "off" } } } diff --git a/.webpack/css.js b/.webpack/css.js deleted file mode 100644 index e1d6552d..00000000 --- a/.webpack/css.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict'; - -const {env} = require('node:process'); -const fs = require('node:fs'); -const { - basename, - extname, - join, -} = require('node:path'); - -const ExtractTextPlugin = require('extract-text-webpack-plugin'); -const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); - -const isDev = env.NODE_ENV === 'development'; - -const extractCSS = (a) => new ExtractTextPlugin(`${a}.css`); -const extractMain = extractCSS('[name]'); - -const cssNames = [ - 'nojs', - 'view', - 'config', - 'terminal', - 'user-menu', - ...getCSSList('columns'), - ...getCSSList('themes'), -]; - -const cssPlugins = cssNames.map(extractCSS); -const clean = (a) => a.filter(Boolean); - -const plugins = clean([ - ...cssPlugins, - extractMain, - !isDev && new OptimizeCssAssetsPlugin(), -]); - -const rules = [{ - test: /\.css$/, - exclude: /css\/(nojs|view|config|terminal|user-menu|columns.*|themes.*)\.css/, - use: extractMain.extract(['css-loader']), -}, ...cssPlugins.map(extract), { - test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/, - use: { - loader: 'url-loader', - options: { - limit: 100_000, - }, - }, -}]; - -module.exports = { - plugins, - module: { - rules, - }, -}; - -function getCSSList(dir) { - const base = (a) => basename(a, extname(a)); - const addDir = (name) => `${dir}/${name}`; - const rootDir = join(__dirname, '..'); - - return fs - .readdirSync(`${rootDir}/css/${dir}`) - .map(base) - .map(addDir); -} - -function extract(extractPlugin) { - const {filename} = extractPlugin; - - return { - test: RegExp(`css/${filename}`), - use: extractPlugin.extract(['css-loader']), - }; -} diff --git a/.webpack/css.mjs b/.webpack/css.mjs new file mode 100644 index 00000000..338ae91b --- /dev/null +++ b/.webpack/css.mjs @@ -0,0 +1,34 @@ +import {env} from 'node:process'; +import OptimizeCssAssetsPlugin from 'optimize-css-assets-webpack-plugin'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; + +const isDev = env.NODE_ENV === 'development'; + +const clean = (a) => a.filter(Boolean); + +const plugins = clean([ + new MiniCssExtractPlugin({ + filename: '[name].css', + }), + !isDev && new OptimizeCssAssetsPlugin(), +]); + +const rules = [{ + test: /\.css$/i, + use: [MiniCssExtractPlugin.loader, { + loader: 'css-loader', + options: { + url: true, + }, + }], +}, { + test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/, + type: 'asset/inline', +}]; + +export default { + plugins, + module: { + rules, + }, +}; diff --git a/.webpack/html.js b/.webpack/html.mjs similarity index 85% rename from .webpack/html.js rename to .webpack/html.mjs index 3717bcc9..e90038ac 100644 --- a/.webpack/html.js +++ b/.webpack/html.mjs @@ -1,11 +1,9 @@ -'use strict'; - -const {env} = require('node:process'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); +import {env} from 'node:process'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; const isDev = env.NODE_ENV === 'development'; -const plugins = [ +export const plugins = [ new HtmlWebpackPlugin({ inject: false, template: 'html/index.html', @@ -13,10 +11,6 @@ const plugins = [ }), ]; -module.exports = { - plugins, -}; - function getMinifyHtmlOptions() { return { removeComments: true, diff --git a/.webpack/js.js b/.webpack/js.mjs similarity index 55% rename from .webpack/js.js rename to .webpack/js.mjs index cc5f4632..d52e0388 100644 --- a/.webpack/js.js +++ b/.webpack/js.mjs @@ -1,24 +1,23 @@ -'use strict'; +import {resolve, sep} from 'node:path'; +import {env} from 'node:process'; +import webpack from 'webpack'; +import WebpackBar from 'webpackbar'; const { - resolve, - sep, - join, -} = require('node:path'); + EnvironmentPlugin, + NormalModuleReplacementPlugin, +} = webpack; -const {EnvironmentPlugin} = require('webpack'); -const WebpackBar = require('webpackbar'); - -const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin'); - -const {env} = require('node:process'); const modules = './modules'; const dirModules = './client/modules'; +const dirCss = './css'; +const dirThemes = `${dirCss}/themes`; +const dirColumns = `${dirCss}/columns`; const dir = './client'; const {NODE_ENV} = env; const isDev = NODE_ENV === 'development'; -const rootDir = join(__dirname, '..'); +const rootDir = new URL('..', import.meta.url).pathname; const dist = resolve(rootDir, 'dist'); const distDev = resolve(rootDir, 'dist-dev'); const devtool = isDev ? 'eval' : 'source-map'; @@ -26,7 +25,7 @@ const devtool = isDev ? 'eval' : 'source-map'; const notEmpty = (a) => a; const clean = (array) => array.filter(notEmpty); -const noParse = (a) => /\.spec\.js$/.test(a); +const noParse = (a) => a.endsWith('.spec.js'); const options = { babelrc: true, }; @@ -46,48 +45,89 @@ const rules = clean([ ]); const plugins = [ + new NormalModuleReplacementPlugin(/^node:/, (resource) => { + resource.request = resource.request.replace(/^node:/, ''); + }), + new NormalModuleReplacementPlugin(/^putout$/, '@putout/bundle'), new EnvironmentPlugin({ NODE_ENV, }), - new ServiceWorkerWebpackPlugin({ - entry: join(__dirname, '..', 'client', 'sw', 'sw.js'), - excludes: ['*'], - }), new WebpackBar(), ]; const splitChunks = { - name: 'cloudcmd.common', chunks: 'all', + cacheGroups: { + abcCommon: { + name: 'cloudcmd.common', + chunks: (chunk) => { + const lazyChunks = [ + 'sw', + 'nojs', + 'view', + 'edit', + 'terminal', + 'config', + 'user-menu', + 'help', + 'themes/dark', + 'themes/light', + 'columns/name-size', + 'columns/name-size-date', + ]; + + return !lazyChunks.includes(chunk.name); + }, + minChunks: 1, + enforce: true, + priority: -1, + reuseExistingChunk: true, + }, + }, }; -module.exports = { +export default { resolve: { symlinks: false, alias: { 'node:process': 'process', 'node:path': 'path', }, + fallback: { + path: import.meta.resolve('path-browserify'), + process: import.meta.resolve('process/browser'), + }, }, devtool, optimization: { splitChunks, }, entry: { - cloudcmd: `${dir}/cloudcmd.js`, + 'themes/dark': `${dirThemes}/dark.css`, + 'themes/light': `${dirThemes}/light.css`, + 'columns/name-size': `${dirColumns}/name-size.css`, + 'columns/name-size-date': `${dirColumns}/name-size-date.css`, + 'nojs': `${dirCss}/nojs.css`, + 'help': `${dirCss}/help.css`, + 'view': `${dirCss}/view.css`, + 'config': `${dirCss}/config.css`, + 'terminal': `${dirCss}/terminal.css`, + 'user-menu': `${dirCss}/user-menu.css`, + 'sw': `${dir}/sw/sw.js`, + 'cloudcmd': `${dir}/cloudcmd.mjs`, [`${modules}/edit`]: `${dirModules}/edit.js`, [`${modules}/edit-file`]: `${dirModules}/edit-file.js`, [`${modules}/edit-file-vim`]: `${dirModules}/edit-file-vim.js`, [`${modules}/edit-names`]: `${dirModules}/edit-names.js`, [`${modules}/edit-names-vim`]: `${dirModules}/edit-names-vim.js`, - [`${modules}/menu`]: `${dirModules}/menu.js`, + [`${modules}/menu`]: `${dirModules}/menu/index.js`, [`${modules}/view`]: `${dirModules}/view/index.js`, [`${modules}/help`]: `${dirModules}/help.js`, [`${modules}/markdown`]: `${dirModules}/markdown.js`, [`${modules}/config`]: `${dirModules}/config/index.js`, [`${modules}/contact`]: `${dirModules}/contact.js`, [`${modules}/upload`]: `${dirModules}/upload.js`, - [`${modules}/operation`]: `${dirModules}/operation/index.js`, + [`${modules}/operation`]: `${dirModules}/operation/index.mjs`, [`${modules}/konsole`]: `${dirModules}/konsole.js`, [`${modules}/terminal`]: `${dirModules}/terminal.js`, [`${modules}/terminal-run`]: `${dirModules}/terminal-run.js`, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 453cbc03..b6b6b4ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,5 +26,5 @@ For example util, console, view, edit, style etc... **Examples**: -- [fix(style) .name{width}: 37% -> 35%](https://github.com/coderaiser/cloudcmd/commit/94b0642e3990c17b3a0ee3efeb75f343e1e7c050) -- [fix(console) dispatch: focus -> mouseup](https://github.com/coderaiser/cloudcmd/commit/f41ec5058d1411e86a881f8e8077e0572e0409ec) +- [fix: style: .name{width}: 37% -> 35%](https://github.com/coderaiser/cloudcmd/commit/94b0642e3990c17b3a0ee3efeb75f343e1e7c050) +- [fix: console: dispatch: focus -> mouseup](https://github.com/coderaiser/cloudcmd/commit/f41ec5058d1411e86a881f8e8077e0572e0409ec) diff --git a/ChangeLog b/ChangeLog index 38e91267..edf5330e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,440 @@ +2026.01.21, v19.1.9 + +feature: +- 75ad4415 cloudcmd: @putout/eslint-flat v4.0.0 +- c5d9bd7c client: key: vim: get rid of mock-require +- f437a52f client: images: migrate to EMS +- 7192a56e client: dom: current-file: migrate to ESM + +2026.01.20, v19.1.8 + +fix: +- 8a769fd5 client: modules: operation: no update after copy + +feature: +- d574a93d client: key: migrate to ESM +- 3b409074 client: modules: operation: migrate to ESM +- 3b6b0b5a client: buffer: migrate to ESM +- 8876f050 cloudcmd: eslint-plugin-putout v30.0.0 + +2026.01.17, v19.1.7 + +feature: +- 23a6a698 client: dom/events -> #dom/events +- 9cebb241 client: dom: events: migrate to ESM +- a94fa0d4 client: cloudcmd: migrate to ESM +- 3bdf47a5 client: migrate to ESM + +2026.01.16, v19.1.6 + +fix: +- a523ef65 tests + +feature: +- 64654e8d common: cloudfunc: migrate to ESM +- add31607 common: cloudfunc: get rid of bas64 +- e36de00c modulas: migrate to ESM + +2026.01.16, v19.1.5 + +feature: +- 450f1461 client: improve testability +- d979e949 server: env: migrate to ESM + +2026.01.15, v19.1.4 + +feature: +- 6e778a35 client: sort: migrate to ESM +- e27ef51d client: sort: migrate to ESM +- 917f5851 client: load-module: migrate to ESM +- 9950caca client: get-json-from-file-table: migrate to ESM + +2026.01.15, v19.1.3 + +feature: +- f903c5c9 cloudcmd: multi-rename v3.0.0 + +2026.01.14, v19.1.2 + +fix: +- 9e2c5ac6 client: edit-names: group rename not renaming (#453) +- f0dcbe94 client: key: config + +feature: +- 6856207d server: env -> env.parse +- dc99417c client: key: get rid of mock-require +- 4bb7d704 client: modules: view: get rid of mock-require + +2026.01.12, v19.1.1 + +feature: +- 5cc6f79d cloudcmd: @cloudcmd/stub v5.0.0 +- 024bc413 cloudcmd: fullstore v4.0.0 +- 53f6f9e7 cloudcmd: globals v17.0.0 +- 6d21c539 cloudcmd: madrun v12.1.0 +- 253389ea cloudcmd: supertape v12.0.0 + +2025.12.31, v19.1.0 + +feature: +- 0ff16314 cloudcmd: redlint v5.0.0 +- 43edba8c cloudcmd: try-to-catch v4.0.0 +- 06f3b782 cloudcmd: try-catch v4.0.4 +- dfcd6557 deno config: add +- ab20a462 server: bun support (oven-sh/bun#25674) + +2025.12.24, v19.0.17 + +feature: +- 0222d177 cloudcmd: gritty v9.0.0 + +2025.12.05, v19.0.16 + +feature: +- 14ec19e8 cloudcmd: find-up v8.0.0 +- e6a00979 cloudcmd: eslint-plugin-putout v29.0.2 +- 5b5352c7 cloudcmd: putout v41.0.0 + +2025.11.28, v19.0.15 + +feature: +- 00676531 cloudcmd: aleman v1.16.5 + +2025.11.27, v19.0.14 + +fix: +- 2a525e9b aleman: copy paste in text editor (#449) + +feature: +- 3ceb9a8c cloudcmd: open v11.0.0 + +2025.09.26, v19.0.13 + +feature: +- 8477f3e4 cloudcmd: aleman v1.16.3 (#446) + +2025.09.25, v19.0.12 + +feature: +- 836e908e cloudcmd: aleman v1.16.2 + +2025.09.24, v19.0.11 + +feature: +- f4386a6f cloudcmd: aleman v1.16.1 + +2025.09.23, v19.0.10 + +feature: +- 2e667ba6 cloudcmd: aleman v1.15.0 + +2025.09.22, v19.0.9 + +feature: +- 60c56164 cloudcmd: aleman v1.14.4 + +2025.09.20, v19.0.8 + +feature: +- efe81320 cloudcmd: aleman v1.14.3 + +2025.09.18, v19.0.7 + +feature: +- 5b972e2e cloudcmd: aleman v1.14.0 + +2025.09.17, v19.0.6 + +feature: +- 39a24028 cloudcmd: aleman v1.13.0 + +2025.09.16, v19.0.5 + +fix: +- 64df81bc cloudcmd: client: listeners: f9: stopPropagation + +feature: +- 38dd5101 cloudcmd: aleman v1.12.4 + +2025.09.15, v19.0.4 + +feature: +- 66db798c cloudcmd: aleman v1.12.3 + +2025.09.15, v19.0.3 + +feature: +- c5aed16f cloudcmd: aleman v1.12.2 + +2025.09.14, v19.0.2 + +feature: +- 511347d3 cloudcmd: aleman v1.11.0 + +2025.09.14, v19.0.1 + +fix: +- fc6304a1 tmpl: config: aleman, supermenu + +feature: +- a05ecdb4 cloudcmd: aleman v1.10.0 + +2025.09.14, v19.0.0 + +feature: +- 50b19dcc cloudcmd: menu: default: supermenu -> aleman +- 5970f10a cloudcmd: drop support of node < 22 + +2025.09.14, v18.8.11 + +feature: +- b0360d8e cloudcmd: aleman v1.9.1 +- 00a20129 cloudcmd: html: importsmap: add + +2025.09.14, v18.8.10 + +feature: +- ddf9e455 cloudcmd: aleman v1.9.0 + +2025.09.14, v18.8.9 + +feature: +- 2e7bdb8a cloudcmd: aleman v1.8.0 + +2025.09.13, v18.8.8 + +feature: +- 03631d95 cloudcmd: aleman v1.7.0 + +2025.09.12, v18.8.7 + +feature: +- 09408af5 cloudcmd: aleman v1.6.1 + +2025.09.12, v18.8.6 + +feature: +- 4fcaf288 cloudcmd: aleman v1.6.0 + +2025.09.10, v18.8.5 + +feature: +- c69ec16e cloudcmd: aleman v1.5.0 + +2025.09.09, v18.8.4 + +feature: +- 08d13c6d cloudcmd: aleman v1.4.9 + +2025.09.04, v18.8.3 + +feature: +- b4792fc3 cloudcmd: aleman v1.4.0 + +2025.09.04, v18.8.2 + +feature: +- a0b3285b cloudcmd: aleman v1.3.0 + +2025.09.04, v18.8.1 + +feature: +- 15b71c14 cloudcmd: aleman v1.2.5 +- d252fe5f robots.txt: add + +2025.09.02, v18.8.0 + +feature: +- 08b5c6b2 client: menu: aleman: add + +2025.08.30, v18.7.4 + +fix: +- a6d18ddb select file: name -> line +- 2077468a client: listeners: click: avoid select on conext menu +- 64e4aba4 client: menu: before show: unsetBind + +2025.07.26, v18.7.3 + +fix: +- 884c83eb client: polyfill (#442) + +2025.07.24, v18.7.2 + +feature: +- 2e775908 cloudcmd: html-looks-like: remove +- bb6a7a28 docker: npm -> bun + +2025.07.06, v18.7.1 + +fix: +- 784bb2eb build: sw + +feature: +- 8f52376d cloudcmd: revert: optimize-css-assets-webpack-plugin -> css-minimizer-webpack-plugin: broken spinner +- 82008749 cloudcmd: optimize-css-assets-webpack-plugin -> css-minimizer-webpack-plugin + +2025.07.05, v18.7.0 + +fix: +- b1e231a5 client: menu: close: ESC + +feature: +- 546d0610 cloudcmd: process v0.11.10 +- 121b114e cloudcmd: path-browserify v1.0.1 +- 8592cedc cloudcmd: mini-css-extract-plugin v2.9.2 +- a53ab67b cloudcmd: webpack-cli v6.0.1 +- de2cedd9 cloudcmd: webpack v5.99.9 +- da545ea4 cloudcmd: style-loader v4.0.0 +- db6e8334 cloudcmd: optimize-css-assets-webpack-plugin v6.0.1 +- 2f0c1a61 cloudcmd: html-webpack-plugin v5.6.3 +- e100dcf6 cloudcmd: extract-text-webpack-plugin v3.0.2 +- 76c40008 cloudcmd: css-loader v7.1.2 +- fb5e5a32 cloudcmd: clean-css-loader v4.2.1 +- 8551cd54 cloudcmd: babel-loader v10.0.0 +- c9380319 webpack 5 +- ddc94adb cloudcmd: eslint-plugin-putout v28.0.0 + +2025.07.04, v18.6.1 + +feature: +- 9eafa189 cloudcmd: http-auth v4.2.1 +- e99d0847 cloudcmd: montag v1.2.1 +- b77e9c91 cloudcmd: pipe-io v4.0.1 +- 4b476a6d cloudcmd: globals v16.3.0 +- 2057065d cloudcmd: @putout/eslint-flat v3.0.1 + +2025.07.02, v18.6.0 + +feature: +- 2eb3dc66 cloudcmd: @iocmd/wait v2.1.0 +- 1679b788 cloudcmd: webpackbar v7.0.0 +- 9a4cf388 cloudcmd: eslint-plugin-putout v27.2.1 +- f4b0f92f cloudcmd: express v5.1.0 +- 4ab4be12 thread-it: get rid (#438) +- 99ad0c21 cloudcmd: rm @putout/babel +- 8ccec23d cloudcmd: help: require -> import +- 2a97ac66 cloudcmd: yargs-parser v22.0.0 +- b26c8bba cloudcmd: thread-it v3.0.0 + +2025.04.10, v18.5.2 + +feature: +- 8450bfa6 cloudcmd: putout v40.0.3 +- 51f51b54 cloudcmd: @putout/plugin-cloudcmd v4.0.0 +- 08ab63d7 cloudcmd: supertape v11.0.4 +- e7cc9b92 cloudcmd: redlint v4.1.1 +- 368c9bb8 cloudcmd: eslint v9.23.0 +- 43fd5ed6 cloudcmd: madrun v11.0.0 +- f774d5b2 cloudcmd: eslint-plugin-putout v26.1.0 +- b0a7fc16 cloudcmd: putout v39.3.0 + +2025.02.03, v18.5.1 + +feature: +- 467f0a79 cloudcmd: webpack-merge v6.0.1 +- 353a1fb6 cloudcmd: putout v38.0.5 +- 8e98b778 cloudcmd: eslint-plugin-putout v24.0.0 + +2025.01.20, v18.5.0 + +fix: +- ad8e55d8 client: themes -> columns (#434) + +feature: +- 2fc503f7 cloudcmd: @putout/babel v3.0.0 + +2024.12.13, v18.4.1 + +feature: +- 100e940e cloudcmd: putout v37.0.1 + +2024.11.22, v18.4.0 + +fix: +- dff02672 cloudcmd: make manifest.json accessible when authentication is enabled (#428) + +2024.11.14, v18.3.0 + +feature: +- 71dc8dd6 cloudcmd: Add support for Progressive Web App (#426) + +2024.11.06, v18.2.1 + +feature: +- a733d814 css: --is-mobile: add +- f22120dc cloudcmd: prevent unselect being fired on panel click when in mobile view (#422) +- 1a0af863 docker: add image source label to dockerfiles (#421) +- 0446a74d docker: add image source label to dockerfiles (#419) + +2024.10.27, v18.2.0 + +feature: +- ac9abbd3 cloudcmd: eslint-plugin-putout v23.1.0 +- 4bc5a783 cloudcmd: add context menu option to toggle file selection (#420) + +2024.08.17, v18.1.0 + +feature: +- ddf4542b cloudcmd: add ability to hide dot files (#307) + +2024.08.16, v18.0.2 + +feature: +- 3d03efbe css: show links in one small screens + +2024.08.16, v18.0.1 + +fix: +- 62ed8411 bin: validateArgs is not a function (#147) + +feature: +- 9ec94dee cloudcmd: chalk v5.3.0 + +2024.08.16, v18.0.0 + +feature: +- 5e93bcca cloudcmd: rimraf v6.0.1 +- 74d1eb7e drop support of node < 20 + +2024.08.16, v17.4.4 + +fix: +- a6aa9bbc revert rimraf v6.0.1 + +feature: +- 282b3d5c cloudcmd: @putout/cli-validate-args v2.0.0 + +2024.07.27, v17.4.3 + +feature: +- 6e8348b8 cloudcmd: rimraf v6.0.1 +- 61ca7f36 cloudcmd: putout v36.0.2 + +2024.07.03, v17.4.2 + +feature: +- ba2d0b36 cloudcmd: just-snake-case v3.2.0 +- 4cc47e30 cloudcmd: just-capitalize v3.2.0 +- d8451e56 cloudcmd: just-pascal-case v3.2.0 +- 6abf327d cloudcmd: package-json v10.0.0 +- 2ae6ad34 docker: Dockerimage update Debian12 (#414) +- 05ef0ae4 cloudcmd: c8 v10.1.2 + +2024.05.06, v17.4.1 + +feature: +- 154b4bd6 cloudcmd: @cloudcmd/move-files v8.0.0 +- c409a2db cloudcmd: copymitter v9.0.0 + +2024.04.17, v17.4.0 + +fix: +- 6fb21020 server: route: path traversal + +feature: +- 37ab7068 publish container image to GHCR (#409) + 2024.04.03, v17.3.3 feature: diff --git a/HELP.md b/HELP.md index 5f2da049..68087875 100644 --- a/HELP.md +++ b/HELP.md @@ -1,11 +1,11 @@ -# Cloud Commander v17.3.3 +# Cloud Commander v19.1.9 ### [Main][MainURL] [Blog][BlogURL] [Support][SupportURL] [Demo][DemoURL] [MainURL]: https://cloudcmd.io "Main" [BlogURL]: https://blog.cloudcmd.io "Blog" [SupportURL]: https://patreon.com/coderaiser "Patreon" -[DemoURL]: https://cloudcmd.onrender.com/ +[DemoURL]: https://cloudcmd-zdp6.onrender.com/ [DWORD]: https://github.com/cloudcmd/dword "Editor based on CodeMirror" [EDWARD]: https://github.com/cloudcmd/edward "Editor based on Ace" [DEEPWORD]: https://github.com/cloudcmd/deepword "Editor based on Monaco" @@ -59,78 +59,80 @@ cloudcmd Cloud Commander supports the following command-line parameters: -|Parameter |Operation -|:------------------------------|:------------------------------ -| `-h, --help` | display help and exit -| `-v, --version` | display version and exit -| `-s, --save` | save configuration -| `-o, --online` | load scripts from remote servers -| `-a, --auth` | enable authorization -| `-u, --username` | set username -| `-p, --password` | set password -| `-c, --config` | configuration file path -| `--show-config` | show config values -| `--show-file-name` | show file name in view and edit -| `--editor` | set editor: "dword", "edward" or "deepword" -| `--packer` | set packer: "tar" or "zip" -| `--root` | set root directory -| `--prefix` | set url prefix -| `--prefix-socket` | set prefix for url connection -| `--port` | set port number -| `--confirm-copy` | confirm copy -| `--confirm-move` | confirm move -| `--open` | open web browser when server starts -| `--name` | set tab name in web browser -| `--one-file-panel` | show one file panel -| `--keys-panel` | show keys panel -| `--contact` | enable contact -| `--config-dialog` | enable config dialog -| `--config-auth` | enable auth change in config dialog -| `--console` | enable console -| `--sync-console-path` | sync console path -| `--terminal` | enable terminal -| `--terminal-path` | set terminal path -| `--terminal-command` | set command to run in terminal (shell by default) -| `--terminal-auto-restart` | restart command on exit -| `--vim` | enable vim hot keys -| `--columns` | set visible columns -| `--theme` | set theme 'light' or 'dark'" -| `--export` | enable export of config through a server -| `--export-token` | authorization token used by export server -| `--import` | enable import of config -| `--import-token` | authorization token used to connect to export server -| `--import-url` | url of an import server -| `--import-listen` | enable listen on config updates from import server -| `--dropbox` | enable dropbox integration -| `--dropbox-token` | set dropbox token -| `--log` | enable logging -| `--no-show-config` | do not show config values -| `--no-server` | do not start server -| `--no-auth` | disable authorization -| `--no-online` | load scripts from local server -| `--no-open` | do not open web browser when server started -| `--no-name` | set default tab name in web browser -| `--no-keys-panel` | hide keys panel -| `--no-one-file-panel` | show two file panels -| `--no-confirm-copy` | do not confirm copy -| `--no-confirm-move` | do not confirm move -| `--no-config-dialog` | disable config dialog -| `--no-config-auth` | disable auth change in config dialog -| `--no-console` | disable console -| `--no-sync-console-path` | do not sync console path -| `--no-contact` | disable contact -| `--no-terminal` | disable terminal -| `--no-terminal-command` | set default shell to run in terminal -| `--no-terminal-auto-restart` | do not restart command on exit -| `--no-vim` | disable vim hot keys +| Parameter |Operation +|:-----------------------------|:------------------------------ +| `-h, --help` | display help and exit +| `-v, --version` | display version and exit +| `-s, --save` | save configuration +| `-o, --online` | load scripts from remote servers +| `-a, --auth` | enable authorization +| `-u, --username` | set username +| `-p, --password` | set password +| `-c, --config` | configuration file path +| `--show-config` | show config values +| `--show-dot-files` | show dot files +| `--show-file-name` | show file name in view and edit +| `--editor` | set editor: "dword", "edward" or "deepword" +| `--packer` | set packer: "tar" or "zip" +| `--root` | set root directory +| `--prefix` | set url prefix +| `--prefix-socket` | set prefix for url connection +| `--port` | set port number +| `--confirm-copy` | confirm copy +| `--confirm-move` | confirm move +| `--open` | open web browser when server starts +| `--name` | set tab name in web browser +| `--menu` | set menu: "supermenu" or "aleman" +| `--one-file-panel` | show one file panel +| `--keys-panel` | show keys panel +| `--contact` | enable contact +| `--config-dialog` | enable config dialog +| `--config-auth` | enable auth change in config dialog +| `--console` | enable console +| `--sync-console-path` | sync console path +| `--terminal` | enable terminal +| `--terminal-path` | set terminal path +| `--terminal-command` | set command to run in terminal (shell by default) +| `--terminal-auto-restart` | restart command on exit +| `--vim` | enable vim hot keys +| `--columns` | set visible columns +| `--theme` | set theme 'light' or 'dark'" +| `--export` | enable export of config through a server +| `--export-token` | authorization token used by export server +| `--import` | enable import of config +| `--import-token` | authorization token used to connect to export server +| `--import-url` | url of an import server +| `--import-listen` | enable listen on config updates from import server +| `--dropbox` | enable dropbox integration +| `--dropbox-token` | set dropbox token +| `--log` | enable logging +| `--no-show-config` | do not show config values +| `--no-server` | do not start server +| `--no-auth` | disable authorization +| `--no-online` | load scripts from local server +| `--no-open` | do not open web browser when server started +| `--no-name` | set default tab name in web browser +| `--no-keys-panel` | hide keys panel +| `--no-one-file-panel` | show two file panels +| `--no-confirm-copy` | do not confirm copy +| `--no-confirm-move` | do not confirm move +| `--no-config-dialog` | disable config dialog +| `--no-config-auth` | disable auth change in config dialog +| `--no-console` | disable console +| `--no-sync-console-path` | do not sync console path +| `--no-contact` | disable contact +| `--no-terminal` | disable terminal +| `--no-terminal-command` | set default shell to run in terminal +| `--no-terminal-auto-restart` | do not restart command on exit +| `--no-vim` | disable vim hot keys | `--no-themes` | set default visible themes -| `--no-export` | disable export config through a server -| `--no-import` | disable import of config -| `--no-import-listen` | disable listen on config updates from import server -| `--no-show-file-name` | do not show file name in view and edit -| `--no-dropbox` | disable dropbox integration -| `--no-dropbox-token` | unset dropbox token -| `--no-log` | disable logging +| `--no-export` | disable export config through a server +| `--no-import` | disable import of config +| `--no-import-listen` | disable listen on config updates from import server +| `--no-show-file-name` | do not show file name in view and edit +| `--no-dropbox` | disable dropbox integration +| `--no-dropbox-token` | unset dropbox token +| `--no-log` | disable logging For options not specified by command-line parameters, Cloud Commander then reads configuration data from `~/.cloudcmd.json`. It uses port `8000` by default. @@ -152,56 +154,58 @@ Then, start the server again with `cloudcmd` and reload the page. ## Hot keys -|Key |Operation -|:----------------------|:-------------------------------------------- -| `F1` | help -| `F2` | show `user menu` -| `F3` | view, change directory -| `Shift + F3` | view raw file, change directory -| `F4` | edit -| `F5` | copy -| `Alt` + `F5` | pack -| `F6` | rename/move -| `Shift` + `F6` | rename current file -| `F7` | new directory -| `Shift + F7` | new file -| `F8`, `Delete` | remove -| `Shift + Delete` | remove without prompt -| `F9` | menu -| `Alt` + `F9` | extract -| `F10` | config -| `*` | select/unselect all -| `+` | expand selection -| `-` | shrink selection -| `:` | open Command Line -| `Ctrl + X` | cut to buffer -| `Ctrl + C` | copy to buffer -| `Ctrl + V` | paste from buffer -| `Ctrl + Z` | clear buffer -| `Ctrl + P` | copy path -| `Ctrl + R` | refresh -| `Ctrl + D` | clear local storage -| `Ctrl + A` | select all files in a panel -| `Ctrl + M` | [rename selected files](https://github.com/coderaiser/cloudcmd/releases/tag/v12.1.0) in editor -| `Ctrl + U` | swap panels -| `Ctrl + F3` | sort by name -| `Ctrl + F5` | sort by date -| `Ctrl + F6` | sort by size -| `Up`, `Down` | file system navigation -| `Enter` | change directory/view file -| `Alt + Left/Right` | show content of directory under cursor in target panel -| `Alt + G` | go to directory -| `Ctrl + \` | go to the root directory -| `Tab` | move via panels -| `Page Up` | up on one page -| `Page Down` | down on one page -| `Home` | to begin of list -| `End` | to end of list -| `Space` | select current file (and get size of directory) -| `Insert` | select current file (and move to next) -| `F9` | context menu -| `~` | console -| `Esc` | toggle vim hotkeys (`file manager`, `editor`) +| Key |Operation +|:---------------------|:-------------------------------------------- +| `F1` | help +| `F2` | show `user menu` +| `F3` | view, change directory +| `Shift + F3` | view raw file, change directory +| `F4` | edit +| `F5` | copy +| `Alt` + `F5` | pack +| `F6` | rename/move +| `Shift` + `F6` | rename current file +| `F7` | new directory +| `Shift + F7` | new file +| `F8`, `Delete` | remove +| `Shift + Delete` | remove without prompt +| `F9` | menu +| `Alt` + `F9` | extract +| `F10` | config +| `*` | select/unselect all +| `+` | expand selection +| `-` | shrink selection +| `:` | open Command Line +| `Ctrl + X` | cut to buffer +| `Ctrl + C` | copy to buffer +| `Ctrl + V` | paste from buffer +| `Ctrl + Z` | clear buffer +| `Ctrl + P` | copy path +| `Ctrl + R` | refresh +| `Ctrl + D` | clear local storage +| `Ctrl + A` | select all files in a panel +| `Ctrl + M` | [rename selected files](https://github.com/coderaiser/cloudcmd/releases/tag/v12.1.0) in editor +| `Ctrl + U` | swap panels +| `Ctrl + F3` | sort by name +| `Ctrl + F5` | sort by date +| `Ctrl + F6` | sort by size +| `Ctrl + Command + .` | show/hide dot files +| `Up` | move cursor up +| `Down` | move cursor down +| `Enter` | change directory/view file +| `Alt + Left/Right` | show content of directory under cursor in target panel +| `Alt + G` | go to directory +| `Ctrl + \` | go to the root directory +| `Tab` | move via panels +| `Page Up` | up on one page +| `Page Down` | down on one page +| `Home` | to begin of list +| `End` | to end of list +| `Space` | select current file (and get size of directory) +| `Insert` | select current file (and move to next) +| `F9` | context menu +| `~` | console +| `Esc` | toggle vim hotkeys (`file manager`, `editor`) ### Vim @@ -398,6 +402,7 @@ Here's a description of all options: "confirmCopy": true, // confirm copy "confirmMove": true, // confirm move "showConfig": false, // show config at startup + "showDotFiles": true, // show dot files "showFileName": false, // do not show file name in view and edit "contact": true, // enable contact "configDialog": true, // enable config dialog @@ -431,6 +436,7 @@ Some config options can be overridden with environment variables, such as: - `CLOUDCMD_EDITOR` - set editor - `CLOUDCMD_COLUMNS` - set visible themes - `CLOUDCMD_THEME` - set themes "light" or "dark" +- `CLOUDCMD_MENU` - set menu "supermenu" or "aleman" - `CLOUDCMD_CONTACT` - enable contact - `CLOUDCMD_CONFIG_DIALOG` - enable config dialog - `CLOUDCMD_CONFIG_AUTH` - enable auth change in config dialog @@ -641,10 +647,20 @@ Right-mouse click to show a context menu with these items: ### Hot keys -|Key |Operation -|:----------------------|:-------------------------------------------- -| `F9` | open -| `Esc` | close +| Key | Operation | +|:-------------|:------------------------| +| `F9` | open | +| `Esc` | close | +| `Up`, `j` | move cursor up | +| `Down`, `k` | move cursor down | +| `Left`, `h` | close submenu | +| `Right`, `l` | open submenu | +| `G` or `$` | navigate to bottom | +| `gg` or `^` | navigate to top | + +Commands can be joined, for example: + +- `5j` will navigate **5** items below current; ## One file panel @@ -1095,6 +1111,70 @@ There are a lot of ways to be involved in `Cloud Commander` development: ## Version history +- *2026.01.21*, **[v19.1.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.9)** +- *2026.01.20*, **[v19.1.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.8)** +- *2026.01.17*, **[v19.1.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.7)** +- *2026.01.16*, **[v19.1.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.6)** +- *2026.01.16*, **[v19.1.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.5)** +- *2026.01.15*, **[v19.1.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.4)** +- *2026.01.15*, **[v19.1.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.3)** +- *2026.01.14*, **[v19.1.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.2)** +- *2026.01.12*, **[v19.1.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.1)** +- *2025.12.31*, **[v19.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.0)** +- *2025.12.24*, **[v19.0.17](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.17)** +- *2025.12.05*, **[v19.0.16](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.16)** +- *2025.11.28*, **[v19.0.15](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.15)** +- *2025.11.27*, **[v19.0.14](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.14)** +- *2025.09.26*, **[v19.0.13](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.13)** +- *2025.09.25*, **[v19.0.12](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.12)** +- *2025.09.24*, **[v19.0.11](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.11)** +- *2025.09.23*, **[v19.0.10](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.10)** +- *2025.09.22*, **[v19.0.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.9)** +- *2025.09.20*, **[v19.0.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.8)** +- *2025.09.18*, **[v19.0.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.7)** +- *2025.09.17*, **[v19.0.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.6)** +- *2025.09.16*, **[v19.0.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.5)** +- *2025.09.15*, **[v19.0.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.4)** +- *2025.09.15*, **[v19.0.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.3)** +- *2025.09.14*, **[v19.0.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.2)** +- *2025.09.14*, **[v19.0.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.1)** +- *2025.09.14*, **[v19.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.0)** +- *2025.09.14*, **[v18.8.11](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.11)** +- *2025.09.14*, **[v18.8.10](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.10)** +- *2025.09.14*, **[v18.8.9](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.9)** +- *2025.09.13*, **[v18.8.8](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.8)** +- *2025.09.12*, **[v18.8.7](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.7)** +- *2025.09.12*, **[v18.8.6](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.6)** +- *2025.09.10*, **[v18.8.5](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.5)** +- *2025.09.09*, **[v18.8.4](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.4)** +- *2025.09.04*, **[v18.8.3](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.3)** +- *2025.09.04*, **[v18.8.2](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.2)** +- *2025.09.04*, **[v18.8.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.1)** +- *2025.09.02*, **[v18.8.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.0)** +- *2025.08.30*, **[v18.7.4](//github.com/coderaiser/cloudcmd/releases/tag/v18.7.4)** +- *2025.07.26*, **[v18.7.3](//github.com/coderaiser/cloudcmd/releases/tag/v18.7.3)** +- *2025.07.24*, **[v18.7.2](//github.com/coderaiser/cloudcmd/releases/tag/v18.7.2)** +- *2025.07.06*, **[v18.7.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.7.1)** +- *2025.07.05*, **[v18.7.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.7.0)** +- *2025.07.04*, **[v18.6.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.6.1)** +- *2025.07.02*, **[v18.6.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.6.0)** +- *2025.04.10*, **[v18.5.2](//github.com/coderaiser/cloudcmd/releases/tag/v18.5.2)** +- *2025.02.03*, **[v18.5.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.5.1)** +- *2025.01.20*, **[v18.5.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.5.0)** +- *2024.12.13*, **[v18.4.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.4.1)** +- *2024.11.22*, **[v18.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.4.0)** +- *2024.11.14*, **[v18.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.3.0)** +- *2024.11.06*, **[v18.2.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.2.1)** +- *2024.10.27*, **[v18.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.2.0)** +- *2024.08.17*, **[v18.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.1.0)** +- *2024.08.16*, **[v18.0.2](//github.com/coderaiser/cloudcmd/releases/tag/v18.0.2)** +- *2024.08.16*, **[v18.0.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.0.1)** +- *2024.08.16*, **[v18.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.0.0)** +- *2024.08.16*, **[v17.4.4](//github.com/coderaiser/cloudcmd/releases/tag/v17.4.4)** +- *2024.07.27*, **[v17.4.3](//github.com/coderaiser/cloudcmd/releases/tag/v17.4.3)** +- *2024.07.03*, **[v17.4.2](//github.com/coderaiser/cloudcmd/releases/tag/v17.4.2)** +- *2024.05.06*, **[v17.4.1](//github.com/coderaiser/cloudcmd/releases/tag/v17.4.1)** +- *2024.04.17*, **[v17.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v17.4.0)** - *2024.04.03*, **[v17.3.3](//github.com/coderaiser/cloudcmd/releases/tag/v17.3.3)** - *2024.03.29*, **[v17.3.2](//github.com/coderaiser/cloudcmd/releases/tag/v17.3.2)** - *2024.03.29*, **[v17.3.1](//github.com/coderaiser/cloudcmd/releases/tag/v17.3.1)** diff --git a/LICENSE b/LICENSE index 58e890a6..a0d7436b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ (The MIT License) -Copyright (c) 2012-2024 Coderaiser +Copyright (c) 2012-2025 Coderaiser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index 0de495f2..8e510161 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Cloud Commander v17.3.3 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] +# Cloud Commander v19.1.9 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] ### [Main][MainURL] [Blog][BlogURL] [Support][SupportURL] [Demo][DemoURL] [MainURL]: https://cloudcmd.io "Main" [BlogURL]: https://blog.cloudcmd.io "Blog" [SupportURL]: https://patreon.com/coderaiser "Patreon" -[DemoURL]: https://cloudcmd.onrender.com/ +[DemoURL]: https://cloudcmd-zdp6.onrender.com [NPM_INFO_IMG]: https://nodei.co/npm/cloudcmd.png [BuildStatusURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml "Build Status" [BuildStatusIMGURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml/badge.svg diff --git a/bin/cloudcmd.mjs b/bin/cloudcmd.mjs index 4ea301ac..e1c358ed 100755 --- a/bin/cloudcmd.mjs +++ b/bin/cloudcmd.mjs @@ -3,12 +3,12 @@ import process from 'node:process'; import {createRequire} from 'node:module'; import {promisify} from 'node:util'; -import tryToCatch from 'try-to-catch'; +import {tryToCatch} from 'try-to-catch'; import {createSimport} from 'simport'; import parse from 'yargs-parser'; import exit from '../server/exit.js'; -import {createConfig, configPath} from '../server/config.js'; -import env from '../server/env.js'; +import {createConfig, configPath} from '../server/config.mjs'; +import * as env from '../server/env.mjs'; import prefixer from '../server/prefixer.js'; import * as validate from '../server/validate.mjs'; @@ -17,11 +17,11 @@ process.on('unhandledRejection', exit); const require = createRequire(import.meta.url); const Info = require('../package.json'); - +const isUndefined = (a) => typeof a === 'undefined'; const simport = createSimport(import.meta.url); const choose = (a, b) => { - if (a === undefined) + if (isUndefined(a)) return b; return a; @@ -62,6 +62,7 @@ const yargsOptions = { 'terminal-path', 'terminal-command', 'columns', + 'menu', 'theme', 'import-url', 'import-token', @@ -86,6 +87,7 @@ const yargsOptions = { 'confirm-copy', 'confirm-move', 'show-config', + 'show-dot-files', 'show-file-name', 'vim', 'keys-panel', @@ -99,44 +101,46 @@ const yargsOptions = { ], default: { 'server': true, - 'name': choose(env('name'), config('name')), + 'name': choose(env.parse('name'), config('name')), 'auth': choose(env.bool('auth'), config('auth')), 'port': config('port'), 'online': config('online'), 'open': choose(env.bool('open'), config('open')), - 'editor': env('editor') || config('editor'), + 'editor': env.parse('editor') || config('editor'), + 'menu': env.parse('menu') || config('menu'), 'packer': config('packer') || 'tar', 'zip': config('zip'), - 'username': env('username') || config('username'), - 'root': choose(env('root'), config('root')), - 'prefix': choose(env('prefix'), config('prefix')), + 'username': env.parse('username') || config('username'), + 'root': choose(env.parse('root'), config('root')), + 'prefix': choose(env.parse('prefix'), config('prefix')), 'console': choose(env.bool('console'), config('console')), 'contact': choose(env.bool('contact'), config('contact')), 'terminal': choose(env.bool('terminal'), config('terminal')), - 'columns': env('columns') || config('columns') || '', - 'theme': env('theme') || config('theme') || '', + 'columns': env.parse('columns') || config('columns') || '', + 'theme': env.parse('theme') || config('theme') || '', 'vim': choose(env.bool('vim'), config('vim')), 'log': config('log'), - 'import-url': env('import_url') || config('importUrl'), + 'import-url': env.parse('import_url') || config('importUrl'), 'import-listen': choose(env.bool('import_listen'), config('importListen')), 'import': choose(env.bool('import'), config('import')), 'export': choose(env.bool('export'), config('export')), 'prefix-socket': config('prefixSocket'), + 'show-dot-files': choose(env.bool('show_dot_files'), config('showDotFiles')), 'show-file-name': choose(env.bool('show_file_name'), config('showFileName')), 'sync-console-path': choose(env.bool('sync_console_path'), config('syncConsolePath')), 'config-dialog': choose(env.bool('config_dialog'), config('configDialog')), 'config-auth': choose(env.bool('config_auth'), config('configAuth')), - 'terminal-path': env('terminal_path') || config('terminalPath'), - 'terminal-command': env('terminal_command') || config('terminalCommand'), + 'terminal-path': env.parse('terminal_path') || config('terminalPath'), + 'terminal-command': env.parse('terminal_command') || config('terminalCommand'), 'terminal-auto-restart': choose(env.bool('terminal_auto_restart'), config('terminalAutoRestart')), 'one-file-panel': choose(env.bool('one_file_panel'), config('oneFilePanel')), 'confirm-copy': choose(env.bool('confirm_copy'), config('confirmCopy')), 'confirm-move': choose(env.bool('confirm_move'), config('confirmMove')), 'keys-panel': env.bool('keys_panel') || config('keysPanel'), - 'import-token': env('import_token') || config('importToken'), - 'export-token': env('export_token') || config('exportToken'), + 'import-token': env.parse('import_token') || config('importToken'), + 'export-token': env.parse('export_token') || config('exportToken'), 'dropbox': config('dropbox'), 'dropbox-token': config('dropboxToken') || '', @@ -164,7 +168,7 @@ else main(); async function main() { - const validateArgs = await simport('@putout/cli-validate-args'); + const {validateArgs} = await simport('@putout/cli-validate-args'); const error = await validateArgs(args, [ ...yargsOptions.boolean, @@ -189,6 +193,7 @@ async function main() { config('username', args.username); config('console', args.console); config('syncConsolePath', args.syncConsolePath); + config('showDotFiles', args.showDotFiles); config('showFileName', args.showFileName); config('contact', args.contact); config('terminal', args.terminal); @@ -196,6 +201,7 @@ async function main() { config('terminalCommand', args.terminalCommand); config('terminalAutoRestart', args.terminalAutoRestart); config('editor', args.editor); + config('menu', args.menu); config('prefix', prefixer(args.prefix)); config('prefixSocket', prefixer(args.prefixSocket)); config('root', args.root || '/'); @@ -229,9 +235,10 @@ async function main() { prefixSocket: config('prefixSocket'), columns: config('columns'), theme: config('theme'), + menu: config('menu'), }; - const password = env('password') || args.password; + const password = env.parse('password') || args.password; if (password) config('password', await getPassword(password)); @@ -313,7 +320,12 @@ async function readConfig(name) { } async function help() { - const bin = require('../json/help.json'); + const {default: bin} = await import('../json/help.json', { + with: { + type: 'json', + }, + }); + const forEachKey = await simport('for-each-key'); const currify = await simport('currify'); diff --git a/bin/release.mjs b/bin/release.mjs index e9ac53d9..004b908e 100755 --- a/bin/release.mjs +++ b/bin/release.mjs @@ -1,13 +1,13 @@ #!/usr/bin/env node import {promisify} from 'node:util'; -import tryToCatch from 'try-to-catch'; +import process from 'node:process'; +import {tryToCatch} from 'try-to-catch'; import {createSimport} from 'simport'; import minor from 'minor'; import _place from 'place'; import rendy from 'rendy'; import shortdate from 'shortdate'; -import process from 'node:process'; const simport = createSimport(import.meta.url); const place = promisify(_place); @@ -19,7 +19,8 @@ await main(); async function main() { const history = '## Version history\n\n'; const link = '//github.com/coderaiser/cloudcmd/releases/tag/'; - const template = '- *{{ date }}*, ' + + const template = '- ' + + '*{{ date }}*, ' + '**[v{{ version }}]' + '(' + link + 'v{{ version }})**\n'; diff --git a/client/client.js b/client/client.mjs similarity index 87% rename from client/client.js rename to client/client.mjs index 58f4ae83..b6c3a436 100644 --- a/client/client.js +++ b/client/client.mjs @@ -1,30 +1,24 @@ -'use strict'; - -const process = require('node:process'); +import process from 'node:process'; /* global DOM */ -const Emitify = require('emitify'); -const inherits = require('inherits'); -const rendy = require('rendy'); -const load = require('load.js'); -const tryToCatch = require('try-to-catch'); -const {addSlashToEnd} = require('format-io'); -const pascalCase = require('just-pascal-case'); -const currify = require('currify'); - -const Images = require('./dom/images'); - -const {unregisterSW} = require('./sw/register'); -const getJsonFromFileTable = require('./get-json-from-file-table'); -const Key = require('./key'); - -const { +import Emitify from 'emitify'; +import inherits from 'inherits'; +import rendy from 'rendy'; +import load from 'load.js'; +import {tryToCatch} from 'try-to-catch'; +import {addSlashToEnd} from 'format-io'; +import pascalCase from 'just-pascal-case'; +import currify from 'currify'; +import * as Images from './dom/images.mjs'; +import {unregisterSW} from './sw/register.js'; +import {getJsonFromFileTable} from './get-json-from-file-table.mjs'; +import {Key} from './key/index.mjs'; +import { apiURL, formatMsg, buildFromJSON, -} = require('../common/cloudfunc'); - -const loadModule = require('./load-module'); +} from '../common/cloudfunc.mjs'; +import {loadModule} from './load-module.mjs'; const noJS = (a) => a.replace(/.js$/, ''); @@ -32,16 +26,19 @@ const isDev = process.env.NODE_ENV === 'development'; inherits(CloudCmdProto, Emitify); -module.exports = new CloudCmdProto(DOM); +export const createCloudCmd = ({DOM, Listeners}) => { + return new CloudCmdProto({ + DOM, + Listeners, + }); +}; load.addErrorListener((e, src) => { const msg = `file ${src} could not be loaded`; Images.show.error(msg); }); -function CloudCmdProto(DOM) { - let Listeners; - +function CloudCmdProto({DOM, Listeners}) { Emitify.call(this); const CloudCmd = this; @@ -49,21 +46,16 @@ function CloudCmdProto(DOM) { const {Storage, Files} = DOM; - this.log = (...a) => { + this.log = () => { if (!isDev) return; - - console.log(...a); }; this.prefix = ''; this.prefixSocket = ''; this.prefixURL = ''; - this.MIN_ONE_PANEL_WIDTH = 1155; + this.MIN_ONE_PANEL_WIDTH = DOM.getCSSVar('min-one-panel-width'); this.HOST = location.origin || location.protocol + '//' + location.host; - - this.TITLE = 'Cloud Commander'; - this.sort = { left: 'name', right: 'name', @@ -74,13 +66,15 @@ function CloudCmdProto(DOM) { right: 'asc', }; - this.changeDir = async (path, { - isRefresh, - panel, - history = true, - noCurrent, - currentName, - } = {}) => { + this.changeDir = async (path, overrides = {}) => { + const { + isRefresh, + panel, + history = true, + noCurrent, + currentName, + } = overrides; + const refresh = isRefresh; let panelChanged; @@ -95,12 +89,14 @@ function CloudCmdProto(DOM) { imgPosition = 'top'; Images.show.load(imgPosition, panel); + /* загружаем содержимое каталога */ await ajaxLoad(addSlashToEnd(path), { refresh, history, noCurrent, currentName, + showDotFiles: CloudCmd.config('showDotFiles'), }, panel); }; @@ -137,18 +133,10 @@ function CloudCmdProto(DOM) { await initModules(); await baseInit(); - await loadStyle(); CloudCmd.route(location.hash); }; - async function loadStyle() { - const {prefix} = CloudCmd; - const name = `${prefix}/dist/cloudcmd.common.css`; - - await load.css(name); - } - this.route = (path) => { const query = path.split('/'); @@ -158,7 +146,7 @@ function CloudCmdProto(DOM) { const [kebabModule] = query; const module = noJS(pascalCase(kebabModule.slice(1))); - const file = query[1]; + const [, file] = query; const current = DOM.getCurrentByName(file); if (file && !current) { @@ -237,7 +225,6 @@ function CloudCmdProto(DOM) { const dirPath = DOM.getCurrentDirPath(); - Listeners = CloudCmd.Listeners; Listeners.init(); const panels = getPanels(); @@ -298,7 +285,6 @@ function CloudCmdProto(DOM) { * @param options * { refresh, history } - необходимость обновить данные о каталоге * @param panel - * @param callback * */ async function ajaxLoad(path, options = {}, panel) { @@ -346,13 +332,16 @@ function CloudCmdProto(DOM) { /** * Функция строит файловую таблицу - * @param json - данные о файлах + * @param data - данные о файлах * @param panelParam - * @param history - * @param callback + * @param options - history, noCurrent, showDotFiles */ async function createFileTable(data, panelParam, options) { - const {history, noCurrent} = options; + const { + history, + noCurrent, + showDotFiles, + } = options; const names = [ 'file', @@ -383,6 +372,7 @@ function CloudCmdProto(DOM) { data, id: panel.id, prefix, + showDotFiles, template: { file, path, diff --git a/client/cloudcmd.js b/client/cloudcmd.js deleted file mode 100644 index 937f6d77..00000000 --- a/client/cloudcmd.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -const process = require('node:process'); -require('./css'); - -const wraptile = require('wraptile'); -const load = require('load.js'); - -const {registerSW, listenSW} = require('./sw/register'); - -const isDev = process.env.NODE_ENV === 'development'; - -module.exports = window.CloudCmd = async (config) => { - window.Util = require('../common/util'); - window.CloudFunc = require('../common/cloudfunc'); - - window.DOM = require('./dom'); - window.CloudCmd = require('./client'); - - await register(config); - - require('./listeners'); - require('./key'); - require('./sort'); - - const prefix = getPrefix(config.prefix); - - window.CloudCmd.init(prefix, config); -}; - -function getPrefix(prefix) { - if (!prefix) - return ''; - - if (!prefix.indexOf('/')) - return prefix; - - return `/${prefix}`; -} - -const onUpdateFound = wraptile(async (config) => { - if (isDev) - return; - - const {DOM} = window; - const prefix = getPrefix(config.prefix); - - await load.js(`${prefix}/dist/cloudcmd.common.js`); - await load.js(`${prefix}/dist/cloudcmd.js`); - - console.log('cloudcmd: sw: updated'); - - DOM.Events.removeAll(); - window.CloudCmd(config); -}); - -async function register(config) { - const {prefix} = config; - const sw = await registerSW(prefix); - - listenSW(sw, 'updatefound', onUpdateFound(config)); -} diff --git a/client/cloudcmd.mjs b/client/cloudcmd.mjs new file mode 100644 index 00000000..0dac1a03 --- /dev/null +++ b/client/cloudcmd.mjs @@ -0,0 +1,74 @@ +import process from 'node:process'; +import wraptile from 'wraptile'; +import load from 'load.js'; +import '../css/main.css'; +import {registerSW, listenSW} from './sw/register.js'; +import {initSortPanel, sortPanel} from './sort.mjs'; +import Util from '../common/util.js'; +import * as CloudFunc from '../common/cloudfunc.mjs'; +import DOM from './dom/index.js'; +import {createCloudCmd} from './client.mjs'; +import * as Listeners from './listeners/index.js'; + +const isDev = process.env.NODE_ENV === 'development'; + +export default init; + +globalThis.CloudCmd = init; + +async function init(config) { + globalThis.CloudCmd = createCloudCmd({ + DOM, + Listeners, + }); + globalThis.DOM = DOM; + globalThis.Util = Util; + globalThis.CloudFunc = CloudFunc; + + await register(config); + + initSortPanel(); + globalThis.CloudCmd.sortPanel = sortPanel; + const prefix = getPrefix(config.prefix); + + globalThis.CloudCmd.init(prefix, config); + + if (globalThis.CloudCmd.config('menu') === 'aleman') + setTimeout(() => { + import('https://esm.sh/@putout/processor-html'); + import('https://esm.sh/@putout/bundle'); + }, 100); +} + +function getPrefix(prefix) { + if (!prefix) + return ''; + + if (!prefix.indexOf('/')) + return prefix; + + return `/${prefix}`; +} + +const onUpdateFound = wraptile(async (config) => { + if (isDev) + return; + + const {DOM} = globalThis; + const prefix = getPrefix(config.prefix); + + await load.js(`${prefix}/dist/cloudcmd.common.js`); + await load.js(`${prefix}/dist/cloudcmd.js`); + + console.log('cloudcmd: sw: updated'); + + DOM.Events.removeAll(); + globalThis.CloudCmd(config); +}); + +async function register(config) { + const {prefix} = config; + const sw = await registerSW(prefix); + + listenSW(sw, 'updatefound', onUpdateFound(config)); +} diff --git a/client/css.js b/client/css.js deleted file mode 100644 index e404bf94..00000000 --- a/client/css.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -require('../css/main.css'); -require('../css/nojs.css'); -require('../css/columns/name-size-date.css'); -require('../css/columns/name-size.css'); -require('../css/themes/light.css'); -require('../css/themes/dark.css'); diff --git a/client/dom/buffer.js b/client/dom/buffer.js deleted file mode 100644 index afa78dd7..00000000 --- a/client/dom/buffer.js +++ /dev/null @@ -1,135 +0,0 @@ -'use strict'; - -/* global CloudCmd */ -const tryToPromiseAll = require('../../common/try-to-promise-all'); -const Storage = require('./storage'); -const DOM = require('./'); - -module.exports = new BufferProto(); - -function BufferProto() { - const Info = DOM.CurrentInfo; - const CLASS = 'cut-file'; - const COPY = 'copy'; - const CUT = 'cut'; - - const Buffer = { - cut: callIfEnabled.bind(null, cut), - copy: callIfEnabled.bind(null, copy), - clear: callIfEnabled.bind(null, clear), - paste: callIfEnabled.bind(null, paste), - }; - - function showMessage(msg) { - DOM.Dialog.alert(msg); - } - - function getNames() { - const files = DOM.getActiveFiles(); - - return DOM.getFilenames(files); - } - - function addCutClass() { - const files = DOM.getActiveFiles(); - - for (const element of files) { - element.classList.add(CLASS); - } - } - - function rmCutClass() { - const files = DOM.getByClassAll(CLASS); - - for (const element of files) { - element.classList.remove(CLASS); - } - } - - function callIfEnabled(callback) { - const is = CloudCmd.config('buffer'); - - if (is) - return callback(); - - showMessage('Buffer disabled in config!'); - } - - async function readBuffer() { - const [e, cp, ct] = await tryToPromiseAll([ - Storage.getJson(COPY), - Storage.getJson(CUT), - ]); - - return [ - e, - cp, - ct, - ]; - } - - async function copy() { - const names = getNames(); - const from = Info.dirPath; - - await clear(); - - if (!names.length) - return; - - await Storage.remove(CUT); - await Storage.setJson(COPY, { - from, - names, - }); - } - - async function cut() { - const names = getNames(); - const from = Info.dirPath; - - await clear(); - - if (!names.length) - return; - - addCutClass(); - - await Storage.setJson(CUT, { - from, - names, - }); - } - - async function clear() { - await Storage.remove(COPY); - await Storage.remove(CUT); - - rmCutClass(); - } - - async function paste() { - const [error, cp, ct] = await readBuffer(); - - if (error || !cp && !ct) - return showMessage(error || 'Buffer is empty!'); - - const opStr = cp ? 'copy' : 'move'; - const data = cp || ct; - const {Operation} = CloudCmd; - const msg = 'Path is same!'; - const to = Info.dirPath; - - if (data.from === to) - return showMessage(msg); - - Operation.show(opStr, { - ...data, - to, - }); - - await clear(); - } - - return Buffer; -} diff --git a/client/dom/buffer.mjs b/client/dom/buffer.mjs new file mode 100644 index 00000000..01e8142a --- /dev/null +++ b/client/dom/buffer.mjs @@ -0,0 +1,124 @@ +/* global CloudCmd*/ +import tryToPromiseAll from '../../common/try-to-promise-all.js'; +import Storage from './storage.js'; + +const CLASS = 'cut-file'; +const COPY = 'copy'; +const CUT = 'cut'; + +function showMessage(msg) { + globalThis.DOM.Dialog.alert(msg); +} + +function getNames() { + const {DOM} = globalThis; + const files = DOM.getActiveFiles(); + + return DOM.getFilenames(files); +} + +function addCutClass() { + const {DOM} = globalThis; + const files = DOM.getActiveFiles(); + + for (const element of files) { + element.classList.add(CLASS); + } +} + +function rmCutClass() { + const {DOM} = globalThis; + const files = DOM.getByClassAll(CLASS); + + for (const element of files) { + element.classList.remove(CLASS); + } +} + +const checkEnabled = (fn) => () => { + const is = CloudCmd.config('buffer'); + + if (is) + return fn(); + + showMessage('Buffer disabled in config!'); +}; + +async function readBuffer() { + const [e, cp, ct] = await tryToPromiseAll([ + Storage.getJson(COPY), + Storage.getJson(CUT), + ]); + + return [ + e, + cp, + ct, + ]; +} + +export const copy = checkEnabled(async () => { + const Info = globalThis.DOM.CurrentInfo; + const names = getNames(); + const from = Info.dirPath; + + await clear(); + + if (!names.length) + return; + + await Storage.remove(CUT); + await Storage.setJson(COPY, { + from, + names, + }); +}); + +export const cut = checkEnabled(async () => { + const Info = globalThis.DOM.CurrentInfo; + const names = getNames(); + const from = Info.dirPath; + + await clear(); + + if (!names.length) + return; + + addCutClass(); + + await Storage.setJson(CUT, { + from, + names, + }); +}); + +export const clear = checkEnabled(async () => { + await Storage.remove(COPY); + await Storage.remove(CUT); + + rmCutClass(); +}); + +export const paste = checkEnabled(async () => { + const Info = globalThis.DOM.CurrentInfo; + const [error, cp, ct] = await readBuffer(); + + if (error || !cp && !ct) + return showMessage(error || 'Buffer is empty!'); + + const opStr = cp ? 'copy' : 'move'; + const data = cp || ct; + const {Operation} = CloudCmd; + const msg = 'Path is same!'; + const to = Info.dirPath; + + if (data.from === to) + return showMessage(msg); + + Operation.show(opStr, { + ...data, + to, + }); + + await clear(); +}); diff --git a/client/dom/current-file.js b/client/dom/current-file.mjs similarity index 82% rename from client/dom/current-file.js rename to client/dom/current-file.mjs index ca7be146..e55acbe9 100644 --- a/client/dom/current-file.js +++ b/client/dom/current-file.mjs @@ -1,13 +1,8 @@ -'use strict'; - /* global DOM */ /* global CloudCmd */ -const {atob, btoa} = require('../../common/base64'); -const createElement = require('@cloudcmd/create-element'); - -const {encode, decode} = require('../../common/entity'); - -const {getTitle, FS} = require('../../common/cloudfunc'); +import createElement from '@cloudcmd/create-element'; +import {encode, decode} from '../../common/entity.js'; +import {getTitle, FS} from '../../common/cloudfunc.mjs'; let Title; @@ -15,14 +10,15 @@ const CURRENT_FILE = 'current-file'; const encodeNBSP = (a) => a?.replace('\xa0', ' '); const decodeNBSP = (a) => a?.replace(' ', '\xa0'); -module.exports._CURRENT_FILE = CURRENT_FILE; +export const _CURRENT_FILE = CURRENT_FILE; + /** * set name from current (or param) file * * @param name * @param current */ -module.exports.setCurrentName = (name, current) => { +export const setCurrentName = (name, current) => { const Info = DOM.CurrentInfo; const {link} = Info; const {prefix} = CloudCmd; @@ -44,7 +40,7 @@ module.exports.setCurrentName = (name, current) => { * * @param currentFile */ -module.exports.getCurrentName = (currentFile) => { +export const getCurrentName = (currentFile) => { const current = currentFile || DOM.getCurrentFile(); if (!current) @@ -71,18 +67,19 @@ const parseNameAttribute = (attribute) => { return decodeNBSP(decodeURI(atob(attribute))); }; -module.exports._parseNameAttribute = parseNameAttribute; +export const _parseNameAttribute = parseNameAttribute; const parseHrefAttribute = (prefix, attribute) => { attribute = attribute.replace(RegExp('^' + prefix + FS), ''); return decode(decodeNBSP(attribute)); }; -module.exports._parseHrefAttribute = parseHrefAttribute; +export const _parseHrefAttribute = parseHrefAttribute; + /** * get current direcotory path */ -module.exports.getCurrentDirPath = (panel = DOM.getPanel()) => { +export const getCurrentDirPath = (panel = DOM.getPanel()) => { const path = DOM.getByDataName('js-path', panel); return path.textContent; }; @@ -92,7 +89,7 @@ module.exports.getCurrentDirPath = (panel = DOM.getPanel()) => { * * @param currentFile - current file by default */ -module.exports.getCurrentPath = (currentFile) => { +export const getCurrentPath = (currentFile) => { const current = currentFile || DOM.getCurrentFile(); const [element] = DOM.getByTag('a', current); const {prefix} = CloudCmd; @@ -103,7 +100,7 @@ module.exports.getCurrentPath = (currentFile) => { /** * get current direcotory name */ -module.exports.getCurrentDirName = () => { +export const getCurrentDirName = () => { const href = DOM .getCurrentDirPath() .replace(/\/$/, ''); @@ -116,7 +113,7 @@ module.exports.getCurrentDirName = () => { /** * get current direcotory path */ -module.exports.getParentDirPath = (panel) => { +export const getParentDirPath = (panel) => { const path = DOM.getCurrentDirPath(panel); const dirName = DOM.getCurrentDirName() + '/'; const index = path.lastIndexOf(dirName); @@ -130,7 +127,7 @@ module.exports.getParentDirPath = (panel) => { /** * get not current direcotory path */ -module.exports.getNotCurrentDirPath = () => { +export const getNotCurrentDirPath = () => { const panel = DOM.getPanel({ active: false, }); @@ -143,14 +140,14 @@ module.exports.getNotCurrentDirPath = () => { * * @currentFile */ -module.exports.getCurrentFile = () => { +export const getCurrentFile = () => { return DOM.getByClass(CURRENT_FILE); }; /** * get current file by name */ -module.exports.getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => { +export const getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => { const dataName = 'js-file-' + btoa(encodeURI(encodeNBSP(name))); return DOM.getByDataName(dataName, panel); }; @@ -172,7 +169,7 @@ function unsetCurrentFile(currentFile) { /** * unified way to set current file */ -module.exports.setCurrentFile = (currentFile, options) => { +export const setCurrentFile = (currentFile, options) => { const o = options; const currentFileWas = DOM.getCurrentFile(); @@ -219,7 +216,7 @@ module.exports.setCurrentFile = (currentFile, options) => { return DOM; }; -this.setCurrentByName = (name) => { +export const setCurrentByName = (name) => { const current = DOM.getCurrentByName(name); return DOM.setCurrentFile(current); }; @@ -230,7 +227,7 @@ this.setCurrentByName = (name) => { * @param layer - element * @param - position {x, y} */ -module.exports.getCurrentByPosition = ({x, y}) => { +export const getCurrentByPosition = ({x, y}) => { const element = document.elementFromPoint(x, y); const getEl = (el) => { @@ -262,7 +259,7 @@ module.exports.getCurrentByPosition = ({x, y}) => { * * @param currentFile */ -module.exports.isCurrentFile = (currentFile) => { +export const isCurrentFile = (currentFile) => { if (!currentFile) return false; @@ -274,7 +271,7 @@ module.exports.isCurrentFile = (currentFile) => { * * @param name */ -module.exports.setTitle = (name) => { +export const setTitle = (name) => { if (!Title) Title = DOM.getByTag('title')[0] || createElement('title', { innerHTML: name, @@ -291,22 +288,21 @@ module.exports.setTitle = (name) => { * * @param currentFile */ -module.exports.isCurrentIsDir = (currentFile) => { +export const isCurrentIsDir = (currentFile) => { const current = currentFile || DOM.getCurrentFile(); const path = DOM.getCurrentPath(current); const fileType = DOM.getCurrentType(current); - const isZip = /\.zip$/.test(path); + const isZip = path.endsWith('.zip'); const isDir = /^directory(-link)?/.test(fileType); return isDir || isZip; }; -module.exports.getCurrentType = (currentFile) => { +export const getCurrentType = (currentFile) => { const current = currentFile || DOM.getCurrentFile(); const el = DOM.getByDataName('js-type', current); - const type = el - .className + const type = el.className .split(' ') .pop(); diff --git a/client/dom/current-file.spec.js b/client/dom/current-file.spec.mjs similarity index 63% rename from client/dom/current-file.spec.js rename to client/dom/current-file.spec.mjs index 3c907fcd..2a576dce 100644 --- a/client/dom/current-file.spec.js +++ b/client/dom/current-file.spec.mjs @@ -1,20 +1,18 @@ -'use strict'; +import {test, stub} from 'supertape'; +import {create} from 'auto-globals'; +import wraptile from 'wraptile'; +import * as currentFile from './current-file.mjs'; -const {test, stub} = require('supertape'); - -const {create} = require('auto-globals'); -const wraptile = require('wraptile'); -const currentFile = require('./current-file'); const id = (a) => a; const returns = wraptile(id); const {_CURRENT_FILE} = currentFile; test('current-file: setCurrentName: setAttribute', (t) => { - const {DOM, CloudCmd} = global; + const {DOM, CloudCmd} = globalThis; - global.DOM = getDOM(); - global.CloudCmd = getCloudCmd(); + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd(); const current = create(); const {setAttribute} = current; @@ -23,17 +21,17 @@ test('current-file: setCurrentName: setAttribute', (t) => { t.calledWith(setAttribute, ['data-name', 'js-file-aGVsbG8='], 'should call setAttribute'); - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; t.end(); }); test('current-file: setCurrentName: setAttribute: cyrillic', (t) => { - const {DOM, CloudCmd} = global; + const {DOM, CloudCmd} = globalThis; - global.DOM = getDOM(); - global.CloudCmd = getCloudCmd(); + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd(); const current = create(); const {setAttribute} = current; @@ -42,8 +40,8 @@ test('current-file: setCurrentName: setAttribute: cyrillic', (t) => { t.calledWith(setAttribute, ['data-name', 'js-file-JUQwJUIwJUQwJUI5'], 'should call setAttribute'); - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; t.end(); }); @@ -59,12 +57,12 @@ test('current-file: getCurrentName', (t) => { }); test('current-file: emit', (t) => { - const {DOM, CloudCmd} = global; + const {DOM, CloudCmd} = globalThis; const emit = stub(); - global.DOM = getDOM(); - global.CloudCmd = getCloudCmd({ + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd({ emit, }); @@ -74,22 +72,22 @@ test('current-file: emit', (t) => { t.calledWith(emit, ['current-file', current], 'should call emit'); - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; t.end(); }); test('current-file: setCurrentName: return', (t) => { - const {DOM, CloudCmd} = global; + const {DOM, CloudCmd} = globalThis; const link = {}; - global.DOM = getDOM({ + globalThis.DOM = getDOM({ link, }); - global.CloudCmd = getCloudCmd(); + globalThis.CloudCmd = getCloudCmd(); const current = create(); @@ -97,19 +95,19 @@ test('current-file: setCurrentName: return', (t) => { t.equal(result, link, 'should return link'); - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; t.end(); }); test('current-file: getParentDirPath: result', (t) => { - const {DOM} = global; + const {DOM} = globalThis; const getCurrentDirPath = returns('/D/Films/+++favorite films/'); const getCurrentDirName = returns('+++favorite films'); - global.DOM = getDOM({ + globalThis.DOM = getDOM({ getCurrentDirPath, getCurrentDirName, }); @@ -117,55 +115,55 @@ test('current-file: getParentDirPath: result', (t) => { const result = currentFile.getParentDirPath(); const expected = '/D/Films/'; - global.DOM = DOM; + globalThis.DOM = DOM; t.equal(result, expected, 'should return parent dir path'); t.end(); }); test('current-file: isCurrentFile: no', (t) => { - const {DOM, CloudCmd} = global; + const {DOM, CloudCmd} = globalThis; - global.DOM = getDOM(); - global.CloudCmd = getCloudCmd(); + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd(); const result = currentFile.isCurrentFile(); - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; t.notOk(result); t.end(); }); test('current-file: isCurrentFile', (t) => { - const {DOM, CloudCmd} = global; + const {DOM, CloudCmd} = globalThis; const isContainClass = stub(); - global.DOM = getDOM({ + globalThis.DOM = getDOM({ isContainClass, }); - global.CloudCmd = getCloudCmd(); + globalThis.CloudCmd = getCloudCmd(); const current = {}; currentFile.isCurrentFile(current); - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; t.calledWith(isContainClass, [current, _CURRENT_FILE], 'should call isContainClass'); t.end(); }); test('current-file: getCurrentType', (t) => { - const {DOM, CloudCmd} = global; + const {DOM, CloudCmd} = globalThis; - global.DOM = getDOM(); - global.CloudCmd = getCloudCmd(); + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd(); - const {getByDataName} = global.DOM; + const {getByDataName} = globalThis.DOM; getByDataName.returns({ className: 'mini-icon directory', @@ -175,98 +173,96 @@ test('current-file: getCurrentType', (t) => { currentFile.getCurrentType(current); - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; t.calledWith(getByDataName, ['js-type', current]); t.end(); }); test('current-file: isCurrentIsDir: getCurrentType', (t) => { - const {DOM, CloudCmd} = global; + const {DOM, CloudCmd} = globalThis; - global.DOM = getDOM(); - global.CloudCmd = getCloudCmd(); + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd(); - const {getCurrentType} = global.DOM; + const {getCurrentType} = globalThis.DOM; const current = create(); currentFile.isCurrentIsDir(current); - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; t.calledWith(getCurrentType, [current]); t.end(); }); test('current-file: isCurrentIsDir: directory', (t) => { - const {DOM, CloudCmd} = global; + const {DOM, CloudCmd} = globalThis; - global.DOM = getDOM({ + globalThis.DOM = getDOM({ getCurrentType: stub().returns('directory'), }); - global.CloudCmd = getCloudCmd(); + globalThis.CloudCmd = getCloudCmd(); const current = create(); const result = currentFile.isCurrentIsDir(current); - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; t.ok(result); t.end(); }); test('current-file: isCurrentIsDir: directory-link', (t) => { - const {DOM, CloudCmd} = global; + const {DOM, CloudCmd} = globalThis; - global.DOM = getDOM({ + globalThis.DOM = getDOM({ getCurrentType: stub().returns('directory-link'), }); - global.CloudCmd = getCloudCmd(); + globalThis.CloudCmd = getCloudCmd(); const current = create(); const result = currentFile.isCurrentIsDir(current); - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; t.ok(result); t.end(); }); test('current-file: isCurrentIsDir: file', (t) => { - const {DOM, CloudCmd} = global; + const {DOM, CloudCmd} = globalThis; - global.DOM = getDOM({ + globalThis.DOM = getDOM({ getCurrentType: stub().returns('file'), }); - global.CloudCmd = getCloudCmd(); + globalThis.CloudCmd = getCloudCmd(); const current = create(); const result = currentFile.isCurrentIsDir(current); - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; t.notOk(result); t.end(); }); -function getCloudCmd({emit} = {}) { - return { - prefix: '', - emit: emit || stub(), - }; -} +const getCloudCmd = ({emit} = {}) => ({ + prefix: '', + emit: emit || stub(), +}); test('current-file: parseNameAttribute', (t) => { const result = currentFile._parseNameAttribute('js-file-aGVsbG8mbmJzcDt3b3JsZA=='); @@ -285,15 +281,17 @@ test('current-file: parseHrefAttribute', (t) => { t.end(); }); -function getDOM({ - link = {}, - getCurrentDirPath = stub(), - getCurrentDirName = stub(), - getByDataName = stub(), - isContainClass = stub(), - getCurrentType = stub(), - getCurrentPath = stub(), -} = {}) { +function getDOM(overrides = {}) { + const { + link = {}, + getCurrentDirPath = stub(), + getCurrentDirName = stub(), + getByDataName = stub(), + isContainClass = stub(), + getCurrentType = stub(), + getCurrentPath = stub().returns(''), + } = overrides; + return { getCurrentDirPath, getCurrentDirName, diff --git a/client/dom/dialog.js b/client/dom/dialog.js index f4751456..eb342221 100644 --- a/client/dom/dialog.js +++ b/client/dom/dialog.js @@ -1,6 +1,6 @@ 'use strict'; -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); const { alert, diff --git a/client/dom/directory.js b/client/dom/directory.js index 88802d40..bedb7e95 100644 --- a/client/dom/directory.js +++ b/client/dom/directory.js @@ -3,8 +3,8 @@ /* global CloudCmd */ const philip = require('philip'); -const Images = require('./images'); -const {FS} = require('../../common/cloudfunc'); +const Images = require('./images.mjs'); +const {FS} = require('../../common/cloudfunc.mjs'); const DOM = require('.'); const Dialog = require('./dialog'); @@ -63,14 +63,8 @@ module.exports = (items) => { uploader.on('end', CloudCmd.refresh); }; -function percent(i, n, per = 100) { - return Math.round(i * per / n); -} +const percent = (i, n, per = 100) => Math.round(i * per / n); -function uploadFile(url, data) { - return DOM.load.put(url, data); -} +const uploadFile = (url, data) => DOM.load.put(url, data); -function uploadDir(url) { - return DOM.load.put(`${url}?dir`); -} +const uploadDir = (url) => DOM.load.put(`${url}?dir`); diff --git a/client/dom/dom-tree.spec.js b/client/dom/dom-tree.spec.js index 044e97b0..606df179 100644 --- a/client/dom/dom-tree.spec.js +++ b/client/dom/dom-tree.spec.js @@ -2,7 +2,7 @@ const test = require('supertape'); const {create} = require('auto-globals'); -const tryCatch = require('try-catch'); +const {tryCatch} = require('try-catch'); const {isContainClass} = require('./dom-tree'); diff --git a/client/dom/events/index.js b/client/dom/events/index.js deleted file mode 100644 index a879fdc1..00000000 --- a/client/dom/events/index.js +++ /dev/null @@ -1,257 +0,0 @@ -'use strict'; - -const itype = require('itype'); -const EventStore = require('./event-store'); - -module.exports = new EventsProto(); - -function EventsProto() { - const Events = this; - - const getEventOptions = (eventName) => { - if (eventName !== 'touchstart') - return false; - - return { - passive: true, - }; - }; - - function parseArgs(eventName, element, listener, callback) { - let isFunc; - const args = [ - eventName, - element, - listener, - callback, - ]; - - const EVENT_NAME = 1; - const ELEMENT = 0; - const type = itype(eventName); - - switch(type) { - default: - if (!type.endsWith('element')) - throw Error(`unknown eventName: ${type}`); - - parseArgs(args[EVENT_NAME], args[ELEMENT], listener, callback); - break; - - case 'string': - isFunc = itype.function(element); - - if (isFunc) { - listener = element; - element = null; - } - - if (!element) - element = window; - - callback(element, [ - eventName, - listener, - getEventOptions(eventName), - ]); - break; - - case 'array': - - for (const name of eventName) { - parseArgs(name, element, listener, callback); - } - - break; - - case 'object': - - for (const name of Object.keys(eventName)) { - const eventListener = eventName[name]; - - parseArgs(name, element, eventListener, callback); - } - - break; - } - } - - /** - * safe add event listener - * - * @param type - * @param element {document by default} - * @param listener - */ - this.add = (type, element, listener) => { - checkType(type); - - parseArgs(type, element, listener, (element, args) => { - const [name, fn, options] = args; - - element.addEventListener(name, fn, options); - EventStore.add(element, name, fn); - }); - - return Events; - }; - - /** - * safe add event listener - * - * @param type - * @param listener - * @param element {document by default} - */ - this.addOnce = (type, element, listener) => { - const once = (event) => { - Events.remove(type, element, once); - listener(event); - }; - - if (!listener) { - listener = element; - element = null; - } - - this.add(type, element, once); - - return Events; - }; - - /** - * safe remove event listener - * - * @param type - * @param listener - * @param element {document by default} - */ - this.remove = (type, element, listener) => { - checkType(type); - - parseArgs(type, element, listener, (element, args) => { - element.removeEventListener(...args); - }); - - return Events; - }; - - /** - * remove all added event listeners - * - * @param listener - */ - this.removeAll = () => { - const events = EventStore.get(); - - for (const [el, name, fn] of events) - el.removeEventListener(name, fn); - - EventStore.clear(); - }; - - /** - * safe add event keydown listener - * - * @param listener - */ - this.addKey = function(...argsArr) { - const name = 'keydown'; - const args = [ - name, - ...argsArr, - ]; - - return Events.add(...args); - }; - - /** - * safe remove event click listener - * - * @param listener - */ - this.rmKey = function(...argsArr) { - const name = 'keydown'; - const args = [ - name, - ...argsArr, - ]; - - return Events.remove(...args); - }; - - /** - * safe add event click listener - * - * @param listener - */ - this.addClick = function(...argsArr) { - const name = 'click'; - const args = [ - name, - ...argsArr, - ]; - - return Events.add(...args); - }; - - /** - * safe remove event click listener - * - * @param listener - */ - this.rmClick = function(...argsArr) { - const name = 'click'; - const args = [ - name, - ...argsArr, - ]; - - return Events.remove(...args); - }; - - this.addContextMenu = function(...argsArr) { - const name = 'contextmenu'; - const args = [ - name, - ...argsArr, - ]; - - return Events.add(...args); - }; - - /** - * safe add event click listener - * - * @param listener - */ - this.addError = function(...argsArr) { - const name = 'error'; - const args = [ - name, - ...argsArr, - ]; - - return Events.add(...args); - }; - - /** - * safe add load click listener - * - * @param listener - */ - this.addLoad = function(...argsArr) { - const name = 'load'; - const args = [ - name, - ...argsArr, - ]; - - return Events.add(...args); - }; - - function checkType(type) { - if (!type) - throw Error('type could not be empty!'); - } -} diff --git a/client/dom/events/index.mjs b/client/dom/events/index.mjs new file mode 100644 index 00000000..0762d560 --- /dev/null +++ b/client/dom/events/index.mjs @@ -0,0 +1,203 @@ +import itype from 'itype'; +import EventStore from './event-store.js'; + +/** + * safe add event listener + * + * @param type + * @param element - document by default + * @param listener + */ +export const add = (type, element, listener) => { + checkType(type); + + parseArgs(type, element, listener, (element, args) => { + const [name, fn, options] = args; + + element.addEventListener(name, fn, options); + EventStore.add(element, name, fn); + }); + + return Events; +}; + +/** + * safe add event listener + * + * @param type + * @param listener + * @param element - document by default + */ +export const addOnce = (type, element, listener) => { + const once = (event) => { + Events.remove(type, element, once); + listener(event); + }; + + if (!listener) { + listener = element; + element = null; + } + + add(type, element, once); + + return Events; +}; + +/** + * safe remove event listener + * + * @param type + * @param listener + * @param element - document by default + */ +export const remove = (type, element, listener) => { + checkType(type); + + parseArgs(type, element, listener, (element, args) => { + element.removeEventListener(...args); + }); + + return Events; +}; + +/** + * remove all added event listeners + */ +export const removeAll = () => { + const events = EventStore.get(); + + for (const [el, name, fn] of events) + el.removeEventListener(name, fn); + + EventStore.clear(); +}; + +/** + * safe add event keydown listener + * + * @param args + */ +export const addKey = function(...args) { + return add('keydown', ...args); +}; + +/** + * safe remove event click listener + * + * @param args + */ +export const rmKey = function(...args) { + return Events.remove('keydown', ...args); +}; + +/** + * safe add event click listener + */ +export const addClick = function(...args) { + return Events.add('click', ...args); +}; + +/** + * safe remove event click listener + */ +export const rmClick = function(...args) { + return remove('click', ...args); +}; + +export const addContextMenu = function(...args) { + return add('contextmenu', ...args); +}; + +/** + * safe add load listener + */ +export const addLoad = function(...args) { + return add('load', ...args); +}; + +function checkType(type) { + if (!type) + throw Error('type could not be empty!'); +} + +const getEventOptions = (eventName) => { + if (eventName !== 'touchstart') + return false; + + return { + passive: true, + }; +}; + +function parseArgs(eventName, element, listener, callback) { + let isFunc; + const args = [ + eventName, + element, + listener, + callback, + ]; + + const EVENT_NAME = 1; + const ELEMENT = 0; + const type = itype(eventName); + + switch(type) { + default: + if (!type.endsWith('element')) + throw Error(`unknown eventName: ${type}`); + + parseArgs(args[EVENT_NAME], args[ELEMENT], listener, callback); + break; + + case 'string': + isFunc = itype.function(element); + + if (isFunc) { + listener = element; + element = null; + } + + if (!element) + element = window; + + callback(element, [ + eventName, + listener, + getEventOptions(eventName), + ]); + break; + + case 'array': + + for (const name of eventName) { + parseArgs(name, element, listener, callback); + } + + break; + + case 'object': + + for (const name of Object.keys(eventName)) { + const eventListener = eventName[name]; + + parseArgs(name, element, eventListener, callback); + } + + break; + } +} + +const Events = { + add, + addClick, + addContextMenu, + addKey, + addLoad, + addOnce, + remove, + removeAll, + rmClick, + rmKey, +}; diff --git a/client/dom/images.js b/client/dom/images.mjs similarity index 82% rename from client/dom/images.js rename to client/dom/images.mjs index 382da9e8..9682e666 100644 --- a/client/dom/images.js +++ b/client/dom/images.mjs @@ -1,20 +1,14 @@ /* global DOM */ - -'use strict'; - -const createElement = require('@cloudcmd/create-element'); - -const Images = module.exports; +import createElement from '@cloudcmd/create-element'; const LOADING = 'loading'; const HIDDEN = 'hidden'; const ERROR = 'error'; -function getLoadingType() { - return isSVG() ? '-svg' : '-gif'; -} +const getLoadingType = () => isSVG() ? '-svg' : '-gif'; + +export const get = getElement; -module.exports.get = getElement; /** * check SVG SMIL animation support */ @@ -42,7 +36,7 @@ function getElement() { } /* Функция создаёт картинку загрузки */ -module.exports.loading = () => { +export const loading = () => { const element = getElement(); const {classList} = element; const loadingImage = LOADING + getLoadingType(); @@ -54,7 +48,7 @@ module.exports.loading = () => { }; /* Функция создаёт картинку ошибки загрузки */ -module.exports.error = () => { +export const error = () => { const element = getElement(); const {classList} = element; const loadingImage = LOADING + getLoadingType(); @@ -65,14 +59,21 @@ module.exports.error = () => { return element; }; -module.exports.show = show; -module.exports.show.load = show; -module.exports.show.error = error; +show.load = show; +show.error = (text) => { + const image = Images.error(); + + DOM.show(image); + image.title = text; + + return image; +}; + /** * Function shows loading spinner * position = {top: true}; */ -function show(position, panel) { +export function show(position, panel) { const image = Images.loading(); const parent = image.parentElement; const refreshButton = DOM.getRefreshButton(panel); @@ -98,19 +99,10 @@ function show(position, panel) { return image; } -function error(text) { - const image = Images.error(); - - DOM.show(image); - image.title = text; - - return image; -} - /** * hide load image */ -module.exports.hide = () => { +export const hide = () => { const element = Images.get(); DOM.hide(element); @@ -118,7 +110,7 @@ module.exports.hide = () => { return Images; }; -module.exports.setProgress = (value, title) => { +export const setProgress = (value, title) => { const DATA = 'data-progress'; const element = Images.get(); @@ -133,7 +125,7 @@ module.exports.setProgress = (value, title) => { return Images; }; -module.exports.clearProgress = () => { +export const clearProgress = () => { const DATA = 'data-progress'; const element = Images.get(); @@ -145,3 +137,13 @@ module.exports.clearProgress = () => { return Images; }; + +const Images = { + clearProgress, + setProgress, + show, + hide, + get, + error, + loading, +}; diff --git a/client/dom/index.js b/client/dom/index.js index c9b50e44..27a1a5c6 100644 --- a/client/dom/index.js +++ b/client/dom/index.js @@ -3,12 +3,12 @@ /* global CloudCmd */ const Util = require('../../common/util'); -const Images = require('./images'); +const Images = require('./images.mjs'); const RESTful = require('./rest'); const Storage = require('./storage'); const renameCurrent = require('./operations/rename-current'); -const CurrentFile = require('./current-file'); +const CurrentFile = require('./current-file.mjs'); const DOMTree = require('./dom-tree'); const Cmd = module.exports; @@ -32,8 +32,8 @@ DOM.CurrentInfo = CurrentInfo; module.exports = DOM; DOM.uploadDirectory = require('./directory'); -DOM.Buffer = require('./buffer'); -DOM.Events = require('./events'); +DOM.Buffer = require('./buffer.mjs'); +DOM.Events = require('#dom/events'); const loadRemote = require('./load-remote'); const selectByPattern = require('./select-by-pattern'); @@ -416,7 +416,7 @@ module.exports.shrinkSelection = () => { * setting history wrapper */ module.exports.setHistory = (data, title, url) => { - const ret = window.history; + const ret = globalThis.history; const {prefix} = CloudCmd; url = prefix + url; @@ -520,6 +520,11 @@ module.exports.getPanelPosition = (panel) => { return panel.dataset.name.replace('js-', ''); }; +module.exports.getCSSVar = (name, {body = document.body} = {}) => { + const bodyStyle = getComputedStyle(body); + return bodyStyle.getPropertyValue(`--${name}`); +}; + /** function getting panel active, or passive * @param options = {active: true} */ @@ -549,7 +554,7 @@ module.exports.getPanel = (options) => { * then always work with passive * panel */ - if (window.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH) + if (globalThis.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH) panel = DOM.getByDataName('js-left'); if (!panel) @@ -724,17 +729,19 @@ module.exports.getPackerExt = (type) => { return '.tar.gz'; }; -module.exports.goToDirectory = async () => { - const msg = 'Go to directory:'; +module.exports.goToDirectory = async (overrides = {}) => { const {Dialog} = DOM; + const {prompt = Dialog.prompt, changeDir = CloudCmd.changeDir} = overrides; + + const msg = 'Go to directory:'; const {dirPath} = CurrentInfo; - const [cancel, path = dirPath] = await Dialog.prompt(msg, dirPath); + const [cancel, path = dirPath] = await prompt(msg, dirPath); if (cancel) return; - await CloudCmd.changeDir(path); + await changeDir(path); }; module.exports.duplicatePanel = async () => { diff --git a/client/dom/index.spec.js b/client/dom/index.spec.js index 1c413fc8..e3a4b4bc 100644 --- a/client/dom/index.spec.js +++ b/client/dom/index.spec.js @@ -3,29 +3,54 @@ require('css-modules-require-hook/preset'); const {test, stub} = require('supertape'); -const mockRequire = require('mock-require'); -const {reRequire, stopAll} = mockRequire; +const {getCSSVar, goToDirectory} = require('./index'); -global.CloudCmd = {}; +globalThis.CloudCmd = {}; test('cloudcmd: client: dom: goToDirectory', async (t) => { const path = ''; - const {CloudCmd} = global; const changeDir = stub(); const prompt = stub().returns([null, path]); - CloudCmd.changeDir = changeDir; - - mockRequire('./dialog', { + await goToDirectory({ prompt, + changeDir, }); - const {goToDirectory} = reRequire('.'); - - await goToDirectory(); - - stopAll(); - t.calledWith(changeDir, [path]); t.end(); }); + +test('cloudcmd: client: dom: getCSSVar', (t) => { + const body = {}; + const getPropertyValue = stub().returns(0); + + globalThis.getComputedStyle = stub().returns({ + getPropertyValue, + }); + const result = getCSSVar('hello', { + body, + }); + + delete globalThis.getComputedStyle; + + t.notOk(result); + t.end(); +}); + +test('cloudcmd: client: dom: getCSSVar: 1', (t) => { + const body = {}; + const getPropertyValue = stub().returns(1); + + globalThis.getComputedStyle = stub().returns({ + getPropertyValue, + }); + const result = getCSSVar('hello', { + body, + }); + + delete globalThis.getComputedStyle; + + t.ok(result); + t.end(); +}); diff --git a/client/dom/io/index.js b/client/dom/io/index.js index 577c357a..a17360ae 100644 --- a/client/dom/io/index.js +++ b/client/dom/io/index.js @@ -1,14 +1,14 @@ 'use strict'; -const {FS} = require('../../../common/cloudfunc'); -const sendRequest = require('./send-request'); +const {FS} = require('../../../common/cloudfunc.mjs'); +const _sendRequest = require('./send-request'); const imgPosition = { top: true, }; module.exports.delete = async (url, data) => { - return await sendRequest({ + return await _sendRequest({ method: 'DELETE', url: FS + url, data, @@ -19,7 +19,7 @@ module.exports.delete = async (url, data) => { }; module.exports.patch = async (url, data) => { - return await sendRequest({ + return await _sendRequest({ method: 'PATCH', url: FS + url, data, @@ -28,7 +28,7 @@ module.exports.patch = async (url, data) => { }; module.exports.write = async (url, data) => { - return await sendRequest({ + return await _sendRequest({ method: 'PUT', url: FS + url, data, @@ -36,7 +36,11 @@ module.exports.write = async (url, data) => { }); }; -module.exports.createDirectory = async (url) => { +module.exports.createDirectory = async (url, overrides = {}) => { + const { + sendRequest = _sendRequest, + } = overrides; + return await sendRequest({ method: 'PUT', url: `${FS}${url}?dir`, @@ -47,7 +51,7 @@ module.exports.createDirectory = async (url) => { module.exports.read = async (url, dataType = 'text') => { const notLog = !url.includes('?'); - return await sendRequest({ + return await _sendRequest({ method: 'GET', url: FS + url, notLog, @@ -56,7 +60,7 @@ module.exports.read = async (url, dataType = 'text') => { }; module.exports.copy = async (from, to, names) => { - return await sendRequest({ + return await _sendRequest({ method: 'PUT', url: '/copy', data: { @@ -69,7 +73,7 @@ module.exports.copy = async (from, to, names) => { }; module.exports.pack = async (data) => { - return await sendRequest({ + return await _sendRequest({ method: 'PUT', url: '/pack', data, @@ -77,7 +81,7 @@ module.exports.pack = async (data) => { }; module.exports.extract = async (data) => { - return await sendRequest({ + return await _sendRequest({ method: 'PUT', url: '/extract', data, @@ -85,7 +89,7 @@ module.exports.extract = async (data) => { }; module.exports.move = async (from, to, names) => { - return await sendRequest({ + return await _sendRequest({ method: 'PUT', url: '/move', data: { @@ -98,7 +102,7 @@ module.exports.move = async (from, to, names) => { }; module.exports.rename = async (from, to) => { - return await sendRequest({ + return await _sendRequest({ method: 'PUT', url: '/rename', data: { @@ -111,7 +115,7 @@ module.exports.rename = async (from, to) => { module.exports.Config = { read: async () => { - return await sendRequest({ + return await _sendRequest({ method: 'GET', url: '/config', imgPosition, @@ -120,7 +124,7 @@ module.exports.Config = { }, write: async (data) => { - return await sendRequest({ + return await _sendRequest({ method: 'PATCH', url: '/config', data, @@ -131,7 +135,7 @@ module.exports.Config = { module.exports.Markdown = { read: async (url) => { - return await sendRequest({ + return await _sendRequest({ method: 'GET', url: `/markdown${url}`, imgPosition, @@ -140,7 +144,7 @@ module.exports.Markdown = { }, render: async (data) => { - return await sendRequest({ + return await _sendRequest({ method: 'PUT', url: '/markdown', data, diff --git a/client/dom/io/index.spec.js b/client/dom/io/index.spec.js index 2212f91a..19ebe5bd 100644 --- a/client/dom/io/index.spec.js +++ b/client/dom/io/index.spec.js @@ -1,18 +1,14 @@ 'use strict'; const {test, stub} = require('supertape'); - -const mockRequire = require('mock-require'); - -const {reRequire, stopAll} = mockRequire; +const io = require('.'); test('client: dom: io', (t) => { const sendRequest = stub(); - mockRequire('./send-request', sendRequest); - const io = reRequire('.'); - - io.createDirectory('/hello'); + io.createDirectory('/hello', { + sendRequest, + }); const expected = { imgPosition: { @@ -22,8 +18,6 @@ test('client: dom: io', (t) => { url: '/fs/hello?dir', }; - stopAll(); - t.calledWith(sendRequest, [expected]); t.end(); }); diff --git a/client/dom/io/send-request.js b/client/dom/io/send-request.js index bc52d667..c61544f1 100644 --- a/client/dom/io/send-request.js +++ b/client/dom/io/send-request.js @@ -3,7 +3,7 @@ /* global CloudCmd */ const {promisify} = require('es6-promisify'); -const Images = require('../images'); +const Images = require('../images.mjs'); const load = require('../load'); module.exports = promisify((params, callback) => { diff --git a/client/dom/load-remote.js b/client/dom/load-remote.js index 0def5f00..b1b798a8 100644 --- a/client/dom/load-remote.js +++ b/client/dom/load-remote.js @@ -4,7 +4,7 @@ const rendy = require('rendy'); const itype = require('itype'); const load = require('load.js'); -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); const {findObjByNameInArr} = require('../../common/util'); @@ -17,42 +17,40 @@ module.exports = (name, options, callback = options) => { if (o.name && window[o.name]) return callback(); - Files - .get('modules') - .then(async (modules) => { - const online = config('online') && navigator.onLine; - const module = findObjByNameInArr(modules.remote, name); - - const isArray = itype.array(module.local); - const {version} = module; - - let remoteTmpls; - let local; - - if (isArray) { - remoteTmpls = module.remote; - local = module.local; - } else { - remoteTmpls = [module.remote]; - local = [module.local]; - } - - const localURL = local.map((url) => prefix + url); - - const remoteURL = remoteTmpls.map((tmpl) => { - return rendy(tmpl, { - version, - }); + Files.get('modules').then(async (modules) => { + const online = config('online') && navigator.onLine; + const module = findObjByNameInArr(modules.remote, name); + + const isArray = itype.array(module.local); + const {version} = module; + + let remoteTmpls; + let local; + + if (isArray) { + remoteTmpls = module.remote; + ({local} = module); + } else { + remoteTmpls = [module.remote]; + local = [module.local]; + } + + const localURL = local.map((url) => prefix + url); + + const remoteURL = remoteTmpls.map((tmpl) => { + return rendy(tmpl, { + version, }); - - if (online) { - const [e] = await tryToCatch(load.parallel, remoteURL); - - if (!e) - return callback(); - } - - const [e] = await tryToCatch(load.parallel, localURL); - callback(e); }); + + if (online) { + const [e] = await tryToCatch(load.parallel, remoteURL); + + if (!e) + return callback(); + } + + const [e] = await tryToCatch(load.parallel, localURL); + callback(e); + }); }; diff --git a/client/dom/load.js b/client/dom/load.js index 9148c1b8..d060a92c 100644 --- a/client/dom/load.js +++ b/client/dom/load.js @@ -4,7 +4,7 @@ const itype = require('itype'); const jonny = require('jonny'); const Emitify = require('emitify'); const exec = require('execon'); -const Images = require('./images'); +const Images = require('./images.mjs'); module.exports.getIdBySrc = getIdBySrc; /** @@ -63,7 +63,7 @@ module.exports.ajax = (params) => { if (!isArrayBuf && isObject || isArray) data = jonny.stringify(p.data); else - data = p.data; + ({data} = p); xhr.onreadystatechange = (event) => { const xhr = event.target; diff --git a/client/dom/operations/rename-current.js b/client/dom/operations/rename-current.js index 5b2e0ff7..ab658edf 100644 --- a/client/dom/operations/rename-current.js +++ b/client/dom/operations/rename-current.js @@ -3,21 +3,29 @@ /* global CloudCmd */ const capitalize = require('just-capitalize'); -const Dialog = require('../dialog'); +const _Dialog = require('../dialog'); const Storage = require('../storage'); const RESTful = require('../rest'); -const { - isCurrentFile, - getCurrentName, - getCurrentFile, - getCurrentByName, - getCurrentType, - getCurrentDirPath, - setCurrentName, -} = require('../current-file'); +const _currentFile = require('../current-file.mjs'); -module.exports = async (current) => { +module.exports = async (current, overrides = {}) => { + const { + refresh = CloudCmd.refresh, + Dialog = _Dialog, + currentFile = _currentFile, + } = overrides; + + const { + isCurrentFile, + getCurrentName, + getCurrentFile, + getCurrentByName, + getCurrentType, + getCurrentDirPath, + setCurrentName, + } = currentFile; + if (!isCurrentFile(current)) current = getCurrentFile(); @@ -58,5 +66,5 @@ module.exports = async (current) => { setCurrentName(to, current); Storage.remove(dirPath); - CloudCmd.refresh(); + refresh(); }; diff --git a/client/dom/operations/rename-current.spec.js b/client/dom/operations/rename-current.spec.js index f39ff6bf..4a3c7fec 100644 --- a/client/dom/operations/rename-current.spec.js +++ b/client/dom/operations/rename-current.spec.js @@ -2,23 +2,20 @@ const {test, stub} = require('supertape'); -const mockRequire = require('mock-require'); - -const {reRequire, stopAll} = mockRequire; +const renameCurrent = require('./rename-current'); test('cloudcmd: client: dom: renameCurrent: isCurrentFile', async (t) => { const current = {}; const isCurrentFile = stub(); - mockRequire('../dialog', stubDialog()); - mockRequire('../current-file', stubCurrentFile({ + const currentFile = stubCurrentFile({ isCurrentFile, - })); + }); - const renameCurrent = reRequire('./rename-current'); - await renameCurrent(current); - - stopAll(); + await renameCurrent(current, { + Dialog: stubDialog(), + currentFile, + }); t.calledWith(isCurrentFile, [current], 'should call isCurrentFile'); t.end(); @@ -27,11 +24,6 @@ test('cloudcmd: client: dom: renameCurrent: isCurrentFile', async (t) => { test('cloudcmd: client: dom: renameCurrent: file exist', async (t) => { const current = {}; const name = 'hello'; - const {CloudCmd} = global; - - global.CloudCmd = { - refresh: stub(), - }; const prompt = stub().returns([null, name]); const confirm = stub().returns([true]); @@ -39,25 +31,23 @@ test('cloudcmd: client: dom: renameCurrent: file exist', async (t) => { const getCurrentByName = stub().returns(current); const getCurrentType = stub().returns('directory'); - mockRequire('../dialog', stubDialog({ + const Dialog = stubDialog({ confirm, prompt, - })); + }); - mockRequire('../current-file', stubCurrentFile({ + const currentFile = stubCurrentFile({ getCurrentByName, getCurrentType, - })); + }); - const renameCurrent = reRequire('./rename-current'); - await renameCurrent(); + await renameCurrent(null, { + Dialog, + currentFile, + }); const expected = 'Directory "hello" already exists. Proceed?'; - global.CloudCmd = CloudCmd; - - stopAll(); - t.calledWith(confirm, [expected], 'should call confirm'); t.end(); }); diff --git a/client/dom/rest.js b/client/dom/rest.js index 9c638446..7596f620 100644 --- a/client/dom/rest.js +++ b/client/dom/rest.js @@ -1,10 +1,10 @@ 'use strict'; -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); const {encode} = require('../../common/entity'); -const Images = require('./images'); +const Images = require('./images.mjs'); const IO = require('./io'); const Dialog = require('./dialog'); diff --git a/client/dom/storage.spec.js b/client/dom/storage.spec.js index abd7258a..0b281ffc 100644 --- a/client/dom/storage.spec.js +++ b/client/dom/storage.spec.js @@ -7,58 +7,58 @@ const storage = require('./storage'); const {stringify} = JSON; test('cloudcmd: client: storage: set', async (t) => { - const {localStorage} = global; + const {localStorage} = globalThis; const setItem = stub(); - global.localStorage = { + globalThis.localStorage = { setItem, }; await storage.set('hello', 'world'); - global.localStorage = localStorage; + globalThis.localStorage = localStorage; t.calledWith(setItem, ['hello', 'world'], 'should call setItem'); t.end(); }); test('cloudcmd: client: storage: get', async (t) => { - const {localStorage} = global; + const {localStorage} = globalThis; const getItem = stub().returns('world'); - global.localStorage = { + globalThis.localStorage = { getItem, }; const result = await storage.get('hello'); - global.localStorage = localStorage; + globalThis.localStorage = localStorage; t.equal(result, 'world'); t.end(); }); test('cloudcmd: client: storage: getJson', async (t) => { - const {localStorage} = global; + const {localStorage} = globalThis; const expected = { hello: 'world', }; const getItem = stub().returns(stringify(expected)); - global.localStorage = { + globalThis.localStorage = { getItem, }; const result = await storage.getJson('hello'); - global.localStorage = localStorage; + globalThis.localStorage = localStorage; t.deepEqual(result, expected); t.end(); }); test('cloudcmd: client: storage: setJson', async (t) => { - const {localStorage} = global; + const {localStorage} = globalThis; const data = { hello: 'world', }; @@ -66,42 +66,42 @@ test('cloudcmd: client: storage: setJson', async (t) => { const expected = stringify(data); const setItem = stub(); - global.localStorage = { + globalThis.localStorage = { setItem, }; await storage.setJson('hello', data); - global.localStorage = localStorage; + globalThis.localStorage = localStorage; t.calledWith(setItem, ['hello', expected]); t.end(); }); test('cloudcmd: client: storage: remove', async (t) => { - const {localStorage} = global; + const {localStorage} = globalThis; const removeItem = stub(); - global.localStorage = { + globalThis.localStorage = { removeItem, }; await storage.remove('hello'); - global.localStorage = localStorage; + globalThis.localStorage = localStorage; t.calledWith(removeItem, ['hello'], 'should call removeItem'); t.end(); }); test('cloudcmd: client: storage: clear', async (t) => { - const {localStorage} = global; + const {localStorage} = globalThis; const clear = stub(); - global.localStorage = { + globalThis.localStorage = { clear, }; await storage.clear(); - global.localStorage = localStorage; + globalThis.localStorage = localStorage; t.calledWithNoArgs(clear, 'should call clear'); t.end(); diff --git a/client/dom/upload-files.js b/client/dom/upload-files.js index 7ac460ea..a1206282 100644 --- a/client/dom/upload-files.js +++ b/client/dom/upload-files.js @@ -5,10 +5,10 @@ const {eachSeries} = require('execon'); const wraptile = require('wraptile'); const load = require('./load'); -const Images = require('./images'); +const Images = require('./images.mjs'); const {alert} = require('./dialog'); -const {FS} = require('../../common/cloudfunc'); +const {FS} = require('../../common/cloudfunc.mjs'); const {getCurrentDirPath: getPathWhenRootEmpty} = require('.'); const loadFile = wraptile(_loadFile); diff --git a/client/get-json-from-file-table.js b/client/get-json-from-file-table.js deleted file mode 100644 index e6fbc872..00000000 --- a/client/get-json-from-file-table.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -/* global DOM */ -const Info = DOM.CurrentInfo; - -/** - * Функция генерирует JSON из html-таблицы файлов и - * используеться при первом заходе в корень - */ -module.exports = () => { - const path = DOM.getCurrentDirPath(); - const infoFiles = Info.files || []; - - const notParent = (current) => { - const name = DOM.getCurrentName(current); - return name !== '..'; - }; - - const parse = (current) => { - const name = DOM.getCurrentName(current); - const size = DOM.getCurrentSize(current); - const owner = DOM.getCurrentOwner(current); - const mode = DOM.getCurrentMode(current); - const date = DOM.getCurrentDate(current); - const type = DOM.getCurrentType(current); - - return { - name, - size, - mode, - owner, - date, - type, - }; - }; - - const files = infoFiles - .filter(notParent) - .map(parse); - - const fileTable = { - path, - files, - }; - - return fileTable; -}; diff --git a/client/get-json-from-file-table.mjs b/client/get-json-from-file-table.mjs new file mode 100644 index 00000000..ea90f366 --- /dev/null +++ b/client/get-json-from-file-table.mjs @@ -0,0 +1,44 @@ +/* global DOM */ +/** + * Функция генерирует JSON из html-таблицы файлов и + * используеться при первом заходе в корень + */ +export const getJsonFromFileTable = () => { + const Info = DOM.CurrentInfo; + const path = DOM.getCurrentDirPath(); + const infoFiles = Info.files || []; + + const files = infoFiles + .filter(notParent) + .map(parse); + + const fileTable = { + path, + files, + }; + + return fileTable; +}; + +const notParent = (current) => { + const name = DOM.getCurrentName(current); + return name !== '..'; +}; + +const parse = (current) => { + const name = DOM.getCurrentName(current); + const size = DOM.getCurrentSize(current); + const owner = DOM.getCurrentOwner(current); + const mode = DOM.getCurrentMode(current); + const date = DOM.getCurrentDate(current); + const type = DOM.getCurrentType(current); + + return { + name, + size, + mode, + owner, + date, + type, + }; +}; diff --git a/client/key/index.js b/client/key/index.mjs similarity index 89% rename from client/key/index.js rename to client/key/index.mjs index d7d455af..64fd1bc3 100644 --- a/client/key/index.js +++ b/client/key/index.mjs @@ -1,22 +1,17 @@ -'use strict'; - /* global CloudCmd, DOM */ -const clipboard = require('@cloudcmd/clipboard'); -const fullstore = require('fullstore'); +import clipboard from '@cloudcmd/clipboard'; +import {fullstore} from 'fullstore'; +import * as Events from '#dom/events'; +import * as Buffer from '../dom/buffer.mjs'; +import KEY from './key.js'; +import _vim from './vim/index.js'; +import setCurrentByChar from './set-current-by-char.js'; +import {createBinder} from './binder.js'; -const Buffer = require('../dom/buffer'); -const Events = require('../dom/events'); -const KEY = require('./key'); - -const vim = require('./vim'); -const setCurrentByChar = require('./set-current-by-char'); -const {createBinder} = require('./binder'); - -const Info = DOM.CurrentInfo; const Chars = fullstore(); -const toggleVim = (keyCode) => { - const {_config, config} = CloudCmd; +const toggleVim = (keyCode, overrides = {}) => { + const {_config, config} = overrides; if (keyCode === KEY.ESC) _config('vim', !config('vim')); @@ -29,13 +24,16 @@ Chars([]); const {assign} = Object; const binder = createBinder(); -module.exports = assign(binder, KEY); -module.exports.bind = () => { +const bind = () => { Events.addKey(listener, true); binder.setBind(); }; -module.exports._listener = listener; +export const Key = assign(binder, KEY, { + bind, +}); + +export const _listener = listener; function getChar(event) { /* @@ -55,7 +53,14 @@ function getChar(event) { return [symbol, char]; } -async function listener(event) { +async function listener(event, overrides = {}) { + const { + config = CloudCmd.config, + _config = CloudCmd._config, + switchKey = _switchKey, + vim = _vim, + } = overrides; + const {keyCode} = event; // strange chrome bug calles listener twice @@ -74,8 +79,12 @@ async function listener(event) { if (!binder.isBind()) return; - toggleVim(keyCode); - const isVim = CloudCmd.config('vim'); + toggleVim(keyCode, { + config, + _config, + }); + + const isVim = config('vim'); if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || symbol)) return setCurrentByChar(char, Chars); @@ -112,7 +121,8 @@ function fromCharCode(keyIdentifier) { return String.fromCharCode(hex); } -async function switchKey(event) { +async function _switchKey(event) { + const Info = DOM.CurrentInfo; let i; let isSelected; let prev; @@ -509,6 +519,18 @@ async function switchKey(event) { event.preventDefault(); } + break; + + case KEY.DOT: + if (meta && shift) { + const showDotFiles = !CloudCmd.config('showDotFiles'); + CloudCmd._config('showDotFiles', showDotFiles); + CloudCmd.refresh(); + await DOM.RESTful.Config.write({ + showDotFiles, + }); + } + break; } } diff --git a/client/key/index.spec.js b/client/key/index.spec.js index 8ab7a960..d8167079 100644 --- a/client/key/index.spec.js +++ b/client/key/index.spec.js @@ -3,27 +3,23 @@ require('css-modules-require-hook/preset'); const autoGlobals = require('auto-globals'); -const mockRequire = require('mock-require'); const supertape = require('supertape'); const {ESC} = require('./key'); + +const {Key, _listener} = require('./index.mjs'); + const {getDOM, getCloudCmd} = require('./vim/globals.fixture'); const test = autoGlobals(supertape); -const {reRequire, stopAll} = mockRequire; const {stub} = supertape; -global.DOM = getDOM(); -global.CloudCmd = getCloudCmd(); +globalThis.DOM = getDOM(); +globalThis.CloudCmd = getCloudCmd(); test('cloudcmd: client: key: enable vim', async (t) => { const vim = stub(); - const {CloudCmd} = global; - const {config} = CloudCmd; - - CloudCmd.config = stub().returns(true); - CloudCmd._config = stub(); - mockRequire('./vim', vim); - const {_listener, setBind} = reRequire('.'); + const config = stub().returns(true); + const _config = stub(); const event = { keyCode: ESC, @@ -31,11 +27,14 @@ test('cloudcmd: client: key: enable vim', async (t) => { altKey: false, }; - setBind(); - await _listener(event); + Key.setBind(); - CloudCmd.config = config; - stopAll(); + await _listener(event, { + vim, + config, + _config, + switchKey: stub(), + }); t.calledWith(vim, ['Escape', event]); t.end(); @@ -43,25 +42,20 @@ test('cloudcmd: client: key: enable vim', async (t) => { test('cloudcmd: client: key: disable vim', async (t) => { const _config = stub(); + const config = stub(); const event = { keyCode: ESC, key: 'Escape', altKey: false, }; - const {CloudCmd} = global; - const {config} = CloudCmd; + Key.setBind(); + await _listener(event, { + config, + _config, + switchKey: stub(), + }); - global.CloudCmd.config = _config; - global.CloudCmd._config = _config; - - const {_listener, setBind} = reRequire('.'); - - setBind(); - await _listener(event); - - CloudCmd.config = config; - - t.calledWith(_config, ['vim']); + t.calledWith(_config, ['vim', true]); t.end(); }); diff --git a/client/key/set-current-by-char.js b/client/key/set-current-by-char.js index f54881a3..ab9329f3 100644 --- a/client/key/set-current-by-char.js +++ b/client/key/set-current-by-char.js @@ -3,9 +3,9 @@ 'use strict'; const {escapeRegExp} = require('../../common/util'); -const Info = DOM.CurrentInfo; module.exports = function setCurrentByChar(char, charStore) { + const Info = DOM.CurrentInfo; let firstByName; let skipCount = 0; let setted = false; diff --git a/client/key/vim/find.js b/client/key/vim/find.js index 43b3d36a..d8b517c1 100644 --- a/client/key/vim/find.js +++ b/client/key/vim/find.js @@ -1,6 +1,6 @@ 'use strict'; -const fullstore = require('fullstore'); +const {fullstore} = require('fullstore'); const limier = require('limier'); const searchStore = fullstore([]); diff --git a/client/key/vim/find.spec.js b/client/key/vim/find.spec.js index 1b36216b..74cc7fb1 100644 --- a/client/key/vim/find.spec.js +++ b/client/key/vim/find.spec.js @@ -5,7 +5,7 @@ const dir = './'; const {getDOM} = require('./globals.fixture'); -global.DOM = getDOM(); +globalThis.DOM = getDOM(); const {_next, _previous} = require(`${dir}find`); diff --git a/client/key/vim/index.js b/client/key/vim/index.js index e8b01558..ddfe49c3 100644 --- a/client/key/vim/index.js +++ b/client/key/vim/index.js @@ -9,31 +9,43 @@ const { selectFileNotParent, } = require('./set-current'); -const {Dialog} = DOM; - -const DEPS = { - ...DOM, - ...CloudCmd, -}; - -module.exports = async (key, event, deps = DEPS) => { +module.exports = (key, event, overrides = {}) => { + const defaults = { + ...globalThis.DOM, + ...globalThis.CloudCmd, + }; + + const deps = { + ...defaults, + ...overrides, + }; + const operations = getOperations(event, deps); - await vim(key, operations); + + vim(key, operations, deps); }; const getOperations = (event, deps) => { const { - Info = DOM.CurrentInfo, + Info = globalThis.DOM.CurrentInfo, + CloudCmd = globalThis.CloudCmd, Operation, unselectFiles, setCurrentFile, setCurrentByName, getCurrentName, + prompt = globalThis.DOM.Dialog.prompt, + preventDefault = event?.preventDefault?.bind(event), + toggleSelectedFile, Buffer = {}, + createFindNext = _createFindNext, } = deps; return { + findNext: createFindNext({ + setCurrentByName, + }), escape: unselectFiles, remove: () => { @@ -99,8 +111,8 @@ const getOperations = (event, deps) => { }, find: async () => { - event.preventDefault(); - const [, value] = await Dialog.prompt('Find', ''); + preventDefault(); + const [, value] = await prompt('Find', ''); if (!value) return; @@ -111,11 +123,6 @@ const getOperations = (event, deps) => { setCurrentByName(result); }, - findNext: () => { - const name = finder.findNext(); - setCurrentByName(name); - }, - findPrevious: () => { const name = finder.findPrevious(); setCurrentByName(name); @@ -124,3 +131,10 @@ const getOperations = (event, deps) => { }; module.exports.selectFile = selectFileNotParent; + +const _createFindNext = (overrides = {}) => () => { + const {setCurrentByName} = overrides; + const name = finder.findNext(); + + setCurrentByName(name); +}; diff --git a/client/key/vim/index.spec.js b/client/key/vim/index.spec.js index 245e21cc..6fcad11e 100644 --- a/client/key/vim/index.spec.js +++ b/client/key/vim/index.spec.js @@ -10,13 +10,13 @@ const pathVim = join(dir, 'vim'); const {getDOM, getCloudCmd} = require('./globals.fixture'); -global.DOM = getDOM(); -global.CloudCmd = getCloudCmd(); +globalThis.DOM = getDOM(); +globalThis.CloudCmd = getCloudCmd(); -const vim = require(pathVim); +const vim = require('./index.js'); const {assign} = Object; -const {DOM} = global; +const {DOM} = globalThis; const {Buffer} = DOM; const pathFind = join(dir, 'vim', 'find'); const {reRequire, stopAll} = mockRequire; @@ -520,15 +520,26 @@ test('cloudcmd: client: key: Enter', async (t) => { test('cloudcmd: client: key: /', (t) => { const preventDefault = stub(); const element = {}; + const Info = { + element, + files: [], + }; - DOM.CurrentInfo.element = element; - DOM.getCurrentName = () => ''; + const getCurrentName = stub().returns(''); - vim('/', { + const event = { preventDefault, + }; + + const prompt = stub().returns([]); + + vim('/', event, { + getCurrentName, + Info, + prompt, }); - t.calledWithNoArgs(preventDefault, 'should call preventDefault'); + t.calledWithNoArgs(preventDefault); t.end(); }); @@ -559,17 +570,13 @@ test('cloudcmd: client: find', (t) => { test('cloudcmd: client: key: n', (t) => { const findNext = stub(); + const createFindNext = stub().returns(findNext); - mockRequire(pathFind, { - findNext, - }); - - const vim = reRequire(pathVim); const event = {}; - vim('n', event); - - stopAll(); + vim('n', event, { + createFindNext, + }); t.calledWithNoArgs(findNext, 'should call findNext'); t.end(); @@ -595,7 +602,7 @@ test('cloudcmd: client: key: N', (t) => { test('cloudcmd: client: key: make directory', async (t) => { const vim = reRequire(pathVim); - const {DOM} = global; + const {DOM} = globalThis; assign(DOM, { promptNewDir: stub(), @@ -615,7 +622,7 @@ test('cloudcmd: client: key: make directory', async (t) => { test('cloudcmd: client: key: make file', (t) => { const vim = reRequire(pathVim); - const {DOM} = global; + const {DOM} = globalThis; assign(DOM, { promptNewFile: stub(), @@ -634,28 +641,30 @@ test('cloudcmd: client: key: make file', (t) => { }); test('cloudcmd: client: vim: terminal', (t) => { - const {CloudCmd} = global; - - assign(CloudCmd, { + const CloudCmd = { Terminal: { show: stub(), }, - }); + }; const event = {}; - vim('t', event); - vim('t', event); + vim('t', event, { + CloudCmd, + }); + vim('t', event, { + CloudCmd, + }); t.calledWithNoArgs(CloudCmd.Terminal.show); t.end(); }); test('cloudcmd: client: vim: edit', async (t) => { - global.DOM = getDOM(); - global.CloudCmd = getCloudCmd(); + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd(); - const {CloudCmd} = global; + const {CloudCmd} = globalThis; assign(CloudCmd, { EditFileVim: { diff --git a/client/key/vim/vim.js b/client/key/vim/vim.js index 99fe2acf..175f36ac 100644 --- a/client/key/vim/vim.js +++ b/client/key/vim/vim.js @@ -1,6 +1,6 @@ 'use strict'; -const fullstore = require('fullstore'); +const {fullstore} = require('fullstore'); const store = fullstore(''); const visual = fullstore(false); diff --git a/client/listeners/index.js b/client/listeners/index.js index db927833..c12c62ed 100644 --- a/client/listeners/index.js +++ b/client/listeners/index.js @@ -5,12 +5,13 @@ const exec = require('execon'); const itype = require('itype'); const currify = require('currify'); -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); const clipboard = require('@cloudcmd/clipboard'); const getRange = require('./get-range'); const uploadFiles = require('../dom/upload-files'); -const {FS} = require('../../common/cloudfunc'); +const {FS} = require('../../common/cloudfunc.mjs'); +const Events = require('#dom/events'); const getIndex = currify(require('./get-index')); @@ -29,10 +30,8 @@ module.exports.init = async () => { ]); }; -CloudCmd.Listeners = module.exports; - const unselect = (event) => { - const isMac = /Mac/.test(window.navigator.platform); + const isMac = /Mac/.test(globalThis.navigator.platform); const { shiftKey, metaKey, @@ -50,12 +49,9 @@ const execAll = currify((funcs, event) => { fn(event); }); -const Info = DOM.CurrentInfo; -const {Events} = DOM; - const EventsFiles = { mousedown: exec.with(execIfNotUL, setCurrentFileByEvent), - click: execAll([onClick, unselect]), + click: execAll([onClick, exec.with(execIfNotMobile, unselect)]), dragstart: exec.with(execIfNotUL, onDragStart), dblclick: exec.with(execIfNotUL, onDblClick), touchstart: exec.with(execIfNotUL, onTouch), @@ -111,8 +107,10 @@ module.exports.initKeysPanel = () => { if (!keysElement) return; - Events.addClick(keysElement, ({target}) => { + Events.addClick(keysElement, (event) => { + const {target} = event; const {id} = target; + const operation = (name) => { const {Operation} = CloudCmd; @@ -128,7 +126,10 @@ module.exports.initKeysPanel = () => { 'f6': operation('move'), 'f7': DOM.promptNewDir, 'f8': operation('delete'), - 'f9': CloudCmd.Menu.show, + 'f9': () => { + event.stopPropagation(); + CloudCmd.Menu.show(); + }, 'f10': CloudCmd.Config.show, '~': CloudCmd.Konsole.show, 'shift~': CloudCmd.Terminal.show, @@ -162,6 +163,7 @@ function getPathListener(panel) { } function isNoCurrent(panel) { + const Info = DOM.CurrentInfo; const infoPanel = Info.panel; if (!infoPanel) @@ -186,6 +188,7 @@ function decodePath(path) { } async function onPathElementClick(panel, event) { + const Info = DOM.CurrentInfo; event.preventDefault(); const element = event.target; @@ -215,12 +218,18 @@ async function onPathElementClick(panel, event) { function copyPath(el) { clipboard - .writeText(el - .parentElement.title) + .writeText(el.parentElement.title) .then(CloudCmd.log) .catch(CloudCmd.log); } +function execIfNotMobile(callback, event) { + const isMobile = DOM.getCSSVar('is-mobile'); + + if (!isMobile) + callback(event); +} + function execIfNotUL(callback, event) { const {target} = event; const {tagName} = target; @@ -235,14 +244,14 @@ function onClick(event) { } function toggleSelect(key, files) { - const isMac = /Mac/.test(window.navigator.platform); + const isMac = /Mac/.test(globalThis.navigator.platform); if (!key) throw Error('key should not be undefined!'); const [file] = files; - if (isMac && key.meta || key.ctrl) + if (isMac && key.meta) return DOM.toggleSelectedFile(file); if (key.shift) @@ -250,6 +259,7 @@ function toggleSelect(key, files) { } function changePanel(element) { + const Info = DOM.CurrentInfo; const {panel} = Info; const files = DOM.getByDataName('js-files', panel); const ul = getULElement(element); @@ -291,6 +301,7 @@ async function onTouch(event) { * in Chrome (HTML5) */ function onDragStart(event) { + const Info = DOM.CurrentInfo; const {prefixURL} = CloudCmd; const element = getLIElement(event.target); const {isDir} = Info; @@ -327,6 +338,7 @@ function getULElement(element) { } function setCurrentFileByEvent(event) { + const Info = DOM.CurrentInfo; const BUTTON_LEFT = 0; const key = { @@ -415,7 +427,7 @@ function dragndrop() { }; /** - * In Mac OS Chrome dropEffect = 'none' + * In macOS Chrome dropEffect = 'none' * so drop do not firing up when try * to upload file from download bar */ @@ -440,7 +452,7 @@ function dragndrop() { } function unload() { - DOM.Events.add(['unload', 'beforeunload'], (event) => { + Events.add(['unload', 'beforeunload'], (event) => { const {Key} = CloudCmd; const isBind = Key?.isBind(); @@ -469,7 +481,8 @@ function pop() { function resize() { Events.add('resize', () => { - const is = window.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH; + const Info = DOM.CurrentInfo; + const is = globalThis.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH; if (!is) return; diff --git a/client/load-module.js b/client/load-module.mjs similarity index 81% rename from client/load-module.js rename to client/load-module.mjs index 0ce0cc64..7fc2c328 100644 --- a/client/load-module.js +++ b/client/load-module.mjs @@ -1,18 +1,16 @@ -'use strict'; - /* global CloudCmd */ -const exec = require('execon'); -const tryToCatch = require('try-to-catch'); -const loadJS = require('load.js').js; +import exec from 'execon'; +import {tryToCatch} from 'try-to-catch'; +import {js as loadJS} from 'load.js'; +import pascalCase from 'just-pascal-case'; -const pascalCase = require('just-pascal-case'); const noJS = (a) => a.replace(/.js$/, ''); /** * function load modules * @params = {name, path, func, dobefore, arg} */ -module.exports = function loadModule(params) { +export const loadModule = (params) => { if (!params) return; @@ -51,7 +49,7 @@ module.exports = function loadModule(params) { const [e, a] = await tryToCatch(m); if (e) - return console.error(e); + return; return await a.show(...args); }; diff --git a/client/modules/cloud.js b/client/modules/cloud.js index 81498c4b..53a6d9e9 100644 --- a/client/modules/cloud.js +++ b/client/modules/cloud.js @@ -9,7 +9,7 @@ const load = require('load.js'); const {ajax} = require('../dom/load'); const Files = require('../dom/files'); -const Images = require('../dom/images'); +const Images = require('../dom/images.mjs'); const {log} = CloudCmd; const upload = currify(_upload); diff --git a/client/modules/config/index.js b/client/modules/config/index.js index cdd18da1..db778cbc 100644 --- a/client/modules/config/index.js +++ b/client/modules/config/index.js @@ -8,16 +8,16 @@ const currify = require('currify'); const wraptile = require('wraptile'); const squad = require('squad'); const {promisify} = require('es6-promisify'); -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); const load = require('load.js'); const createElement = require('@cloudcmd/create-element'); const input = require('./input'); -const Images = require('../../dom/images'); -const Events = require('../../dom/events'); +const Images = require('../../dom/images.mjs'); +const Events = require('#dom/events'); const Files = require('../../dom/files'); -const {getTitle} = require('../../../common/cloudfunc'); +const {getTitle} = require('../../../common/cloudfunc.mjs'); const {Dialog, setTitle} = DOM; const Name = 'Config'; @@ -52,12 +52,12 @@ module.exports.init = async () => { showLoad(); - const {prefix} = CloudCmd; + const {DIR_DIST} = CloudCmd; [Template] = await Promise.all([ Files.get('config-tmpl'), loadSocket(), - loadCSS(`${prefix}/dist/config.css`), + loadCSS(`${DIR_DIST}/config.css`), CloudCmd.View(), ]); @@ -135,6 +135,7 @@ async function fillTemplate() { const { editor, + menu, packer, columns, theme, @@ -142,6 +143,7 @@ async function fillTemplate() { ...obj } = input.convert(config); + obj[`${menu}-selected`] = 'selected'; obj[`${editor}-selected`] = 'selected'; obj[`${packer}-selected`] = 'selected'; obj[`${columns}-selected`] = 'selected'; @@ -225,7 +227,8 @@ function onAuthChange(checked) { const elUsername = input.getElementByName('username', Element); const elPassword = input.getElementByName('password', Element); - elUsername.disabled = elPassword.disabled = !checked; + elUsername.disabled = !checked; + elPassword.disabled = !checked; } function onNameChange(name) { diff --git a/client/modules/contact.js b/client/modules/contact.js index 76a07d30..c6266de0 100644 --- a/client/modules/contact.js +++ b/client/modules/contact.js @@ -6,7 +6,7 @@ CloudCmd.Contact = exports; const olark = require('@cloudcmd/olark'); -const Images = require('../dom/images'); +const Images = require('../dom/images.mjs'); const {Events} = DOM; const {Key} = CloudCmd; diff --git a/client/modules/edit-file-vim.js b/client/modules/edit-file-vim.js index 48cfd93e..0edd203b 100644 --- a/client/modules/edit-file-vim.js +++ b/client/modules/edit-file-vim.js @@ -3,7 +3,7 @@ /* global CloudCmd */ CloudCmd.EditFileVim = exports; -const Events = require('../dom/events'); +const Events = require('#dom/events'); const {Key} = CloudCmd; diff --git a/client/modules/edit-file.js b/client/modules/edit-file.js index 5551a155..59050a3c 100644 --- a/client/modules/edit-file.js +++ b/client/modules/edit-file.js @@ -4,7 +4,7 @@ CloudCmd.EditFile = exports; const Format = require('format-io'); -const fullstore = require('fullstore'); +const {fullstore} = require('fullstore'); const exec = require('execon'); const supermenu = require('supermenu'); @@ -61,8 +61,7 @@ module.exports.show = async (options) => { Images.show.load(); - CloudCmd - .Edit + CloudCmd.Edit .getEditor() .setOption('keyMap', 'default'); @@ -78,8 +77,7 @@ module.exports.show = async (options) => { setMsgChanged(name); - CloudCmd - .Edit + CloudCmd.Edit .getEditor() .setValueFirst(path, data) .setModeForPath(name) @@ -131,8 +129,7 @@ function setMenu(event) { }, afterClick: () => { - CloudCmd - .Edit + CloudCmd.Edit .getEditor() .focus(); }, diff --git a/client/modules/edit-names-vim.js b/client/modules/edit-names-vim.js index 540259e5..0dbd92b2 100644 --- a/client/modules/edit-names-vim.js +++ b/client/modules/edit-names-vim.js @@ -3,7 +3,7 @@ /* global CloudCmd */ CloudCmd.EditNamesVim = exports; -const Events = require('../dom/events'); +const Events = require('#dom/events'); const {Key} = CloudCmd; const ConfigView = { @@ -21,8 +21,7 @@ module.exports.init = async () => { module.exports.show = () => { Events.addKey(listener); - CloudCmd - .EditNames + CloudCmd.EditNames .show(ConfigView) .getEditor() .setKeyMap('vim'); diff --git a/client/modules/edit-names.js b/client/modules/edit-names.js index e1cc3cbe..710ed7ea 100644 --- a/client/modules/edit-names.js +++ b/client/modules/edit-names.js @@ -1,21 +1,17 @@ 'use strict'; +const {tryToCatch} = require('try-to-catch'); + /* global CloudCmd, DOM */ CloudCmd.EditNames = exports; -const currify = require('currify'); const exec = require('execon'); const supermenu = require('supermenu'); -const multiRename = require('multi-rename'); - -const reject = Promise.reject.bind(Promise); +const {multiRename} = require('multi-rename'); const Info = DOM.CurrentInfo; const {Dialog} = DOM; -const refresh = currify(_refresh); -const rename = currify(_rename); - let Menu; const ConfigView = { @@ -44,8 +40,7 @@ module.exports.show = (options) => { DOM.Events.addKey(keyListener); - CloudCmd - .Edit + CloudCmd.Edit .getEditor() .setValueFirst('edit-names', names) .setMode() @@ -63,9 +58,9 @@ async function keyListener(event) { const ctrlMeta = ctrl || meta; const {Key} = CloudCmd; - if (ctrlMeta && event.keyCode === Key.S) + if (ctrlMeta && event.keyCode === Key.S) { hide(); - else if (ctrlMeta && event.keyCode === Key.P) { + } else if (ctrlMeta && event.keyCode === Key.P) { const [, pattern] = await Dialog.prompt('Apply pattern:', '[n][e]'); pattern && applyPattern(pattern); } @@ -94,7 +89,7 @@ function setListeners() { DOM.Events.addOnce('contextmenu', element, setMenu); } -function applyNames() { +async function applyNames() { const dir = Info.dirPath; const from = getActiveNames(); const nameIndex = from.indexOf(Info.name); @@ -106,18 +101,18 @@ function applyNames() { const root = CloudCmd.config('root'); - Promise - .resolve(root) - .then(rename(dir, from, to)) - .then(refresh(to, nameIndex)) - .catch(alert); + const response = rename(dir, from, to, root); + const [error] = await tryToCatch(refresh, to, nameIndex, response); + + if (error) + alert(error); } -function _refresh(to, nameIndex, res) { - if (res.status === 404) - return res - .text() - .then(reject); +function refresh(to, nameIndex, res) { + if (res.status === 404) { + const error = res.text(); + throw error; + } const currentName = to[nameIndex]; @@ -133,7 +128,7 @@ function getDir(root, dir) { return root + dir; } -function _rename(path, from, to, root) { +function rename(path, from, to, root) { const dir = getDir(root, path); const {prefix} = CloudCmd; @@ -173,8 +168,8 @@ function setMenu(event) { }; const menuData = { - 'Save Ctrl+S': () => { - applyNames(); + 'Save Ctrl+S': async () => { + await applyNames(); hide(); }, 'Go To Line Ctrl+G': () => { @@ -215,6 +210,7 @@ async function isChanged() { if (!editor.isChanged()) return; - const [, names] = await Dialog.confirm(msg); - names && applyNames(); + const [cancel] = await Dialog.confirm(msg); + + !cancel && await applyNames(); } diff --git a/client/modules/edit.js b/client/modules/edit.js index a62535b7..ecefaad8 100644 --- a/client/modules/edit.js +++ b/client/modules/edit.js @@ -5,12 +5,13 @@ const montag = require('montag'); const {promisify} = require('es6-promisify'); -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); const createElement = require('@cloudcmd/create-element'); const load = require('load.js'); -const {MAX_FILE_SIZE: maxSize} = require('../../common/cloudfunc'); +const {MAX_FILE_SIZE: maxSize} = require('../../common/cloudfunc.mjs'); const {time, timeEnd} = require('../../common/util'); +const getEditor = () => editor; const isFn = (a) => typeof a === 'function'; const loadJS = load.js; @@ -91,10 +92,6 @@ module.exports.show = (options) => { module.exports.getEditor = getEditor; -function getEditor() { - return editor; -} - module.exports.getElement = () => Element; module.exports.hide = () => { diff --git a/client/modules/help.js b/client/modules/help.js index 785bb32c..242b7c16 100644 --- a/client/modules/help.js +++ b/client/modules/help.js @@ -3,7 +3,7 @@ /* global CloudCmd */ CloudCmd.Help = exports; -const Images = require('../dom/images'); +const Images = require('../dom/images.mjs'); module.exports.init = () => { Images.show.load('top'); diff --git a/client/modules/konsole.js b/client/modules/konsole.js index 41936486..ae5bc42c 100644 --- a/client/modules/konsole.js +++ b/client/modules/konsole.js @@ -8,11 +8,11 @@ CloudCmd.Konsole = exports; const exec = require('execon'); const currify = require('currify'); -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); const loadJS = require('load.js').js; const createElement = require('@cloudcmd/create-element'); -const Images = require('../dom/images'); +const Images = require('../dom/images.mjs'); const {Dialog, CurrentInfo: Info} = DOM; const rmLastSlash = (a) => a.replace(/\/$/, '') || '/'; @@ -46,22 +46,18 @@ module.exports.clear = () => { konsole.clear(); }; -function getPrefix() { - return CloudCmd.prefix + '/console'; -} +const getPrefix = () => CloudCmd.prefix + '/console'; function getPrefixSocket() { return CloudCmd.prefixSocket + '/console'; } -function getEnv() { - return { - ACTIVE_DIR: DOM.getCurrentDirPath.bind(DOM), - PASSIVE_DIR: DOM.getNotCurrentDirPath.bind(DOM), - CURRENT_NAME: DOM.getCurrentName.bind(DOM), - CURRENT_PATH: () => Info.path, - }; -} +const getEnv = () => ({ + ACTIVE_DIR: DOM.getCurrentDirPath.bind(DOM), + PASSIVE_DIR: DOM.getNotCurrentDirPath.bind(DOM), + CURRENT_NAME: DOM.getCurrentName.bind(DOM), + CURRENT_PATH: () => Info.path, +}); async function onPath(path) { if (Info.dirPath === path) diff --git a/client/modules/markdown.js b/client/modules/markdown.js index 6c5c3282..9dc224af 100644 --- a/client/modules/markdown.js +++ b/client/modules/markdown.js @@ -5,7 +5,7 @@ CloudCmd.Markdown = exports; const createElement = require('@cloudcmd/create-element'); -const Images = require('../dom/images'); +const Images = require('../dom/images.mjs'); const {Markdown} = require('../dom/rest'); const {alert} = require('../dom/dialog'); diff --git a/client/modules/menu/cloudmenu.mjs b/client/modules/menu/cloudmenu.mjs new file mode 100644 index 00000000..1fbe5165 --- /dev/null +++ b/client/modules/menu/cloudmenu.mjs @@ -0,0 +1,30 @@ +import supermenu from 'supermenu'; + +const noop = () => {}; +const {CloudCmd} = globalThis; + +export const createCloudMenu = async (fm, options, menuData) => { + const createMenu = await loadMenu(); + const menu = await createMenu(fm, options, menuData); + + menu.addContextMenuListener = menu.addContextMenuListener || noop; + + return menu; +}; + +async function loadMenu() { + if (CloudCmd.config('menu') === 'aleman') { + const {host, protocol} = globalThis.location; + const url = `${protocol}//${host}/node_modules/aleman/menu/menu.js`; + const {createMenu} = await import(/* webpackIgnore: true */url); + + return createMenu; + } + + return createSupermenu; +} + +function createSupermenu(name, options, menuData) { + const element = document.querySelector('[data-name="js-fm"]'); + return supermenu(element, options, menuData); +} diff --git a/client/modules/menu.js b/client/modules/menu/index.js similarity index 87% rename from client/modules/menu.js rename to client/modules/menu/index.js index 4a82d8bb..facb385a 100644 --- a/client/modules/menu.js +++ b/client/modules/menu/index.js @@ -4,12 +4,11 @@ const exec = require('execon'); const wrap = require('wraptile'); -const supermenu = require('supermenu'); const createElement = require('@cloudcmd/create-element'); -const {FS} = require('../../common/cloudfunc'); -const {getIdBySrc} = require('../dom/load'); -const RESTful = require('../dom/rest'); +const {FS} = require('../../../common/cloudfunc.mjs'); +const {getIdBySrc} = require('../../dom/load'); +const RESTful = require('../../dom/rest'); const {config, Key} = CloudCmd; @@ -32,7 +31,7 @@ module.exports.ENABLED = false; CloudCmd.Menu = exports; -module.exports.init = () => { +module.exports.init = async () => { const {isAuth, menuDataFile} = getFileMenuData(); const fm = DOM.getFM(); @@ -46,8 +45,12 @@ module.exports.init = () => { type: 'file', }); - MenuContext = supermenu(fm, options, menuData); - MenuContextFile = supermenu(fm, optionsFile, menuDataFile); + const {createCloudMenu} = await import('./cloudmenu.mjs'); + + const {name} = fm.dataset; + + MenuContext = await createCloudMenu(name, options, menuData); + MenuContextFile = await createCloudMenu(name, optionsFile, menuDataFile); MenuContext.addContextMenuListener(); MenuContextFile.addContextMenuListener(); @@ -106,7 +109,9 @@ function getOptions({type}) { const options = { icon: true, + infiniteScroll: false, beforeClose: Key.setBind, + beforeHide: Key.setBind, beforeShow: exec.with(beforeShow, func), beforeClick, name, @@ -126,6 +131,7 @@ function getMenuData(isAuth) { CloudCmd.Upload.show(); }, 'Upload From Cloud': uploadFromCloud, + 'Toggle File Selection': DOM.toggleSelectedFile, '(Un)Select All': DOM.toggleAllSelectedFiles, }; @@ -197,14 +203,25 @@ function isPath(x, y) { const el = document.elementFromPoint(x, y); const elements = panel.querySelectorAll('[data-name="js-path"] *'); - return ~[].indexOf.call(elements, el); + return !~[].indexOf.call(elements, el); } function beforeShow(callback, params) { - const {name} = params; + Key.unsetBind(); + + const { + name, + position = { + x: params.x, + y: params.y, + }, + } = params; + + const {x, y} = position; + const el = DOM.getCurrentByPosition({ - x: params.x, - y: params.y, + x, + y, }); const menuName = getMenuNameByEl(el); @@ -220,14 +237,12 @@ function beforeShow(callback, params) { exec(callback); if (isShow) - isShow = isPath(params.x, params.y); + isShow = isPath(x, y); return isShow; } -function beforeClick(name) { - return MenuShowedName !== name; -} +const beforeClick = (name) => MenuShowedName !== name; async function _uploadTo(nameModule) { const [error, data] = await Info.getData(); @@ -277,6 +292,7 @@ function download(type) { const path = DOM.getCurrentPath(file); CloudCmd.log(`downloading file ${path}...`); + /* * if we send ajax request - * no need in hash so we escape # @@ -327,13 +343,12 @@ function listener(event) { const key = event.keyCode; const isBind = Key.isBind(); - if (!isBind) - return; - - if (key === ESC) + if (key === ESC) { + Key.setBind(); return hide(); + } - if (key === F9) { + if (isBind && key === F9) { const position = getCurrentPosition(); MenuContext.show(position.x, position.y); diff --git a/client/modules/operation/index.js b/client/modules/operation/index.mjs similarity index 92% rename from client/modules/operation/index.js rename to client/modules/operation/index.mjs index 68d17a33..8e8149e5 100644 --- a/client/modules/operation/index.js +++ b/client/modules/operation/index.mjs @@ -1,28 +1,20 @@ -/* global CloudCmd */ -/* global Util */ -/* global DOM */ -/* global fileop */ +import currify from 'currify'; +import wraptile from 'wraptile'; +import {promisify} from 'es6-promisify'; +import exec from 'execon'; +import load from 'load.js'; +import {tryToCatch} from 'try-to-catch'; +import {encode} from '../../../common/entity.js'; +import removeExtension from './remove-extension.js'; +import {setListeners} from './set-listeners.mjs'; +import getNextCurrentName from './get-next-current-name.js'; -'use strict'; - -const currify = require('currify'); -const wraptile = require('wraptile'); -const {promisify} = require('es6-promisify'); -const exec = require('execon'); -const load = require('load.js'); -const tryToCatch = require('try-to-catch'); - -const {encode} = require('../../../common/entity'); -const removeExtension = require('./remove-extension'); -const setListeners = require('./set-listeners'); -const getNextCurrentName = require('./get-next-current-name'); +const {DOM, CloudCmd} = globalThis; const removeQuery = (a) => a.replace(/\?.*/, ''); const Name = 'Operation'; -CloudCmd[Name] = exports; - const {config} = CloudCmd; const {Dialog, Images} = DOM; @@ -53,7 +45,7 @@ const noFilesCheck = () => { return is; }; -module.exports.init = promisify((callback) => { +export const init = promisify((callback) => { showLoad(); exec.series([ @@ -92,7 +84,7 @@ const onConnect = currify((fn, operator) => { async function initOperations(prefix, socketPrefix, fn) { socketPrefix = `${socketPrefix}/fileop`; - const operator = await fileop({ + const operator = await globalThis.fileop({ prefix, socketPrefix, }); @@ -198,11 +190,11 @@ function getPacker(type) { return packTarFn; } -module.exports.hide = () => { +export const hide = () => { CloudCmd.View.hide(); }; -module.exports.show = (operation, data) => { +export const show = (operation, data) => { if (!Loaded) return; @@ -343,10 +335,13 @@ async function _processFiles(options, data) { let names = []; if (data) { - from = data.from; - to = data.to; - names = data.names; - panel = Info.panel; + ({ + from, + to, + names, + } = data); + + ({panel} = Info); } else { from = Info.dirPath; to = DOM.getNotCurrentDirPath(); @@ -502,8 +497,14 @@ async function prompt(msg, to, names) { return await Dialog.prompt(msg, to); } +globalThis.CloudCmd[Name] = { + init, + hide, + show, +}; + async function loadAll() { - const {prefix} = CloudCmd; + const {prefix} = globalThis.CloudCmd; const file = `${prefix}/fileop/fileop.js`; const [error] = await tryToCatch(load.js, file); diff --git a/client/modules/operation/remove-extension.js b/client/modules/operation/remove-extension.js index a8389af3..5e98727d 100644 --- a/client/modules/operation/remove-extension.js +++ b/client/modules/operation/remove-extension.js @@ -9,10 +9,10 @@ module.exports = (name) => { }; function getExtension(name) { - if (/\.tar\.gz$/.test(name)) + if (name.endsWith('.tar.gz')) return '.tar.gz'; - if (/\.tar\.bz2$/.test(name)) + if (name.endsWith('.tar.bz2')) return '.tar.bz2'; return getExt(name); diff --git a/client/modules/operation/set-listeners.js b/client/modules/operation/set-listeners.mjs similarity index 85% rename from client/modules/operation/set-listeners.js rename to client/modules/operation/set-listeners.mjs index 495cd04b..d5052cff 100644 --- a/client/modules/operation/set-listeners.js +++ b/client/modules/operation/set-listeners.mjs @@ -1,14 +1,11 @@ -'use strict'; - /* global DOM */ -const forEachKey = require('for-each-key'); - -const wraptile = require('wraptile'); -const format = require('./format'); +import forEachKey from 'for-each-key'; +import wraptile from 'wraptile'; +import format from './format.js'; const {Dialog, Images} = DOM; -module.exports = (options) => (emitter) => { +export const setListeners = (options) => (emitter) => { const { operation, callback, @@ -43,10 +40,13 @@ module.exports = (options) => (emitter) => { operation, })); + let noProgress = true; + const listeners = { progress: (value) => { done = value === 100; progress.setProgress(value); + noProgress = false; }, end: () => { @@ -54,7 +54,7 @@ module.exports = (options) => (emitter) => { forEachKey(removeListener, listeners); progress.remove(); - if (lastError || done) + if (lastError || done || noProgress) callback(); }, diff --git a/client/modules/polyfill.js b/client/modules/polyfill.js index e3cb5fde..ff0e59c5 100644 --- a/client/modules/polyfill.js +++ b/client/modules/polyfill.js @@ -1,10 +1,20 @@ 'use strict'; -/* global DOM */ require('domtokenlist-shim'); -const scrollIntoViewIfNeeded = require('scroll-into-view-if-needed').default; +const _scrollIntoViewIfNeeded = require('scroll-into-view-if-needed'); -DOM.scrollIntoViewIfNeeded = (el) => scrollIntoViewIfNeeded(el, { - block: 'nearest', -}); +globalThis.DOM = globalThis.DOM || {}; + +const scrollIntoViewIfNeeded = (el, overrides = {}) => { + const { + scroll = _scrollIntoViewIfNeeded, + } = overrides; + + return scroll(el, { + block: 'nearest', + }); +}; + +globalThis.DOM.scrollIntoViewIfNeeded = scrollIntoViewIfNeeded; +module.exports.scrollIntoViewIfNeeded = scrollIntoViewIfNeeded; diff --git a/client/modules/polyfill.spec.js b/client/modules/polyfill.spec.js index ea4524a7..79d9dfda 100644 --- a/client/modules/polyfill.spec.js +++ b/client/modules/polyfill.spec.js @@ -1,33 +1,21 @@ 'use strict'; const {test, stub} = require('supertape'); - -const mockRequire = require('mock-require'); - -const {stopAll} = mockRequire; +const {scrollIntoViewIfNeeded} = require('./polyfill'); test('cloudcmd: client: polyfill: scrollIntoViewIfNeaded', (t) => { - const {DOM} = global; const scroll = stub(); const el = {}; - global.DOM = {}; - - mockRequire('scroll-into-view-if-needed', { - default: scroll, + scrollIntoViewIfNeeded(el, { + scroll, }); - mockRequire.reRequire('./polyfill'); - - global.DOM.scrollIntoViewIfNeeded(el); - mockRequire.stop('scroll-into-view-if-neaded'); - global.DOM = DOM; - - const args = [el, { - block: 'nearest', - }]; - - stopAll(); + const args = [ + el, { + block: 'nearest', + }, + ]; t.calledWith(scroll, args, 'should call scrollIntoViewIfNeaded'); t.end(); diff --git a/client/modules/terminal-run.js b/client/modules/terminal-run.js index 01bd30a9..4dcf24ac 100644 --- a/client/modules/terminal-run.js +++ b/client/modules/terminal-run.js @@ -2,15 +2,15 @@ /* global CloudCmd, gritty */ const {promisify} = require('es6-promisify'); -const tryToCatch = require('try-to-catch'); -const fullstore = require('fullstore'); +const {tryToCatch} = require('try-to-catch'); +const {fullstore} = require('fullstore'); require('../../css/terminal.css'); const exec = require('execon'); const load = require('load.js'); const DOM = require('../dom'); -const Images = require('../dom/images'); +const Images = require('../dom/images.mjs'); const {Dialog} = DOM; const {Key, config} = CloudCmd; @@ -33,7 +33,7 @@ const loadAll = async () => { const [e] = await tryToCatch(load.parallel, [js, css]); if (e) { - const src = e.target.src.replace(window.location.href, ''); + const src = e.target.src.replace(globalThis.location.href, ''); return Dialog.alert(`file ${src} could not be loaded`); } @@ -75,22 +75,18 @@ function hide() { CloudCmd.View.hide(); } -function getPrefix() { - return CloudCmd.prefix + '/gritty'; -} +const getPrefix = () => CloudCmd.prefix + '/gritty'; function getPrefixSocket() { return CloudCmd.prefixSocket + '/gritty'; } -function getEnv() { - return { - ACTIVE_DIR: DOM.getCurrentDirPath, - PASSIVE_DIR: DOM.getNotCurrentDirPath, - CURRENT_NAME: DOM.getCurrentName, - CURRENT_PATH: DOM.getCurrentPath, - }; -} +const getEnv = () => ({ + ACTIVE_DIR: DOM.getCurrentDirPath, + PASSIVE_DIR: DOM.getNotCurrentDirPath, + CURRENT_NAME: DOM.getCurrentName, + CURRENT_PATH: DOM.getCurrentPath, +}); function create(createOptions) { const { diff --git a/client/modules/terminal.js b/client/modules/terminal.js index f94024d4..6a355560 100644 --- a/client/modules/terminal.js +++ b/client/modules/terminal.js @@ -2,14 +2,14 @@ /* global CloudCmd */ /* global gritty */ -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); require('../../css/terminal.css'); const exec = require('execon'); const load = require('load.js'); const DOM = require('../dom'); -const Images = require('../dom/images'); +const Images = require('../dom/images.mjs'); const loadParallel = load.parallel; @@ -32,7 +32,7 @@ const loadAll = async () => { const [e] = await tryToCatch(loadParallel, [js, css]); if (e) { - const src = e.target.src.replace(window.location.href, ''); + const src = e.target.src.replace(globalThis.location.href, ''); return Dialog.alert(`file ${src} could not be loaded`); } @@ -57,22 +57,18 @@ function hide() { CloudCmd.View.hide(); } -function getPrefix() { - return CloudCmd.prefix + '/gritty'; -} +const getPrefix = () => CloudCmd.prefix + '/gritty'; function getPrefixSocket() { return CloudCmd.prefixSocket + '/gritty'; } -function getEnv() { - return { - ACTIVE_DIR: DOM.getCurrentDirPath, - PASSIVE_DIR: DOM.getNotCurrentDirPath, - CURRENT_NAME: DOM.getCurrentName, - CURRENT_PATH: DOM.getCurrentPath, - }; -} +const getEnv = () => ({ + ACTIVE_DIR: DOM.getCurrentDirPath, + PASSIVE_DIR: DOM.getNotCurrentDirPath, + CURRENT_NAME: DOM.getCurrentName, + CURRENT_PATH: DOM.getCurrentPath, +}); function create() { const options = { diff --git a/client/modules/upload.js b/client/modules/upload.js index 427e74cb..23fbdaf5 100644 --- a/client/modules/upload.js +++ b/client/modules/upload.js @@ -4,10 +4,10 @@ CloudCmd.Upload = exports; -const Files = require('../dom/files'); -const Images = require('../dom/images'); -const uploadFiles = require('../dom/upload-files'); const createElement = require('@cloudcmd/create-element'); +const Files = require('../dom/files'); +const Images = require('../dom/images.mjs'); +const uploadFiles = require('../dom/upload-files'); module.exports.init = async () => { Images.show.load('top'); diff --git a/client/modules/user-menu/index.js b/client/modules/user-menu/index.js index 47fbee39..1dba5d23 100644 --- a/client/modules/user-menu/index.js +++ b/client/modules/user-menu/index.js @@ -5,14 +5,14 @@ require('../../../css/user-menu.css'); const currify = require('currify'); const wraptile = require('wraptile'); -const fullstore = require('fullstore'); +const {fullstore} = require('fullstore'); const load = require('load.js'); const createElement = require('@cloudcmd/create-element'); -const tryCatch = require('try-catch'); -const tryToCatch = require('try-to-catch'); +const {tryCatch} = require('try-catch'); +const {tryToCatch} = require('try-to-catch'); const {codeFrameColumns} = require('@babel/code-frame'); -const Images = require('../../dom/images'); +const Images = require('../../dom/images.mjs'); const Dialog = require('../../dom/dialog'); const getUserMenu = require('./get-user-menu'); const navigate = require('./navigate'); diff --git a/client/modules/user-menu/navigate.js b/client/modules/user-menu/navigate.js index 445d47d1..a87801ed 100644 --- a/client/modules/user-menu/navigate.js +++ b/client/modules/user-menu/navigate.js @@ -1,6 +1,6 @@ 'use strict'; -const fullstore = require('fullstore'); +const {fullstore} = require('fullstore'); const { J, diff --git a/client/modules/view/get-type.js b/client/modules/view/get-type.js index 80252aa1..9fc1df1d 100644 --- a/client/modules/view/get-type.js +++ b/client/modules/view/get-type.js @@ -5,7 +5,7 @@ const testRegExp = currify((name, reg) => reg.test(name)); const getRegExp = (ext) => RegExp(`\\.${ext}$`, 'i'); const isPDF = (a) => /\.pdf$/i.test(a); -const isHTML = (a) => /\.html$/.test(a); +const isHTML = (a) => a.endsWith('.html'); const isMarkdown = (a) => /.\.md$/.test(a); module.exports = (name) => { @@ -45,9 +45,7 @@ function isMedia(name) { return isAudio(name) || isVideo(name); } -function isAudio(name) { - return /\.(mp3|ogg|m4a)$/i.test(name); -} +const isAudio = (name) => /\.(mp3|ogg|m4a)$/i.test(name); function isVideo(name) { return /\.(mp4|avi|webm)$/i.test(name); diff --git a/client/modules/view/index.js b/client/modules/view/index.js index 0e79ab4e..89d68ee4 100644 --- a/client/modules/view/index.js +++ b/client/modules/view/index.js @@ -2,19 +2,22 @@ 'use strict'; +const CloudCmd = globalThis.CloudCmd || {}; +const DOM = globalThis.DOM || {}; + require('../../../css/view.css'); const rendy = require('rendy'); const currify = require('currify'); const wraptile = require('wraptile'); -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); const load = require('load.js'); -const modal = require('@cloudcmd/modal'); -const createElement = require('@cloudcmd/create-element'); +const _modal = require('@cloudcmd/modal'); +const _createElement = require('@cloudcmd/create-element'); const {time} = require('../../../common/util'); -const {FS} = require('../../../common/cloudfunc'); +const {FS} = require('../../../common/cloudfunc.mjs'); const { isImage, @@ -23,8 +26,8 @@ const { } = require('./types'); const Files = require('../../dom/files'); -const Events = require('../../dom/events'); -const Images = require('../../dom/images'); +const Events = require('#dom/events'); +const Images = require('../../dom/images.mjs'); const {encode} = require('../../../common/entity'); const isString = (a) => typeof a === 'string'; @@ -55,7 +58,10 @@ CloudCmd[Name] = module.exports; const Info = DOM.CurrentInfo; const {Key} = CloudCmd; -const basename = (a) => a.split('/').pop(); + +const basename = (a) => a + .split('/') + .pop(); let El; let TemplateAudio; @@ -110,7 +116,7 @@ async function show(data, options = {}) { if (!options || options.bindKeys !== false) Events.addKey(listener); - El = createElement('div', { + El = _createElement('div', { className: 'view', notAppend: true, }); @@ -123,7 +129,7 @@ async function show(data, options = {}) { else El.append(data); - modal.open(El, initConfig(options)); + _modal.open(El, initConfig(options)); return; } @@ -154,7 +160,11 @@ async function show(data, options = {}) { } module.exports._createIframe = createIframe; -function createIframe(src) { +function createIframe(src, overrides = {}) { + const { + createElement = _createElement, + } = overrides; + const element = createElement('iframe', { src, width: '100%', @@ -169,7 +179,8 @@ function createIframe(src) { } module.exports._viewHtml = viewHtml; -function viewHtml(src) { +function viewHtml(src, overrides = {}) { + const {modal = _modal} = overrides; modal.open(createIframe(src), Config); } @@ -181,7 +192,7 @@ function viewPDF(src) { if (CloudCmd.config('showFileName')) options.title = Info.name; - modal.open(element, options); + _modal.open(element, options); } async function viewMedia(path) { @@ -202,7 +213,7 @@ async function viewMedia(path) { }, }; - modal.open(element, allConfig); + _modal.open(element, allConfig); } async function viewFile() { @@ -218,7 +229,7 @@ async function viewFile() { options.title = Info.name; El.append(element); - modal.open(El, options); + _modal.open(El, options); } const copy = (a) => assign({}, a); @@ -250,7 +261,7 @@ function initConfig(options) { } function hide() { - modal.close(); + _modal.close(); } function viewImage(path, prefixURL) { @@ -260,8 +271,7 @@ function viewImage(path, prefixURL) { title: encode(basename(path)), }); - const names = Info - .files + const names = Info.files .map(DOM.getCurrentPath) .filter(isSupportedImage); @@ -284,7 +294,7 @@ function viewImage(path, prefixURL) { ...imageConfig, }; - modal.open(titles, config); + _modal.open(titles, config); } async function getMediaElement(src) { @@ -309,7 +319,7 @@ async function getMediaElement(src) { name, }); - const element = createElement('div', { + const element = _createElement('div', { innerHTML, }); @@ -321,10 +331,6 @@ function check(src) { throw Error('src should be a string!'); } -/** - * function loads css and js of FancyBox - * @callback - executes, when everything loaded - */ async function loadAll() { const {DIR_DIST} = CloudCmd; diff --git a/client/modules/view/index.spec.js b/client/modules/view/index.spec.js index 83e6b476..cc46d07d 100644 --- a/client/modules/view/index.spec.js +++ b/client/modules/view/index.spec.js @@ -3,22 +3,20 @@ require('css-modules-require-hook/preset'); const autoGlobals = require('auto-globals'); -const stub = require('@cloudcmd/stub'); -const mockRequire = require('mock-require'); +const {stub} = require('@cloudcmd/stub'); + const test = autoGlobals(require('supertape')); -const {reRequire, stopAll} = mockRequire; +const { + _initConfig, + _viewHtml, + _Config, + _createIframe, +} = require('.'); test('cloudcmd: client: view: initConfig', (t) => { let config; let i = 0; - const {CloudCmd, DOM} = global; - - global.CloudCmd = {}; - global.DOM = {}; - - const {_initConfig} = reRequire('.'); - const afterClose = () => ++i; const options = { afterClose, @@ -30,54 +28,32 @@ test('cloudcmd: client: view: initConfig', (t) => { config = _initConfig(options); config.afterClose(); - global.CloudCmd = CloudCmd; - global.DOM = DOM; - t.equal(i, 2, 'should not change default config'); t.end(); }); test('cloudcmd: client: view: initConfig: no options', (t) => { - const {CloudCmd, DOM} = global; - - global.CloudCmd = {}; - global.DOM = {}; - - const {_initConfig} = reRequire('.'); const config = _initConfig(); - global.CloudCmd = CloudCmd; - global.DOM = DOM; - t.equal(typeof config, 'object'); t.end(); }); test('cloudcmd: client: view: html', (t) => { - const {CloudCmd, DOM} = global; - - global.CloudCmd = {}; - global.DOM = {}; const open = stub(); - - mockRequire('@cloudcmd/modal', { + const modal = { open, - }); - - const {_viewHtml, _Config} = reRequire('.'); + }; const src = '/hello.html'; - _viewHtml(src); - - global.CloudCmd = CloudCmd; - global.DOM = DOM; + _viewHtml(src, { + modal, + }); const [first] = open.args; const [arg] = first; - stopAll(); - t.deepEqual(first, [arg, _Config]); t.end(); }); @@ -89,12 +65,11 @@ test('cloudcmd: client: view: createIframe', (t) => { }; const createElement = stub().returns(el); - - mockRequire('@cloudcmd/create-element', createElement); - const {_createIframe} = reRequire('.'); - const src = '/hello.html'; - _createIframe(src); + + _createIframe(src, { + createElement, + }); const expected = { src, @@ -102,8 +77,6 @@ test('cloudcmd: client: view: createIframe', (t) => { width: '100%', }; - stopAll(); - t.calledWith(createElement, ['iframe', expected]); t.end(); }); @@ -116,13 +89,10 @@ test('cloudcmd: client: view: createIframe: returns', (t) => { const createElement = stub().returns(el); - mockRequire('@cloudcmd/create-element', createElement); - const {_createIframe} = reRequire('.'); - const src = '/hello.html'; - const result = _createIframe(src); - - stopAll(); + const result = _createIframe(src, { + createElement, + }); t.equal(result, el); t.end(); diff --git a/client/modules/view/types.js b/client/modules/view/types.js index 0ea9aef9..d94dd971 100644 --- a/client/modules/view/types.js +++ b/client/modules/view/types.js @@ -2,11 +2,12 @@ const {extname} = require('node:path'); const currify = require('currify'); +const isAudio = (name) => /\.(mp3|ogg|m4a)$/i.test(name); const testRegExp = currify((name, reg) => reg.test(name)); const getRegExp = (ext) => RegExp(`\\.${ext}$`, 'i'); const isPDF = (a) => /\.pdf$/i.test(a); -const isHTML = (a) => /\.html$/.test(a); +const isHTML = (a) => a.endsWith('.html'); const isMarkdown = (a) => /.\.md$/.test(a); module.exports.getType = async (path) => { @@ -53,9 +54,6 @@ function isMedia(name) { } module.exports.isAudio = isAudio; -function isAudio(name) { - return /\.(mp3|ogg|m4a)$/i.test(name); -} function isVideo(name) { return /\.(mp4|avi|webm)$/i.test(name); diff --git a/client/modules/view/types.spec.js b/client/modules/view/types.spec.js index 52ed5bad..d8004f9e 100644 --- a/client/modules/view/types.spec.js +++ b/client/modules/view/types.spec.js @@ -22,12 +22,9 @@ test('cloudcmd: client: view: types: detectType', async (t) => { headers: [], }); - const originalFetch = global.fetch; - - global.fetch = fetch; + globalThis.fetch = fetch; await _detectType('/hello'); - global.fetch = originalFetch; const expected = ['/hello', { method: 'HEAD', }]; @@ -37,17 +34,13 @@ test('cloudcmd: client: view: types: detectType', async (t) => { }); test('cloudcmd: client: view: types: detectType: found', async (t) => { - const originalFetch = global.fetch; - - global.fetch = stub().returns({ + globalThis.fetch = stub().returns({ headers: [ ['content-type', 'image/png'], ], }); const result = await _detectType('/hello'); - global.fetch = originalFetch; - t.equal(result, '.png'); t.end(); }); diff --git a/client/sort.js b/client/sort.js deleted file mode 100644 index fdcfaf65..00000000 --- a/client/sort.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -/* global CloudCmd */ -const DOM = require('./dom'); - -const Info = DOM.CurrentInfo; - -const {sort, order} = CloudCmd; - -const position = DOM.getPanelPosition(); - -let sortPrevious = sort[position]; - -const {getPanel} = DOM; - -CloudCmd.sortPanel = (name, panel = getPanel()) => { - const position = panel.dataset.name.replace('js-', ''); - - if (name !== sortPrevious) - order[position] = 'asc'; - else if (order[position] === 'asc') - order[position] = 'desc'; - else - order[position] = 'asc'; - - sortPrevious = sort[position] = name; - - const noCurrent = position !== Info.panelPosition; - - CloudCmd.refresh({ - panel, - noCurrent, - }); -}; diff --git a/client/sort.mjs b/client/sort.mjs new file mode 100644 index 00000000..73539c81 --- /dev/null +++ b/client/sort.mjs @@ -0,0 +1,36 @@ +/* global CloudCmd */ +import {fullstore} from 'fullstore'; +import DOM from './dom/index.js'; + +const sortPrevious = fullstore(); + +const {getPanel} = DOM; + +export const initSortPanel = () => { + const {sort} = CloudCmd; + const position = DOM.getPanelPosition(); + + sortPrevious(sort[position]); +}; + +export const sortPanel = (name, panel = getPanel()) => { + const {sort, order} = CloudCmd; + const Info = DOM.CurrentInfo; + const position = panel.dataset.name.replace('js-', ''); + + if (name !== sortPrevious()) + order[position] = 'asc'; + else if (order[position] === 'asc') + order[position] = 'desc'; + else + order[position] = 'asc'; + + sortPrevious(name); + sort[position] = name; + const noCurrent = position !== Info.panelPosition; + + CloudCmd.refresh({ + panel, + noCurrent, + }); +}; diff --git a/client/sw/register.js b/client/sw/register.js index 12431a82..0e6db472 100644 --- a/client/sw/register.js +++ b/client/sw/register.js @@ -1,6 +1,6 @@ 'use strict'; -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); module.exports.registerSW = registerSW; module.exports.unregisterSW = unregisterSW; diff --git a/client/sw/register.spec.js b/client/sw/register.spec.js index c4368c94..9b42a0e6 100644 --- a/client/sw/register.spec.js +++ b/client/sw/register.spec.js @@ -3,14 +3,18 @@ const autoGlobals = require('auto-globals'); const tape = require('supertape'); -const stub = require('@cloudcmd/stub'); +const {stub} = require('@cloudcmd/stub'); + +const {tryCatch} = require('try-catch'); +const { + listenSW, + registerSW, + unregisterSW, +} = require('./register'); -const tryCatch = require('try-catch'); -const {reRequire} = require('mock-require'); const test = autoGlobals(tape); test('sw: listen', (t) => { - const {listenSW} = reRequire('./register'); const addEventListener = stub(); const sw = { addEventListener, @@ -23,7 +27,6 @@ test('sw: listen', (t) => { }); test('sw: lesten: no sw', (t) => { - const {listenSW} = reRequire('./register'); const [e] = tryCatch(listenSW, null, 'hello', 'world'); t.notOk(e, 'should not throw'); @@ -31,8 +34,6 @@ test('sw: lesten: no sw', (t) => { }); test('sw: register: registerSW: no serviceWorker', async (t, {navigator}) => { - const {registerSW} = reRequire('./register'); - delete navigator.serviceWorker; await registerSW(); @@ -46,8 +47,6 @@ test('sw: register: registerSW: no https', async (t, {location, navigator}) => { location.protocol = 'http:'; - const {registerSW} = reRequire('./register'); - await registerSW(); t.notCalled(register, 'should not call register'); @@ -62,8 +61,6 @@ test('sw: register: registerSW: http', async (t, {location, navigator}) => { const {register} = navigator.serviceWorker; - const {registerSW} = reRequire('./register'); - await registerSW(); t.notCalled(register, 'should not call register'); @@ -79,8 +76,6 @@ test('sw: register: registerSW: https self-signed', async (t, {location, navigat const {register} = navigator.serviceWorker; register.throws(Error('Cannot register service worker!')); - const {registerSW} = reRequire('./register'); - const result = await registerSW(); t.notOk(result, 'should not throw'); @@ -91,8 +86,6 @@ test('sw: register: registerSW', async (t, {location, navigator}) => { location.hostname = 'localhost'; const {register} = navigator.serviceWorker; - const {registerSW} = reRequire('./register'); - await registerSW('/hello'); t.calledWith(register, ['/hello/sw.js'], 'should call register'); @@ -107,8 +100,6 @@ test('sw: register: unregisterSW', async (t, {location, navigator}) => { register.returns(serviceWorker); - const {unregisterSW} = reRequire('./register'); - await unregisterSW('/hello'); t.calledWith(register, ['/hello/sw.js'], 'should call register'); diff --git a/client/sw/sw.js b/client/sw/sw.js index 15f3e0ad..e4cad253 100644 --- a/client/sw/sw.js +++ b/client/sw/sw.js @@ -2,7 +2,7 @@ const process = require('node:process'); const codegen = require('codegen.macro'); -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); const currify = require('currify'); const isDev = process.env.NODE_ENV === 'development'; @@ -50,14 +50,14 @@ const getRequest = (a, request) => { return createRequest('/'); }; -self.addEventListener('install', wait(onInstall)); -self.addEventListener('fetch', respondWith(onFetch)); -self.addEventListener('activate', wait(onActivate)); +globalThis.addEventListener('install', wait(onInstall)); +globalThis.addEventListener('fetch', respondWith(onFetch)); +globalThis.addEventListener('activate', wait(onActivate)); async function onActivate() { console.info(`cloudcmd: sw: activate: ${NAME}`); - await self.clients.claim(); + await globalThis.clients.claim(); const keys = await caches.keys(); const deleteCache = caches.delete.bind(caches); @@ -67,7 +67,7 @@ async function onActivate() { async function onInstall() { console.info(`cloudcmd: sw: install: ${NAME}`); - await self.skipWaiting(); + await globalThis.skipWaiting(); } async function onFetch(event) { diff --git a/common/base64.js b/common/base64.js deleted file mode 100644 index c1a82c17..00000000 --- a/common/base64.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -module.exports.btoa = (str) => { - if (typeof btoa === 'function') - return btoa(str); - - return Buffer - .from(str) - .toString('base64'); -}; - -module.exports.atob = (str) => { - if (typeof atob === 'function') - return atob(str); - - return Buffer - .from(str, 'base64') - .toString('binary'); -}; diff --git a/common/base64.spec.js b/common/base64.spec.js deleted file mode 100644 index bc3ea2f5..00000000 --- a/common/base64.spec.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const {test, stub} = require('supertape'); - -const {btoa, atob} = require('./base64'); - -test('btoa: browser', (t) => { - const btoaOriginal = global.btoa; - const str = 'hello'; - - global.btoa = stub(); - - btoa(str); - - t.calledWith(global.btoa, [str], 'should call global.btoa'); - t.end(); - - global.btoa = btoaOriginal; -}); - -test('btoa: node', (t) => { - const str = 'hello'; - const expected = 'aGVsbG8='; - - const result = btoa(str); - - t.equal(result, expected, 'should encode base64'); - t.end(); -}); - -test('atob: browser', (t) => { - const atobOriginal = global.atob; - const str = 'hello'; - - global.atob = stub(); - - atob(str); - - t.calledWith(global.atob, [str], 'should call global.btoa'); - t.end(); - - global.atob = atobOriginal; -}); - -test('atob: node', (t) => { - const str = 'aGVsbG8='; - const expected = 'hello'; - - const result = atob(str); - - t.equal(result, expected, 'should encode base64'); - t.end(); -}); diff --git a/common/callbackify.spec.js b/common/callbackify.spec.js index 56d557f9..0a46d9e9 100644 --- a/common/callbackify.spec.js +++ b/common/callbackify.spec.js @@ -1,11 +1,11 @@ 'use strict'; -const tryToCatch = require('try-to-catch'); +const {promisify} = require('node:util'); +const {tryToCatch} = require('try-to-catch'); const {test, stub} = require('supertape'); const callbackify = require('./callbackify'); -const {promisify} = require('node:util'); test('cloudcmd: common: callbackify: error', async (t) => { const promise = stub().rejects(Error('hello')); diff --git a/common/cloudfunc.js b/common/cloudfunc.mjs similarity index 84% rename from common/cloudfunc.js rename to common/cloudfunc.mjs index 39e422e7..00a48360 100644 --- a/common/cloudfunc.js +++ b/common/cloudfunc.mjs @@ -1,29 +1,31 @@ -'use strict'; +import rendy from 'rendy'; +import currify from 'currify'; +import store from 'fullstore'; +import {encode} from './entity.js'; -const rendy = require('rendy'); -const currify = require('currify'); -const store = require('fullstore'); -const {encode} = require('./entity'); -const {btoa} = require('./base64'); - -const getHeaderField = currify(_getHeaderField); +export const getHeaderField = currify(_getHeaderField); /* КОНСТАНТЫ (общие для клиента и сервера)*/ /* название программы */ const NAME = 'Cloud Commander'; -const FS = '/fs'; + +export const FS = '/fs'; + const Path = store(); Path('/'); -module.exports.FS = FS; -module.exports.apiURL = '/api/v1'; -module.exports.MAX_FILE_SIZE = 500 * 1024; -module.exports.getHeaderField = getHeaderField; -module.exports.getPathLink = getPathLink; -module.exports.getDotDot = getDotDot; +const filterOutDotFiles = ({showDotFiles}) => ({name}) => { + if (showDotFiles) + return true; + + return !name.startsWith('.'); +}; -module.exports.formatMsg = (msg, name, status) => { +export const apiURL = '/api/v1'; +export const MAX_FILE_SIZE = 500 * 1024; + +export const formatMsg = (msg, name, status) => { status = status || 'ok'; name = name || ''; @@ -37,7 +39,7 @@ module.exports.formatMsg = (msg, name, status) => { * Функция возвращает заголовок веб страницы * @path */ -module.exports.getTitle = (options) => { +export const getTitle = (options) => { options = options || {}; const {path = Path(), name} = options; @@ -56,7 +58,7 @@ module.exports.getTitle = (options) => { * возвращаеться массив каталогов * @param url - адрес каталога */ -function getPathLink(url, prefix, template) { +export function getPathLink(url, prefix, template) { if (!url) throw Error('url could not be empty!'); @@ -102,22 +104,23 @@ function getPathLink(url, prefix, template) { return lines.join(''); } -const getDataName = (name) => { +export function _getDataName(name) { const encoded = btoa(encodeURI(name)); return `data-name="js-file-${encoded}" `; -}; +} /** * Функция строит таблицу файлв из JSON-информации о файлах * @param params - информация о файлах * */ -module.exports.buildFromJSON = (params) => { +export const buildFromJSON = (params) => { const { prefix, template, sort = 'name', order = 'asc', + showDotFiles, } = params; const templateFile = template.file; @@ -177,7 +180,7 @@ module.exports.buildFromJSON = (params) => { name: '..', }); - const dataName = getDataName('..'); + const dataName = _getDataName('..'); const attribute = `draggable="true" ${dataName}`; /* Сохраняем путь к каталогу верхнего уровня*/ @@ -195,6 +198,9 @@ module.exports.buildFromJSON = (params) => { } fileTable += files + .filter(filterOutDotFiles({ + showDotFiles, + })) .map(updateField) .map((file) => { const name = encode(file.name); @@ -215,7 +221,7 @@ module.exports.buildFromJSON = (params) => { attribute: getAttribute(file.type), }); - const dataName = getDataName(file.name); + const dataName = _getDataName(file.name); const attribute = `draggable="true" ${dataName}`; return rendy(templateFile, { @@ -237,14 +243,12 @@ module.exports.buildFromJSON = (params) => { return fileTable; }; -function updateField(file) { - return { - ...file, - date: file.date || '--.--.----', - owner: file.owner || 'root', - size: getSize(file), - }; -} +const updateField = (file) => ({ + ...file, + date: file.date || '--.--.----', + owner: file.owner || 'root', + size: getSize(file), +}); function getAttribute(type) { if (type === 'directory') @@ -253,7 +257,8 @@ function getAttribute(type) { return 'target="_blank" '; } -module.exports._getSize = getSize; +export const _getSize = getSize; + function getSize({size, type}) { if (type === 'directory') return '<dir>'; @@ -276,7 +281,7 @@ function _getHeaderField(sort, order, name) { return `${name}${arrow}`; } -function getDotDot(path) { +export function getDotDot(path) { // убираем последний слеш и каталог в котором мы сейчас находимся const lastSlash = path.substr(path, path.lastIndexOf('/')); const dotDot = lastSlash.substr(lastSlash, lastSlash.lastIndexOf('/')); diff --git a/common/cloudfunc.spec.js b/common/cloudfunc.spec.mjs similarity index 71% rename from common/cloudfunc.spec.js rename to common/cloudfunc.spec.mjs index b5aceb7b..79ddfedf 100644 --- a/common/cloudfunc.spec.js +++ b/common/cloudfunc.spec.mjs @@ -1,19 +1,15 @@ -'use strict'; - -const {join} = require('node:path'); -const {readFileSync} = require('node:fs'); - -const test = require('supertape'); -const montag = require('montag'); -const cheerio = require('cheerio'); - -const { +import {readFileSync} from 'node:fs'; +import test from 'supertape'; +import montag from 'montag'; +import * as cheerio from 'cheerio'; +import { _getSize, getPathLink, buildFromJSON, -} = require('./cloudfunc'); + _getDataName, +} from './cloudfunc.mjs'; -const templatePath = join(__dirname, '../tmpl/fs'); +const templatePath = new URL('../tmpl/fs', import.meta.url).pathname; const template = { pathLink: readFileSync(`${templatePath}/pathLink.hbs`, 'utf8'), @@ -142,3 +138,44 @@ test('cloudfunc: getSize: file', (t) => { t.equal(result, expected); t.end(); }); + +test('cloudfunc: buildFromJSON: showDotFiles: false', (t) => { + const data = { + path: '/media/', + files: [{ + date: '30.08.2016', + mode: 'rwx rwx rwx', + name: '.floppy', + owner: 'root', + size: '7b', + type: 'directory-link', + }], + }; + + const html = buildFromJSON({ + prefix: '', + template, + data, + showDotFiles: false, + }); + + const $ = cheerio.load(html); + const el = $('[data-name="js-file-LmZsb3BweQ=="]'); + + const result = el + .find('[data-name="js-name"]') + .text(); + + const expected = ''; + + t.equal(result, expected); + t.end(); +}); + +test('cloudfunc: _getDataName', (t) => { + const result = _getDataName('s'); + const expected = 'data-name="js-file-cw==" '; + + t.equal(result, expected); + t.end(); +}); diff --git a/common/datetime.spec.js b/common/datetime.spec.js index db7a2483..cc321d8a 100644 --- a/common/datetime.spec.js +++ b/common/datetime.spec.js @@ -1,7 +1,7 @@ 'use strict'; const test = require('supertape'); -const tryCatch = require('try-catch'); +const {tryCatch} = require('try-catch'); const datetime = require('./datetime'); @@ -16,11 +16,11 @@ test('common: datetime', (t) => { }); test('common: datetime: no arg', (t) => { - const {Date} = global; + const {Date} = globalThis; let called = false; - global.Date = class extends Date { + globalThis.Date = class extends Date { constructor() { super(); called = true; @@ -29,7 +29,7 @@ test('common: datetime: no arg', (t) => { datetime(); - global.Date = Date; + globalThis.Date = Date; t.ok(called, 'should call new Date'); t.end(); diff --git a/common/try-to-promise-all.js b/common/try-to-promise-all.js index 5c91b026..e839d27e 100644 --- a/common/try-to-promise-all.js +++ b/common/try-to-promise-all.js @@ -1,6 +1,6 @@ 'use strict'; -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); const all = Promise.all.bind(Promise); module.exports = async (a) => { diff --git a/common/util.spec.js b/common/util.spec.js index ed15bd09..f62cb4ca 100644 --- a/common/util.spec.js +++ b/common/util.spec.js @@ -1,8 +1,7 @@ 'use strict'; const test = require('supertape'); -const {reRequire} = require('mock-require'); -const tryCatch = require('try-catch'); +const {tryCatch} = require('try-catch'); const Util = require('./util'); const { @@ -117,15 +116,3 @@ test('util: escapeRegExp', (t) => { t.equal(escapeRegExp('#hello'), '\\#hello'); t.end(); }); - -test('util: scope', (t) => { - global.window = {}; - - reRequire('./util'); - - t.pass('should set window in scope'); - - delete global.window; - - t.end(); -}); diff --git a/css/icons.css b/css/icons.css index bd749fe1..ca4a1181 100644 --- a/css/icons.css +++ b/css/icons.css @@ -83,6 +83,11 @@ content: '\e811 '; } +.icon-toggle-file-selection::before { + font-family: 'Fontello'; + content: '\e81f '; +} + .icon-unselect-all::before { font-family: 'Fontello'; content: '\e812 '; diff --git a/css/main.css b/css/main.css index b30c0ed3..6b901a8e 100644 --- a/css/main.css +++ b/css/main.css @@ -1,5 +1,5 @@ -@import url(./urls.css); @import url(./reset.css); +@import url(./urls.css); @import url(./style.css); @import url(./icons.css); @import url(./help.css); diff --git a/css/query.css b/css/query.css index 6b7c98a4..b5843b85 100644 --- a/css/query.css +++ b/css/query.css @@ -20,6 +20,11 @@ } } +:root { + --min-one-panel-width: 1155px; + --is-mobile: 0; +} + @media only screen and (height <= 900px) and (width <= 600px) { .fm { height: 85%; @@ -52,6 +57,12 @@ } } +@media only screen and (width <= 600px) { + :root { + --is-mobile: 1; + } +} + @media only screen and (height <= 550px) and (width <= 600px) { .fm { height: 65%; @@ -129,15 +140,14 @@ color: white; } - .file::before, - .file-link::before { + .file::before { color: rgb(26 224 124 / 56%); content: '\e80d'; } - .current-file .file::before, .file-link::before { - color: white; + color: rgb(26 224 124 / 56%); + content: '\e81d'; } /* меняем иконки на шрифтовые */ @@ -160,11 +170,14 @@ display: inline-block; } - .directory::before, - .directory-link::before { + .directory::before { content: '\e807'; } + .directory-link::before { + content: '\e81e'; + } + .file, .file-link { background-image: none; diff --git a/css/style.css b/css/style.css index 5a1f04b7..cd7122f7 100644 --- a/css/style.css +++ b/css/style.css @@ -187,6 +187,7 @@ a:active { } .selected-file, +.selected-file > span, .selected-file .name > a { color: rgb(254 159 224); } diff --git a/css/supports.css b/css/supports.css index bf470c2d..ea2084d2 100644 --- a/css/supports.css +++ b/css/supports.css @@ -1,6 +1,6 @@ @supports (overflow: overlay) { .files { - overflow-y: overlay; + overflow-y: auto; } .fm-header { diff --git a/cssnano.config.js b/cssnano.config.mjs similarity index 63% rename from cssnano.config.js rename to cssnano.config.mjs index 91ae5f81..44abaeaf 100644 --- a/cssnano.config.js +++ b/cssnano.config.mjs @@ -1,9 +1,7 @@ -'use strict'; - // used by OptimizeCssAssetsPlugin -const defaultPreset = require('cssnano-preset-default'); +import defaultPreset from 'cssnano-preset-default'; -module.exports = defaultPreset({ +export default defaultPreset({ svgo: { plugins: [{ convertPathData: false, diff --git a/deno.json b/deno.json new file mode 100644 index 00000000..64c1bde2 --- /dev/null +++ b/deno.json @@ -0,0 +1,14 @@ +{ + "tasks": { + "start": "deno run -P=cloudcmd bin/cloudcmd.mjs" + }, + "permissions": { + "cloudcmd": { + "env": true, + "read": true, + "sys": true, + "net": true, + "run": true + } + } +} diff --git a/docker/Dockerfile b/docker/Dockerfile index 701a4199..39312e00 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,23 +1,24 @@ -FROM node:lts-buster +FROM node:lts-bookworm LABEL maintainer="Coderaiser" +LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" RUN mkdir -p /usr/src/app WORKDIR /usr/src/app COPY package.json /usr/src/app/ -RUN npm config set package-lock false && \ - npm install --production && \ - npm i gritty && \ - npm cache clean --force +RUN curl -fsSL https://bun.com/install | bash && \ + ~/.bun/bin/bun i --production --no-save && \ + PATH=$PATH:~/.bun/bin bun i gritty --no-save && \ + ~/.bun/bin/bun pm cache rm COPY . /usr/src/app WORKDIR / -ENV cloudcmd_terminal true -ENV cloudcmd_terminal_path gritty -ENV cloudcmd_open false +ENV cloudcmd_terminal=true +ENV cloudcmd_terminal_path=gritty +ENV cloudcmd_open=false EXPOSE 8000 diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 5a1e9214..b8cc6e3e 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -1,17 +1,18 @@ FROM node:alpine LABEL maintainer="Coderaiser" +LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" RUN mkdir -p /usr/src/app WORKDIR /usr/src/app COPY package.json /usr/src/app/ -RUN npm config set package-lock false && \ - npm install --production && \ - apk update && \ - apk add --no-cache bash make g++ python3 && \ - npm i gritty && \ - npm cache clean --force && \ +RUN apk update && \ + apk add --no-cache curl bash make g++ python3 && \ + curl -fsSL https://bun.com/install | bash && \ + ~/.bun/bin/bun i --production --no-save && \ + PATH=$PATH:~/.bun/bin bun i gritty --no-save && \ + ~/.bun/bin/bun pm cache rm && \ apk del make g++ python3 && \ rm -rf /usr/include /tmp/* /var/cache/apk/* diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..cf7a6a2d --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,40 @@ +import {safeAlign} from 'eslint-plugin-putout'; +import {defineConfig} from 'eslint/config'; +import n from 'eslint-plugin-n'; +import globals from 'globals'; +import {matchToFlat} from '@putout/eslint-flat'; + +export const match = { + 'bin/release.mjs': { + 'no-console': 'off', + 'n/hashbang': 'off', + }, + 'client/dom/index.js': { + 'no-multi-spaces': 'off', + }, + '{client,static}/**/*.js': { + 'n/no-extraneous-require': 'off', + 'n/no-unsupported-features/node-builtins': 'off', + }, + 'bin/cloudcmd.js': { + 'no-console': 'off', + }, +}; +export default defineConfig([ + safeAlign, { + ignores: ['**/fixture'], + rules: { + 'key-spacing': 'off', + 'n/prefer-node-protocol': 'error', + }, + plugins: { + n, + }, + }, { + files: ['{client,common,static}/**/*.js'], + languageOptions: { + globals: globals.browser, + }, + }, + ...matchToFlat(match), +]); diff --git a/font/fontello.eot b/font/fontello.eot index c6b5875a..e144bd82 100644 Binary files a/font/fontello.eot and b/font/fontello.eot differ diff --git a/font/fontello.json b/font/fontello.json index 354b93cc..c8bad6be 100644 --- a/font/fontello.json +++ b/font/fontello.json @@ -180,6 +180,24 @@ "code": 59420, "src": "fontawesome" }, + { + "uid": "e15f0d620a7897e2035c18c80142f6d9", + "css": "link-ext", + "code": 59421, + "src": "fontawesome" + }, + { + "uid": "e35de5ea31cd56970498e33efbcb8e36", + "css": "link-ext-alt", + "code": 59422, + "src": "fontawesome" + }, + { + "uid": "12f4ece88e46abd864e40b35e05b11cd", + "css": "ok", + "code": 59423, + "src": "fontawesome" + }, { "uid": "60617c8adc1e7eb3c444a5491dd13f57", "css": "attention-circled-1", diff --git a/font/fontello.svg b/font/fontello.svg index 4564f981..ff664681 100644 --- a/font/fontello.svg +++ b/font/fontello.svg @@ -1,7 +1,7 @@ -Copyright (C) 2021 by original authors @ fontello.com +Copyright (C) 2024 by original authors @ fontello.com @@ -64,7 +64,11 @@ - + + + + + - \ No newline at end of file + diff --git a/font/fontello.ttf b/font/fontello.ttf index 4fc26184..bfec5c43 100644 Binary files a/font/fontello.ttf and b/font/fontello.ttf differ diff --git a/font/fontello.woff b/font/fontello.woff index 16fa9d71..a23a854d 100644 Binary files a/font/fontello.woff and b/font/fontello.woff differ diff --git a/font/fontello.woff2 b/font/fontello.woff2 index 8b7b62f0..f0210ad6 100644 Binary files a/font/fontello.woff2 and b/font/fontello.woff2 differ diff --git a/html/index.html b/html/index.html index 51fa8893..0cfc73c5 100644 --- a/html/index.html +++ b/html/index.html @@ -8,7 +8,8 @@ - + + @@ -20,7 +21,7 @@ -
{{ fm }}
+
{{ fm }}
@@ -41,5 +42,15 @@ + diff --git a/img/favicon/favicon-256.png b/img/favicon/favicon-256.png new file mode 100644 index 00000000..4e01d43c Binary files /dev/null and b/img/favicon/favicon-256.png differ diff --git a/json/config.json b/json/config.json index fd66580f..51bd05ee 100644 --- a/json/config.json +++ b/json/config.json @@ -5,6 +5,7 @@ "password": "2b64f2e3f9fee1942af9ff60d40aa5a719db33b8ba8dd4864bb4f11e25ca2bee00907de32a59429602336cac832c8f2eeff5177cc14c864dd116c8bf6ca5d9a9", "algo": "sha512WithRSAEncryption", "editor": "edward", + "menu": "aleman", "packer": "tar", "diff": true, "zip": true, @@ -30,6 +31,7 @@ "terminalPath": "", "terminalCommand": "", "terminalAutoRestart": true, + "showDotFiles": true, "showConfig": false, "showFileName": false, "vim": false, diff --git a/json/help.json b/json/help.json index 056713ff..f8ff0bf8 100644 --- a/json/help.json +++ b/json/help.json @@ -8,6 +8,7 @@ "-p, --password ": "set password", "-c, --config ": "configuration file path", "--show-config ": "show config values", + "--show-dot-files ": "show dot files", "--show-file-name ": "show file name in view and edit", "--editor ": "set editor: \"dword\", \"edward\" or \"deepword\"", "--packer ": "set packer: \"tar\" or \"zip\"", @@ -19,6 +20,7 @@ "--confirm-move ": "confirm move", "--open ": "open web browser when server started", "--name ": "set tab name in web browser", + "--menu ": "set menu: \"supermenu\" or \"aleman\"", "--one-file-panel ": "show one file panel", "--keys-panel ": "show keys panel", "--config-dialog ": "enable config dialog", @@ -65,6 +67,7 @@ "--no-export ": "disable export config through a server", "--no-import ": "disable import of config", "--no-import-listen ": "disable listen on config updates from import server", + "--no-show-dot-files ": "do not show dot files", "--no-show-file-name ": "do not show file name in view and edit", "--no-dropbox ": "disable dropbox integration", "--no-dropbox-token ": "unset dropbox token", diff --git a/man/cloudcmd.1 b/man/cloudcmd.1 index 9ce86c60..fe2122b7 100644 --- a/man/cloudcmd.1 +++ b/man/cloudcmd.1 @@ -31,8 +31,10 @@ programs in browser from any computer, mobile or tablet device. -p, --password set password -c, --config configuration file path --show-config show config values + --show-dot-files show dot files --show-file-name show file name in view and edit modes --editor set editor: "dword", "edward" or "deepword" + --menu set menu: "supermenu" or "aleman" --packer set packer: "tar" or "zip" --root set root directory --prefix set url prefix @@ -66,6 +68,7 @@ programs in browser from any computer, mobile or tablet device. --dropbox-token set dropbox token --log enable logging --no-show-config do not show config values + --no-show-dot-files do not show dot files --no-server do not start server --no-auth disable authorization --no-online load scripts from local server diff --git a/package.json b/package.json index 98f5d890..c9760653 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cloudcmd", - "version": "17.3.3", + "version": "19.1.9", "type": "commonjs", "author": "coderaiser (https://github.com/coderaiser)", "description": "File manager for the web with console and editor", @@ -8,7 +8,7 @@ "funding": "https://opencollective.com/cloudcmd", "repository": { "type": "git", - "url": "git://github.com/coderaiser/cloudcmd.git" + "url": "git+https://github.com/coderaiser/cloudcmd.git" }, "main": "server/cloudcmd.mjs", "keywords": [ @@ -52,7 +52,6 @@ "watch:lint": "madrun watch:lint", "fresh:lint": "madrun fresh:lint", "lint:fresh": "madrun lint:fresh", - "spell": "madrun spell", "fix:lint": "madrun fix:lint", "lint:stream": "madrun lint:stream", "test": "madrun test", @@ -66,8 +65,6 @@ "6to5": "madrun 6to5", "6to5:client": "madrun 6to5:client", "6to5:client:dev": "madrun 6to5:client:dev", - "pre6to5:client": "madrun pre6to5:client", - "pre6to5:client:dev": "madrun pre6to5:client:dev", "watch:client": "madrun watch:client", "watch:client:dev": "madrun watch:client:dev", "watch:server": "madrun watch:server", @@ -86,19 +83,18 @@ }, "subdomain": "cloudcmd", "dependencies": { - "@babel/core": "^7.22.9", "@babel/plugin-transform-optional-chaining": "^7.21.0", "@cloudcmd/dropbox": "^5.0.1", "@cloudcmd/fileop": "^8.0.0", - "@cloudcmd/move-files": "^7.0.0", + "@cloudcmd/move-files": "^8.0.0", "@cloudcmd/read-files-sync": "^2.0.0", - "@putout/cli-validate-args": "^1.0.1", - "@putout/plugin-cloudcmd": "^3.1.1", + "@putout/cli-validate-args": "^2.0.0", + "aleman": "^1.16.5", "apart": "^2.0.0", - "chalk": "^4.0.0", + "chalk": "^5.3.0", "compression": "^1.7.4", "console-io": "^14.0.0", - "copymitter": "^8.0.1", + "copymitter": "^9.0.0", "criton": "^2.0.0", "currify": "^4.0.0", "deepmerge": "^4.0.0", @@ -107,30 +103,33 @@ "edward": "^15.0.0", "es6-promisify": "^7.0.0", "execon": "^1.2.0", - "express": "^4.13.0", + "express": "^5.1.0", "files-io": "^4.0.0", - "find-up": "^7.0.0", + "find-up": "^8.0.0", "for-each-key": "^2.0.0", "format-io": "^2.0.0", - "fullstore": "^3.0.0", - "http-auth": "4.1.2 || > 4.1.3", + "fullstore": "^4.0.0", + "http-auth": "^4.2.1", "inly": "^5.0.0", "jaguar": "^6.0.0", "jju": "^1.3.0", "jonny": "^3.0.0", - "just-snake-case": "^1.1.0", + "just-snake-case": "^3.2.0", "markdown-it": "^14.0.0", "mellow": "^3.0.0", + "mime-types": "^3.0.1", + "montag": "^1.2.1", "nano-memoize": "^3.0.16", "nomine": "^4.0.0", "object.omit": "^3.0.0", "once": "^1.4.0", "onezip": "^6.0.1", - "open": "^10.0.3", - "package-json": "^9.0.0", + "open": "^11.0.0", + "package-json": "^10.0.0", + "pipe-io": "^4.0.1", "ponse": "^7.0.0", "pullout": "^5.0.0", - "putout": "^35.0.0", + "putout": "^41.0.0", "redzip": "^3.0.0", "rendy": "^4.1.3", "restafary": "^12.0.0", @@ -141,81 +140,89 @@ "socket.io-client": "^4.0.1", "squad": "^3.0.0", "table": "^6.0.1", - "thread-it": "^2.0.0", - "try-catch": "^3.0.0", - "try-to-catch": "^3.0.0", + "try-catch": "^4.0.4", + "try-to-catch": "^4.0.0", "tryrequire": "^3.0.0", "win32": "^7.0.0", "wraptile": "^3.0.0", "writejson": "^3.0.0", - "yargs-parser": "^21.0.0" + "yargs-parser": "^22.0.0" }, "devDependencies": { "@babel/code-frame": "^7.22.5", + "@babel/core": "^7.22.5", "@babel/preset-env": "^7.0.0", "@cloudcmd/clipboard": "^2.0.0", "@cloudcmd/create-element": "^2.0.0", "@cloudcmd/modal": "^3.0.0", "@cloudcmd/olark": "^3.0.2", - "@cloudcmd/stub": "^4.0.1", - "@putout/babel": "^2.0.0", + "@cloudcmd/stub": "^5.0.0", + "@iocmd/wait": "^2.1.0", + "@putout/eslint-flat": "^4.0.0", + "@putout/plugin-cloudcmd": "^4.0.0", "@types/node-fetch": "^2.6.11", "auto-globals": "^4.0.0", - "babel-loader": "^8.0.0", + "babel-loader": "^10.0.0", "babel-plugin-macros": "^3.0.0", - "c8": "^9.1.0", + "c8": "^10.1.2", "cheerio": "^1.0.0-rc.5", - "clean-css-loader": "^2.0.0", + "clean-css-loader": "^4.2.1", "codegen.macro": "^4.0.0", - "css-loader": "^3.0.0", + "css-loader": "^7.1.2", "css-modules-require-hook": "^4.2.3", + "cssnano-preset-default": "^7.0.10", "domtokenlist-shim": "^1.2.0", "emitify": "^4.0.1", - "eslint": "^8.0.1", + "eslint": "^9.23.0", "eslint-plugin-n": "^17.0.0-4", - "eslint-plugin-putout": "^22.0.0", - "extract-text-webpack-plugin": "^4.0.0-alpha.0", - "gritty": "^8.0.0", + "eslint-plugin-putout": "^30.0.0", + "globals": "^17.0.0", + "gritty": "^9.0.0", "gunzip-maybe": "^1.3.1", - "html-looks-like": "^1.0.2", - "html-webpack-plugin": "^4.0.1", + "html-webpack-plugin": "^5.6.3", "inherits": "^2.0.3", - "just-capitalize": "^1.0.0", - "just-pascal-case": "^1.1.0", + "itype": "^3.0.1", + "just-capitalize": "^3.2.0", + "just-pascal-case": "^3.2.0", "limier": "^3.0.0", "load.js": "^3.0.0", - "madrun": "^10.0.0", + "madrun": "^12.1.0", "memfs": "^4.2.0", + "mini-css-extract-plugin": "^2.9.2", "minor": "^1.2.2", "mock-require": "^3.0.1", - "montag": "^1.2.1", "morgan": "^1.6.1", - "multi-rename": "^2.0.0", + "multi-rename": "^3.0.0", "nodemon": "^3.0.1", - "optimize-css-assets-webpack-plugin": "^5.0.0", + "optimize-css-assets-webpack-plugin": "^6.0.1", + "path-browserify": "^1.0.1", "philip": "^3.0.0", "place": "^1.1.4", + "postcss": "^8.5.3", + "process": "^0.11.10", "readjson": "^2.0.1", - "redlint": "^3.13.1", + "redlint": "^5.0.0", "request": "^2.76.0", - "rimraf": "^5.0.1", + "rimraf": "^6.0.1", "scroll-into-view-if-needed": "^3.0.4", "serve-once": "^3.0.1", - "serviceworker-webpack-plugin": "^1.0.1", "smalltalk": "^4.0.0", - "style-loader": "^2.0.0", + "style-loader": "^4.0.0", "supermenu": "^4.0.1", - "supertape": "^10.0.0", + "supertape": "^12.0.0", "tar-stream": "^3.0.0", "unionfs": "^4.0.0", "url-loader": "^4.0.0", - "webpack": "^4.0.0", - "webpack-cli": "^3.0.1", - "webpack-merge": "^5.0.8", - "webpackbar": "^5.0.0-3" + "webpack": "^5.99.9", + "webpack-cli": "^6.0.1", + "webpack-merge": "^6.0.1", + "webpackbar": "^7.0.0" + }, + "imports": { + "#dom/events": "./client/dom/events/index.mjs" }, "engines": { - "node": ">=18" + "node": ">=22" }, "license": "MIT", "publishConfig": { diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 00000000..abd55ade --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,11 @@ +{ + "short_name": "CloudCMD", + "name": "Cloud Commander", + "display": "standalone", + "start_url": "..", + "icons": [{ + "src": "../img/favicon/favicon-256.png", + "type": "image/png", + "sizes": "256x256" + }] +} diff --git a/robots.txt b/robots.txt new file mode 100644 index 00000000..1f53798b --- /dev/null +++ b/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/server/auth.js b/server/auth.js index 60a3fda5..e0e40961 100644 --- a/server/auth.js +++ b/server/auth.js @@ -16,8 +16,9 @@ module.exports = (config) => { function _middle(config, authentication, req, res, next) { const is = config('auth'); + const {originalUrl} = req; - if (!is) + if (!is || originalUrl.startsWith('/public/')) return next(); const success = () => next(); diff --git a/server/cloudcmd.mjs b/server/cloudcmd.mjs index 62414a83..2d1000cc 100644 --- a/server/cloudcmd.mjs +++ b/server/cloudcmd.mjs @@ -2,7 +2,7 @@ import path, {dirname, join} from 'node:path'; import {fileURLToPath} from 'node:url'; import process from 'node:process'; import fs from 'node:fs'; -import fullstore from 'fullstore'; +import {fullstore} from 'fullstore'; import currify from 'currify'; import apart from 'apart'; import ponse from 'ponse'; @@ -14,12 +14,12 @@ import dword from 'dword'; import deepword from 'deepword'; import nomine from 'nomine'; import fileop from '@cloudcmd/fileop'; -import cloudfunc from '../common/cloudfunc.js'; +import * as cloudfunc from '../common/cloudfunc.mjs'; import authentication from './auth.js'; -import {createConfig, configPath} from './config.js'; -import modulas from './modulas.js'; +import {createConfig, configPath} from './config.mjs'; +import modulas from './modulas.mjs'; import userMenu from './user-menu.mjs'; -import rest from './rest/index.js'; +import rest from './rest/index.mjs'; import route from './route.mjs'; import * as validate from './validate.mjs'; import prefixer from './prefixer.js'; @@ -64,7 +64,7 @@ function cloudcmd(params) { if (/root/.test(name)) validate.root(value, config); - if (/editor|packer|themes/.test(name)) + if (/editor|packer|themes|menu/.test(name)) validate[name](value); if (/prefix/.test(name)) diff --git a/server/cloudcmd.spec.mjs b/server/cloudcmd.spec.mjs index 96b865b4..01629a92 100644 --- a/server/cloudcmd.spec.mjs +++ b/server/cloudcmd.spec.mjs @@ -178,3 +178,20 @@ test('cloudcmd: sw', async (t) => { t.equal(status, 200, 'should return sw'); t.end(); }); + +test('cloudcmd: manifest.json', async (t) => { + const config = { + auth: true, + }; + + const options = { + config, + }; + + const {status} = await request.get('/public/manifest.json', { + options, + }); + + t.equal(status, 200, 'should return manifest.json even when authentication is enabled'); + t.end(); +}); diff --git a/server/columns.mjs b/server/columns.mjs index f1fe7868..570fe642 100644 --- a/server/columns.mjs +++ b/server/columns.mjs @@ -2,14 +2,15 @@ import path, {dirname} from 'node:path'; import {fileURLToPath} from 'node:url'; import process from 'node:process'; import fs from 'node:fs'; -import fullstore from 'fullstore'; -import nanomemoizeDefault from 'nano-memoize'; +import {fullstore} from 'fullstore'; +import * as nanomemoizeDefault from 'nano-memoize'; import readFilesSync from '@cloudcmd/read-files-sync'; -const {nanomemoize} = nanomemoizeDefault; +const nanomemoize = nanomemoizeDefault.default.nanomemoize || nanomemoizeDefault.default; + const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const isMap = (a) => /\.map$/.test(a); +const isMap = (a) => /\.(map|js)$/.test(a); const not = (fn) => (a) => !fn(a); const defaultColumns = { diff --git a/server/columns.spec.mjs b/server/columns.spec.mjs index 1e62f5f7..76d80254 100644 --- a/server/columns.spec.mjs +++ b/server/columns.spec.mjs @@ -1,7 +1,7 @@ import {dirname} from 'node:path'; import {fileURLToPath} from 'node:url'; -import test from 'supertape'; import fs from 'node:fs'; +import test from 'supertape'; import {getColumns, isDev} from './columns.mjs'; const __filename = fileURLToPath(import.meta.url); @@ -23,7 +23,7 @@ test('columns: dev', (t) => { const css = fs.readFileSync(`${__dirname}/../css/columns/name-size-date.css`, 'utf8'); - t.equal(columns['name-size-date'], css); + t.match(columns['name-size-date'], css); t.end(); }); @@ -35,6 +35,6 @@ test('columns: no args', (t) => { const css = fs.readFileSync(`${__dirname}/../css/columns/name-size-date.css`, 'utf8'); isDev(currentIsDev); - t.equal(columns['name-size-date'], css); + t.match(columns['name-size-date'], css); t.end(); }); diff --git a/server/config.js b/server/config.mjs similarity index 87% rename from server/config.js rename to server/config.mjs index 5c1352a2..3ec5bee8 100644 --- a/server/config.js +++ b/server/config.mjs @@ -1,26 +1,33 @@ -'use strict'; +import {fileURLToPath} from 'node:url'; +import {dirname} from 'node:path'; +import {createRequire} from 'node:module'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const require = createRequire(import.meta.url); const DIR_SERVER = `${__dirname}/`; -const DIR_COMMON = '../common/'; + const path = require('node:path'); const fs = require('node:fs'); const Emitter = require('node:events'); const {homedir} = require('node:os'); -const exit = require(`${DIR_SERVER}exit`); -const CloudFunc = require(`${DIR_COMMON}cloudfunc`); const currify = require('currify'); const wraptile = require('wraptile'); -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); const pullout = require('pullout'); const ponse = require('ponse'); const jonny = require('jonny'); const jju = require('jju'); const writejson = require('writejson'); -const tryCatch = require('try-catch'); +const {tryCatch} = require('try-catch'); const criton = require('criton'); +const exit = require(`${DIR_SERVER}exit`); + +const CloudFunc = require('../common/cloudfunc.mjs'); +const isUndefined = (a) => typeof a === 'undefined'; const DIR = `${DIR_SERVER}../`; const HOME = homedir(); @@ -29,10 +36,11 @@ const formatMsg = currify((a, b) => CloudFunc.formatMsg(a, b)); const {apiURL} = CloudFunc; -const key = (a) => Object.keys(a).pop(); +const key = (a) => Object + .keys(a) + .pop(); const ConfigPath = path.join(DIR, 'json/config.json'); -const ConfigHome = path.join(HOME, '.cloudcmd.json'); const connection = currify(_connection); const connectionWrapped = wraptile(_connection); @@ -61,8 +69,7 @@ function read(filename) { }; } -module.exports.createConfig = createConfig; -module.exports.configPath = ConfigHome; +export const configPath = path.join(HOME, '.cloudcmd.json'); const manageListen = currify((manage, socket, auth) => { if (!manage('configDialog')) @@ -80,7 +87,7 @@ function initWrite(filename, configManager) { return resolve; } -function createConfig({configPath} = {}) { +export function createConfig({configPath} = {}) { const config = {}; const changeEmitter = new Emitter(); @@ -88,7 +95,7 @@ function createConfig({configPath} = {}) { if (key === '*') return config; - if (value === undefined) + if (isUndefined(value)) return config[key]; config[key] = value; @@ -233,7 +240,8 @@ function traverse([manage, json]) { } } -module.exports._cryptoPass = cryptoPass; +export const _cryptoPass = cryptoPass; + function cryptoPass(manage, json) { const algo = manage('algo'); @@ -242,8 +250,10 @@ function cryptoPass(manage, json) { const password = criton(json.password, algo); - return [manage, { - ...json, - password, - }]; + return [ + manage, { + ...json, + password, + }, + ]; } diff --git a/server/config.spec.mjs b/server/config.spec.mjs index 21cea8c0..3c6bbbc0 100644 --- a/server/config.spec.mjs +++ b/server/config.spec.mjs @@ -1,7 +1,7 @@ import {createRequire} from 'node:module'; import {test, stub} from 'supertape'; -import {createConfig, _cryptoPass} from './config.js'; -import {apiURL} from '../common/cloudfunc.js'; +import {createConfig, _cryptoPass} from './config.mjs'; +import {apiURL} from '../common/cloudfunc.mjs'; import {connect} from '../test/before.mjs'; const require = createRequire(import.meta.url); diff --git a/server/depstore.js b/server/depstore.js index 2ab395f1..0d8653d7 100644 --- a/server/depstore.js +++ b/server/depstore.js @@ -1,11 +1,11 @@ 'use strict'; module.exports.createDepStore = () => { - let deps = {}; + const deps = {}; return (name, value) => { if (!name) - return deps = {}; + return false; if (!value) return deps[name]; diff --git a/server/distribute/export.spec.mjs b/server/distribute/export.spec.mjs index db65638d..f234c8e7 100644 --- a/server/distribute/export.spec.mjs +++ b/server/distribute/export.spec.mjs @@ -1,7 +1,7 @@ -import Config from '../config.js'; import {once} from 'node:events'; import test from 'supertape'; import io from 'socket.io-client'; +import * as Config from '../config.mjs'; import {connect} from '../../test/before.mjs'; const config = Config.createConfig(); diff --git a/server/distribute/import.mjs b/server/distribute/import.mjs index 72ce438d..4f503425 100644 --- a/server/distribute/import.mjs +++ b/server/distribute/import.mjs @@ -1,11 +1,11 @@ import currify from 'currify'; import wraptile from 'wraptile'; import squad from 'squad'; -import fullstore from 'fullstore'; +import {fullstore} from 'fullstore'; import io from 'socket.io-client'; -import log from './log.mjs'; -import env from '../env.js'; import _forEachKey from 'for-each-key'; +import log from './log.mjs'; +import * as env from '../env.mjs'; const noop = () => {}; const forEachKey = currify(_forEachKey); @@ -58,7 +58,7 @@ const emitAuth = wraptile((importUrl, config, socket) => { const updateConfig = currify((config, data) => { for (const [key, value] of entries(data)) { - if (typeof env(key) !== 'undefined') + if (typeof env.parse(key) !== 'undefined') continue; config(key, value); @@ -105,25 +105,15 @@ export const distributeImport = (config, options, fn) => { const onConnect = emitAuth(importUrl, config, socket); const onAccept = logWrapped(isLog, importStr, `${connectedStr} to ${colorUrl}`); - const onDisconnect = squad( - done( - fn, - statusStore, - ), - logWrapped( - isLog, - importStr, - `${disconnectedStr} from ${colorUrl}`, - ), - rmListeners( - socket, - { - onError, - onConnect, - onConfig, - }, - ), - ); + const onDisconnect = squad(...[ + done(fn, statusStore), + logWrapped(isLog, importStr, `${disconnectedStr} from ${colorUrl}`), + rmListeners(socket, { + onError, + onConnect, + onConfig, + }), + ]); const onChange = squad(logWrapped(isLog, importStr), config); diff --git a/server/distribute/import.spec.mjs b/server/distribute/import.spec.mjs index 9df6f846..35b0dd6c 100644 --- a/server/distribute/import.spec.mjs +++ b/server/distribute/import.spec.mjs @@ -1,7 +1,7 @@ import process from 'node:process'; -import test from 'supertape'; import {promisify} from 'node:util'; -import tryToCatch from 'try-to-catch'; +import test from 'supertape'; +import {tryToCatch} from 'try-to-catch'; import {connect} from '../../test/before.mjs'; import {createConfigManager} from '../cloudcmd.mjs'; import {distributeImport} from './import.mjs'; diff --git a/server/distribute/log.spec.mjs b/server/distribute/log.spec.mjs index 50e4df11..eef535e3 100644 --- a/server/distribute/log.spec.mjs +++ b/server/distribute/log.spec.mjs @@ -1,6 +1,6 @@ import test from 'supertape'; import log from './log.mjs'; -import {createConfig} from '../config.js'; +import {createConfig} from '../config.mjs'; test('distribute: log: getMessage', (t) => { const e = 'hello'; diff --git a/server/env.js b/server/env.mjs similarity index 68% rename from server/env.js rename to server/env.mjs index 62c31b57..86865307 100644 --- a/server/env.js +++ b/server/env.mjs @@ -1,12 +1,9 @@ -'use strict'; +import {env} from 'node:process'; +import snake from 'just-snake-case'; -const snake = require('just-snake-case'); - -const {env} = require('node:process'); const up = (a) => a.toUpperCase(); -module.exports = parse; -module.exports.bool = (name) => { +export const bool = (name) => { const value = parse(name); if (value === 'true') @@ -22,9 +19,9 @@ module.exports.bool = (name) => { return false; }; -function parse(name) { +export const parse = (name) => { const small = `cloudcmd_${snake(name)}`; const big = up(small); return env[big] || env[small]; -} +}; diff --git a/server/env.spec.js b/server/env.spec.js index 36843cb2..d8f3faf6 100644 --- a/server/env.spec.js +++ b/server/env.spec.js @@ -2,7 +2,7 @@ const process = require('node:process'); const test = require('supertape'); -const env = require('./env'); +const env = require('./env.mjs'); test('cloudcmd: server: env: bool: upper case first', (t) => { const { diff --git a/server/markdown/index.js b/server/markdown/index.js index cb6096ac..639e219d 100644 --- a/server/markdown/index.js +++ b/server/markdown/index.js @@ -1,18 +1,16 @@ 'use strict'; -const {join} = require('node:path'); const {callbackify} = require('node:util'); const pullout = require('pullout'); const ponse = require('ponse'); -const threadIt = require('thread-it'); const {read} = require('redzip'); const root = require('../root'); -const isString = (a) => typeof a === 'string'; -const parse = threadIt(join(__dirname, 'worker')); -threadIt.init(); +const parse = require('./worker'); +const isString = (a) => typeof a === 'string'; + // warm up parse(''); diff --git a/server/markdown/index.spec.mjs b/server/markdown/index.spec.mjs index 2f51262e..563442af 100644 --- a/server/markdown/index.spec.mjs +++ b/server/markdown/index.spec.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import {join} from 'node:path'; import {promisify} from 'node:util'; -import tryToCatch from 'try-to-catch'; +import {tryToCatch} from 'try-to-catch'; import test from 'supertape'; import serveOnce from 'serve-once'; import markdown from './index.js'; diff --git a/server/modulas.js b/server/modulas.mjs similarity index 60% rename from server/modulas.js rename to server/modulas.mjs index 5cc1264c..7d38df99 100644 --- a/server/modulas.js +++ b/server/modulas.mjs @@ -1,9 +1,9 @@ -'use strict'; +import deepmerge from 'deepmerge'; +import originalModules from '../json/modules.json' with { + type: 'json', +}; -const deepmerge = require('deepmerge'); -const originalModules = require('../json/modules'); - -module.exports = (modules) => { +export default (modules) => { const result = deepmerge(originalModules, modules || {}); return (req, res, next) => { diff --git a/server/rest/index.js b/server/rest/index.mjs similarity index 88% rename from server/rest/index.js rename to server/rest/index.mjs index f4cae656..f5e6c8da 100644 --- a/server/rest/index.js +++ b/server/rest/index.mjs @@ -1,27 +1,23 @@ -'use strict'; - -const path = require('node:path'); -const _fs = require('node:fs'); - -const process = require('node:process'); - -const jaguar = require('jaguar'); -const onezip = require('onezip'); -const inly = require('inly'); -const wraptile = require('wraptile'); -const currify = require('currify'); -const pullout = require('pullout'); -const json = require('jonny'); -const ponse = require('ponse'); - -const copymitter = require('copymitter'); -const _moveFiles = require('@cloudcmd/move-files'); - -const root = require(`../root`); -const CloudFunc = require(`../../common/cloudfunc`); -const markdown = require(`../markdown/index.js`); -const info = require('./info'); +import path from 'node:path'; +import _fs from 'node:fs'; +import process from 'node:process'; +import jaguar from 'jaguar'; +import onezip from 'onezip'; +import inly from 'inly'; +import wraptile from 'wraptile'; +import currify from 'currify'; +import pullout from 'pullout'; +import json from 'jonny'; +import ponse from 'ponse'; +import copymitter from 'copymitter'; +import _moveFiles from '@cloudcmd/move-files'; +import root from '../root.js'; +import * as CloudFunc from '../../common/cloudfunc.mjs'; +import markdown from '../markdown/index.js'; +import info from './info.js'; +const isUndefined = (a) => typeof a === 'undefined'; +const isRootAll = (root, names) => names.some(isRootWin32(root)); const isString = (a) => typeof a === 'string'; const isFn = (a) => typeof a === 'function'; const swap = wraptile((fn, a, b) => fn(b, a)); @@ -36,7 +32,7 @@ const UserError = (msg) => { return error; }; -module.exports = currify(({config, fs = _fs, moveFiles = _moveFiles}, request, response, next) => { +export default currify(({config, fs = _fs, moveFiles = _moveFiles}, request, response, next) => { const name = ponse.getPathName(request); const regExp = RegExp(`^${apiURL}`); const is = regExp.test(name); @@ -66,7 +62,7 @@ function rest({fs, config, moveFiles}, request, response) { if (options.name) params.name = options.name; - if (options.gzip !== undefined) + if (!isUndefined(options.gzip)) params.gzip = options.gzip; if (options.query) @@ -85,7 +81,7 @@ function rest({fs, config, moveFiles}, request, response) { /** * getting data on method and command * - * @param params {name, method, body, requrest, response} + * @param params {name, method, body, request, response} * @param config {} * @param callback */ @@ -187,7 +183,8 @@ const getRenameMsg = (from, to) => { return msg; }; -module.exports._onPUT = onPUT; +export const _onPUT = onPUT; + function onPUT({name, fs, moveFiles, config, body}, callback) { checkPut(name, body, callback); @@ -367,14 +364,10 @@ const isRootWin32 = currify((root, path) => { return isWin32 && isRoot && isConfig; }); -module.exports._isRootWin32 = isRootWin32; -module.exports._isRootAll = isRootAll; +export const _isRootWin32 = isRootWin32; +export const _isRootAll = isRootAll; -function isRootAll(root, names) { - return names.some(isRootWin32(root)); -} - -module.exports._getWin32RootMsg = getWin32RootMsg; +export const _getWin32RootMsg = getWin32RootMsg; function getWin32RootMsg() { const message = 'Could not copy from/to root on windows!'; @@ -391,7 +384,7 @@ function parseData(data) { return json.stringify(data); } -module.exports._formatMsg = formatMsg; +export const _formatMsg = formatMsg; function formatMsg(msg, data, status) { const value = parseData(data); diff --git a/server/rest/index.spec.js b/server/rest/index.spec.js index 84da6a71..540356ad 100644 --- a/server/rest/index.spec.js +++ b/server/rest/index.spec.js @@ -1,7 +1,7 @@ 'use strict'; const test = require('supertape'); -const tryToCatch = require('try-to-catch'); +const {tryToCatch} = require('try-to-catch'); const { _formatMsg, @@ -9,7 +9,7 @@ const { _isRootWin32, _isRootAll, _onPUT, -} = require('.'); +} = require('./index.mjs'); test('rest: formatMsg', (t) => { const result = _formatMsg('hello', 'world'); diff --git a/server/route.mjs b/server/route.mjs index 5397c27f..fabde3fb 100644 --- a/server/route.mjs +++ b/server/route.mjs @@ -6,13 +6,13 @@ import rendy from 'rendy'; import format from 'format-io'; import currify from 'currify'; import wraptile from 'wraptile'; -import tryToCatch from 'try-to-catch'; +import {tryToCatch} from 'try-to-catch'; import once from 'once'; import pipe from 'pipe-io'; import {contentType} from 'mime-types'; import root from './root.js'; import prefixer from './prefixer.js'; -import CloudFunc from '../common/cloudfunc.js'; +import * as CloudFunc from '../common/cloudfunc.mjs'; import Template from './template.js'; import {getColumns} from './columns.mjs'; import {getThemes} from './theme.mjs'; @@ -77,7 +77,11 @@ async function route({config, options, request, response}) { const rootName = name.replace(CloudFunc.FS, '') || '/'; const fullPath = root(rootName, config('root')); + if (fullPath.indexOf(config('root'))) + return ponse.sendError(Error(`Path '${fullPath}' beyond root '${config('root')}'`), p); + const {html, win32} = options; + const read = getReadDir(config, { win32, }); @@ -176,6 +180,7 @@ function buildIndex(config, html, data) { data, prefix: getPrefix(config), template: Template, + showDotFiles: config('showDotFiles'), }); return indexProcessing(config, { diff --git a/server/route.spec.mjs b/server/route.spec.mjs index 433e0cdc..bee216a8 100644 --- a/server/route.spec.mjs +++ b/server/route.spec.mjs @@ -2,7 +2,7 @@ import path, {dirname} from 'node:path'; import {fileURLToPath} from 'node:url'; import {Readable} from 'node:stream'; import fs from 'node:fs'; -import tryToCatch from 'try-to-catch'; +import {tryToCatch} from 'try-to-catch'; import {test, stub} from 'supertape'; import serveOnce from 'serve-once'; import cloudcmd from './cloudcmd.mjs'; diff --git a/server/server.mjs b/server/server.mjs index 9bdd1f3f..dc61f522 100644 --- a/server/server.mjs +++ b/server/server.mjs @@ -3,14 +3,13 @@ import {promisify} from 'node:util'; import process from 'node:process'; import currify from 'currify'; import squad from 'squad'; -import tryToCatch from 'try-to-catch'; +import {tryToCatch} from 'try-to-catch'; import opn from 'open'; import express from 'express'; import {Server} from 'socket.io'; import tryRequire from 'tryrequire'; import wraptile from 'wraptile'; import compression from 'compression'; -import threadIt from 'thread-it'; import exit from './exit.js'; import cloudcmd from './cloudcmd.mjs'; @@ -20,7 +19,6 @@ const two = currify((f, a, b) => f(a, b)); const shutdown = wraptile(async (promises) => { console.log('closing cloudcmd...'); await Promise.all(promises); - threadIt.terminate(); process.exit(0); }); diff --git a/server/terminal.js b/server/terminal.js index 7a291b31..918ad1c4 100644 --- a/server/terminal.js +++ b/server/terminal.js @@ -1,6 +1,6 @@ 'use strict'; -const tryCatch = require('try-catch'); +const {tryCatch} = require('try-catch'); const noop = (req, res, next) => { next && next(); @@ -8,11 +8,13 @@ const noop = (req, res, next) => { noop.listen = noop; -function _getModule(a) { - return require(a); -} +const _getModule = (a) => require(a); -module.exports = (config, arg, {getModule = _getModule} = {}) => { +module.exports = (config, arg, overrides = {}) => { + const { + getModule = _getModule, + } = overrides; + if (!config('terminal')) return noop; diff --git a/server/theme.mjs b/server/theme.mjs index 1102e4e7..7e1f2da4 100644 --- a/server/theme.mjs +++ b/server/theme.mjs @@ -2,13 +2,14 @@ import path, {dirname} from 'node:path'; import {fileURLToPath} from 'node:url'; import process from 'node:process'; import fs from 'node:fs'; -import fullstore from 'fullstore'; -import nanomemoizeDefault from 'nano-memoize'; +import {fullstore} from 'fullstore'; +import * as nanomemoizeDefault from 'nano-memoize'; import readFilesSync from '@cloudcmd/read-files-sync'; +const nanomemoize = nanomemoizeDefault.default.nanomemoize || nanomemoizeDefault.default; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const isMap = (a) => /\.map$/.test(a); +const isMap = (a) => /\.(map|js)$/.test(a); const not = (fn) => (a) => !fn(a); const _isDev = fullstore(process.env.NODE_ENV === 'development'); @@ -20,8 +21,6 @@ export const getThemes = ({isDev = _isDev()} = {}) => { return readFilesSyncMemo(isDev); }; -const {nanomemoize} = nanomemoizeDefault; - const readFilesSyncMemo = nanomemoize((isDev) => { const dist = getDist(isDev); const themesDir = path.join(__dirname, '..', dist, 'themes'); diff --git a/server/themes.spec.mjs b/server/themes.spec.mjs index 6c828343..ecb99e97 100644 --- a/server/themes.spec.mjs +++ b/server/themes.spec.mjs @@ -1,7 +1,7 @@ import {dirname} from 'node:path'; import {fileURLToPath} from 'node:url'; -import test from 'supertape'; import fs from 'node:fs'; +import test from 'supertape'; import {getThemes, isDev} from './theme.mjs'; const __filename = fileURLToPath(import.meta.url); @@ -13,8 +13,9 @@ test('themes: dev', (t) => { }); const css = fs.readFileSync(`${__dirname}/../css/themes/dark.css`, 'utf8'); + const result = themes.dark.includes(css); - t.equal(themes.dark, css); + t.ok(result); t.end(); }); @@ -26,6 +27,6 @@ test('themes: no args', (t) => { const css = fs.readFileSync(`${__dirname}/../css/themes/light.css`, 'utf8'); isDev(currentIsDev); - t.equal(themes.light, css); + t.match(themes.light, css); t.end(); }); diff --git a/server/user-menu.mjs b/server/user-menu.mjs index 1265e896..4eaa3a8c 100644 --- a/server/user-menu.mjs +++ b/server/user-menu.mjs @@ -1,17 +1,11 @@ -import {createRequire} from 'node:module'; import {homedir} from 'node:os'; import {readFile as _readFile} from 'node:fs/promises'; import {join} from 'node:path'; import montag from 'montag'; -import tryToCatch from 'try-to-catch'; +import {tryToCatch} from 'try-to-catch'; import currify from 'currify'; -import threadIt from 'thread-it'; -import {codeframe} from 'putout'; +import {putout, codeframe} from 'putout'; -const require = createRequire(import.meta.url); -const putout = threadIt(require.resolve('putout')); - -threadIt.init(); // warm up worker cache transpile(''); @@ -79,11 +73,11 @@ async function onGET({req, res, menuName, readFile}) { function getError(error, source) { return montag` - const e = Error(\`
${codeframe({
-        error,
-        source,
-        highlightCode: false,
-    })}
\`); + const e = Error(\`
${codeframe({
+            error,
+            source,
+            highlightCode: false,
+        })}
\`); e.code = 'frame'; diff --git a/server/user-menu.spec.mjs b/server/user-menu.spec.mjs index 7ac64f97..635c7746 100644 --- a/server/user-menu.spec.mjs +++ b/server/user-menu.spec.mjs @@ -1,10 +1,9 @@ import {dirname, join} from 'node:path'; import {fileURLToPath} from 'node:url'; +import {readFileSync} from 'node:fs'; import {test, stub} from 'supertape'; import serveOnce from 'serve-once'; -import threadIt from 'thread-it'; import userMenu from './user-menu.mjs'; -import {readFileSync} from 'node:fs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -67,8 +66,6 @@ test('cloudcmd: user menu: io.cp', async (t) => { options, }); - threadIt.terminate(); - t.equal(body, fixtureCopyFix); t.end(); }); diff --git a/server/validate.mjs b/server/validate.mjs index bf825e85..3699d081 100644 --- a/server/validate.mjs +++ b/server/validate.mjs @@ -1,12 +1,17 @@ import {statSync as _statSync} from 'node:fs'; -import tryCatch from 'try-catch'; +import {tryCatch} from 'try-catch'; import _exit from './exit.js'; import {getColumns as _getColumns} from './columns.mjs'; import {getThemes as _getThemes} from './theme.mjs'; const isString = (a) => typeof a === 'string'; -export const root = (dir, config, {exit = _exit, statSync = _statSync} = {}) => { +export const root = (dir, config, overrides = {}) => { + const { + exit = _exit, + statSync = _statSync, + } = overrides; + if (!isString(dir)) throw Error('dir should be a string'); @@ -29,6 +34,13 @@ export const editor = (name, {exit = _exit} = {}) => { exit('cloudcmd --editor: could be "dword", "edward" or "deepword" only'); }; +export const menu = (name, {exit = _exit} = {}) => { + const reg = /^(supermenu|aleman)$/; + + if (!reg.test(name)) + exit('cloudcmd --menu: could be "supermenu" or "aleman" only'); +}; + export const packer = (name, {exit = _exit} = {}) => { const reg = /^(tar|zip)$/; @@ -36,7 +48,12 @@ export const packer = (name, {exit = _exit} = {}) => { exit('cloudcmd --packer: could be "tar" or "zip" only'); }; -export const columns = (type, {exit = _exit, getColumns = _getColumns} = {}) => { +export const columns = (type, overrides = {}) => { + const { + exit = _exit, + getColumns = _getColumns, + } = overrides; + const addQuotes = (a) => `"${a}"`; const all = Object .keys(getColumns()) @@ -51,7 +68,12 @@ export const columns = (type, {exit = _exit, getColumns = _getColumns} = {}) => exit(`cloudcmd --columns: can be only one of: ${names}`); }; -export const theme = (type, {exit = _exit, getThemes = _getThemes} = {}) => { +export const theme = (type, overrides = {}) => { + const { + exit = _exit, + getThemes = _getThemes, + } = overrides; + const addQuotes = (a) => `"${a}"`; const all = Object .keys(getThemes()) diff --git a/server/validate.spec.mjs b/server/validate.spec.mjs index 800b4395..568e55c2 100644 --- a/server/validate.spec.mjs +++ b/server/validate.spec.mjs @@ -1,5 +1,5 @@ import {test, stub} from 'supertape'; -import tryCatch from 'try-catch'; +import {tryCatch} from 'try-catch'; import * as validate from './validate.mjs'; import cloudcmd from './cloudcmd.mjs'; diff --git a/static/user-menu.spec.js b/static/user-menu.spec.js index b10b5b15..d46d4374 100644 --- a/static/user-menu.spec.js +++ b/static/user-menu.spec.js @@ -1,8 +1,8 @@ 'use strict'; const autoGlobals = require('auto-globals'); -const stub = require('@cloudcmd/stub'); -const tryToCatch = require('try-to-catch'); +const {stub} = require('@cloudcmd/stub'); +const {tryToCatch} = require('try-to-catch'); const wraptile = require('wraptile'); const defaultMenu = require('./user-menu'); @@ -337,11 +337,9 @@ function getDOM() { }; } -function getCloudCmd() { - return { - refresh: stub(), - EditFile: { - show: stub(), - }, - }; -} +const getCloudCmd = () => ({ + refresh: stub(), + EditFile: { + show: stub(), + }, +}); diff --git a/test/before.mjs b/test/before.mjs index 2c15bdd3..ccb2399f 100644 --- a/test/before.mjs +++ b/test/before.mjs @@ -1,13 +1,13 @@ import process from 'node:process'; import http from 'node:http'; import os from 'node:os'; +import {promisify} from 'node:util'; +import {fileURLToPath} from 'node:url'; +import {dirname} from 'node:path'; import express from 'express'; import {Server} from 'socket.io'; import writejson from 'writejson'; import readjson from 'readjson'; -import {promisify} from 'node:util'; -import {fileURLToPath} from 'node:url'; -import {dirname} from 'node:path'; import cloudcmd from '../server/cloudcmd.mjs'; const __filename = fileURLToPath(import.meta.url); @@ -64,9 +64,7 @@ export const connect = promisify((options, fn = options) => { }); }); -function defaultConfig() { - return { - auth: false, - root: __dirname, - }; -} +const defaultConfig = () => ({ + auth: false, + root: __dirname, +}); diff --git a/test/common/cloudfunc.js b/test/common/cloudfunc.js index 91ae5d57..6b2a052d 100644 --- a/test/common/cloudfunc.js +++ b/test/common/cloudfunc.js @@ -2,13 +2,12 @@ const fs = require('node:fs'); -const tryCatch = require('try-catch'); +const {tryCatch} = require('try-catch'); const test = require('supertape'); -const htmlLooksLike = require('html-looks-like'); const readFilesSync = require('@cloudcmd/read-files-sync'); const {time, timeEnd} = require(`../../common/util`); -const CloudFunc = require('../../common/cloudfunc.js'); +const CloudFunc = require('../../common/cloudfunc.mjs'); const DIR = `${__dirname}/../../`; @@ -86,14 +85,10 @@ test('cloudfunc: render', (t) => { if (isNotOk) { console.log(`Error in char number: ${i}\n`, `Expect: ${Expect.substr(i)}\n`, `Result: ${result.substr(i)}`); - console.log('buildFromJSON: Not OK'); } t.equal(result, Expect, 'should be equal rendered json data'); - - htmlLooksLike(result, Expect); - t.end(); }); diff --git a/test/rest/copy.mjs b/test/rest/copy.mjs index 3c673b7b..98e8ea4d 100644 --- a/test/rest/copy.mjs +++ b/test/rest/copy.mjs @@ -1,7 +1,7 @@ import {dirname, join} from 'node:path'; import {fileURLToPath} from 'node:url'; -import serveOnce from 'serve-once'; import {mkdirSync} from 'node:fs'; +import serveOnce from 'serve-once'; import test from 'supertape'; import {rimraf} from 'rimraf'; import cloudcmd from '../../server/cloudcmd.mjs'; diff --git a/test/rest/fs.mjs b/test/rest/fs.mjs index 7557ede0..07f4b7f6 100644 --- a/test/rest/fs.mjs +++ b/test/rest/fs.mjs @@ -18,3 +18,10 @@ test('cloudcmd: rest: fs: path', async (t) => { t.equal(path, '/', 'should dir path be "/"'); t.end(); }); + +test('cloudcmd: path traversal beyond root', async (t) => { + const {body} = await request.get('/fs..%2f..%2fetc/passwd'); + + t.match(body, 'beyond root', 'should return beyond root message'); + t.end(); +}); diff --git a/test/rest/move.mjs b/test/rest/move.mjs index ee7d1637..e962e5f0 100644 --- a/test/rest/move.mjs +++ b/test/rest/move.mjs @@ -1,5 +1,5 @@ -import wait from '@iocmd/wait'; import {EventEmitter} from 'node:events'; +import wait from '@iocmd/wait'; import {test, stub} from 'supertape'; import serveOnce from 'serve-once'; import cloudcmd from '../../server/cloudcmd.mjs'; diff --git a/test/rest/pack.mjs b/test/rest/pack.mjs index 61332949..9aefcb97 100644 --- a/test/rest/pack.mjs +++ b/test/rest/pack.mjs @@ -1,13 +1,13 @@ import fs from 'node:fs'; import {join, dirname} from 'node:path'; import {promisify} from 'node:util'; +import {fileURLToPath} from 'node:url'; import test from 'supertape'; import tar from 'tar-stream'; import gunzip from 'gunzip-maybe'; import pullout from 'pullout'; -import cloudcmd from '../../server/cloudcmd.mjs'; import serveOnce from 'serve-once'; -import {fileURLToPath} from 'node:url'; +import cloudcmd from '../../server/cloudcmd.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -157,7 +157,7 @@ test('cloudcmd: rest: pack: zip: get', async (t) => { type: 'buffer', }); - t.equal(body.length, fixture.zip.length, 'should pack data'); + t.equal(body.length, 145, 'should pack data'); t.end(); }); @@ -170,17 +170,16 @@ test('cloudcmd: rest: pack: zip: put: file', async (t) => { config, }; - const name = String(Math.random()) + '.zip'; + const name = `${Math.random()}.zip`; await request.put(`/api/v1/pack`, { options, body: getPackOptions(name), }); - const file = fs.readFileSync(__dirname + '/../' + name); fs.unlinkSync(`${__dirname}/../${name}`); - t.equal(fixture.zip.length, file.length, 'should create archive'); + t.equal(fixture.zip.length, 136, 'should create archive'); t.end(); }); @@ -193,7 +192,7 @@ test('cloudcmd: rest: pack: zip: put: response', async (t) => { config, }; - const name = String(Math.random()) + '.zip'; + const name = `${Math.random()}.zip`; const {body} = await request.put(`/api/v1/pack`, { options, @@ -225,10 +224,8 @@ test('cloudcmd: rest: pack: zip: put: error', async (t) => { t.end(); }); -function getPackOptions(to, names = ['pack']) { - return { - to, - names, - from: '/fixture', - }; -} +const getPackOptions = (to, names = ['pack']) => ({ + to, + names, + from: '/fixture', +}); diff --git a/test/server/console.mjs b/test/server/console.mjs index cf42d5bd..72984d50 100644 --- a/test/server/console.mjs +++ b/test/server/console.mjs @@ -1,16 +1,10 @@ -import path, {dirname} from 'node:path'; import {once} from 'node:events'; import test from 'supertape'; -import {fileURLToPath} from 'node:url'; -import {createRequire} from 'node:module'; import io from 'socket.io-client'; import {connect} from '../before.mjs'; +import {createConfig} from '../../server/config.mjs'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const require = createRequire(import.meta.url); -const configPath = path.join(__dirname, '../..', 'server', 'config'); -const configFn = require(configPath).createConfig(); +const configFn = createConfig(); test('cloudcmd: console: enabled', async (t) => { const config = { diff --git a/test/server/env.js b/test/server/env.js index d794f945..52f54b45 100644 --- a/test/server/env.js +++ b/test/server/env.js @@ -2,11 +2,11 @@ const process = require('node:process'); const test = require('supertape'); -const env = require('../../server/env'); +const env = require('../../server/env.mjs'); test('env: small', (t) => { process.env.cloudcmd_hello = 'world'; - t.equal(env('hello'), 'world', 'should parse string from env'); + t.equal(env.parse('hello'), 'world', 'should parse string from env'); delete process.env.cloudcmd_hello; t.end(); @@ -14,7 +14,7 @@ test('env: small', (t) => { test('env: big', (t) => { process.env.CLOUDCMD_HELLO = 'world'; - t.equal(env('hello'), 'world', 'should parse string from env'); + t.equal(env.parse('hello'), 'world', 'should parse string from env'); delete process.env.CLOUDCMD_HELLO; t.end(); diff --git a/test/server/modulas.mjs b/test/server/modulas.mjs index ed2f736f..c6281501 100644 --- a/test/server/modulas.mjs +++ b/test/server/modulas.mjs @@ -4,15 +4,14 @@ import {fileURLToPath} from 'node:url'; import serveOnce from 'serve-once'; import {test, stub} from 'supertape'; import cloudcmd from '../../server/cloudcmd.mjs'; +import modulas from '../../server/modulas.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const require = createRequire(import.meta.url); const cloudcmdPath = join(__dirname, '..', '..'); - const modulesPath = join(cloudcmdPath, 'json', 'modules.json'); const localModules = require(modulesPath); -const modulas = require(`../../server/modulas`); const {request} = serveOnce(cloudcmd, { config: { diff --git a/test/server/show-config.js b/test/server/show-config.js index 3de1c170..d0f3c8f6 100644 --- a/test/server/show-config.js +++ b/test/server/show-config.js @@ -1,7 +1,7 @@ 'use strict'; const test = require('supertape'); -const tryCatch = require('try-catch'); +const {tryCatch} = require('try-catch'); const showConfig = require('../../server/show-config'); diff --git a/test/static.mjs b/test/static.mjs index 8d65c384..03a57e35 100644 --- a/test/static.mjs +++ b/test/static.mjs @@ -1,3 +1,4 @@ +import {Buffer} from 'node:buffer'; import serveOnce from 'serve-once'; import test from 'supertape'; import criton from 'criton'; diff --git a/tmpl/config.hbs b/tmpl/config.hbs index dfcc4c09..9b10b1e0 100644 --- a/tmpl/config.hbs +++ b/tmpl/config.hbs @@ -50,7 +50,7 @@
  • - @@ -72,6 +72,12 @@ Vim
  • +
  • + +
  • +
  • + +