diff --git a/.babelrc.json b/.babelrc.json index 30d1c518f7..33d70c39e6 100644 --- a/.babelrc.json +++ b/.babelrc.json @@ -4,6 +4,6 @@ ], "plugins": [ "module:babel-plugin-macros", - "@babel/plugin-proposal-optional-chaining" + "@babel/plugin-transform-optional-chaining" ] } diff --git a/.cloudcmd.menu.js b/.cloudcmd.menu.js index c73086103f..0e0126b644 100644 --- a/.cloudcmd.menu.js +++ b/.cloudcmd.menu.js @@ -1,46 +1,31 @@ -'use strict'; +export default { + 'F2 - Rename File': renameCurrent, + 'L - Lint': run('npm run lint'), + 'F - Fix Lint': run('npm run fix:lint'), + 'T - Test': run('npm run test'), + 'C - Coverage': run('npm run coverage'), + 'D - Build Dev': run('npm run build:client:dev'), + 'P - Build Prod': run('npm run build:client'), +}; -module.exports = { - 'F2 - Rename file': async ({DOM}) => { - await DOM.renameCurrent(); - }, - 'L - Lint': async ({CloudCmd}) => { - const {TerminalRun} = CloudCmd; - await run(TerminalRun, 'npm run lint'); - }, - 'F - Fix Lint': async ({CloudCmd}) => { - const {TerminalRun} = CloudCmd; - await run(TerminalRun, 'npm run fix:lint'); - }, - 'T - Test': async ({CloudCmd}) => { - const {TerminalRun} = CloudCmd; - - await run(TerminalRun, 'npm run test'); - }, - 'C - Coverage': async ({CloudCmd}) => { - const {TerminalRun} = CloudCmd; +async function renameCurrent(DOM) { + await DOM.renameCurrent(); +} + +function run(command) { + return async ({CloudCmd, DOM}) => { + const {TerminalRun, config} = CloudCmd; - await run(TerminalRun, 'npm run coverage'); - }, - 'D - Build Dev': async ({CloudCmd}) => { - const {TerminalRun} = CloudCmd; + const {CurrentInfo} = DOM; + const {dirPath} = CurrentInfo; - await run(TerminalRun, 'npm run build:client:dev'); - CloudCmd.refresh(); - }, - 'P - Build Prod': async ({CloudCmd}) => { - const {TerminalRun} = CloudCmd; + const cwd = config('root') + dirPath; - await run(TerminalRun, 'npm run build:client'); - CloudCmd.refresh(); - }, -}; - -async function run(TerminalRun, command) { - await TerminalRun.show({ - command, - closeMessage: 'Press any key to close Terminal', - autoClose: false, - }); + return await TerminalRun.show({ + cwd, + command, + closeMessage: 'Press any key to close Terminal', + autoClose: false, + }); + }; } - diff --git a/.editorconfig b/.editorconfig index 09fa94224b..439abfd03a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,7 @@ root = true charset = utf-8 end_of_line = lf insert_final_newline = true -trim_trailing_whitespace = true +trim_trailing_whitespace = false indent_style = space indent_size = 4 diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 7b6b53ace5..0000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -module.exports = { - extends: [ - 'plugin:putout/recommended', - ], - plugins: [ - 'putout', - 'node', - ], - rules: { - 'key-spacing': 'off', - }, - overrides: [{ - files: ['bin/release.js'], - rules: { - 'no-console': 'off', - 'node/shebang': 'off', - }, - extends: [ - 'plugin:node/recommended', - ], - }, { - files: ['client/dom/index.js'], - rules: { - 'no-multi-spaces': 'off', - }, - }, { - files: ['bin/cloudcmd.js'], - rules: { - 'no-console': 'off', - }, - extends: [ - 'plugin:node/recommended', - ], - }, { - files: ['{client,common,static}/**/*.js'], - env: { - browser: true, - }, - }], -}; diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..9fdb6d3cc8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: coderaiser +open_collective: cloudcmd +ko_fi: coderaiser diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 7672550740..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,11 +0,0 @@ - - -- **Version** (`cloudcmd -v`): -- **Node Version** `node -v`: -- **OS** (`uname -a` on Linux): -- **Browser name/version**: -- **Used Command Line Parameters**: -- **Changed Config**: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..5c1e746046 --- /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 0000000000..5f41e73a56 --- /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 0000000000..549a88740b --- /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 0000000000..17bf5831b7 --- /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/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 96b7e2ed84..fb62bd82c8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,5 +4,5 @@ about something, just do as best as you're able. --> - [ ] commit message named according to [Contributing Guide](https://github.com/coderaiser/cloudcmd/blob/master/CONTRIBUTING.md "Contributting Guide") -- [ ] `npm run codestyle` is OK +- [ ] `npm run fix:lint` is OK - [ ] `npm test` is OK diff --git a/.github/workflows/docker-io.yml b/.github/workflows/docker-io.yml new file mode 100644 index 0000000000..beff45891d --- /dev/null +++ b/.github/workflows/docker-io.yml @@ -0,0 +1,58 @@ +name: Docker IO +on: + - push +jobs: + buildx: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v5 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Use Node.js 24.x + uses: actions/setup-node@v6 + with: + node-version: 24.x + - name: Install Redrun + run: bun i redrun -g --no-save + - name: NPM Install + run: bun i --no-save + - name: Lint + run: redrun lint + - name: Build + id: build + run: | + redrun build + VERSION=$(grep '"version":' package.json -m1 | cut -d\" -f4) + echo "version=$VERSION" >> $GITHUB_OUTPUT + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Login to DockerHub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push io-image + uses: docker/build-push-action@v7 + with: + context: . + file: docker/Dockerfile.io + platforms: linux/amd64 + push: true + tags: | + coderaiser/cloudcmd:io + coderaiser/cloudcmd:${{ steps.build.outputs.version }}-io + ghcr.io/${{ github.repository }}-io + ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-io diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 124061f742..1a85b29e10 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -6,51 +6,81 @@ on: jobs: buildx: runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - name: Checkout - uses: actions/checkout@v2 - - name: Use Node.js 14.x - uses: actions/setup-node@v2 + uses: actions/checkout@v5 + - uses: oven-sh/setup-bun@v2 with: - node-version: 16.x + bun-version: latest + - name: Use Node.js 22.x + uses: actions/setup-node@v6 + with: + node-version: 22.x + - name: Install Redrun + run: bun i redrun -g --no-save - name: NPM Install + run: bun i --no-save + - name: Lint + run: redrun lint + - name: Build + id: build run: | - npm install - - name: NPM Lint - run: | - npm run lint - - name: NPM Build - id: npm-build - run: > - npm run build - - echo "::set-output name=version::$(grep '"version":' package.json -m1 | cut -d\" -f4)" + redrun build + VERSION=$(grep '"version":' package.json -m1 | cut -d\" -f4) + echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v4 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push base-image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v7 with: context: . file: docker/Dockerfile - platforms: linux/amd64,linux/arm/v7,linux/arm64 + platforms: linux/amd64,linux/arm64 push: true tags: | coderaiser/cloudcmd:latest - coderaiser/cloudcmd:${{ steps.npm-build.outputs.version }} + 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@v2 + uses: docker/build-push-action@v7 with: context: . file: docker/Dockerfile.alpine - platforms: linux/amd64,linux/arm/v7,linux/arm64 + platforms: linux/amd64,linux/arm64 push: true tags: | + coderaiser/cloudcmd:alpine coderaiser/cloudcmd:latest-alpine - coderaiser/cloudcmd:${{ steps.npm-build.outputs.version }}-alpine + coderaiser/cloudcmd:${{ steps.build.outputs.version }}-alpine + ghcr.io/${{ github.repository }}:latest-alpine + ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-alpine + - name: Build and push slim-image + uses: docker/build-push-action@v7 + with: + context: . + file: docker/Dockerfile.slim + platforms: linux/amd64,linux/arm64 + push: true + tags: | + coderaiser/cloudcmd:slim + coderaiser/cloudcmd:latest-slim + coderaiser/cloudcmd:${{ steps.build.outputs.version }}-slim + ghcr.io/${{ github.repository }}-slim + ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-slim diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 768c0d1057..9e24773d63 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,6 +1,8 @@ name: Node CI on: - push +permissions: + contents: write jobs: build: runs-on: ubuntu-latest @@ -9,25 +11,33 @@ jobs: strategy: matrix: node-version: - - 14.x - - 16.x - - 17.x + - 22.x + - 24.x + - 26.x steps: - - uses: actions/checkout@v2 + - 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@v2 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Install Redrun - run: npm i redrun -g + run: bun i redrun -g --no-save - name: Install - run: npm install + run: bun i --no-save - name: Lint run: redrun fix:lint + - name: Typos + uses: coderaiser/typos.ai@v1.1.8 + with: + key: ${{ secrets.TYPOS_AI_KEY }} - name: Commit fixes - uses: EndBug/add-and-commit@v7 + uses: EndBug/add-and-commit@v10 + continue-on-error: true with: - message: chore(${{ env.NAME }}) lint using actions + message: "chore: ${{ env.NAME }}: actions: lint ☘️" - name: Build run: redrun build - name: Test @@ -35,6 +45,7 @@ jobs: - name: Coverage run: redrun coverage coverage:report - name: Coveralls - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@v2 + continue-on-error: true with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 690e9c074d..6aa86aa9cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,17 @@ +*.swp +*.log +*.lock + +.nyc_output +.DS_Store +.idea + package-lock.json -yarn.lock -yarn-error.log -node_modules npm-debug.log* -coverage +node_modules +coverage modules/execon modules/emitify - -.nyc_output - -*.swp -.DS_Store - dist dist-dev - diff --git a/.madrun.mjs b/.madrun.js similarity index 66% rename from .madrun.mjs rename to .madrun.js index 2f020589ed..7452328cda 100644 --- a/.madrun.mjs +++ b/.madrun.js @@ -1,44 +1,41 @@ -import { - run, - cutEnv, -} from 'madrun'; +import {run, cutEnv} from 'madrun'; +import {defineEnv} from 'supertape/env'; -const testEnv = { - THREAD_IT_COUNT: 0, -}; - -const is17 = /^v17/.test(process.version); +const testEnv = defineEnv({ + timeout: 7000, + css: true, +}); -// fix for ERR_OSSL_EVP_UNSUPPORTED on node v17 -// flag '--openssl-legacy-provider' not supported -// on earlier version of node.js -// -// https://stackoverflow.com/a/69746937/4536327 -const buildEnv = is17 && { - NODE_OPTIONS: '--openssl-legacy-provider', +const buildEnv = { + NODE_ENV: 'production', }; export default { - 'start': () => 'node bin/cloudcmd.mjs', + 'start': () => 'node bin/cloudcmd.js', 'start:dev': async () => await run('start', null, { NODE_ENV: 'development', }), 'build:start': () => run(['build:client', 'start']), - 'build:start:dev': () => run(['build:client:dev', 'start:dev']), - 'lint:all': () => run(['lint:progress', 'spell']), - 'lint': () => 'putout .', + 'build:start:dev': () => run([ + 'build:client:dev', + 'start:dev', + ]), + 'lint:all': () => run('lint:progress'), + 'lint': () => 'redlint fix; putout . --rulesdir rules', 'lint:progress': () => run('lint', '-f progress'), 'watch:lint': () => 'nodemon -w client -w server -w test -w common -w .webpack -x "putout -s"', 'fresh:lint': () => run('lint', '--fresh'), 'lint:fresh': () => run('lint', '--fresh'), - 'spell': () => 'yaspeller . || true', - 'fix:lint': () => run('lint', '--fix'), + 'fix:lint': async () => `putout --rulesdir rules --fix . && redlint fix`, 'lint:stream': () => run('lint', '-f stream'), - 'test': () => [testEnv, `tape --no-check-duplicates 'test/**/*.js' '{client,static,common,server}/**/*.spec.js' -f fail`], + 'test': () => [testEnv, `tape '{test,test-e2e}/**/*.js' '{bin,client,static,common,server}/**/*.spec.js' -f fail`], + 'test:e2e': () => `tape 'test-e2e/**/*.js'`, 'test:client': () => `tape 'test/client/**/*.js'`, 'test:server': () => `tape 'test/**/*.js' 'server/**/*.spec.js' 'common/**/*.spec.js'`, - 'wisdom': () => run(['lint:all', 'build', 'test']), - 'wisdom:type': () => 'bin/release.mjs', + 'wisdom': async () => await run(['lint:all', 'build', 'test'], null, { + CI: 1, + }), + 'wisdom:type': () => 'bin/release.js', 'coverage': async () => [testEnv, `c8 ${await cutEnv('test')}`], 'coverage:report': () => 'c8 report --reporter=lcov', 'report': () => 'c8 report --reporter=lcov', @@ -56,7 +53,9 @@ export default { 'watch:test:client': async () => `nodemon -w client -w test/client -x ${await run('test:client')}`, 'watch:test:server': async () => `nodemon -w client -w test/client -x ${await run('test:server')}`, 'watch:coverage': async () => [testEnv, `nodemon -w server -w test -w common -x ${await cutEnv('coverage')}`], + 'watch:fix:lint': async () => `nodemon -w client -w server -w test -w common -x '${await run('fix:lint')}'`, 'build': async () => run('6to5:*'), + 'build:dev': async () => run('build:client:dev'), 'build:client': () => run('6to5:client'), 'build:client:dev': () => run('6to5:client:dev'), 'heroku-postbuild': () => run('6to5:client'), diff --git a/.npmignore b/.npmignore index de3559c780..b83b59235c 100644 --- a/.npmignore +++ b/.npmignore @@ -1,32 +1,29 @@ -.* -*.spec.js +*.spec.* +*.config.* *.fixture.js* +*.ai +*.cdr +*.eps +*.log +*.lock + +.* + manifest.yml -docker docker-compose.yml -test -fixture -fixture-* -coverage -css -html -yarn-error.log -yarn.lock now.json -cssnano.config.js - app.json bower.json -manifest.yml - -bin/release.js - -client +deno.json +bin/release.* img/logo/cloudcmd-hq.png - webpack.config.js -*.ai -*.cdr -*.eps - +docker +test* +fixture +fixture-* +coverage +css +html +client diff --git a/.nycrc.json b/.nycrc.json index df171b2466..1fe174f3b9 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -1,10 +1,12 @@ { - "check-coverage": false, + "checkCoverage": false, "all": false, "exclude": [ "**/*.spec.js", + "**/*.*.js", + "**/*.config.*", "**/fixture", - "**/*.*.js" + "**/test/**" ], "branches": 100, "lines": 100, diff --git a/.putout.json b/.putout.json index e3d5807093..7154775a32 100644 --- a/.putout.json +++ b/.putout.json @@ -1,36 +1,44 @@ { + "plugins": ["cloudcmd"], "ignore": [ - "html", - "fixture*", + "*.md", "app.json", - "fontello.json" + "fontello.json", + "html", + "fixture*" ], + "rules": { + "package-json/add-type": "off" + }, "match": { - "base64": { - "convert-typeof-to-is-type": "off" + ".filesystem.json": { + "nodejs/rename-file-cjs-to-js": "off" }, - "route.spec.js": { - "tape/convert-ok-to-match": "off" - }, - "*.js": { - "convert-esm-to-commonjs": "on" + "base64": { + "types/convert-typeof-to-is-type": "off" }, "*.md": { - "convert-commonjs-to-esm": "on" + "nodejs/convert-commonjs-to-esm": "on" }, ".webpack": { "webpack": "on" }, "server": { - "remove-process-exit": "on" + "nodejs/remove-process-exit": "on" }, "server/{server,exit}.js": { - "remove-process-exit": "off" + "nodejs/remove-process-exit": "off" }, - "server/{server,exit,terminal,distribute/log}.js": { + "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" }, "test/common/cloudfunc.js": { @@ -41,6 +49,12 @@ }, "docker.yml": { "github/set-node-versions": "off" + }, + "vim.js": { + "merge-duplicate-functions": "off" + }, + "common": { + "nodejs/declare": "off" } } } diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000000..dc8af2538b --- /dev/null +++ b/.typos.toml @@ -0,0 +1,2 @@ +[files] +extend-exclude = ["ChangeLog"] diff --git a/.webpack/css.js b/.webpack/css.js index 31ab5f93c4..311290cccb 100644 --- a/.webpack/css.js +++ b/.webpack/css.js @@ -1,81 +1,45 @@ -'use strict'; +import {env} from 'node:process'; +import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; -const fs = require('fs'); -const { - basename, - extname, - join, -} = require('path'); - -const {env} = process; const isDev = env.NODE_ENV === 'development'; - -const ExtractTextPlugin = require('extract-text-webpack-plugin'); -const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); - -const extractCSS = (a) => new ExtractTextPlugin(`${a}.css`); -const extractMain = extractCSS('[name]'); - -const cssNames = [ - 'nojs', - 'view', - 'config', - 'terminal', - 'user-menu', - ...getCSSList('columns'), -]; - -const cssPlugins = cssNames.map(extractCSS); const clean = (a) => a.filter(Boolean); const plugins = clean([ - ...cssPlugins, - extractMain, - !isDev && new OptimizeCssAssetsPlugin(), + new MiniCssExtractPlugin({ + filename: '[name].css', + }), ]); const rules = [{ - test: /\.css$/, - exclude: /css\/(nojs|view|config|terminal|user-menu|columns.*)\.css/, - use: extractMain.extract([ - 'css-loader', - ]), -}, -...cssPlugins.map(extract), { - test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/, - use: { - loader: 'url-loader', + test: /\.css$/i, + use: [MiniCssExtractPlugin.loader, { + loader: 'css-loader', options: { - limit: 100_000, + url: true, }, - }, + }], +}, { + test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/, + type: 'asset/inline', }]; -module.exports = { +export default { + mode: isDev ? 'development' : 'production', plugins, module: { rules, }, + optimization: { + minimize: !isDev, + minimizer: [ + new CssMinimizerPlugin({ + minimizerOptions: { + preset: ['default', { + svgo: false, + }], + }, + }), + ], + }, }; - -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/html.js b/.webpack/html.js index 28e40c6595..e90038ac50 100644 --- a/.webpack/html.js +++ b/.webpack/html.js @@ -1,11 +1,9 @@ -'use strict'; +import {env} from 'node:process'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; -const {env} = process; const isDev = env.NODE_ENV === 'development'; -const HtmlWebpackPlugin = require('html-webpack-plugin'); - -const plugins = [ +export const plugins = [ new HtmlWebpackPlugin({ inject: false, template: 'html/index.html', @@ -13,31 +11,26 @@ const plugins = [ }), ]; -module.exports = { - plugins, -}; - function getMinifyHtmlOptions() { return { - removeComments: true, - removeCommentsFromCDATA: true, - removeCDATASectionsFromCDATA: true, - collapseWhitespace: true, - collapseBooleanAttributes: true, - removeAttributeQuotes: true, - removeRedundantAttributes: true, - useShortDoctype: true, - removeEmptyAttributes: true, + removeComments: true, + removeCommentsFromCDATA: true, + removeCDATASectionsFromCDATA: true, + collapseWhitespace: true, + collapseBooleanAttributes: true, + removeAttributeQuotes: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, /* оставляем, поскольку у нас * в элемент fm генерируеться * таблица файлов */ - removeEmptyElements: false, - removeOptionalTags: true, - removeScriptTypeAttributes: true, - removeStyleLinkTypeAttributes: true, + removeEmptyElements: false, + removeOptionalTags: true, + removeScriptTypeAttributes: true, + removeStyleLinkTypeAttributes: true, - minifyJS: true, + minifyJS: true, }; } - diff --git a/.webpack/js.js b/.webpack/js.js index bfb0af36b9..fdc8855601 100644 --- a/.webpack/js.js +++ b/.webpack/js.js @@ -1,25 +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('path'); + EnvironmentPlugin, + NormalModuleReplacementPlugin, +} = webpack; -const {EnvironmentPlugin} = require('webpack'); -const WebpackBar = require('webpackbar'); - -const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin'); - -const dir = './client'; -const dirModules = './client/modules'; const modules = './modules'; - -const {env} = process; +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'; @@ -27,72 +25,123 @@ 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, }; const rules = clean([ !isDev && { - test: /\.js$/, + test: /\.[mc]?js$/, exclude: /node_modules/, loader: 'babel-loader', }, isDev && { - test: /\.js$/, + test: /\.[mc]?js$/, exclude: /node_modules/, loader: 'babel-loader', options, - }]); + }, +]); 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(), + new webpack.ProvidePlugin({ + process: 'process/browser', + }), ]; 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', + 'columns/name-size-time', + 'columns/name-size-date-time', + ]; + + 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'), + util: import.meta.resolve('util'), + }, }, devtool, optimization: { splitChunks, }, entry: { - cloudcmd: `${dir}/cloudcmd.js`, - [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 + '/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 + '/konsole']: `${dirModules}/konsole.js`, - [modules + '/terminal']: `${dirModules}/terminal.js`, - [modules + '/terminal-run']: `${dirModules}/terminal-run.js`, - [modules + '/cloud']: `${dirModules}/cloud.js`, - [modules + '/user-menu']: `${dirModules}/user-menu/index.js`, - [modules + '/polyfill']: `${dirModules}/polyfill.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`, + 'columns/name-size-date-time': `${dirColumns}/name-size-date-time.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.js`, + [`${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/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}/konsole`]: `${dirModules}/konsole.js`, + [`${modules}/terminal`]: `${dirModules}/terminal.js`, + [`${modules}/terminal-run`]: `${dirModules}/terminal-run.js`, + [`${modules}/cloud`]: `${dirModules}/cloud.js`, + [`${modules}/user-menu`]: `${dirModules}/user-menu/index.js`, + [`${modules}/polyfill`]: `${dirModules}/polyfill.js`, + [`${modules}/command-line`]: `${dirModules}/command-line.js`, }, output: { filename: '[name].js', @@ -101,9 +150,6 @@ module.exports = { devtoolModuleFilenameTemplate, publicPath: '/dist/', }, - externals: [ - externals, - ], module: { rules, noParse, @@ -115,20 +161,7 @@ module.exports = { }, }; -function externals(context, request, fn) { - if (!isDev) - return fn(); - - const list = []; - - if (list.includes(request)) - return fn(null, request); - - fn(); -} - function devtoolModuleFilenameTemplate(info) { const resource = info.absoluteResourcePath.replace(rootDir + sep, ''); return `file://cloudcmd/${resource}`; } - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7438bf1bbb..b6b6b4efb3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,12 +4,12 @@ Format of the commit message: **type(scope) subject** **Type**: -- feature(scope) subject -- fix(scope) subject -- docs(scope) subject -- refactor(scope) subject -- test(scope) subject -- chore(scope) subject +- feature: scope: subject +- fix: scope: subject +- docs: scope: subject +- refactor: scope: subject +- test: scope: subject +- chore: scope: subject **Scope**: Scope could be anything specifying place of the commit change. @@ -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 5d612dfa60..49ed06d59c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,1546 @@ +2026.05.26, v19.19.0 + +feature: +- 4ee38db6 client: view: flac: add support + +2026.05.26, v19.18.1 + +feature: +- 40ecef5e cloudcmd: ratelimit: X-Forwarded-For (#437) + +2026.05.26, v19.18.0 + +fix: +- 161bede8 client: menu: @putout/bundle@5.5. +- 447d990f cloudcmd: server: rest: sendError + +feature: +- 5d9628ce cloudcmd: rate limit: add support (#437) +- 2be395e6 cloudcmd: get rid of manifest: 401 basic auth + +2026.05.17, v19.17.0 + +fix: +- d96f6c46 css: query: hide time on mobile +- e7c55e05 docker: io: XDG_CONFIG_HOME + +feature: +- ae1ca0f8 cloudcmd: cssnano-preset-default v8.0.1 +- 94d5096a style: owner, mode: improve +- 807f8346 cloudcmd: superc8 v12.6.0 +- 67a95722 docker: io: cline: add +- 68eacb91 qword: add +- 4acd294b docker: io: tmux +- 05c80043 cloudcmd: @supertape/loader-css v1.0.0 +- 800a6545 cloudcmd: eslint-plugin-n v18.0.1 +- a985bb36 cloudcmd: supertape v13.0.0 +- 4315ec61 docker: io XDG_CONFIG_HOME: /etc -> /usr/local/etc + +2026.05.03, v19.16.0 + +feature: +- d82d0335 client: vim: rr for rename file +- acfa27cf docker: io: nvchad: add +- 14f009f8 docker: io: bash-completion: add +- c6c60146 docker: io: f4 +- 9a6b8935 docker: io: GOPATH +- a9bc22ce cloudcmd: operation: rm useless checks +- 36bacfe9 cloudcmd: client: key: vim: cc, mm +- 591da25c cloudcmd: ponse v8.0.0 +- 1e1b073c docker: io: /usr/src/cloudcmd -> /usr/local/share/cloudcmd + +2026.04.28, v19.15.0 + +feature: +- 9d97343b cloudcmd: operation: rm useless checks +- 46a88cfa cloudcmd: client: key: vim: cc, mm + +2026.04.28, v19.14.0 + +feature: +- 36a8b641 cloudcmd: ponse v8.0.0 +- 1c263c18 docker: io: /usr/src/cloudcmd -> /usr/local/share/cloudcmd +- ea7b828c docker: io: ubuntu: resolute +- cd6c11ac docker: io: far2l +- c4beeec6 docker: io: gdu: add + +2026.04.21, v19.13.1 + +feature: +- d5cd11e8 cloudcmd: montag v2.0.1 +- dc94d2db docker: io: add latest git version +- 1637beee docker: io: /usr/local/src -> /usr/local/share +- b1bc4e73 Docker: io: pv +- 7c0dca60 docker: io: git: master + +2026.04.15, v19.13.0 + +fix: +- 48693d9e docker: io: XDG_CACHE_HOME + +feature: +- 4b2395c5 cloudcmd: Ctrl + L: logout +- 977a8aaa docker: io: strace: add +- 7d0098fd docker: io: XDG_CACHE_HOME=/tmp/cache +- a89e901b actions: docker: io: add + +2026.04.12, v19.12.5 + +feature: +- 1cfc1a6f docker: io: only amd64 + +2026.04.12, v19.12.4 + +feature: +- 3c2b5658 docker: io: haskell +- d37f8cd3 docker: io: palabra: node + +2026.04.12, v19.12.3 + +feature: +- cb6cabd4 docker: io: PALABRA_DIR + +2026.04.11, v19.12.2 + +feature: +- 2ce11fa2 docker: io: get rid of haskell: to slow install +- aa741232 docker: io: rizin, yara + +2026.04.09, v19.12.1 + +feature: +- 5bd03215 docker: io: add PREFIX + +2026.04.09, v19.12.0 + +feature: +- 13b15b7b docker: io: palabra + +2026.04.07, v19.11.14 + +feature: +- a31beab0 docker: io: rustup env + +2026.04.07, v19.11.13 + +feature: +- 6a08479a docker: io: BUN_INSTALL + +2026.04.06, v19.11.12 + +feature: +- 46c65554 docker: io: npm_config_cache + +2026.04.06, v19.11.11 + +feature: +- ae2ce388 docker: io: $DENO_DIR + +2026.04.06, v19.11.10 + +feature: +- 12ea14ac docker: io: nvm: node + +2026.04.06, v19.11.9 + +feature: +- ea96d13f docker: io: net-tools + +2026.04.06, v19.11.8 + +feature: +- 5c08565f docker: io: debian -> ubuntu + +2026.04.05, v19.11.7 + +feature: +- 36cdef37 docker: io: bookworm + +2026.04.05, v19.11.6 + +feature: +- 845f9bd1 cloudcmd: gritty v10.2.0 + +2026.04.05, v19.11.5 + +feature: +- f9c513cc docker: io: hexyl: add + +2026.04.04, v19.11.4 + +feature: +- e7347d25 docker: io: neovim: apt -> github + +2026.04.04, v19.11.3 + +feature: +- 9970ff76 docker: io: btop +- 4395a471 docker: io: $PATH: add $HOME/.local/bin + +2026.04.04, v19.11.2 + +feature: +- c40ae8e4 docker: io: htop +- f463c5c5 docker: io: aptitude: add +- d1032f09 docker: io: remove unused + +2026.04.04, v19.11.1 + +feature: +- 7787bfc2 cloudcmd: user-menu: runFromCDN + +2026.04.04, v19.11.0 + +feature: +- 32f89d38 cloudcmd: user-menu: root + +2026.04.04, v19.10.2 + +feature: +- df4fb517 cloudcmd: aleman v2.0.1 + +2026.04.03, v19.10.1 + +feature: +- ceb7ef4f docker: io: keep /var/lib/apt/lists + +2026.04.02, v19.10.0 + +fix: +- d12e7bd0 distribute: fix event listener leak on socket disconnect (#462) + +2026.04.02, v19.9.24 + +feature: +- c4d26c6a docker: io: apt-get upgrade +- 9ddb8c29 docker: io: get rid of nix +- e4d7d441 docker: io: nix + +2026.04.01, v19.9.23 + +feature: +- 5a3413ce docker: io: nix + +2026.03.31, v19.9.22 + +feature: +- b4345ed4 docker: io: DENO_DIR + +2026.03.31, v19.9.21 + +fix: +- 5c6a9a95 css: columns: name: 40% -> 35% + +2026.03.30, v19.9.20 + +feature: +- cd0b5554 iocmd: io: nvm + +2026.03.30, v19.9.19 + +feature: +- 6a52b11e docker: io: go, rust + +2026.03.30, v19.9.18 + +feature: +- 738059f2 docker: io: fzf +- 3fc8932f docker: io: less, el_GR + +2026.03.30, v19.9.17 + +feature: +- cf424d6c docker: io: command-not-found update + +2026.03.29, v19.9.16 + +feature: +- 19347a2b docker: io: add command-not-found + +2026.03.29, v19.9.15 + +feature: +- ee170552 docker: io: ubuntu +- e04c4594 docker: io: net-tools: add + +2026.03.29, v19.9.14 + +fix: +- 6c7709be docker: io: PS1 + +2026.03.29, v19.9.13 + +fix: +- 439b3710 bin: currify +- e4182841 docker: io: xterm-256color + +2026.03.29, v19.9.12 + +feature: +- 811a47fd cloudcmd: bin: get rid of require + +2026.03.29, v19.9.11 + +feature: +- 1f95f188 cloudcmd: get rid of simport + +2026.03.29, v19.9.10 + +feature: +- f0deb323 docker: io: add ja_JP.UTF-8 + +2026.03.29, v19.9.9 + +fix: +- f671f798 docker: io: PS1 environment variable in Dockerfile + +feature: +- cc7f9dc7 docker: io: use ubuntu + +2026.03.28, v19.9.8 + +feature: +- 4d1cd8cd docker: io: ffmpeg +- 54b56fdc cloudcmd: vim: ESC: use only to enable, do not use to disable + +2026.03.27, v19.9.7 + +feature: +- 110908e2 docker: io: apt-get + +2026.03.26, v19.9.6 + +feature: +- 6450a2f8 docker: io: add UTF-8 + +2026.03.26, v19.9.5 + +fix: +- e761cacb columns: name-size-date-time: 20% -> 19% + +2026.03.26, v19.9.4 + +feature: +- 69498ed6 docker: io: pull.rebase by default + +2026.03.24, v19.9.3 + +feature: +- 8763788b docker: io: healthcheck + +2026.03.23, v19.9.2 + +feature: +- 09a02074 docker: io: git config: add +- c448eaa4 docker: io: buni + +2026.03.23, v19.9.1 + +fix: +- 6e5318fa client: modules: config: input: quote + +2026.03.23, v19.9.0 + +feature: +- a1216cdd cloudcmd: add ability to hide port configuration + +2026.03.23, v19.8.15 + +feature: +- 68b2aa78 docker: io: cloudcmd_vim + +2026.03.23, v19.8.14 + +fix: +- 665ed9c2 docker: io: get back port + +2026.03.23, v19.8.13 + +feature: +- 618a5615 docker: io: PS1 + +2026.03.23, v19.8.12 + +feature: +- 556b0150 docker: io: PS1 + +2026.03.23, v19.8.11 + +fix: +- 97672ef5 docker: io: apt-get install -> apt-get + +feature: +- 67f27d84 docker: io: bun, deno + +2026.03.23, v19.8.10 + +feature: +- 025b005e docker: io: add PS1 + +2026.03.23, v19.8.9 + +fix: +- b052cf22 cloudcmd: no time available: --.--.---- -> --:--:-- (#461) + +2026.03.23, v19.8.8 + +feature: +- 02dbe56d server: user-menu: when error send it + +2026.03.23, v19.8.7 + +feature: +- ecc76e8b docker: io: renamify-cli, runny, redfork + +2026.03.23, v19.8.6 + +fix: +- 53c072ab @putout/plugin-cloudcmd: devDependencies -> dependnecies + +feature: +- 4b9922bf docker: /usr/src: app -> cloudcmd + +2026.03.23, v19.8.5 + +fix: +- 56fc8b83 docker: gritty + +2026.03.22, v19.8.4 + +feature: +- 5000227e docker: vim nvim + +2026.03.22, v19.8.3 + +fix: +- 01677e6a docker: io: slim -> io + +feature: +- 7e35c606 docker: io: curl wget + +2026.03.22, v19.8.2 + +feature: +- e5b221f7 docker: io: add + +2026.03.22, v19.8.1 + +feature: +- 708a4c6b docker: slim: add +- 80613f46 docker: slim: add + +2026.03.20, v19.8.0 + +fix: +- 59037f2c cloudcmd: bin: --show-config + +feature: +- 10934b3a cloudcmd: add ability to show modification time (#230) + +2026.03.18, v19.7.1 + +feature: +- b0c1d36c cloudcmd: @cloudcmd/fileop v9.0.7 (#460) + +2026.03.17, v19.7.0 + +feature: +- daf83875 cloudfunc: override date format (#459) + +2026.03.17, v19.6.9 + +feature: +- b28a070a cloudcmd: redzip v4.6.1 +- 43c5a011 cloudcmd: css-minimizer-webpack-plugin v8.0.0 +- 15dcae5c cloudcmd: webpack-cli v7.0.2 +- 5976da81 cloudcmd: @cloudcmd/fileop v9.0.5 +- 37cb83f2 cloudcmd: redzip v4.6.0 + +2026.02.27, v19.6.8 + +feature: +- 15fab514 cloudcmd: copymitter v10.3.0 (#458) + +2026.02.26, v19.6.7 + +feature: +- 68c7d0be cloudcmd: onezip v7.0.0 + +2026.02.26, v19.6.6 + +feature: +- 3987cc82 cloudcmd: redzip v4.5.1 (#457) + +2026.02.26, v19.6.5 + +feature: +- 964ae989 cloudcmd: redzip v4.5.0 (#457) + +2026.02.25, v19.6.4 + +feature: +- a66eeda3 cloudcmd: copymitter v10.2.0 (coderaiser/cloudcmd#457) +- 4340533a cloudcmd: c8 v11.0.0 +- 0857711f cloudcmd: redzip v4.2.0 (#457) + +2026.02.24, v19.6.3 + +feature: +- 2234f1b4 cloudcmd: redzip v4.2.0 (#475) + +2026.02.24, v19.6.2 + +feature: +- 321a54dd cloudcmd: @cloudcmd/fileop v9.0.2 (#457) + +2026.02.24, v19.6.1 + +feature: +- 7c5ac408 cloudcmd: @cloudcmd/fileop v9.0.1 (#457) + +2026.02.21, v19.6.0 + +feature: +- 6d19bf2e common: object.omit -> omit + +2026.02.18, v19.5.1 + +feature: +- 6e1cf4ef cloudcmd: supermenu v5.0.0 + +2026.02.18, v19.5.0 + +feature: +- b20539ef common: entity: encode {,} +- 7ef134f4 cloudcmd: rendy v5.0.0 + +2026.02.18, v19.4.1 + +feature: +- 1e18d513 cloudcmd: @cloudcmd/fileop v9.0.0 + +2026.02.18, v19.4.0 + +fix: +- 45cf9baf menu: prefix (#456) + +feature: +- 3e647290 cloudcmd: redlint v6.0.0 +- 800ed012 cloudcmd: putout v42.0.5 +- 525c17d4 cloudcmd: madrun v13.0.0 +- 44247499 cloudcmd: eslint-plugin-putout v31.0.0 + +2026.02.15, v19.3.9 + +feature: +- 9ffe3ef1 cloudcmd: copymitter v10.0.0 (#457) + +2026.02.15, v19.3.8 + +fix: +- d274a2b3 spinner (#456) + +2026.02.13, v19.3.7 + +feature: +- 8fd79a27 cloudcmd: win32 v8.0.0 + +2026.02.12, v19.3.6 + +feature: +- ac94eccd cloudcmd: konsole: named +- 144e4a34 cloudcmd: gritty v10.0.0 +- 938f9e76 cloudcmd: console-io v15.0.1 + +2026.02.08, v19.3.5 + +feature: +- fb40bd9c rm cssnano: has no sense for spinner, option disabled by default (https://svgo.dev/docs/plugins/convertPathData/) +- 76125be9 cloudcmd: eslint v10.0.0 + +2026.02.06, v19.3.4 + +feature: +- 66a08c7f cloudcmd: deepword v11.0.0 + +2026.02.05, v19.3.3 + +feature: +- 4a5a56f4 cloudcmd: dword v16.0.0 + +2026.02.04, v19.3.2 + +fix: +- 99d8435e cloudcmd: exports +- a266c145 cloudcmd: default -> named +- 6e3ba271 Closing X in editor disappeared (#455) + +feature: +- 30f42e94 cloudcmd: restafary v13.0.1 +- f84ce853 cloudcmd: edward v16.0.0 + +2026.02.03, v19.3.1 + +fix: +- 5661bc4f Closing X in editor disappeared (#455) + +2026.02.03, v19.3.0 + +feature: +- 4533a25c cloudcmd: migrate to ESM +- e8a81c49 client: dom: events: migrate to ESM +- 071141bc client: terminal-run: migrate to ESM + +2026.02.03, v19.2.0 + +feature: +- bb32f7c4 client: dom: migrate to ESM +- 457c83db client: migrate to ESM +- f8a941bd client: modules: markdown: migrate to ESM +- 9d6cffaf client: polifyll: migrate to ESM +- 5b704d06 client: edit-names: migrate to ESM +- 2c2ca8eb client: edit-file: migrate to ESM +- c9f57c5f client: modules: edit-file-vim: migrate to ESM +- 327ac9de client: modules: help: migrate to ESM +- dc34ee87 cloudcmd: @putout/plugin-cloudcmd v5.0.0 +- 3cd3695b client: modules: edit-names-vim: migrate to ESM +- dfcbfd63 client: modules: terminal: migrate to ESM + +2026.02.02, v19.1.21 + +feature: +- e3ad330e client: konsole: migrate to ESM +- 983fd9af client: edit: migrate to ESM +- ebabad94 common: entity: migrate to ESM +- c3b71653 client: dom: images -> #images +- 8cad7514 common: cloudfunc -> #common/cloudfunc +- 1f174870 client: view: migrate to ESM +- 7173f6cb cloudcmd: smalltalk v5.0.0 + +2026.01.31, v19.1.20 + +feature: +- c1014c9c client: dom: operations: rename-current: migrate to ESM +- 2e486f8b cloudcmd: restafary v13.0.0 +- 6addb29f cloudcmd: redzip v4.0.0 + +2026.01.30, v19.1.19 + +feature: +- 2a0feac7 cloudcmd: @cloudcmd/modal v4.0.0 + +2026.01.30, v19.1.18 + +feature: +- 73fa4961 client: modules: user-menu: migrate to ESM +- ebfdf8c0 client: modules: operation: migrate to ESM +- ad683171 client: modules: menu: migrate to ESM +- 0054cfa3 client: dom: load-remote: migrate to ESM +- e4d0ece0 client: dom: rest: migrate to ESM +- c704ffe4 client: dom: storage: migrate to ESM +- 5182cb81 client: modules: upload: migrate to ESM +- 9543f1ad client: dom: upload-files: migrate to ESM + +2026.01.29, v19.1.17 + +feature: +- f9c28319 client: dom: migrate to ESM +- 9d2c4e4a client: dom: cmd: move out +- 5a2aa70f client: dom: simplify require +- dee50a85 client: dom: files: migrate to ESM +- 23c0d770 client: dom: dom-tree: migrate to ESM +- cf2c6415 client: directory: migrate to ESM +- 0eb802e7 client: dom: dialog: migrate to ESM + +2026.01.28, v19.1.16 + +fix: +- 4c242631 css: spinner: do not minify svg + +2026.01.28, v19.1.15 + +feature: +- 265c0b49 client: key: vim: migrate to ESM +- 3bc49f02 client: set-current-by-char: migrate to ESM + +2026.01.28, v19.1.14 + +feature: +- f8a63b5a client: key: binder: migrate to ESM +- 2cc97f33 client: cloud: migrate to ESM + +2026.01.27, v19.1.13 + +feature: +- 41b5a96f client: load: migrate to ESM + +2026.01.27, v19.1.12 + +feature: +- 27a52d33 client: sw: migrate to ESM +- 6b049d95 client: sw: register: migrate to ESM + +2026.01.25, v19.1.11 + +feature: +- f849b842 client: listeners: migrate to ESM +- 091f9017 client: listeners: simplify +- 3c1a82e7 client: key: view: get rid of mock-require + +2026.01.25, v19.1.10 + +feature: +- dc5867b0 client: key: vim: get rid of mock-require +- 77b70b21 cloudcmd: aleman v2.0.0 + +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: +- b088b84e cloudcmd: deepword v10.0.0 + +2024.03.29, v17.3.2 + +fix: +- f7a6a366 typo: Wraped -> Wrapped + +2024.03.29, v17.3.1 + +feature: +- d7581829 distribute: convert to ESM + +2024.03.29, v17.3.0 + +feature: +- 6bc4f3ec dark theme: add (#332) +- 35622082 route: convert to ESM + +2024.03.29, v17.2.1 + +fix: +- cc134464 client: vim: space + +feature: +- e3f89e88 dark mode: add +- c45b23fe css: vars: add +- b1f74c00 css: add vars + +2024.03.22, v17.2.0 + +feature: +- 3e565109 convert to ESM +- 770a0812 pack: get rid of mock-require +- 25d8faea rest: get rid of mock-require +- 401a669a user-menu: get rid of mock-require +- 4e32241d terminal: get rid of mock-require + +2024.03.21, v17.1.6 + +feature: +- e01ee457 server: route: get rid of mock-require +- c7f90901 root: get rid of mock-require +- fcce26d4 cloudfunc: get rid of mock-require + +2024.03.20, v17.1.5 + +feature: +- bf90bf22 server: validate: get rid of mock-require + +2024.03.20, v17.1.4 + +feature: +- 98d3a4cc server: columns: get rid of mock-require + +2024.03.18, v17.1.3 + +feature: +- e080a540 server: cloudcmd: get rid of mock-require + +2024.03.18, v17.1.2 + +fix: +- 857c9700 docker: alpine + +feature: +- bf614e1d cloudcmd: redlint v3.13.1 + +2024.03.16, v17.1.1 + +feature: +- a92a5a0d cloudcmd: restbox v4.0.0 +- c51ba1d8 docker: drop arm v7 + +2024.03.16, v17.1.0 + +feature: +- 10d6d2e2 cloudcmd: @types/node-fetch v2.6.11 +- 2047cb7a cloudcmd: @cloudcmd/dropbox v5.0.1 +- 6b793cca cloudcmd: docker: alpine +- 5fa9fcc5 cloudcmd: pullout v5.0.0 +- bc617c17 cloudcmd: serve-once v3.0.1 + +2024.03.12, v17.0.7 + +feature: +- 97627dc2 cloudcmd: auto-globals v4.0.0 +- 683c865e cloudcmd: gritty v8.0.0 + +2024.03.11, v17.0.6 + +fix: +- d928c0b8 cloudcmd: ocker: revert alpine (#406) + +2024.03.11, v17.0.5 + +fix: +- 33201dad cloudcmd: docker: alpine (#406) + +2024.02.02, v17.0.4 + +feature: +- 7ce95450 cloudcmd: deepword v9.0.0 +- 1c73e525 cloudcmd: edward v15.0.0 +- da967f08 cloudcmd: dword v15.0.0 +- f0a6109a cloudcmd: restafary v12.0.0 + +2024.02.01, v17.0.3 + +feature: +- aca4119f cloudcmd: inly v5.0.0 + +2024.02.01, v17.0.2 + +feature: +- 5324a41a cloudcmd: supertape v10.0.0 +- d453a1b2 cloudcmd: onezip v6.0.1 +- 71b915be cloudcmd: @cloudcmd/fileop v8.0.0 + +2024.01.25, v17.0.1 + +feature: +- d79a5776 cloudcmd: putout v35.0.0 +- 8d92aa91 cloudcmd: package-json v9.0.0 +- 5ab5576e cloudcmd: open v10.0.3 +- c5cfe68c cloudcmd: c8 v9.1.0 + +2023.12.12, v17.0.0 + +feature: +- 154e3b07 cloudcmd: find-up v7.0.0 +- a02d288d cloudcmd: @putout/babel v2.0.0 +- 3314f9b9 drop support of node < 18 +- a6afa205 cloudcmd: supertape v9.0.0 +- e600c05d cloudcmd: eslint-plugin-putout v22.0.0 +- 99c00954 cloudcmd: madrun v10.0.0 +- afdf7434 cloudcmd: putout v34.0.7 + +2023.12.08, v16.18.0 + +feature: +- 4462f269 cloudcmd: markdown-it v14.0.0 +- 0cc76bd4 cloudcmd: philip v3.0.0: decrease bundle size +- 656ebd87 client: move out set-current + +2023.12.04, v16.17.9 + +fix: +- fb2d0814 github actions + +2023.12.04, v16.17.8 + +feature: +- 93aa7278 yaspeller: rm +- af9c916e cloudcmd: eslint-plugin-putout v21.0.2 +- 01dccbfd cloudcmd: putout v33.13.3 + +2023.10.12, v16.17.7 + +feature: +- 7857fb72 package: rendy v4.1.3 + +2023.09.22, v16.17.6 + +feature: +- 570cb8c0 package: nodemon v3.0.1 +- 828d10a8 package: rimraf v5.0.1 +- 88908b35 package: eslint-plugin-putout v20.0.0 +- 93f4a07e package: putout v32.0.6 +- a5f93523 github actions: use bun + +2023.09.06, v16.17.5 + +feature: +- 3b0941bc actions: docker/login-action@v2 (#396) + +2023.09.05, v16.17.4 + +feature: +- 35dedfdf github: update docker actions (#396) + +2023.09.05, v16.17.3 + +feature: +- 3c93b29b github: docker/build-push-action v4 (#396) + +2023.09.04, v16.17.2 + +fix: +- 621f52da docker + +2023.09.04, v16.17.1 + +fix: +- eb3f037a docker: bun -> node + +2023.09.04, v16.17.0 + +feature: +- 224e5397 docker: use bun instead of node +- 4b9267f3 package: edward v14.2.0 + +2023.08.10, v16.16.3 + +feature: +- 354c137d package: console-io v14.0.0 + +2023.08.09, v16.16.2 + +feature: +- e353fff7 package: redzip v3.0.0 + +2023.08.07, v16.16.1 + +feature: +- d63169cc package: @putout/babel v1.0.5 +- bb9276be package: eslint-plugin-putout v19.0.3 +- 9aed5f75 package: putout v31.0.3 + +2023.07.09, v16.16.0 + +feature: +- c4f56c59 package: memfs v4.2.0 +- 0e663e1d package: @putout/plugin-cloudcmd v3.1.1 +- ff9265b7 package: c8 v8.0.0 +- 22aa337a package: eslint-plugin-n v16.0.1 +- 13350b55 package: eslint-plugin-putout v18.0.0 +- ce196abf package: putout v30.1.1 + +2023.05.17, v16.15.0 + +feature: +- a1bf40bd package: open v9.1.0 +- ef608853 client: self signed certs on Chrome (#393) + +2023.03.21, v16.14.1 + +feature: +- b336a472 package: eslint-plugin-putout v17.1.0 +- 019e15b8 package: @cloudcmd/fileop v7.0.0 +- 64354300 package: copymitter v8.0.1 +- 27437880 package: @cloudcmd/move-files v7.0.0 + +2023.03.08, v16.14.0 + +feature: +- 6b22b241 package: putout v29.0.3 + +2023.01.30, v16.13.1 + +feature: +- client: add DIR_DIST +- client: DIRCLIENT -> DIR_CLIENT + +2023.01.29, v16.13.0 + +feature: +- client: key: vim: open editor with 'e' +- client: vim: add ability to show terminal with 'tt' + +2023.01.29, v16.12.0 + +feature: +- client: vim: add ability to create directory with 'md', and create file with 'mf' + +2023.01.22, v16.11.0 + +fix: +- lint: eslint-plugin-node -> eslint-plugin-n + +feature: +- user-menu: add support of mov +- client: user-menu: navigate: add support of \D + [JK] to speed up vim navigation + +2023.01.19, v16.10.0 + +fix: +- lint: eslint-plugin-node -> eslint-plugin-n + +feature: +- client: user-menu: navigate: add support of \D + [JK] to speed up vim navigation + +2023.01.18, v16.9.1 + +fix: +- static: user-menu: convert flac to mp3 + +2023.01.17, v16.9.0 + +feature: +- package: scroll-into-view-if-needed v3.0.4 +- package: tar-stream v3.0.0 +- static: user-menu: add recipes from Cookbook + +2023.01.16, v16.8.0 + +feature: +- package: @putout/plugin-cloudcmd v2.0.0: CloudCmd.loadDir() -> CloudCmd.changeDir()' + +2023.01.15, v16.7.0 + +feature: +- package: @cloudcmd/modal v3.0.0: add ability to not set cursor when close the modal +- package: auto-globals v3.0.0 +- package: rimraf v4.0.5 +- user-menu: rm border + +2022.10.20, v16.6.1 + +feature: +- package: package-json v8.1.0 +- package: supertape v8.1.0 +- package: putout v28.0.0 + +2022.10.09, v16.6.0 + +feature: +- package: @cloudcmd/fileop v6.0.0 +- package: @cloudcmd/move-files v6.0.0 +- package: copymitter v7.0.0 + +2022.08.06, v16.5.0 + +feature: +- client: add Command Line +- package: add funding +- (package) gritty v7.0.0 + +2022.07.20, v16.4.1 + +feature: +- (package) eslint-plugin-n v15.2.4 +- (package) putout v27.0.1 +- (package) eslint-plugin-putout v16.0.0 + + +2022.07.11, v16.4.0 + +feature: +- (cloudcmd) env: add ability to pass 0 and 1 + + +2022.07.02, v16.3.1 + +fix: +- (client) cloudcmd: rm window.Emitify + + +2022.07.01, v16.3.0 + +feature: +- (cloudcmd) terminal-run: return -1, when not load + + +2022.06.17, v16.2.0 + +feature: +- (package) markdown-it v13.0.1 +- (cloudcmd) server: convert to ESM +- (package) thread-it v2.0.0 + + +2022.05.12, v16.1.1 + +feature: +- (package) edward v14.0.0 +- (package) putout v26.0.1 + + +2022.04.23, v16.1.0 + +feature: +- (cloudcmd) improve support of NBSP + + +2022.04.22, v16.0.1 + +feature: +- (package) dword v14.0.0 +- (package) restafary v11.0.0 +- (package) @cloudcmd/stub v4.0.1 +- (package) win32 v7.0.0 +- (package) eslint-plugin-putout v14.4.0 + + +2022.02.19, v16.0.0 + +feature: +- (cloudcmd) drop support of node < 16 +- (package) supertape v7.1.0 +- (package) eslint-plugin-putout v13.11.0 +- (package) madrun v9.0.0 +- (package) putout v25.0.1 + +2022.01.20, v15.9.15 + +fix: +- (css) icons (#368) + +2022.01.13, v15.9.14 + +fix: +- (cloudcmd) client: edit-names in vim mode +- (docker) images: make dockerfiles use node:lts-buster and node:lts-buster-slim for alpine (#363) +- (docker) images: make dockerfiles use node:lts-buster-slim as base image (#357) +feature: +- (package) eslint-plugin-putout v13.0.1 +- (package) putout v24.0.2 + 2021.12.23, v15.9.13 feature: @@ -5216,7 +6759,7 @@ fix: - (rest) onDelete: func(null, body) -> func - (rest) onStat: add var - (cloudcmd) change index path -- (server) start: url -> URL +- (server) start: url -> PREFIX - (server) start: SSLPort -> sslPort - (server) start: Port -> port - (dom) getCurrentDirPath: "," -> ";" @@ -6228,7 +7771,7 @@ with clicks on files. - Removed allowed from cache property in config. - Added ability to hide "Upload To" menu item if -no storage modules setted up in modules.json. +no storage modules set up in modules.json. - From now any file minifying only if last modified time was changed. @@ -6434,7 +7977,7 @@ in any position in anyLoadOnLoad function. - Added chainable to Cache object in DOM. - Changed default ip to null so IP would be geted from -config.json only if it setted up. +config.json only if it set up. - Fixed bug with starting node from other then projects dir. @@ -6743,7 +8286,7 @@ time was possible. - Fixed bug with undefined size on root directory of Cloud Commander. Now Cloud Commander writes size 0, if can't get size, and besides will -setted b char: "0b". +set b char: "0b". - Added supporting of Russian language in directory names. diff --git a/HELP.md b/HELP.md index 0427c1b80d..0b32ca8def 100644 --- a/HELP.md +++ b/HELP.md @@ -1,19 +1,25 @@ -# Cloud Commander v15.9.13 +# Cloud Commander v19.19.0 -### [Main][MainURL] [Blog][BlogURL] +### [Main][MainURL] [Blog][BlogURL] [Demo][DemoURL] [Deploy](#deploy) -[MainURL]: http://cloudcmd.io "Main" -[BlogURL]: http://blog.cloudcmd.io "Blog" +[MainURL]: https://cloudcmd.io "Main" +[BlogURL]: https://blog.cloudcmd.io "Blog" +[SupportURL]: https://patreon.com/coderaiser "Patreon" +[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" +[QWORD]: https://github.com/cloudcmd/qword "Editor based on CodeMirror 6" [EDWARD_KEYS]: https://github.com/cloudcmd/edward/#hot-keys "Edward Hot keys" [TERMUX]: https://termux.com "Termux" [INLY]: https://github.com/coderaiser/node-inly "Extract archive" +[DeployInstaPodsIMG]: https://img.shields.io/badge/deploy%20on-InstaPods-blue +[DeployInstaPodsURL]: https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd + **Cloud Commander** is a file manager for the web. It includes a command-line console and a text editor. Cloud Commander helps you manage your server and work with files, directories and programs in a web browser from any computer, mobile or tablet. -![Cloud Commander](/img/logo/cloudcmd.png "Cloud Commander") +![Cloud Commander](https://cloudcmd.io/img/logo/cloudcmd.png "Cloud Commander") ## Benefits @@ -24,7 +30,7 @@ - Server works on **Windows**, **Linux**, **Mac OS** and **Android** (with help of [Termux][TERMUX]). - Can be used local or remotely. - Adapts to screen size. -- **3 built-in editors** with support of **syntax highlighting**: [Dword][DWORD], [Edward][EDWARD] and [Deepword][DEEPWORD]. +- **3 built-in editors** with support of **syntax highlighting**: [Dword][DWORD], [Edward][EDWARD] [Deepword][DEEPWORD] and [Qword][QWORD]. - [Console](https://github.com/cloudcmd/console "Console") with support of the default OS command line. - Written in **JavaScript/Node.js**. - Built-in archives pack: **zip** and **tar.gz**. @@ -57,81 +63,86 @@ 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 -| `--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-columns` | set default visible columns -| `--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 +| 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 +| `--config-port` | enable port 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-config-port` | disable port 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 For options not specified by command-line parameters, Cloud Commander then reads configuration data from `~/.cloudcmd.json`. It uses port `8000` by default. -To begin using the web client, go to this URL in your browser: +To begin using the web client, go to this PREFIX in your browser: ``` http://localhost:8000 @@ -149,74 +160,85 @@ 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 -| `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 + L` | logout +| `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 When the `--vim` option is provided, or the configuration parameter `vim` is set, the following hotkeys become available: -|Key |Operation -|:----------------------|:-------------------------------------------- -| `j` | navigate to next file -| `k` | navigate to previous file -| `dd` | remove current file -| `G` or `$` | navigate to bottom file -| `gg` or `^` | navigate to top file -| `v` | visual mode -| `y` | copy (selected in visual mode files) -| `p` | paste files -| `Esc` | unselect all -| `/` | find file in current directory -| `n` | navigate to next found file -| `N` | navigate to previous found file +| Key |Operation +|:------------|:-------------------------------------------- +| `j` | navigate to next file +| `k` | navigate to previous file +| `dd` | remove current file +| `G` or `$` | navigate to bottom file +| `gg` or `^` | navigate to top file +| `v` | visual mode +| `y` | copy (selected in visual mode files) +| `p` | paste files +| `Esc` | unselect all +| `/` | find file in current directory +| `n` | navigate to next found file +| `N` | navigate to previous found file +| `md` | make directory +| `mf` | make file +| `tt` | show terminal +| `e` | edit file +| `cc` | copy +| `mm` | move +| `rr` | rename file Commands can be joined, for example: @@ -367,50 +389,51 @@ Here's a description of all options: ```json { - "name" : "", // set tab name in web browser - "auth" : false, // enable http authentication - "username" : "root", // username for authentication - "password" : "toor", // password hash for authentication - "algo" : "sha512WithRSAEncryption", // cryptographic algorithm - "editor" : "edward", // default, could be "dword" or "edward" - "packer" : "tar", // default, could be "tar" or "zip" - "diff" : true, // when save - send patch, not whole file - "zip" : true, // zip text before send / unzip before save - "buffer" : true, // buffer for copying files - "dirStorage" : true, // store directory listing - "online" : false, // do not load js files from cdn - "open" : true, // open web browser when server started - "oneFilePanel" : false, // show one file panel - "keysPanel" : true, // show classic panel with buttons of keys - "port" : 8000, // http port - "ip" : null, // ip or null(default) - "root" : "/", // root directory - "prefix" : "", // url prefix - "prefixSocket" : "", // prefix for socket connection - "confirmCopy" : true, // confirm copy - "confirmMove" : true, // confirm move - "showConfig" : false, // show config at startup - "showFileName" : false, // do not show file name in view and edit - "contact" : true, // enable contact - "configDialog" : true, // enable config dialog - "configAuth" : true, // enable auth change in config dialog - "console" : true, // enable console - "syncConsolePath" : false, // do not sync console path - "terminal" : false, // disable terminal - "terminalPath" : "", // path of a terminal - "terminalCommand" : "", // set command to run in terminal - "terminalAutoRestart" : true, // restart command on exit - "vim" : false, // disable vim hot keys - "columns" : "name-size-date-owner-mode", // set visible columns - "export" : false, // enable export of config through a server - "exportToken" : "root", // token used by export server - "import" : false, // enable import of config - "import-url" : "http://localhost:8000", // url of an export server - "importToken" : "root", // token used to connect to export server - "importListen" : false, // listen on config updates - "dropbox" : false, // disable dropbox integration - "dropboxToken" : "", // unset dropbox token - "log" : true // logging + "name": "", // set tab name in web browser + "auth": false, // enable http authentication + "username": "root", // username for authentication + "password": "toor", // password hash for authentication + "algo": "sha512WithRSAEncryption", // cryptographic algorithm + "editor": "edward", // default, could be "dword" or "edward" + "packer": "tar", // default, could be "tar" or "zip" + "diff": true, // when save - send patch, not whole file + "zip": true, // zip text before send / unzip before save + "buffer": true, // buffer for copying files + "dirStorage": true, // store directory listing + "online": false, // do not load js files from cdn + "open": true, // open web browser when server started + "oneFilePanel": false, // show one file panel + "keysPanel": true, // show classic panel with buttons of keys + "port": 8000, // http port + "ip": null, // ip or null(default) + "root": "/", // root directory + "prefix": "", // url prefix + "prefixSocket": "", // prefix for socket connection + "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 + "configAuth": true, // enable auth change in config dialog + "console": true, // enable console + "syncConsolePath": false, // do not sync console path + "terminal": false, // disable terminal + "terminalPath": "", // path of a terminal + "terminalCommand": "", // set command to run in terminal + "terminalAutoRestart": true, // restart command on exit + "vim": false, // disable vim hot keys + "themes": "name-size-date-owner-mode", // set visible themes + "export": false, // enable export of config through a server + "exportToken": "root", // token used by export server + "import": false, // enable import of config + "import-url": "http://localhost:8000", // url of an export server + "importToken": "root", // token used to connect to export server + "importListen": false, // listen on config updates + "dropbox": false, // disable dropbox integration + "dropboxToken": "", // unset dropbox token + "log": true // logging } ``` @@ -421,10 +444,13 @@ Some config options can be overridden with environment variables, such as: - `CLOUDCMD_NAME` - set tab name in web browser - `CLOUDCMD_OPEN` - open web browser when server started - `CLOUDCMD_EDITOR` - set editor -- `CLOUDCMD_COLUMNS` - set visible columns +- `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 +- `CLOUDCMD_CONFIG_PORT` - enable port change in config dialog - `CLOUDCMD_CONSOLE` - enable console - `CLOUDCMD_SYNC_CONSOLE_PATH` - sync console path - `CLOUDCMD_TERMINAL` - enable terminal @@ -447,7 +473,7 @@ Some config options can be overridden with environment variables, such as: - `CLOUDCMD_IMPORT` - enable import of config - `CLOUDCMD_IMPORT_TOKEN` - authorization token used to connect to export server - `CLOUDCMD_IMPORT_URL` - url of an import server -- `CLOUDCMD_IMPORT_LISTEN`- enable listen on config updates from import server +- `CLOUDCMD_IMPORT_LISTEN` - enable listen on config updates from import server ### User Menu @@ -460,9 +486,7 @@ const RENAME_FILE = 'Rename file'; export default { '__settings': { - select: [ - RENAME_FILE, - ], + select: [RENAME_FILE], run: false, }, [`F2 - ${RENAME_FILE}`]: async ({DOM}) => { @@ -492,7 +516,10 @@ export default { const path = `${dirPath}.cloudcmd.menu.js`; const {prefix} = CloudCmd; - const data = await readDefaultMenu({prefix}); + const data = await readDefaultMenu({ + prefix, + }); + await createDefaultMenu({ path, data, @@ -631,10 +658,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 @@ -661,8 +698,8 @@ npm i cloudcmd express socket.io -S And create `index.js`: ```js -import http from 'http'; -import cloudcmd from 'cloudcmd'; +import http from 'node:http'; +import {cloudcmd} from 'cloudcmd'; import {Server} from 'socket.io'; import express from 'express'; @@ -714,7 +751,7 @@ server.listen(port); Here is example with two `Config Managers`: ```js -import http from 'http'; +import http from 'node:http'; import cloudcmd from 'cloudcmd'; import {Server} from 'socket.io'; import express from 'express'; @@ -736,6 +773,7 @@ const socket2 = new Server(server, { }); const configManager1 = createConfigManager(); + configManager1('name', '1'); const configManager2 = createConfigManager(); @@ -758,8 +796,10 @@ If you want to enable authorization, you can pass credentials to Cloud Commander ```js import criton from 'criton'; -const algo = 'sha512WithRSAEncryption'; // default +const algo = 'sha512WithRSAEncryption'; + +// default // you can generate a hash dynamically const password = criton('root', algo); @@ -893,9 +933,9 @@ ln -s ./sites-available/io.cloudcmd.io ./sites-enabled ## Deploy -`Cloud Commander` can be easily deployed to [Heroku](https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy to Heroku"). +`Cloud Commander` can be easily deployed to [InstaPods][DeployInstaPodsURL]. After deploy you receive email with credentials. -[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.png "Deploy to Heroku")](https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd) +[![Deploy on InstaPods](https://instapods.com/deploy-button.svg)](https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd) ## Docker @@ -1082,6 +1122,245 @@ There are a lot of ways to be involved in `Cloud Commander` development: ## Version history +- *2026.05.26*, **[v19.19.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.19.0)** +- *2026.05.26*, **[v19.18.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.18.1)** +- *2026.05.26*, **[v19.18.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.18.0)** +- *2026.05.17*, **[v19.17.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.17.0)** +- *2026.05.03*, **[v19.16.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.16.0)** +- *2026.04.28*, **[v19.15.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.15.0)** +- *2026.04.28*, **[v19.14.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.14.0)** +- *2026.04.21*, **[v19.13.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.13.1)** +- *2026.04.15*, **[v19.13.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.13.0)** +- *2026.04.12*, **[v19.12.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.5)** +- *2026.04.12*, **[v19.12.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.4)** +- *2026.04.12*, **[v19.12.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.3)** +- *2026.04.11*, **[v19.12.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.2)** +- *2026.04.09*, **[v19.12.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.1)** +- *2026.04.09*, **[v19.12.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.0)** +- *2026.04.07*, **[v19.11.14](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.14)** +- *2026.04.07*, **[v19.11.13](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.13)** +- *2026.04.06*, **[v19.11.12](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.12)** +- *2026.04.06*, **[v19.11.11](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.11)** +- *2026.04.06*, **[v19.11.10](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.10)** +- *2026.04.06*, **[v19.11.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.9)** +- *2026.04.06*, **[v19.11.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.8)** +- *2026.04.05*, **[v19.11.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.7)** +- *2026.04.05*, **[v19.11.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.6)** +- *2026.04.05*, **[v19.11.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.5)** +- *2026.04.04*, **[v19.11.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.4)** +- *2026.04.04*, **[v19.11.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.3)** +- *2026.04.04*, **[v19.11.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.2)** +- *2026.04.04*, **[v19.11.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.1)** +- *2026.04.04*, **[v19.11.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.0)** +- *2026.04.04*, **[v19.10.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.10.2)** +- *2026.04.03*, **[v19.10.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.10.1)** +- *2026.04.02*, **[v19.10.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.10.0)** +- *2026.04.02*, **[v19.9.24](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.24)** +- *2026.04.01*, **[v19.9.23](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.23)** +- *2026.03.31*, **[v19.9.22](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.22)** +- *2026.03.31*, **[v19.9.21](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.21)** +- *2026.03.30*, **[v19.9.20](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.20)** +- *2026.03.30*, **[v19.9.19](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.19)** +- *2026.03.30*, **[v19.9.18](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.18)** +- *2026.03.30*, **[v19.9.17](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.17)** +- *2026.03.29*, **[v19.9.16](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.16)** +- *2026.03.29*, **[v19.9.15](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.15)** +- *2026.03.29*, **[v19.9.14](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.14)** +- *2026.03.29*, **[v19.9.13](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.13)** +- *2026.03.29*, **[v19.9.12](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.12)** +- *2026.03.29*, **[v19.9.11](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.11)** +- *2026.03.29*, **[v19.9.10](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.10)** +- *2026.03.29*, **[v19.9.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.9)** +- *2026.03.28*, **[v19.9.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.8)** +- *2026.03.27*, **[v19.9.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.7)** +- *2026.03.26*, **[v19.9.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.6)** +- *2026.03.26*, **[v19.9.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.5)** +- *2026.03.26*, **[v19.9.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.4)** +- *2026.03.24*, **[v19.9.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.3)** +- *2026.03.23*, **[v19.9.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.2)** +- *2026.03.23*, **[v19.9.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.1)** +- *2026.03.23*, **[v19.9.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.0)** +- *2026.03.23*, **[v19.8.15](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.15)** +- *2026.03.23*, **[v19.8.14](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.14)** +- *2026.03.23*, **[v19.8.13](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.13)** +- *2026.03.23*, **[v19.8.12](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.12)** +- *2026.03.23*, **[v19.8.11](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.11)** +- *2026.03.23*, **[v19.8.10](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.10)** +- *2026.03.23*, **[v19.8.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.9)** +- *2026.03.23*, **[v19.8.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.8)** +- *2026.03.23*, **[v19.8.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.7)** +- *2026.03.23*, **[v19.8.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.6)** +- *2026.03.23*, **[v19.8.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.5)** +- *2026.03.22*, **[v19.8.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.4)** +- *2026.03.22*, **[v19.8.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.3)** +- *2026.03.22*, **[v19.8.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.2)** +- *2026.03.22*, **[v19.8.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.1)** +- *2026.03.20*, **[v19.8.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.0)** +- *2026.03.18*, **[v19.7.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.7.1)** +- *2026.03.17*, **[v19.7.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.7.0)** +- *2026.03.17*, **[v19.6.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.9)** +- *2026.02.27*, **[v19.6.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.8)** +- *2026.02.26*, **[v19.6.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.7)** +- *2026.02.26*, **[v19.6.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.6)** +- *2026.02.26*, **[v19.6.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.5)** +- *2026.02.25*, **[v19.6.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.4)** +- *2026.02.24*, **[v19.6.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.3)** +- *2026.02.24*, **[v19.6.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.2)** +- *2026.02.24*, **[v19.6.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.1)** +- *2026.02.21*, **[v19.6.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.0)** +- *2026.02.18*, **[v19.5.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.5.1)** +- *2026.02.18*, **[v19.5.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.5.0)** +- *2026.02.18*, **[v19.4.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.4.1)** +- *2026.02.18*, **[v19.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.4.0)** +- *2026.02.15*, **[v19.3.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.9)** +- *2026.02.15*, **[v19.3.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.8)** +- *2026.02.13*, **[v19.3.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.7)** +- *2026.02.12*, **[v19.3.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.6)** +- *2026.02.08*, **[v19.3.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.5)** +- *2026.02.06*, **[v19.3.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.4)** +- *2026.02.05*, **[v19.3.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.3)** +- *2026.02.04*, **[v19.3.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.2)** +- *2026.02.03*, **[v19.3.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.1)** +- *2026.02.03*, **[v19.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.0)** +- *2026.02.03*, **[v19.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.2.0)** +- *2026.02.02*, **[v19.1.21](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.21)** +- *2026.01.31*, **[v19.1.20](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.20)** +- *2026.01.30*, **[v19.1.19](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.19)** +- *2026.01.30*, **[v19.1.18](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.18)** +- *2026.01.29*, **[v19.1.17](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.17)** +- *2026.01.28*, **[v19.1.16](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.16)** +- *2026.01.28*, **[v19.1.15](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.15)** +- *2026.01.28*, **[v19.1.14](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.14)** +- *2026.01.27*, **[v19.1.13](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.13)** +- *2026.01.27*, **[v19.1.12](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.12)** +- *2026.01.25*, **[v19.1.11](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.11)** +- *2026.01.25*, **[v19.1.10](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.10)** +- *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)** +- *2024.03.29*, **[v17.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v17.3.0)** +- *2024.03.29*, **[v17.2.1](//github.com/coderaiser/cloudcmd/releases/tag/v17.2.1)** +- *2024.03.22*, **[v17.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v17.2.0)** +- *2024.03.21*, **[v17.1.6](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.6)** +- *2024.03.20*, **[v17.1.5](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.5)** +- *2024.03.20*, **[v17.1.4](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.4)** +- *2024.03.18*, **[v17.1.3](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.3)** +- *2024.03.18*, **[v17.1.2](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.2)** +- *2024.03.16*, **[v17.1.1](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.1)** +- *2024.03.16*, **[v17.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.0)** +- *2024.03.12*, **[v17.0.7](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.7)** +- *2024.03.11*, **[v17.0.6](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.6)** +- *2024.03.11*, **[v17.0.5](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.5)** +- *2024.02.02*, **[v17.0.4](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.4)** +- *2024.02.01*, **[v17.0.3](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.3)** +- *2024.02.01*, **[v17.0.2](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.2)** +- *2024.01.25*, **[v17.0.1](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.1)** +- *2023.12.12*, **[v17.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.0)** +- *2023.12.08*, **[v16.18.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.18.0)** +- *2023.12.04*, **[v16.17.9](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.9)** +- *2023.12.04*, **[v16.17.8](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.8)** +- *2023.10.12*, **[v16.17.7](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.7)** +- *2023.09.22*, **[v16.17.6](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.6)** +- *2023.09.06*, **[v16.17.5](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.5)** +- *2023.09.05*, **[v16.17.4](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.4)** +- *2023.09.05*, **[v16.17.3](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.3)** +- *2023.09.04*, **[v16.17.2](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.2)** +- *2023.09.04*, **[v16.17.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.1)** +- *2023.09.04*, **[v16.17.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.0)** +- *2023.08.10*, **[v16.16.3](//github.com/coderaiser/cloudcmd/releases/tag/v16.16.3)** +- *2023.08.09*, **[v16.16.2](//github.com/coderaiser/cloudcmd/releases/tag/v16.16.2)** +- *2023.08.07*, **[v16.16.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.16.1)** +- *2023.07.09*, **[v16.16.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.16.0)** +- *2023.05.17*, **[v16.15.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.15.0)** +- *2023.03.21*, **[v16.14.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.14.1)** +- *2023.03.08*, **[v16.14.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.14.0)** +- *2023.01.30*, **[v16.13.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.13.1)** +- *2023.01.29*, **[v16.13.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.13.0)** +- *2023.01.29*, **[v16.12.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.12.0)** +- *2023.01.22*, **[v16.11.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.11.0)** +- *2023.01.19*, **[v16.10.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.10.0)** +- *2023.01.18*, **[v16.9.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.9.1)** +- *2023.01.17*, **[v16.9.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.9.0)** +- *2023.01.16*, **[v16.8.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.8.0)** +- *2023.01.15*, **[v16.7.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.7.0)** +- *2022.10.20*, **[v16.6.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.6.1)** +- *2022.10.09*, **[v16.6.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.6.0)** +- *2022.08.06*, **[v16.5.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.5.0)** +- *2022.07.20*, **[v16.4.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.4.1)** +- *2022.07.11*, **[v16.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.4.0)** +- *2022.07.02*, **[v16.3.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.3.1)** +- *2022.07.01*, **[v16.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.3.0)** +- *2022.06.17*, **[v16.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.2.0)** +- *2022.05.12*, **[v16.1.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.1.1)** +- *2022.04.23*, **[v16.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.1.0)** +- *2022.04.22*, **[v16.0.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.0.1)** +- *2022.02.19*, **[v16.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.0.0)** +- *2022.01.20*, **[v15.9.15](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.15)** +- *2022.01.13*, **[v15.9.14](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.14)** - *2021.12.23*, **[v15.9.13](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.13)** - *2021.12.16*, **[v15.9.12](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.12)** - *2021.12.09*, **[v15.9.11](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.11)** @@ -1476,7 +1755,7 @@ There are a lot of ways to be involved in `Cloud Commander` development: ## Special Thanks -- [Olena Zalitok](http://www.linkedin.com/in/olena-zalitok-ux-designer "Olena Zalitok") for **logo** and **favicon**. +- [Olena Zalitok](https://www.linkedin.com/in/ozalitok-ux-ui/ "Olena Zalitok") for **logo** and **favicon**. - [TarZak](https://github.com/tarzak "TarZak") - Russian and Ukrainian translations; - config template and style; diff --git a/LICENSE b/LICENSE index 50fc1d35b1..a0d7436b90 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ (The MIT License) -Copyright (c) 2012-2019 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 375635496c..332bb7fcc7 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,22 @@ -# Cloud Commander v15.9.13 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] +# Cloud Commander v19.19.0 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![Deploy on InstaPods][DeployInstaPodsIMG]][DeployInstaPodsURL] -### [Main][MainURL] [Blog][BlogURL] +### [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-zdp6.onrender.com [NPM_INFO_IMG]: https://nodei.co/npm/cloudcmd.png -[MainURL]: http://cloudcmd.io "Main" -[BlogURL]: http://blog.cloudcmd.io "Blog" -[BuildStatusURL]: https://travis-ci.org/coderaiser/cloudcmd "Build Status" -[BuildStatusIMGURL]: https://img.shields.io/travis/coderaiser/cloudcmd.svg?style=flat-squere&longCache=true -[BuildAppveyorURL]: https://ci.appveyor.com/project/coderaiser/cloudcmd -[BuildAppveyorIMGURL]: https://ci.appveyor.com/api/projects/status/tse6sc8dxrqxqehi?svg=true +[BuildStatusURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml "Build Status" +[BuildStatusIMGURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml/badge.svg [CodacyURL]: https://www.codacy.com/app/coderaiser/cloudcmd [CodacyIMG]: https://api.codacy.com/project/badge/Grade/ddda78be780549ce8754f8d47a8c0e36 [GitterURL]: https://gitter.im/cloudcmd/hello [GitterIMGURL]: https://img.shields.io/gitter/room/coderaiser/cloudcmd.js.svg [DeployURL]: https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy" [DeployIMG]: https://www.herokucdn.com/deploy/button.png +[DeployInstaPodsIMG]: https://img.shields.io/badge/deploy%20on-InstaPods-blue +[DeployInstaPodsURL]: https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd **Cloud Commander** a file manager for the web with console and editor. @@ -68,8 +70,8 @@ npm i cloudcmd express socket.io -S And create `index.js`: ```js -import http from 'http'; -import cloudcmd from 'cloudcmd'; +import http from 'node:http'; +import {cloudcmd} from 'cloudcmd'; import {Server} from 'socket.io'; import express from 'express'; @@ -126,10 +128,8 @@ The docker images are provided for multiple architectures and types. The followi | Architecture | Type | |----------------|--------------| | amd64 | linux | -| arm/v7 | linux | | arm64 (arm/v8) | linux | | amd64 | linux-alpine | -| arm/v7 | linux-alpine | | arm64 (arm/v8) | linux-alpine | `Cloud Commander` could be used as a [docker container](https://hub.docker.com/r/coderaiser/cloudcmd/ "Docker container") this way: diff --git a/bin/cloudcmd.mjs b/bin/cloudcmd.js similarity index 63% rename from bin/cloudcmd.mjs rename to bin/cloudcmd.js index 4c8a3056bd..6ca6716c50 100755 --- a/bin/cloudcmd.mjs +++ b/bin/cloudcmd.js @@ -1,39 +1,32 @@ #!/usr/bin/env node -const DIR_SERVER = '../server/'; - -import {createRequire} from 'module'; -import {promisify} from 'util'; -import tryToCatch from 'try-to-catch'; -import {createSimport} from 'simport'; +import process from 'node:process'; +import {promisify} from 'node:util'; +import {tryToCatch} from 'try-to-catch'; import parse from 'yargs-parser'; - import exit from '../server/exit.js'; -import { - createConfig, - configPath, -} from '../server/config.js'; +import {createConfig, configPath} from '../server/config.js'; +import * as env from '../server/env.js'; +import prefixer from '../server/prefixer.js'; +import * as validate from '../server/validate.js'; +import Info from '../package.json' with { + type: 'json', +}; -const config = createConfig({ - configPath, -}); +process.on('unhandledRejection', exit); -import env from '../server/env.js'; -import prefixer from '../server/prefixer.js'; +const isUndefined = (a) => typeof a === 'undefined'; const choose = (a, b) => { - if (a === undefined) + if (isUndefined(a)) return b; return a; }; -process.on('unhandledRejection', exit); - -const simport = createSimport(import.meta.url); -const require = createRequire(import.meta.url); - -const Info = require('../package.json'); +const config = createConfig({ + configPath, +}); const maybeRoot = (a) => { if (a === '.') @@ -64,6 +57,8 @@ const yargsOptions = { 'terminal-path', 'terminal-command', 'columns', + 'menu', + 'theme', 'import-url', 'import-token', 'export-token', @@ -78,6 +73,7 @@ const yargsOptions = { 'open', 'config-dialog', 'config-auth', + 'config-port', 'console', 'sync-console-path', 'contact', @@ -87,6 +83,7 @@ const yargsOptions = { 'confirm-copy', 'confirm-move', 'show-config', + 'show-dot-files', 'show-file-name', 'vim', 'keys-panel', @@ -99,44 +96,48 @@ const yargsOptions = { 'dropbox', ], default: { - 'server' : true, - 'name' : choose(env('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'), - 'packer' : config('packer') || 'tar', - 'zip' : config('zip'), - 'username' : env('username') || config('username'), - 'root' : choose(env('root'), config('root')), - 'prefix' : choose(env('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') || '', - 'vim' : choose(env.bool('vim'), config('vim')), - 'log' : config('log'), + 'server': true, + '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.parse('editor') || config('editor'), + 'menu': env.parse('menu') || config('menu'), + 'packer': config('packer') || 'tar', + 'zip': config('zip'), + '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.parse('columns') || config('columns') || '', + 'theme': env.parse('theme') || config('theme') || '', + 'vim': choose(env.bool('vim'), config('vim')), + 'log': choose(env.bool('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')), + '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'), + 'config-port': choose(env.bool('config_port'), config('configPort')), + '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 +165,7 @@ else main(); async function main() { - const validateArgs = await simport('@putout/cli-validate-args'); + const {validateArgs} = await import('@putout/cli-validate-args'); const error = await validateArgs(args, [ ...yargsOptions.boolean, @@ -175,7 +176,10 @@ async function main() { return exit(error); if (args.repl) - repl(); + await repl(); + + validate.columns(args.columns); + validate.theme(args.theme); port(args.port); @@ -186,6 +190,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); @@ -193,10 +198,12 @@ 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 || '/'); config('vim', args.vim); + config('theme', args.theme); config('columns', args.columns); config('log', args.log); config('confirmCopy', args.confirmCopy); @@ -204,6 +211,7 @@ async function main() { config('oneFilePanel', args.oneFilePanel); config('configDialog', args.configDialog); config('configAuth', args.configAuth); + config('configPort', args.configPort); config('keysPanel', args.keysPanel); config('export', args.export); config('exportToken', args.exportToken); @@ -224,20 +232,22 @@ async function main() { prefix: config('prefix'), 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)); - await validateRoot(options.root, config); + validateRoot(options.root, config); if (args.showConfig) await showConfig(); - const distribute = await simport('../server/distribute/index.js'); - const importConfig = promisify(distribute.import); + const {distributeImport} = await import('../server/distribute/import.js'); + const importConfig = promisify(distributeImport); await start(options, config); @@ -248,18 +258,18 @@ async function main() { await importConfig(config); } -async function validateRoot(root, config) { - const validate = await simport(DIR_SERVER + 'validate.js'); +function validateRoot(root, config) { validate.root(root, config); if (root === '/') return; - console.log(`root: ${root}`); + if (config('log')) + console.log(`root: ${root}`); } async function getPassword(password) { - const criton = await simport('criton'); + const {default: criton} = await import('criton'); return criton(password, config('algo')); } @@ -268,12 +278,10 @@ function version() { } async function start(options, config) { - const SERVER = DIR_SERVER + 'server.js'; - if (!args.server) return; - const server = await simport(SERVER); + const {default: server} = await import('../server/server.js'); server(options, config); } @@ -287,8 +295,8 @@ function port(arg) { } async function showConfig() { - const show = await simport('../server/show-config'); - const data = show(config('*')); + const {showConfig} = await import('../server/show-config.js'); + const data = showConfig(config('*')); console.log(data); } @@ -297,21 +305,26 @@ async function readConfig(name) { if (!name) return; - const tryToCatch = await simport('try-to-catch'); - const forEachKey = await simport('for-each-key'); + const {default: forEachKey} = await import('for-each-key'); - const [error, data] = await tryToCatch(simport, name); - - if (error) - return exit(error.message); + const data = await import(name, { + with: { + type: 'json', + }, + }); forEachKey(config, data); } async function help() { - const bin = require('../json/help.json'); - const forEachKey = await simport('for-each-key'); - const currify = await simport('currify'); + const {default: bin} = await import('../json/help.json', { + with: { + type: 'json', + }, + }); + + const {default: forEachKey} = await import('for-each-key'); + const {default: currify} = await import('currify'); const usage = 'Usage: cloudcmd [options]'; const url = Info.homepage; @@ -323,15 +336,15 @@ async function help() { console.log('\nGeneral help using Cloud Commander: <%s>', url); } -function repl() { +async function repl() { console.log('REPL mode enabled (telnet localhost 1337)'); - require(DIR_SERVER + 'repl'); + await import('../server/repl.js'); } async function checkUpdate() { - const load = await simport('package-json'); - + const {default: load} = await import('package-json'); const {version} = await load(Info.name, 'latest'); + await showUpdateInfo(version); } @@ -339,12 +352,11 @@ async function showUpdateInfo(version) { if (version === Info.version) return; - const chalk = await simport('chalk'); + const {default: chalk} = await import('chalk'); - const latestVersion = chalk.green.bold('v' + version); + const latestVersion = chalk.green.bold(`v${version}`); const latest = `update available: ${latestVersion}`; const current = chalk.dim(`(current: v${Info.version})`); console.log('%s %s', latest, current); } - diff --git a/bin/cloudcmd.spec.js b/bin/cloudcmd.spec.js new file mode 100644 index 0000000000..d09541b911 --- /dev/null +++ b/bin/cloudcmd.spec.js @@ -0,0 +1,26 @@ +import {spawnSync} from 'node:child_process'; +import {test} from 'supertape'; +import info from '../package.json' with { + type: 'json', +}; + +const cliPath = new URL('cloudcmd.js', import.meta.url).pathname; + +test('cloudcmd: bin: cli: -h', (t) => { + const {stdout} = spawnSync(cliPath, ['-h'], { + encoding: 'utf8', + }); + + t.match(stdout, `Options`); + t.end(); +}); + +test('cloudcmd: bin: cli: -v', (t) => { + const {version} = info; + const {stdout} = spawnSync(cliPath, ['-v'], { + encoding: 'utf8', + }); + + t.match(stdout, `v${version}`); + t.end(); +}); diff --git a/bin/release.mjs b/bin/release.js similarity index 72% rename from bin/release.mjs rename to bin/release.js index d484991c03..86e1d7142b 100755 --- a/bin/release.mjs +++ b/bin/release.js @@ -1,27 +1,30 @@ #!/usr/bin/env node -import {promisify} from 'util'; - -import tryToCatch from 'try-to-catch'; -import {createSimport} from 'simport'; +import {promisify} from 'node:util'; +import process from 'node:process'; +import {tryToCatch} from 'try-to-catch'; import minor from 'minor'; import _place from 'place'; -import rendy from 'rendy'; +import {rendy} from 'rendy'; import shortdate from 'shortdate'; +import Info from '../package.json' with { + type: 'json', +}; -const simport = createSimport(import.meta.url); const place = promisify(_place); -const Info = await simport('../package.json'); - await main(); async function main() { const history = '## Version history\n\n'; const link = '//github.com/coderaiser/cloudcmd/releases/tag/'; - const template = '- *{{ date }}*, ' + - '**[v{{ version }}]' + - '(' + link + 'v{{ version }})**\n'; + + const template = '- ' + + '*{{ date }}*, ' + + '**[v{{ version }}]' + + '(' + + link + + 'v{{ version }})**\n'; const {version} = Info; @@ -33,10 +36,11 @@ async function main() { await replaceVersion('README.md', version, versionNew); await replaceVersion('HELP.md', version, versionNew); - const historyNew = history + rendy(template, { - date : shortdate(), - version : versionNew, - }); + const historyNew = history + + rendy(template, { + date: shortdate(), + version: versionNew, + }); await replaceVersion('HELP.md', history, historyNew); } @@ -47,7 +51,7 @@ async function replaceVersion(name, version, versionNew) { if (error) return console.error(error); - console.log('done: ' + name); + console.log(`done: ${name}`); } async function cl() { @@ -71,4 +75,3 @@ function getVersionNew(last, match) { return last.substr(3); } - diff --git a/client/client.js b/client/client.js index 3b39445759..a0e3cb0783 100644 --- a/client/client.js +++ b/client/client.js @@ -1,75 +1,59 @@ -'use strict'; - -/* 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 isDev = process.env.NODE_ENV === 'development'; - -const Images = require('./dom/images'); -const {unregisterSW} = require('./sw/register'); -const getJsonFromFileTable = require('./get-json-from-file-table'); -const Key = require('./key'); - -const noJS = (a) => a.replace(/.js$/, ''); - -const { +import process from 'node:process'; +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 { apiURL, formatMsg, buildFromJSON, -} = require('../common/cloudfunc'); +} from '#common/cloudfunc'; +import * as Images from '#dom/images'; +import {unregisterSW} from './sw/register.js'; +import {getJsonFromFileTable} from './get-json-from-file-table.js'; +import {Key} from './key/index.js'; +import {loadModule} from './load-module.js'; + +const noJS = (a) => a.replace(/.js$/, ''); -const loadModule = require('./load-module'); +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; - - const log = (...a) => { - if (!isDev) - return; - - console.log(...a); - }; - +function CloudCmdProto({DOM, Listeners}) { Emitify.call(this); const CloudCmd = this; const Info = DOM.CurrentInfo; - const { - Storage, - Files, - } = DOM; + const {Storage, Files} = DOM; - this.log = log; + this.log = () => { + if (!isDev) + return; + }; this.prefix = ''; this.prefixSocket = ''; this.prefixURL = ''; - this.DIRCLIENT = '/dist/'; - this.DIRCLIENT_MODULES = this.DIRCLIENT + 'modules/'; - 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; - - const TITLE = 'Cloud Commander'; - this.TITLE = TITLE; - this.sort = { left: 'name', right: 'name', @@ -80,28 +64,16 @@ function CloudCmdProto(DOM) { right: 'asc', }; - /** - * Функция привязываеться ко всем ссылкам и - * загружает содержимое каталогов - * - * @param params - { - * paramLink - ссылка - * needRefresh - необходимость обязательной загрузки данных с сервера - * panel - * } - * @param callback - */ - this.loadDir = async (params) => { - const p = params; - const refresh = p.isRefresh; - + this.changeDir = async (path, overrides = {}) => { const { + isRefresh, panel, history = true, noCurrent, currentName, - } = p; + } = overrides; + const refresh = isRefresh; let panelChanged; if (!noCurrent && panel && panel !== Info.panel) { @@ -115,14 +87,14 @@ function CloudCmdProto(DOM) { imgPosition = 'top'; Images.show.load(imgPosition, panel); - const path = addSlashToEnd(p.path); /* загружаем содержимое каталога */ - await ajaxLoad(path, { + await ajaxLoad(addSlashToEnd(path), { refresh, history, noCurrent, currentName, + showDotFiles: CloudCmd.config('showDotFiles'), }, panel); }; @@ -135,6 +107,8 @@ function CloudCmdProto(DOM) { CloudCmd.prefix = prefix; CloudCmd.prefixURL = `${prefix}${apiURL}`; CloudCmd.prefixSocket = config.prefixSocket; + CloudCmd.DIR_DIST = `${prefix}/dist`; + CloudCmd.DIR_MODULES = `${this.DIR_DIST}/modules`; CloudCmd.config = (key) => config[key]; CloudCmd.config.if = currify((key, fn, a) => config[key] && fn(a)); @@ -143,7 +117,6 @@ function CloudCmdProto(DOM) { * should be called from config.js only * after successful update on server */ - if (key === 'password') return; @@ -154,22 +127,14 @@ function CloudCmdProto(DOM) { CloudCmd.MIN_ONE_PANEL_WIDTH = Infinity; if (!document.body.scrollIntoViewIfNeeded) - await load.js(prefix + CloudCmd.DIRCLIENT_MODULES + 'polyfill.js'); + await load.js(`${CloudCmd.DIR_MODULES}/polyfill.js`); 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('/'); @@ -179,12 +144,13 @@ 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) { const msg = formatMsg('set current file', file, 'error'); CloudCmd.log(msg); + return; } @@ -256,7 +222,7 @@ function CloudCmdProto(DOM) { }); const dirPath = DOM.getCurrentDirPath(); - Listeners = CloudCmd.Listeners; + Listeners.init(); const panels = getPanels(); @@ -293,19 +259,15 @@ function CloudCmdProto(DOM) { }; this.refresh = async (options = {}) => { - const { - panel = Info.panel, - currentName, - } = options; + const {panel = Info.panel, currentName} = options; const path = DOM.getCurrentDirPath(panel); const isRefresh = true; const history = false; - const noCurrent = options ? options.noCurrent : false; + const noCurrent = options?.noCurrent; - await CloudCmd.loadDir({ - path, + await CloudCmd.changeDir(path, { isRefresh, history, panel, @@ -321,22 +283,18 @@ function CloudCmdProto(DOM) { * @param options * { refresh, history } - необходимость обновить данные о каталоге * @param panel - * @param callback * */ async function ajaxLoad(path, options = {}, panel) { const {RESTful} = DOM; - CloudCmd.log('reading dir: "' + path + '";'); + CloudCmd.log(`reading dir: "${path}";`); const dirStorage = CloudCmd.config('dirStorage'); const json = dirStorage && await Storage.getJson(path); const name = options.currentName || Info.name; - const { - noCurrent, - refresh, - } = options; + const {noCurrent, refresh} = options; if (!refresh && json) return await createFileTable(json, panel, options); @@ -353,7 +311,8 @@ function CloudCmdProto(DOM) { const [, newObj] = await RESTful.read(path + query, 'json'); if (!newObj) - return; // that's OK, error handled by RESTful + // that's OK, error handled by RESTful + return; options.sort = sort; options.order = order; @@ -371,15 +330,15 @@ 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, + showDotFiles, } = options; const names = [ @@ -389,10 +348,12 @@ function CloudCmdProto(DOM) { 'pathLink', ]; - const [ - error, - [file, path, link, pathLink], - ] = await tryToCatch(Files.get, names); + const [error, [ + file, + path, + link, + pathLink, + ]] = await tryToCatch(Files.get, names); if (error) return DOM.Dialog.alert(error.responseText); @@ -400,10 +361,7 @@ function CloudCmdProto(DOM) { const panel = panelParam || DOM.getPanel(); const {prefix} = CloudCmd; - const { - dir, - name, - } = Info; + const {dir, name} = Info; const {childNodes} = panel; let i = childNodes.length; @@ -412,12 +370,13 @@ function CloudCmdProto(DOM) { panel.removeChild(panel.lastChild); panel.innerHTML = buildFromJSON({ - sort : options.sort, - order : options.order, + sort: options.sort, + order: options.order, data, - id : panel.id, + id: panel.id, prefix, - template : { + showDotFiles, + template: { file, path, pathLink, @@ -457,9 +416,7 @@ function CloudCmdProto(DOM) { const path = parentDirPath; - await CloudCmd.loadDir({ - path, - }); + await CloudCmd.changeDir(path); const current = DOM.getCurrentByName(dir); const [first] = DOM.getFiles(panel); @@ -469,4 +426,3 @@ function CloudCmdProto(DOM) { }); }; } - diff --git a/client/cloudcmd.js b/client/cloudcmd.js index 854bcf066e..f2416fd440 100644 --- a/client/cloudcmd.js +++ b/client/cloudcmd.js @@ -1,42 +1,44 @@ -'use strict'; - -require('../css/main.css'); -require('../css/nojs.css'); -require('../css/columns/name-size-date.css'); -require('../css/columns/name-size.css'); - -const wraptile = require('wraptile'); -const load = require('load.js'); +import '../css/main.css'; +import process from 'node:process'; +import wraptile from 'wraptile'; +import load from 'load.js'; +import * as Util from '#common/util'; +import * as CloudFunc from '#common/cloudfunc'; +import DOM from '#dom'; +import {registerSW, listenSW} from './sw/register.js'; +import {initSortPanel, sortPanel} from './sort.js'; +import {createCloudCmd} from './client.js'; +import * as Listeners from './listeners/index.js'; const isDev = process.env.NODE_ENV === 'development'; -const { - registerSW, - listenSW, -} = require('./sw/register'); +export default init; -// prevent additional loading of emitify -window.Emitify = require('emitify'); +globalThis.CloudCmd = init; -module.exports = window.CloudCmd = async (config) => { - window.Util = require('../common/util'); - window.CloudFunc = require('../common/cloudfunc'); - - const DOM = require('./dom'); - - window.DOM = DOM; - window.CloudCmd = require('./client'); +async function init(config) { + globalThis.CloudCmd = createCloudCmd({ + DOM, + Listeners, + }); + globalThis.DOM = DOM; + globalThis.Util = Util; + globalThis.CloudFunc = CloudFunc; await register(config); - require('./listeners'); - require('./key'); - require('./sort'); - + initSortPanel(); + globalThis.CloudCmd.sortPanel = sortPanel; const prefix = getPrefix(config.prefix); - window.CloudCmd.init(prefix, config); -}; + 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@5.5'); + }, 100); +} function getPrefix(prefix) { if (!prefix) @@ -52,7 +54,7 @@ const onUpdateFound = wraptile(async (config) => { if (isDev) return; - const {DOM} = window; + const {DOM} = globalThis; const prefix = getPrefix(config.prefix); await load.js(`${prefix}/dist/cloudcmd.common.js`); @@ -61,7 +63,7 @@ const onUpdateFound = wraptile(async (config) => { console.log('cloudcmd: sw: updated'); DOM.Events.removeAll(); - window.CloudCmd(config); + globalThis.CloudCmd(config); }); async function register(config) { @@ -70,4 +72,3 @@ async function register(config) { listenSW(sw, 'updatefound', onUpdateFound(config)); } - diff --git a/client/dom/buffer.js b/client/dom/buffer.js index 1c81d47340..c6d20ccdc9 100644 --- a/client/dom/buffer.js +++ b/client/dom/buffer.js @@ -1,137 +1,124 @@ -'use strict'; +/* global CloudCmd*/ +import * as Storage from '#dom/storage'; +import tryToPromiseAll from '../../common/try-to-promise-all.js'; -/* global CloudCmd */ +const CLASS = 'cut-file'; +const COPY = 'copy'; +const CUT = 'cut'; -const tryToPromiseAll = require('../../common/try-to-promise-all'); -const Storage = require('./storage'); -const DOM = require('./'); +function showMessage(msg) { + globalThis.DOM.Dialog.alert(msg); +} -module.exports = new BufferProto(); +function getNames() { + const {DOM} = globalThis; + const files = DOM.getActiveFiles(); + + return DOM.getFilenames(files); +} -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 addCutClass() { + const {DOM} = globalThis; + const files = DOM.getActiveFiles(); - function getNames() { - const files = DOM.getActiveFiles(); - const names = DOM.getFilenames(files); - - return names; + for (const element of files) { + element.classList.add(CLASS); } +} + +function rmCutClass() { + const {DOM} = globalThis; + const files = DOM.getByClassAll(CLASS); - function addCutClass() { - const files = DOM.getActiveFiles(); - - for (const element of files) { - element.classList.add(CLASS); - } + for (const element of files) { + element.classList.remove(CLASS); } +} + +const checkEnabled = (fn) => () => { + const is = CloudCmd.config('buffer'); - function rmCutClass() { - const files = DOM.getByClassAll(CLASS); - - for (const element of files) { - element.classList.remove(CLASS); - } - } + if (is) + return fn(); - function callIfEnabled(callback) { - const is = CloudCmd.config('buffer'); - - if (is) - return callback(); - - showMessage('Buffer disabled in config!'); - } + showMessage('Buffer disabled in config!'); +}; + +async function readBuffer() { + const [e, cp, ct] = await tryToPromiseAll([ + Storage.getJson(COPY), + Storage.getJson(CUT), + ]); - async function readBuffer() { - const [e, cp, ct] = await tryToPromiseAll([ - Storage.getJson(COPY), - Storage.getJson(CUT), - ]); - - return [ - e, - cp, - ct, - ]; - } + return [ + e, + cp, + ct, + ]; +} + +export const copy = checkEnabled(async () => { + const Info = globalThis.DOM.CurrentInfo; + const names = getNames(); + const from = Info.dirPath; - 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, - }); - } + await clear(); - async function cut() { - const names = getNames(); - const from = Info.dirPath; - - await clear(); - - if (!names.length) - return; - - addCutClass(); - - await Storage.setJson(CUT, { - from, - names, - }); - } + if (!names.length) + return; - async function clear() { - await Storage.remove(COPY); - await Storage.remove(CUT); - - rmCutClass(); - } + 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; - 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(); - } + await clear(); - return Buffer; -} + 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/cmd.js b/client/dom/cmd.js new file mode 100644 index 0000000000..d228b315fb --- /dev/null +++ b/client/dom/cmd.js @@ -0,0 +1,83 @@ +/* global DOM */ +const SELECTED_FILE = 'selected-file'; +const Cmd = { + getSelectedFiles, + isSelected, + unselectFile, + selectFile, + selectAllFiles, + toggleSelectedFile, + toggleAllSelectedFiles, +}; + +/** + * selected file check + * + * @param currentFile + */ +export function isSelected(currentFile) { + if (!currentFile) + return false; + + return DOM.isContainClass(currentFile, SELECTED_FILE); +} + +/** + * select current file + * @param currentFile + */ +export function selectFile(currentFile) { + const current = currentFile || DOM.getCurrentFile(); + + current.classList.add(SELECTED_FILE); + + return Cmd; +} + +export function unselectFile(currentFile) { + const current = currentFile || DOM.getCurrentFile(); + + current.classList.remove(SELECTED_FILE); + + return Cmd; +} + +export function toggleSelectedFile(currentFile) { + const current = currentFile || DOM.getCurrentFile(); + const name = DOM.getCurrentName(current); + + if (name === '..') + return Cmd; + + current.classList.toggle(SELECTED_FILE); + + return Cmd; +} + +export function toggleAllSelectedFiles() { + DOM + .getAllFiles() + .map(DOM.toggleSelectedFile); + + return Cmd; +} + +export function selectAllFiles() { + DOM + .getAllFiles() + .map(DOM.selectFile); + + return Cmd; +} + +/** + * unified way to get selected files + * + * @currentFile + */ +export function getSelectedFiles() { + const panel = DOM.getPanel(); + const selected = DOM.getByClassAll(SELECTED_FILE, panel); + + return Array.from(selected); +} diff --git a/client/dom/current-file.js b/client/dom/current-file.js index f7acb979e3..b81130544f 100644 --- a/client/dom/current-file.js +++ b/client/dom/current-file.js @@ -1,33 +1,16 @@ -/** - * Parse a `data-name` attribute string back into the original filename - * @param attribute The string we wish to decode - */ - -'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 {getTitle, FS} from '#common/cloudfunc'; +import {encode, decode} from '#common/entity'; let Title; const CURRENT_FILE = 'current-file'; -const NBSP_REG = RegExp(String.fromCharCode(160), 'g'); -const SPACE = ' '; +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 @@ -35,7 +18,7 @@ module.exports._CURRENT_FILE = CURRENT_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; @@ -57,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) @@ -81,16 +64,24 @@ const createNameAttribute = (name) => { */ const parseNameAttribute = (attribute) => { attribute = attribute.replace('js-file-', ''); - return decodeURI(atob(attribute)); + return decodeNBSP(decodeURI(atob(attribute))); }; +export const _parseNameAttribute = parseNameAttribute; + +const parseHrefAttribute = (prefix, attribute) => { + attribute = attribute.replace(RegExp('^' + prefix + FS), ''); + return decode(decodeNBSP(attribute)); +}; + +export const _parseHrefAttribute = parseHrefAttribute; + /** - * get current direcotory path + * get current directory path */ -module.exports.getCurrentDirPath = (panel = DOM.getPanel()) => { +export const getCurrentDirPath = (panel = DOM.getPanel()) => { const path = DOM.getByDataName('js-path', panel); - return path.textContent - .replace(NBSP_REG, SPACE); + return path.textContent; }; /** @@ -98,36 +89,31 @@ 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; - const path = element - .getAttribute('href') - .replace(RegExp('^' + prefix + FS), '') - .replace(NBSP_REG, SPACE); - - return decode(path); + return parseHrefAttribute(prefix, element.getAttribute('href')); }; /** - * get current direcotory name + * get current directory name */ -module.exports.getCurrentDirName = () => { - const href = DOM.getCurrentDirPath() +export const getCurrentDirName = () => { + const href = DOM + .getCurrentDirPath() .replace(/\/$/, ''); const substr = href.substr(href, href.lastIndexOf('/')); - const ret = href.replace(substr + '/', '') || '/'; - return ret; + return href.replace(`${substr}/`, '') || '/'; }; /** - * get current direcotory path + * get current directory path */ -module.exports.getParentDirPath = (panel) => { +export const getParentDirPath = (panel) => { const path = DOM.getCurrentDirPath(panel); const dirName = DOM.getCurrentDirName() + '/'; const index = path.lastIndexOf(dirName); @@ -139,9 +125,9 @@ module.exports.getParentDirPath = (panel) => { }; /** - * get not current direcotory path + * get not current directory path */ -module.exports.getNotCurrentDirPath = () => { +export const getNotCurrentDirPath = () => { const panel = DOM.getPanel({ active: false, }); @@ -154,20 +140,20 @@ 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) => { - const dataName = 'js-file-' + btoa(encodeURI(name)); +export const getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => { + const dataName = 'js-file-' + btoa(encodeURI(encodeNBSP(name))); return DOM.getByDataName(dataName, panel); }; /** - * private function thet unset currentfile + * private function that unset currentFile * * @currentFile */ @@ -183,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(); @@ -220,6 +206,7 @@ module.exports.setCurrentFile = (currentFile, options) => { /* scrolling to current file */ const CENTER = true; + DOM.scrollIntoViewIfNeeded(currentFile, CENTER); CloudCmd.emit('current-file', currentFile); @@ -229,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); }; @@ -240,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) => { @@ -272,7 +259,7 @@ module.exports.getCurrentByPosition = ({x, y}) => { * * @param currentFile */ -module.exports.isCurrentFile = (currentFile) => { +export const isCurrentFile = (currentFile) => { if (!currentFile) return false; @@ -284,8 +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, @@ -302,24 +288,24 @@ 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 .split(' ') .pop(); return type; }; - diff --git a/client/dom/current-file.spec.js b/client/dom/current-file.spec.js index c82afe04b5..6bf0509bc6 100644 --- a/client/dom/current-file.spec.js +++ b/client/dom/current-file.spec.js @@ -1,55 +1,53 @@ -'use strict'; +import {test, stub} from 'supertape'; +import {create} from 'auto-globals'; +import wraptile from 'wraptile'; +import * as currentFile from './current-file.js'; -const test = require('supertape'); -const {create} = require('auto-globals'); -const stub = require('@cloudcmd/stub'); const id = (a) => a; -const wraptile = require('wraptile'); -const returns = wraptile(id); -const currentFile = require('./current-file'); +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; currentFile.setCurrentName('hello', current); - t.calledWith(setAttribute, ['data-name', 'js-file-aGVsbG8='], 'should call setAttribute'); + 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; currentFile.setCurrentName('ай', current); - t.calledWith(setAttribute, ['data-name', 'js-file-JUQwJUIwJUQwJUI5'], 'should call setAttribute'); + 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(); }); @@ -65,15 +63,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, }); @@ -83,25 +78,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(); @@ -109,19 +101,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, }); @@ -129,65 +121,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(); - const expect = false; - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; - t.equal(result, expect); + 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', @@ -197,120 +179,125 @@ 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=='); + const expected = 'hello\xa0world'; + + t.equal(result, expected); + t.end(); +}); -function getDOM({ - link = {}, - getCurrentDirPath = stub(), - getCurrentDirName = stub(), - getByDataName = stub(), - isContainClass = stub(), - getCurrentType = stub(), - getCurrentPath = stub(), -} = {}) { +test('current-file: parseHrefAttribute', (t) => { + const prefix = '/api/v1'; + const result = currentFile._parseHrefAttribute(prefix, '/api/v1/fs/hello world'); + const expected = '/hello\xa0world'; + + t.equal(result, expected); + t.end(); +}); + +function getDOM(overrides = {}) { + const { + link = {}, + getCurrentDirPath = stub(), + getCurrentDirName = stub(), + getByDataName = stub(), + isContainClass = stub(), + getCurrentType = stub(), + getCurrentPath = stub().returns(''), + } = overrides; + return { getCurrentDirPath, getCurrentDirName, @@ -324,4 +311,3 @@ function getDOM({ }, }; } - diff --git a/client/dom/dialog.js b/client/dom/dialog.js index 3fcf7fb6a8..b1791d927c 100644 --- a/client/dom/dialog.js +++ b/client/dom/dialog.js @@ -1,27 +1,18 @@ -'use strict'; - -const tryToCatch = require('try-to-catch'); - -const { - alert, - prompt, - confirm, - progress, -} = require('smalltalk'); +import {tryToCatch} from 'try-to-catch'; +import * as smalltalk from 'smalltalk'; const title = 'Cloud Commander'; -module.exports.alert = (...a) => alert(title, ...a, { +export const alert = (...a) => smalltalk.alert(title, ...a, { cancel: false, }); -module.exports.prompt = (...a) => tryToCatch(prompt, title, ...a); -module.exports.confirm = (...a) => tryToCatch(confirm, title, ...a); -module.exports.progress = (...a) => progress(title, ...a); +export const prompt = (...a) => tryToCatch(smalltalk.prompt, title, ...a); +export const confirm = (...a) => tryToCatch(smalltalk.confirm, title, ...a); +export const progress = (...a) => smalltalk.progress(title, ...a); -module.exports.alert.noFiles = () => { - return alert(title, 'No files selected!', { +alert.noFiles = () => { + return smalltalk.alert(title, 'No files selected!', { cancel: false, }); }; - diff --git a/client/dom/directory.js b/client/dom/directory.js index e9e3d722e6..efbca11991 100644 --- a/client/dom/directory.js +++ b/client/dom/directory.js @@ -1,25 +1,19 @@ -'use strict'; +/* global DOM, CloudCmd */ +import philip from 'philip'; +import * as Dialog from '#dom/dialog'; +import {FS} from '#common/cloudfunc'; +import * as Images from '#dom/images'; -/* global CloudCmd */ - -const philip = require('philip'); - -const Images = require('./images'); -const {FS} = require('../../common/cloudfunc'); -const DOM = require('.'); -const Dialog = require('./dialog'); - -const {getCurrentDirPath: getPathWhenRootEmpty} = DOM; - -module.exports = (items) => { +export const uploadDirectory = (items) => { if (items.length) Images.show('top'); - const entries = Array.from(items).map((item) => item.webkitGetAsEntry()); + const entries = Array + .from(items) + .map((item) => item.webkitGetAsEntry()); - const dirPath = getPathWhenRootEmpty(); - const path = dirPath - .replace(/\/$/, ''); + const dirPath = DOM.getCurrentDirPath(); + const path = dirPath.replace(/\/$/, ''); const progress = Dialog.progress('Uploading...'); @@ -63,15 +57,8 @@ module.exports = (items) => { uploader.on('end', CloudCmd.refresh); }; -function percent(i, n, per = 100) { - return Math.round(i * per / n); -} - -function uploadFile(url, data) { - return DOM.load.put(url, data); -} +const percent = (i, n, per = 100) => Math.round(i * per / n); -function uploadDir(url) { - return DOM.load.put(url + '?dir'); -} +const uploadFile = (url, data) => DOM.load.put(url, data); +const uploadDir = (url) => DOM.load.put(`${url}?dir`); diff --git a/client/dom/dom-tree.js b/client/dom/dom-tree.js index 2927f97eb9..fad5ad405d 100644 --- a/client/dom/dom-tree.js +++ b/client/dom/dom-tree.js @@ -1,8 +1,15 @@ -'use strict'; +import currify from 'currify'; -const currify = require('currify'); - -const DOM = module.exports; +const DOM = { + show, + hide, + getByClass, + getByClassAll, + getByDataName, + getById, + getByTag, + isContainClass, +}; /** * check class of element @@ -10,7 +17,7 @@ const DOM = module.exports; * @param element * @param className */ -const isContainClass = (element, className) => { +export function isContainClass(element, className) { if (!element) throw Error('element could not be empty!'); @@ -18,65 +25,68 @@ const isContainClass = (element, className) => { throw Error('className could not be empty!'); if (Array.isArray(className)) - return className.some(currify(isContainClass, element)); + return className.some(currify( + isContainClass, + element, + )); const {classList} = element; return classList.contains(className); -}; - -module.exports.isContainClass = isContainClass; +} /** * Function search element by tag * @param tag - className * @param element - element */ -module.exports.getByTag = (tag, element = document) => { +export function getByTag(tag, element = document) { return element.getElementsByTagName(tag); -}; +} /** * Function search element by id - * @param Id - id + * @param id + * @param element */ -module.exports.getById = (id, element = document) => { - return element.querySelector('#' + id); -}; +export function getById(id, element = document) { + return element.querySelector(`#${id}`); +} /** * Function search first element by class name * @param className - className * @param element - element */ -module.exports.getByClass = (className, element = document) => DOM.getByClassAll(className, element)[0]; +export function getByClass(className, element = document) { + return DOM.getByClassAll(className, element)[0]; +} -module.exports.getByDataName = (attribute, element = document) => { +export function getByDataName(attribute, element = document) { const selector = '[' + 'data-name="' + attribute + '"]'; return element.querySelector(selector); -}; +} /** * Function search element by class name - * @param pClass - className - * @param element - element + * @param className + * @param element */ -module.exports.getByClassAll = (className, element) => { +export function getByClassAll(className, element) { return (element || document).getElementsByClassName(className); -}; +} /** * add class=hidden to element * * @param element */ -module.exports.hide = (element) => { +export function hide(element) { element.classList.add('hidden'); return DOM; -}; +} -module.exports.show = (element) => { +export function show(element) { element.classList.remove('hidden'); return DOM; -}; - +} diff --git a/client/dom/dom-tree.spec.js b/client/dom/dom-tree.spec.js index 5c3e9a97cc..13f56f6d5a 100644 --- a/client/dom/dom-tree.spec.js +++ b/client/dom/dom-tree.spec.js @@ -1,10 +1,7 @@ -'use strict'; - -const test = require('supertape'); -const {create} = require('auto-globals'); -const tryCatch = require('try-catch'); - -const {isContainClass} = require('./dom-tree'); +import test from 'supertape'; +import {create} from 'auto-globals'; +import {tryCatch} from 'try-catch'; +import {isContainClass} from './dom-tree.js'; test('dom: isContainClass: no element', (t) => { const [e] = tryCatch(isContainClass); @@ -36,13 +33,8 @@ test('dom: isContainClass: contains: array', (t) => { const {contains} = el.classList; const className = 'hello'; - isContainClass(el, [ - 'world', - className, - 'hello', - ]); + isContainClass(el, ['world', className, 'hello']); t.calledWith(contains, [className], 'should call contains'); t.end(); }); - diff --git a/client/dom/events/event-store.js b/client/dom/events/event-store.js index ae08a7ee0c..1cbfe7a4df 100644 --- a/client/dom/events/event-store.js +++ b/client/dom/events/event-store.js @@ -1,8 +1,6 @@ -'use strict'; - let list = []; -module.exports.add = (el, name, fn) => { +export const add = (el, name, fn) => { list.push([ el, name, @@ -10,9 +8,8 @@ module.exports.add = (el, name, fn) => { ]); }; -module.exports.clear = () => { +export const clear = () => { list = []; }; -module.exports.get = () => list; - +export const get = () => list; diff --git a/client/dom/events/event-store.spec.js b/client/dom/events/event-store.spec.js index 7214f3f2cb..6a62393935 100644 --- a/client/dom/events/event-store.spec.js +++ b/client/dom/events/event-store.spec.js @@ -1,7 +1,5 @@ -'use strict'; - -const test = require('supertape'); -const eventStore = require('./event-store'); +import {test} from 'supertape'; +import * as eventStore from './event-store.js'; test('event-store: get', (t) => { const el = {}; @@ -10,11 +8,16 @@ test('event-store: get', (t) => { eventStore.add(el, name, fn); const result = eventStore.get(); + const expected = [ - [el, name, fn], + [ + el, + name, + fn, + ], ]; - t.deepEqual(result, expected, 'should equal'); + t.deepEqual(result, expected); t.end(); }); @@ -29,6 +32,6 @@ test('event-store: clear', (t) => { const result = eventStore.get(); const expected = []; - t.deepEqual(result, expected, 'should equal'); + t.deepEqual(result, expected); t.end(); }); diff --git a/client/dom/events/index.js b/client/dom/events/index.js index 195987da27..c49d23330b 100644 --- a/client/dom/events/index.js +++ b/client/dom/events/index.js @@ -1,250 +1,203 @@ -'use strict'; +import itype from 'itype'; +import * as EventStore from './event-store.js'; -const itype = require('itype'); -const EventStore = require('./event-store'); - -module.exports = new EventsProto(); - -function EventsProto() { - const Events = this; +/** + * safe add event listener + * + * @param type + * @param element - document by default + * @param listener + */ +export const add = (type, element, listener) => { + checkType(type); - const getEventOptions = (eventName) => { - if (eventName !== 'touchstart') - return false; + parseArgs(type, element, listener, (element, args) => { + const [name, fn, options] = args; - return { - passive: true, - }; + 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); }; - 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 (!/element$/.test(type)) - 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; - } + if (!listener) { + listener = element; + element = null; } - /** - * 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; - }; + add(type, element, once); - /** - * 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; - }; + return Events; +}; + +/** + * safe remove event listener + * + * @param type + * @param listener + * @param element - document by default + */ +export const remove = (type, element, listener) => { + checkType(type); - /** - * 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; - }; + parseArgs(type, element, listener, (element, args) => { + element.removeEventListener(...args); + }); - /** - * 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(); - }; + return Events; +}; + +/** + * remove all added event listeners + */ +export const removeAll = () => { + const events = EventStore.get(); - /** - * safe add event keydown listener - * - * @param listener - */ - this.addKey = function(...argsArr) { - const name = 'keydown'; - const args = [name, ...argsArr]; - - return Events.add(...args); - }; + for (const [el, name, fn] of events) + el.removeEventListener(name, fn); - /** - * safe remove event click listener - * - * @param listener - */ - this.rmKey = function(...argsArr) { - const name = 'keydown'; - const args = [name, ...argsArr]; - - return Events.remove(...args); - }; + 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; - /** - * safe add event click listener - * - * @param listener - */ - this.addClick = function(...argsArr) { - const name = 'click'; - const args = [name, ...argsArr]; - - return Events.add(...args); + return { + passive: true, }; +}; + +function parseArgs(eventName, element, listener, callback) { + let isFunc; + const args = [ + eventName, + element, + listener, + callback, + ]; - /** - * safe remove event click listener - * - * @param listener - */ - this.rmClick = function(...argsArr) { - const name = 'click'; - const args = [name, ...argsArr]; - - return Events.remove(...args); - }; + const EVENT_NAME = 1; + const ELEMENT = 0; + const type = itype(eventName); - this.addContextMenu = function(...argsArr) { - const name = 'contextmenu'; - const args = [name, ...argsArr]; + switch(type) { + default: + if (!type.endsWith('element')) + throw Error(`unknown eventName: ${type}`); - return Events.add(...args); - }; + parseArgs(args[EVENT_NAME], args[ELEMENT], listener, callback); + break; - /** - * safe add event click listener - * - * @param listener - */ - this.addError = function(...argsArr) { - const name = 'error'; - const args = [name, ...argsArr]; + case 'string': + isFunc = itype.function(element); - return Events.add(...args); - }; + if (isFunc) { + listener = element; + element = null; + } + + if (!element) + element = window; + + callback(element, [ + eventName, + listener, + getEventOptions(eventName), + ]); + break; - /** - * safe add load click listener - * - * @param listener - */ - this.addLoad = function(...argsArr) { - const name = 'load'; - const args = [name, ...argsArr]; + case 'array': - return Events.add(...args); - }; + for (const name of eventName) { + parseArgs(name, element, listener, callback); + } + + break; - function checkType(type) { - if (!type) - throw Error('type could not be empty!'); + 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/files.js b/client/dom/files.js index dce0ac39b7..395ac0faa8 100644 --- a/client/dom/files.js +++ b/client/dom/files.js @@ -1,23 +1,19 @@ -'use strict'; - /* global CloudCmd */ - -const itype = require('itype'); -const {promisify} = require('es6-promisify'); - -const load = require('./load'); -const RESTful = require('./rest'); +import itype from 'itype'; +import {promisify} from 'es6-promisify'; +import * as load from '#dom/load'; +import * as RESTful from '#dom/rest'; const Promises = {}; const FILES_JSON = 'config|modules'; const FILES_HTML = 'file|path|link|pathLink|media'; const FILES_HTML_ROOT = 'view/media-tmpl|config-tmpl|upload'; const DIR_HTML = '/tmpl/'; -const DIR_HTML_FS = DIR_HTML + 'fs/'; +const DIR_HTML_FS = `${DIR_HTML}fs/`; const DIR_JSON = '/json/'; const timeout = getTimeoutOnce(2000); -module.exports.get = getFile; +export const get = getFile; function getFile(name) { const type = itype(name); @@ -36,8 +32,8 @@ function check(name) { } function getModule(name) { - const regExpHTML = new RegExp(FILES_HTML + '|' + FILES_HTML_ROOT); - const regExpJSON = new RegExp(FILES_JSON); + const regExpHTML = RegExp(FILES_HTML + '|' + FILES_HTML_ROOT); + const regExpJSON = RegExp(FILES_JSON); const isHTML = regExpHTML.test(name); const isJSON = regExpJSON.test(name); @@ -49,12 +45,13 @@ function getModule(name) { return getConfig(); const path = getPath(name, isHTML, isJSON); + return getSystemFile(path); } function getPath(name, isHTML, isJSON) { let path; - const regExp = new RegExp(FILES_HTML_ROOT); + const regExp = RegExp(FILES_HTML_ROOT); const isRoot = regExp.test(name); if (isHTML) { @@ -72,7 +69,7 @@ function getPath(name, isHTML, isJSON) { } function showError(name) { - const str = 'Wrong file name: ' + name; + const str = `Wrong file name: ${name}`; const error = Error(str); throw error; @@ -137,4 +134,3 @@ function getTimeoutOnce(time) { }, time); }; } - diff --git a/client/dom/images.js b/client/dom/images.js index ac3592f2e8..75663e72af 100644 --- a/client/dom/images.js +++ b/client/dom/images.js @@ -1,20 +1,13 @@ /* 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'; -module.exports.get = getElement; +export const get = getElement; /** * check SVG SMIL animation support @@ -43,7 +36,7 @@ function getElement() { } /* Функция создаёт картинку загрузки */ -module.exports.loading = () => { +export const loading = () => { const element = getElement(); const {classList} = element; const loadingImage = LOADING + getLoadingType(); @@ -55,7 +48,7 @@ module.exports.loading = () => { }; /* Функция создаёт картинку ошибки загрузки */ -module.exports.error = () => { +export const error = () => { const element = getElement(); const {classList} = element; const loadingImage = LOADING + getLoadingType(); @@ -66,15 +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); @@ -100,34 +99,25 @@ 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 function hide() { const element = Images.get(); DOM.hide(element); return Images; -}; +} -module.exports.setProgress = (value, title) => { +export const setProgress = (value, title) => { const DATA = 'data-progress'; const element = Images.get(); if (!element) return Images; - element.setAttribute(DATA, value + '%'); + element.setAttribute(DATA, `${value}%`); if (title) element.title = title; @@ -135,7 +125,7 @@ module.exports.setProgress = (value, title) => { return Images; }; -module.exports.clearProgress = () => { +export const clearProgress = () => { const DATA = 'data-progress'; const element = Images.get(); @@ -148,3 +138,12 @@ 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 1473a02cda..f6ce6bc724 100644 --- a/client/dom/index.js +++ b/client/dom/index.js @@ -1,29 +1,79 @@ -'use strict'; - /* global CloudCmd */ +import * as load from '#dom/load'; +import * as Files from '#dom/files'; +import * as Dialog from '#dom/dialog'; +import * as Events from '#dom/events'; +import {getExt} from '#common/util'; +import * as Storage from '#dom/storage'; +import * as RESTful from '#dom/rest'; +import * as Images from '#dom/images'; +import renameCurrent from './operations/rename-current.js'; +import * as CurrentFile from './current-file.js'; +import * as DOMTree from './dom-tree.js'; +import * as Cmd from './cmd.js'; +import * as IO from './io/index.js'; +import {uploadDirectory} from './directory.js'; +import * as Buffer from './buffer.js'; +import {loadRemote as _loadRemote} from './load-remote.js'; +import {selectByPattern} from './select-by-pattern.js'; + +const {assign} = Object; -const Util = require('../../common/util'); - -const Images = require('./images'); -const load = require('./load'); -const Files = require('./files'); -const RESTful = require('./rest'); -const IO = require('./io'); -const Storage = require('./storage'); -const Dialog = require('./dialog'); -const renameCurrent = require('./operations/rename-current'); - -const CurrentFile = require('./current-file'); -const DOMTree = require('./dom-tree'); - -const Cmd = module.exports; const DOM = { + getCurrentDirName, + getNotCurrentDirPath, + getParentDirPath, + loadRemote, + loadSocket, + promptNewDir, + promptNewFile, + unselectFiles, + getActiveFiles, + getCurrentDate, + getCurrentSize, + loadCurrentSize, + loadCurrentHash, + setCurrentSize, + getCurrentMode, + getCurrentOwner, + getCurrentData, + getRefreshButton, + getAllFiles, + expandSelection, + shrinkSelection, + setHistory, + getCurrentLink, + getFilenames, + checkStorageHash, + saveDataToStorage, + getFM, + getPanelPosition, + getCSSVar, + getPanel, + getFiles, + showPanel, + hidePanel, + remove, + deleteCurrent, + deleteSelected, + renameCurrent, + scrollIntoViewIfNeeded, + scrollByPages, + changePanel, + getPackerExt, + goToDirectory, + duplicatePanel, + swapPanels, + updateCurrentInfo, +}; + +assign(DOM, { ...DOMTree, ...CurrentFile, ...Cmd, -}; +}); -const CurrentInfo = {}; +export const CurrentInfo = {}; DOM.Images = Images; DOM.load = load; @@ -34,41 +84,38 @@ DOM.Storage = Storage; DOM.Dialog = Dialog; DOM.CurrentInfo = CurrentInfo; -module.exports = DOM; - -DOM.uploadDirectory = require('./directory'); -DOM.Buffer = require('./buffer'); -DOM.Events = require('./events'); +export default DOM; -const loadRemote = require('./load-remote'); -const selectByPattern = require('./select-by-pattern'); +DOM.uploadDirectory = uploadDirectory; +DOM.Buffer = Buffer; +DOM.Events = Events; +const isString = (a) => typeof a === 'string'; -const SELECTED_FILE = 'selected-file'; const TabPanel = { - 'js-left' : null, - 'js-right' : null, + 'js-left': null, + 'js-right': null, }; -module.exports.loadRemote = (name, options, callback) => { - loadRemote(name, options, callback); +export function loadRemote(name, options, callback) { + _loadRemote(name, options, callback); return DOM; -}; +} -module.exports.loadSocket = (callback) => { +export function loadSocket(callback) { DOM.loadRemote('socket', { - name : 'io', + name: 'io', }, callback); return DOM; -}; +} /** * create new folder * */ -module.exports.promptNewDir = async function() { +export async function promptNewDir() { await promptNew('directory'); -}; +} /** * create new file @@ -76,14 +123,15 @@ module.exports.promptNewDir = async function() { * @typeName * @type */ -module.exports.promptNewFile = async () => { +export async function promptNewFile() { await promptNew('file'); -}; +} async function promptNew(typeName) { const {Dialog} = DOM; const dir = DOM.getCurrentDirPath(); - const msg = 'New ' + typeName || 'File'; + const msg = `New ${typeName}` || 'File'; + const getName = () => { const name = DOM.getCurrentName(); @@ -112,22 +160,22 @@ async function promptNew(typeName) { } /** - * get current direcotory name + * get current directory name */ -module.exports.getCurrentDirName = () => { - const href = DOM.getCurrentDirPath() +export function getCurrentDirName() { + const href = DOM + .getCurrentDirPath() .replace(/\/$/, ''); - const substr = href.substr(href, href.lastIndexOf('/')); - const ret = href.replace(substr + '/', '') || '/'; + const substr = href.substr(href, href.lastIndexOf('/')); - return ret; -}; + return href.replace(`${substr}/`, '') || '/'; +} /** - * get current direcotory path + * get current directory path */ -module.exports.getParentDirPath = (panel) => { +export function getParentDirPath(panel) { const path = DOM.getCurrentDirPath(panel); const dirName = DOM.getCurrentDirName() + '/'; const index = path.lastIndexOf(dirName); @@ -136,55 +184,36 @@ module.exports.getParentDirPath = (panel) => { return path.slice(0, index); return path; -}; - -/** - * get not current direcotory path - */ -module.exports.getNotCurrentDirPath = () => { - const panel = DOM.getPanel({active: false}); - const path = DOM.getCurrentDirPath(panel); - - return path; -}; - -/** - * get current file by name - */ -module.exports.getCurrentByName = (name, panel = CurrentInfo.panel) => { - const dataName = 'js-file-' + btoa(encodeURI(name)); - const element = DOM.getByDataName(dataName, panel); - - return element; -}; +} /** - * unified way to get selected files - * - * @currentFile + * get not current directory path */ -module.exports.getSelectedFiles = () => { - const panel = DOM.getPanel(); - const selected = DOM.getByClassAll(SELECTED_FILE, panel); +export function getNotCurrentDirPath() { + const panel = DOM.getPanel({ + active: false, + }); - return Array.from(selected); -}; + return DOM.getCurrentDirPath(panel); +} /* * unselect all files */ -module.exports.unselectFiles = (files) => { +export function unselectFiles(files) { files = files || DOM.getSelectedFiles(); - Array.from(files).forEach(DOM.toggleSelectedFile); -}; + Array + .from(files) + .forEach(DOM.toggleSelectedFile); +} /** * get all selected files or current when none selected * * @currentFile */ -module.exports.getActiveFiles = () => { +export function getActiveFiles() { const current = DOM.getCurrentFile(); const files = DOM.getSelectedFiles(); const name = DOM.getCurrentName(current); @@ -193,36 +222,35 @@ module.exports.getActiveFiles = () => { return [current]; return files; -}; +} -module.exports.getCurrentDate = (currentFile) => { +export function getCurrentDate(currentFile) { const current = currentFile || DOM.getCurrentFile(); - const date = DOM - .getByDataName('js-date', current) - .textContent; - return date; -}; + return DOM.getByDataName('js-date', current).textContent; +} /** * get size * @currentFile */ -module.exports.getCurrentSize = (currentFile) => { +export function getCurrentSize(currentFile) { const current = currentFile || DOM.getCurrentFile(); + /* если это папка - возвращаем слово dir вместо размера*/ - const size = DOM.getByDataName('js-size', current) + const size = DOM + .getByDataName('js-size', current) .textContent .replace(/^<|>$/g, ''); return size; -}; +} /** * get size * @currentFile */ -module.exports.loadCurrentSize = async (currentFile) => { +export async function loadCurrentSize(currentFile) { const current = currentFile || DOM.getCurrentFile(); const query = '?size'; const link = DOM.getCurrentPath(current); @@ -238,59 +266,60 @@ module.exports.loadCurrentSize = async (currentFile) => { Images.hide(); return current; -}; +} /** * load hash * @callback * @currentFile */ -module.exports.loadCurrentHash = async (currentFile) => { +export async function loadCurrentHash(currentFile) { const current = currentFile || DOM.getCurrentFile(); const query = '?hash'; const link = DOM.getCurrentPath(current); const [, data] = await RESTful.read(link + query); + return data; -}; +} /** * set size * @currentFile */ -module.exports.setCurrentSize = (size, currentFile) => { +export function setCurrentSize(size, currentFile) { const current = currentFile || DOM.getCurrentFile(); const sizeElement = DOM.getByDataName('js-size', current); sizeElement.textContent = size; -}; +} /** * @currentFile */ -module.exports.getCurrentMode = (currentFile) => { +export function getCurrentMode(currentFile) { const current = currentFile || DOM.getCurrentFile(); const mode = DOM.getByDataName('js-mode', current); return mode.textContent; -}; +} /** * @currentFile */ -module.exports.getCurrentOwner = (currentFile) => { +export function getCurrentOwner(currentFile) { const current = currentFile || DOM.getCurrentFile(); const owner = DOM.getByDataName('js-owner', current); return owner.textContent; -}; +} /** * unified way to get current file content * * @param currentFile */ -module.exports.getCurrentData = async (currentFile) => { +export async function getCurrentData(currentFile) { const {Dialog} = DOM; const Info = DOM.CurrentInfo; const current = currentFile || DOM.getCurrentFile(); @@ -299,7 +328,9 @@ module.exports.getCurrentData = async (currentFile) => { if (Info.name === '..') { Dialog.alert.noFiles(); - return [Error('No Files')]; + return [ + Error('No Files'), + ]; } if (isDir) @@ -308,7 +339,9 @@ module.exports.getCurrentData = async (currentFile) => { const [hashNew, hash] = await DOM.checkStorageHash(path); if (!hashNew) - return [Error(`Can't get hash of a file`)]; + return [ + Error(`Can't get hash of a file`), + ]; if (hash === hashNew) return [null, await Storage.get(`${path}-data`)]; @@ -316,69 +349,28 @@ module.exports.getCurrentData = async (currentFile) => { const [e, data] = await RESTful.read(path); if (e) - return [e, null]; + return [ + e, + null, + ]; - const ONE_MEGABYTE = 1024 * 1024 * 1024; + const ONE_MEGABYTE = 1024 ** 2 * 1024; const {length} = data; if (hash && length < ONE_MEGABYTE) await DOM.saveDataToStorage(path, data, hashNew); return [null, data]; -}; +} /** * unified way to get RefreshButton */ -module.exports.getRefreshButton = (panel = DOM.getPanel()) => { +export function getRefreshButton(panel = DOM.getPanel()) { return DOM.getByDataName('js-refresh', panel); -}; - -/** - * select current file - * @param currentFile - */ -module.exports.selectFile = (currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - - current.classList.add(SELECTED_FILE); - - return Cmd; -}; - -module.exports.unselectFile = (currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - - current.classList.remove(SELECTED_FILE); - - return Cmd; -}; - -module.exports.toggleSelectedFile = (currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - const name = DOM.getCurrentName(current); - - if (name === '..') - return Cmd; - - current.classList.toggle(SELECTED_FILE); - - return Cmd; -}; - -module.exports.toggleAllSelectedFiles = () => { - DOM.getAllFiles().map(DOM.toggleSelectedFile); - - return Cmd; -}; - -module.exports.selectAllFiles = () => { - DOM.getAllFiles().map(DOM.selectFile); - - return Cmd; -}; +} -module.exports.getAllFiles = () => { +export function getAllFiles() { const panel = DOM.getPanel(); const files = DOM.getFiles(panel); const name = DOM.getCurrentName(files[0]); @@ -386,34 +378,36 @@ module.exports.getAllFiles = () => { const from = (a) => a === '..' ? 1 : 0; const i = from(name); - return Array.from(files).slice(i); -}; + return Array + .from(files) + .slice(i); +} /** * open dialog with expand selection */ -module.exports.expandSelection = () => { +export async function expandSelection() { const msg = 'expand'; const {files} = CurrentInfo; - selectByPattern(msg, files); -}; + await selectByPattern(msg, files); +} /** * open dialog with shrink selection */ -module.exports.shrinkSelection = () => { +export async function shrinkSelection() { const msg = 'shrink'; const {files} = CurrentInfo; - selectByPattern(msg, files); -}; + await selectByPattern(msg, files); +} /** * setting history wrapper */ -module.exports.setHistory = (data, title, url) => { - const ret = window.history; +export function setHistory(data, title, url) { + const ret = globalThis.history; const {prefix} = CloudCmd; url = prefix + url; @@ -422,33 +416,21 @@ module.exports.setHistory = (data, title, url) => { history.pushState(data, title, url); return ret; -}; - -/** - * selected file check - * - * @param currentFile - */ -module.exports.isSelected = (selected) => { - if (!selected) - return false; - - return DOM.isContainClass(selected, SELECTED_FILE); -}; +} /** * get link from current (or param) file * * @param currentFile - current file by default */ -module.exports.getCurrentLink = (currentFile) => { +export function getCurrentLink(currentFile) { const current = currentFile || DOM.getCurrentFile(); const link = DOM.getByTag('a', current); return link[0]; -}; +} -module.exports.getFilenames = (files) => { +export function getFilenames(files) { if (!files) throw Error('AllFiles could not be empty'); @@ -465,15 +447,15 @@ module.exports.getFilenames = (files) => { }); return names; -}; +} /** * check storage hash */ -module.exports.checkStorageHash = async (name) => { - const nameHash = name + '-hash'; +export async function checkStorageHash(name) { + const nameHash = `${name}-hash`; - if (typeof name !== 'string') + if (!isString(name)) throw Error('name should be a string!'); const [loadHash, storeHash] = await Promise.all([ @@ -482,7 +464,7 @@ module.exports.checkStorageHash = async (name) => { ]); return [loadHash, storeHash]; -}; +} /** * save data to storage @@ -490,9 +472,8 @@ module.exports.checkStorageHash = async (name) => { * @param name * @param data * @param hash - * @param callback */ -module.exports.saveDataToStorage = async (name, data, hash) => { +export async function saveDataToStorage(name, data, hash) { const isDir = DOM.isCurrentIsDir(); if (isDir) @@ -500,27 +481,35 @@ module.exports.saveDataToStorage = async (name, data, hash) => { hash = hash || await DOM.loadCurrentHash(); - const nameHash = name + '-hash'; - const nameData = name + '-data'; + const nameHash = `${name}-hash`; + const nameData = `${name}-data`; await Storage.set(nameHash, hash); await Storage.set(nameData, data); return hash; -}; +} -module.exports.getFM = () => DOM.getPanel().parentElement; +export function getFM() { + const {parentElement} = DOM.getPanel(); + return parentElement; +} -module.exports.getPanelPosition = (panel) => { +export function getPanelPosition(panel) { panel = panel || DOM.getPanel(); return panel.dataset.name.replace('js-', ''); -}; +} + +export function getCSSVar(name, {body = document.body} = {}) { + const bodyStyle = getComputedStyle(body); + return bodyStyle.getPropertyValue(`--${name}`); +} /** function getting panel active, or passive * @param options = {active: true} */ -module.exports.getPanel = (options) => { +export function getPanel(options) { let files; let panel; let isLeft; @@ -546,25 +535,27 @@ 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) throw Error('can not find Active Panel!'); return panel; -}; +} -module.exports.getFiles = (element) => { +export function getFiles(element) { const files = DOM.getByDataName('js-files', element); return files.children || []; -}; +} /** * shows panel right or left (or active) */ -module.exports.showPanel = (active) => { - const panel = DOM.getPanel({active}); +export function showPanel(active) { + const panel = DOM.getPanel({ + active, + }); if (!panel) return false; @@ -572,12 +563,12 @@ module.exports.showPanel = (active) => { DOM.show(panel); return true; -}; +} /** * hides panel right or left (or active) */ -module.exports.hidePanel = (active) => { +export function hidePanel(active) { const panel = DOM.getPanel({ active, }); @@ -586,27 +577,27 @@ module.exports.hidePanel = (active) => { return false; return DOM.hide(panel); -}; +} /** * remove child of element - * @param pChild + * @param child * @param element */ -module.exports.remove = (child, element) => { +export function remove(child, element) { const parent = element || document.body; parent.removeChild(child); return DOM; -}; +} /** * remove current file from file table * @param current * */ -module.exports.deleteCurrent = (current) => { +export function deleteCurrent(current) { if (!current) DOM.getCurrentFile(); @@ -620,52 +611,44 @@ module.exports.deleteCurrent = (current) => { DOM.setCurrentFile(next || prev); parent.removeChild(current); } -}; +} /** * remove selected files from file table * @Selected */ -module.exports.deleteSelected = (selected) => { +export function deleteSelected(selected) { selected = selected || DOM.getSelectedFiles(); if (!selected) return; selected.map(DOM.deleteCurrent); -}; +} /** * rename current file * * @currentFile */ -module.exports.renameCurrent = renameCurrent; - -/** - * unified way to scrollIntoViewIfNeeded - * (native suporte by webkit only) - * @param element - * @param center - to scroll as small as possible param should be false - */ -module.exports.scrollIntoViewIfNeeded = (element, center = false) => { +export function scrollIntoViewIfNeeded(element, center = false) { if (!element || !element.scrollIntoViewIfNeeded) return; element.scrollIntoViewIfNeeded(center); -}; +} /* scroll on one page */ -module.exports.scrollByPages = (element, pPages) => { +export function scrollByPages(element, pPages) { const ret = element?.scrollByPages && pPages; if (ret) element.scrollByPages(pPages); return ret; -}; +} -module.exports.changePanel = () => { +export function changePanel() { const Info = CurrentInfo; let panel = DOM.getPanel(); @@ -711,34 +694,34 @@ module.exports.changePanel = () => { CloudCmd.emit('active-dir', Info.dirPath); return DOM; -}; +} -module.exports.getPackerExt = (type) => { +export function getPackerExt(type) { if (type === 'zip') return '.zip'; return '.tar.gz'; -}; +} -module.exports.goToDirectory = async () => { - const msg = 'Go to directory:'; +export async function goToDirectory(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.loadDir({ - path, - }); -}; + await changeDir(path); +} -module.exports.duplicatePanel = async () => { +export async function duplicatePanel() { const Info = CurrentInfo; const {isDir} = Info; const panel = Info.panelPassive; @@ -753,14 +736,13 @@ module.exports.duplicatePanel = async () => { const path = getPath(isDir); - await CloudCmd.loadDir({ - path, + await CloudCmd.changeDir(path, { panel, noCurrent, }); -}; +} -module.exports.swapPanels = async () => { +export async function swapPanels() { const Info = CurrentInfo; const { panel, @@ -774,14 +756,12 @@ module.exports.swapPanels = async () => { let currentIndex = files.indexOf(element); - await CloudCmd.loadDir({ - path, + await CloudCmd.changeDir(path, { panel: panelPassive, noCurrent: true, }); - await CloudCmd.loadDir({ - path: dirPathPassive, + await CloudCmd.changeDir(dirPathPassive, { panel, }); @@ -793,15 +773,12 @@ module.exports.swapPanels = async () => { const el = Info.files[currentIndex]; DOM.setCurrentFile(el); -}; - -module.exports.CurrentInfo = CurrentInfo; +} -module.exports.updateCurrentInfo = (currentFile) => { +export function updateCurrentInfo(currentFile) { const info = DOM.CurrentInfo; const current = currentFile || DOM.getCurrentFile(); const files = current.parentElement; - const panel = files.parentElement || DOM.getPanel(); const panelPassive = DOM.getPanel({ active: false, @@ -810,27 +787,29 @@ module.exports.updateCurrentInfo = (currentFile) => { const filesPassive = DOM.getFiles(panelPassive); const name = DOM.getCurrentName(current); - info.dir = DOM.getCurrentDirName(); - info.dirPath = DOM.getCurrentDirPath(); - info.parentDirPath = DOM.getParentDirPath(); - info.element = current; - info.ext = Util.getExt(name); - info.files = Array.from(files.children); - info.filesPassive = Array.from(filesPassive); - info.first = files.firstChild; - info.getData = DOM.getCurrentData; - info.last = files.lastChild; - info.link = DOM.getCurrentLink(current); - info.mode = DOM.getCurrentMode(current); - info.name = name; - info.path = DOM.getCurrentPath(current); - info.panel = panel; - info.panelPassive = panelPassive; - info.size = DOM.getCurrentSize(current); - info.isDir = DOM.isCurrentIsDir(); - info.isSelected = DOM.isSelected(current); - info.panelPosition = DOM.getPanel().dataset.name.replace('js-', ''); - info.isOnePanel = - info.panel.getAttribute('data-name') === - info.panelPassive.getAttribute('data-name'); -}; + info.dir = DOM.getCurrentDirName(); + info.dirPath = DOM.getCurrentDirPath(); + info.parentDirPath = DOM.getParentDirPath(); + info.element = current; + info.ext = getExt(name); + info.files = Array.from(files.children); + info.filesPassive = Array.from(filesPassive); + info.first = files.firstChild; + info.getData = DOM.getCurrentData; + info.last = files.lastChild; + info.link = DOM.getCurrentLink(current); + info.mode = DOM.getCurrentMode(current); + info.name = name; + info.path = DOM.getCurrentPath(current); + info.panel = files.parentElement || DOM.getPanel(); + info.panelPassive = panelPassive; + info.size = DOM.getCurrentSize(current); + info.isDir = DOM.isCurrentIsDir(); + info.isSelected = DOM.isSelected(current); + info.panelPosition = DOM + .getPanel() + .dataset + .name + .replace('js-', ''); + info.isOnePanel = info.panel.getAttribute('data-name') === info.panelPassive.getAttribute('data-name'); +} diff --git a/client/dom/index.spec.js b/client/dom/index.spec.js index 64bb51ce8e..268efe5fa0 100644 --- a/client/dom/index.spec.js +++ b/client/dom/index.spec.js @@ -1,32 +1,52 @@ -'use strict'; +import {test, stub} from 'supertape'; +import {getCSSVar, goToDirectory} from '#dom'; -require('css-modules-require-hook'); - -const {test, stub} = require('supertape'); -const mockRequire = require('mock-require'); -const {reRequire, stopAll} = mockRequire; - -global.CloudCmd = {}; +globalThis.CloudCmd = {}; test('cloudcmd: client: dom: goToDirectory', async (t) => { const path = ''; - const {CloudCmd} = global; - const loadDir = stub(); + const changeDir = stub(); const prompt = stub().returns([null, path]); - CloudCmd.loadDir = loadDir; - - mockRequire('./dialog', { + await goToDirectory({ prompt, + changeDir, }); - const {goToDirectory} = reRequire('.'); + t.calledWith(changeDir, [path]); + t.end(); +}); + +test('cloudcmd: client: dom: getCSSVar', (t) => { + const body = {}; + const getPropertyValue = stub().returns(0); - await goToDirectory(); + globalThis.getComputedStyle = stub().returns({ + getPropertyValue, + }); + const result = getCSSVar('hello', { + body, + }); - stopAll(); + delete globalThis.getComputedStyle; - t.calledWith(loadDir, [{path}]); + 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 1219631a10..192769e618 100644 --- a/client/dom/io/index.js +++ b/client/dom/io/index.js @@ -1,25 +1,36 @@ -'use strict'; +import {FS} from '#common/cloudfunc'; +import {sendRequest as _sendRequest} from './send-request.js'; -const {FS} = require('../../../common/cloudfunc'); -const sendRequest = require('./send-request'); +const {assign} = Object; const imgPosition = { top: true, }; -module.exports.delete = async (url, data) => { - return await sendRequest({ - method : 'DELETE', - url : FS + url, - data, - imgPosition : { +export const remove = async (url, data, overrides = {}) => { + const { + sendRequest = _sendRequest, + } = overrides; + + const request = { + method: 'DELETE', + url: FS + url, + imgPosition: { top: Boolean(data), }, - }); + }; + + if (data) + assign(request, { + data, + url: `${request.url}?files`, + }); + + return await sendRequest(request); }; -module.exports.patch = async (url, data) => { - return await sendRequest({ +export const patch = async (url, data) => { + return await _sendRequest({ method: 'PATCH', url: FS + url, data, @@ -27,8 +38,8 @@ module.exports.patch = async (url, data) => { }); }; -module.exports.write = async (url, data) => { - return await sendRequest({ +export const write = async (url, data) => { + return await _sendRequest({ method: 'PUT', url: FS + url, data, @@ -36,7 +47,11 @@ module.exports.write = async (url, data) => { }); }; -module.exports.createDirectory = async (url) => { +export const createDirectory = async (url, overrides = {}) => { + const { + sendRequest = _sendRequest, + } = overrides; + return await sendRequest({ method: 'PUT', url: `${FS}${url}?dir`, @@ -44,10 +59,10 @@ module.exports.createDirectory = async (url) => { }); }; -module.exports.read = async (url, dataType = 'text') => { +export const read = async (url, dataType = 'text') => { const notLog = !url.includes('?'); - return await sendRequest({ + return await _sendRequest({ method: 'GET', url: FS + url, notLog, @@ -55,8 +70,8 @@ module.exports.read = async (url, dataType = 'text') => { }); }; -module.exports.copy = async (from, to, names) => { - return await sendRequest({ +export const copy = async (from, to, names) => { + return await _sendRequest({ method: 'PUT', url: '/copy', data: { @@ -68,24 +83,24 @@ module.exports.copy = async (from, to, names) => { }); }; -module.exports.pack = async (data) => { - return await sendRequest({ +export const pack = async (data) => { + return await _sendRequest({ method: 'PUT', url: '/pack', data, }); }; -module.exports.extract = async (data) => { - return await sendRequest({ +export const extract = async (data) => { + return await _sendRequest({ method: 'PUT', url: '/extract', data, }); }; -module.exports.move = async (from, to, names) => { - return await sendRequest({ +export const move = async (from, to, names) => { + return await _sendRequest({ method: 'PUT', url: '/move', data: { @@ -97,8 +112,8 @@ module.exports.move = async (from, to, names) => { }); }; -module.exports.rename = async (from, to) => { - return await sendRequest({ +export const rename = async (from, to) => { + return await _sendRequest({ method: 'PUT', url: '/rename', data: { @@ -109,9 +124,9 @@ module.exports.rename = async (from, to) => { }); }; -module.exports.Config = { +export const Config = { read: async () => { - return await sendRequest({ + return await _sendRequest({ method: 'GET', url: '/config', imgPosition, @@ -120,7 +135,7 @@ module.exports.Config = { }, write: async (data) => { - return await sendRequest({ + return await _sendRequest({ method: 'PATCH', url: '/config', data, @@ -129,18 +144,18 @@ module.exports.Config = { }, }; -module.exports.Markdown = { +export const Markdown = { read: async (url) => { - return await sendRequest({ + return await _sendRequest({ method: 'GET', - url: '/markdown' + url, + url: `/markdown${url}`, imgPosition, notLog: true, }); }, render: async (data) => { - return await sendRequest({ + return await _sendRequest({ method: 'PUT', url: '/markdown', data, @@ -149,4 +164,3 @@ module.exports.Markdown = { }); }, }; - diff --git a/client/dom/io/index.spec.js b/client/dom/io/index.spec.js index 80c9bee2ce..8e9e2d0810 100644 --- a/client/dom/io/index.spec.js +++ b/client/dom/io/index.spec.js @@ -1,18 +1,12 @@ -'use strict'; - -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); -const mockRequire = require('mock-require'); - -const {reRequire, stopAll} = mockRequire; +import {test, stub} from 'supertape'; +import * as io from './index.js'; 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,7 +16,45 @@ test('client: dom: io', (t) => { url: '/fs/hello?dir', }; - stopAll(); + t.calledWith(sendRequest, [expected]); + t.end(); +}); + +test('client: dom: io: remove: no files', async (t) => { + const sendRequest = stub(); + + await io.remove('/hello', null, { + sendRequest, + }); + + const expected = { + imgPosition: { + top: false, + }, + method: 'DELETE', + url: '/fs/hello', + }; + + t.calledWith(sendRequest, [expected]); + t.end(); +}); + +test('client: dom: io: remove: files', async (t) => { + const sendRequest = stub(); + const files = ['world']; + + await io.remove('/hello', files, { + sendRequest, + }); + + const expected = { + imgPosition: { + top: true, + }, + data: ['world'], + method: 'DELETE', + url: '/fs/hello?files', + }; t.calledWith(sendRequest, [expected]); t.end(); diff --git a/client/dom/io/send-request.js b/client/dom/io/send-request.js index f330776469..177d34b577 100644 --- a/client/dom/io/send-request.js +++ b/client/dom/io/send-request.js @@ -1,13 +1,9 @@ -'use strict'; - /* global CloudCmd */ +import {promisify} from 'es6-promisify'; +import * as Images from '#dom/images'; +import * as load from '#dom/load'; -const {promisify} = require('es6-promisify'); - -const Images = require('../images'); -const load = require('../load'); - -module.exports = promisify((params, callback) => { +export const sendRequest = promisify((params, callback) => { const p = params; const {prefixURL} = CloudCmd; @@ -17,17 +13,14 @@ module.exports = promisify((params, callback) => { p.url = replaceHash(p.url); load.ajax({ - method : p.method, - url : p.url, - data : p.data, - dataType : p.dataType, - error : (jqXHR) => { + method: p.method, + url: p.url, + data: p.data, + dataType: p.dataType, + error: (jqXHR) => { const response = jqXHR.responseText; - const { - statusText, - status, - } = jqXHR; + const {statusText, status} = jqXHR; const text = status === 404 ? response : statusText; @@ -44,7 +37,8 @@ module.exports = promisify((params, callback) => { }); }); -module.exports._replaceHash = replaceHash; +export const _replaceHash = replaceHash; + function replaceHash(url) { /* * if we send ajax request - @@ -52,4 +46,3 @@ function replaceHash(url) { */ return url.replace(/#/g, '%23'); } - diff --git a/client/dom/io/send-request.spec.js b/client/dom/io/send-request.spec.js index 077ba4c7bc..f8a960d874 100644 --- a/client/dom/io/send-request.spec.js +++ b/client/dom/io/send-request.spec.js @@ -1,7 +1,5 @@ -'use strict'; - -const test = require('supertape'); -const {_replaceHash} = require('./send-request'); +import {test} from 'supertape'; +import {_replaceHash} from './send-request.js'; test('cloudcmd: client: io: replaceHash', (t) => { const url = '/hello/####world'; @@ -11,4 +9,3 @@ test('cloudcmd: client: io: replaceHash', (t) => { t.equal(result, expected); t.end(); }); - diff --git a/client/dom/load-remote.js b/client/dom/load-remote.js index eb295bb7ee..43af0cf506 100644 --- a/client/dom/load-remote.js +++ b/client/dom/load-remote.js @@ -1,58 +1,52 @@ -'use strict'; - /* global CloudCmd */ +import {callbackify} from 'node:util'; +import {rendy} from 'rendy'; +import itype from 'itype'; +import * as load from 'load.js'; +import {tryToCatch} from 'try-to-catch'; +import {findObjByNameInArr} from '#common/util'; +import * as Files from '#dom/files'; -const rendy = require('rendy'); -const itype = require('itype'); -const load = require('load.js'); -const tryToCatch = require('try-to-catch'); - -const {findObjByNameInArr} = require('../../common/util'); - -const Files = require('./files'); - -module.exports = (name, options, callback = options) => { +export const loadRemote = callbackify(async (name, options) => { const {prefix, config} = CloudCmd; const o = options; if (o.name && window[o.name]) - return callback(); + return; - 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, - }); + const modules = await Files.get('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; + } + + await load.parallel(localURL); +}); diff --git a/client/dom/load.js b/client/dom/load.js index f76cbe86c8..8c7011c108 100644 --- a/client/dom/load.js +++ b/client/dom/load.js @@ -1,12 +1,8 @@ -'use strict'; - -const itype = require('itype'); -const jonny = require('jonny'); -const Emitify = require('emitify'); -const exec = require('execon'); -const Images = require('./images'); - -module.exports.getIdBySrc = getIdBySrc; +import itype from 'itype'; +import jonny from 'jonny'; +import Emitify from 'emitify'; +import exec from 'execon'; +import * as Images from '#dom/images'; /** * Function gets id by src @@ -14,7 +10,7 @@ module.exports.getIdBySrc = getIdBySrc; * * Example: http://domain.com/1.js -> 1_js */ -function getIdBySrc(src) { +export function getIdBySrc(src) { const isStr = itype.string(src); if (!isStr) @@ -25,6 +21,7 @@ function getIdBySrc(src) { const num = src.lastIndexOf('/') + 1; const sub = src.substr(src, num); + const id = src .replace(sub, '') .replace(/\./g, '-'); @@ -37,15 +34,15 @@ function getIdBySrc(src) { * * @param params */ -module.exports.ajax = (params) => { +export const ajax = (params) => { const p = params; const isObject = itype.object(p.data); const isArray = itype.array(p.data); const isArrayBuf = itype(p.data) === 'arraybuffer'; const type = p.type || p.method || 'GET'; - const { - headers = {}, - } = p; + + const {headers = {}} = p; + const xhr = new XMLHttpRequest(); xhr.open(type, p.url, true); @@ -63,7 +60,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; @@ -94,12 +91,11 @@ module.exports.ajax = (params) => { xhr.send(data); }; -module.exports.put = (url, body) => { +export const put = (url, body) => { const emitter = Emitify(); const xhr = new XMLHttpRequest(); - url = encodeURI(url) - .replace(/#/g, '%23'); + url = encodeURI(url).replace(/#/g, '#'); xhr.open('put', url, true); @@ -133,4 +129,3 @@ module.exports.put = (url, body) => { return emitter; }; - diff --git a/client/dom/operations/rename-current.js b/client/dom/operations/rename-current.js index aa01bf024e..8b6536d344 100644 --- a/client/dom/operations/rename-current.js +++ b/client/dom/operations/rename-current.js @@ -1,23 +1,27 @@ -'use strict'; - /* global CloudCmd */ +import capitalize from 'just-capitalize'; +import * as _Dialog from '#dom/dialog'; +import * as Storage from '#dom/storage'; +import * as RESTful from '#dom/rest'; +import * as _currentFile from '../current-file.js'; -const capitalize = require('just-capitalize'); - -const Dialog = require('../dialog'); -const Storage = require('../storage'); -const RESTful = require('../rest'); -const { - isCurrentFile, - getCurrentName, - getCurrentFile, - getCurrentByName, - getCurrentType, - getCurrentDirPath, - setCurrentName, -} = require('../current-file'); - -module.exports = async (current) => { +export default 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,6 +62,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 4aa6f7e654..a5ebb41583 100644 --- a/client/dom/operations/rename-current.spec.js +++ b/client/dom/operations/rename-current.spec.js @@ -1,24 +1,18 @@ -'use strict'; - -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); -const mockRequire = require('mock-require'); - -const {reRequire, stopAll} = mockRequire; +import {test, stub} from 'supertape'; +import renameCurrent from './rename-current.js'; 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,13 +21,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; - - const CloudCmdStub = { - refresh: stub(), - }; - - global.CloudCmd = CloudCmdStub; const prompt = stub().returns([null, name]); const confirm = stub().returns([true]); @@ -41,23 +28,22 @@ 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(); @@ -98,4 +84,3 @@ const stubCurrentFile = (fns = {}) => { setCurrentName, }; }; - diff --git a/client/dom/rest.js b/client/dom/rest.js index 9ce9c3020b..3cf2fb66f9 100644 --- a/client/dom/rest.js +++ b/client/dom/rest.js @@ -1,12 +1,8 @@ -'use strict'; - -const tryToCatch = require('try-to-catch'); - -const {encode} = require('../../common/entity'); - -const Images = require('./images'); -const IO = require('./io'); -const Dialog = require('./dialog'); +import {tryToCatch} from 'try-to-catch'; +import * as Dialog from '#dom/dialog'; +import * as Images from '#dom/images'; +import {encode} from '#common/entity'; +import * as IO from './io/index.js'; const handleError = (promise) => async (...args) => { const [e, data] = await tryToCatch(promise, ...args); @@ -22,24 +18,23 @@ const handleError = (promise) => async (...args) => { return [e, data]; }; -module.exports.delete = handleError(IO.delete); -module.exports.patch = handleError(IO.patch); -module.exports.write = handleError(IO.write); -module.exports.createDirectory = handleError(IO.createDirectory); -module.exports.read = handleError(IO.read); -module.exports.copy = handleError(IO.copy); -module.exports.pack = handleError(IO.pack); -module.exports.extract = handleError(IO.extract); -module.exports.move = handleError(IO.move); -module.exports.rename = handleError(IO.rename); - -module.exports.Config = { +export const remove = handleError(IO.remove); +export const patch = handleError(IO.patch); +export const write = handleError(IO.write); +export const createDirectory = handleError(IO.createDirectory); +export const read = handleError(IO.read); +export const copy = handleError(IO.copy); +export const pack = handleError(IO.pack); +export const extract = handleError(IO.extract); +export const move = handleError(IO.move); +export const rename = handleError(IO.rename); + +export const Config = { read: handleError(IO.Config.read), write: handleError(IO.Config.write), }; -module.exports.Markdown = { +export const Markdown = { read: handleError(IO.Markdown.read), render: handleError(IO.Markdown.render), }; - diff --git a/client/dom/select-by-pattern.js b/client/dom/select-by-pattern.js index eec89bb4ea..0bada6ef3b 100644 --- a/client/dom/select-by-pattern.js +++ b/client/dom/select-by-pattern.js @@ -1,22 +1,18 @@ -'use strict'; +import {alert, prompt} from '#dom/dialog'; +import {getRegExp} from '#common/util'; +import {getCurrentName} from './current-file.js'; +import { + isSelected, + toggleSelectedFile, +} from './cmd.js'; let SelectType = '*.*'; -const {getRegExp} = require('../../common/util'); -const { - alert, - prompt, -} = require('./dialog'); - -const DOM = require('.'); - -module.exports = async (msg, files) => { +export const selectByPattern = async (msg, files) => { if (!files) return; const allMsg = `Specify file type for ${msg} selection`; - - /* eslint require-atomic-updates: 0 */ const [cancel, type] = await prompt(allMsg, SelectType); if (cancel) @@ -28,24 +24,23 @@ module.exports = async (msg, files) => { let matches = 0; for (const current of files) { - const name = DOM.getCurrentName(current); + const name = getCurrentName(current); if (name === '..' || !regExp.test(name)) continue; ++matches; - let isSelected = DOM.isSelected(current); + let selected = isSelected(current); const shouldSel = msg === 'expand'; if (shouldSel) - isSelected = !isSelected; + selected = !selected; - if (isSelected) - DOM.toggleSelectedFile(current); + if (selected) + toggleSelectedFile(current); } if (!matches) alert('No matches found!'); }; - diff --git a/client/dom/storage.js b/client/dom/storage.js index d9eeb9380f..d5cf231f4d 100644 --- a/client/dom/storage.js +++ b/client/dom/storage.js @@ -1,29 +1,26 @@ -'use strict'; - const {parse, stringify} = JSON; -module.exports.set = async (name, data) => { +export const set = (name, data) => { localStorage.setItem(name, data); }; -module.exports.setJson = async (name, data) => { +export const setJson = (name, data) => { localStorage.setItem(name, stringify(data)); }; -module.exports.get = async (name) => { +export const get = (name) => { return localStorage.getItem(name); }; -module.exports.getJson = async (name) => { +export const getJson = (name) => { const data = localStorage.getItem(name); return parse(data); }; -module.exports.clear = () => { +export const clear = () => { localStorage.clear(); }; -module.exports.remove = (item) => { +export const remove = (item) => { localStorage.removeItem(item); }; - diff --git a/client/dom/storage.spec.js b/client/dom/storage.spec.js index 84fdfe713b..c538952d4f 100644 --- a/client/dom/storage.spec.js +++ b/client/dom/storage.spec.js @@ -1,61 +1,61 @@ -'use strict'; - -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); -const storage = require('./storage'); +import {test, stub} from 'supertape'; +import * as storage from '#dom/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', }; @@ -63,44 +63,43 @@ 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.ok(clear.calledWith(), 'should call clear'); + t.calledWithNoArgs(clear, 'should call clear'); t.end(); }); - diff --git a/client/dom/upload-files.js b/client/dom/upload-files.js index 8b0e8b7a3e..1c37fe1bd4 100644 --- a/client/dom/upload-files.js +++ b/client/dom/upload-files.js @@ -1,25 +1,19 @@ -'use strict'; - /* global CloudCmd */ +import {eachSeries} from 'execon'; +import wraptile from 'wraptile'; +import * as load from '#dom/load'; +import {alert} from '#dom/dialog'; +import {FS} from '#common/cloudfunc'; +import * as Images from '#dom/images'; +import {getCurrentDirPath} from './current-file.js'; -const {eachSeries} = require('execon'); -const wraptile = require('wraptile'); - -const load = require('./load'); -const Images = require('./images'); -const {alert} = require('./dialog'); - -const {FS} = require('../../common/cloudfunc'); - -const onEnd = wraptile(_onEnd); const loadFile = wraptile(_loadFile); +const onEnd = wraptile(_onEnd); -const {getCurrentDirPath: getPathWhenRootEmpty} = require('.'); - -module.exports = (dir, files) => { +export const uploadFiles = (dir, files) => { if (!files) { files = dir; - dir = getPathWhenRootEmpty(); + dir = getCurrentDirPath(); } const n = files.length; @@ -55,7 +49,8 @@ function _loadFile(dir, n, file, callback) { ++i; - load.put(api + path, file) + load + .put(api + path, file) .on('error', showError) .on('end', callback) .on('progress', (count) => { @@ -70,4 +65,3 @@ function _loadFile(dir, n, file, callback) { function showError({message}) { alert(message); } - diff --git a/client/get-json-from-file-table.js b/client/get-json-from-file-table.js index 747cae9dc6..ea90f366b8 100644 --- a/client/get-json-from-file-table.js +++ b/client/get-json-from-file-table.js @@ -1,40 +1,13 @@ -'use strict'; - /* global DOM */ - -const Info = DOM.CurrentInfo; - /** * Функция генерирует JSON из html-таблицы файлов и * используеться при первом заходе в корень */ -module.exports = () => { +export const getJsonFromFileTable = () => { + const Info = DOM.CurrentInfo; 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); @@ -47,3 +20,25 @@ module.exports = () => { 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/binder.js b/client/key/binder.js index 6765f931bf..6aec97eafd 100644 --- a/client/key/binder.js +++ b/client/key/binder.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports.createBinder = () => { +export const createBinder = () => { let binded = false; return { @@ -15,4 +13,3 @@ module.exports.createBinder = () => { }, }; }; - diff --git a/client/key/index.js b/client/key/index.js index 3f04fb4096..6f55746c1c 100644 --- a/client/key/index.js +++ b/client/key/index.js @@ -1,28 +1,20 @@ -'use strict'; - /* global CloudCmd, DOM */ +import clipboard from '@cloudcmd/clipboard'; +import {fullstore} from 'fullstore'; +import * as Events from '#dom/events'; +import * as Buffer from '../dom/buffer.js'; +import * as KEY from './key.js'; +import _vim from './vim/index.js'; +import setCurrentByChar from './set-current-by-char.js'; +import {createBinder} from './binder.js'; -const Info = DOM.CurrentInfo; - -const clipboard = require('@cloudcmd/clipboard'); - -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 fullstore = require('fullstore'); 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')); - } + if (!config('vim') && keyCode === KEY.ESC) + _config('vim', true); }; const isUndefined = (a) => typeof a === 'undefined'; @@ -30,38 +22,48 @@ const isUndefined = (a) => typeof a === 'undefined'; 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) { /* * event.keyIdentifier deprecated in chrome v51 * but event.key is absent in chrome <= v51 */ - const { key, shift, keyCode, keyIdentifier, } = event; + const char = key || fromCharCode(keyIdentifier); const symbol = getSymbol(shift, keyCode); 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 + // strange chrome bug calls listener twice // in second time event misses a lot fields if (isUndefined(event.altKey)) return; @@ -77,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); @@ -111,12 +117,12 @@ function getSymbol(shift, keyCode) { function fromCharCode(keyIdentifier) { const code = keyIdentifier.substring(2); const hex = parseInt(code, 16); - const char = String.fromCharCode(hex); - return char; + return String.fromCharCode(hex); } -async function switchKey(event) { +async function _switchKey(event) { + const Info = DOM.CurrentInfo; let i; let isSelected; let prev; @@ -133,9 +139,10 @@ async function switchKey(event) { const { Operation, - loadDir, + changeDir, config, } = CloudCmd; + const {keyCode} = event; const alt = event.altKey; @@ -156,12 +163,14 @@ async function switchKey(event) { break; case KEY.INSERT: - DOM .toggleSelectedFile(current) + DOM + .toggleSelectedFile(current) .setCurrentFile(next); break; case KEY.INSERT_MAC: - DOM .toggleSelectedFile(current) + DOM + .toggleSelectedFile(current) .setCurrentFile(next); break; @@ -170,6 +179,7 @@ async function switchKey(event) { Operation.show('delete:silent'); else Operation.show('delete'); + break; case KEY.ASTERISK: @@ -199,9 +209,7 @@ async function switchKey(event) { event.preventDefault(); if (Info.isDir) - await loadDir({ - path, - }); + await changeDir(path); else if (shift) CloudCmd.View.show(null, { raw: true, @@ -306,6 +314,7 @@ async function switchKey(event) { DOM.swapPanels(); event.preventDefault(); } + break; /* navigation on file table: * @@ -400,9 +409,10 @@ async function switchKey(event) { case KEY.ENTER: if (Info.isDir) - await loadDir({path}); + await changeDir(path); else CloudCmd.View.show(); + break; case KEY.BACKSPACE: @@ -412,9 +422,8 @@ async function switchKey(event) { case KEY.BACKSLASH: if (ctrlMeta) - await loadDir({ - path: '/', - }); + await changeDir('/'); + break; case KEY.A: @@ -433,6 +442,14 @@ async function switchKey(event) { break; + case KEY.L: + if (ctrlMeta) { + CloudCmd.logOut(); + event.preventDefault(); + } + + break; + case KEY.M: if (ctrlMeta) { if (config('vim')) @@ -455,6 +472,7 @@ async function switchKey(event) { .catch(CloudCmd.log); break; + /** * обновляем страницу, * загружаем содержимое каталога @@ -468,26 +486,36 @@ async function switchKey(event) { CloudCmd.refresh(); event.preventDefault(); } + break; case KEY.C: if (ctrlMeta) Buffer.copy(); + break; case KEY.X: if (ctrlMeta) Buffer.cut(); + break; case KEY.V: if (ctrlMeta) Buffer.paste(); + break; case KEY.Z: if (ctrlMeta) Buffer.clear(); + + break; + + case KEY.COLON: + CloudCmd.CommandLine.show(); + event.preventDefault(); break; /* чистим хранилище */ @@ -498,7 +526,19 @@ async function switchKey(event) { CloudCmd.log('storage cleared'); 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 ae55456411..bec92b319f 100644 --- a/client/key/index.spec.js +++ b/client/key/index.spec.js @@ -1,33 +1,19 @@ -'use strict'; +import autoGlobals from 'auto-globals'; +import supertape from 'supertape'; +import {ESC} from './key.js'; +import {Key, _listener} from './index.js'; +import {getDOM, getCloudCmd} from './vim/globals.fixture.js'; -const autoGlobals = require('auto-globals'); -const test = autoGlobals(require('supertape')); -const stub = require('@cloudcmd/stub'); -const mockRequire = require('mock-require'); -const {reRequire, stopAll} = mockRequire; +const test = autoGlobals(supertape); +const {stub} = supertape; -const {ESC} = require('./key'); - -const { - getDOM, - getCloudCmd, -} = require('./vim/globals.fixture'); - -const DOM = getDOM(); -const CloudCmd = getCloudCmd(); - -global.DOM = DOM; -global.CloudCmd = CloudCmd; +globalThis.DOM = getDOM(); +globalThis.CloudCmd = getCloudCmd(); test('cloudcmd: client: key: enable vim', async (t) => { const vim = stub(); - const configStub = stub().returns(true); - const {CloudCmd} = global; - const {config} = CloudCmd; - - CloudCmd.config = configStub; - mockRequire('./vim', vim); - const {_listener, setBind} = reRequire('.'); + const config = stub().returns(true); + const _config = stub(); const event = { keyCode: ESC, @@ -35,11 +21,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(); @@ -47,8 +36,7 @@ test('cloudcmd: client: key: enable vim', async (t) => { test('cloudcmd: client: key: disable vim', async (t) => { const _config = stub(); - - const {_listener, setBind} = reRequire('.'); + const config = stub(); const event = { keyCode: ESC, @@ -56,17 +44,13 @@ test('cloudcmd: client: key: disable vim', async (t) => { altKey: false, }; - const {CloudCmd} = global; - const {config} = CloudCmd; - CloudCmd.config = _config; - - setBind(); - await _listener(event); - await _listener(event); + Key.setBind(); + await _listener(event, { + config, + _config, + switchKey: stub(), + }); - CloudCmd.config = config; - - t.calledWith(_config, ['vim']); + t.calledWith(_config, ['vim', true]); t.end(); }); - diff --git a/client/key/key.js b/client/key/key.js index 0ca7963fa2..6772e34c21 100644 --- a/client/key/key.js +++ b/client/key/key.js @@ -1,82 +1,58 @@ -'use strict'; - -module.exports = { - BACKSPACE : 8, - TAB : 9, - ENTER : 13, - CAPSLOCK : 20, - ESC : 27, - - SPACE : 32, - PAGE_UP : 33, - PAGE_DOWN : 34, - END : 35, - HOME : 36, - - LEFT : 37, - UP : 38, - RIGHT : 39, - DOWN : 40, - - INSERT : 45, - DELETE : 46, - - ZERO : 48, - - SEMICOLON : 52, - - COLON : 54, - - A : 65, - - C : 67, - D : 68, - - G : 71, - - J : 74, - K : 75, - - M : 77, - - O : 79, - P : 80, - Q : 81, - R : 82, - S : 83, - T : 84, - U : 85, - - V : 86, - - X : 88, - - Z : 90, - - INSERT_MAC : 96, - - ASTERISK : 106, - PLUS : 107, - MINUS : 109, - - F1 : 112, - F2 : 113, - F3 : 114, - F4 : 115, - F5 : 116, - F6 : 117, - F7 : 118, - F8 : 119, - F9 : 120, - F10 : 121, - - EQUAL : 187, - HYPHEN : 189, - DOT : 190, - SLASH : 191, - TRA : 192, /* Typewritten Reverse Apostrophe (`) */ - BACKSLASH : 220, - - BRACKET_CLOSE: 221, -}; - +export const BACKSPACE = 8; +export const TAB = 9; +export const ENTER = 13; +export const CAPSLOCK = 20; +export const ESC = 27; +export const SPACE = 32; +export const PAGE_UP = 33; +export const PAGE_DOWN = 34; +export const END = 35; +export const HOME = 36; +export const LEFT = 37; +export const UP = 38; +export const RIGHT = 39; +export const DOWN = 40; +export const INSERT = 45; +export const DELETE = 46; +export const ZERO = 48; +export const SEMICOLON = 52; +export const A = 65; +export const C = 67; +export const D = 68; +export const G = 71; +export const J = 74; +export const K = 75; +export const L = 76; +export const M = 77; +export const O = 79; +export const P = 80; +export const Q = 81; +export const R = 82; +export const S = 83; +export const T = 84; +export const U = 85; +export const V = 86; +export const X = 88; +export const Z = 90; +export const INSERT_MAC = 96; +export const ASTERISK = 106; +export const PLUS = 107; +export const MINUS = 109; +export const F1 = 112; +export const F2 = 113; +export const F3 = 114; +export const F4 = 115; +export const F5 = 116; +export const F6 = 117; +export const F7 = 118; +export const F8 = 119; +export const F9 = 120; +export const F10 = 121; +export const COLON = 186; +export const EQUAL = 187; +export const HYPHEN = 189; +export const DOT = 190; +export const SLASH = 191; +export const TRA = 192; +export const BACKSLASH = 220; +export const BRACKET_CLOSE = 221; diff --git a/client/key/set-current-by-char.js b/client/key/set-current-by-char.js index 3fbf79cd1b..92764f11fc 100644 --- a/client/key/set-current-by-char.js +++ b/client/key/set-current-by-char.js @@ -1,18 +1,15 @@ /* global DOM */ +import {escapeRegExp} from '#common/util'; -'use strict'; - -const Info = DOM.CurrentInfo; -const {escapeRegExp} = require('../../common/util'); - -module.exports = function setCurrentByChar(char, charStore) { +export default function setCurrentByChar(char, charStore) { + const Info = DOM.CurrentInfo; let firstByName; let skipCount = 0; - let setted = false; + let set = false; let i = 0; const escapeChar = escapeRegExp(char); - const regExp = new RegExp('^' + escapeChar + '.*$', 'i'); + const regExp = new RegExp(`^${escapeChar}.*$`, 'i'); const {files} = Info; const chars = charStore(); const n = chars.length; @@ -31,12 +28,14 @@ module.exports = function setCurrentByChar(char, charStore) { const isTest = (a) => regExp.test(a); const isRoot = (a) => a === '..'; const not = (f) => (a) => !f(a); + const setCurrent = (name) => { const byName = DOM.getCurrentByName(name); if (!skipCount) { - setted = true; + set = true; DOM.setCurrentFile(byName); + return true; } @@ -51,9 +50,8 @@ module.exports = function setCurrentByChar(char, charStore) { .filter(not(isRoot)) .some(setCurrent); - if (!setted) { + if (!set) { DOM.setCurrentFile(firstByName); charStore([char]); } -}; - +} diff --git a/client/key/vim/find.js b/client/key/vim/find.js index a5f415d0de..e8db825aa1 100644 --- a/client/key/vim/find.js +++ b/client/key/vim/find.js @@ -1,12 +1,10 @@ -'use strict'; - -const fullstore = require('fullstore'); -const limier = require('limier'); +import {fullstore} from 'fullstore'; +import limier from 'limier'; const searchStore = fullstore([]); const searchIndex = fullstore(0); -module.exports.find = (value, names) => { +export const find = (value, names) => { const result = limier(value, names); searchStore(result); @@ -15,7 +13,7 @@ module.exports.find = (value, names) => { return result; }; -module.exports.findNext = () => { +export const findNext = () => { const names = searchStore(); const index = next(searchIndex(), names.length); @@ -23,7 +21,7 @@ module.exports.findNext = () => { return names[searchIndex()]; }; -module.exports.findPrevious = () => { +export const findPrevious = () => { const names = searchStore(); const index = previous(searchIndex(), names.length); @@ -31,8 +29,8 @@ module.exports.findPrevious = () => { return names[index]; }; -module.exports._next = next; -module.exports._previous = previous; +export const _next = next; +export const _previous = previous; function next(index, length) { if (index === length - 1) @@ -47,4 +45,3 @@ function previous(index, length) { return --index; } - diff --git a/client/key/vim/find.spec.js b/client/key/vim/find.spec.js index 6dbe073d0b..59c9a249a3 100644 --- a/client/key/vim/find.spec.js +++ b/client/key/vim/find.spec.js @@ -1,16 +1,8 @@ -'use strict'; +import test from 'supertape'; +import {getDOM} from './globals.fixture.js'; +import {_next, _previous} from './find.js'; -const test = require('supertape'); -const dir = './'; - -const {getDOM} = require('./globals.fixture'); - -global.DOM = getDOM(); - -const { - _next, - _previous, -} = require(dir + 'find'); +globalThis.DOM = getDOM(); test('cloudcmd: client: vim: _next', (t) => { const result = _next(1, 2); @@ -25,4 +17,3 @@ test('cloudcmd: client: vim: _previous', (t) => { t.equal(result, 1, 'should return 1'); t.end(); }); - diff --git a/client/key/vim/globals.fixture.js b/client/key/vim/globals.fixture.js index ec1fbbcfc2..c49894d75f 100644 --- a/client/key/vim/globals.fixture.js +++ b/client/key/vim/globals.fixture.js @@ -1,8 +1,6 @@ -'use strict'; - const noop = () => {}; -module.exports.getDOM = () => { +export const getDOM = () => { const prompt = Promise.resolve.bind(Promise); const CurrentInfo = { element: {}, @@ -29,14 +27,16 @@ module.exports.getDOM = () => { getCurrentName: noop, setCurrentByName: noop, toggleSelectedFile: noop, + prompNewDirectory: noop, + promptNewFile: noop, }; }; -module.exports.getCloudCmd = () => { +export const getCloudCmd = () => { const show = () => {}; return { - Operation: { + Operation: { show, }, @@ -44,4 +44,3 @@ module.exports.getCloudCmd = () => { _config: noop, }; }; - diff --git a/client/key/vim/index.js b/client/key/vim/index.js index 7b781057d5..981f6f19bf 100644 --- a/client/key/vim/index.js +++ b/client/key/vim/index.js @@ -1,109 +1,167 @@ -'use strict'; +import vim from './vim.js'; +import * as finder from './find.js'; +import { + setCurrent, + selectFileNotParent, +} from './set-current.js'; -/* global CloudCmd */ -/* global DOM */ - -const vim = require('./vim'); -const finder = require('./find'); - -const Info = DOM.CurrentInfo; -const {Dialog} = DOM; - -module.exports = async (key, event) => { - const operations = getOperations(event); - await vim(key, operations); -}; - -const getOperations = (event) => ({ - escape: DOM.unselectFiles, - - remove: () => { - CloudCmd.Operation.show('delete'); - }, - - copy: () => { - DOM.Buffer.copy(); - DOM.unselectFiles(); - }, - - select: () => { - const current = Info.element; - DOM.toggleSelectedFile(current); - }, +export default (key, event, overrides = {}) => { + const defaults = { + ...globalThis.DOM, + ...globalThis.CloudCmd, + }; - paste: DOM.Buffer.paste, + const deps = { + ...defaults, + ...overrides, + }; - moveNext: ({count, isVisual, isDelete}) => { - setCurrent('next', { - count, - isVisual, - isDelete, - }); - }, + const operations = getOperations(event, deps); - movePrevious: ({count, isVisual, isDelete}) => { - setCurrent('previous', { - count, - isVisual, - isDelete, - }); - }, + vim(key, operations, deps); +}; + +const getOperations = (event, deps) => { + const { + Info = globalThis.DOM.CurrentInfo, + CloudCmd = globalThis.CloudCmd, + Operation, + unselectFiles, + setCurrentFile, + setCurrentByName, + getCurrentName, + prompt = globalThis.DOM.Dialog.prompt, + preventDefault = event?.preventDefault?.bind(event), + stopImmediatePropagation = event?.preventDefault?.bind(event), + promptNewFile = globalThis.DOM.promptNewFile, + toggleSelectedFile, + Buffer = {}, + createFindNext = _createFindNext, + createFindPrevious = _createFindPrevious, + createMakeFile = _createMakeFile, + renameCurrent, + } = deps; - find: async () => { - event.preventDefault(); - const [, value] = await Dialog.prompt('Find', ''); + return { + makeFile: createMakeFile({ + promptNewFile, + preventDefault, + stopImmediatePropagation, + }), + findNext: createFindNext({ + setCurrentByName, + }), + findPrevious: createFindPrevious({ + setCurrentByName, + }), + escape: unselectFiles, + rename: () => { + event.preventDefault(); + renameCurrent(); + }, + remove: () => { + Operation.show('delete'); + }, + operationCopy: () => { + event.preventDefault(); + Operation.show('copy'); + }, + operationMove: () => { + event.preventDefault(); + Operation.show('move'); + }, - if (!value) - return; + makeDirectory: () => { + event.stopImmediatePropagation(); + event.preventDefault(); + globalThis.DOM.promptNewDir(); + }, - const names = Info.files.map(DOM.getCurrentName); - const [result] = finder.find(value, names); + terminal: () => { + CloudCmd.Terminal.show(); + }, - DOM.setCurrentByName(result); - }, - - findNext: () => { - const name = finder.findNext(); - DOM.setCurrentByName(name); - }, - - findPrevious: () => { - const name = finder.findPrevious(); - DOM.setCurrentByName(name); - }, -}); + edit: () => { + CloudCmd.EditFileVim.show(); + }, + + copy: () => { + Buffer.copy(); + unselectFiles(); + }, + + select: () => { + const current = Info.element; + toggleSelectedFile(current); + }, + + paste: Buffer.paste, + + moveNext: ({count, isVisual, isDelete}) => { + setCurrent('next', { + count, + isVisual, + isDelete, + }, { + Info, + setCurrentFile, + unselectFiles, + Operation, + }); + }, + + movePrevious: ({count, isVisual, isDelete}) => { + setCurrent('previous', { + count, + isVisual, + isDelete, + }, { + Info, + setCurrentFile, + unselectFiles, + Operation, + }); + }, + + find: async () => { + preventDefault(); + const [, value] = await prompt('Find', ''); + + if (!value) + return; + + const names = Info.files.map(getCurrentName); + const [result] = finder.find(value, names); + + setCurrentByName(result); + }, + }; +}; -module.exports.selectFile = selectFile; +export const selectFile = selectFileNotParent; -function selectFile(current) { - const name = DOM.getCurrentName(current); +const _createFindPrevious = (overrides = {}) => () => { + const {setCurrentByName} = overrides; + const name = finder.findPrevious(); - if (name === '..') - return; - - DOM.selectFile(current); -} + setCurrentByName(name); +}; -function setCurrent(sibling, {count, isVisual, isDelete}) { - let current = Info.element; - const select = isVisual ? selectFile : DOM.unselectFile; - - select(current); +const _createFindNext = (overrides = {}) => () => { + const {setCurrentByName} = overrides; + const name = finder.findNext(); - const position = `${sibling}Sibling`; - for (let i = 0; i < count; i++) { - const next = current[position]; - - if (!next) - break; - - current = next; - select(current); - } - - DOM.setCurrentFile(current); - - if (isDelete) - CloudCmd.Operation.show('delete'); -} + setCurrentByName(name); +}; +const _createMakeFile = (overrides = {}) => () => { + const { + promptNewFile, + stopImmediatePropagation, + preventDefault, + } = overrides; + + stopImmediatePropagation(); + preventDefault(); + promptNewFile(); +}; diff --git a/client/key/vim/index.spec.js b/client/key/vim/index.spec.js index 00cd1603d6..e1ca4eeeb8 100644 --- a/client/key/vim/index.spec.js +++ b/client/key/vim/index.spec.js @@ -1,45 +1,34 @@ -'use strict'; +import {test, stub} from 'supertape'; +import {getDOM, getCloudCmd} from './globals.fixture.js'; +import vim, {selectFile as vimSelectFile} from './index.js'; -const {join} = require('path'); +globalThis.DOM = getDOM(); +globalThis.CloudCmd = getCloudCmd(); -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); -const mockRequire = require('mock-require'); -const {reRequire, stopAll} = mockRequire; - -const dir = '../'; - -const pathVim = join(dir, 'vim'); -const pathFind = join(dir, 'vim', 'find'); - -const { - getDOM, - getCloudCmd, -} = require('./globals.fixture'); - -global.DOM = getDOM(); -global.CloudCmd = getCloudCmd(); - -const {DOM} = global; +const {assign} = Object; +const {DOM} = globalThis; const {Buffer} = DOM; -const vim = require(pathVim); - test('cloudcmd: client: key: set next file: no', (t) => { const element = {}; - const setCurrentFile = stub(); + const unselectFiles = stub(); - global.DOM.CurrentInfo.element = element; - global.DOM.setCurrentFile = setCurrentFile; + const Info = { + element, + }; - vim('j', {}); + vim('j', {}, { + Info, + setCurrentFile, + unselectFiles, + }); t.calledWith(setCurrentFile, [element], 'should set next file'); t.end(); }); -test('cloudcmd: client: key: set next file current', (t) => { +test('cloudcmd: client: key: set next file current: j', async (t) => { const nextSibling = 'hello'; const element = { nextSibling, @@ -47,16 +36,21 @@ test('cloudcmd: client: key: set next file current', (t) => { const setCurrentFile = stub(); - global.DOM.CurrentInfo.element = element; - global.DOM.setCurrentFile = setCurrentFile; + const Info = { + element, + }; - vim('j', {}); + await vim('j', {}, { + Info, + setCurrentFile, + unselectFiles: stub(), + }); t.calledWith(setCurrentFile, [nextSibling], 'should set next file'); t.end(); }); -test('cloudcmd: client: key: set next file current', (t) => { +test('cloudcmd: client: key: set next file current: mjj', (t) => { const nextSibling = 'hello'; const element = { nextSibling, @@ -64,12 +58,19 @@ test('cloudcmd: client: key: set next file current', (t) => { const setCurrentFile = stub(); - global.DOM.CurrentInfo.element = element; - global.DOM.setCurrentFile = setCurrentFile; + const Info = { + element, + }; + + const deps = { + Info, + setCurrentFile, + unselectFiles: stub(), + }; - vim('m', {}); - vim('j', {}); - vim('j', {}); + vim('m', {}, deps); + vim('j', {}, deps); + vim('j', {}, deps); t.calledWith(setCurrentFile, [nextSibling], 'should set next file'); t.end(); @@ -83,11 +84,18 @@ test('cloudcmd: client: key: set next file current: g', (t) => { const setCurrentFile = stub(); - global.DOM.CurrentInfo.element = element; - global.DOM.setCurrentFile = setCurrentFile; + const Info = { + element, + }; + + const deps = { + Info, + setCurrentFile, + unselectFiles: stub(), + }; - vim('g', {}); - vim('j', {}); + vim('g', {}, deps); + vim('j', {}, deps); t.calledWith(setCurrentFile, [nextSibling], 'should ignore g'); t.end(); @@ -95,22 +103,23 @@ test('cloudcmd: client: key: set next file current: g', (t) => { test('cloudcmd: client: key: set +2 file current', (t) => { const last = {}; - const nextSibling = { - nextSibling: last, - }; - const element = { - nextSibling, - }; - const setCurrentFile = stub(); + const element = {}; + + const Info = { + element, + }; - global.DOM.CurrentInfo.element = element; - global.DOM.setCurrentFile = setCurrentFile; + const deps = { + setCurrentFile, + Info, + unselectFiles: stub(), + }; const event = {}; - vim('2', event); - vim('j', event); + vim('2', event, deps); + vim('j', event, deps); t.calledWith(setCurrentFile, [last], 'should set next file'); t.end(); @@ -121,23 +130,37 @@ test('cloudcmd: client: key: select +2 files from current before delete', (t) => const nextSibling = { nextSibling: last, }; + const element = { nextSibling, }; const setCurrentFile = stub(); - global.DOM.CurrentInfo.element = element; - global.DOM.setCurrentFile = setCurrentFile; - global.DOM.selectFile = stub(); - global.DOM.getCurrentName = () => false; - global.CloudCmd.Operation.show = stub(); + const Info = { + element, + }; + + const Operation = { + show: stub(), + }; + + const selectFile = stub(); + const getCurrentName = stub().returns('x'); const event = {}; - vim('d', event); - vim('2', event); - vim('j', event); + const deps = { + Info, + setCurrentFile, + selectFile, + getCurrentName, + Operation, + }; + + vim('d', event, deps); + vim('2', event, deps); + vim('j', event, deps); t.calledWith(setCurrentFile, [last], 'should set next file'); t.end(); @@ -148,6 +171,7 @@ test('cloudcmd: client: key: delete +2 files from current', (t) => { const nextSibling = { nextSibling: last, }; + const element = { nextSibling, }; @@ -155,17 +179,24 @@ test('cloudcmd: client: key: delete +2 files from current', (t) => { const setCurrentFile = stub(); const show = stub(); - global.DOM.CurrentInfo.element = element; - global.DOM.setCurrentFile = setCurrentFile; - global.DOM.selectFile = stub(); - global.DOM.getCurrentName = () => false; - global.CloudCmd.Operation.show = show; + const deps = { + Info: { + element, + }, + Operation: { + show, + }, + setCurrentFile, + selectFile: stub(), + getCurrentName: stub().returns('x'), + unselectFiles: stub(), + }; const event = {}; - vim('d', event); - vim('2', event); - vim('j', event); + vim('d', event, deps); + vim('2', event, deps); + vim('j', event, deps); t.calledWith(show, ['delete'], 'should call delete'); t.end(); @@ -178,11 +209,19 @@ test('cloudcmd: client: key: set previous file current', (t) => { }; const setCurrentFile = stub(); + const unselectFiles = stub(); - global.DOM.CurrentInfo.element = element; - global.DOM.setCurrentFile = setCurrentFile; + const Info = { + element, + }; - vim('k', {}); + const deps = { + Info, + setCurrentFile, + unselectFiles, + }; + + vim('k', {}, deps); t.calledWith(setCurrentFile, [previousSibling], 'should set previous file'); t.end(); @@ -191,35 +230,63 @@ test('cloudcmd: client: key: set previous file current', (t) => { test('cloudcmd: client: key: copy: no', (t) => { const copy = stub(); - Buffer.copy = copy; - - vim('y', {}); + vim('y', {}, { + unselectFiles: stub(), + Buffer: { + copy, + }, + }); - t.notOk(copy.called, 'should not copy files'); + t.notCalled(copy, 'should not copy files'); t.end(); }); test('cloudcmd: client: key: copy', (t) => { const copy = stub(); + const Info = { + element: {}, + }; - Buffer.copy = copy; + const toggleSelectedFile = stub(); + const unselectFiles = stub(); - vim('v', {}); - vim('y', {}); + const deps = { + Info, + unselectFiles, + toggleSelectedFile, + Buffer: { + copy, + }, + }; - t.ok(copy.calledWith(), 'should copy files'); + vim('v', {}, deps); + vim('y', {}, deps); + + t.calledWithNoArgs(copy, 'should copy files'); t.end(); }); test('cloudcmd: client: key: copy: unselectFiles', (t) => { const unselectFiles = stub(); + const Info = { + element: {}, + }; - DOM.unselectFiles = unselectFiles; + const toggleSelectedFile = stub(); + + const deps = { + Info, + unselectFiles, + toggleSelectedFile, + Buffer: { + copy: stub(), + }, + }; - vim('v', {}); - vim('y', {}); + vim('v', {}, deps); + vim('y', {}, deps); - t.ok(unselectFiles.calledWith(), 'should unselect files'); + t.calledWithNoArgs(unselectFiles, 'should unselect files'); t.end(); }); @@ -228,55 +295,61 @@ test('cloudcmd: client: key: paste', (t) => { Buffer.paste = paste; - vim('p', {}); + vim('p', {}, { + Buffer, + }); - t.ok(paste.calledWith(), 'should paste files'); + t.calledWithNoArgs(paste, 'should paste files'); t.end(); }); test('cloudcmd: client: key: selectFile: ..', (t) => { + const getCurrentName = stub().returns('..'); const selectFile = stub(); - const getCurrentName = stub(); - - DOM.selectFile = selectFile; - DOM.getCurrentName = () => '..'; - const current = {}; - vim.selectFile(current); - t.notOk(getCurrentName.called, 'should not call selectFile'); + vimSelectFile(current, { + selectFile, + getCurrentName, + }); + + t.notCalled(selectFile, 'should not call selectFile'); t.end(); }); test('cloudcmd: client: key: selectFile', (t) => { const selectFile = stub(); - - DOM.selectFile = selectFile; - DOM.getCurrentName = (a) => a.name; - + const getCurrentName = stub().returns('x'); const current = {}; - vim.selectFile(current); + vimSelectFile(current, { + selectFile, + getCurrentName, + }); t.calledWith(selectFile, [current], 'should call selectFile'); t.end(); }); -test('cloudcmd: client: key: set last file current: shift + g', (t) => { +test('cloudcmd: client: key: set last file current: shift + g', async (t) => { const last = 'last'; const nextSibling = { nextSibling: last, }; + const element = { nextSibling, }; const setCurrentFile = stub(); - global.DOM.CurrentInfo.element = element; - global.DOM.setCurrentFile = setCurrentFile; - - vim('G', {}); + await vim('G', {}, { + Info: { + element, + }, + setCurrentFile, + unselectFiles: stub(), + }); t.calledWith(setCurrentFile, [last], 'should set last file'); t.end(); @@ -287,16 +360,20 @@ test('cloudcmd: client: key: set last file current: $', (t) => { const nextSibling = { nextSibling: last, }; + const element = { nextSibling, }; const setCurrentFile = stub(); - global.DOM.CurrentInfo.element = element; - global.DOM.setCurrentFile = setCurrentFile; - - vim('$', {}); + vim('$', {}, { + Info: { + element, + }, + setCurrentFile, + unselectFiles: stub(), + }); t.calledWith(setCurrentFile, [last], 'should set last file'); t.end(); @@ -312,19 +389,30 @@ test('cloudcmd: client: key: set first file current: gg', (t) => { previousSibling, }; + const Operation = { + show: stub(), + }; + + const unselectFiles = stub(); const setCurrentFile = stub(); - global.DOM.CurrentInfo.element = element; - global.DOM.setCurrentFile = setCurrentFile; + const deps = { + Operation, + unselectFiles, + setCurrentFile, + Info: { + element, + }, + }; - vim('g', {}); - vim('g', {}); + vim('g', {}, deps); + vim('g', {}, deps); t.calledWith(setCurrentFile, [first], 'should set first file'); t.end(); }); -test('cloudcmd: client: key: set first file current: ^', (t) => { +test('cloudcmd: client: key: set first file current: ^', async (t) => { const first = 'first'; const previousSibling = { previousSibling: first, @@ -334,12 +422,23 @@ test('cloudcmd: client: key: set first file current: ^', (t) => { previousSibling, }; + const Operation = { + show: stub(), + }; + + const unselectFiles = stub(); const setCurrentFile = stub(); - global.DOM.CurrentInfo.element = element; - global.DOM.setCurrentFile = setCurrentFile; + const deps = { + setCurrentFile, + Info: { + element, + }, + unselectFiles, + Operation, + }; - vim('^', {}); + await vim('^', {}, deps); t.calledWith(setCurrentFile, [first], 'should set first file'); t.end(); @@ -347,13 +446,16 @@ test('cloudcmd: client: key: set first file current: ^', (t) => { test('cloudcmd: client: key: visual', (t) => { const element = {}; - const toggleSelectedFile = stub(); - global.DOM.CurrentInfo.element = element; - global.DOM.toggleSelectedFile = toggleSelectedFile; + const Info = { + element, + }; - vim('v', {}); + vim('v', {}, { + Info, + toggleSelectedFile, + }); t.calledWith(toggleSelectedFile, [element], 'should toggle selection'); t.end(); @@ -361,32 +463,45 @@ test('cloudcmd: client: key: visual', (t) => { test('cloudcmd: client: key: ESC', (t) => { const element = {}; - const unselectFiles = stub(); - global.DOM.CurrentInfo.element = element; - global.DOM.unselectFiles = unselectFiles ; + const Info = { + element, + }; - vim('Escape'); + vim('Escape', null, { + Info, + unselectFiles, + }); - t.ok(unselectFiles.calledWith(), 'should toggle selection'); + t.calledWithNoArgs(unselectFiles, 'should toggle selection'); t.end(); }); -test('cloudcmd: client: key: Enter', (t) => { +test('cloudcmd: client: key: Enter', async (t) => { const nextSibling = 'hello'; const element = { nextSibling, }; + const unselectFiles = stub(); const setCurrentFile = stub(); - DOM.CurrentInfo.element = element; - DOM.setCurrentFile = setCurrentFile; + const Info = { + element, + }; - vim('Enter'); + await vim('Enter', null, { + Info, + setCurrentFile, + unselectFiles, + }); - vim('j'); + await vim('j', null, { + Info, + setCurrentFile, + unselectFiles, + }); t.calledWith(setCurrentFile, [nextSibling], 'should set next file'); t.end(); @@ -396,50 +511,167 @@ test('cloudcmd: client: key: /', (t) => { const preventDefault = stub(); const element = {}; - DOM.CurrentInfo.element = element; - DOM.getCurrentName = () => ''; + const Info = { + element, + files: [], + }; + + const getCurrentName = stub().returns(''); - vim('/', { + const event = { preventDefault, + }; + + const prompt = stub().returns([]); + + vim('/', event, { + getCurrentName, + Info, + prompt, }); - t.ok(preventDefault.calledWith(), 'should call preventDefault'); + t.calledWithNoArgs(preventDefault); + t.end(); +}); + +test('cloudcmd: client: find', (t) => { + assign(DOM.Dialog, { + prompt: stub().returns([]), + }); + + const setCurrentByName = stub(); + + assign(DOM, { + setCurrentByName, + }); + + const event = { + preventDefault: stub(), + }; + + vim('/', event); + + t.notCalled(setCurrentByName); t.end(); }); test('cloudcmd: client: key: n', (t) => { const findNext = stub(); + const createFindNext = stub().returns(findNext); + + const event = {}; - mockRequire(pathFind, { - findNext, + vim('n', event, { + createFindNext, }); - const vim = reRequire(pathVim); + t.calledWithNoArgs(findNext, 'should call findNext'); + t.end(); +}); + +test('cloudcmd: client: key: N', (t) => { + const findPrevious = stub(); + const createFindPrevious = stub().returns(findPrevious); const event = {}; - vim('n', event); + vim('N', event, { + createFindPrevious, + }); - stopAll(); + t.calledWithNoArgs(findPrevious); + t.end(); +}); + +test('cloudcmd: client: key: make directory', async (t) => { + const {DOM} = globalThis; + + assign(DOM, { + promptNewDir: stub(), + }); - t.ok(findNext.calledWith(), 'should call findNext'); + const event = { + stopImmediatePropagation: stub(), + preventDefault: stub(), + }; + + await vim('m', event); + await vim('d', event); + + t.calledWithNoArgs(DOM.promptNewDir); t.end(); }); -test('cloudcmd: client: key: N', (t) => { - const findPrevious = stub(); +test('cloudcmd: client: key: make file', (t) => { + const promptNewFile = stub(); + + const event = { + stopImmediatePropagation: stub(), + preventDefault: stub(), + }; - mockRequire(pathFind, { - findPrevious, + vim('m', event); + vim('f', event, { + promptNewFile, }); - const vim = reRequire(dir + 'vim'); + t.calledWithNoArgs(promptNewFile); + t.end(); +}); + +test('cloudcmd: client: vim: terminal', (t) => { + const CloudCmd = { + Terminal: { + show: stub(), + }, + }; + const event = {}; - vim('N', event); + vim('t', event, { + CloudCmd, + }); + vim('t', event, { + CloudCmd, + }); + + t.calledWithNoArgs(CloudCmd.Terminal.show); + t.end(); +}); + +test('cloudcmd: client: vim: edit', async (t) => { + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd(); + + const {CloudCmd} = globalThis; + + assign(CloudCmd, { + EditFileVim: { + show: stub(), + }, + }); + + const event = {}; - stopAll(); + await vim('e', event); - t.ok(findPrevious.calledWith(), 'should call findPrevious'); + t.calledWithNoArgs(CloudCmd.EditFileVim.show); t.end(); }); +test('cloudcmd: client: vim: rename', async (t) => { + const DOM = getDOM(); + const renameCurrent = stub(); + + assign(DOM, { + renameCurrent, + }); + + const event = { + preventDefault: stub(), + }; + + await vim('rr', event, DOM); + + t.calledWithNoArgs(renameCurrent); + t.end(); +}); diff --git a/client/key/vim/set-current.js b/client/key/vim/set-current.js new file mode 100644 index 0000000000..a2fc86464b --- /dev/null +++ b/client/key/vim/set-current.js @@ -0,0 +1,33 @@ +/* global DOM */ +export function selectFileNotParent(current, {getCurrentName, selectFile} = DOM) { + const name = getCurrentName(current); + + if (name === '..') + return; + + selectFile(current); +} + +export const setCurrent = (sibling, {count, isVisual, isDelete}, {Info, setCurrentFile, unselectFiles, Operation}) => { + let current = Info.element; + const select = isVisual ? selectFileNotParent : unselectFiles; + + select(current); + + const position = `${sibling}Sibling`; + + for (let i = 0; i < count; i++) { + const next = current[position]; + + if (!next) + break; + + current = next; + select(current); + } + + setCurrentFile(current); + + if (isDelete) + Operation.show('delete'); +}; diff --git a/client/key/vim/vim.js b/client/key/vim/vim.js index 7a3d401713..95a27caf20 100644 --- a/client/key/vim/vim.js +++ b/client/key/vim/vim.js @@ -1,6 +1,5 @@ -'use strict'; +import {fullstore} from 'fullstore'; -const fullstore = require('fullstore'); const store = fullstore(''); const visual = fullstore(false); @@ -21,10 +20,11 @@ const rmFirst = (a) => { const noop = () => {}; -module.exports = (key, operations) => { +export default (key, operations = {}) => { const prevStore = store(); const isVisual = visual(); const value = store(prevStore.concat(key)); + const { escape = noop, moveNext = noop, @@ -36,6 +36,13 @@ module.exports = (key, operations) => { find = noop, findNext = noop, findPrevious = noop, + makeFile = noop, + makeDirectory = noop, + terminal = noop, + edit = noop, + operationCopy = noop, + operationMove = noop, + rename = noop, } = operations; if (key === 'Enter') @@ -44,6 +51,7 @@ module.exports = (key, operations) => { if (key === 'Escape') { visual(false); escape(); + return end(); } @@ -80,10 +88,7 @@ module.exports = (key, operations) => { } if (value === 'gg' || key === '^') { - const { - isDelete, - isVisual, - } = handleDelete(prevStore); + const {isDelete, isVisual} = handleDelete(prevStore); movePrevious({ count: Infinity, @@ -94,9 +99,45 @@ module.exports = (key, operations) => { return end(); } + if (value === 'md') { + makeDirectory(); + return end(); + } + + if (value === 'tt') { + terminal(); + return end(); + } + + if (value === 'e') { + edit(); + return end(); + } + + if (value === 'cc') { + operationCopy(); + return end(); + } + + if (value === 'mm') { + operationMove(); + return end(); + } + + if (value === 'mf') { + makeFile(); + return end(); + } + + if (value === 'rr') { + rename(); + return end(); + } + if (key === 'd' && (visual() || prevStore === 'd')) { stopVisual(); remove(); + return end(); } @@ -115,6 +156,7 @@ module.exports = (key, operations) => { stopVisual(); copy(); + return end(); } @@ -126,6 +168,7 @@ module.exports = (key, operations) => { if (/^v$/i.test(key)) { visual(!visual()); select(); + return end(); } @@ -143,6 +186,9 @@ module.exports = (key, operations) => { findPrevious(); return end(); } + + if (key === ' ') + return end(); }; function handleDelete(prevStore) { @@ -172,4 +218,3 @@ function getNumber(value) { return parseInt(value); } - diff --git a/client/key/vim/vim.spec.js b/client/key/vim/vim.spec.js index f39552ee9c..02fd52d430 100644 --- a/client/key/vim/vim.spec.js +++ b/client/key/vim/vim.spec.js @@ -1,9 +1,5 @@ -'use strict'; - -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); - -const vim = require('./vim'); +import {test, stub} from 'supertape'; +import vim from './vim.js'; test('vim: no operations', (t) => { const result = vim('hello', {}); @@ -12,6 +8,24 @@ test('vim: no operations', (t) => { t.end(); }); +test('vim: space', (t) => { + const moveNext = stub(); + + vim(' '); + vim('j', { + moveNext, + }); + + const args = [{ + count: 1, + isDelete: false, + isVisual: false, + }]; + + t.calledWith(moveNext, args); + t.end(); +}); + test('vim: ^', (t) => { const movePrevious = stub(); @@ -29,6 +43,28 @@ test('vim: ^', (t) => { t.end(); }); +test('vim: cc', (t) => { + const operationCopy = stub(); + + vim('cc', { + operationCopy, + }); + + t.calledWithNoArgs(operationCopy); + t.end(); +}); + +test('vim: mm', (t) => { + const operationMove = stub(); + + vim('mm', { + operationMove, + }); + + t.calledWithNoArgs(operationMove); + t.end(); +}); + test('vim: w', (t) => { const moveNext = stub(); @@ -62,4 +98,3 @@ test('vim: b', (t) => { t.calledWith(movePrevious, [expected], 'should call movePrevious'); t.end(); }); - diff --git a/client/listeners/get-index.js b/client/listeners/get-index.js index d78706088d..45ce85e8ee 100644 --- a/client/listeners/get-index.js +++ b/client/listeners/get-index.js @@ -1,11 +1,10 @@ -'use strict'; +import currify from 'currify'; -module.exports = (array, item) => { +export const getIndex = currify((array, item) => { const index = array.indexOf(item); if (!~index) return 0; return index; -}; - +}); diff --git a/test/client/listeners/get-index.js b/client/listeners/get-index.spec.js similarity index 65% rename from test/client/listeners/get-index.js rename to client/listeners/get-index.spec.js index 0f13acd6af..0380cdc17a 100644 --- a/test/client/listeners/get-index.js +++ b/client/listeners/get-index.spec.js @@ -1,9 +1,5 @@ -'use strict'; - -const test = require('supertape'); - -const dir = '../../../client/listeners'; -const getIndex = require(`${dir}/get-index`); +import test from 'supertape'; +import {getIndex} from './get-index.js'; test('cloudcmd: client: listeners: getIndex: not found', (t) => { const array = ['hello']; @@ -13,9 +9,11 @@ test('cloudcmd: client: listeners: getIndex: not found', (t) => { }); test('cloudcmd: client: listeners: getIndex: found', (t) => { - const array = ['hello', 'world']; + const array = [ + 'hello', + 'world', + ]; t.equal(getIndex(array, 'world'), 1, 'should return index'); t.end(); }); - diff --git a/client/listeners/get-range.js b/client/listeners/get-range.js index ef1ea0f873..839c2287ef 100644 --- a/client/listeners/get-range.js +++ b/client/listeners/get-range.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = (indexFrom, indexTo, files) => { +export const getRange = (indexFrom, indexTo, files) => { if (indexFrom < indexTo) return files.slice(indexFrom, indexTo + 1); @@ -9,4 +7,3 @@ module.exports = (indexFrom, indexTo, files) => { return [files[indexFrom]]; }; - diff --git a/test/client/listeners/get-range.js b/client/listeners/get-range.spec.js similarity index 56% rename from test/client/listeners/get-range.js rename to client/listeners/get-range.spec.js index 1cd7cd7b66..77c950f481 100644 --- a/test/client/listeners/get-range.js +++ b/client/listeners/get-range.spec.js @@ -1,13 +1,18 @@ -'use strict'; - -const test = require('supertape'); - -const dir = '../../../client/listeners'; -const getRange = require(`${dir}/get-range`); +import test from 'supertape'; +import {getRange} from './get-range.js'; test('cloudcmd: client: listeners: getRange: direct', (t) => { - const expected = ['hello', 'world']; - const files = [...expected, 'how', 'come']; + const expected = [ + 'hello', + 'world', + ]; + + const files = [ + ...expected, + 'how', + 'come', + ]; + const result = getRange(0, 1, files); t.deepEqual(result, expected, 'should return range'); @@ -15,8 +20,17 @@ test('cloudcmd: client: listeners: getRange: direct', (t) => { }); test('cloudcmd: client: listeners: getRange: reverse', (t) => { - const expected = ['hello', 'world']; - const files = [...expected, 'how', 'come']; + const expected = [ + 'hello', + 'world', + ]; + + const files = [ + ...expected, + 'how', + 'come', + ]; + const result = getRange(1, 0, files); t.deepEqual(result, expected, 'should return range'); @@ -25,10 +39,14 @@ test('cloudcmd: client: listeners: getRange: reverse', (t) => { test('cloudcmd: client: listeners: getRange: one', (t) => { const expected = ['hello']; - const files = [...expected, 'how', 'come']; + const files = [ + ...expected, + 'how', + 'come', + ]; + const result = getRange(0, 0, files); t.deepEqual(result, expected, 'should return range'); t.end(); }); - diff --git a/client/listeners/index.js b/client/listeners/index.js index 2fee5c2453..babc1e0d41 100644 --- a/client/listeners/index.js +++ b/client/listeners/index.js @@ -1,38 +1,30 @@ /* global DOM, CloudCmd */ - -'use strict'; - -const exec = require('execon'); -const itype = require('itype'); -const currify = require('currify'); -const tryToCatch = require('try-to-catch'); -const clipboard = require('@cloudcmd/clipboard'); - -const getRange = require('./get-range'); -const getIndex = currify(require('./get-index')); -const uploadFiles = require('../dom/upload-files'); - -const {FS} = require('../../common/cloudfunc'); +import exec from 'execon'; +import itype from 'itype'; +import currify from 'currify'; +import {tryToCatch} from 'try-to-catch'; +import clipboard from '@cloudcmd/clipboard'; +import * as Events from '#dom/events'; +import {uploadFiles} from '#dom/upload-files'; +import {FS} from '#common/cloudfunc'; +import {getRange} from './get-range.js'; +import {getIndex} from './get-index.js'; const NBSP_REG = RegExp(String.fromCharCode(160), 'g'); const SPACE = ' '; -module.exports.init = async () => { - await Promise.all([ - contextMenu(), - dragndrop(), - unload(), - pop(), - resize(), - header(), - config(), - ]); -}; - -CloudCmd.Listeners = module.exports; +export async function init() { + contextMenu(); + dragndrop(); + unload(); + pop(); + resize(); + header(); + await config(); +} const unselect = (event) => { - const isMac = /Mac/.test(window.navigator.platform); + const isMac = /Mac/.test(globalThis.navigator.platform); const { shiftKey, metaKey, @@ -50,14 +42,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), @@ -68,6 +55,7 @@ let EXT; function header() { const fm = DOM.getFM(); const isDataset = (el) => el.dataset; + const isPanel = (el) => { return /^js-(left|right)$/.test(el.dataset.name); }; @@ -79,8 +67,7 @@ function header() { if (parent.dataset.name !== 'js-fm-header') return; - const name = (el.dataset.name || '') - .replace('js-', ''); + const name = (el.dataset.name || '').replace('js-', ''); if (!/^(name|size|date)$/.test(name)) return; @@ -108,35 +95,39 @@ async function config() { EXT = DOM.getPackerExt(type); } -module.exports.initKeysPanel = () => { +export const initKeysPanel = () => { const keysElement = DOM.getById('js-keyspanel'); if (!keysElement) return; - Events.addClick(keysElement, ({target}) => { + Events.addClick(keysElement, (event) => { + const {target} = event; const {id} = target; + const operation = (name) => { const {Operation} = CloudCmd; - const fn = Operation.show.bind(null, name); - return fn; + return Operation.show.bind(null, name); }; const clickFuncs = { - 'f1' : CloudCmd.Help.show, - 'f2' : CloudCmd.UserMenu.show, - 'f3' : CloudCmd.View.show, - 'f4' : CloudCmd.EditFile.show, - 'f5' : operation('copy'), - 'f6' : operation('move'), - 'f7' : DOM.promptNewDir, - 'f8' : operation('delete'), - 'f9' : CloudCmd.Menu.show, - 'f10' : CloudCmd.Config.show, - '~' : CloudCmd.Konsole.show, - 'shift~' : CloudCmd.Terminal.show, - 'contact' : CloudCmd.Contact.show, + 'f1': CloudCmd.Help.show, + 'f2': CloudCmd.UserMenu.show, + 'f3': CloudCmd.View.show, + 'f4': CloudCmd.EditFile.show, + 'f5': operation('copy'), + 'f6': operation('move'), + 'f7': DOM.promptNewDir, + 'f8': operation('delete'), + 'f9': () => { + event.stopPropagation(); + CloudCmd.Menu.show(); + }, + 'f10': CloudCmd.Config.show, + '~': CloudCmd.Konsole.show, + 'shift~': CloudCmd.Terminal.show, + 'contact': CloudCmd.Contact.show, }; exec(clickFuncs[id]); @@ -147,10 +138,10 @@ const getPanel = (side) => { if (!itype.string(side)) return side; - return DOM.getByDataName('js-' + side); + return DOM.getByDataName(`js-${side}`); }; -module.exports.setOnPanel = (side) => { +export const setOnPanel = (side) => { const panel = getPanel(side); const filesElement = DOM.getByDataName('js-files', panel); @@ -166,6 +157,7 @@ function getPathListener(panel) { } function isNoCurrent(panel) { + const Info = DOM.CurrentInfo; const infoPanel = Info.panel; if (!infoPanel) @@ -184,13 +176,13 @@ function decodePath(path) { return decodeURI(path) .replace(url, '') - .replace(prefixReg, '') - // browser doesn't replace % -> %25% do it for him + .replace(prefixReg, '') // browser doesn't replace % -> %25% do it for him .replace('%%', '%25%') .replace(NBSP_REG, SPACE) || '/'; } async function onPathElementClick(panel, event) { + const Info = DOM.CurrentInfo; event.preventDefault(); const element = event.target; @@ -212,19 +204,26 @@ async function onPathElementClick(panel, event) { const {href} = element; const path = decodePath(href); - await CloudCmd.loadDir({ - path, + await CloudCmd.changeDir(path, { isRefresh: false, panel: noCurrent ? panel : Info.panel, }); } function copyPath(el) { - clipboard.writeText(el.parentElement.title) + clipboard + .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; @@ -239,14 +238,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) @@ -254,6 +253,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); @@ -272,9 +272,7 @@ async function onDblClick(event) { if (!isDir) return CloudCmd.View.show(); - await CloudCmd.loadDir({ - path, - }); + await CloudCmd.changeDir(path); } async function onTouch(event) { @@ -289,9 +287,7 @@ async function onTouch(event) { if (!isCurrent) return; - await CloudCmd.loadDir({ - path: DOM.getCurrentPath(current), - }); + await CloudCmd.changeDir(DOM.getCurrentPath(current)); } /* @@ -299,6 +295,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; @@ -315,8 +312,8 @@ function onDragStart(event) { event.dataTransfer.setData( 'DownloadURL', - 'application/octet-stream' + ':' + - name + ':' + + 'application/octet-stream' + ':' + name + + ':' + link, ); } @@ -339,6 +336,7 @@ function getULElement(element) { } function setCurrentFileByEvent(event) { + const Info = DOM.CurrentInfo; const BUTTON_LEFT = 0; const key = { @@ -402,10 +400,7 @@ function dragndrop() { }; const onDrop = (event) => { - const { - files, - items, - } = event.dataTransfer; + const {files, items} = event.dataTransfer; const {length: filesCount} = files; @@ -415,7 +410,10 @@ function dragndrop() { return uploadFiles(files); const isFile = (item) => item.kind === 'file'; - const dirFiles = Array.from(items).filter(isFile); + + const dirFiles = Array + .from(items) + .filter(isFile); if (dirFiles.length) return DOM.uploadDirectory(dirFiles); @@ -427,7 +425,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 */ @@ -452,7 +450,7 @@ function dragndrop() { } function unload() { - DOM.Events.add(['unload', 'beforeunload'], (event) => { + Events.add(['unload', 'beforeunload'], (event) => { const {Key} = CloudCmd; const isBind = Key?.isBind(); @@ -472,8 +470,8 @@ function pop() { return CloudCmd.route(location.hash); const history = false; - await CloudCmd.loadDir({ - path, + + await CloudCmd.changeDir(path, { history, }); }); @@ -481,7 +479,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; @@ -501,4 +500,3 @@ function resize() { DOM.changePanel(); }); } - diff --git a/client/load-module.js b/client/load-module.js index b9fb9f413c..7fc2c3286a 100644 --- a/client/load-module.js +++ b/client/load-module.js @@ -1,19 +1,16 @@ -'use strict'; - /* global CloudCmd */ +import exec from 'execon'; +import {tryToCatch} from 'try-to-catch'; +import {js as loadJS} from 'load.js'; +import pascalCase from 'just-pascal-case'; -const exec = require('execon'); -const tryToCatch = require('try-to-catch'); -const loadJS = require('load.js').js; - -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; @@ -25,25 +22,24 @@ module.exports = function loadModule(params) { if (CloudCmd[name]) return; - CloudCmd[name] = () => { + CloudCmd[name] = async () => { exec(doBefore); - const {prefix} = CloudCmd; - const pathFull = prefix + CloudCmd.DIRCLIENT_MODULES + path + '.js'; - - return loadJS(pathFull).then(async () => { - const newModule = async (f) => f && f(); - const module = CloudCmd[name]; - - Object.assign(newModule, module); - - CloudCmd[name] = newModule; - - CloudCmd.log('init', name); - await module.init(); - - return newModule; - }); + const {DIR_MODULES} = CloudCmd; + const pathFull = `${DIR_MODULES}/${path}.js`; + + await loadJS(pathFull); + const newModule = async (f) => f && f(); + const module = CloudCmd[name]; + + Object.assign(newModule, module); + + CloudCmd[name] = newModule; + CloudCmd.log('init', name); + + await module.init(); + + return newModule; }; CloudCmd[name].show = async (...args) => { @@ -53,9 +49,8 @@ module.exports = function loadModule(params) { const [e, a] = await tryToCatch(m); if (e) - return console.error(e); + return; - await a.show(...args); + return await a.show(...args); }; }; - diff --git a/client/modules/cloud.js b/client/modules/cloud.js index 4d6e4bd01c..cfe0d70868 100644 --- a/client/modules/cloud.js +++ b/client/modules/cloud.js @@ -1,31 +1,32 @@ /* global CloudCmd, filepicker */ - -'use strict'; - -const exec = require('execon'); -const currify = require('currify'); -const load = require('load.js'); +import exec from 'execon'; +import currify from 'currify'; +import load from 'load.js'; +import {ajax} from '#dom/load'; +import * as Files from '#dom/files'; +import * as Images from '#dom/images'; const {log} = CloudCmd; -const {ajax} = require('../dom/load'); -const Files = require('../dom/files'); -const Images = require('../dom/images'); - const upload = currify(_upload); const Name = 'Cloud'; -CloudCmd[Name] = module.exports; -module.exports.init = async () => { +CloudCmd[Name] = { + init, + uploadFile, + saveFile, +}; + +export async function init() { const [modules] = await loadFiles(); const {key} = modules.data.FilePicker; filepicker.setKey(key); Images.hide(); -}; +} -module.exports.uploadFile = (filename, data) => { +export function uploadFile(filename, data) { const mimetype = ''; filepicker.store(data, { @@ -34,17 +35,14 @@ module.exports.uploadFile = (filename, data) => { }, (fpFile) => { filepicker.exportFile(fpFile, log, log); }); -}; +} -module.exports.saveFile = (callback) => { +export function saveFile(callback) { filepicker.pick(upload(callback)); -}; +} function _upload(callback, file) { - const { - url, - filename, - } = file; + const {url, filename} = file; const responseType = 'arraybuffer'; const success = exec.with(callback, filename); @@ -64,4 +62,3 @@ function loadFiles() { load.js(js), ]); } - diff --git a/client/modules/command-line.js b/client/modules/command-line.js new file mode 100644 index 0000000000..7a1c39931f --- /dev/null +++ b/client/modules/command-line.js @@ -0,0 +1,31 @@ +/* global CloudCmd */ +import * as Dialog from '#dom/dialog'; + +export function init() {} +CloudCmd.CommandLine = { + init, + show, + hide, +}; + +export async function show() { + const [, cmd] = await Dialog.prompt('Command Line', ''); + const TERMINAL = '^(t|terminal)'; + + if (RegExp(`${TERMINAL}$`).test(cmd)) + return await CloudCmd.Terminal.show(); + + if (RegExp(TERMINAL).test(cmd)) { + const command = cmd.replace(RegExp(`${TERMINAL} `), ''); + const exitCode = await CloudCmd.TerminalRun.show({ + command: `bash -c '${command}'`, + }); + + if (exitCode === -1) + await Dialog.alert(`☝️ Looks like Terminal is disabled, start Cloud Coammnder with '--terminal' flag.`); + + return; + } +} + +export function hide() {} diff --git a/client/modules/config/index.js b/client/modules/config/index.js index 389639293c..dfa4c10547 100644 --- a/client/modules/config/index.js +++ b/client/modules/config/index.js @@ -1,28 +1,23 @@ -'use strict'; +import '../../../css/config.css'; +import {rendy} from 'rendy'; +import currify from 'currify'; +import wraptile from 'wraptile'; +import squad from 'squad'; +import {promisify} from 'es6-promisify'; +import {tryToCatch} from 'try-to-catch'; +import load from 'load.js'; +import createElement from '@cloudcmd/create-element'; +import * as Events from '#dom/events'; +import * as Files from '#dom/files'; +import {getTitle} from '#common/cloudfunc'; +import * as Images from '#dom/images'; +import * as input from './input.js'; + +const {CloudCmd, DOM} = globalThis; -/* global CloudCmd, DOM, io */ - -require('../../../css/config.css'); - -const rendy = require('rendy'); -const currify = require('currify'); -const wraptile = require('wraptile'); -const squad = require('squad'); -const {promisify} = require('es6-promisify'); -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 Files = require('../../dom/files'); - -const {getTitle} = require('../../../common/cloudfunc'); const {Dialog, setTitle} = DOM; const Name = 'Config'; -CloudCmd[Name] = module.exports; const loadSocket = promisify(DOM.loadSocket); @@ -46,28 +41,25 @@ let Template; const loadCSS = load.css; -module.exports.init = async () => { +export async function init() { if (!CloudCmd.config('configDialog')) return; 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(), ]); initSocket(); -}; +} -const { - config, - Key, -} = CloudCmd; +const {config, Key} = CloudCmd; let Element; @@ -77,24 +69,20 @@ function getHost() { origin, protocol, } = location; - const href = origin || `${protocol}//${host}`; - return href; + return origin || `${protocol}//${host}`; } function initSocket() { const href = getHost(); - const { - prefixSocket, - prefix, - } = CloudCmd; + const {prefixSocket, prefix} = CloudCmd; const ONE_MINUTE = 60 * 1000; - const socket = io.connect(href + prefixSocket + '/config', { + const socket = globalThis.io.connect(href + prefixSocket + '/config', { reconnectionAttempts: Infinity, reconnectionDelay: ONE_MINUTE, - path: prefix + '/socket.io', + path: `${prefix}/socket.io`, }); const save = (data) => { @@ -125,9 +113,7 @@ function authCheck(socket) { Config.save = saveHttp; -module.exports.show = show; - -async function show() { +export async function show() { if (!CloudCmd.config('configDialog')) return; @@ -142,21 +128,27 @@ async function fillTemplate() { const { editor, + menu, packer, columns, + theme, configAuth, + configPort, ...obj } = input.convert(config); - obj[editor + '-selected'] = 'selected'; - obj[packer + '-selected'] = 'selected'; - obj[columns + '-selected'] = 'selected'; + obj[`${menu}-selected`] = 'selected'; + obj[`${editor}-selected`] = 'selected'; + obj[`${packer}-selected`] = 'selected'; + obj[`${columns}-selected`] = 'selected'; + obj[`${theme}-selected`] = 'selected'; obj.configAuth = configAuth ? '' : 'hidden'; + obj.configPort = configPort ? '' : 'hidden'; const innerHTML = rendy(Template, obj); Element = createElement('form', { - className : 'config', + className: 'config', innerHTML, }); @@ -173,20 +165,20 @@ async function fillTemplate() { const getTarget = ({target}) => target; const handleChange = squad(onChange, getTarget); - Array.from(inputs) + Array + .from(inputs) .map(addKey(onKey)) .map(addChange(handleChange)); const autoSize = true; + CloudCmd.View.show(Element, { autoSize, afterShow, }); } -module.exports.hide = hide; - -function hide() { +export function hide() { CloudCmd.View.hide(); } @@ -228,7 +220,7 @@ function onAuthChange(checked) { const elUsername = input.getElementByName('username', Element); const elPassword = input.getElementByName('password', Element); - elUsername.disabled = + elUsername.disabled = !checked; elPassword.disabled = !checked; } @@ -248,3 +240,8 @@ async function onKey({keyCode, target}) { } } +CloudCmd[Name] = { + init, + show, + hide, +}; diff --git a/client/modules/config/input.js b/client/modules/config/input.js index 3929154fb5..34a4be6991 100644 --- a/client/modules/config/input.js +++ b/client/modules/config/input.js @@ -1,21 +1,17 @@ -'use strict'; +import {encode} from '#common/entity'; -const currify = require('currify'); +const isBool = (a) => typeof a === 'boolean'; +const isString = (a) => typeof a === 'string'; -const isType = currify((type, object, name) => typeof object[name] === type); +const {keys} = Object; -const isBool = isType('boolean'); - -module.exports.getElementByName = getElementByName; - -function getElementByName(selector, element) { +export function getElementByName(selector, element) { const str = `[data-name="js-${selector}"]`; - return element - .querySelector(str); + return element.querySelector(str); } -module.exports.getName = (element) => { +export const getName = (element) => { const name = element .getAttribute('data-name') .replace(/^js-/, ''); @@ -23,15 +19,21 @@ module.exports.getName = (element) => { return name; }; -module.exports.convert = (config) => { +export const convert = (config) => { const result = config; - const array = Object.keys(config); - const filtered = array.filter(isBool(config)); - - for (const name of filtered) { + for (const name of keys(config)) { const item = config[name]; - result[name] = setState(item); + + if (isBool(item)) { + result[name] = setState(item); + continue; + } + + if (isString(item)) { + result[name] = encode(item); + continue; + } } return result; @@ -44,7 +46,7 @@ function setState(state) { return ''; } -module.exports.getValue = (name, element) => { +export const getValue = (name, element) => { const el = getElementByName(name, element); const {type} = el; @@ -60,7 +62,7 @@ module.exports.getValue = (name, element) => { } }; -module.exports.setValue = (name, value, element) => { +export const setValue = (name, value, element) => { const el = getElementByName(name, element); const {type} = el; @@ -74,4 +76,3 @@ module.exports.setValue = (name, value, element) => { break; } }; - diff --git a/client/modules/config/input.spec.js b/client/modules/config/input.spec.js new file mode 100644 index 0000000000..537c163238 --- /dev/null +++ b/client/modules/config/input.spec.js @@ -0,0 +1,15 @@ +import {test} from 'supertape'; +import {convert} from './input.js'; + +test('cloudcmd: client: config: input: convert', (t) => { + const result = convert({ + name: 'hello ', + }); + + const expected = { + name: 'hello <world>', + }; + + t.deepEqual(result, expected); + t.end(); +}); diff --git a/client/modules/contact.js b/client/modules/contact.js index abcfd003c1..48c7bd1950 100644 --- a/client/modules/contact.js +++ b/client/modules/contact.js @@ -1,36 +1,34 @@ /* global CloudCmd */ /* global DOM */ +import olark from '@cloudcmd/olark'; +import * as Images from '#dom/images'; -'use strict'; - -CloudCmd.Contact = exports; - -const olark = require('@cloudcmd/olark'); -const Images = require('../dom/images'); +CloudCmd.Contact = { + init, + show, + hide, +}; const {Events} = DOM; const {Key} = CloudCmd; -module.exports.show = show; -module.exports.hide = hide; - -module.exports.init = () => { +export function init() { Events.addKey(onKey); olark.identify('6216-545-10-4223'); olark('api.box.onExpand', show); olark('api.box.onShow', show); olark('api.box.onShrink', hide); -}; +} -function show() { +export function show() { Key.unsetBind(); Images.hide(); olark('api.box.expand'); } -function hide() { +export function hide() { Key.setBind(); olark('api.box.hide'); } @@ -39,4 +37,3 @@ function onKey({keyCode}) { if (keyCode === Key.ESC) hide(); } - diff --git a/client/modules/edit-file-vim.js b/client/modules/edit-file-vim.js index c9b9c8911e..16d557ae70 100644 --- a/client/modules/edit-file-vim.js +++ b/client/modules/edit-file-vim.js @@ -1,10 +1,12 @@ -'use strict'; +import * as Events from '#dom/events'; -/* global CloudCmd */ +const {CloudCmd} = globalThis; -CloudCmd.EditFileVim = exports; - -const Events = require('../dom/events'); +CloudCmd.EditFileVim = { + init, + show, + hide, +}; const {Key} = CloudCmd; @@ -16,36 +18,29 @@ const ConfigView = { }, }; -module.exports.init = async () => { +export async function init() { await CloudCmd.EditFile(); -}; +} -module.exports.show = async () => { +export async function show() { Events.addKey(listener); - const editFile = await CloudCmd.EditFile - .show(ConfigView); + const editFile = await CloudCmd.EditFile.show(ConfigView); editFile .getEditor() .setKeyMap('vim'); -}; - -module.exports.hide = hide; +} -function hide() { +export function hide() { CloudCmd.Edit.hide(); } function listener(event) { - const { - keyCode, - shiftKey, - } = event; + const {keyCode, shiftKey} = event; if (shiftKey && keyCode === Key.ESC) { event.preventDefault(); hide(); } } - diff --git a/client/modules/edit-file.js b/client/modules/edit-file.js index b101ddbb4f..83a07e366d 100644 --- a/client/modules/edit-file.js +++ b/client/modules/edit-file.js @@ -1,20 +1,19 @@ -'use strict'; - /* global CloudCmd, DOM*/ - -CloudCmd.EditFile = exports; - -const Format = require('format-io'); -const fullstore = require('fullstore'); -const exec = require('execon'); -const supermenu = require('supermenu'); +import Format from 'format-io'; +import {fullstore} from 'fullstore'; +import exec from 'execon'; +import {supermenu} from 'supermenu'; + +CloudCmd.EditFile = { + init, + show, + hide, + isChanged, +}; const Info = DOM.CurrentInfo; -const { - Dialog, - Images, -} = DOM; +const {Dialog, Images} = DOM; const {config} = CloudCmd; @@ -30,7 +29,7 @@ const ConfigView = { }, }; -module.exports.init = async () => { +export async function init() { isLoading(true); await CloudCmd.Edit(); @@ -40,7 +39,7 @@ module.exports.init = async () => { setListeners(editor); isLoading(false); -}; +} function getName() { const {name, isDir} = Info; @@ -51,7 +50,7 @@ function getName() { return name; } -module.exports.show = async (options) => { +export async function show(options) { if (isLoading()) return; @@ -78,6 +77,7 @@ module.exports.show = async (options) => { const {path} = Info; const name = getName(); + setMsgChanged(name); CloudCmd.Edit @@ -89,11 +89,9 @@ module.exports.show = async (options) => { CloudCmd.Edit.show(optionsEdit); return CloudCmd.Edit; -}; - -module.exports.hide = hide; +} -function hide() { +export function hide() { CloudCmd.Edit.hide(); } @@ -150,38 +148,36 @@ function getMenuData() { const editor = CloudCmd.Edit.getEditor(); return { - 'Save Ctrl+S' : () => { + 'Save Ctrl+S': () => { editor.save(); }, - 'Go To Line Ctrl+G' : () => { + 'Go To Line Ctrl+G': () => { editor.goToLine(); }, - 'Cut Ctrl+X' : () => { + 'Cut Ctrl+X': () => { editor.cutToClipboard(); }, - 'Copy Ctrl+C' : () => { + 'Copy Ctrl+C': () => { editor.copyToClipboard(); }, - 'Paste Ctrl+V' : () => { + 'Paste Ctrl+V': () => { editor.pasteFromClipboard(); }, - 'Delete Del' : () => { + 'Delete Del': () => { editor.remove('right'); }, - 'Select All Ctrl+A' : () => { + 'Select All Ctrl+A': () => { editor.selectAll(); }, - 'Close Esc' : hide, + 'Close Esc': hide, }; } function setMsgChanged(name) { - MSG_CHANGED = 'Do you want to save changes to ' + name + '?'; + MSG_CHANGED = `Do you want to save changes to ${name}?`; } -module.exports.isChanged = isChanged; - -async function isChanged() { +export async function isChanged() { const editor = CloudCmd.Edit.getEditor(); const is = editor.isChanged(); @@ -195,4 +191,3 @@ async function isChanged() { editor.save(); } - diff --git a/client/modules/edit-names-vim.js b/client/modules/edit-names-vim.js index 6608729446..8b3b2f8943 100644 --- a/client/modules/edit-names-vim.js +++ b/client/modules/edit-names-vim.js @@ -1,10 +1,12 @@ -'use strict'; +import * as Events from '#dom/events'; -/* global CloudCmd */ +const {CloudCmd} = globalThis; -CloudCmd.EditNamesVim = exports; - -const Events = require('../dom/events'); +CloudCmd.EditNamesVim = { + init, + show, + hide, +}; const {Key} = CloudCmd; const ConfigView = { @@ -15,34 +17,28 @@ const ConfigView = { }, }; -module.exports.init = async () => { +export async function init() { await CloudCmd.EditNames(); -}; +} -module.exports.show = () => { +export function show() { Events.addKey(listener); CloudCmd.EditNames .show(ConfigView) .getEditor() .setKeyMap('vim'); -}; - -module.exports.hide = hide; +} -function hide() { +export function hide() { CloudCmd.Edit.hide(); } function listener(event) { - const { - keyCode, - shiftKey, - } = event; + const {keyCode, shiftKey} = event; if (shiftKey && keyCode === Key.ESC) { event.preventDefault(); hide(); } } - diff --git a/client/modules/edit-names.js b/client/modules/edit-names.js index ed71e90e3d..7bc29054f8 100644 --- a/client/modules/edit-names.js +++ b/client/modules/edit-names.js @@ -1,22 +1,20 @@ -'use strict'; - -/* 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); +import {tryToCatch} from 'try-to-catch'; +import exec from 'execon'; +import {supermenu} from 'supermenu'; +import {multiRename} from 'multi-rename'; + +const {CloudCmd, DOM} = globalThis; + +CloudCmd.EditNames = { + init, + show, + hide, + isChanged, +}; const Info = DOM.CurrentInfo; const {Dialog} = DOM; -const refresh = currify(_refresh); -const rename = currify(_rename); - let Menu; const ConfigView = { @@ -27,13 +25,13 @@ const ConfigView = { }, }; -module.exports.init = async () => { +export async function init() { await CloudCmd.Edit(); setListeners(); -}; +} -module.exports.show = (options) => { +export function show(options) { const names = getActiveNames().join('\n'); const config = { ...ConfigView, @@ -55,7 +53,7 @@ module.exports.show = (options) => { CloudCmd.Edit.show(config); return CloudCmd.Edit; -}; +} async function keyListener(event) { const ctrl = event.ctrlKey; @@ -63,15 +61,15 @@ async function keyListener(event) { const ctrlMeta = ctrl || meta; const {Key} = CloudCmd; - if (ctrlMeta && event.keyCode === Key.S) + if (ctrlMeta && event.keyCode === Key.S) { hide(); + return; + } - else if (ctrlMeta && event.keyCode === Key.P) { + if (ctrlMeta && event.keyCode === Key.P) { const [, pattern] = await Dialog.prompt('Apply pattern:', '[n][e]'); pattern && applyPattern(pattern); } - - event.preventDefault(); } function applyPattern(pattern) { @@ -85,9 +83,7 @@ function getActiveNames() { return DOM.getFilenames(DOM.getActiveFiles()); } -module.exports.hide = hide; - -function hide() { +export function hide() { CloudCmd.Edit.hide(); } @@ -97,7 +93,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); @@ -109,15 +105,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 +132,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; @@ -160,6 +159,7 @@ function setMenu(event) { return; const editor = CloudCmd.Edit.getEditor(); + const options = { beforeShow: (params) => { params.x -= 18; @@ -172,29 +172,29 @@ function setMenu(event) { }; const menuData = { - 'Save Ctrl+S' : () => { - applyNames(); + 'Save Ctrl+S': async () => { + await applyNames(); hide(); }, - 'Go To Line Ctrl+G' : () => { + 'Go To Line Ctrl+G': () => { editor.goToLine(); }, - 'Cut Ctrl+X' : () => { + 'Cut Ctrl+X': () => { editor.cutToClipboard(); }, - 'Copy Ctrl+C' : () => { + 'Copy Ctrl+C': () => { editor.copyToClipboard(); }, - 'Paste Ctrl+V' : () => { + 'Paste Ctrl+V': () => { editor.pasteFromClipboard(); }, - 'Delete Del' : () => { + 'Delete Del': () => { editor.remove('right'); }, - 'Select All Ctrl+A' : () => { + 'Select All Ctrl+A': () => { editor.selectAll(); }, - 'Close Esc' : hide, + 'Close Esc': hide, }; const element = CloudCmd.Edit.getElement(); @@ -205,16 +205,14 @@ function setMenu(event) { Menu.show(position.x, position.y); } -module.exports.isChanged = isChanged; - -async function isChanged() { +export async function isChanged() { const editor = CloudCmd.Edit.getEditor(); const msg = 'Apply new names?'; 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 5676034c30..593a11cb8f 100644 --- a/client/modules/edit.js +++ b/client/modules/edit.js @@ -1,19 +1,28 @@ /* global CloudCmd */ +import {montag} from 'montag'; +import {promisify} from 'es6-promisify'; +import {tryToCatch} from 'try-to-catch'; +import createElement from '@cloudcmd/create-element'; +import load from 'load.js'; +import {MAX_FILE_SIZE as maxSize} from '#common/cloudfunc'; +import {time, timeEnd} from '#common/util'; + +export function getEditor() { + return editor; +} -'use strict'; - -const {promisify} = require('es6-promisify'); -const tryToCatch = require('try-to-catch'); -const createElement = require('@cloudcmd/create-element'); -const load = require('load.js'); +const isFn = (a) => typeof a === 'function'; const loadJS = load.js; -const {MAX_FILE_SIZE: maxSize} = require('../../common/cloudfunc'); -const {time, timeEnd} = require('../../common/util'); - const Name = 'Edit'; -CloudCmd[Name] = exports; +CloudCmd[Name] = { + init, + show, + hide, + getEditor, + getElement, +}; const EditorName = CloudCmd.config('editor'); @@ -29,19 +38,20 @@ const ConfigView = { }, }; -module.exports.init = async () => { +export async function init() { const element = create(); await CloudCmd.View(); await loadFiles(element); -}; +} function create() { const element = createElement('div', { - style: - 'width : 100%;' + - 'height : 100%;' + - 'font-family: "Droid Sans Mono";', + style: montag` + width: 100%; + height: 100%; + font-family: "Droid Sans Mono"; + `, notAppend: true, }); @@ -51,8 +61,8 @@ function create() { } function checkFn(name, fn) { - if (typeof fn !== 'function') - throw Error(name + ' should be a function!'); + if (!isFn(fn)) + throw Error(`${name} should be a function!`); } function initConfig(options = {}) { @@ -66,41 +76,32 @@ function initConfig(options = {}) { checkFn('options.afterShow', options.afterShow); - const afterShow = { - config, - }; - config.afterShow = () => { - afterShow(); + ConfigView.afterShow(); options.afterShow(); }; return config; } -module.exports.show = (options) => { +export function show(options) { if (Loading) return; CloudCmd.View.show(Element, initConfig(options)); - getEditor() - .setOptions({ - fontSize: 16, - }); -}; - -module.exports.getEditor = getEditor; - -function getEditor() { - return editor; + getEditor().setOptions({ + fontSize: 16, + }); } -module.exports.getElement = () => Element; +export function getElement() { + return Element; +} -module.exports.hide = () => { +export function hide() { CloudCmd.View.hide(); -}; +} const loadFiles = async (element) => { const prefix = `${CloudCmd.prefix}/${EditorName}`; @@ -108,7 +109,7 @@ const loadFiles = async (element) => { const prefixSocket = `${CloudCmd.prefixSocket}/${EditorName}`; const url = `${prefix}/${EditorName}.js`; - time(Name + ' load'); + time(`${Name} load`); await loadJS(url); @@ -120,8 +121,7 @@ const loadFiles = async (element) => { socketPath, }); - timeEnd(Name + ' load'); + timeEnd(`${Name} load`); editor = ed; Loading = false; }; - diff --git a/client/modules/help.js b/client/modules/help.js index 5e85911582..74657a900c 100644 --- a/client/modules/help.js +++ b/client/modules/help.js @@ -1,31 +1,27 @@ -'use strict'; +import * as Images from '#dom/images'; -/* global CloudCmd */ +const {CloudCmd} = globalThis; -CloudCmd.Help = exports; - -const Images = require('../dom/images'); - -module.exports.init = () => { - Images.show.load('top'); +CloudCmd.Help = { + init, + show, + hide, }; -module.exports.show = show; -module.exports.hide = hide; +export function init() { + Images.show.load('top'); +} -function show() { +export function show() { const positionLoad = 'top'; const relative = true; - CloudCmd - .Markdown - .show('/HELP.md', { - positionLoad, - relative, - }); + CloudCmd.Markdown.show('/HELP.md', { + positionLoad, + relative, + }); } -function hide() { +export function hide() { CloudCmd.View.hide(); } - diff --git a/client/modules/konsole.js b/client/modules/konsole.js index df143e8d6f..e9a465af9d 100644 --- a/client/modules/konsole.js +++ b/client/modules/konsole.js @@ -1,23 +1,21 @@ -'use strict'; - /* global CloudCmd */ /* global Util */ /* global DOM */ /* global Console */ +import exec from 'execon'; +import currify from 'currify'; +import {tryToCatch} from 'try-to-catch'; +import {js as loadJS} from 'load.js'; +import createElement from '@cloudcmd/create-element'; +import * as Images from '#dom/images'; + +CloudCmd.Konsole = { + init, + show, + hide, +}; -CloudCmd.Konsole = exports; - -const exec = require('execon'); -const currify = require('currify'); -const tryToCatch = require('try-to-catch'); -const loadJS = require('load.js').js; -const createElement = require('@cloudcmd/create-element'); - -const Images = require('../dom/images'); -const { - Dialog, - CurrentInfo:Info, -} = DOM; +const {Dialog, CurrentInfo: Info} = DOM; const rmLastSlash = (a) => a.replace(/\/$/, '') || '/'; @@ -31,7 +29,7 @@ const Name = 'Konsole'; let Element; let Loaded; -module.exports.init = async () => { +export async function init() { if (!config('console')) return; @@ -40,40 +38,34 @@ module.exports.init = async () => { await CloudCmd.View(); await load(); await create(); -}; +} -module.exports.hide = () => { +export function hide() { CloudCmd.View.hide(); -}; +} -module.exports.clear = () => { +export const 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) return; - await CloudCmd.loadDir({ - path, - }); + await CloudCmd.changeDir(path); } const getDirPath = () => { @@ -119,7 +111,7 @@ function authCheck(konsole) { }); } -module.exports.show = (callback) => { +export function show(callback) { if (!Loaded) return; @@ -132,21 +124,20 @@ module.exports.show = (callback) => { exec(callback); }, }); -}; +} const load = async () => { - Util.time(Name + ' load'); + Util.time(`${Name} load`); const prefix = getPrefix(); - const url = prefix + '/console.js'; + const url = `${prefix}/console.js`; const [error] = await tryToCatch(loadJS, url); Loaded = true; - Util.timeEnd(Name + ' load'); + Util.timeEnd(`${Name} load`); if (error) return Dialog.alert(error.message, { cancel: false, }); }; - diff --git a/client/modules/markdown.js b/client/modules/markdown.js index e5810300ba..c94c64e869 100644 --- a/client/modules/markdown.js +++ b/client/modules/markdown.js @@ -1,37 +1,32 @@ -'use strict'; +import createElement from '@cloudcmd/create-element'; +import * as Images from '#dom/images'; +import {Markdown} from '#dom/rest'; +import {alert} from '#dom/dialog'; -/* global CloudCmd */ +const {CloudCmd} = globalThis; -CloudCmd.Markdown = exports; - -const createElement = require('@cloudcmd/create-element'); - -const Images = require('../dom/images'); -const {Markdown} = require('../dom/rest'); -const {alert} = require('../dom/dialog'); +CloudCmd.Markdown = { + init, + show, + hide, +}; -module.exports.init = async () => { +export async function init() { Images.show.load('top'); await CloudCmd.View(); -}; - -module.exports.show = show; +} -module.exports.hide = () => { +export function hide() { CloudCmd.View.hide(); -}; +} -async function show(name, options = {}) { - const relativeQuery = '?relative'; - const { - positionLoad, - relative, - } = options; +export async function show(name, options = {}) { + const {positionLoad, relative} = options; Images.show.load(positionLoad); if (relative) - name += relativeQuery; + name += '?relative'; const [error, innerHTML] = await Markdown.read(name); Images.hide(); @@ -42,6 +37,7 @@ async function show(name, options = {}) { }); const className = 'help'; + const div = createElement('div', { className, innerHTML, @@ -49,4 +45,3 @@ async function show(name, options = {}) { CloudCmd.View.show(div); } - diff --git a/client/modules/menu/cloudmenu.js b/client/modules/menu/cloudmenu.js new file mode 100644 index 0000000000..8f7c1e68de --- /dev/null +++ b/client/modules/menu/cloudmenu.js @@ -0,0 +1,31 @@ +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 {prefix} = CloudCmd; + const {host, protocol} = globalThis.location; + const url = `${protocol}//${host}${prefix}/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 76% rename from client/modules/menu.js rename to client/modules/menu/index.js index 4ecc17cba3..7235591e6a 100644 --- a/client/modules/menu.js +++ b/client/modules/menu/index.js @@ -1,20 +1,12 @@ -/* global CloudCmd, DOM */ +import exec from 'execon'; +import wrap from 'wraptile'; +import createElement from '@cloudcmd/create-element'; +import {getIdBySrc} from '#dom/load'; +import * as RESTful from '#dom/rest'; +import {FS} from '#common/cloudfunc'; -'use strict'; - -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 { - config, - Key, -} = CloudCmd; +const {CloudCmd, DOM} = globalThis; +const {config, Key} = CloudCmd; const { Buffer, @@ -31,45 +23,54 @@ let MenuShowedName; let MenuContext; let MenuContextFile; -module.exports.ENABLED = false; +export const ENABLED = false; -CloudCmd.Menu = exports; +CloudCmd.Menu = { + init, + show, + hide, +}; -module.exports.init = () => { - const { - isAuth, - menuDataFile, - } = getFileMenuData(); +export async function init() { + const {isAuth, menuDataFile} = getFileMenuData(); const fm = DOM.getFM(); const menuData = getMenuData(isAuth); - const options = getOptions({type: 'context'}); - const optionsFile = getOptions({type: 'file'}); - MenuContext = supermenu(fm, options, menuData); - MenuContextFile = supermenu(fm, optionsFile, menuDataFile); + const options = getOptions({ + type: 'context', + }); + + const optionsFile = getOptions({ + type: 'file', + }); + + const {createCloudMenu} = await import('./cloudmenu.js'); + + const {name} = fm.dataset; + + MenuContext = await createCloudMenu(name, options, menuData); + MenuContextFile = await createCloudMenu(name, optionsFile, menuDataFile); MenuContext.addContextMenuListener(); MenuContextFile.addContextMenuListener(); Events.addKey(listener); -}; - -module.exports.hide = hide; +} -function hide() { +export function hide() { MenuContext.hide(); MenuContextFile.hide(); } -module.exports.show = (position) => { +export function show(position) { const {x, y} = getPosition(position); MenuContext.show(x, y); MenuContextFile.show(x, y); Images.hide(); -}; +} function getPosition(position) { if (position) @@ -105,9 +106,11 @@ function getOptions({type}) { } const options = { - icon : true, - beforeClose : Key.setBind, - beforeShow : exec.with(beforeShow, func), + icon: true, + infiniteScroll: false, + beforeClose: Key.setBind, + beforeHide: Key.setBind, + beforeShow: exec.with(beforeShow, func), beforeClick, name, }; @@ -126,6 +129,7 @@ function getMenuData(isAuth) { CloudCmd.Upload.show(); }, 'Upload From Cloud': uploadFromCloud, + 'Toggle File Selection': DOM.toggleSelectedFile, '(Un)Select All': DOM.toggleAllSelectedFiles, }; @@ -196,16 +200,26 @@ function isPath(x, y) { const el = document.elementFromPoint(x, y); const elements = panel.querySelectorAll('[data-name="js-path"] *'); - const is = ~[].indexOf.call(elements, el); - return is; + 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); @@ -221,14 +235,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(); @@ -239,7 +251,7 @@ async function _uploadTo(nameModule) { const {name} = Info; CloudCmd.execFromModule(nameModule, 'uploadFile', name, data); - CloudCmd.log('Uploading to ' + name + '...'); + CloudCmd.log(`Uploading to ${name}...`); } function uploadFromCloud() { @@ -252,7 +264,9 @@ function uploadFromCloud() { if (e) return; - await CloudCmd.refresh({currentName}); + await CloudCmd.refresh({ + currentName, + }); }); } @@ -275,13 +289,14 @@ function download(type) { const isDir = DOM.isCurrentIsDir(file); const path = DOM.getCurrentPath(file); - CloudCmd.log('downloading file ' + path + '...'); + CloudCmd.log(`downloading file ${path}...`); + /* * if we send ajax request - * no need in hash so we escape # * and all other characters, like "%" */ - const encodedPath = encodeURI(path).replace(/#/g, '%23'); + const encodedPath = encodeURI(path).replace(/#/g, '#'); const id = getIdBySrc(path); let src; @@ -292,7 +307,7 @@ function download(type) { src = prefixURL + FS + encodedPath + '?download'; const element = createElement('iframe', { - id : id + '-' + date, + id: id + '-' + date, async: false, className: 'hidden', src, @@ -321,21 +336,17 @@ function getCurrentPosition() { } function listener(event) { - const { - F9, - ESC, - } = Key; + const {F9, ESC} = Key; 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/format.js b/client/modules/operation/format.js index 373d069c87..c639799e70 100644 --- a/client/modules/operation/format.js +++ b/client/modules/operation/format.js @@ -1,9 +1,6 @@ -'use strict'; - -module.exports = (operation, from, to) => { +export const format = (operation, from, to) => { if (!to) return `${operation} ${from}`; return `${operation} ${from} -> ${to}`; }; - diff --git a/client/modules/operation/get-next-current-name.js b/client/modules/operation/get-next-current-name.js index d2b96545e0..6344588803 100644 --- a/client/modules/operation/get-next-current-name.js +++ b/client/modules/operation/get-next-current-name.js @@ -1,11 +1,9 @@ -'use strict'; - -const currify = require('currify'); +import currify from 'currify'; const not = currify((array, value) => !array.includes(value)); const notOneOf = currify((a, b) => a.filter(not(b))); -module.exports = (currentName, names, removedNames) => { +export const getNextCurrentName = (currentName, names, removedNames) => { const i = names.indexOf(currentName); const nextNames = notOneOf(names, removedNames); @@ -16,4 +14,3 @@ module.exports = (currentName, names, removedNames) => { return nextNames[length - 1]; }; - diff --git a/client/modules/operation/index.js b/client/modules/operation/index.js index c58e629d5c..f349f7d8e3 100644 --- a/client/modules/operation/index.js +++ b/client/modules/operation/index.js @@ -1,26 +1,19 @@ -/* global CloudCmd */ -/* global Util */ -/* global DOM */ -/* global fileop */ - -'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'); +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'; +import {removeExtension} from './remove-extension.js'; +import {setListeners} from './set-listeners.js'; +import {getNextCurrentName} from './get-next-current-name.js'; + +const {DOM, CloudCmd} = globalThis; const removeQuery = (a) => a.replace(/\?.*/, ''); const Name = 'Operation'; -CloudCmd[Name] = exports; const {config} = CloudCmd; const {Dialog, Images} = DOM; @@ -52,7 +45,7 @@ const noFilesCheck = () => { return is; }; -module.exports.init = promisify((callback) => { +export const init = promisify((callback) => { showLoad(); exec.series([ @@ -61,10 +54,7 @@ module.exports.init = promisify((callback) => { if (config('dropbox')) return callback(); - const { - prefix, - prefixSocket, - } = CloudCmd; + const {prefix, prefixSocket} = CloudCmd; await loadAll(); await initOperations(prefix, prefixSocket, callback); @@ -94,7 +84,11 @@ const onConnect = currify((fn, operator) => { async function initOperations(prefix, socketPrefix, fn) { socketPrefix = `${socketPrefix}/fileop`; - const operator = await fileop({prefix, socketPrefix}); + const operator = await globalThis.fileop({ + prefix, + socketPrefix, + }); + operator.on('connect', authCheck(operator, onConnect(fn))); } @@ -109,7 +103,8 @@ function setOperations(operator) { to, }); - operator.tar(from, to, names) + operator + .tar(from, to, names) .then(listen); }; @@ -123,7 +118,8 @@ function setOperations(operator) { to, }); - operator.zip(from, to, names) + operator + .zip(from, to, names) .then(listen); }; @@ -137,7 +133,8 @@ function setOperations(operator) { from, }); - operator.remove(from, files) + operator + .remove(from, files) .then(listen); }; @@ -151,7 +148,8 @@ function setOperations(operator) { names, }); - operator.copy(from, to, names) + operator + .copy(from, to, names) .then(listen); }; @@ -164,7 +162,8 @@ function setOperations(operator) { to, }); - operator.move(from, to, names) + operator + .move(from, to, names) .then(listen); }; @@ -178,7 +177,8 @@ function setOperations(operator) { to, }); - operator.extract(from, to) + operator + .extract(from, to) .then(listen); }; } @@ -190,11 +190,11 @@ function getPacker(type) { return packTarFn; } -module.exports.hide = () => { +export function hide() { CloudCmd.View.hide(); -}; +} -module.exports.show = (operation, data) => { +export function show(operation, data) { if (!Loaded) return; @@ -215,7 +215,7 @@ module.exports.show = (operation, data) => { if (operation === 'extract') return Operation.extract(); -}; +} Operation.copy = processFiles({ type: 'copy', @@ -274,6 +274,7 @@ async function promptDelete() { const type = getType(isDir) + ' '; const name = DOM.getCurrentName(current); + msg = msgAsk + msgSel + type + name + '?'; } @@ -333,20 +334,22 @@ async function _processFiles(options, data) { let names = []; - /* eslint no-multi-spaces: 0 */ 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(); - selFiles = DOM.getSelectedFiles(); - names = DOM.getFilenames(selFiles); - data = {}; - shouldAsk = true; - panel = Info.panelPassive; + from = Info.dirPath; + to = DOM.getNotCurrentDirPath(); + selFiles = DOM.getSelectedFiles(); + names = DOM.getFilenames(selFiles); + data = {}; + shouldAsk = true; + panel = Info.panelPassive; } if (!names.length) @@ -383,10 +386,14 @@ async function _processFiles(options, data) { if (ok && !shouldAsk || !sameName) return go(); - const str = `"${ name }" already exist. Overwrite?`; + const str = `"${name}" already exist. Overwrite?`; const cancel = false; - Dialog.confirm(str, {cancel}).then(go); + Dialog + .confirm(str, { + cancel, + }) + .then(go); function go() { showLoad(); @@ -400,15 +407,7 @@ async function _processFiles(options, data) { operation(files, async () => { await DOM.Storage.remove(from); - const { - panel, - panelPassive, - } = Info; - - const setCurrent = () => { - const currentName = name || data.names[0]; - DOM.setCurrentByName(currentName); - }; + const {panel, panelPassive} = Info; if (!Info.isOnePanel) CloudCmd.refresh({ @@ -416,7 +415,9 @@ async function _processFiles(options, data) { noCurrent: true, }); - CloudCmd.refresh({panel}, setCurrent); + CloudCmd.refresh({ + panel, + }); }); } } @@ -424,7 +425,7 @@ async function _processFiles(options, data) { function checkEmpty(name, operation) { if (!operation) - throw Error(name + ' could not be empty!'); + throw Error(`${name} could not be empty!`); } function twopack(operation, type) { @@ -432,10 +433,7 @@ function twopack(operation, type) { let fileFrom; let currentName = Info.name; - const { - path, - dirPath, - } = Info; + const {path, dirPath} = Info; const activeFiles = DOM.getActiveFiles(); const names = DOM.getFilenames(activeFiles); @@ -449,7 +447,7 @@ function twopack(operation, type) { case 'extract': op = extractFn; - fileFrom = { + fileFrom = { from: path, to: dirPath, }; @@ -462,7 +460,7 @@ function twopack(operation, type) { op = getPacker(type); if (names.length > 1) - currentName = Info.dir; + currentName = Info.dir; currentName += DOM.getPackerExt(type); @@ -490,17 +488,23 @@ async function prompt(msg, to, names) { msg += ' '; if (names.length > 1) - msg += n + ' file(s)'; + msg += `${n} file(s)`; else - msg += '"' + name + '"'; + msg += `"${name}"`; msg += ' to'; 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); @@ -510,4 +514,3 @@ async function loadAll() { Loaded = true; } - diff --git a/client/modules/operation/remove-extension.js b/client/modules/operation/remove-extension.js index 52772c305b..e6ce2c4698 100644 --- a/client/modules/operation/remove-extension.js +++ b/client/modules/operation/remove-extension.js @@ -1,20 +1,17 @@ -'use strict'; +import {getExt} from '#common/util'; -const {getExt} = require('../../../common/util'); - -module.exports = (name) => { +export const removeExtension = (name) => { const ext = getExtension(name); return name.replace(ext, ''); }; 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/remove-extension.spec.js b/client/modules/operation/remove-extension.spec.js index 0cf0aaccf3..fa11e9e27b 100644 --- a/client/modules/operation/remove-extension.spec.js +++ b/client/modules/operation/remove-extension.spec.js @@ -1,7 +1,5 @@ -'use strict'; - -const test = require('supertape'); -const removeExtension = require(`./remove-extension`); +import test from 'supertape'; +import {removeExtension} from './remove-extension.js'; test('cloudcmd: client: modules: operation: removeExtension: .tar.gz', (t) => { const name = 'hello'; @@ -26,4 +24,3 @@ test('cloudcmd: client: modules: operation: removeExtension: .bz2', (t) => { t.equal(removeExtension(fullName), name, 'should remove .bz2'); t.end(); }); - diff --git a/client/modules/operation/set-listeners.js b/client/modules/operation/set-listeners.js index df83d243fc..e54e7d140d 100644 --- a/client/modules/operation/set-listeners.js +++ b/client/modules/operation/set-listeners.js @@ -1,18 +1,11 @@ -'use strict'; - /* global DOM */ +import forEachKey from 'for-each-key'; +import wraptile from 'wraptile'; +import {format} from './format.js'; -const { - Dialog, - Images, -} = DOM; - -const forEachKey = require('for-each-key'); -const wraptile = require('wraptile'); - -const format = require('./format'); +const {Dialog, Images} = DOM; -module.exports = (options) => (emitter) => { +export const setListeners = (options) => (emitter) => { const { operation, callback, @@ -22,13 +15,11 @@ module.exports = (options) => (emitter) => { } = options; let done; - let lastError; const onAbort = wraptile(({emitter, operation}) => { emitter.abort(); const msg = `${operation} aborted`; - lastError = true; Dialog.alert(msg, { cancel: false, @@ -57,21 +48,20 @@ module.exports = (options) => (emitter) => { forEachKey(removeListener, listeners); progress.remove(); - if (lastError || done) - callback(); + callback(); }, error: async (error) => { - lastError = error; - if (noContinue) { listeners.end(error); Dialog.alert(error); progress.remove(); + return; } - const [cancel] = await Dialog.confirm(error + '\n Continue?'); + const [cancel] = await Dialog.confirm(`${error} + Continue?`); if (!done && !cancel) return emitter.continue(); @@ -83,4 +73,3 @@ module.exports = (options) => (emitter) => { forEachKey(on, listeners); }; - diff --git a/client/modules/polyfill.js b/client/modules/polyfill.js index e4fe9fcf87..90b6268983 100644 --- a/client/modules/polyfill.js +++ b/client/modules/polyfill.js @@ -1,11 +1,15 @@ -'use strict'; +import _scrollIntoViewIfNeeded from 'scroll-into-view-if-needed'; -/* global DOM */ +globalThis.DOM = globalThis.DOM || {}; -require('domtokenlist-shim'); - -const scrollIntoViewIfNeeded = require('scroll-into-view-if-needed').default; -DOM.scrollIntoViewIfNeeded = (el) => scrollIntoViewIfNeeded(el, { - block: 'nearest', -}); +export const scrollIntoViewIfNeeded = (el, overrides = {}) => { + const { + scroll = _scrollIntoViewIfNeeded, + } = overrides; + + return scroll(el, { + block: 'nearest', + }); +}; +globalThis.DOM.scrollIntoViewIfNeeded = scrollIntoViewIfNeeded; diff --git a/client/modules/polyfill.spec.js b/client/modules/polyfill.spec.js index 5c31b27fdc..740947eb7a 100644 --- a/client/modules/polyfill.spec.js +++ b/client/modules/polyfill.spec.js @@ -1,35 +1,20 @@ -'use strict'; +import {test, stub} from 'supertape'; +import {scrollIntoViewIfNeeded} from './polyfill.js'; -const test = require('supertape'); -const mockRequire = require('mock-require'); -const stub = require('@cloudcmd/stub'); - -const {stopAll} = mockRequire; - -test('cloudcmd: client: polyfill: scrollIntoViewIfNeaded', (t) => { - const {DOM} = global; +test('cloudcmd: client: polyfill: scrollIntoViewIfNeeded', (t) => { 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(); + }, + ]; - t.calledWith(scroll, args, 'should call scrollIntoViewIfNeaded'); + t.calledWith(scroll, args, 'should call scrollIntoViewIfNeeded'); t.end(); }); diff --git a/client/modules/terminal-run.js b/client/modules/terminal-run.js index cc2df175e2..029415eac1 100644 --- a/client/modules/terminal-run.js +++ b/client/modules/terminal-run.js @@ -1,25 +1,21 @@ -'use strict'; - -/* global CloudCmd, gritty */ - -const {promisify} = require('es6-promisify'); -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'); +import '../../css/terminal.css'; +import {promisify} from 'es6-promisify'; +import {tryToCatch} from 'try-to-catch'; +import {fullstore} from 'fullstore'; +import exec from 'execon'; +import load from 'load.js'; +import DOM from '#dom'; +import * as Images from '#dom/images'; const {Dialog} = DOM; -const { - Key, - config, -} = CloudCmd; +const {CloudCmd} = globalThis; +const {Key, config} = CloudCmd; -CloudCmd.TerminalRun = exports; +CloudCmd.TerminalRun = { + init, + show, + hide, +}; let Loaded; let Terminal; @@ -37,14 +33,14 @@ 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`); } Loaded = true; }; -module.exports.init = async () => { +export async function init() { if (!config('terminal')) return; @@ -52,11 +48,15 @@ module.exports.init = async () => { await CloudCmd.View(); await loadAll(); -}; +} + +export async function show(options = {}) { + return await runTerminal(options); +} -module.exports.show = promisify((options = {}, fn) => { +const runTerminal = promisify((options, fn) => { if (!Loaded) - return; + return fn(null, -1); if (!config('terminal')) return; @@ -73,28 +73,22 @@ module.exports.show = promisify((options = {}, fn) => { }); }); -module.exports.hide = hide; - -function hide() { +export 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 { @@ -116,7 +110,7 @@ function create(createOptions) { let commandExit = false; - const {socket, terminal} = gritty(document.body, options); + const {socket, terminal} = globalThis.gritty(document.body, options); Socket = socket; Terminal = terminal; @@ -127,9 +121,8 @@ function create(createOptions) { if (commandExit) hide(); - if (shiftKey && keyCode === Key.ESC) { + if (shiftKey && keyCode === Key.ESC) hide(); - } }); Socket.on('exit', (code) => { @@ -152,4 +145,3 @@ function authCheck(spawn) { Dialog.alert('Wrong credentials!'); }); } - diff --git a/client/modules/terminal.js b/client/modules/terminal.js index db203cb923..186ee0b240 100644 --- a/client/modules/terminal.js +++ b/client/modules/terminal.js @@ -1,26 +1,22 @@ -'use strict'; - -/* global CloudCmd */ -/* global gritty */ - -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'); +import '#css/terminal.css'; +import {tryToCatch} from 'try-to-catch'; +import exec from 'execon'; +import load from 'load.js'; +import * as Images from '#dom/images'; +import DOM from '#dom'; const loadParallel = load.parallel; +const {CloudCmd} = globalThis; + const {Dialog} = DOM; -const { - Key, - config, -} = CloudCmd; +const {Key, config} = globalThis.CloudCmd; -CloudCmd.Terminal = exports; +CloudCmd.Terminal = { + init, + show, + hide, +}; let Loaded; let Terminal; @@ -36,14 +32,14 @@ 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`); } Loaded = true; }; -module.exports.init = async () => { +export async function init() { if (!config('terminal')) return; @@ -52,31 +48,24 @@ module.exports.init = async () => { await CloudCmd.View(); await loadAll(); create(); -}; - -module.exports.show = show; -module.exports.hide = hide; +} -function hide() { +export 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 = { @@ -86,7 +75,7 @@ function create() { fontFamily: 'Droid Sans Mono', }; - const {socket, terminal} = gritty(document.body, options); + const {socket, terminal} = globalThis.gritty(document.body, options); Socket = socket; Terminal = terminal; @@ -94,9 +83,8 @@ function create() { Terminal.onKey(({domEvent}) => { const {keyCode, shiftKey} = domEvent; - if (shiftKey && keyCode === Key.ESC) { + if (shiftKey && keyCode === Key.ESC) hide(); - } }); Socket.on('connect', exec.with(authCheck, socket)); @@ -110,7 +98,7 @@ function authCheck(spawn) { }); } -function show() { +export function show() { if (!Loaded) return; @@ -123,4 +111,3 @@ function show() { }, }); } - diff --git a/client/modules/upload.js b/client/modules/upload.js index 868428ad85..8433fbf543 100644 --- a/client/modules/upload.js +++ b/client/modules/upload.js @@ -1,23 +1,21 @@ /* global CloudCmd, DOM */ +import createElement from '@cloudcmd/create-element'; +import * as Files from '#dom/files'; +import {uploadFiles} from '#dom/upload-files'; +import * as Images from '#dom/images'; -'use strict'; - -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'); +CloudCmd.Upload = { + init, + show, + hide, +}; -module.exports.init = async () => { +export async function init() { Images.show.load('top'); await CloudCmd.View(); -}; - -module.exports.show = show; -module.exports.hide = hide; +} -async function show() { +export async function show() { Images.show.load('top'); const innerHTML = await Files.get('upload'); @@ -49,7 +47,7 @@ async function show() { }); } -function hide() { +export function hide() { CloudCmd.View.hide(); } @@ -66,4 +64,3 @@ function afterShow() { uploadFiles(files); }); } - diff --git a/client/modules/user-menu/get-user-menu.js b/client/modules/user-menu/get-user-menu.js index 2df176966e..aa3a1076df 100644 --- a/client/modules/user-menu/get-user-menu.js +++ b/client/modules/user-menu/get-user-menu.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = (menuFn) => { +export const getUserMenu = (menuFn) => { const module = {}; const fn = Function('module', menuFn); @@ -8,4 +6,3 @@ module.exports = (menuFn) => { return module.exports; }; - diff --git a/client/modules/user-menu/get-user-menu.spec.js b/client/modules/user-menu/get-user-menu.spec.js index 911a5e1421..606eb82074 100644 --- a/client/modules/user-menu/get-user-menu.spec.js +++ b/client/modules/user-menu/get-user-menu.spec.js @@ -1,7 +1,5 @@ -'use strict'; - -const test = require('supertape'); -const getUserMenu = require('./get-user-menu'); +import test from 'supertape'; +import {getUserMenu} from './get-user-menu.js'; test('user-menu: getUserMenu', (t) => { const menu = `module.exports = { @@ -18,4 +16,3 @@ test('user-menu: getUserMenu', (t) => { t.equal(key, 'F2 - Rename file'); t.end(); }); - diff --git a/client/modules/user-menu/index.js b/client/modules/user-menu/index.js index 19f9e5ab0f..60d32d955e 100644 --- a/client/modules/user-menu/index.js +++ b/client/modules/user-menu/index.js @@ -1,47 +1,49 @@ -'use strict'; - -/* global CloudCmd, DOM */ - -require('../../../css/user-menu.css'); - -const currify = require('currify'); -const wraptile = require('wraptile'); -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 {codeFrameColumns} = require('@babel/code-frame'); - -const Images = require('../../dom/images'); -const Dialog = require('../../dom/dialog'); -const getUserMenu = require('./get-user-menu'); -const navigate = require('./navigate'); -const parseError = require('./parse-error'); -const parseUserMenu = require('./parse-user-menu'); -const {runSelected} = require('./run'); +import '../../../css/user-menu.css'; +import currify from 'currify'; +import wraptile from 'wraptile'; +import {fullstore} from 'fullstore'; +import load from 'load.js'; +import createElement from '@cloudcmd/create-element'; +import {tryCatch} from 'try-catch'; +import {tryToCatch} from 'try-to-catch'; +import {codeFrameColumns} from '@babel/code-frame'; +import * as Dialog from '#dom/dialog'; +import * as Images from '#dom/images'; +import {getUserMenu} from './get-user-menu.js'; +import {navigate} from './navigate.js'; +import {parseError} from './parse-error.js'; +import {parseUserMenu} from './parse-user-menu.js'; +import {runSelected} from './run.js'; const loadCSS = load.css; const sourceStore = fullstore(); +const { + CloudCmd, + DOM, + CloudFunc, +} = globalThis; + const Name = 'UserMenu'; -CloudCmd[Name] = module.exports; + +CloudCmd[Name] = { + init, + show, + hide, +}; const {Key} = CloudCmd; -module.exports.init = async () => { +export async function init() { await Promise.all([ loadCSS(`${CloudCmd.prefix}/dist/user-menu.css`), CloudCmd.View(), ]); -}; - -module.exports.show = show; -module.exports.hide = hide; +} const {CurrentInfo} = DOM; -async function show() { +export async function show() { Images.show.load('top'); const {dirPath} = CurrentInfo; @@ -52,7 +54,10 @@ async function show() { Images.hide(); if (error) - return Dialog.alert(getCodeFrame({error, source})); + return Dialog.alert(getCodeFrame({ + error, + source, + })); sourceStore(source); @@ -104,7 +109,7 @@ function fillTemplate(options) { return result.join(''); } -function hide() { +export function hide() { CloudCmd.View.hide(); } @@ -118,10 +123,7 @@ const onButtonClick = wraptile(async (items, {value}) => { }); const onKeyDown = currify(async ({keys, userMenu}, e) => { - const { - keyCode, - target, - } = e; + const {keyCode, target} = e; const keyName = e.key.toUpperCase(); @@ -149,6 +151,7 @@ const runUserMenu = async (fn) => { const [error] = await tryToCatch(fn, { DOM, CloudCmd, + CloudFunc, tryToCatch, }); @@ -156,6 +159,7 @@ const runUserMenu = async (fn) => { return; const source = sourceStore(); + return Dialog.alert(getCodeFrame({ error, source, @@ -169,6 +173,7 @@ function getCodeFrame({error, source}) { return error.message; const [line, column] = parseError(error); + const start = { line, column, @@ -185,4 +190,3 @@ function getCodeFrame({error, source}) { return `
${frame}
`; } - diff --git a/client/modules/user-menu/navigate.js b/client/modules/user-menu/navigate.js index 67c12e0ad5..0fa4d67201 100644 --- a/client/modules/user-menu/navigate.js +++ b/client/modules/user-menu/navigate.js @@ -1,35 +1,53 @@ -'use strict'; - -const { +import {fullstore} from 'fullstore'; +import { J, K, UP, DOWN, -} = require('../../key/key.js'); +} from '../../key/key.js'; + +const store = fullstore(1); +const isDigit = (a) => /^\d+$/.test(a); -module.exports = (el, {keyCode}) => { - if (keyCode === DOWN || keyCode === J) - return down(el); +export const navigate = (el, {key, keyCode}) => { + if (isDigit(key)) + store(Number(key)); - if (keyCode === UP || keyCode === K) - return up(el); + if (keyCode === DOWN || keyCode === J) { + const count = store(); + store(1); + + return down(el, count); + } + + if (keyCode === UP || keyCode === K) { + const count = store(); + store(1); + + return up(el, count); + } }; -function down(el) { +function down(el, count) { const {length} = el; if (el.selectedIndex === length - 1) el.selectedIndex = 0; else - ++el.selectedIndex; + el.selectedIndex += count; + + if (el.selectedIndex < 0) + el.selectedIndex = length - 1; } -function up(el) { +function up(el, count) { const {length} = el; if (!el.selectedIndex) el.selectedIndex = length - 1; else - --el.selectedIndex; + el.selectedIndex -= count; + + if (el.selectedIndex < 0) + el.selectedIndex = 0; } - diff --git a/client/modules/user-menu/navigate.spec.js b/client/modules/user-menu/navigate.spec.js index 7a5ba70b89..6539c8e94e 100644 --- a/client/modules/user-menu/navigate.spec.js +++ b/client/modules/user-menu/navigate.spec.js @@ -1,14 +1,11 @@ -'use strict'; - -const test = require('supertape'); -const navigate = require('./navigate'); - -const { +import test from 'supertape'; +import {navigate} from './navigate.js'; +import { UP, DOWN, J, K, -} = require('../../key/key.js'); +} from '../../key/key.js'; test('cloudcmd: user-menu: navigate: DOWN', (t) => { const el = { @@ -108,3 +105,59 @@ test('cloudcmd: user-menu: navigate', (t) => { t.end(); }); +test('cloudcmd: user-menu: navigate: DOWN: count', (t) => { + const el = { + length: 3, + selectedIndex: 0, + }; + + navigate(el, { + keyCode: 53, + key: '5', + }); + + navigate(el, { + keyCode: DOWN, + }); + + t.equal(el.selectedIndex, 5); + t.end(); +}); + +test('cloudcmd: user-menu: navigate: J: count: to big', (t) => { + const el = { + length: 3, + selectedIndex: -Infinity, + }; + + navigate(el, { + keyCode: 53, + key: '5', + }); + + navigate(el, { + keyCode: J, + }); + + t.equal(el.selectedIndex, 2); + t.end(); +}); + +test('cloudcmd: user-menu: navigate: K: count: to small', (t) => { + const el = { + length: 3, + selectedIndex: -Infinity, + }; + + navigate(el, { + keyCode: 53, + key: '5', + }); + + navigate(el, { + keyCode: K, + }); + + t.equal(el.selectedIndex, 0); + t.end(); +}); diff --git a/client/modules/user-menu/parse-error.js b/client/modules/user-menu/parse-error.js index 190c4ff676..23d605e0a2 100644 --- a/client/modules/user-menu/parse-error.js +++ b/client/modules/user-menu/parse-error.js @@ -1,19 +1,11 @@ -'use strict'; - const isNumber = (a) => typeof a === 'number'; -module.exports = (error) => { - const { - lineNumber, - columnNumber, - } = error; +export const parseError = (error) => { + const {lineNumber, columnNumber} = error; // thank you firefox if (isNumber(lineNumber) && isNumber(columnNumber)) - return [ - lineNumber, - columnNumber, - ]; + return [lineNumber, columnNumber]; const before = error.stack.indexOf('>'); const str = error.stack.slice(before + 1); @@ -27,4 +19,3 @@ module.exports = (error) => { Number(column), ]; }; - diff --git a/client/modules/user-menu/parse-error.spec.js b/client/modules/user-menu/parse-error.spec.js index ebd215539a..5ec8da45d2 100644 --- a/client/modules/user-menu/parse-error.spec.js +++ b/client/modules/user-menu/parse-error.spec.js @@ -1,7 +1,5 @@ -'use strict'; - -const test = require('supertape'); -const parseError = require('./parse-error'); +import test from 'supertape'; +import {parseError} from './parse-error.js'; test('user-menu: parse-error', (t) => { const result = parseError({ @@ -15,7 +13,7 @@ test('user-menu: parse-error', (t) => { t.end(); }); -test('user-menu: parse-error', (t) => { +test('user-menu: parse-error: stack', (t) => { const stack = ` ReferenceError: s is not defined at eval (eval at module.exports (get-user-menu.js:NaN), :1:2) @@ -24,7 +22,10 @@ test('user-menu: parse-error', (t) => { at AsyncFunction.show (index.js:67) `; - const result = parseError({stack}); + const result = parseError({ + stack, + }); + const expected = [1, 2]; t.deepEqual(result, expected); diff --git a/client/modules/user-menu/parse-user-menu.js b/client/modules/user-menu/parse-user-menu.js index 78ce016fdd..e4ad39e36f 100644 --- a/client/modules/user-menu/parse-user-menu.js +++ b/client/modules/user-menu/parse-user-menu.js @@ -1,8 +1,6 @@ -'use strict'; - const {entries, assign} = Object; -module.exports = (userMenu) => { +export const parseUserMenu = (userMenu) => { const names = []; const keys = {}; const items = {}; @@ -14,9 +12,8 @@ module.exports = (userMenu) => { continue; } - if (/^_/.test(str)) { + if (str.startsWith('_')) continue; - } names.push(str); const [key, name] = str.split(' - '); @@ -32,4 +29,3 @@ module.exports = (userMenu) => { settings, }; }; - diff --git a/client/modules/user-menu/parse-user-menu.spec.js b/client/modules/user-menu/parse-user-menu.spec.js index 3cdd72cdf2..fb76320990 100644 --- a/client/modules/user-menu/parse-user-menu.spec.js +++ b/client/modules/user-menu/parse-user-menu.spec.js @@ -1,19 +1,20 @@ -'use strict'; - -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); -const parse = require('./parse-user-menu'); +import {test, stub} from 'supertape'; +import {parseUserMenu} from './parse-user-menu.js'; test('cloudcmd: user menu: parse', (t) => { const fn = stub(); const __settings = {}; - const result = parse({ + + const result = parseUserMenu({ __settings, 'F2 - Rename file': fn, '_f': fn, }); - const names = ['F2 - Rename file']; + const names = [ + 'F2 - Rename file', + ]; + const keys = { F2: fn, }; diff --git a/client/modules/user-menu/run.js b/client/modules/user-menu/run.js index 463396d271..51e9fe024f 100644 --- a/client/modules/user-menu/run.js +++ b/client/modules/user-menu/run.js @@ -1,8 +1,5 @@ -'use strict'; - -module.exports.runSelected = async (selectedItems, items, runUserMenu) => { +export const runSelected = async (selectedItems, items, runUserMenu) => { for (const selected of selectedItems) { await runUserMenu(items[selected]); } }; - diff --git a/client/modules/user-menu/run.spec.js b/client/modules/user-menu/run.spec.js index 98165ec69e..86738712a1 100644 --- a/client/modules/user-menu/run.spec.js +++ b/client/modules/user-menu/run.spec.js @@ -1,15 +1,10 @@ -'use strict'; - -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); -const {runSelected} = require('./run'); +import {test, stub} from 'supertape'; +import {runSelected} from './run.js'; test('cloudcmd: client: user menu: run', async (t) => { const runUserMenu = stub(); const fn = stub(); - const selected = [ - 'hello', - ]; + const selected = ['hello']; const items = { hello: fn, @@ -20,4 +15,3 @@ test('cloudcmd: client: user menu: run', async (t) => { t.calledWith(runUserMenu, [fn]); t.end(); }); - diff --git a/client/modules/view/get-type.js b/client/modules/view/get-type.js index 80252aa1c4..a061399faf 100644 --- a/client/modules/view/get-type.js +++ b/client/modules/view/get-type.js @@ -1,14 +1,13 @@ -'use strict'; +import currify from 'currify'; -const currify = require('currify'); 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) => { +export default (name) => { if (isPDF(name)) return 'pdf'; @@ -45,9 +44,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 44fb3384b8..6626f1aada 100644 --- a/client/modules/view/index.js +++ b/client/modules/view/index.js @@ -1,32 +1,26 @@ -'use strict'; - -/* global CloudCmd, DOM */ - -require('../../../css/view.css'); - -const rendy = require('rendy'); -const currify = require('currify'); -const wraptile = require('wraptile'); -const tryToCatch = require('try-to-catch'); -const load = require('load.js'); - -const modal = require('@cloudcmd/modal'); -const createElement = require('@cloudcmd/create-element'); - -const {time} = require('../../../common/util'); -const {FS} = require('../../../common/cloudfunc'); -const { +import '#css/view.css'; +import {rendy} from 'rendy'; +import currify from 'currify'; +import wraptile from 'wraptile'; +import {tryToCatch} from 'try-to-catch'; +import load from 'load.js'; +import * as _modal from '@cloudcmd/modal'; +import _createElement from '@cloudcmd/create-element'; +import {time} from '#common/util'; +import * as Files from '#dom/files'; +import * as Events from '#dom/events'; +import {FS} from '#common/cloudfunc'; +import * as Images from '#dom/images'; +import {encode} from '#common/entity'; +import { isImage, isAudio, getType, -} = require('./types'); - -const Files = require('../../dom/files'); -const Events = require('../../dom/events'); -const Images = require('../../dom/images'); - -const {encode} = require('../../../common/entity'); +} from './types.js'; +const CloudCmd = globalThis.CloudCmd || {}; +const DOM = globalThis.DOM || {}; +const isString = (a) => typeof a === 'string'; const {assign} = Object; const {isArray} = Array; @@ -43,17 +37,22 @@ const addEvent = lifo(Events.add); const loadCSS = load.css; -module.exports.show = show; -module.exports.hide = hide; - let Loading = false; const Name = 'View'; -CloudCmd[Name] = module.exports; + +CloudCmd[Name] = { + init, + show, + hide, +}; const Info = DOM.CurrentInfo; const {Key} = CloudCmd; -const basename = (a) => a.split('/').pop(); + +const basename = (a) => a + .split('/') + .pop(); let El; let TemplateAudio; @@ -82,9 +81,10 @@ const Config = { title: {}, }, }; -module.exports._Config = Config; -module.exports.init = async () => { +export const _Config = Config; + +export async function init() { await loadAll(); const events = [ @@ -92,10 +92,13 @@ module.exports.init = async () => { 'contextmenu', ]; - events.forEach(addEvent(Overlay, onOverlayClick)); -}; + events.forEach(addEvent( + Overlay, + onOverlayClick, + )); +} -async function show(data, options = {}) { +export async function show(data, options = {}) { const prefixURL = CloudCmd.prefixURL + FS; if (Loading) @@ -104,7 +107,7 @@ async function show(data, options = {}) { if (!options || options.bindKeys !== false) Events.addKey(listener); - El = createElement('div', { + El = _createElement('div', { className: 'view', notAppend: true, }); @@ -117,7 +120,7 @@ async function show(data, options = {}) { else El.append(data); - modal.open(El, initConfig(options)); + _modal.open(El, initConfig(options)); return; } @@ -147,8 +150,13 @@ async function show(data, options = {}) { } } -module.exports._createIframe = createIframe; -function createIframe(src) { +export const _createIframe = createIframe; + +function createIframe(src, overrides = {}) { + const { + createElement = _createElement, + } = overrides; + const element = createElement('iframe', { src, width: '100%', @@ -162,8 +170,10 @@ function createIframe(src) { return element; } -module.exports._viewHtml = viewHtml; -function viewHtml(src) { +export const _viewHtml = viewHtml; + +function viewHtml(src, overrides = {}) { + const {modal = _modal} = overrides; modal.open(createIframe(src), Config); } @@ -175,7 +185,7 @@ function viewPDF(src) { if (CloudCmd.config('showFileName')) options.title = Info.name; - modal.open(element, options); + _modal.open(element, options); } async function viewMedia(path) { @@ -196,7 +206,7 @@ async function viewMedia(path) { }, }; - modal.open(element, allConfig); + _modal.open(element, allConfig); } async function viewFile() { @@ -212,12 +222,13 @@ async function viewFile() { options.title = Info.name; El.append(element); - modal.open(El, options); + _modal.open(El, options); } const copy = (a) => assign({}, a); -module.exports._initConfig = initConfig; +export const _initConfig = initConfig; + function initConfig(options) { const config = copy(Config); @@ -225,6 +236,7 @@ function initConfig(options) { return config; const names = Object.keys(options); + for (const name of names) { const isConfig = Boolean(config[name]); const item = options[name]; @@ -235,14 +247,15 @@ function initConfig(options) { } const fn = config[name]; + config[name] = series(fn, item); } return config; } -function hide() { - modal.close(); +export function hide() { + _modal.close(); } function viewImage(path, prefixURL) { @@ -256,17 +269,17 @@ function viewImage(path, prefixURL) { .map(DOM.getCurrentPath) .filter(isSupportedImage); - const titles = names - .map(makeTitle); + const titles = names.map(makeTitle); const index = names.indexOf(Info.path); + const imageConfig = { index, - autoSize : true, - arrows : true, - keys : true, - helpers : { - title : {}, + autoSize: true, + arrows: true, + keys: true, + helpers: { + title: {}, }, }; @@ -275,7 +288,7 @@ function viewImage(path, prefixURL) { ...imageConfig, }; - modal.open(titles, config); + _modal.open(titles, config); } async function getMediaElement(src) { @@ -300,7 +313,7 @@ async function getMediaElement(src) { name, }); - const element = createElement('div', { + const element = _createElement('div', { innerHTML, }); @@ -308,21 +321,17 @@ async function getMediaElement(src) { } function check(src) { - if (typeof src !== 'string') + if (!isString(src)) throw Error('src should be a string!'); } -/** - * function loads css and js of FancyBox - * @callback - executes, when everything loaded - */ async function loadAll() { - const {prefix} = CloudCmd; + const {DIR_DIST} = CloudCmd; - time(Name + ' load'); + time(`${Name} load`); Loading = true; - await loadCSS(`${prefix}/dist/view.css`); + await loadCSS(`${DIR_DIST}/view.css`); Loading = false; } @@ -341,10 +350,7 @@ function setCurrentByPosition(position) { if (!element) return; - const { - files, - filesPassive, - } = Info; + const {files, filesPassive} = Info; const isFiles = files.includes(element); const isFilesPassive = filesPassive.includes(element); @@ -364,4 +370,3 @@ function listener({keyCode}) { if (keyCode === Key.ESC) hide(); } - diff --git a/client/modules/view/index.spec.js b/client/modules/view/index.spec.js index 8bb034933e..8d18454899 100644 --- a/client/modules/view/index.spec.js +++ b/client/modules/view/index.spec.js @@ -1,24 +1,19 @@ -'use strict'; +import autoGlobals from 'auto-globals'; +import {stub} from '@cloudcmd/stub'; +import {test as tape} from 'supertape'; +import { + _initConfig, + _viewHtml, + _Config, + _createIframe, +} from './index.js'; -require('css-modules-require-hook/preset'); - -const autoGlobals = require('auto-globals'); -const test = autoGlobals(require('supertape')); -const stub = require('@cloudcmd/stub'); -const mockRequire = require('mock-require'); -const {reRequire, stopAll} = mockRequire; +const test = autoGlobals(tape); 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,56 +25,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,13 +60,13 @@ test('cloudcmd: client: view: createIframe', (t) => { const el = { addEventListener, }; - const createElement = stub().returns(el); - - mockRequire('@cloudcmd/create-element', createElement); - const {_createIframe} = reRequire('.'); + const createElement = stub().returns(el); const src = '/hello.html'; - _createIframe(src); + + _createIframe(src, { + createElement, + }); const expected = { src, @@ -103,8 +74,6 @@ test('cloudcmd: client: view: createIframe', (t) => { width: '100%', }; - stopAll(); - t.calledWith(createElement, ['iframe', expected]); t.end(); }); @@ -114,15 +83,13 @@ test('cloudcmd: client: view: createIframe: returns', (t) => { const el = { addEventListener, }; - const createElement = stub().returns(el); - mockRequire('@cloudcmd/create-element', createElement); - const {_createIframe} = reRequire('.'); + const createElement = stub().returns(el); 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 c4cf5ce611..320a4bc87b 100644 --- a/client/modules/view/types.js +++ b/client/modules/view/types.js @@ -1,15 +1,16 @@ -'use strict'; +import {extname} from 'node:path'; +import currify from 'currify'; + +export const isAudio = (name) => /\.(mp3|ogg|m4a|flac)$/i.test(name); -const {extname} = require('path'); -const currify = require('currify'); 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) => { +export const getType = async (path) => { const ext = extname(path); if (!ext) @@ -31,8 +32,7 @@ module.exports.getType = async (path) => { return 'markdown'; }; -module.exports.isImage = isImage; -function isImage(name) { +export function isImage(name) { const images = [ 'jp(e|g|eg)', 'gif', @@ -52,16 +52,12 @@ function isMedia(name) { return isAudio(name) || isVideo(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); } -module.exports._detectType = detectType; +export const _detectType = detectType; + async function detectType(path) { const {headers} = await fetch(path, { method: 'HEAD', @@ -69,9 +65,10 @@ async function detectType(path) { for (const [name, value] of headers) { if (name === 'content-type') - return `.${value.split('/').pop()}`; + return `.${value + .split('/') + .pop()}`; } return ''; } - diff --git a/client/modules/view/types.spec.js b/client/modules/view/types.spec.js index fc93fa9755..f0542c180b 100644 --- a/client/modules/view/types.spec.js +++ b/client/modules/view/types.spec.js @@ -1,7 +1,5 @@ -'use strict'; - -const {test, stub} = require('supertape'); -const {isAudio, _detectType} = require('./types'); +import {test, stub} from 'supertape'; +import {isAudio, _detectType} from './types.js'; test('cloudcmd: client: view: types: isAudio', (t) => { const result = isAudio('hello.mp3'); @@ -10,6 +8,13 @@ test('cloudcmd: client: view: types: isAudio', (t) => { t.end(); }); +test('cloudcmd: client: view: types: isAudio: flac', (t) => { + const result = isAudio('hello.flac'); + + t.ok(result); + t.end(); +}); + test('cloudcmd: client: view: types: isAudio: no', (t) => { const result = isAudio('hello'); @@ -22,35 +27,25 @@ 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', - }, - ]; + const expected = ['/hello', { + method: 'HEAD', + }]; t.calledWith(fetch, expected); t.end(); }); test('cloudcmd: client: view: types: detectType: found', async (t) => { - const fetch = stub().returns({ + globalThis.fetch = stub().returns({ headers: [ ['content-type', 'image/png'], ], }); - - const originalFetch = global.fetch; - global.fetch = fetch; const result = await _detectType('/hello'); - global.fetch = originalFetch; - t.equal(result, '.png'); t.end(); }); - diff --git a/client/sort.js b/client/sort.js index f46f61a206..ed0971f3a1 100644 --- a/client/sort.js +++ b/client/sort.js @@ -1,39 +1,32 @@ -'use strict'; - /* global CloudCmd */ -const DOM = require('./dom'); - -const Info = DOM.CurrentInfo; - -const { - sort, - order, -} = CloudCmd; +import {fullstore} from 'fullstore'; +import DOM from '#dom'; -const position = DOM.getPanelPosition(); - -let sortPrevious = sort[position]; +const sortPrevious = fullstore(); const {getPanel} = DOM; -CloudCmd.sortPanel = (name, panel = getPanel()) => { - const position = panel - .dataset - .name - .replace('js-', ''); +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) { + if (name !== sortPrevious()) + order[position] = 'asc'; + else if (order[position] === 'asc') + order[position] = 'desc'; + else order[position] = 'asc'; - } else { - if (order[position] === 'asc') - order[position] = 'desc'; - else - order[position] = 'asc'; - } - sortPrevious = + sortPrevious(name); sort[position] = name; - const noCurrent = position !== Info.panelPosition; CloudCmd.refresh({ @@ -41,4 +34,3 @@ CloudCmd.sortPanel = (name, panel = getPanel()) => { noCurrent, }); }; - diff --git a/client/sw/register.js b/client/sw/register.js index 518bceff95..928d63c9e0 100644 --- a/client/sw/register.js +++ b/client/sw/register.js @@ -1,13 +1,10 @@ -'use strict'; +import {tryToCatch} from 'try-to-catch'; -module.exports.registerSW = registerSW; -module.exports.unregisterSW = unregisterSW; - -module.exports.listenSW = (sw, ...args) => { +export const listenSW = (sw, ...args) => { sw?.addEventListener(...args); }; -async function registerSW(prefix) { +export async function registerSW(prefix) { if (!navigator.serviceWorker) return; @@ -17,10 +14,17 @@ async function registerSW(prefix) { if (!isHTTPS && !isLocalhost) return; - return await navigator.serviceWorker.register(`${prefix}/sw.js`); + const {serviceWorker} = navigator; + const register = serviceWorker.register.bind(serviceWorker); + const [e, sw] = await tryToCatch(register, `${prefix}/sw.js`); + + if (e) + return null; + + return sw; } -async function unregisterSW(prefix) { + +export async function unregisterSW(prefix) { const reg = await registerSW(prefix); reg?.unregister(prefix); } - diff --git a/client/sw/register.spec.js b/client/sw/register.spec.js index 7b5cb98fb9..47420ba705 100644 --- a/client/sw/register.spec.js +++ b/client/sw/register.spec.js @@ -1,16 +1,16 @@ -'use strict'; - -const autoGlobals = require('auto-globals'); -const tape = require('supertape'); +import autoGlobals from 'auto-globals'; +import tape from 'supertape'; +import {stub} from '@cloudcmd/stub'; +import {tryCatch} from 'try-catch'; +import { + listenSW, + registerSW, + unregisterSW, +} from './register.js'; const test = autoGlobals(tape); -const stub = require('@cloudcmd/stub'); -const tryCatch = require('try-catch'); -const {reRequire} = require('mock-require'); - test('sw: listen', (t) => { - const {listenSW} = reRequire('./register'); const addEventListener = stub(); const sw = { addEventListener, @@ -23,7 +23,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 +30,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,11 +43,9 @@ test('sw: register: registerSW: no https', async (t, {location, navigator}) => { location.protocol = 'http:'; - const {registerSW} = reRequire('./register'); - await registerSW(); - t.notOk(register.called, 'should not call register'); + t.notCalled(register, 'should not call register'); t.end(); }); @@ -62,11 +57,24 @@ test('sw: register: registerSW: http', async (t, {location, navigator}) => { const {register} = navigator.serviceWorker; - const {registerSW} = reRequire('./register'); - await registerSW(); - t.notOk(register.called, 'should not call register'); + t.notCalled(register, 'should not call register'); + t.end(); +}); + +test('sw: register: registerSW: https self-signed', async (t, {location, navigator}) => { + Object.assign(location, { + protocol: 'https', + hostname: 'self-signed.badssl.com', + }); + + const {register} = navigator.serviceWorker; + register.throws(Error('Cannot register service worker!')); + + const result = await registerSW(); + + t.notOk(result, 'should not throw'); t.end(); }); @@ -74,8 +82,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'); @@ -90,11 +96,8 @@ 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'); t.end(); }); - diff --git a/client/sw/sw.js b/client/sw/sw.js index 3c849bd846..5b5643a386 100644 --- a/client/sw/sw.js +++ b/client/sw/sw.js @@ -1,8 +1,7 @@ -'use strict'; - -const codegen = require('codegen.macro'); -const tryToCatch = require('try-to-catch'); -const currify = require('currify'); +import process from 'node:process'; +import codegen from 'codegen.macro'; +import {tryToCatch} from 'try-to-catch'; +import currify from 'currify'; const isDev = process.env.NODE_ENV === 'development'; @@ -15,7 +14,7 @@ const respondWith = currify((f, e) => { const {url} = request; const pathname = getPathName(url); - if (/\/$/.test(url) || /\^\/fs/.test(pathname)) + if (url.endsWith('/') || /\^\/fs/.test(pathname)) return; if (!isGet(request)) @@ -24,7 +23,7 @@ const respondWith = currify((f, e) => { if (!isBasic(request)) return; - if (/^\/api/.test(pathname)) + if (pathname.startsWith('/api')) return; if (/^socket.io/.test(pathname)) @@ -49,14 +48,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); @@ -66,7 +65,7 @@ async function onActivate() { async function onInstall() { console.info(`cloudcmd: sw: install: ${NAME}`); - await self.skipWaiting(); + await globalThis.skipWaiting(); } async function onFetch(event) { @@ -97,4 +96,3 @@ async function addToCache(request, response) { const cache = await caches.open(NAME); return cache.put(request, response); } - diff --git a/common/base64.js b/common/base64.js deleted file mode 100644 index bc73b7f380..0000000000 --- a/common/base64.js +++ /dev/null @@ -1,20 +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 0c8f3ca73a..0000000000 --- a/common/base64.spec.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; - -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); - -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.js b/common/callbackify.js index a865748070..71160ac716 100644 --- a/common/callbackify.js +++ b/common/callbackify.js @@ -1,12 +1,9 @@ -'use strict'; - const success = (f) => (data) => f(null, data); -module.exports = (promise) => (...a) => { +export default (promise) => (...a) => { const fn = a.pop(); promise(...a) .then(success(fn)) .catch(fn); }; - diff --git a/common/callbackify.spec.js b/common/callbackify.spec.js index 9558dfcb29..d1586d5acb 100644 --- a/common/callbackify.spec.js +++ b/common/callbackify.spec.js @@ -1,19 +1,14 @@ -'use strict'; - -const tryToCatch = require('try-to-catch'); - -const { - test, - stub, -} = require('supertape'); -const callbackify = require('./callbackify'); -const {promisify} = require('util'); +import {promisify} from 'node:util'; +import {tryToCatch} from 'try-to-catch'; +import {test, stub} from 'supertape'; +import callbackify from './callbackify.js'; test('cloudcmd: common: callbackify: error', async (t) => { const promise = stub().rejects(Error('hello')); const fn = callbackify(promise); - const [error] = await tryToCatch(promisify(fn)); + const newPromise = promisify(fn); + const [error] = await tryToCatch(newPromise); t.equal(error.message, 'hello'); t.end(); @@ -29,4 +24,3 @@ test('cloudcmd: common: callbackify', async (t) => { t.equal(data, 'hi'); t.end(); }); - diff --git a/common/cloudfunc.js b/common/cloudfunc.js index ac0cc221f6..7884d06988 100644 --- a/common/cloudfunc.js +++ b/common/cloudfunc.js @@ -1,50 +1,47 @@ -'use strict'; +import {rendy} from 'rendy'; +import currify from 'currify'; +import {fullstore} from 'fullstore'; +import {encode} from '#common/entity'; -const rendy = require('rendy'); -const currify = require('currify'); -const store = require('fullstore'); -const {encode} = require('./entity'); -const {btoa} = require('./base64'); - -const getHeaderField = currify(_getHeaderField); +const id = (a) => a; +const NAME = 'Cloud Commander'; -/* КОНСТАНТЫ (общие для клиента и сервера)*/ +export const dateFormatter = fullstore(id); +export const getHeaderField = currify(_getHeaderField); +export const FS = '/fs'; -/* название программы */ -const NAME = 'Cloud Commander'; -const FS = '/fs'; -const Path = store(); +const Path = fullstore(); 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('.'); +}; + +export const apiURL = '/api/v1'; +export const MAX_FILE_SIZE = 500 * 1024; -module.exports.formatMsg = (msg, name, status) => { +export const formatMsg = (msg, name, status) => { status = status || 'ok'; name = name || ''; if (name) - name = '("' + name + '")'; + name = `("${name}")`; - return msg + ': ' + status + name; + return `${msg}: ${status}${name}`; }; /** * Функция возвращает заголовок веб страницы * @path */ -module.exports.getTitle = (options) => { +export const getTitle = (options) => { options = options || {}; - const { - path = Path(), - name, - } = options; + const {path = Path(), name} = options; const array = [ name || NAME, @@ -60,7 +57,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!'); @@ -71,7 +68,11 @@ function getPathLink(url, prefix, template) { .split('/') .slice(1, -1); - const allNames = ['/', ...names]; + const allNames = [ + '/', + ...names, + ]; + const lines = []; const n = allNames.length; @@ -82,10 +83,10 @@ function getPathLink(url, prefix, template) { const isLast = i === n - 1; if (i) - path += name + '/'; + path += `${name}/`; if (i && isLast) { - lines.push(name + '/'); + lines.push(`${name}/`); continue; } @@ -102,30 +103,32 @@ 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 formatDate = dateFormatter(); + const templateFile = template.file; const templateLink = template.link; const json = params.data; const path = encode(json.path); - const {files} = json; /* @@ -148,6 +151,7 @@ module.exports.buildFromJSON = (params) => { const name = getFieldName('name'); const size = getFieldName('size'); const date = getFieldName('date'); + const time = getFieldName('time'); const header = rendy(templateFile, { tag: 'div', @@ -157,6 +161,7 @@ module.exports.buildFromJSON = (params) => { name, size, date, + time, owner, mode, }); @@ -164,7 +169,7 @@ module.exports.buildFromJSON = (params) => { /* сохраняем путь */ Path(path); - fileTable += header + '
    '; + fileTable += `${header}
      `; /* Если мы не в корне */ if (path !== '/') { @@ -177,8 +182,8 @@ module.exports.buildFromJSON = (params) => { name: '..', }); - const dataName = getDataName('..'); - const attribute = 'draggable="true" ' + dataName; + const dataName = _getDataName('..'); + const attribute = `draggable="true" ${dataName}`; /* Сохраняем путь к каталогу верхнего уровня*/ fileTable += rendy(template.file, { @@ -189,12 +194,16 @@ module.exports.buildFromJSON = (params) => { name: linkResult, size: '<dir>', date: '--.--.----', + time: '--:--:--', owner: '.', mode: '--- --- ---', }); } fileTable += files + .filter(filterOutDotFiles({ + showDotFiles, + })) .map(updateField) .map((file) => { const name = encode(file.name); @@ -204,6 +213,7 @@ module.exports.buildFromJSON = (params) => { type, mode, date, + time, owner, size, } = file; @@ -215,7 +225,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, { @@ -225,25 +235,26 @@ module.exports.buildFromJSON = (params) => { type, name: linkResult, size, - date, + date: formatDate(date), + time, owner, mode, }); - }).join(''); + }) + .join(''); fileTable += '
    '; return fileTable; }; -function updateField(file) { - return { - ...file, - date: file.date || '--.--.----', - owner: file.owner || 'root', - size: getSize(file), - }; -} +const updateField = (file) => ({ + ...file, + date: file.date || '--.--.----', + time: file.time || '--:--:--', + owner: file.owner || 'root', + size: getSize(file), +}); function getAttribute(type) { if (type === 'directory') @@ -252,13 +263,9 @@ function getAttribute(type) { return 'target="_blank" '; } -module.exports._getSize = getSize; -function getSize(file) { - const { - size, - type, - } = file; - +export const _getSize = getSize; + +function getSize({size, type}) { if (type === 'directory') return '<dir>'; @@ -280,7 +287,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('/')); @@ -290,4 +297,3 @@ function getDotDot(path) { return dotDot; } - diff --git a/common/cloudfunc.spec.js b/common/cloudfunc.spec.js index b77dfd8ff9..dca24b5a77 100644 --- a/common/cloudfunc.spec.js +++ b/common/cloudfunc.spec.js @@ -1,19 +1,17 @@ -'use strict'; - -const {join} = require('path'); -const {readFileSync} = require('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, + dateFormatter, +} from '#common/cloudfunc'; + +const templatePath = new URL('../tmpl/fs', import.meta.url).pathname; -const templatePath = join(__dirname, '../tmpl/fs'); const template = { pathLink: readFileSync(`${templatePath}/pathLink.hbs`, 'utf8'), path: readFileSync(`${templatePath}/path.hbs`, 'utf8'), @@ -42,7 +40,11 @@ test('cloudfunc: buildFromJSON: ..', (t) => { const $ = cheerio.load(html); const el = $('[data-name="js-file-Li4="]'); - const result = el.find('[data-name="js-name"]').text(); + + const result = el + .find('[data-name="js-name"]') + .text(); + const expected = '..'; t.equal(result, expected); @@ -52,6 +54,7 @@ test('cloudfunc: buildFromJSON: ..', (t) => { test('cloudfunc: getPathLink: /', (t) => { const {pathLink} = template; const result = getPathLink('/', '', pathLink); + const expected = montag` / `; @@ -63,6 +66,7 @@ test('cloudfunc: getPathLink: /', (t) => { test('cloudfunc: getPathLink: /hello/world', (t) => { const {pathLink} = template; const result = getPathLink('/hello/world', '', pathLink); + const expected = montag` /hello/ `; @@ -74,6 +78,7 @@ test('cloudfunc: getPathLink: /hello/world', (t) => { test('cloudfunc: getPathLink: prefix', (t) => { const {pathLink} = template; const result = getPathLink('/hello/world', '/cloudcmd', pathLink); + const expected = montag` /hello/ `; @@ -85,6 +90,7 @@ test('cloudfunc: getPathLink: prefix', (t) => { test('cloudfunc: getSize: dir', (t) => { const type = 'directory'; const size = 0; + const result = _getSize({ type, size, @@ -99,6 +105,7 @@ test('cloudfunc: getSize: dir', (t) => { test('cloudfunc: getSize: link: dir', (t) => { const type = 'directory-link'; const size = 0; + const result = _getSize({ type, size, @@ -113,6 +120,7 @@ test('cloudfunc: getSize: link: dir', (t) => { test('cloudfunc: getSize: link: file', (t) => { const type = 'file-link'; const size = 0; + const result = _getSize({ type, size, @@ -127,6 +135,7 @@ test('cloudfunc: getSize: link: file', (t) => { test('cloudfunc: getSize: file', (t) => { const type = 'file'; const size = '100.00kb'; + const result = _getSize({ type, size, @@ -137,3 +146,123 @@ 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: buildFromJSON: name: {{ }}', (t) => { + const data = { + path: '/media/', + files: [{ + date: '30.08.2016', + mode: 'rwx rwx rwx', + name: '{{}}', + owner: 'root', + size: '7b', + type: 'file', + }], + }; + + const html = buildFromJSON({ + prefix: '', + template, + data, + showDotFiles: false, + }); + + const $ = cheerio.load(html); + const el = $('[data-name="js-file-JTdCJTdCJTdEJTdE"]'); + + 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(); +}); + +test('cloudfunc: buildFromJSON: formatDate', (t) => { + const data = { + path: '/media/', + files: [{ + date: '30.08.2016', + mode: 'rwx rwx rwx', + name: '{{}}', + owner: 'root', + size: '7b', + type: 'file', + }], + }; + + const oldFormatter = dateFormatter(); + + const formatDate = (str) => { + const [day, month, year] = str.split('.'); + const date = new Date(year, month - 1, day); + + return date.toLocaleDateString('en-US'); + }; + + dateFormatter(formatDate); + + const html = buildFromJSON({ + prefix: '', + template, + data, + showDotFiles: false, + }); + + dateFormatter(oldFormatter); + + const $ = cheerio.load(html); + const el = $('[data-name="js-file-JTdCJTdCJTdEJTdE"]'); + + const result = el + .find('[data-name="js-date"]') + .text(); + + const expected = '8/30/2016'; + + t.equal(result, expected); + t.end(); +}); diff --git a/common/datetime.js b/common/datetime.js index 131c27fb00..d7982ab484 100644 --- a/common/datetime.js +++ b/common/datetime.js @@ -1,8 +1,6 @@ -'use strict'; +import shortdate from 'shortdate'; -const shortdate = require('shortdate'); - -module.exports = (date) => { +export default (date) => { date = date || new Date(); check(date); diff --git a/common/datetime.spec.js b/common/datetime.spec.js index 2d8f529ec5..e3edb2ef01 100644 --- a/common/datetime.spec.js +++ b/common/datetime.spec.js @@ -1,9 +1,6 @@ -'use strict'; - -const test = require('supertape'); -const tryCatch = require('try-catch'); - -const datetime = require('./datetime'); +import {test} from 'supertape'; +import {tryCatch} from 'try-catch'; +import datetime from './datetime.js'; test('common: datetime', (t) => { const dateStr = 'Fri, 17 Aug 2018 10:56:48'; @@ -16,21 +13,20 @@ test('common: datetime', (t) => { }); test('common: datetime: no arg', (t) => { - const {Date} = global; + const {Date} = globalThis; let called = false; - const myDate = class extends Date { + + globalThis.Date = class extends Date { constructor() { super(); called = true; } }; - global.Date = myDate; - datetime(); - global.Date = Date; + globalThis.Date = Date; t.ok(called, 'should call new Date'); t.end(); @@ -52,4 +48,3 @@ test('common: datetime: wrong args', (t) => { t.equal(error.message, 'date should be instanceof Date!', 'should throw'); t.end(); }); - diff --git a/common/entity.js b/common/entity.js index bad8ef77cf..690bbd7a78 100644 --- a/common/entity.js +++ b/common/entity.js @@ -1,15 +1,14 @@ -'use strict'; - const Entities = { - ' ': ' ', '<': '<', '>': '>', '"': '"', + '{': '{', + '}': '}', }; const keys = Object.keys(Entities); -module.exports.encode = (str) => { +export const encode = (str) => { for (const code of keys) { const char = Entities[code]; const reg = RegExp(char, 'g'); @@ -20,7 +19,7 @@ module.exports.encode = (str) => { return str; }; -module.exports.decode = (str) => { +export const decode = (str) => { for (const code of keys) { const char = Entities[code]; const reg = RegExp(code, 'g'); @@ -30,4 +29,3 @@ module.exports.decode = (str) => { return str; }; - diff --git a/test/common/entity.js b/common/entity.spec.js similarity index 53% rename from test/common/entity.js rename to common/entity.spec.js index 8e38f46ab0..79313f7c9e 100644 --- a/test/common/entity.js +++ b/common/entity.spec.js @@ -1,29 +1,34 @@ -'use strict'; - -const test = require('supertape'); -const entity = require('../../common/entity'); +import {test} from 'supertape'; +import * as entity from '#common/entity'; test('cloudcmd: entity: encode', (t) => { const result = entity.encode(' '); - const expected = '<hello> '; + const expected = '<hello> '; + + t.equal(result, expected, 'should encode entity'); + t.end(); +}); + +test('cloudcmd: entity: {{}}', (t) => { + const result = entity.encode('{{}}'); + const expected = '{{}}'; t.equal(result, expected, 'should encode entity'); t.end(); }); test('cloudcmd: entity: decode', (t) => { - const result = entity.decode('<hello> '); + const result = entity.decode('<hello> '); const expected = ' '; t.equal(result, expected, 'should decode entity'); t.end(); }); -test('cloudcmd: entity: encode', (t) => { +test('cloudcmd: entity: encode quote', (t) => { const result = entity.encode('"hello"'); const expected = '"hello"'; t.equal(result, expected, 'should encode entity'); t.end(); }); - diff --git a/common/omit.js b/common/omit.js new file mode 100644 index 0000000000..284c2f5b70 --- /dev/null +++ b/common/omit.js @@ -0,0 +1,12 @@ +const difference = (a, b) => new Set(a).difference(new Set(b)); +const {keys} = Object; + +export const omit = (a, list) => { + const result = {}; + + for (const key of difference(keys(a), list)) { + result[key] = a[key]; + } + + return result; +}; diff --git a/common/omit.spec.js b/common/omit.spec.js new file mode 100644 index 0000000000..ff70541cdc --- /dev/null +++ b/common/omit.spec.js @@ -0,0 +1,18 @@ +import {test} from 'supertape'; +import {omit} from '#common/omit'; + +test('cloudcmd: common: omit', (t) => { + const a = { + hello: 1, + world: 2, + }; + + const result = omit(a, ['world']); + + const expected = { + hello: 1, + }; + + t.deepEqual(result, expected); + t.end(); +}); diff --git a/common/try-to-promise-all.js b/common/try-to-promise-all.js index c8a54eab3b..1dcfa3a800 100644 --- a/common/try-to-promise-all.js +++ b/common/try-to-promise-all.js @@ -1,9 +1,8 @@ -'use strict'; +import {tryToCatch} from 'try-to-catch'; -const tryToCatch = require('try-to-catch'); const all = Promise.all.bind(Promise); -module.exports = async (a) => { +export default async (a) => { const [e, result = []] = await tryToCatch(all, a); return [ @@ -11,4 +10,3 @@ module.exports = async (a) => { ...result, ]; }; - diff --git a/common/try-to-promise-all.spec.js b/common/try-to-promise-all.spec.js index 628bc93169..eafb036ca4 100644 --- a/common/try-to-promise-all.spec.js +++ b/common/try-to-promise-all.spec.js @@ -1,24 +1,25 @@ -'use strict'; - -const test = require('supertape'); -const tryToPromiseAll = require('./try-to-promise-all'); +import {test} from 'supertape'; +import tryToPromiseAll from './try-to-promise-all.js'; const resolve = Promise.resolve.bind(Promise); const reject = Promise.reject.bind(Promise); -test('try-to-promise-all', async (t) => { +test('commons: try-to-promise-all', async (t) => { const [, ...result] = await tryToPromiseAll([ resolve('a'), resolve('b'), ]); - const expected = ['a', 'b']; + const expected = [ + 'a', + 'b', + ]; t.deepEqual(result, expected); t.end(); }); -test('try-to-promise-all: error', async (t) => { +test('commons: try-to-promise-all: error', async (t) => { const [e] = await tryToPromiseAll([ reject('a'), ]); @@ -26,4 +27,3 @@ test('try-to-promise-all: error', async (t) => { t.equal(e, 'a'); t.end(); }); - diff --git a/common/util.js b/common/util.js index 8551b1efab..422b3c22d7 100644 --- a/common/util.js +++ b/common/util.js @@ -1,9 +1,9 @@ -'use strict'; +import exec from 'execon'; -const exec = require('execon'); +const isString = (a) => typeof a === 'string'; -module.exports.escapeRegExp = (str) => { - const isStr = typeof str === 'string'; +export const escapeRegExp = (str) => { + const isStr = isString(str); if (isStr) str = str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); @@ -14,25 +14,24 @@ module.exports.escapeRegExp = (str) => { /** * get regexp from wild card */ -module.exports.getRegExp = (wildcard) => { - const escaped = '^' + wildcard // search from start of line +export const getRegExp = (wildcard) => { + const escaped = `^${wildcard // search from start of line .replace(/\./g, '\\.') .replace(/\*/g, '.*') - .replace('?', '.?') + '$'; // search to end of line + .replace('?', '.?')}$`; + // search to end of line return RegExp(escaped); }; -module.exports.exec = exec; - /** * function gets file extension * * @param name * @return ext */ -module.exports.getExt = (name) => { - const isStr = typeof name === 'string'; +export const getExt = (name) => { + const isStr = isString(name); if (!isStr) return ''; @@ -46,18 +45,18 @@ module.exports.getExt = (name) => { }; /** - * find object by name in arrray + * find object by name in array * * @param array * @param name */ -module.exports.findObjByNameInArr = (array, name) => { +export const findObjByNameInArr = (array, name) => { let ret; if (!Array.isArray(array)) throw Error('array should be array!'); - if (typeof name !== 'string') + if (!isString(name)) throw Error('name should be string!'); array.some((item) => { @@ -89,7 +88,7 @@ module.exports.findObjByNameInArr = (array, name) => { * start timer * @param name */ -module.exports.time = (name) => { +export const time = (name) => { exec.ifExist(console, 'time', [name]); }; @@ -97,7 +96,6 @@ module.exports.time = (name) => { * stop timer * @param name */ -module.exports.timeEnd = (name) => { +export const timeEnd = (name) => { exec.ifExist(console, 'timeEnd', [name]); }; - diff --git a/common/util.spec.js b/common/util.spec.js index 36ee25d3be..6a32848cf0 100644 --- a/common/util.spec.js +++ b/common/util.spec.js @@ -1,20 +1,16 @@ -'use strict'; - -const test = require('supertape'); -const {reRequire} = require('mock-require'); -const tryCatch = require('try-catch'); -const Util = require('./util'); - -const { +import test from 'supertape'; +import {tryCatch} from 'try-catch'; +import { findObjByNameInArr, getRegExp, escapeRegExp, -} = Util; + getExt, +} from '#common/util'; test('getExt: no extension', (t) => { const EXT = ''; - const name = 'file-withot-extension'; - const ext = Util.getExt(name); + const name = 'file-without-extension'; + const ext = getExt(name); t.equal(ext, EXT, 'should return "" when extension is none'); t.end(); @@ -23,14 +19,14 @@ test('getExt: no extension', (t) => { test('getExt: return extension', (t) => { const EXT = '.png'; const name = 'picture.png'; - const ext = Util.getExt(name); + const ext = getExt(name); t.equal(ext, EXT, 'should return ".png" in files "picture.png"'); t.end(); }); test('util: getExt: no name', (t) => { - const ext = Util.getExt(); + const ext = getExt(); t.equal(ext, '', 'should return empty string'); t.end(); @@ -56,9 +52,7 @@ test('util: findObjByNameInArr: object', (t) => { name, }; - const array = [ - obj, - ]; + const array = [obj]; const result = findObjByNameInArr(array, name); @@ -69,6 +63,7 @@ test('util: findObjByNameInArr: object', (t) => { test('util: findObjByNameInArr: array', (t) => { const name = 'hello'; const data = 'abc'; + const item = { name, data, @@ -79,10 +74,8 @@ test('util: findObjByNameInArr: array', (t) => { }; const array = [ - name, [ - obj, - item, - ], + name, + [obj, item], ]; const result = findObjByNameInArr(array, name); @@ -92,9 +85,9 @@ test('util: findObjByNameInArr: array', (t) => { }); test('util: getRegExp', (t) => { - const reg = getRegExp('hel?o.*'); + const reg = getRegExp('help?o.*'); - t.deepEqual(reg, /^hel.?o\..*$/, 'should return regexp'); + t.deepEqual(reg, /^help.?o\..*$/, 'should return regexp'); t.end(); }); @@ -121,16 +114,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/columns/name-size-date-time.css b/css/columns/name-size-date-time.css new file mode 100644 index 0000000000..f46e63f8e8 --- /dev/null +++ b/css/columns/name-size-date-time.css @@ -0,0 +1,26 @@ +.name { + width: 35%; +} + +.size { + float: none; +} + +.owner { + display: none; +} + +.mode { + display: none; +} + +.date { + float: right; + width: 19%; +} + +.time { + display: inline; + float: right; + width: 20%; +} diff --git a/css/columns/name-size-date.css b/css/columns/name-size-date.css index 196a2b7fbc..9248f7002f 100644 --- a/css/columns/name-size-date.css +++ b/css/columns/name-size-date.css @@ -19,3 +19,7 @@ width: 19%; } +.time { + display: none; +} + diff --git a/css/columns/name-size-time.css b/css/columns/name-size-time.css new file mode 100644 index 0000000000..1bbd59dea8 --- /dev/null +++ b/css/columns/name-size-time.css @@ -0,0 +1,24 @@ +.name { + width: 55%; +} + +.size { + float: none; +} + +.owner { + display: none; +} + +.mode { + display: none; +} + +.date { + display: none; +} + +.time { + float: right; + width: 19%; +} diff --git a/css/columns/name-size.css b/css/columns/name-size.css index e58d1844e9..c02472a5a5 100644 --- a/css/columns/name-size.css +++ b/css/columns/name-size.css @@ -18,3 +18,8 @@ .date { display: none; } + +.time { + display: none; +} + diff --git a/css/config.css b/css/config.css index fc4d419607..4804374ab8 100644 --- a/css/config.css +++ b/css/config.css @@ -1,7 +1,7 @@ .config { white-space: normal; overflow: hidden; - width : 250px; + width: 250px; } .list li { @@ -18,15 +18,24 @@ padding: 0 12px; font-size: 16px; line-height: 1.5; - color: #555; + color: var(--column-color); + background: var(--internal-background); border: 1px solid #ccc; -webkit-box-shadow: inset 0 1px 1px rgb(0 0 0 / 7.5%); -moz-box-shadow: inset 0 1px 1px rgb(0 0 0 / 7.5%); box-shadow: inset 0 1px 1px rgb(0 0 0 / 7.5%); - -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; - -moz-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; - -o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; - transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + -webkit-transition: + border-color ease-in-out 0.15s, + box-shadow ease-in-out 0.15s; + -moz-transition: + border-color ease-in-out 0.15s, + box-shadow ease-in-out 0.15s; + -o-transition: + border-color ease-in-out 0.15s, + box-shadow ease-in-out 0.15s; + transition: + border-color ease-in-out 0.15s, + box-shadow ease-in-out 0.15s; } .config .form-control::-moz-placeholder { @@ -44,9 +53,15 @@ .config .form-control:focus { border-color: #66afe9; outline: 0; - -webkit-box-shadow: inset 0 1px 1px rgb(0 0 0 / 7.5%), 0 0 1px rgb(102 175 233 / 60%); - -moz-box-shadow: inset 0 1px 1px rgb(0 0 0 / 7.5%), 0 0 1px rgb(102 175 233 / 60%); - box-shadow: inset 0 1px 1px rgb(0 0 0 / 7.5%), 0 0 1px rgb(102 175 233 / 60%); + -webkit-box-shadow: + inset 0 1px 1px rgb(0 0 0 / 7.5%), + 0 0 1px rgb(102 175 233 / 60%); + -moz-box-shadow: + inset 0 1px 1px rgb(0 0 0 / 7.5%), + 0 0 1px rgb(102 175 233 / 60%); + box-shadow: + inset 0 1px 1px rgb(0 0 0 / 7.5%), + 0 0 1px rgb(102 175 233 / 60%); } .config .form-control:focus:invalid:focus { @@ -57,8 +72,8 @@ } .config .list { - padding : 0; - margin : 5%; + padding: 0; + margin: 5%; } .config .full-width { diff --git a/css/help.css b/css/help.css index 4260261856..6728c58925 100644 --- a/css/help.css +++ b/css/help.css @@ -1,12 +1,12 @@ .help { - margin : 25px; - white-space : normal; + margin: 25px; + white-space: normal; } .help li { - list-style-type : disc; + list-style-type: disc; } .help img { - max-width : 100%; + max-width: 100%; } diff --git a/css/icons.css b/css/icons.css index c657bc151b..ca4a118163 100644 --- a/css/icons.css +++ b/css/icons.css @@ -1,140 +1,144 @@ .icon-help::before { - font-family : 'Fontello'; - content : '\e801 '; + font-family: 'Fontello'; + content: '\e801 '; } .icon-rename::before { - font-family : 'Fontello'; - content : '\e802 '; + font-family: 'Fontello'; + content: '\e802 '; } .icon-view::before { - font-family : 'Fontello'; - content : '\e803 '; + font-family: 'Fontello'; + content: '\e803 '; } .icon-edit::before { - font-family : 'Fontello'; - content : '\e804 '; + font-family: 'Fontello'; + content: '\e804 '; } .icon-copy::before { - font-family : 'Fontello'; - content : '\e805 '; + font-family: 'Fontello'; + content: '\e805 '; } .icon-move::before { - font-family : 'Fontello'; - content : '\e806 '; + font-family: 'Fontello'; + content: '\e806 '; } -.icon-directory { - font-family : 'Fontello'; - content : '\e807 '; +.icon-directory::before { + font-family: 'Fontello'; + content: '\e807 '; } .icon-delete::before { - font-family : 'Fontello'; - content : '\e808 '; + font-family: 'Fontello'; + content: '\e808 '; } .icon-menu::before { - font-family : 'Fontello'; - content : '\e809 '; + font-family: 'Fontello'; + content: '\e809 '; } .icon-config::before { - font-family : 'Fontello'; - content : '\e80a '; + font-family: 'Fontello'; + content: '\e80a '; } .icon-console::before { - font-family : 'Fontello'; - content : '\e80b '; + font-family: 'Fontello'; + content: '\e80b '; } .icon-contact::before { - font-family : 'Fontello'; - content : '\e80c '; + font-family: 'Fontello'; + content: '\e80c '; } -.icon-file::before, { - font-family : 'Fontello'; - content : '\e80d '; +.icon-file::before { + font-family: 'Fontello'; + content: '\e80d '; } .icon-upload-to-cloud::before { - font-family : 'Fontello'; - content : '\e80e '; + font-family: 'Fontello'; + content: '\e80e '; } .icon-upload-from-cloud::before { - font-family : 'Fontello'; - content : '\e80f '; + font-family: 'Fontello'; + content: '\e80f '; } .icon-download::before { - font-family : 'Fontello'; - content : '\e810 '; + font-family: 'Fontello'; + content: '\e810 '; } .icon-new::before { - font-family : 'Fontello'; - content : '\e811 '; + font-family: 'Fontello'; + content: '\e811 '; +} + +.icon-toggle-file-selection::before { + font-family: 'Fontello'; + content: '\e81f '; } .icon-unselect-all::before { - font-family : 'Fontello'; - content : '\e812 '; + font-family: 'Fontello'; + content: '\e812 '; } .icon-pack::before { - font-family : 'Fontello'; - content : '\e813 '; + font-family: 'Fontello'; + content: '\e813 '; } .icon-extract::before { - font-family : 'Fontello'; - content : '\e814 '; + font-family: 'Fontello'; + content: '\e814 '; } .icon-copy-to-clipboard::before { - font-family : 'Fontello'; - content : '\e815 '; + font-family: 'Fontello'; + content: '\e815 '; } .icon-refresh::before { - font-family : 'Fontello'; - content : '\e816 '; + font-family: 'Fontello'; + content: '\e816 '; } .icon-cut::before { - font-family : 'Fontello'; - content : '\e817 '; + font-family: 'Fontello'; + content: '\e817 '; } .icon-paste::before { - font-family : 'Fontello'; - content : '\e818 '; + font-family: 'Fontello'; + content: '\e818 '; } .icon-upload::before { - font-family : 'Fontello'; - content : '\e819 '; + font-family: 'Fontello'; + content: '\e819 '; } .icon-log-out::before { - font-family : 'Fontello'; - content : '\e81a '; + font-family: 'Fontello'; + content: '\e81a '; } .icon-terminal::before { - font-family : 'Fontello'; - content : '\e81b '; + font-family: 'Fontello'; + content: '\e81b '; } .icon-user-menu::before { - font-family : 'Fontello'; - content : '\e81c '; + font-family: 'Fontello'; + content: '\e81c '; } - diff --git a/css/main.css b/css/main.css index 6008c2a9d2..6b901a8e56 100644 --- a/css/main.css +++ b/css/main.css @@ -1,8 +1,7 @@ -@import './urls.css'; -@import './reset.css'; -@import './style.css'; -@import './icons.css'; -@import './help.css'; -@import './query.css'; -@import './supports.css'; - +@import url(./reset.css); +@import url(./urls.css); +@import url(./style.css); +@import url(./icons.css); +@import url(./help.css); +@import url(./query.css); +@import url(./supports.css); diff --git a/css/nojs.css b/css/nojs.css index 1df4bd12fa..350ac4e665 100644 --- a/css/nojs.css +++ b/css/nojs.css @@ -1,4 +1,5 @@ -.path-icon, .keyspanel { +.path-icon, +.keyspanel { display: none; } diff --git a/css/query.css b/css/query.css index d4a4e15490..509f1b45d3 100644 --- a/css/query.css +++ b/css/query.css @@ -1,4 +1,4 @@ -@media only screen and (min-width: 1600px) { +@media only screen and (width >= 1600px) { .name { width: 40%; } @@ -20,7 +20,12 @@ } } -@media only screen and (max-height: 900px) and (max-width: 600px) { +:root { + --min-one-panel-width: 1155px; + --is-mobile: 0; +} + +@media only screen and (height <= 900px) and (width <= 600px) { .fm { height: 85%; } @@ -30,7 +35,7 @@ } } -@media only screen and (min-height: 550px) and (max-width: 600px) { +@media only screen and (height >= 550px) and (width <= 600px) { .fm { height: 80%; } @@ -40,43 +45,49 @@ } } -@media only screen and (max-height: 750px) and (max-width: 600px) { +@media only screen and (height <= 750px) and (width <= 600px) { .fm { height: 75%; } } -@media only screen and (max-height: 450px) and (max-width: 600px) { +@media only screen and (height <= 450px) and (width <= 600px) { .fm { height: 75%; } } -@media only screen and (max-height: 550px) and (max-width: 600px) { +@media only screen and (width <= 600px) { + :root { + --is-mobile: 1; + } +} + +@media only screen and (height <= 550px) and (width <= 600px) { .fm { height: 65%; } } -@media only screen and (max-height: 550px) and (max-width: 550px) { +@media only screen and (height <= 550px) and (width <= 550px) { .fm { height: 70%; } } -@media only screen and (min-height: 850px) and (min-width: 650px) { +@media only screen and (height >= 850px) and (width >= 650px) { .fm { height: 95%; } } -@media only screen and (max-height: 850px) { +@media only screen and (height <= 850px) { .files { height: 90%; } } -@media only screen and (max-height: 700px) and (min-width: 600px) { +@media only screen and (height <= 700px) and (width >= 600px) { .fm { height: 85%; } @@ -86,13 +97,13 @@ } } -@media only screen and (max-height: 450px) { +@media only screen and (height <= 450px) { .fm { height: 65%; } } -@media only screen and (max-height: 640px) and (max-width: 360px) { +@media only screen and (height <= 640px) and (width <= 360px) { .fm { height: 75%; } @@ -103,8 +114,8 @@ } /* iphone 6 landscape */ -@media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: landscape), - @media only screen and (max-height: 360px) and (max-width: 640px) { +@media only screen and (device-width >= 375px) and (device-width <= 667px) and (orientation: landscape), + only screen and (height <= 360px) and (width <= 640px) { .fm { height: 55%; } @@ -114,14 +125,14 @@ } } -@media only screen and (max-width: 600px) { +@media only screen and (width <= 600px) { .panel { font-size: 26px; } /* текущий файл под курсором */ .current-file { - background-color: var(--color-transparent); + background-color: var(--border-color); color: white; } /* делаем иконки под курсом белыми */ @@ -129,25 +140,30 @@ 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; + .file-link::before { + color: rgb(26 224 124 / 56%); + content: '\e81d'; } /* меняем иконки на шрифтовые */ .mini-icon { - color : rgb(246 224 124 / 56%); - font : 16px 'Fontello'; + color: rgb(246 224 124 / 56%); + font: 16px 'Fontello'; background-image: none; - padding : 1%; + padding: 1%; } - .size, .date, .owner, .mode { - display: none; + .size, + .date, + .owner, + .time, + .mode { + display: none !important; } .name { @@ -155,19 +171,26 @@ display: inline-block; } - .directory::before, .directory-link::before { + .directory::before { content: '\e807'; } - .file, .file-link { + .directory-link::before { + content: '\e81e'; + } + + .file, + .file-link { background-image: none; } - .archive, .archive-link { + .archive, + .archive-link { background-image: none; } - .archive::before, .archive-link { + .archive::before, + .archive-link { color: rgb(26 224 124 / 56%); content: '\e81d'; } @@ -183,24 +206,26 @@ } } -@media only screen and (min-width: 601px) and (max-width: 785px) { +@media only screen and (width >= 601px) and (width <= 785px) { .cmd-button { width: 13%; } } -@media only screen and (min-width: 786px) and (max-width: 1155px) { +@media only screen and (width >= 786px) and (width <= 1155px) { .cmd-button { width: 10%; } } -@media only screen and (max-width: 1155px) { +@media only screen and (width <= 1155px) { .panel { width: 98%; } /* если правая панель не помещаеться - прячем её */ - .panel-right, .cmd-button#f5, .cmd-button#f6 { + .panel-right, + .cmd-button#f5, + .cmd-button#f6 { display: none; } } @@ -213,7 +238,8 @@ border: none; } - .keyspanel, .panel-right { + .keyspanel, + .panel-right { display: none; } diff --git a/css/style.css b/css/style.css index 8044b300ed..10820444c1 100644 --- a/css/style.css +++ b/css/style.css @@ -1,22 +1,19 @@ -:root { - --color: rgb(49 123 249); - --color-transparent: rgb(49 123 249 / 40%); -} - html { - height : 94%; + height: 94%; } body { - width : 100%; - height : 95%; - overflow : hidden; - background-color : white; + width: 100%; + height: 95%; + overflow: hidden; + background-color: var(--background); } -body, pre, code { - font-family : 'Droid Sans Mono', 'Ubuntu Mono', 'Consolas', monospace; - font-size : 16px; +body, +pre, +code { + font-family: 'Droid Sans Mono', 'Ubuntu Mono', 'Consolas', monospace; + font-size: 16px; } .hidden { @@ -27,31 +24,34 @@ body, pre, code { display: none; } -.fm, .keyspanel { - cursor : default; +.fm, +.keyspanel { + cursor: default; -webkit-tap-highlight-color: rgb(0 0 0 / 0%); - -webkit-user-select : none; - -moz-user-select : none; - -ms-user-select : none; - -o-user-select : none; - user-select : none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; } .links { - -webkit-user-select : initial; - -moz-user-select : initial; - -ms-user-select : initial; - -o-user-select : initial; - user-select : text; + -webkit-user-select: initial; + -moz-user-select: initial; + -ms-user-select: initial; + -o-user-select: initial; + user-select: text; + color: var(--column-color); } -.panel, .cmd-button { +.panel, +.cmd-button { border: 1px solid; - border-color: var(--color-transparent); + border-color: var(--border-color); } .icon { - margin-left : 0.5%; + margin-left: 0.5%; cursor: default; } @@ -61,7 +61,7 @@ body, pre, code { .path-icon { position: relative; - color: #222; + color: var(--icon-color); } .path-icon:active { @@ -69,41 +69,41 @@ body, pre, code { } .path-icon:hover { - color : #06e; + color: #06e; cursor: pointer; } .error::before { - font-family : 'Fontello'; - font-size : 14px; - color : rgb(222 41 41); - cursor : default; - content : '\e800'; + font-family: 'Fontello'; + font-size: 14px; + color: rgb(222 41 41); + cursor: default; + content: '\e800'; } .loading { - position : relative; - display : inline-block; - width : 16px; - height : 16px; - vertical-align : middle; + position: relative; + display: inline-block; + width: 16px; + height: 16px; + vertical-align: middle; } .loading::after { - position : relative; - bottom : 5px; - left : 16px; - font-size : 10px; - color : var(--color); - content : attr(data-progress); + position: relative; + bottom: 5px; + left: 16px; + font-size: 10px; + color: var(--link-color); + content: attr(data-progress); } .cmd-button { width: 5%; height: 30px; margin: 20px 2px 0; - color: #222; - background-color: white; + color: var(--icon-color); + background-color: var(--button-background); transition: ease 0.1s; } @@ -114,7 +114,7 @@ body, pre, code { .cmd-button:active { color: white; - background-color: var(--color); + background-color: var(--link-color); transition: ease 0.1s; } @@ -126,7 +126,8 @@ a { text-decoration: none; } -a:hover, a:active { +a:hover, +a:active { color: #06e; text-decoration: none; } @@ -150,13 +151,14 @@ a:hover, a:active { } .fm { - width : 98%; + width: 98%; height: 90%; margin: 26px auto 0; } .fm-header { font-weight: bold; + color: var(--column-color); } .panel-left { @@ -164,14 +166,29 @@ a:hover, a:active { } .current-file { - box-shadow: 0 0 0 1px var(--color-transparent) inset; + box-shadow: 0 0 0 1px var(--border-color) inset; } .cut-file { opacity: 0.7; } -.selected-file, .selected-file .name > a { +.name { + float: left; + width: 26%; +} + +.name a { + color: var(--link-color); +} + +.name a:hover { + cursor: default; +} + +.selected-file, +.selected-file > span, +.selected-file .name > a { color: rgb(254 159 224); } @@ -197,47 +214,48 @@ a:hover, a:active { text-align: center; } -.name { - float: left; - width: 26%; -} - -.name a:hover { - cursor: default; -} - .size { float: left; width: 12%; margin-right: 27px; text-align: right; + color: var(--column-color); } .date { float: left; width: 19%; + color: var(--column-color); +} + +.time { + color: var(--column-color); + display: none; } .owner { - display : inline-block; - width : 13%; + display: inline-block; + width: 12%; + /* when inline-block * vertical align should be * set top to prevent additional * spaces behind lines */ - vertical-align : top; + vertical-align: top; + color: var(--column-color); } .mode { float: right; - width: 18%; + width: 22%; + color: var(--column-color); } .reduce-text { - overflow : hidden; - text-overflow : ellipsis; - white-space : nowrap; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .files { @@ -252,4 +270,3 @@ a:hover, a:active { .files li { overflow: hidden; } - diff --git a/css/supports.css b/css/supports.css index bf470c2d63..ea2084d26f 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/css/terminal.css b/css/terminal.css index 20cf94b4b6..e7b1fea612 100644 --- a/css/terminal.css +++ b/css/terminal.css @@ -1,4 +1,3 @@ .terminal { height: 100%; } - diff --git a/css/themes/dark.css b/css/themes/dark.css new file mode 100644 index 0000000000..311a809355 --- /dev/null +++ b/css/themes/dark.css @@ -0,0 +1,49 @@ +:root { + --link-color: #317bf9; + --border-color: rgb(49 123 249 / 40%); + --background: #22272e; + --column-color: #727e8c; + --icon-color: #478be6; + --button-background: #22272e; + --internal-background: #373e47; +} + +.view { + background: var(--internal-background) !important; + color: var(--column-color) !important; +} +.view a { + color: var(--link-color) !important; +} + +.smalltalk .page, +.smalltalk header, +.smalltalk .button-strip button, +.smalltalk input { + background: var(--internal-background) !important; + color: var(--link-color) !important; + text-shadow: none !important; +} + +.cloudcmd-user-menu, +.cloudcmd-user-menu-button { + background: var(--internal-background) !important; + color: var(--link-color) !important; +} + +.jqconsole { + background: #373e47 !important; +} + +.jqconsole-prompt { + color: var(--column-color) !important; +} + +.log-msg { + color: var(--column-color) !important; +} + +.menu { + color: var(--link-color) !important; + background: var(--internal-background) !important; +} diff --git a/css/themes/light.css b/css/themes/light.css new file mode 100644 index 0000000000..e95eb4419f --- /dev/null +++ b/css/themes/light.css @@ -0,0 +1,9 @@ +:root { + --link-color: blue; + --selected-menu-item-color: #317bf9; + --border-color: rgb(49 123 249 / 40%); + --background: white; + --column-color: black; + --icon-color: #222; + --button-background: white; +} diff --git a/css/urls.css b/css/urls.css index 7291d6a9d1..be90e2350e 100644 --- a/css/urls.css +++ b/css/urls.css @@ -17,20 +17,23 @@ } @font-face { - font-family : 'Droid Sans Mono'; - src : url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot); - src : + font-family: 'Droid Sans Mono'; + src: url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot); + src: local('Droid Sans Mono'), local('DroidSansMono'), url(../font/DroidSansMono.eot) format('embedded-opentype'), - url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot?#iefix) format('embedded-opentype'), - url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot) format('embedded-opentype'), + url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot?#iefix) + format('embedded-opentype'), + url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot) + format('embedded-opentype'), url(../font/DroidSansMono.woff2) format('woff2'), url(../font/DroidSansMono.woff) format('woff'), - url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJUYuTAAIFFn5GTWtryCmBQ4.woff) format('woff'), + url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJUYuTAAIFFn5GTWtryCmBQ4.woff) + format('woff'), local('Consolas'); - font-style : normal; - font-weight : 400; + font-style: normal; + font-weight: 400; } .directory { @@ -64,4 +67,3 @@ .loading-gif { background: url(../img/spinner.gif); } - diff --git a/css/user-menu.css b/css/user-menu.css index 7ce0e97167..792747f476 100644 --- a/css/user-menu.css +++ b/css/user-menu.css @@ -1,6 +1,7 @@ .cloudcmd-user-menu { font-size: 16px; font-family: 'Droid Sans Mono', 'Ubuntu Mono', 'Consolas', monospace; + border: 0; } .cloudcmd-user-menu:focus { @@ -8,7 +9,7 @@ } .cloudcmd-user-menu > option:checked { - box-shadow: 20px -20px 0 2px var(--color) inset; + box-shadow: 20px -20px 0 2px var(--selected-menu-item-color) inset; } .cloudcmd-user-menu-button { @@ -20,4 +21,3 @@ border: 0; overflow: auto; } - diff --git a/css/view.css b/css/view.css index 61463ca63a..b6fb20ed9b 100644 --- a/css/view.css +++ b/css/view.css @@ -15,11 +15,12 @@ } .view-overlay { - display : block; - background : rgb(255 255 255 / 10%); + display: block; + background: rgb(255 255 255 / 10%); } -.media, video { +.media, +video { width: 100%; } diff --git a/cssnano.config.js b/cssnano.config.js deleted file mode 100644 index 9598da4123..0000000000 --- a/cssnano.config.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -// used by OptimizeCssAssetsPlugin - -const defaultPreset = require('cssnano-preset-default'); - -module.exports = defaultPreset({ - svgo: { - plugins: [{ - convertPathData: false, - }, { - convertShapeToPath: false, - }], - }, -}); - diff --git a/deno.json b/deno.json new file mode 100644 index 0000000000..64c1bde21a --- /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-compose.yml b/docker-compose.yml index e358cb83ab..0fb72a342b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '2' +version: "2" services: web: ports: @@ -7,4 +7,3 @@ services: - ~:/root - /:/mnt/fs image: coderaiser/cloudcmd - diff --git a/docker/Dockerfile b/docker/Dockerfile index b8433ab077..7c5d7b643b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,25 +1,29 @@ -FROM node:lts-buster +FROM node + LABEL maintainer="Coderaiser" +LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" + +RUN mkdir -p /usr/src/cloudcmd -RUN mkdir -p /usr/src/app -WORKDIR /usr/src/app +WORKDIR /usr/src/cloudcmd -COPY package.json /usr/src/app/ +COPY package.json /usr/src/cloudcmd/ -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 && \ + ln -s /root/.bun/bin/bun /usr/local/bin/bun && \ + chmod +x /usr/local/bin/bun && \ + bun r gritty --omit dev && \ + bun i gritty --omit dev && \ + bun pm cache rm -COPY . /usr/src/app +COPY . /usr/src/cloudcmd 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 -ENTRYPOINT ["/usr/src/app/bin/cloudcmd.mjs"] - +ENTRYPOINT ["/usr/src/cloudcmd/bin/cloudcmd.js"] diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index b00d3a21e9..5e38ab8a5f 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -1,29 +1,34 @@ -FROM node:lts-buster-slim +FROM node:alpine + LABEL maintainer="Coderaiser" +LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" + +RUN mkdir -p /usr/src/cloudcmd -RUN mkdir -p /usr/src/app -WORKDIR /usr/src/app +WORKDIR /usr/src/cloudcmd -COPY package.json /usr/src/app/ +COPY package.json /usr/src/cloudcmd/ -RUN npm config set package-lock false && \ - npm install --production && \ - apt update && \ - apt install -y make g++ python3 && \ - npm i gritty && \ - npm cache clean --force && \ - apt remove -y make g++ python3 && \ - rm -rf /usr/include /tmp/* /var/cache/apt/* +RUN apk update && \ + apk add --no-cache curl bash make g++ python3 && \ + curl -fsSL https://bun.sh/install | bash && \ + ln -s ~/.bun/bin/bun /usr/local/bin/bun && \ + chmod +x /usr/local/bin/bun && \ + bun r gritty --omit dev && \ + bun i gritty --omit dev && \ + bun pm cache rm && \ + apk del make g++ python3 && \ + rm -rf /usr/include /tmp/* /var/cache/apk/* -COPY . /usr/src/app +COPY . /usr/src/cloudcmd 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 +ENV cloudcmd_vim=true EXPOSE 8000 -ENTRYPOINT ["/usr/src/app/bin/cloudcmd.mjs"] - +ENTRYPOINT ["/usr/src/cloudcmd/bin/cloudcmd.js"] diff --git a/docker/Dockerfile.io b/docker/Dockerfile.io new file mode 100644 index 0000000000..d5a244596a --- /dev/null +++ b/docker/Dockerfile.io @@ -0,0 +1,113 @@ +FROM ubuntu:resolute + +LABEL maintainer="Coderaiser" +LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" + +RUN mkdir -p /usr/local/share/cloudcmd + +WORKDIR /usr/local/share/cloudcmd + +COPY package.json /usr/local/share/cloudcmd/ + +ENV DEBIAN_FRONTEND=noninteractive \ + NVM_DIR=/usr/local/share/nvm \ + npm_config_cache=/tmp/npm-cache \ + GOPATH=/usr/local/share/go \ + PATH=/usr/local/share/bun/bin:$PATH \ + BUN_INSTALL=/usr/local/share/bun \ + NPM_CONFIG_CACHE=/tmp/.npm \ + NPM_CONFIG_PREFIX=/usr/local \ + NPM_CONFIG_PACKAGE_LOCK=false \ + PALABRA_DIR=/usr/local/share \ + XDG_CONFIG_HOME=/usr/local/etc + +ARG UBUNTU_DEPS="libatomic1 curl wget git net-tools iproute2 software-properties-common" +ARG RUST_DEPS="build-essential" +ARG DEPS="pv gcc gdb strace upx-ucl less ffmpeg net-tools netcat-openbsd mc far2l iputils-ping vim bat fzf locales sudo command-not-found ncdu aptitude htop btop hexyl tmux" +ARG PALABRA_DEPS="nvm node rust go deno fasm nvchad rizin yara gdu f4 typos shellcheck gh" +ARG BUN_DEPS="palabra wisdom nupdate version-io redrun superc8 supertape madrun redlint putout renamify-cli runny redfork cline" + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get autoremove && \ + apt-get install -y ${UBUNTU_DEPS} ${RUST_DEPES} ${DEPS} && \ + echo "> Install git" && \ + add-apt-repository ppa:git-core/ppa -y && \ + echo "> Update command-not-found database. Run 'sudo apt update' to populate it." && \ + apt-get update && \ + apt-get upgrade -y && \ + apt-get autoremove && \ + apt-get clean && \ + echo "> create user" && \ + useradd -m -s /bin/bash -u 1337 instalador && \ + chown -R instalador /usr/local && \ + chown -R instalador /tmp + +USER instalador + +RUN echo "> install bun" && \ + curl https://bun.sh/install | bash && \ + echo "> install npm globals" && \ + bun i ${BUN_DEPS} -g && \ + echo "> install rust go deno bun fasm nvim" && \ + bun ${BUN_INSTALL}/bin/palabra i ${PALABRA_DEPS} && \ + echo "> install node" && \ + . $NVM_DIR/nvm.sh + +USER root + +RUN echo "> remove user" && \ + userdel -r instalador && \ + echo "> install gritty" && \ + bun r gritty --omit dev && \ + bun i gritty --omit dev && \ + bun pm cache rm && \ + echo "> setup cloudcmd" && \ + ln -s /usr/local/share/cloudcmd/bin/cloudcmd.js /usr/local/bin/cloudcmd && \ + echo "> setup git" && \ + git config --global core.whitespace -trailing-space && \ + git config --global pull.rebase true && \ + git config --global init.defaultBranch master && \ + echo "> configure bash" && \ + echo "alias ls='ls --color=auto'" >> /etc/bash.bashrc && \ + echo "alias buni='bun i --no-save'" >> /etc/bash.bashrc && \ + echo "alias bat='batcat'" >> /etc/bash.bashrc && \ + echo ". /usr/local/share/nvm/nvm.sh" >> /etc/bash.bashrc && \ + echo ". /usr/share/bash-completion/completions/git" >> /etc/bash.bashrc && \ + echo 'PS1="\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ "' >> /etc/bash.bashrc && \ + echo "> setup inputrc" && \ + echo "set editing-mode vi" >> /etc/inputrc && \ + echo "TAB: menu-complete" >> /etc/inputrc && \ + echo "set UTF-8" && \ + echo " > configure languages" && \ + echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \ + echo "ru_RU.UTF-8 UTF-8" >> /etc/locale.gen && \ + echo "uk_UA.UTF-8 UTF-8" >> /etc/locale.gen && \ + echo "es_ES.UTF-8 UTF-8" >> /etc/locale.gen && \ + echo "ja_JP.UTF-8 UTF-8" >> /etc/locale.gen && \ + echo "el_GR.UTF-8 UTF-8" >> /etc/locale.gen && \ + locale-gen + +COPY . /usr/local/share/cloudcmd + +WORKDIR / + +ENV cloudcmd_terminal=true \ + cloudcmd_terminal_path=gritty \ + cloudcmd_vim=true \ + cloudcmd_open=false \ + PATH=node_modules/.bin:$PATH \ + PATH=~/.local/bin:$PATH \ + BUN_INSTALL_CACHE_DIR=/tmp/bun-cache \ + DENO_DIR=/tmp/deno-cache \ + LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 \ + TERM=xterm-256color \ + XDG_CACHE_HOME=/tmp \ + XDG_DATA_HOME=/usr/local/share \ + XDG_CONFIG_HOME=~/.config + +EXPOSE 8000 + +ENTRYPOINT ["/usr/local/share/cloudcmd/bin/cloudcmd.js"] diff --git a/docker/Dockerfile.slim b/docker/Dockerfile.slim new file mode 100644 index 0000000000..c6f273b8f7 --- /dev/null +++ b/docker/Dockerfile.slim @@ -0,0 +1,39 @@ +FROM node:slim AS build + +RUN mkdir -p /usr/src/cloudcmd/ + +WORKDIR /usr/src/cloudcmd + +COPY package.json /usr/src/cloudcmd/ + +RUN apt-get update && \ + apt-get install -y build-essential python3 libncurses5-dev pkg-config && \ + apt-get install -y --no-install-recommends curl ca-certificates unzip && \ + curl -fsSL https://bun.sh/install | bash && \ + ln -s ~/.bun/bin/bun /usr/local/bin/bun && \ + chmod +x /usr/local/bin/bun && \ + bun r gritty --omit dev && \ + bun i gritty --omit dev && \ + ~/.bun/bin/bun pm cache rm && \ + rm -rf /var/lib/apt/lists/* + +COPY . /usr/src/cloudcmd + +FROM node:slim AS runtime + +COPY --from=build /usr/src/cloudcmd /usr/src/cloudcmd +COPY --from=build /root/.bun /root/.bun + +LABEL maintainer="Coderaiser" +LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" + +WORKDIR / + +ENV cloudcmd_terminal=true +ENV cloudcmd_terminal_path=gritty +ENV cloudcmd_open=false +ENV PATH="/root/.bun/bin:$PATH" + +EXPOSE 8000 + +ENTRYPOINT ["/usr/src/cloudcmd/bin/cloudcmd.js"] diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000000..22add654de --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,31 @@ +import {safeAlign} from 'eslint-plugin-putout'; +import {defineConfig} from 'eslint/config'; +import globals from 'globals'; +import {matchToFlat} from '@putout/eslint-flat'; + +export const match = { + 'bin/release.js': { + 'no-console': 'off', + 'n/hashbang': 'off', + }, + 'client/dom/index.*': { + 'no-multi-spaces': 'off', + }, + 'client/**': { + 'n/no-unsupported-features/node-builtins': 'off', + }, +}; +export default defineConfig([ + safeAlign, { + ignores: ['**/fixture'], + rules: { + 'key-spacing': 'off', + }, + }, { + files: ['{client,common,static}/**/*.js'], + languageOptions: { + globals: globals.browser, + }, + }, + ...matchToFlat(match), +]); diff --git a/font/fontello.eot b/font/fontello.eot index c6b5875af9..e144bd820a 100644 Binary files a/font/fontello.eot and b/font/fontello.eot differ diff --git a/font/fontello.json b/font/fontello.json index 354b93cc00..c8bad6be46 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 4564f98197..ff664681f6 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 4fc26184e1..bfec5c432c 100644 Binary files a/font/fontello.ttf and b/font/fontello.ttf differ diff --git a/font/fontello.woff b/font/fontello.woff index 16fa9d7120..a23a854da4 100644 Binary files a/font/fontello.woff and b/font/fontello.woff differ diff --git a/font/fontello.woff2 b/font/fontello.woff2 index 8b7b62f023..f0210ad61b 100644 Binary files a/font/fontello.woff2 and b/font/fontello.woff2 differ diff --git a/html/index.html b/html/index.html index 00ae977e2b..d366ed1cb4 100644 --- a/html/index.html +++ b/html/index.html @@ -8,16 +8,19 @@ - + + -
    {{ fm }}
    +
    {{ fm }}
    @@ -38,5 +41,15 @@ + diff --git a/img/favicon/favicon-256.png b/img/favicon/favicon-256.png new file mode 100644 index 0000000000..4e01d43cbc Binary files /dev/null and b/img/favicon/favicon-256.png differ diff --git a/json/config.json b/json/config.json index 7387e15881..3bbb65f43e 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, @@ -23,6 +24,7 @@ "confirmMove": true, "configDialog": true, "configAuth": true, + "configPort": true, "oneFilePanel": false, "console": true, "syncConsolePath": false, @@ -30,10 +32,12 @@ "terminalPath": "", "terminalCommand": "", "terminalAutoRestart": true, + "showDotFiles": true, "showConfig": false, "showFileName": false, "vim": false, "columns": "name-size-date-owner-mode", + "theme": "light", "export": false, "exportToken": "root", "import": false, diff --git a/json/help.json b/json/help.json index 8efa9e60ac..44f792489b 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,10 +20,12 @@ "--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", "--config-auth ": "enable auth change in config dialog", + "--config-port ": "enable port change in config dialog", "--console ": "enable console", "--sync-console-path ": "sync console path", "--contact ": "enable contact", @@ -32,6 +35,7 @@ "--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", @@ -53,6 +57,7 @@ "--no-confirm-move ": "do not confirm move", "--no-config-dialog ": "disable config dialog", "--no-config-auth ": "disable auth change in config dialog", + "--no-config-port ": "disable port change in config dialog", "--no-console ": "disable console", "--no-sync-console-path ": "do not sync console path", "--no-contact ": "disable contact", @@ -64,6 +69,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/json/modules.json b/json/modules.json index 17691e0bc1..6ed586d144 100644 --- a/json/modules.json +++ b/json/modules.json @@ -11,6 +11,7 @@ "markdown", "config", "contact", + "command-line", "upload", "operation", "konsole", diff --git a/man/cloudcmd.1 b/man/cloudcmd.1 index 781e01e1bd..dab36a2cff 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" + --editor set editor: "dword", "edward", "deepword" or "qword" + --menu set menu: "supermenu" or "aleman" --packer set packer: "tar" or "zip" --root set root directory --prefix set url prefix @@ -47,6 +49,7 @@ programs in browser from any computer, mobile or tablet device. --contact enable contact --config-dialog enable config dialog --config-auth enable auth change in config dialog + --config-port enable port change in config dialog --console enable console --sync-console-path sync console path --terminal enable terminal @@ -55,6 +58,7 @@ programs in browser from any computer, mobile or tablet device. --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 @@ -65,6 +69,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 @@ -77,6 +82,7 @@ programs in browser from any computer, mobile or tablet device. --no-contact disable contact --no-config-dialog disable config dialog --no-config-auth disable auth change in config dialog + --no-config-port disable port change in config dialog --no-console disable console --no-sync-console-path do not sync console path --no-terminal disable terminal diff --git a/manifest.yml b/manifest.yml index 97b2810f48..c10c360d8e 100644 --- a/manifest.yml +++ b/manifest.yml @@ -1,4 +1,3 @@ ---- applications: .: name: cloudcmd @@ -7,7 +6,7 @@ applications: info: mem: 512M description: Node.js Application - exec: + exec: null url: ${name}.${target-base} mem: 128M instances: 2 diff --git a/package.json b/package.json index fa7d245f7b..a6b65f6f39 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,16 @@ { "name": "cloudcmd", - "version": "15.9.13", - "type": "commonjs", + "version": "19.19.0", + "type": "module", "author": "coderaiser (https://github.com/coderaiser)", "description": "File manager for the web with console and editor", "homepage": "http://cloudcmd.io", + "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.js", "keywords": [ "console", "terminal", @@ -37,7 +39,7 @@ "server" ], "bin": { - "cloudcmd": "bin/cloudcmd.mjs" + "cloudcmd": "bin/cloudcmd.js" }, "scripts": { "start": "madrun start", @@ -46,13 +48,14 @@ "build:start:dev": "madrun build:start:dev", "lint:all": "madrun lint:all", "lint": "madrun lint", + "lint:progress": "madrun lint:progress", "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", + "test:e2e": "madrun test:e2e", "test:client": "madrun test:client", "test:server": "madrun test:server", "wisdom": "madrun wisdom", @@ -70,7 +73,9 @@ "watch:test:client": "madrun watch:test:client", "watch:test:server": "madrun watch:test:server", "watch:coverage": "madrun watch:coverage", + "watch:fix:lint": "madrun watch:fix:lint", "build": "madrun build", + "build:dev": "madrun build:dev", "build:client": "madrun build:client", "build:client:dev": "madrun build:client:dev", "heroku-postbuild": "madrun heroku-postbuild" @@ -80,136 +85,162 @@ }, "subdomain": "cloudcmd", "dependencies": { - "@cloudcmd/dropbox": "^4.0.1", - "@cloudcmd/fileop": "^5.0.0", - "@cloudcmd/move-files": "^5.0.0", + "@cloudcmd/dropbox": "^5.0.1", + "@cloudcmd/fileop": "^9.0.7", + "@cloudcmd/move-files": "^8.0.0", "@cloudcmd/read-files-sync": "^2.0.0", - "@putout/cli-validate-args": "^1.0.1", - "@putout/plugin-cloudcmd": "^1.2.0", + "@putout/cli-validate-args": "^2.0.0", + "@putout/plugin-cloudcmd": "^5.2.0", + "aleman": "^2.0.1", "apart": "^2.0.0", - "chalk": "^4.0.0", + "chalk": "^5.3.0", "compression": "^1.7.4", - "console-io": "^13.0.0", - "copymitter": "^6.0.0", + "console-io": "^15.0.1", + "copymitter": "^10.3.0", "criton": "^2.0.0", "currify": "^4.0.0", "deepmerge": "^4.0.0", - "deepword": "^8.0.0", - "dword": "^13.0.0", - "edward": "^13.0.0", + "deepword": "^11.0.0", + "dword": "^16.0.0", + "edward": "^16.0.0", "es6-promisify": "^7.0.0", "execon": "^1.2.0", - "express": "^4.13.0", + "express": "^5.1.0", + "express-rate-limit": "^8.5.2", "files-io": "^4.0.0", - "find-up": "^6.1.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", - "inly": "^4.0.0", + "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", - "markdown-it": "^12.0.0", + "just-snake-case": "^3.2.0", + "markdown-it": "^14.0.0", "mellow": "^3.0.0", + "mime-types": "^3.0.1", + "montag": "^2.0.1", + "nano-memoize": "^3.0.16", "nomine": "^4.0.0", - "object.omit": "^3.0.0", "once": "^1.4.0", - "onezip": "^5.0.0", - "open": "^8.0.5", - "package-json": "^7.0.0", - "ponse": "^6.0.0", - "pullout": "^4.0.0", - "putout": "^23.0.0", - "redzip": "^2.0.0", - "rendy": "^3.0.0", - "restafary": "^10.0.0", - "restbox": "^3.0.0", + "onezip": "^7.0.0", + "open": "^11.0.0", + "package-json": "^10.0.0", + "pipe-io": "^4.0.1", + "ponse": "^8.0.0", + "pullout": "^5.0.0", + "putout": "^42.0.5", + "qword": "^1.0.0", + "redzip": "^4.6.1", + "rendy": "^5.0.0", + "restafary": "^13.0.1", + "restbox": "^4.0.0", "shortdate": "^2.0.0", - "simport": "^1.0.1", "socket.io": "^4.0.0", "socket.io-client": "^4.0.1", "squad": "^3.0.0", "table": "^6.0.1", - "thread-it": "^1.1.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": "^6.0.0", + "win32": "^8.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.5.5", - "@babel/core": "^7.0.0", - "@babel/plugin-proposal-optional-chaining": "^7.12.7", + "@babel/code-frame": "^7.22.5", + "@babel/core": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.21.0", "@babel/preset-env": "^7.0.0", "@cloudcmd/clipboard": "^2.0.0", "@cloudcmd/create-element": "^2.0.0", - "@cloudcmd/modal": "^2.0.0", + "@cloudcmd/modal": "^4.0.0", "@cloudcmd/olark": "^3.0.2", - "@cloudcmd/stub": "^3.0.0", - "auto-globals": "^2.0.0", - "babel-loader": "^8.0.0", + "@cloudcmd/stub": "^5.0.0", + "@iocmd/wait": "^2.1.0", + "@putout/eslint-flat": "^4.0.0", + "@supertape/loader-css": "^1.0.0", + "@types/node-fetch": "^2.6.11", + "auto-globals": "^4.0.0", + "babel-loader": "^10.0.0", "babel-plugin-macros": "^3.0.0", - "c8": "^7.5.0", "cheerio": "^1.0.0-rc.5", - "clean-css-loader": "^2.0.0", + "clean-css-loader": "^4.2.1", "codegen.macro": "^4.0.0", - "coveralls": "^3.0.0", - "css-loader": "^3.0.0", + "css-loader": "^7.1.2", + "css-minimizer-webpack-plugin": "^8.0.0", "css-modules-require-hook": "^4.2.3", + "cssnano-preset-default": "^8.0.1", "domtokenlist-shim": "^1.2.0", "emitify": "^4.0.1", - "eslint": "^8.0.1", - "eslint-plugin-node": "^11.0.0", - "eslint-plugin-putout": "^12.2.0", - "extract-text-webpack-plugin": "^4.0.0-alpha.0", - "gritty": "^6.0.0", + "eslint": "^10.0.0", + "eslint-plugin-putout": "^31.0.0", + "globals": "^17.0.0", + "gritty": "^10.2.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": "^8.6.0", - "memfs": "^3.0.1", + "madrun": "^13.0.0", + "memfs": "^4.2.0", + "mini-css-extract-plugin": "^2.9.2", "minor": "^1.2.2", - "mock-require": "^3.0.1", "morgan": "^1.6.1", - "multi-rename": "^2.0.0", - "nodemon": "^2.0.1", - "optimize-css-assets-webpack-plugin": "^5.0.0", - "philip": "^2.0.0", + "multi-rename": "^3.0.0", + "nodemon": "^3.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": "^6.0.0", "request": "^2.76.0", - "rimraf": "^3.0.0", - "scroll-into-view-if-needed": "^2.2.5", - "serve-once": "^2.0.0", - "serviceworker-webpack-plugin": "^1.0.1", - "smalltalk": "^4.0.0", - "style-loader": "^2.0.0", - "supermenu": "^4.0.1", - "supertape": "^6.9.1", - "tar-stream": "^2.0.0", + "rimraf": "^6.0.1", + "scroll-into-view-if-needed": "^3.0.4", + "serve-once": "^3.0.1", + "smalltalk": "^5.0.0", + "style-loader": "^4.0.0", + "superc8": "^12.6.0", + "supermenu": "^5.0.0", + "supertape": "^13.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", - "yaspeller": "^8.0.0" + "util": "^0.12.5", + "webpack": "^5.99.9", + "webpack-cli": "^7.0.2", + "webpack-merge": "^6.0.1", + "webpackbar": "^7.0.0" + }, + "imports": { + "#css/": "./css/", + "#dom": "./client/dom/index.js", + "#dom/events": "./client/dom/events/index.js", + "#dom/load": "./client/dom/load.js", + "#dom/dialog": "./client/dom/dialog.js", + "#dom/images": "./client/dom/images.js", + "#dom/files": "./client/dom/files.js", + "#dom/upload-files": "./client/dom/upload-files.js", + "#dom/storage": "./client/dom/storage.js", + "#dom/rest": "./client/dom/rest.js", + "#common/util": "./common/util.js", + "#common/omit": "./common/omit.js", + "#common/cloudfunc": "./common/cloudfunc.js", + "#common/entity": "./common/entity.js", + "#server/cloudcmd": "./server/cloudcmd.js" }, "engines": { - "node": ">=14" + "node": ">=22" }, "license": "MIT", - "main": "server/cloudcmd.js", "publishConfig": { "access": "public" } diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000000..1f53798bb4 --- /dev/null +++ b/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/rules/split-autoglobals-with-tape.js b/rules/split-autoglobals-with-tape.js new file mode 100644 index 0000000000..5c3c3eb24b --- /dev/null +++ b/rules/split-autoglobals-with-tape.js @@ -0,0 +1,8 @@ +export const report = () => `Use 'if condition' instead of 'ternary expression'`; + +export const replace = () => ({ + 'const test = autoGlobals(require("supertape"))': `{ + const {test: tape} = require('supertape'); + const test = autoGlobals(tape); + }`, +}); diff --git a/server/auth.js b/server/auth.js index 69fc85eeaf..09da0c2a9f 100644 --- a/server/auth.js +++ b/server/auth.js @@ -1,12 +1,11 @@ -'use strict'; +import httpAuth from 'http-auth'; +import criton from 'criton'; +import currify from 'currify'; -const httpAuth = require('http-auth'); -const criton = require('criton'); -const currify = require('currify'); const middle = currify(_middle); const check = currify(_check); -module.exports = (config) => { +export default (config) => { const auth = httpAuth.basic({ realm: 'Cloud Commander', }, check(config)); @@ -16,11 +15,13 @@ 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(/* success */); + const success = () => next(); + return authentication.check(success)(req, res); } @@ -38,4 +39,3 @@ function _check(config, username, password, callback) { callback(sameName && samePass); } - diff --git a/server/cloudcmd.js b/server/cloudcmd.js index 527fc63383..2e75152056 100644 --- a/server/cloudcmd.js +++ b/server/cloudcmd.js @@ -1,45 +1,44 @@ -'use strict'; +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 currify from 'currify'; +import apart from 'apart'; +import * as ponse from 'ponse'; +import {restafary} from 'restafary'; +import restbox from 'restbox'; +import {konsole} from 'console-io'; +import {edward} from 'edward'; +import {dword} from 'dword'; +import {deepword} from 'deepword'; +import {qword} from 'qword'; +import nomine from 'nomine'; +import {fileop} from '@cloudcmd/fileop'; +import * as cloudfunc from '#common/cloudfunc'; +import authentication from './auth.js'; +import {createConfig, configPath} from './config.js'; +import modulas from './modulas.js'; +import {userMenu} from './user-menu.js'; +import rest from './rest/index.js'; +import route from './route.js'; +import * as validate from './validate.js'; +import prefixer from './prefixer.js'; +import terminal from './terminal.js'; +import {distributeExport} from './distribute/export.js'; +import {createDepStore} from './depstore.js'; -const DIR = __dirname + '/'; -const DIR_ROOT = DIR + '../'; -const DIR_COMMON = DIR + '../common/'; - -const path = require('path'); -const fs = require('fs'); - -const cloudfunc = require(DIR_COMMON + 'cloudfunc'); -const authentication = require(DIR + 'auth'); -const { - createConfig, - configPath, -} = require(DIR + 'config'); - -const modulas = require(DIR + 'modulas'); -const userMenu = require(DIR + 'user-menu'); -const rest = require(DIR + 'rest'); -const route = require(DIR + 'route'); -const validate = require(DIR + 'validate'); -const prefixer = require(DIR + 'prefixer'); -const terminal = require(DIR + 'terminal'); -const distribute = require(DIR + 'distribute'); - -const currify = require('currify'); -const apart = require('apart'); -const ponse = require('ponse'); -const restafary = require('restafary'); -const restbox = require('restbox'); -const konsole = require('console-io'); -const edward = require('edward'); -const dword = require('dword'); -const deepword = require('deepword'); -const nomine = require('nomine'); -const fileop = require('@cloudcmd/fileop'); - -const isDev = process.env.NODE_ENV === 'development'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const {assign} = Object; +const DIR = `${__dirname}/`; +const DIR_ROOT = join(DIR, '..'); const getDist = (isDev) => isDev ? 'dist-dev' : 'dist'; +const isDev = fullstore(process.env.NODE_ENV === 'development'); + const getIndexPath = (isDev) => path.join(DIR, '..', `${getDist(isDev)}/index.html`); -const html = fs.readFileSync(getIndexPath(isDev), 'utf8'); +const html = fs.readFileSync(getIndexPath(isDev()), 'utf8'); const initAuth = currify(_initAuth); const notEmpty = (a) => a; @@ -48,15 +47,17 @@ const clean = (a) => a.filter(notEmpty); const isUndefined = (a) => typeof a === 'undefined'; const isFn = (a) => typeof a === 'function'; -module.exports = (params) => { +export default cloudcmd; + +export function cloudcmd(params) { const p = params || {}; const options = p.config || {}; + const config = p.configManager || createConfig({ configPath, }); const {modules} = p; - const keys = Object.keys(options); for (const name of keys) { @@ -65,7 +66,7 @@ module.exports = (params) => { if (/root/.test(name)) validate.root(value, config); - if (/editor|packer|columns/.test(name)) + if (/editor|packer|themes|menu/.test(name)) validate[name](value); if (/prefix/.test(name)) @@ -86,16 +87,20 @@ module.exports = (params) => { socket: p.socket, }); - return cloudcmd({ + return cloudcmdMiddle({ modules, config, }); -}; +} + +const depStore = createDepStore(); -module.exports.createConfigManager = createConfig; -module.exports.configPath = configPath; +export const createConfigManager = createConfig; +export { + configPath, +}; -module.exports._getIndexPath = getIndexPath; +export const _getIndexPath = getIndexPath; function defaultValue(config, name, options) { const value = options[name]; @@ -107,7 +112,8 @@ function defaultValue(config, name, options) { return value; } -module.exports._getPrefix = getPrefix; +export const _getPrefix = getPrefix; + function getPrefix(prefix) { if (isFn(prefix)) return prefix() || ''; @@ -115,8 +121,7 @@ function getPrefix(prefix) { return prefix || ''; } -module.exports._initAuth = _initAuth; -function _initAuth(config, accept, reject, username, password) { +export function _initAuth(config, accept, reject, username, password) { if (!config('auth')) return accept(); @@ -139,50 +144,56 @@ function listen({prefixSocket, socket, config}) { edward.listen(socket, { root, auth, - prefixSocket: prefixSocket + '/edward', + prefixSocket: `${prefixSocket}/edward`, }); dword.listen(socket, { root, auth, - prefixSocket: prefixSocket + '/dword', + prefixSocket: `${prefixSocket}/dword`, + }); + + qword.listen(socket, { + root, + auth, + prefixSocket: `${prefixSocket}/qword`, }); deepword.listen(socket, { root, auth, - prefixSocket: prefixSocket + '/deepword', + prefixSocket: `${prefixSocket}/deepword`, }); config('console') && konsole.listen(socket, { auth, - prefixSocket: prefixSocket + '/console', + prefixSocket: `${prefixSocket}/console`, }); fileop.listen(socket, { root, auth, - prefix: prefixSocket + '/fileop', + prefix: `${prefixSocket}/fileop`, }); config('terminal') && terminal(config).listen(socket, { auth, - prefix: prefixSocket + '/gritty', + prefix: `${prefixSocket}/gritty`, command: config('terminalCommand'), autoRestart: config('terminalAutoRestart'), }); - distribute.export(config, socket); + distributeExport(config, socket); } -function cloudcmd({modules, config}) { +function cloudcmdMiddle({modules, config}) { const online = apart(config, 'online'); const cache = false; const diff = apart(config, 'diff'); const zip = apart(config, 'zip'); const root = apart(config, 'root'); - const ponseStatic = ponse.static({ + const ponseStatic = ponse.createStatic({ cache, root: DIR_ROOT, }); @@ -194,9 +205,7 @@ function cloudcmd({modules, config}) { config('console') && konsole({ online, }), - config('terminal') && terminal(config, {}), - edward({ root, online, @@ -205,7 +214,6 @@ function cloudcmd({modules, config}) { dropbox, dropboxToken, }), - dword({ root, online, @@ -214,7 +222,14 @@ function cloudcmd({modules, config}) { dropbox, dropboxToken, }), - + qword({ + root, + online, + diff, + zip, + dropbox, + dropboxToken, + }), deepword({ root, online, @@ -223,38 +238,36 @@ function cloudcmd({modules, config}) { dropbox, dropboxToken, }), - fileop(), nomine(), - setUrl, setSW, logout, authentication(config), config.middle, - modules && modulas(modules), - config('dropbox') && restbox({ prefix: cloudfunc.apiURL, root, token: dropboxToken, }), - restafary({ prefix: cloudfunc.apiURL + '/fs', root, }), - userMenu({ menuName: '.cloudcmd.menu.js', + config, + }), + rest({ + config, + fs: depStore('fs'), + moveFiles: depStore('moveFiles'), }), - - rest(config), route(config, { html, + win32: depStore('win32'), }), - ponseStatic, ]); @@ -268,9 +281,11 @@ function logout(req, res, next) { res.sendStatus(401); } -module.exports._replaceDist = replaceDist; +export const _isDev = isDev; +export const _replaceDist = replaceDist; + function replaceDist(url) { - if (!isDev) + if (!isDev()) return url; return url.replace(/^\/dist\//, '/dist-dev/'); @@ -287,11 +302,17 @@ function setUrl(req, res, next) { function setSW(req, res, next) { const {url} = req; - const isSW = /^\/sw\.js(\.map)?$/.test(url); + const isSW = /^\/sw\.[mc]?js(\.map)?$/.test(url); - if (isSW) + if (isSW) { + const url = req.url.replace(/[cm]js/, 'js'); req.url = replaceDist(`/dist${url}`); + } next(); } +assign(cloudcmd, { + depStore, + createConfigManager, +}); diff --git a/server/cloudcmd.spec.js b/server/cloudcmd.spec.js index 1deaef420b..97949cd3dc 100644 --- a/server/cloudcmd.spec.js +++ b/server/cloudcmd.spec.js @@ -1,22 +1,20 @@ -'use strict'; - -const path = require('path'); - -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); -const {reRequire} = require('mock-require'); - -const DIR = './'; -const cloudcmdPath = DIR + 'cloudcmd'; - -const cloudcmd = require(cloudcmdPath); -const { +import path, {dirname} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import serveOnce from 'serve-once'; +import {test, stub} from 'supertape'; +import cloudcmd, { + _isDev, + _replaceDist, createConfigManager, _getPrefix, _initAuth, -} = cloudcmd; + _getIndexPath, +} from '#server/cloudcmd'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); -const {request} = require('serve-once')(cloudcmd, { +const {request} = serveOnce(cloudcmd, { config: { auth: false, dropbox: false, @@ -75,16 +73,14 @@ test('cloudcmd: getPrefix: function: empty', (t) => { }); test('cloudcmd: replaceDist', (t) => { - const {NODE_ENV} = process.env; - process.env.NODE_ENV = 'development'; - - const {_replaceDist} = reRequire(cloudcmdPath); + const currentIsDev = _isDev(); + _isDev(true); const url = '/dist/hello'; const result = _replaceDist(url); const expected = '/dist-dev/hello'; - process.env.NODE_ENV = NODE_ENV; + _isDev(currentIsDev); t.equal(result, expected); t.end(); @@ -92,13 +88,12 @@ test('cloudcmd: replaceDist', (t) => { test('cloudcmd: replaceDist: !isDev', (t) => { const url = '/dist/hello'; - const cloudcmdPath = DIR + 'cloudcmd'; - const reset = cleanNodeEnv(); - const {_replaceDist} = reRequire(cloudcmdPath); + const currentIsDev = _isDev(); + _isDev(false); const result = _replaceDist(url); - reset(); + _isDev(currentIsDev); t.equal(result, url); t.end(); @@ -165,7 +160,7 @@ test('cloudcmd: getIndexPath: production', (t) => { const isDev = false; const name = path.join(__dirname, '..', 'dist', 'index.html'); - t.equal(cloudcmd._getIndexPath(isDev), name); + t.equal(_getIndexPath(isDev), name); t.end(); }); @@ -173,25 +168,13 @@ test('cloudcmd: getIndexPath: development', (t) => { const isDev = true; const name = path.join(__dirname, '..', 'dist-dev', 'index.html'); - t.equal(cloudcmd._getIndexPath(isDev), name); + t.equal(_getIndexPath(isDev), name); t.end(); }); test('cloudcmd: sw', async (t) => { - const {status} = await request.get('/sw.js'); + const {status} = await request.get('/sw.mjs'); t.equal(status, 200, 'should return sw'); t.end(); }); - -function cleanNodeEnv() { - const {NODE_ENV} = process.env; - process.env.NODE_ENV = ''; - - const reset = () => { - process.env.NODE_ENV = NODE_ENV; - }; - - return reset; -} - diff --git a/server/columns.js b/server/columns.js index ac2575603b..eff6395783 100644 --- a/server/columns.js +++ b/server/columns.js @@ -1,9 +1,16 @@ -'use strict'; - -const path = require('path'); -const fs = require('fs'); -const readFilesSync = require('@cloudcmd/read-files-sync'); -const isMap = (a) => /\.map$/.test(a); +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 * 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|js)$/.test(a); const not = (fn) => (a) => !fn(a); const defaultColumns = { @@ -11,19 +18,27 @@ const defaultColumns = { 'name-size-date-owner-mode': '', }; -const isDev = process.env.NODE_ENV === 'development'; +const _isDev = fullstore(process.env.NODE_ENV === 'development'); const getDist = (isDev) => isDev ? 'dist-dev' : 'dist'; -const dist = getDist(isDev); -const columnsDir = path.join(__dirname, '..', dist, 'columns'); - -const names = fs.readdirSync(columnsDir) - .filter(not(isMap)); - -const columns = readFilesSync(columnsDir, names, 'utf8'); +export const isDev = _isDev; -module.exports = { - ...columns, - ...defaultColumns, +export const getColumns = ({isDev = _isDev()} = {}) => { + const columns = readFilesSyncMemo(isDev); + + return { + ...columns, + ...defaultColumns, + }; }; +const readFilesSyncMemo = nanomemoize((isDev) => { + const dist = getDist(isDev); + const columnsDir = path.join(__dirname, '..', dist, 'columns'); + + const names = fs + .readdirSync(columnsDir) + .filter(not(isMap)); + + return readFilesSync(columnsDir, names, 'utf8'); +}); diff --git a/server/columns.spec.js b/server/columns.spec.js new file mode 100644 index 0000000000..533cfe7514 --- /dev/null +++ b/server/columns.spec.js @@ -0,0 +1,40 @@ +import {dirname} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import fs from 'node:fs'; +import test from 'supertape'; +import {getColumns, isDev} from './columns.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +test('columns: prod', (t) => { + const columns = getColumns({ + isDev: false, + }); + + t.equal(columns[''], ''); + t.end(); +}); + +test('columns: dev', (t) => { + const columns = getColumns({ + isDev: true, + }); + + const css = fs.readFileSync(`${__dirname}/../css/columns/name-size-date.css`, 'utf8'); + + t.match(columns['name-size-date'], css); + t.end(); +}); + +test('columns: no args', (t) => { + const currentIsDev = isDev(); + isDev(true); + const columns = getColumns(); + + const css = fs.readFileSync(`${__dirname}/../css/columns/name-size-date.css`, 'utf8'); + isDev(currentIsDev); + + t.match(columns['name-size-date'], css); + t.end(); +}); diff --git a/server/config.js b/server/config.js index 75d6c5a3ab..77c4a6afa5 100644 --- a/server/config.js +++ b/server/config.js @@ -1,41 +1,41 @@ -'use strict'; - -const DIR_SERVER = __dirname + '/'; -const DIR_COMMON = '../common/'; -const DIR = DIR_SERVER + '../'; - -const path = require('path'); -const fs = require('fs'); -const Emitter = require('events'); -const {homedir} = require('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 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 criton = require('criton'); +import path from 'node:path'; +import fs from 'node:fs'; +import Emitter from 'node:events'; +import {homedir} from 'node:os'; +import currify from 'currify'; +import wraptile from 'wraptile'; +import {tryToCatch} from 'try-to-catch'; +import pullout from 'pullout'; +import * as ponse from 'ponse'; +import jonny from 'jonny'; +import jju from 'jju'; +import writejson from 'writejson'; +import {tryCatch} from 'try-catch'; +import criton from 'criton'; +import * as CloudFunc from '#common/cloudfunc'; +import exit from './exit.js'; +import rootConfig from '../json/config.json' with { + type: 'json', +}; + +const isUndefined = (a) => typeof a === 'undefined'; + const HOME = homedir(); const resolve = Promise.resolve.bind(Promise); + const formatMsg = currify((a, b) => CloudFunc.formatMsg(a, b)); const {apiURL} = CloudFunc; -const key = (a) => Object.keys(a).pop(); - -const ConfigPath = path.join(DIR, 'json/config.json'); -const ConfigHome = path.join(HOME, '.cloudcmd.json'); +const key = (a) => Object + .keys(a) + .pop(); const connection = currify(_connection); -const connectionWraped = wraptile(_connection); + +const connectionWrapped = wraptile(_connection); + const middle = currify(_middle); const readjsonSync = (name) => { @@ -44,8 +44,6 @@ const readjsonSync = (name) => { }); }; -const rootConfig = readjsonSync(ConfigPath); - function read(filename) { if (!filename) return rootConfig; @@ -61,8 +59,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 +77,7 @@ function initWrite(filename, configManager) { return resolve; } -function createConfig({configPath} = {}) { +export function createConfig({configPath} = {}) { const config = {}; const changeEmitter = new Emitter(); @@ -88,7 +85,7 @@ function createConfig({configPath} = {}) { if (key === '*') return config; - if (value === undefined) + if (isUndefined(value)) return config[key]; config[key] = value; @@ -107,15 +104,16 @@ function createConfig({configPath} = {}) { }; configManager.unsubscribe = (fn) => { - // replace to off on node v10 - changeEmitter.removeListener('change', fn); + changeEmitter.off('change', fn); }; return configManager; } const write = (filename, config) => { - return writejson(filename, config('*'), {mode: 0o600}); + return writejson(filename, config('*'), { + mode: 0o600, + }); }; function _connection(manage, socket) { @@ -137,7 +135,8 @@ function _connection(manage, socket) { socket.emit('log', data); }; - manage.write() + manage + .write() .then(send) .catch(emit(socket, 'err')); }); @@ -146,13 +145,14 @@ function _connection(manage, socket) { function listen(manage, sock, auth) { const prefix = manage('prefixSocket'); - sock.of(prefix + '/config') + sock + .of(`${prefix}/config`) .on('connection', (socket) => { if (!manage('auth')) return connection(manage, socket); const reject = () => socket.emit('reject'); - socket.on('auth', auth(connectionWraped(manage, socket), reject)); + socket.on('auth', auth(connectionWrapped(manage, socket), reject)); }); } @@ -195,6 +195,7 @@ function get(manage, request, response) { async function patch(manage, request, response) { const name = 'config.json'; const cache = false; + const options = { name, request, @@ -230,7 +231,8 @@ function traverse([manage, json]) { } } -module.exports._cryptoPass = cryptoPass; +export const _cryptoPass = cryptoPass; + function cryptoPass(manage, json) { const algo = manage('algo'); @@ -239,9 +241,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.js b/server/config.spec.js index dbd8527789..92b0646cad 100644 --- a/server/config.spec.js +++ b/server/config.spec.js @@ -1,22 +1,14 @@ -'use strict'; +import {createRequire} from 'node:module'; +import {test, stub} from 'supertape'; +import {apiURL} from '#common/cloudfunc'; +import {createConfig, _cryptoPass} from './config.js'; +import {connect} from '../test/before.js'; -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); +const require = createRequire(import.meta.url); +const fixture = require('./config.fixture.json'); -const root = '../'; -const configPath = './config'; - -const { - createConfig, - _cryptoPass, -} = require(configPath); const config = createConfig(); -const {apiURL} = require(root + 'common/cloudfunc'); - -const fixture = require('./config.fixture'); -const {connect} = require('../test/before'); - test('config: manage', (t) => { t.equal(undefined, config(), 'should return "undefined"'); t.end(); @@ -27,7 +19,9 @@ test('config: manage: get', async (t) => { const configManager = createConfig(); const {done} = await connect({ - config: {editor}, + config: { + editor, + }, configManager, }); @@ -37,13 +31,15 @@ test('config: manage: get', async (t) => { t.end(); }); -test('config: manage: get', async (t) => { +test('config: manage: get: config', async (t) => { const editor = 'deepword'; const conf = { editor, }; - const {done} = await connect({config: conf}); + const {done} = await connect({ + config: conf, + }); config('editor', 'dword'); done(); @@ -96,6 +92,7 @@ test('config: middle: no', (t) => { const res = null; const url = `${apiURL}/config`; const method = 'POST'; + const req = { url, method, @@ -103,7 +100,6 @@ test('config: middle: no', (t) => { middle(req, res, next); - t.ok(next.calledWith(), 'should call next'); + t.calledWithNoArgs(next, 'should call next'); t.end(); }); - diff --git a/server/depstore.js b/server/depstore.js new file mode 100644 index 0000000000..c5fefaa9fe --- /dev/null +++ b/server/depstore.js @@ -0,0 +1,13 @@ +export const createDepStore = () => { + const deps = {}; + + return (name, value) => { + if (!name) + return false; + + if (!value) + return deps[name]; + + deps[name] = value; + }; +}; diff --git a/server/distribute/export.js b/server/distribute/export.js index 7b2e3641e9..5e1b54d458 100644 --- a/server/distribute/export.js +++ b/server/distribute/export.js @@ -1,13 +1,8 @@ -'use strict'; - -const currify = require('currify'); -const wraptile = require('wraptile'); -const squad = require('squad'); -const omit = require('object.omit'); - -const log = require('./log'); - -const { +import currify from 'currify'; +import wraptile from 'wraptile'; +import squad from 'squad'; +import {omit} from '#common/omit'; +import log, { exportStr, connectedStr, disconnectedStr, @@ -15,8 +10,8 @@ const { makeColor, getMessage, getDescription, - logWraped, -} = log; + logWrapped, +} from './log.js'; const omitList = [ 'auth', @@ -38,7 +33,7 @@ const omitList = [ const omitConfig = (config) => omit(config, omitList); -module.exports = (config, socket) => { +export const distributeExport = (config, socket) => { if (!config('export')) return; @@ -46,14 +41,12 @@ module.exports = (config, socket) => { const distributePrefix = `${prefix}/distribute`; const isLog = config('log'); - const onError = squad( - logWraped(isLog, exportStr), - getMessage, - ); + const onError = squad(logWrapped(isLog, exportStr), getMessage); - const onConnectError = squad(logWraped(isLog, exportStr), getDescription); + const onConnectError = squad(logWrapped(isLog, exportStr), getDescription); - socket.of(distributePrefix) + socket + .of(distributePrefix) .on('connection', onConnection(push, config)) .on('error', onError) .on('connect_error', onConnectError); @@ -90,7 +83,11 @@ const connectPush = wraptile((push, config, socket) => { const host = getHost(socket); const subscription = push(socket); - socket.on('disconnect', onDisconnect(subscription, config, host)); + socket.on('disconnect', onDisconnect( + subscription, + config, + host, + )); log(isLog, exportStr, `${connectedStr} to ${host}`); socket.emit('config', omitConfig(config('*'))); @@ -125,4 +122,3 @@ const onDisconnect = wraptile((subscription, config, host) => { config.unsubscribe(subscription); log(isLog, exportStr, `${disconnectedStr} from ${host}`); }); - diff --git a/server/distribute/export.spec.js b/server/distribute/export.spec.js index 0d2bb8ba96..ca6155a83a 100644 --- a/server/distribute/export.spec.js +++ b/server/distribute/export.spec.js @@ -1,12 +1,10 @@ -'use strict'; +import {once} from 'node:events'; +import test from 'supertape'; +import io from 'socket.io-client'; +import * as Config from '../config.js'; +import {connect} from '../../test/before.js'; -const {once} = require('events'); - -const test = require('supertape'); -const io = require('socket.io-client'); - -const {connect} = require('../../test/before'); -const config = require('../config').createConfig(); +const config = Config.createConfig(); test('distribute: export', async (t) => { const defaultConfig = { @@ -68,4 +66,3 @@ test('distribute: export: config', async (t) => { t.equal(typeof data, 'object', 'should emit object'); t.end(); }); - diff --git a/server/distribute/import.js b/server/distribute/import.js index 3bdeefa1d6..f6cbe61430 100644 --- a/server/distribute/import.js +++ b/server/distribute/import.js @@ -1,15 +1,16 @@ -'use strict'; +import currify from 'currify'; +import wraptile from 'wraptile'; +import squad from 'squad'; +import {fullstore} from 'fullstore'; +import io from 'socket.io-client'; +import _forEachKey from 'for-each-key'; +import log from './log.js'; +import * as env from '../env.js'; -const currify = require('currify'); -const wraptile = require('wraptile'); -const squad = require('squad'); -const fullstore = require('fullstore'); +const isUndefined = (a) => typeof a === 'undefined'; -const io = require('socket.io-client'); -const forEachKey = currify(require('for-each-key')); - -const log = require('./log'); -const env = require('../env'); +const noop = () => {}; +const forEachKey = currify(_forEachKey); const { importStr, @@ -19,13 +20,14 @@ const { authTryStr, getMessage, getDescription, - logWraped, + logWrapped, } = log; const {entries} = Object; const equal = (a, b) => `${a}=${b}`; const append = currify((obj, a, b) => obj.value += b && equal(a, b) + '&'); + const wrapApply = (f, disconnect) => (status) => () => f(null, { status, disconnect, @@ -36,14 +38,19 @@ const addUrl = currify((url, a) => `${url}: ${a}`); const rmListeners = wraptile((socket, listeners) => { socket.removeListener('connect', listeners.onConnect); + socket.removeListener('accept', listeners.onAccept); socket.removeListener('config', listeners.onConfig); socket.removeListener('error', listeners.onError); - socket.removeListener('connection_error', listeners.onError); + socket.removeListener('connect_error', listeners.onConnectError); + socket.removeListener('reject', listeners.onReject); + + if (listeners.onChange) + socket.removeListener('change', listeners.onChange); }); const canceled = (f) => f(null, { status: 'canceled', - disconnect: () => {}, + disconnect: noop, }); const done = wraptile((fn, store) => fn(null, { @@ -58,15 +65,14 @@ const emitAuth = wraptile((importUrl, config, socket) => { const updateConfig = currify((config, data) => { for (const [key, value] of entries(data)) { - if (typeof env(key) !== 'undefined') { + if (!isUndefined(env.parse(key))) continue; - } config(key, value); } }); -module.exports = (config, options, fn) => { +export const distributeImport = (config, options, fn) => { fn = fn || options; if (!config('import')) @@ -84,6 +90,7 @@ module.exports = (config, options, fn) => { }); const url = `${importUrl}/distribute?${query}`; + const socket = io.connect(url, { ...options, rejectUnauthorized: false, @@ -94,50 +101,37 @@ module.exports = (config, options, fn) => { const close = closeIfNot(socket, importListen); const statusStore = fullstore(); - const statusStoreWraped = wraptile(statusStore); - - const onConfig = squad( - close, - logWraped(isLog, importStr, `config received from ${colorUrl}`), - statusStoreWraped('received'), - updateConfig(config), - ); - - const onError = squad( - superFn('error'), - logWraped(isLog, config, importStr), - addUrl(colorUrl), - getMessage, - ); - - const onConnectError = squad( - superFn('connect_error'), - logWraped(isLog, importStr), - addUrl(colorUrl), - getDescription, - ); + const statusStoreWrapped = wraptile(statusStore); + + const onConfig = squad(close, logWrapped(isLog, importStr, `config received from ${colorUrl}`), statusStoreWrapped('received'), updateConfig(config)); + + const onError = squad(superFn('error'), logWrapped(isLog, config, importStr), addUrl(colorUrl), getMessage); + + const onConnectError = squad(superFn('connect_error'), logWrapped(isLog, importStr), addUrl(colorUrl), getDescription); const onConnect = emitAuth(importUrl, config, socket); - const onAccept = logWraped(isLog, importStr, `${connectedStr} to ${colorUrl}`); - const onDisconnect = squad( + const onAccept = logWrapped(isLog, importStr, `${connectedStr} to ${colorUrl}`); + const onChange = squad(logWrapped(isLog, importStr), config); + + const onReject = squad(superFn('reject'), logWrapped( + isLog, + importStr, + tokenRejectedStr, + )); + + const onDisconnect = squad(...[ done(fn, statusStore), - logWraped(isLog, importStr, `${disconnectedStr} from ${colorUrl}`), + logWrapped(isLog, importStr, `${disconnectedStr} from ${colorUrl}`), rmListeners(socket, { - onError, onConnect, + onAccept, onConfig, + onError, + onConnectError, + onReject, + onChange, }), - ); - - const onChange = squad( - logWraped(isLog, importStr), - config, - ); - - const onReject = squad( - superFn('reject'), - logWraped(isLog, importStr, tokenRejectedStr), - ); + ]); socket.on('connect', onConnect); socket.on('accept', onAccept); @@ -164,4 +158,3 @@ function toLine(obj) { return result.value.slice(start, backward * end); } - diff --git a/server/distribute/import.spec.js b/server/distribute/import.spec.js index 8467d1474d..10e246be32 100644 --- a/server/distribute/import.spec.js +++ b/server/distribute/import.spec.js @@ -1,14 +1,13 @@ -'use strict'; - -const test = require('supertape'); -const {promisify} = require('util'); -const tryToCatch = require('try-to-catch'); - -const {connect} = require('../../test/before'); -const {createConfigManager} = require('../cloudcmd'); +import process from 'node:process'; +import {promisify} from 'node:util'; +import test from 'supertape'; +import {tryToCatch} from 'try-to-catch'; +import {createConfigManager} from '#server/cloudcmd'; +import {connect} from '../../test/before.js'; +import {distributeImport} from './import.js'; const distribute = { - import: promisify(require('./import')), + import: promisify(distributeImport), }; const config = createConfigManager(); diff --git a/server/distribute/index.js b/server/distribute/index.js deleted file mode 100644 index 9fae4b04e5..0000000000 --- a/server/distribute/index.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -module.exports.import = require('./import'); -module.exports.export = require('./export'); diff --git a/server/distribute/log.js b/server/distribute/log.js index dc0a48f2b9..acea5759a1 100644 --- a/server/distribute/log.js +++ b/server/distribute/log.js @@ -1,30 +1,27 @@ -'use strict'; +import wraptile from 'wraptile'; +import chalk from 'chalk'; +import datetime from '../../common/datetime.js'; -const wraptile = require('wraptile'); -const chalk = require('chalk'); - -const datetime = require('../../common/datetime'); +const {assign} = Object; const log = (isLog, name, msg) => isLog && console.log(`${datetime()} -> ${name}: ${msg}`); -const makeColor = (a) => chalk.blue(a); -const getMessage = (e) => e.message || e; -const getDescription = (e) => e.message; - -module.exports = log; -module.exports.logWraped = wraptile(log); -module.exports.stringToRGB = stringToRGB; -module.exports.makeColor = makeColor; -module.exports.getMessage = getMessage; -module.exports.getDescription = getDescription; - -module.exports.importStr = 'import'; -module.exports.exportStr = 'export'; -module.exports.connectedStr = chalk.green('connected'); -module.exports.disconnectedStr = chalk.red('disconnected'); -module.exports.tokenRejectedStr = chalk.red('token rejected'); -module.exports.authTryStr = chalk.yellow('try to auth'); - -function stringToRGB(a) { + +export const makeColor = (a) => chalk.blue(a); +export const getMessage = (e) => e.message || e; +export const getDescription = (e) => e.message; + +export default log; + +export const logWrapped = wraptile(log); + +export const importStr = 'import'; +export const exportStr = 'export'; +export const connectedStr = chalk.green('connected'); +export const disconnectedStr = chalk.red('disconnected'); +export const tokenRejectedStr = chalk.red('token rejected'); +export const authTryStr = chalk.yellow('try to auth'); + +export function stringToRGB(a) { return [ a.charCodeAt(0), a.length, @@ -40,3 +37,16 @@ function crc(a) { .reduce(add, 0); } +assign(log, { + getMessage, + makeColor, + getDescription, + authTryStr, + stringToRGB, + logWrapped, + importStr, + exportStr, + connectedStr, + disconnectedStr, + tokenRejectedStr, +}); diff --git a/server/distribute/log.spec.js b/server/distribute/log.spec.js index 1078c82a4d..be50dcc370 100644 --- a/server/distribute/log.spec.js +++ b/server/distribute/log.spec.js @@ -1,8 +1,6 @@ -'use strict'; - -const test = require('supertape'); -const log = require('./log'); -const {createConfig} = require('../config'); +import test from 'supertape'; +import log from './log.js'; +import {createConfig} from '../config.js'; test('distribute: log: getMessage', (t) => { const e = 'hello'; @@ -31,4 +29,6 @@ test('distribute: log: config', (t) => { config('log', logOriginal); t.end(); +}, { + checkAssertionsCount: false, }); diff --git a/server/env.js b/server/env.js index d53e3e488e..8686530783 100644 --- a/server/env.js +++ b/server/env.js @@ -1,25 +1,27 @@ -'use strict'; +import {env} from 'node:process'; +import snake from 'just-snake-case'; -const snake = require('just-snake-case'); - -const {env} = process; const up = (a) => a.toUpperCase(); -module.exports = parse; -module.exports.bool = (name) => { +export const bool = (name) => { const value = parse(name); if (value === 'true') return true; + if (value === '1') + return true; + if (value === 'false') return false; + + if (value === '0') + 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 e090f45a2f..d8065b2c1b 100644 --- a/server/env.spec.js +++ b/server/env.spec.js @@ -1,7 +1,6 @@ -'use strict'; - -const test = require('supertape'); -const env = require('./env'); +import process from 'node:process'; +import {test} from 'supertape'; +import * as env from './env.js'; test('cloudcmd: server: env: bool: upper case first', (t) => { const { @@ -29,3 +28,29 @@ test('cloudcmd: server: env: bool: snake_case', (t) => { t.ok(result); t.end(); }); + +test('cloudcmd: server: env: bool: number', (t) => { + const {cloudcmd_terminal} = process.env; + + process.env.CLOUDCMD_TERMINAL = '1'; + + const result = env.bool('terminal'); + + process.env.CLOUDCMD_TERMINAL = cloudcmd_terminal; + + t.ok(result); + t.end(); +}); + +test('cloudcmd: server: env: bool: number: 0', (t) => { + const {cloudcmd_terminal} = process.env; + + process.env.cloudcmd_terminal = '0'; + + const result = env.bool('terminal'); + + process.env.cloudcmd_terminal = cloudcmd_terminal; + + t.notOk(result); + t.end(); +}); diff --git a/server/exit.js b/server/exit.js index edc6b409d8..ebe5ad8f5a 100644 --- a/server/exit.js +++ b/server/exit.js @@ -1,11 +1,10 @@ -'use strict'; +import process from 'node:process'; const getMessage = (a) => a?.message || a; -module.exports = (...args) => { +export default (...args) => { const messages = args.map(getMessage); console.error(...messages); process.exit(1); }; - diff --git a/server/exit.spec.js b/server/exit.spec.js index 663bd44ae1..3b45481805 100644 --- a/server/exit.spec.js +++ b/server/exit.spec.js @@ -1,11 +1,10 @@ -'use strict'; - -const test = require('supertape'); -const exit = require('./exit'); -const stub = require('@cloudcmd/stub'); +import process from 'node:process'; +import {test, stub} from 'supertape'; +import exit from './exit.js'; test('cloudcmd: exit: process.exit', (t) => { const {exit: exitOriginal} = process; + process.exit = stub(); exit(); @@ -46,4 +45,3 @@ test('cloudcmd: exit.error: console.error: error', (t) => { t.end(); }); - diff --git a/server/fixture-user-menu/io-cp-fix.js b/server/fixture-user-menu/io-cp-fix.js index a8d5279f38..52f9c0b076 100644 --- a/server/fixture-user-menu/io-cp-fix.js +++ b/server/fixture-user-menu/io-cp-fix.js @@ -1,4 +1,3 @@ -'use strict'; async function copy() { await IO.copy(dirPath, mp3Dir, mp3Names); } diff --git a/server/fixture-user-menu/io-mv-fix.js b/server/fixture-user-menu/io-mv-fix.js index 5c66858a23..d95ccf3fb2 100644 --- a/server/fixture-user-menu/io-mv-fix.js +++ b/server/fixture-user-menu/io-mv-fix.js @@ -1,4 +1,3 @@ -'use strict'; async function move() { await IO.move(dirPath, mp3Dir, mp3Names); } diff --git a/server/markdown/index.js b/server/markdown/index.js index 31859918a2..3f80ff0328 100644 --- a/server/markdown/index.js +++ b/server/markdown/index.js @@ -1,25 +1,22 @@ -'use strict'; - -const {join} = require('path'); -const {callbackify} = require('util'); - -const pullout = require('pullout'); -const ponse = require('ponse'); -const threadIt = require('thread-it'); -const {read} = require('redzip'); - -const parse = threadIt(join(__dirname, 'worker')); - -const root = require('../root'); - -threadIt.init(); +import {callbackify} from 'node:util'; +import {fileURLToPath} from 'node:url'; +import {dirname} from 'node:path'; +import pullout from 'pullout'; +import {getQuery} from 'ponse'; +import {read} from 'redzip'; +import root from '../root.js'; +import parse from './worker.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const isString = (a) => typeof a === 'string'; // warm up parse(''); -const DIR_ROOT = __dirname + '/../../'; +const DIR_ROOT = `${__dirname}/../../`; -module.exports = callbackify(async (name, rootDir, request) => { +export default callbackify(async (name, rootDir, request) => { check(name, request); const {method} = request; @@ -43,7 +40,7 @@ function parseName(query, name, rootDir) { } async function onGET(request, name, root) { - const query = ponse.getQuery(request); + const query = getQuery(request); const fileName = parseName(query, name, root); const stream = await read(fileName); const data = await pullout(stream); @@ -57,10 +54,9 @@ async function onPUT(request) { } function check(name, request) { - if (typeof name !== 'string') + if (!isString(name)) throw Error('name should be string!'); if (!request) throw Error('request could not be empty!'); } - diff --git a/server/markdown/index.spec.js b/server/markdown/index.spec.js index d6e9c33860..466271bdaa 100644 --- a/server/markdown/index.spec.js +++ b/server/markdown/index.spec.js @@ -1,40 +1,38 @@ -'use strict'; +import fs from 'node:fs'; +import {join} from 'node:path'; +import {promisify} from 'node:util'; +import {tryToCatch} from 'try-to-catch'; +import test from 'supertape'; +import serveOnce from 'serve-once'; +import {cloudcmd} from '#server/cloudcmd'; +import markdown from './index.js'; -const fs = require('fs'); -const {join} = require('path'); -const {promisify} = require('util'); - -const tryToCatch = require('try-to-catch'); -const serveOnce = require('serve-once'); -const test = require('supertape'); - -const markdown = require('.'); - -const _markdown = promisify(markdown); -const fixtureDir = join(__dirname, 'fixture'); -const cloudcmd = require('../..'); const config = { auth: false, }; const configManager = cloudcmd.createConfigManager(); -const {request} = require('serve-once')(cloudcmd, { +const {request} = serveOnce(cloudcmd, { config, configManager, }); +const fixtureDir = new URL('fixture', import.meta.url).pathname; + +const _markdown = promisify(markdown); + test('cloudcmd: markdown: error', async (t) => { const {body} = await request.get('/api/v1/markdown/not-found'); - t.ok(/ENOENT/.test(body), 'should not found'); + t.match(body, 'ENOENT', 'should not found'); t.end(); }); test('cloudcmd: markdown: relative: error', async (t) => { const {body} = await request.get('/api/v1/markdown/not-found?relative'); - t.ok(/ENOENT/.test(body), 'should not found'); + t.match(body, 'ENOENT', 'should not found'); t.end(); }); @@ -89,9 +87,10 @@ test('cloudcmd: markdown: no request', async (t) => { t.end(); }); -test('cloudcmd: markdown: zip', async (t) => { +test('cloudcmd: markdown', async (t) => { const configManager = cloudcmd.createConfigManager(); - const fixtureDir = join(__dirname, 'fixture'); + const fixtureDir = new URL('fixture', import.meta.url).pathname; + const config = { auth: false, root: fixtureDir, @@ -101,6 +100,7 @@ test('cloudcmd: markdown: zip', async (t) => { config, configManager, }); + const {body} = await request.get('/api/v1/markdown/markdown.md'); t.equal(body, '

    hello

    \n'); @@ -109,7 +109,8 @@ test('cloudcmd: markdown: zip', async (t) => { test('cloudcmd: markdown: zip', async (t) => { const configManager = cloudcmd.createConfigManager(); - const fixtureDir = join(__dirname, 'fixture'); + const fixtureDir = new URL('fixture', import.meta.url).pathname; + const config = { auth: false, root: fixtureDir, @@ -119,6 +120,7 @@ test('cloudcmd: markdown: zip', async (t) => { config, configManager, }); + const {body} = await request.get('/api/v1/markdown/markdown.zip/markdown.md'); t.equal(body, '

    hello

    \n'); diff --git a/server/markdown/worker.js b/server/markdown/worker.js index ae2a95fd8f..ccac2aeefd 100644 --- a/server/markdown/worker.js +++ b/server/markdown/worker.js @@ -1,4 +1,5 @@ -'use strict'; +import createMarkdownIt from 'markdown-it'; -const markdownIt = require('markdown-it')(); -module.exports = (a) => markdownIt.render(a); +const markdownIt = createMarkdownIt(); + +export default (a) => markdownIt.render(a); diff --git a/server/modulas.js b/server/modulas.js index 1b32dffa27..7d38df9952 100644 --- a/server/modulas.js +++ b/server/modulas.js @@ -1,9 +1,9 @@ -'use strict'; - -const deepmerge = require('deepmerge'); -const originalModules = require('../json/modules'); +import deepmerge from 'deepmerge'; +import originalModules from '../json/modules.json' with { + type: 'json', +}; -module.exports = (modules) => { +export default (modules) => { const result = deepmerge(originalModules, modules || {}); return (req, res, next) => { @@ -13,4 +13,3 @@ module.exports = (modules) => { res.send(result); }; }; - diff --git a/server/prefixer.js b/server/prefixer.js index 47675070d5..87a83411ea 100644 --- a/server/prefixer.js +++ b/server/prefixer.js @@ -1,15 +1,14 @@ -'use strict'; +const isString = (a) => typeof a === 'string'; -module.exports = (value) => { - if (typeof value !== 'string') +export default (value) => { + if (!isString(value)) return ''; if (value.length === 1) return ''; if (value && !value.includes('/')) - return '/' + value; + return `/${value}`; return value; }; - diff --git a/server/prefixer.spec.js b/server/prefixer.spec.js index b858e546ea..3ea77dfb60 100644 --- a/server/prefixer.spec.js +++ b/server/prefixer.spec.js @@ -1,8 +1,5 @@ -'use strict'; - -const test = require('supertape'); - -const prefixer = require('./prefixer'); +import {test} from 'supertape'; +import prefixer from './prefixer.js'; test('prefixer: prefix without a slash', (t) => { t.equal(prefixer('hello'), '/hello', 'should add slash'); diff --git a/server/repl.js b/server/repl.js index 43497a1460..46d58a157f 100644 --- a/server/repl.js +++ b/server/repl.js @@ -1,25 +1,25 @@ -'use strict'; - -const net = require('net'); -const repl = require('repl'); - -module.exports = net.createServer((socket) => { - const {pid} = process; - const addr = socket.remoteAddress; - const port = socket.remotePort; - - const r = repl.start({ - prompt: `[${pid} ${addr}:${port}>`, - input: socket, - output: socket, - terminal: true, - useGlobal: false, - }); - - r.on('exit', () => { - socket.end(); - }); - - r.context.socket = socket; -}).listen(1337); +import process from 'node:process'; +import net from 'node:net'; +import repl from 'node:repl'; +export default net + .createServer((socket) => { + const {pid} = process; + const addr = socket.remoteAddress; + const port = socket.remotePort; + + const r = repl.start({ + prompt: `[${pid} ${addr}:${port}>`, + input: socket, + output: socket, + terminal: true, + useGlobal: false, + }); + + r.on('exit', () => { + socket.end(); + }); + + r.context.socket = socket; + }) + .listen(1337); diff --git a/server/rest/index.js b/server/rest/index.js index cbd65d44f5..68bec54e72 100644 --- a/server/rest/index.js +++ b/server/rest/index.js @@ -1,51 +1,49 @@ -'use strict'; - -const DIR = '../'; -const DIR_COMMON = DIR + '../common/'; - -const path = require('path'); -const fs = require('fs'); - -const root = require(DIR + 'root'); -const CloudFunc = require(DIR_COMMON + 'cloudfunc'); -const markdown = require(DIR + 'markdown'); -const info = require('./info'); - -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'); - +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 * as ponse from 'ponse'; +import {copymitter} from 'copymitter'; +import _moveFiles from '@cloudcmd/move-files'; +import * as CloudFunc from '#common/cloudfunc'; +import root from '../root.js'; +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)); const isWin32 = process.platform === 'win32'; const {apiURL} = CloudFunc; const UserError = (msg) => { const error = Error(msg); + error.code = 'EUSER'; return error; }; -module.exports = currify((config, request, response, next) => { +export default currify(({config, fs = _fs, moveFiles = _moveFiles}, request, response, next) => { const name = ponse.getPathName(request); - const regExp = RegExp('^' + apiURL); + const regExp = RegExp(`^${apiURL}`); const is = regExp.test(name); if (!is) return next(); - rest(config, request, response); + rest({fs, config, moveFiles}, request, response); }); -function rest(config, request, response) { +function rest({fs, config, moveFiles}, request, response) { const name = ponse.getPathName(request); const params = { request, @@ -53,7 +51,7 @@ function rest(config, request, response) { name: name.replace(apiURL, '') || '/', }; - sendData(params, config, (error, options, data) => { + sendData(params, {fs, config, moveFiles}, (error, options, data) => { params.gzip = !error; if (!data) { @@ -64,7 +62,7 @@ function rest(config, request, response) { if (options.name) params.name = options.name; - if (options.gzip !== undefined) + if (!isUndefined(options.gzip)) params.gzip = options.gzip; if (options.query) @@ -74,7 +72,7 @@ function rest(config, request, response) { return ponse.sendError(error, params); if (error) - return ponse.sendError(error.stack, params); + return ponse.sendError(error, params); ponse.send(data, params); }); @@ -83,11 +81,13 @@ function rest(config, 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 */ -function sendData(params, config, callback) { +function sendData(params, {fs, config, moveFiles}, callback) { const p = params; - const isMD = /^\/markdown/.test(p.name); + const isMD = p.name.startsWith('/markdown'); const rootDir = config('root'); if (isMD) @@ -104,6 +104,8 @@ function sendData(params, config, callback) { .then((body) => { onPUT({ name: p.name, + fs, + moveFiles, config, body, }, callback); @@ -122,9 +124,10 @@ function onGET(params, config, callback) { if (p.name[0] === '/') cmd = p.name.replace('/', ''); - if (/^pack/.test(cmd)) { + if (cmd.startsWith('pack')) { cmd = cmd.replace(/^pack/, ''); streamPack(root(cmd, rootDir), p.response, packer); + return; } @@ -154,6 +157,7 @@ function streamPack(cmd, response, packer) { const noop = () => {}; const filename = cmd.replace(getPackReg(packer), ''); const dir = path.dirname(filename); + const names = [ path.basename(filename), ]; @@ -168,10 +172,7 @@ function getCMD(cmd) { return cmd; } -const getMoveMsg = (names) => { - const msg = formatMsg('move', names); - return msg; -}; +const getMoveMsg = (names) => formatMsg('move', names); const getRenameMsg = (from, to) => { const msg = formatMsg('rename', { @@ -182,8 +183,9 @@ const getRenameMsg = (from, to) => { return msg; }; -module.exports._onPUT = onPUT; -function onPUT({name, config, body}, callback) { +export const _onPUT = onPUT; + +function onPUT({name, fs, moveFiles, config, body}, callback) { checkPut(name, body, callback); const cmd = getCMD(name); @@ -216,8 +218,10 @@ function onPUT({name, config, body}, callback) { return moveFiles(fromRooted, toRooted, names) .on('error', fn) .on('end', fn); - } case 'rename': - return rename(rootDir, files.from, files.to, callback); + } + + case 'rename': + return rename(rootDir, files.from, files.to, fs, callback); case 'copy': if (!files.from || !files.names || !files.to) @@ -256,7 +260,7 @@ function onPUT({name, config, body}, callback) { } } -function rename(rootDir, from, to, callback) { +function rename(rootDir, from, to, fs, callback) { if (!from) return callback(UserError('"from" should be filled')); @@ -328,7 +332,7 @@ function operation(op, packer, from, to, names, fn) { const [name] = names; pack.on('progress', (count) => { - process.stdout.write(`\r${ op } "${ name }": ${ count }%`); + process.stdout.write(`\r${op} "${name}": ${count}%`); }); pack.on('end', () => { @@ -360,20 +364,15 @@ 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!'; - const error = Error(message); - return error; + return Error(message); } function parseData(data) { @@ -385,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); @@ -393,13 +392,12 @@ function formatMsg(msg, data, status) { } function checkPut(name, body, callback) { - if (typeof name !== 'string') + if (!isString(name)) throw Error('name should be a string!'); - if (typeof body !== 'string') + if (!isString(body)) throw Error('body should be a string!'); - if (typeof callback !== 'function') + if (!isFn(callback)) throw Error('callback should be a function!'); } - diff --git a/server/rest/index.spec.js b/server/rest/index.spec.js index 22ffd4ada7..0fd64d7cb1 100644 --- a/server/rest/index.spec.js +++ b/server/rest/index.spec.js @@ -1,15 +1,12 @@ -'use strict'; - -const test = require('supertape'); -const tryToCatch = require('try-to-catch'); - -const { +import {test} from 'supertape'; +import {tryToCatch} from 'try-to-catch'; +import { _formatMsg, _getWin32RootMsg, _isRootWin32, _isRootAll, _onPUT, -} = require('.'); +} from './index.js'; test('rest: formatMsg', (t) => { const result = _formatMsg('hello', 'world'); @@ -73,4 +70,3 @@ test('rest: onPUT: no callback', async (t) => { t.equal(e.message, 'callback should be a function!', 'should throw when no callback'); t.end(); }); - diff --git a/server/rest/info.js b/server/rest/info.js index 13758b2190..8cf0e1d719 100644 --- a/server/rest/info.js +++ b/server/rest/info.js @@ -1,17 +1,18 @@ -'use strict'; - -const format = require('format-io'); +import process from 'node:process'; +import format from 'format-io'; +import info from '../../package.json' with { + type: 'json', +}; -const {version} = require('../../package'); +const {version} = info; const getMemory = () => { const {rss} = process.memoryUsage(); return format.size(rss); }; -module.exports = (prefix) => ({ +export default (prefix) => ({ version, prefix, memory: getMemory(), }); - diff --git a/server/rest/info.spec.js b/server/rest/info.spec.js index a067cc6867..cac19e53f9 100644 --- a/server/rest/info.spec.js +++ b/server/rest/info.spec.js @@ -1,14 +1,11 @@ -'use strict'; - -const test = require('supertape'); -const info = require('./info'); -const stub = require('@cloudcmd/stub'); +import process from 'node:process'; +import {test, stub} from 'supertape'; +import info from './info.js'; test('cloudcmd: rest: info', (t) => { const {memoryUsage} = process; - const _memoryUsage = stub() - .returns({}); + const _memoryUsage = stub().returns({}); process.memoryUsage = _memoryUsage; @@ -16,7 +13,6 @@ test('cloudcmd: rest: info', (t) => { process.memoryUsage = memoryUsage; - t.ok(_memoryUsage.calledWith(), 'should call memoryUsage'); + t.calledWithNoArgs(_memoryUsage, 'should call memoryUsage'); t.end(); }); - diff --git a/server/root.js b/server/root.js index 105028ed05..b563d77d65 100644 --- a/server/root.js +++ b/server/root.js @@ -1,8 +1,5 @@ -'use strict'; +import mellow from 'mellow'; -const mellow = require('mellow'); - -module.exports = (dir, root) => { - return mellow.webToWin(dir, root || '/'); +export default (dir, root, {webToWin = mellow.webToWin} = {}) => { + return webToWin(dir, root || '/'); }; - diff --git a/server/root.spec.js b/server/root.spec.js index af640fbd18..2d9dd76878 100644 --- a/server/root.spec.js +++ b/server/root.spec.js @@ -1,37 +1,16 @@ -'use strict'; - -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); -const mockRequire = require('mock-require'); -const {reRequire, stopAll} = mockRequire; - -const pathConfig = './config'; -const pathRoot = './root'; +import {test, stub} from 'supertape'; +import root from './root.js'; test('cloudcmd: root: mellow', (t) => { - const config = stub().returns(''); const webToWin = stub(); - const mellow = { - webToWin, - }; - - mockRequire('mellow', mellow); - mockRequire(pathConfig, config); - - const root = reRequire(pathRoot); const dir = 'hello'; const dirRoot = '/'; - root(dir); - - mockRequire.stop('mellow'); - mockRequire.stopAll(pathConfig); - reRequire(pathRoot); - - stopAll(); + root(dir, '', { + webToWin, + }); t.calledWith(webToWin, [dir, dirRoot], 'should call mellow'); t.end(); }); - diff --git a/server/route.js b/server/route.js index c35c1716d4..7844ce096c 100644 --- a/server/route.js +++ b/server/route.js @@ -1,28 +1,25 @@ -'use strict'; - -const DIR_SERVER = './'; -const DIR_COMMON = '../common/'; - -const {extname} = require('path'); - -const {read} = require('win32'); -const ponse = require('ponse'); -const rendy = require('rendy'); -const format = require('format-io'); -const currify = require('currify'); -const wraptile = require('wraptile'); -const tryToCatch = require('try-to-catch'); -const once = require('once'); -const pipe = require('pipe-io'); -const {contentType} = require('mime-types'); - -const root = require(DIR_SERVER + 'root'); -const prefixer = require(DIR_SERVER + 'prefixer'); -const CloudFunc = require(DIR_COMMON + 'cloudfunc'); - -const getPrefix = (config) => prefixer(config('prefix')); - -const onceRequire = once(require); +import {createRequire} from 'node:module'; +import {extname} from 'node:path'; +import * as _win32 from 'win32'; +import * as ponse from 'ponse'; +import {rendy} from 'rendy'; +import format from 'format-io'; +import currify from 'currify'; +import wraptile from 'wraptile'; +import {tryToCatch} from 'try-to-catch'; +import once from 'once'; +import pipe from 'pipe-io'; +import {contentType} from 'mime-types'; +import * as CloudFunc from '#common/cloudfunc'; +import root from './root.js'; +import prefixer from './prefixer.js'; +import Template from './template.js'; +import {getColumns} from './columns.js'; +import {getThemes} from './theme.js'; + +const require = createRequire(import.meta.url); +const {stringify} = JSON; +const {FS} = CloudFunc; const sendIndex = (params, data) => { const ponseParams = { @@ -33,14 +30,12 @@ const sendIndex = (params, data) => { ponse.send(data, ponseParams); }; -const {FS} = CloudFunc; - -const Columns = require(`${DIR_SERVER}/columns`); -const Template = require(`${DIR_SERVER}/template`); +const onceRequire = once(require); +const getPrefix = (config) => prefixer(config('prefix')); -const getReadDir = (config) => { +const getReadDir = (config, {win32 = _win32} = {}) => { if (!config('dropbox')) - return read; + return win32.read; const {readDir} = onceRequire('@cloudcmd/dropbox'); @@ -50,22 +45,27 @@ const getReadDir = (config) => { /** * routing of server queries */ -module.exports = currify((config, options, request, response, next) => { +export default currify((config, options, request, response, next) => { const name = ponse.getPathName(request); - const isFS = RegExp('^/$|^' + FS).test(name); + const isFS = RegExp(`^/$|^${FS}`).test(name); if (!isFS) return next(); - route({config, options, request, response}) - .catch(next); + route({ + config, + options, + request, + response, + }).catch(next); }); -module.exports._getReadDir = getReadDir; +export const _getReadDir = getReadDir; async function route({config, options, request, response}) { const name = ponse.getPathName(request); const gzip = true; + const p = { request, response, @@ -78,13 +78,19 @@ async function route({config, options, request, response}) { const rootName = name.replace(CloudFunc.FS, '') || '/'; const fullPath = root(rootName, config('root')); - const read = getReadDir(config); + 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, + }); + const [error, stream] = await tryToCatch(read, fullPath, { root: config('root'), }); - const {html} = options; - if (error) return ponse.sendError(error, p); @@ -102,10 +108,7 @@ async function route({config, options, request, response}) { response.setHeader('Content-Length', contentLength); response.setHeader('Content-Type', contentType(extname(fullPath))); - await pipe([ - stream, - response, - ]); + await pipe([stream, response]); } /** @@ -131,20 +134,16 @@ function indexProcessing(config, options) { .replace('icon-copy', 'icon-copy none'); if (noContact) - data = data - .replace('icon-contact', 'icon-contact none'); + data = data.replace('icon-contact', 'icon-contact none'); if (noConfig) - data = data - .replace('icon-config', 'icon-config none'); + data = data.replace('icon-config', 'icon-config none'); if (noConsole) - data = data - .replace('icon-console', 'icon-console none'); + data = data.replace('icon-console', 'icon-console none'); if (noTerminal) - data = data - .replace('icon-terminal', 'icon-terminal none'); + data = data.replace('icon-terminal', 'icon-terminal none'); const left = rendy(Template.panel, { side: 'left', @@ -169,8 +168,9 @@ function indexProcessing(config, options) { }), fm: left + right, prefix: getPrefix(config), - config: JSON.stringify(config('*')), - columns: Columns[config('columns')], + config: stringify(config('*')), + columns: getColumns()[config('columns')], + themes: getThemes()[config('theme')], }); return data; @@ -181,6 +181,7 @@ function buildIndex(config, html, data) { data, prefix: getPrefix(config), template: Template, + showDotFiles: config('showDotFiles'), }); return indexProcessing(config, { @@ -189,7 +190,8 @@ function buildIndex(config, html, data) { }); } -module.exports._hideKeysPanel = hideKeysPanel; +export const _hideKeysPanel = hideKeysPanel; + function hideKeysPanel(html) { const keysPanel = '
    { options, }); - t.ok(/icon-console none/.test(body), 'should hide console'); + t.match(body, 'icon-console none', 'should hide console'); t.end(); }); @@ -78,7 +70,7 @@ test('cloudcmd: route: buttons: no config', async (t) => { options, }); - t.ok(/icon-config none/.test(body), 'should hide config'); + t.match(body, 'icon-config none', 'should hide config'); t.end(); }); @@ -95,7 +87,7 @@ test('cloudcmd: route: buttons: no contact', async (t) => { options, }); - t.ok(/icon-contact none/.test(body), 'should hide contact'); + t.match(body, 'icon-contact none', 'should hide contact'); t.end(); }); @@ -112,28 +104,11 @@ test('cloudcmd: route: buttons: one file panel: move', async (t) => { options, }); - t.ok(/icon-move none/.test(body), 'should hide move button'); - t.end(); -}); - -test('cloudcmd: route: buttons: no one file panel: move', async (t) => { - const config = { - oneFilePanel: false, - }; - - const options = { - config, - }; - - const {body} = await request.get('/', { - options, - }); - - t.notOk(/icon-move none/.test(body), 'should not hide move button'); + t.match(body, 'icon-move none', 'should hide move button'); t.end(); }); -test('cloudcmd: route: buttons: one file panel: move', async (t) => { +test('cloudcmd: route: buttons: one file panel: copy', async (t) => { const config = { oneFilePanel: true, }; @@ -146,7 +121,7 @@ test('cloudcmd: route: buttons: one file panel: move', async (t) => { options, }); - t.ok(/icon-copy none/.test(body), 'should hide copy button'); + t.match(body, 'icon-copy none', 'should hide copy button'); t.end(); }); @@ -163,7 +138,7 @@ test('cloudcmd: route: keys panel: hide', async (t) => { options, }); - t.ok(/keyspanel hidden/.test(body), 'should hide keyspanel'); + t.match(body, 'keyspanel hidden', 'should hide keyspanel'); t.end(); }); @@ -229,8 +204,9 @@ test('cloudcmd: route: not found', async (t) => { test('cloudcmd: route: sendIndex: encode', async (t) => { const name = '">'; - const nameEncoded = '"><svg onload=alert(3);>'; + const nameEncoded = '"><svg onload=alert(3);>'; const path = '/'; + const files = [{ name, }]; @@ -248,28 +224,26 @@ test('cloudcmd: route: sendIndex: encode', async (t) => { const read = stub().resolves(stream); - mockRequire('win32', { + cloudcmd.depStore('win32', { read, }); - reRequire(routePath); - const cloudcmd = reRequire(cloudcmdPath); - const {request} = serveOnce(cloudcmd, { configManager: createConfigManager(), }); const {body} = await request.get('/'); - stopAll(); + cloudcmd.depStore(); - t.ok(body.includes(nameEncoded), 'should encode name'); + t.match(body, nameEncoded, 'should encode name'); t.end(); }); test('cloudcmd: route: sendIndex: encode: not encoded', async (t) => { const name = '">'; const path = '/'; + const files = [{ name, }]; @@ -287,17 +261,14 @@ test('cloudcmd: route: sendIndex: encode: not encoded', async (t) => { const read = stub().resolves(stream); - mockRequire('win32', { + cloudcmd.depStore('win32', { read, }); - reRequire(routePath); - const cloudcmd = reRequire(cloudcmdPath); - const {request} = serveOnce(cloudcmd); const {body} = await request.get('/'); - stopAll(); + cloudcmd.depStore(); t.notOk(body.includes(name), 'should not put not encoded name'); t.end(); @@ -306,6 +277,7 @@ test('cloudcmd: route: sendIndex: encode: not encoded', async (t) => { test('cloudcmd: route: sendIndex: ddos: render', async (t) => { const name = `$$$'"`; const path = '/'; + const files = [{ name, }]; @@ -323,20 +295,16 @@ test('cloudcmd: route: sendIndex: ddos: render', async (t) => { const read = stub().resolves(stream); - mockRequire('win32', { + cloudcmd.depStore('win32', { read, }); - - reRequire(routePath); - const cloudcmd = reRequire(cloudcmdPath); - const {request} = serveOnce(cloudcmd, { config: defaultConfig, }); const {status} = await request.get('/'); - stopAll(); + cloudcmd.depStore(); t.equal(status, 200, 'should not hang up'); t.end(); @@ -355,11 +323,11 @@ test('cloudcmd: route: buttons: no terminal', async (t) => { options, }); - t.ok(/icon-terminal none/.test(body), 'should hide terminal'); + t.match(body, 'icon-terminal none', 'should hide terminal'); t.end(); }); -test('cloudcmd: route: no termianl: /fs', async (t) => { +test('cloudcmd: route: no terminal: /fs', async (t) => { const config = { terminal: false, }; @@ -370,11 +338,12 @@ test('cloudcmd: route: no termianl: /fs', async (t) => { }; const {request} = serveOnce(cloudcmd); + const {body} = await request.get('/fs', { options, }); - t.ok(/icon-terminal none/.test(body), 'should hide terminal'); + t.match(body, 'icon-terminal none', 'should hide terminal'); t.end(); }); @@ -392,14 +361,14 @@ test('cloudcmd: route: buttons: terminal: can not load', async (t) => { options, }); - t.ok(/icon-terminal none/.test(body), 'should not enable terminal'); + t.match(body, 'icon-terminal none', 'should not enable terminal'); t.end(); }); test('cloudcmd: route: buttons: terminal', async (t) => { const config = { terminal: true, - terminalPath: 'console-io', + terminalPath: 'gritty', }; const options = { @@ -424,6 +393,7 @@ test('cloudcmd: route: buttons: contact', async (t) => { }; const {request} = serveOnce(cloudcmd); + const {body} = await request.get('/', { options, }); @@ -437,12 +407,10 @@ test('cloudcmd: route: dropbox', async (t) => { config('dropbox', true); config('dropboxToken', ''); - const {_getReadDir} = reRequire(routePath); - const readdir = _getReadDir(config); const [e] = await tryToCatch(readdir, '/root'); - t.ok(/token/.test(e.message), 'should contain word token in message'); + t.match(e.message, 'API', 'should contain word token in message'); t.end(); }); @@ -463,19 +431,17 @@ test('cloudcmd: route: content length', async (t) => { test('cloudcmd: route: read: root', async (t) => { const stream = Readable.from('hello'); + stream.contentLength = 5; const read = stub().returns(stream); - - mockRequire('win32', { + cloudcmd.depStore('win32', { read, }); - reRequire(routePath); - - const cloudcmd = reRequire(cloudcmdPath); const configManager = createConfigManager(); const root = '/hello'; + configManager('root', root); const {request} = serveOnce(cloudcmd, { @@ -483,16 +449,12 @@ test('cloudcmd: route: read: root', async (t) => { }); await request.get('/fs/route.js'); + cloudcmd.depStore(); - const expected = [ - '/hello/route.js', { - root, - }, - ]; - - stopAll(); + const expected = ['/hello/route.js', { + root, + }]; t.calledWith(read, expected); t.end(); }); - diff --git a/server/server.js b/server/server.js index ec97345d32..06b6464a5a 100644 --- a/server/server.js +++ b/server/server.js @@ -1,62 +1,67 @@ -'use strict'; +import http from 'node:http'; +import {promisify} from 'node:util'; +import process from 'node:process'; +import {rateLimit} from 'express-rate-limit'; +import currify from 'currify'; +import squad from 'squad'; +import {tryToCatch} from 'try-to-catch'; +import opn from 'open'; +import express from 'express'; +import {Server} from 'socket.io'; +import wraptile from 'wraptile'; +import compression from 'compression'; +import tryRequire from 'tryrequire'; +import {cloudcmd} from '#server/cloudcmd'; +import exit from './exit.js'; -const DIR_SERVER = './'; -const cloudcmd = require(DIR_SERVER + 'cloudcmd'); +const RATE_LIMIT = 1000; +const RATE_WINDOW = 15 * 60 * 1000; -const http = require('http'); -const {promisify} = require('util'); -const currify = require('currify'); -const squad = require('squad'); -const tryToCatch = require('try-to-catch'); -const wraptile = require('wraptile'); -const compression = require('compression'); -const threadIt = require('thread-it'); - -const two = currify((f, a, b) => f(a, b)); -const exit = require(DIR_SERVER + 'exit'); - -const exitPort = two(exit, 'cloudcmd --port: %s'); const bind = (f, self) => f.bind(self); -const promisifySelf = squad(promisify, bind); -const shutdown = wraptile(async (promises) => { - console.log('closing cloudcmd...'); +const two = currify((f, a, b) => f(a, b)); +const shutdown = wraptile(async (config, promises) => { + if (config('log')) + console.log('closing cloudcmd...'); + await Promise.all(promises); - threadIt.terminate(); process.exit(0); }); -const opn = require('open'); -const express = require('express'); -const io = require('socket.io'); +const promisifySelf = squad(promisify, bind); + +const exitPort = two(exit, 'cloudcmd --port: %s'); -const tryRequire = require('tryrequire'); const logger = tryRequire('morgan'); -module.exports = async (options, config) => { +export default async (options, config) => { const prefix = config('prefix'); - const port = process.env.PORT || /* c9 */ - config('port'); + const port = process.env.PORT /* c9 */ || config('port'); - const ip = process.env.IP || /* c9 */ - config('ip') || - '0.0.0.0'; + const ip = process.env.IP /* c9 */ || config('ip') || '0.0.0.0'; const app = express(); const server = http.createServer(app); - if (logger) + if (config('log') && logger) app.use(logger('dev')); if (prefix) - app.get('/', (req, res) => res.redirect(prefix + '/')); + app.get('/', (req, res) => res.redirect(`${prefix}/`)); - const socketServer = io(server, { + const socketServer = new Server(server, { path: `${prefix}/socket.io`, }); - app.use(compression()); + const limiter = rateLimit({ + windowMs: RATE_WINDOW, + limit: RATE_LIMIT, + }); + + app.set('trust proxy', 1); + app.use(compression()); + app.use(limiter); app.use(prefix, cloudcmd({ config: options, socket: socketServer, @@ -73,13 +78,17 @@ module.exports = async (options, config) => { server.on('error', exitPort); await listen(port, ip); - const close = shutdown([closeServer, closeSocket]); + const close = shutdown(config, [closeServer, closeSocket]); + process.on('SIGINT', close); + process.on('SIGUSR1', close); const host = config('ip') || 'localhost'; const port0 = port || server.address().port; const url = `http://${host}:${port0}${prefix}/`; - console.log(`url: ${url}`); + + if (config('log')) + console.log(`url: ${url}`); if (!config('open')) return; @@ -89,4 +98,3 @@ module.exports = async (options, config) => { if (openError) console.error('cloudcmd --open:', openError.message); }; - diff --git a/server/show-config.js b/server/show-config.js index 4591d068c0..f151dd103b 100644 --- a/server/show-config.js +++ b/server/show-config.js @@ -1,14 +1,17 @@ -'use strict'; - -const { +import { table, getBorderCharacters, -} = require('table'); +} from 'table'; -module.exports = (config) => { +export const showConfig = (config) => { check(config); - const data = Object.keys(config).map((name) => [name, config[name]]); + const data = Object + .keys(config) + .map((name) => [ + name, + config[name], + ]); if (!data.length) return ''; @@ -31,4 +34,3 @@ function check(config) { if (typeof config !== 'object') throw Error('config should be an object!'); } - diff --git a/test/server/show-config.js b/server/show-config.spec.js similarity index 82% rename from test/server/show-config.js rename to server/show-config.spec.js index b3b4cf0efa..b25e56a256 100644 --- a/test/server/show-config.js +++ b/server/show-config.spec.js @@ -1,9 +1,6 @@ -'use strict'; - -const test = require('supertape'); -const tryCatch = require('try-catch'); - -const showConfig = require('../../server/show-config'); +import {test} from 'supertape'; +import {tryCatch} from 'try-catch'; +import {showConfig} from './show-config.js'; test('cloudcmd: show-config: no arguments', (t) => { const [error] = tryCatch(showConfig); @@ -19,7 +16,7 @@ test('cloudcmd: show-config: bad arguments', (t) => { t.end(); }); -test('cloudcmd: show-config: return', (t) => { +test('cloudcmd: show-config: empty: return', (t) => { t.equal(showConfig({}), '', 'should return string'); t.end(); }); @@ -38,4 +35,3 @@ test('cloudcmd: show-config: return', (t) => { t.equal(showConfig(config), result, 'should return table'); t.end(); }); - diff --git a/server/template.js b/server/template.js index 150b63fde5..0e705e79de 100644 --- a/server/template.js +++ b/server/template.js @@ -1,8 +1,9 @@ -'use strict'; +import path, {dirname} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import readFilesSync from '@cloudcmd/read-files-sync'; -const path = require('path'); -const readFilesSync = require('@cloudcmd/read-files-sync'); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); const templatePath = path.join(__dirname, '..', 'tmpl/fs'); -module.exports = readFilesSync(templatePath, 'utf8'); - +export default readFilesSync(templatePath, 'utf8'); diff --git a/server/terminal.js b/server/terminal.js index 9d1e9c35e1..c6d80df94e 100644 --- a/server/terminal.js +++ b/server/terminal.js @@ -1,6 +1,7 @@ -'use strict'; +import {createRequire} from 'node:module'; +import {tryCatch} from 'try-catch'; -const tryCatch = require('try-catch'); +const require = createRequire(import.meta.url); const noop = (req, res, next) => { next && next(); @@ -8,11 +9,18 @@ const noop = (req, res, next) => { noop.listen = noop; -module.exports = (config, arg) => { +const parseDefault = (a) => a.default || a; +const _getModule = (a) => parseDefault(require(a)); + +export default (config, arg, overrides = {}) => { + const { + getModule = _getModule, + } = overrides; + if (!config('terminal')) return noop; - const [e, terminalModule] = tryCatch(require, config('terminalPath')); + const [e, terminalModule] = tryCatch(getModule, config('terminalPath')); if (!e && !arg) return terminalModule; @@ -25,4 +33,3 @@ module.exports = (config, arg) => { return noop; }; - diff --git a/server/terminal.spec.js b/server/terminal.spec.js index d734b54785..22f20282ab 100644 --- a/server/terminal.spec.js +++ b/server/terminal.spec.js @@ -1,15 +1,6 @@ -'use strict'; - -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); - -const mockRequire = require('mock-require'); - -const terminalPath = './terminal'; -const terminal = require('./terminal'); -const {createConfigManager} = require('./cloudcmd'); - -const {stopAll} = mockRequire; +import {test, stub} from 'supertape'; +import {createConfigManager} from '#server/cloudcmd'; +import terminal from './terminal.js'; test('cloudcmd: terminal: disabled', (t) => { const config = createConfigManager(); @@ -34,13 +25,12 @@ test('cloudcmd: terminal: disabled: listen', (t) => { test('cloudcmd: terminal: enabled', (t) => { const term = stub(); const arg = 'hello'; + const config = stub().returns(true); + const getModule = stub().returns(term); - mockRequire(terminalPath, term); - - const terminal = require(terminalPath); - terminal(arg); - - stopAll(); + terminal(config, arg, { + getModule, + }); t.calledWith(term, [arg], 'should call terminal'); t.end(); @@ -68,18 +58,16 @@ test('cloudcmd: terminal: enabled: no string', (t) => { test('cloudcmd: terminal: no arg', (t) => { const gritty = {}; - - mockRequire('gritty', gritty); + const getModule = stub().returns(gritty); const config = createConfigManager(); config('terminal', true); config('terminalPath', 'gritty'); - const result = terminal(config); - - stopAll(); + const result = terminal(config, '', { + getModule, + }); t.equal(result, gritty); t.end(); }); - diff --git a/server/theme.js b/server/theme.js new file mode 100644 index 0000000000..d66623b55b --- /dev/null +++ b/server/theme.js @@ -0,0 +1,33 @@ +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 * 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|js)$/.test(a); +const not = (fn) => (a) => !fn(a); + +const _isDev = fullstore(process.env.NODE_ENV === 'development'); +const getDist = (isDev) => isDev ? 'dist-dev' : 'dist'; + +export const isDev = _isDev; + +export const getThemes = ({isDev = _isDev()} = {}) => { + return readFilesSyncMemo(isDev); +}; + +const readFilesSyncMemo = nanomemoize((isDev) => { + const dist = getDist(isDev); + const themesDir = path.join(__dirname, '..', dist, 'themes'); + + const names = fs + .readdirSync(themesDir) + .filter(not(isMap)); + + return readFilesSync(themesDir, names, 'utf8'); +}); diff --git a/server/themes.spec.js b/server/themes.spec.js new file mode 100644 index 0000000000..1fc95ec889 --- /dev/null +++ b/server/themes.spec.js @@ -0,0 +1,32 @@ +import {dirname} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import fs from 'node:fs'; +import test from 'supertape'; +import {getThemes, isDev} from './theme.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +test('themes: dev', (t) => { + const themes = getThemes({ + isDev: true, + }); + + const css = fs.readFileSync(`${__dirname}/../css/themes/dark.css`, 'utf8'); + const result = themes.dark.includes(css); + + t.ok(result); + t.end(); +}); + +test('themes: no args', (t) => { + const currentIsDev = isDev(); + isDev(true); + const themes = getThemes(); + + const css = fs.readFileSync(`${__dirname}/../css/themes/light.css`, 'utf8'); + isDev(currentIsDev); + + t.match(themes.light, css); + t.end(); +}); diff --git a/server/user-menu.js b/server/user-menu.js index 6410b99e98..5359955374 100644 --- a/server/user-menu.js +++ b/server/user-menu.js @@ -1,27 +1,19 @@ -'use strict'; - -const {homedir} = require('os'); -const {readFile} = require('fs/promises'); - -const {join} = require('path'); - -const montag = require('montag'); -const tryToCatch = require('try-to-catch'); -const currify = require('currify'); -const threadIt = require('thread-it'); -const {codeframe} = require('putout'); -const putout = threadIt(require.resolve('putout')); - -threadIt.init(); +import {homedir} from 'node:os'; +import {readFile as _readFile} from 'node:fs/promises'; +import {join} from 'node:path'; +// warm up worker cache +import {montag} from 'montag'; +import {tryToCatch} from 'try-to-catch'; +import currify from 'currify'; +import {putout, codeframe} from 'putout'; // warm up worker cache transpile(''); -const URL = '/api/v1/user-menu'; -const DEFAULT_MENU_PATH = join(__dirname, '../static/user-menu.js'); +const PREFIX = '/api/v1/user-menu'; -module.exports = currify(async ({menuName}, req, res, next) => { - if (req.url.indexOf(URL)) +export const userMenu = currify(async ({menuName, readFile = _readFile, config}, req, res, next) => { + if (!req.url.startsWith(PREFIX)) return next(); const {method} = req; @@ -31,22 +23,28 @@ module.exports = currify(async ({menuName}, req, res, next) => { req, res, menuName, + readFile, + config, }); next(); }); -async function onGET({req, res, menuName}) { +async function onGET({req, res, menuName, readFile, config}) { const {dir} = req.query; - const url = req.url.replace(URL, ''); + const url = req.url.replace(PREFIX, ''); if (url === '/default') - return sendDefaultMenu(res); + return await sendDefaultMenu(res, { + readFile, + }); const {findUp} = await import('find-up'); - const [errorFind, currentMenuPath] = await tryToCatch(findUp, [ - menuName, - ], {cwd: dir}); + + const cwd = join(config('root'), dir); + const [errorFind, currentMenuPath] = await tryToCatch(findUp, [menuName], { + cwd, + }); if (errorFind && errorFind.code !== 'ENOENT') return res @@ -63,7 +61,9 @@ async function onGET({req, res, menuName}) { .send(e.message); if (e) - return sendDefaultMenu(res); + return await sendDefaultMenu(res, { + readFile, + }); const [parseError, result] = await transpile(source); @@ -79,11 +79,11 @@ async function onGET({req, res, menuName}) { 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'; @@ -91,19 +91,35 @@ function getError(error, source) { `; } -function sendDefaultMenu(res) { - res.sendFile(DEFAULT_MENU_PATH, { - cacheControl: false, +async function sendDefaultMenu(res, {readFile}) { + const menu = await getDefaultMenu({ + readFile, }); + + res + .type('js') + .send(menu); } async function transpile(source) { return await tryToCatch(putout, source, { + rules: { + 'nodejs/convert-esm-to-commonjs': 'on', + }, plugins: [ - 'convert-esm-to-commonjs', - 'strict-mode', + 'nodejs', 'cloudcmd', ], }); } +async function getDefaultMenu({readFile}) { + const DEFAULT_MENU_PATH = new URL('../static/user-menu.js', import.meta.url).pathname; + const menu = await readFile(DEFAULT_MENU_PATH, 'utf8'); + const [error, result] = await transpile(menu); + + if (error) + return String(error); + + return result.code; +} diff --git a/server/user-menu.spec.js b/server/user-menu.spec.js index 623dcfa776..bb78b4e858 100644 --- a/server/user-menu.spec.js +++ b/server/user-menu.spec.js @@ -1,23 +1,18 @@ -'use strict'; +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 {putout} from 'putout'; +import {userMenu} from './user-menu.js'; -const fs = require('fs'); -const {join} = require('path'); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); -const test = require('supertape'); -const serveOnce = require('serve-once'); -const threadIt = require('thread-it'); -const stub = require('@cloudcmd/stub'); -const {reRequire} = require('mock-require'); - -const userMenu = require('./user-menu'); const {request} = serveOnce(userMenu); - -const {readFileSync} = fs; - const userMenuPath = join(__dirname, '..', '.cloudcmd.menu.js'); -const userMenuFile = readFileSync(userMenuPath, 'utf8'); -const fixtureDir = join(__dirname, 'fixture-user-menu'); +const fixtureDir = new URL('fixture-user-menu', import.meta.url).pathname; const fixtureMoveName = join(fixtureDir, 'io-mv.js'); const fixtureMoveFixName = join(fixtureDir, 'io-mv-fix.js'); const fixtureCopyName = join(fixtureDir, 'io-cp.js'); @@ -29,7 +24,11 @@ const fixtureCopy = readFileSync(fixtureCopyName, 'utf8'); const fixtureCopyFix = readFileSync(fixtureCopyFixName, 'utf8'); test('cloudcmd: user menu', async (t) => { + const config = stub().returns(''); + const userMenuFile = getUserMenuFile(); + const options = { + config, menuName: '.cloudcmd.menu.js', }; @@ -37,53 +36,94 @@ test('cloudcmd: user menu', async (t) => { options, }); - threadIt.terminate(); - t.equal(userMenuFile, body); t.end(); }); test('cloudcmd: user menu: io.mv', async (t) => { + const config = stub().returns(''); + const readFile = stub().returns(fixtureMove); + const options = { menuName: '.cloudcmd.menu.js', + readFile, + config, }; - const {readFile} = fs.promises; - - fs.promises.readFile = stub().returns(fixtureMove); - const userMenu = reRequire('./user-menu'); const {request} = serveOnce(userMenu); const {body} = await request.get(`/api/v1/user-menu?dir=${__dirname}`, { options, }); - threadIt.terminate(); - fs.promises.readFile = readFile; + t.equal(body, fixtureMoveFix); + t.end(); +}); + +test('cloudcmd: user menu: default menu', async (t) => { + const config = stub().returns(''); + const options = { + menuName: '111.cloudcmd.menu.js', + config, + }; + + const {request} = serveOnce(userMenu); + + const {body} = await request.get(`/api/v1/user-menu?dir=abcd`, { + options, + }); - t.equal(fixtureMoveFix, body); + t.match(body, 'module.exports'); t.end(); }); test('cloudcmd: user menu: io.cp', async (t) => { + const config = stub().returns(''); + const readFile = stub().returns(fixtureCopy); + const options = { menuName: '.cloudcmd.menu.js', + readFile, + config, }; - const {readFile} = fs.promises; - - fs.promises.readFile = stub().returns(fixtureCopy); - const userMenu = reRequire('./user-menu'); const {request} = serveOnce(userMenu); const {body} = await request.get(`/api/v1/user-menu?dir=${__dirname}`, { options, }); - threadIt.terminate(); - fs.promises.readFile = readFile; + t.equal(body, fixtureCopyFix); + t.end(); +}); + +test('cloudcmd: user menu: broken file', async (t) => { + const readFile = stub().returns('sdfsdf /; s'); + const options = { + menuName: '.cloudcmd.menu.js', + readFile, + }; + + const {request} = serveOnce(userMenu); + + const {body} = await request.get(`/api/v1/user-menu/default`, { + options, + }); + + const expected = 'SyntaxError: Unexpected token (1:8)'; - t.equal(fixtureCopyFix, body); + t.equal(body, expected); t.end(); }); +function getUserMenuFile() { + const userMenuFile = readFileSync(userMenuPath, 'utf8'); + const {code} = putout(userMenuFile, { + rules: { + 'nodejs/convert-esm-to-commonjs': 'on', + }, + plugins: ['nodejs'], + }); + + return code; +} diff --git a/server/validate.js b/server/validate.js index 498cd78e86..e01d050533 100644 --- a/server/validate.js +++ b/server/validate.js @@ -1,12 +1,18 @@ -'use strict'; +import {statSync as _statSync} from 'node:fs'; +import {tryCatch} from 'try-catch'; +import _exit from './exit.js'; +import {getColumns as _getColumns} from './columns.js'; +import {getThemes as _getThemes} from './theme.js'; -const tryCatch = require('try-catch'); +const isString = (a) => typeof a === 'string'; -const exit = require('./exit'); -const columns = require('./columns'); - -module.exports.root = (dir, config) => { - if (typeof dir !== 'string') +export const root = (dir, config, overrides = {}) => { + const { + exit = _exit, + statSync = _statSync, + } = overrides; + + if (!isString(dir)) throw Error('dir should be a string'); if (dir === '/') @@ -15,31 +21,43 @@ module.exports.root = (dir, config) => { if (config('dropbox')) return; - const {statSync} = require('fs'); const [error] = tryCatch(statSync, dir); if (error) return exit('cloudcmd --root: %s', error.message); }; -module.exports.editor = (name) => { - const reg = /^(dword|edward|deepword)$/; +export const editor = (name, {exit = _exit} = {}) => { + const reg = /^(dword|edward|deepword|qword)$/; + + if (!reg.test(name)) + exit(`cloudcmd --editor: could be "dword", "edward", "deepword" or "qword" only, received: "${name}"`); +}; + +export const menu = (name, {exit = _exit} = {}) => { + const reg = /^(supermenu|aleman)$/; if (!reg.test(name)) - exit('cloudcmd --editor: could be "dword", "edward" or "deepword" only'); + exit('cloudcmd --menu: could be "supermenu" or "aleman" only'); }; -module.exports.packer = (name) => { +export const packer = (name, {exit = _exit} = {}) => { const reg = /^(tar|zip)$/; if (!reg.test(name)) exit('cloudcmd --packer: could be "tar" or "zip" only'); }; -module.exports.columns = (type) => { +export const columns = (type, overrides = {}) => { + const { + exit = _exit, + getColumns = _getColumns, + } = overrides; + const addQuotes = (a) => `"${a}"`; + const all = Object - .keys(columns) + .keys(getColumns()) .concat(''); const names = all @@ -51,3 +69,23 @@ module.exports.columns = (type) => { exit(`cloudcmd --columns: can be only one of: ${names}`); }; +export const theme = (type, overrides = {}) => { + const { + exit = _exit, + getThemes = _getThemes, + } = overrides; + + const addQuotes = (a) => `"${a}"`; + + const all = Object + .keys(getThemes()) + .concat(''); + + const names = all + .filter(Boolean) + .map(addQuotes) + .join(', '); + + if (!all.includes(type)) + exit(`cloudcmd --theme: can be only one of: ${names}`); +}; diff --git a/server/validate.spec.js b/server/validate.spec.js index 1486853038..76243e3073 100644 --- a/server/validate.spec.js +++ b/server/validate.spec.js @@ -1,29 +1,16 @@ -'use strict'; - -const fs = require('fs'); - -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); -const tryCatch = require('try-catch'); -const mockRequire = require('mock-require'); -const {reRequire, stopAll} = mockRequire; - -const dir = '..'; - -const validatePath = `${dir}/server/validate`; -const exitPath = `${dir}/server/exit`; -const columnsPath = `${dir}/server/columns`; -const cloudcmdPath = `${dir}/server/cloudcmd`; - -const validate = require(validatePath); -const cloudcmd = require(cloudcmdPath); +import {test, stub} from 'supertape'; +import {tryCatch} from 'try-catch'; +import {cloudcmd} from '#server/cloudcmd'; +import * as validate from './validate.js'; test('validate: root: bad', (t) => { const config = { root: Math.random(), }; - const [e] = tryCatch(cloudcmd, {config}); + const [e] = tryCatch(cloudcmd, { + config, + }); t.equal(e.message, 'dir should be a string', 'should throw'); t.end(); @@ -42,97 +29,105 @@ test('validate: root: /', (t) => { const fn = stub(); validate.root('/', fn); - t.notOk(fn.called, 'should not call fn'); + t.notCalled(fn, 'should not call fn'); t.end(); }); test('validate: root: stat', (t) => { - const fn = stub(); - const {statSync} = fs; - + const config = stub(); const error = 'ENOENT'; - fs.statSync = () => { - throw Error(error); - }; - - mockRequire(exitPath, fn); - - const {root} = reRequire(validatePath); + const statSync = stub().throws(Error(error)); + const exit = stub(); - root('hello', fn); + validate.root('hello', config, { + statSync, + exit, + }); const msg = 'cloudcmd --root: %s'; - fs.statSync = statSync; - - stopAll(); - t.calledWith(fn, [msg, error], 'should call fn'); + t.calledWith(exit, [msg, error], 'should call fn'); t.end(); }); test('validate: packer: not valid', (t) => { - const fn = stub(); - - mockRequire(exitPath, fn); - - const {packer} = reRequire(validatePath); + const exit = stub(); const msg = 'cloudcmd --packer: could be "tar" or "zip" only'; - packer('hello'); - - stopAll(); + validate.packer('hello', { + exit, + }); - t.calledWith(fn, [msg], 'should call fn'); + t.calledWith(exit, [msg], 'should call fn'); t.end(); }); test('validate: editor: not valid', (t) => { - const fn = stub(); - - mockRequire(exitPath, fn); - - const {editor} = reRequire(validatePath); - const msg = 'cloudcmd --editor: could be "dword", "edward" or "deepword" only'; + const exit = stub(); + const msg = 'cloudcmd --editor: could be "dword", "edward", "deepword" or "qword" only, received: "hello"'; - editor('hello'); - - stopAll(); + validate.editor('hello', { + exit, + }); - t.calledWith(fn, [msg], 'should call fn'); + t.calledWith(exit, [msg], 'should call fn'); t.end(); }); test('validate: columns', (t) => { - const fn = stub(); - mockRequire(exitPath, fn); + const exit = stub(); - const {columns} = require(validatePath); - - columns('name-size-date'); - - stopAll(); + validate.columns('name-size-date', { + exit, + }); - t.notOk(fn.called, 'should not call exit'); + t.notCalled(exit, 'should not call exit'); t.end(); }); test('validate: columns: wrong', (t) => { - const fn = stub(); - - mockRequire(exitPath, fn); - mockRequire(columnsPath, { + const getColumns = stub().returns({ 'name-size-date': '', 'name-size': '', }); - const {columns} = reRequire(validatePath); + const exit = stub(); const msg = 'cloudcmd --columns: can be only one of: "name-size-date", "name-size"'; - columns('hello'); + validate.columns('hello', { + exit, + getColumns, + }); - stopAll(); + t.calledWith(exit, [msg], 'should call exit'); + t.end(); +}); + +test('validate: theme', (t) => { + const exit = stub(); + + validate.theme('dark', { + exit, + }); - t.calledWith(fn, [msg], 'should call exit'); + t.notCalled(exit, 'should not call exit'); t.end(); }); +test('validate: theme: wrong', (t) => { + const getThemes = stub().returns({ + light: '', + dark: '', + }); + + const exit = stub(); + const msg = 'cloudcmd --theme: can be only one of: "light", "dark"'; + + validate.theme('hello', { + exit, + getThemes, + }); + + t.calledWith(exit, [msg], 'should call exit'); + t.end(); +}); diff --git a/static/user-menu.js b/static/user-menu.js index 2b4b2625bf..1252aca9b7 100644 --- a/static/user-menu.js +++ b/static/user-menu.js @@ -1,25 +1,76 @@ -'use strict'; - const RENAME_FILE = 'Rename file'; +const CDN = 'https://cdn.jsdelivr.net'; +const CDN_GH = `${CDN}/gh/cloudcmd/user-menu@1.2.4`; -module.exports = { +export default { '__settings': { - select: [ - RENAME_FILE, - ], + select: [RENAME_FILE], run: false, }, [`F2 - ${RENAME_FILE}`]: async ({DOM}) => { await DOM.renameCurrent(); }, + 'F6 - Copy URL to current file': runFromCDN('copy-url-to-current-file'), + + 'R - cd /': async ({CloudCmd}) => { + await CloudCmd.changeDir('/'); + }, + 'Y - Convert YouTube to MP3': async ({CloudCmd, DOM}) => { + const {convertYouTubeToMp3} = await import(`${CDN}/menu/convert-youtube-to-mp3.js`); + + await convertYouTubeToMp3({ + CloudCmd, + DOM, + }); + }, + + 'F - Convert flac to mp3 [ffmpeg]': async ({CloudCmd, DOM}) => { + const {convertFlacToMp3} = await import(`${CDN}/menu/ffmpeg.js`); + + await convertFlacToMp3({ + CloudCmd, + DOM, + }); + }, + 'M - Convert mp4 to mp3 [ffmpeg]': async ({CloudCmd, DOM}) => { + const {convertMp4ToMp3} = await import(`${CDN}/menu/ffmpeg.js`); + + await convertMp4ToMp3({ + CloudCmd, + DOM, + }); + }, + + 'O - Convert mov to mp3 [ffmpeg]': async ({CloudCmd, DOM}) => { + const {convertMovToMp3} = await import(`${CDN}/menu/ffmpeg.js`); + + await convertMovToMp3({ + CloudCmd, + DOM, + }); + }, + 'C - Create User Menu File': async ({DOM, CloudCmd}) => { - const {CurrentInfo} = DOM; + const {Dialog, CurrentInfo} = DOM; + + const currentFile = DOM.getCurrentByName('.cloudcmd.menu.js'); + + if (currentFile) { + const [cancel] = await Dialog.confirm(`Looks like file '.cloudcmd.menu.js' already exists. Overwrite?`); + + if (cancel) + return; + } + const {dirPath} = CurrentInfo; const path = `${dirPath}.cloudcmd.menu.js`; const {prefix} = CloudCmd; - const data = await readDefaultMenu({prefix}); + const data = await readDefaultMenu({ + prefix, + }); + await createDefaultMenu({ path, data, @@ -73,12 +124,12 @@ async function createDefaultMenu({path, data, DOM, CloudCmd}) { async function readDefaultMenu({prefix}) { const res = await fetch(`${prefix}/api/v1/user-menu/default`); - const data = await res.text(); - return data; + return await res.text(); } -module.exports._selectNames = selectNames; +export const _selectNames = selectNames; + function selectNames(names, panel, {selectFile, getCurrentByName}) { for (const name of names) { const file = getCurrentByName(name, panel); @@ -86,7 +137,8 @@ function selectNames(names, panel, {selectFile, getCurrentByName}) { } } -module.exports._compare = compare; +export const _compare = compare; + function compare(a, b) { const result = []; @@ -100,3 +152,17 @@ function compare(a, b) { return result; } +const MAP = { + 'copy-url-to-current-file': 'copyURLToCurrentFile', +}; + +function runFromCDN(name) { + return async (...a) => { + const fnName = MAP[name]; + const { + [fnName]: fn, + } = await import(`${CDN_GH}/menu/${name}.js`); + + await fn(...a); + }; +} diff --git a/static/user-menu.spec.js b/static/user-menu.spec.js index 8382904b3d..e15436086b 100644 --- a/static/user-menu.spec.js +++ b/static/user-menu.spec.js @@ -1,16 +1,14 @@ -'use strict'; - -const autoGlobals = require('auto-globals'); -const test = autoGlobals(require('supertape')); -const stub = require('@cloudcmd/stub'); -const tryToCatch = require('try-to-catch'); -const wraptile = require('wraptile'); - -const defaultMenu = require('./user-menu'); +import {stub} from '@cloudcmd/stub'; +import {tryToCatch} from 'try-to-catch'; +import wraptile from 'wraptile'; +import {test as tape} from 'supertape'; +import autoGlobals from 'auto-globals'; +import defaultMenu, {_selectNames, _compare} from './user-menu.js'; +const test = autoGlobals(tape); const {create} = autoGlobals; - const {_data} = defaultMenu; + const reject = wraptile(async (a) => { throw Error(a); }); @@ -29,6 +27,89 @@ test('cloudcmd: static: user menu: Rename', async (t) => { t.end(); }); +test('cloudcmd: static: user menu: R', (t) => { + const name = 'R - cd /'; + const changeDir = stub(); + + const CloudCmd = { + changeDir, + }; + + const fn = defaultMenu[name]; + + fn({ + CloudCmd, + }); + + t.calledWith(changeDir, ['/']); + t.end(); +}); + +test('cloudcmd: static: user menu: F6', async (t) => { + const name = 'F6 - Copy URL to current file'; + const DOM = {}; + + const fn = defaultMenu[name]; + const [error] = await tryToCatch(fn, { + DOM, + }); + + t.equal(error.code, 'ERR_UNSUPPORTED_ESM_URL_SCHEME'); + t.end(); +}); + +test('cloudcmd: static: user menu: Y', async (t) => { + const name = 'Y - Convert YouTube to MP3'; + const DOM = {}; + + const fn = defaultMenu[name]; + const [error] = await tryToCatch(fn, { + DOM, + }); + + t.equal(error.code, 'ERR_UNSUPPORTED_ESM_URL_SCHEME'); + t.end(); +}); + +test('cloudcmd: static: user menu: F', async (t) => { + const name = 'F - Convert flac to mp3 [ffmpeg]'; + const DOM = {}; + + const fn = defaultMenu[name]; + const [error] = await tryToCatch(fn, { + DOM, + }); + + t.equal(error.code, 'ERR_UNSUPPORTED_ESM_URL_SCHEME'); + t.end(); +}); + +test('cloudcmd: static: user menu: M', async (t) => { + const name = 'M - Convert mp4 to mp3 [ffmpeg]'; + const DOM = {}; + + const fn = defaultMenu[name]; + const [error] = await tryToCatch(fn, { + DOM, + }); + + t.equal(error.code, 'ERR_UNSUPPORTED_ESM_URL_SCHEME'); + t.end(); +}); + +test('cloudcmd: static: user menu: O', async (t) => { + const name = 'O - Convert mov to mp3 [ffmpeg]'; + const DOM = {}; + + const fn = defaultMenu[name]; + const [error] = await tryToCatch(fn, { + DOM, + }); + + t.equal(error.code, 'ERR_UNSUPPORTED_ESM_URL_SCHEME'); + t.end(); +}); + test('cloudcmd: static: user menu: IO.write', async (t) => { const name = 'C - Create User Menu File'; const DOM = getDOM(); @@ -46,6 +127,58 @@ test('cloudcmd: static: user menu: IO.write', async (t) => { t.end(); }); +test('cloudcmd: static: user menu: C: exists: ok', async (t) => { + const name = 'C - Create User Menu File'; + const DOM = getDOM(); + const CloudCmd = getCloudCmd(); + + const { + Dialog, + getCurrentByName, + } = DOM; + + const {confirm} = Dialog; + const {write} = DOM.IO; + + getCurrentByName.returns({}); + confirm.resolves([]); + + await defaultMenu[name]({ + DOM, + CloudCmd, + }); + + const path = '/.cloudcmd.menu.js'; + + t.calledWith(write, [path, _data], 'should call IO.write'); + t.end(); +}); + +test('cloudcmd: static: user menu: C: exists: cancel', async (t) => { + const name = 'C - Create User Menu File'; + const DOM = getDOM(); + const CloudCmd = getCloudCmd(); + + const { + Dialog, + getCurrentByName, + } = DOM; + + const {confirm} = Dialog; + const {write} = DOM.IO; + + getCurrentByName.returns({}); + confirm.resolves([Error('cancel')]); + + await defaultMenu[name]({ + DOM, + CloudCmd, + }); + + t.notCalled(write); + t.end(); +}); + test('cloudcmd: static: user menu: refresh', async (t) => { const name = 'C - Create User Menu File'; const DOM = getDOM(); @@ -57,7 +190,7 @@ test('cloudcmd: static: user menu: refresh', async (t) => { CloudCmd, }); - t.ok(refresh.calledWith(), 'should call CloudCmd.refresh'); + t.calledWithNoArgs(refresh, 'should call CloudCmd.refresh'); t.end(); }); @@ -107,28 +240,27 @@ test('cloudcmd: static: user menu: no EditFile.show', async (t) => { CloudCmd, }); - t.notOk(EditFile.show.called, 'should not call EditFile.show'); + t.notCalled(EditFile.show, 'should not call EditFile.show'); t.end(); }); -test('cloudcmd: static: user menu: compare directories', async (t) => { +test('cloudcmd: static: user menu: compare directories', (t) => { const name = 'D - Compare directories'; const DOM = getDOM(); const CloudCmd = getCloudCmd(); - await defaultMenu[name]({ + defaultMenu[name]({ DOM, CloudCmd, }); - const {files} = DOM.CurrentInfo.files; + const {files} = DOM.CurrentInfo; t.calledWith(DOM.getFilenames, [files], 'should call getFilenames'); t.end(); }); test('cloudcmd: static: user menu: compare directories: select names', (t) => { - const {_selectNames} = defaultMenu; const selectFile = stub(); const file = {}; const getCurrentByName = stub().returns(file); @@ -146,7 +278,6 @@ test('cloudcmd: static: user menu: compare directories: select names', (t) => { }); test('cloudcmd: static: user menu: compare directories: select names: getCurrentByName', (t) => { - const {_selectNames} = defaultMenu; const selectFile = stub(); const getCurrentByName = stub(); @@ -164,16 +295,13 @@ test('cloudcmd: static: user menu: compare directories: select names: getCurrent }); test('cloudcmd: static: user menu: compare directories: select names: compare', (t) => { - const {_compare} = defaultMenu; const a = [1, 2]; const b = [1, 3]; const result = _compare(a, b); - const expected = [ - 2, - ]; + const expected = [2]; - t.deepEqual(result, expected, 'should equal'); + t.deepEqual(result, expected); t.end(); }); @@ -182,16 +310,21 @@ function getDOM() { write: stub(), }; + const Dialog = { + confirm: stub(), + }; + const CurrentInfo = { dirPath: '/', files: [], - filesPasive: [], + filesPassive: [], panel: create(), panelPassive: create(), }; return { IO, + Dialog, CurrentInfo, setCurrentByName: stub(), getFilenames: stub().returns([]), @@ -200,12 +333,9 @@ function getDOM() { }; } -function getCloudCmd() { - return { - refresh: stub(), - EditFile: { - show: stub(), - }, - }; -} - +const getCloudCmd = () => ({ + refresh: stub(), + EditFile: { + show: stub(), + }, +}); diff --git a/test-e2e/ratelimit.js b/test-e2e/ratelimit.js new file mode 100644 index 0000000000..632916c544 --- /dev/null +++ b/test-e2e/ratelimit.js @@ -0,0 +1,44 @@ +import process from 'node:process'; +import {test} from 'supertape'; + +let i = 0; + +test('cloudcmd: server: ratelimit: x-forwarded-for', async (t) => { + const PORT = 3000; + + process.env.PORT = PORT; + process.env.CLOUDCMD_LOG = 0; + + await import('../bin/cloudcmd.js'); + + const {status} = await fetch(`http://localhost:${PORT}`, { + headers: { + 'X-Forwarded-For': '127.0.0.1', + }, + }); + + process.kill(process.pid, 'SIGUSR1'); + + t.notEqual(status, 500); + t.end(); +}); + +test('cloudcmd: server: ratelimit', async (t) => { + const PORT = 3001; + const STATUS = 429; + + process.env.PORT = PORT; + process.env.CLOUDCMD_LOG = 0; + + await import(`../bin/cloudcmd.js?${i++}`); + + for (let i = 0; i < 1000; i++) { + await fetch(`http://localhost:${PORT}`); + } + + const {status} = await fetch(`http://localhost:${PORT}`); + process.kill(process.pid, 'SIGUSR1'); + + t.equal(status, STATUS); + t.end(); +}); diff --git a/test/before.js b/test/before.js index ad36c6687b..970a4a4f67 100644 --- a/test/before.js +++ b/test/before.js @@ -1,23 +1,25 @@ -'use strict'; +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 {cloudcmd} from '#server/cloudcmd'; -const http = require('http'); -const os = require('os'); - -const express = require('express'); -const io = require('socket.io'); -const writejson = require('writejson'); -const readjson = require('readjson'); -const {promisify} = require('util'); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); process.env.NODE_ENV = 'development'; - -const cloudcmd = require('../server/cloudcmd'); const {assign} = Object; const pathConfig = os.homedir() + '/.cloudcmd.json'; const currentConfig = readjson.sync.try(pathConfig); -module.exports = before; +export default before; function before(options, fn = options) { const { @@ -29,6 +31,7 @@ function before(options, fn = options) { const app = express(); const server = http.createServer(app); + const after = (cb) => { if (currentConfig) writejson.sync(pathConfig, currentConfig); @@ -36,7 +39,7 @@ function before(options, fn = options) { server.close(cb); }; - const socket = io(server); + const socket = new Server(server); app.use(cloudcmd({ socket, @@ -47,20 +50,21 @@ function before(options, fn = options) { })); server.listen(() => { - fn(server.address().port, promisify(after)); + fn(server + .address().port, promisify(after)); }); } -module.exports.connect = promisify((options, fn = options) => { +export const connect = promisify((options, fn = options) => { before(options, (port, done) => { - fn(null, {port, done}); + fn(null, { + port, + done, + }); }); }); -function defaultConfig() { - return { - auth: false, - root: __dirname, - }; -} - +const defaultConfig = () => ({ + auth: false, + root: __dirname, +}); diff --git a/test/common/cloudfunc.html b/test/common/cloudfunc.html index 8db8081a9c..a032b10783 100644 --- a/test/common/cloudfunc.html +++ b/test/common/cloudfunc.html @@ -2,6 +2,7 @@ name size + time date owner mode @@ -9,6 +10,7 @@ .. <dir> + --:--:-- --.--.---- . --- --- --- @@ -16,6 +18,7 @@ applnk <dir> + --:--:-- 21.02.2016 root rwx r-x r-x @@ -23,7 +26,8 @@ ай 1.30kb + --:--:-- --.--.---- root rwx r-x r-x -
\ No newline at end of file + diff --git a/test/common/cloudfunc.js b/test/common/cloudfunc.js index 218de467bd..86f939081d 100644 --- a/test/common/cloudfunc.js +++ b/test/common/cloudfunc.js @@ -1,29 +1,21 @@ -'use strict'; +import fs from 'node:fs'; +import {fileURLToPath} from 'node:url'; +import {dirname} from 'node:path'; +import {tryCatch} from 'try-catch'; +import {test} from 'supertape'; +import readFilesSync from '@cloudcmd/read-files-sync'; +import * as CloudFunc from '#common/cloudfunc'; +import {time, timeEnd} from '#common/util'; -const fs = require('fs'); -const tryCatch = require('try-catch'); +const __filename = fileURLToPath(import.meta.url); -const DIR = __dirname + '/../../'; -const COMMONDIR = DIR + 'common/'; -const TMPLDIR = DIR + 'tmpl/'; +const __dirname = dirname(__filename); +const DIR = `${__dirname}/../../`; -const { - time, - timeEnd, -} = require(COMMONDIR + 'util'); +const TMPLDIR = `${DIR}tmpl/`; -const CloudFuncPath = COMMONDIR + 'cloudfunc'; - -const CloudFunc = require(CloudFuncPath); - -const test = require('supertape'); -const {reRequire} = require('mock-require'); - -const htmlLooksLike = require('html-looks-like'); -const readFilesSync = require('@cloudcmd/read-files-sync'); - -const FS_DIR = TMPLDIR + 'fs/'; -const EXPECT_PATH = __dirname + '/cloudfunc.html'; +const FS_DIR = `${TMPLDIR}fs/`; +const EXPECT_PATH = `${__dirname}/cloudfunc.html`; const addHBS = (a) => `${a}.hbs`; const TMPL = [ @@ -52,24 +44,22 @@ const data = { }], }; -let Expect = - '
' + - '' + - '' + - '' + - '' + - '/' + - '' + - 'etc' + - '/X11/' + - '' + +let Expect = '
' + + '' + + '' + + '' + + '' + + '/' + + '' + + 'etc' + + '/X11/' + + '' + '
'; test('cloudfunc: render', (t) => { const template = readFilesSync(FS_DIR, TMPL, 'utf8'); - const expect = fs.readFileSync(EXPECT_PATH, 'utf8'); time('CloudFunc.buildFromJSON'); const result = CloudFunc.buildFromJSON({ @@ -78,17 +68,19 @@ test('cloudfunc: render', (t) => { template, }); - Expect += expect; + Expect += fs + .readFileSync(EXPECT_PATH, 'utf8') + .slice(0, -1); let i; + const isNotOk = Expect .split('') .some((item, number) => { const ret = result[number] !== item; - if (ret) { + if (ret) i = number; - } return ret; }); @@ -96,19 +88,11 @@ test('cloudfunc: render', (t) => { timeEnd('CloudFunc.buildFromJSON'); if (isNotOk) { - console.log( - `Error in char number: ${i}\n`, - `Expect: ${Expect.substr(i)}\n`, - `Result: ${result.substr(i)}`, - ); - + 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(); }); @@ -123,7 +107,7 @@ test('cloudfunc: formatMsg', (t) => { t.end(); }); -test('cloudfunc: formatMsg', (t) => { +test('cloudfunc: formatMsg: no name', (t) => { const msg = 'hello'; const name = null; const status = 'ok'; @@ -135,16 +119,15 @@ test('cloudfunc: formatMsg', (t) => { }); test('cloudfunc: getTitle', (t) => { - const CloudFunc = reRequire(CloudFuncPath); - - const result = CloudFunc.getTitle(); + const result = CloudFunc.getTitle({ + path: '/', + }); t.equal(result, 'Cloud Commander - /'); t.end(); }); test('cloudfunc: getTitle: no name', (t) => { - const CloudFunc = reRequire(CloudFuncPath); const path = '/hello/world'; const result = CloudFunc.getTitle({ @@ -156,7 +139,6 @@ test('cloudfunc: getTitle: no name', (t) => { }); test('cloudfunc: getTitle: name, path', (t) => { - const CloudFunc = reRequire(CloudFuncPath); const name = 'hello'; const path = '/hello/world'; @@ -210,4 +192,3 @@ test('cloudfunc: getDotDot: two levels deep', (t) => { t.equal(dotDot, '/home', 'should return up level'); t.end(); }); - diff --git a/test/rest/config.js b/test/rest/config.js index e2b8fb3e5a..737d422e30 100644 --- a/test/rest/config.js +++ b/test/rest/config.js @@ -1,11 +1,9 @@ -'use strict'; +import serveOnce from 'serve-once'; +import test from 'supertape'; +import {cloudcmd} from '#server/cloudcmd'; -const test = require('supertape'); - -const cloudcmd = require('../..'); const configManager = cloudcmd.createConfigManager(); - -const {request} = require('serve-once')(cloudcmd, { +const {request} = serveOnce(cloudcmd, { config: { auth: false, }, @@ -104,4 +102,3 @@ test('cloudcmd: rest: config: patch: save config', async (t) => { t.equal(configManager('editor'), 'dword', 'should change config file on patch'); t.end(); }); - diff --git a/test/rest/copy.js b/test/rest/copy.js index 8d41e566da..f11ef7d40f 100644 --- a/test/rest/copy.js +++ b/test/rest/copy.js @@ -1,32 +1,34 @@ -'use strict'; +import {dirname, join} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {mkdirSync} from 'node:fs'; +import serveOnce from 'serve-once'; +import test from 'supertape'; +import {rimraf} from 'rimraf'; +import {cloudcmd} from '#server/cloudcmd'; -const {mkdirSync} = require('fs'); -const {join} = require('path'); -const test = require('supertape'); -const rimraf = require('rimraf'); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); -const fixtureDir = join(__dirname, '..', 'fixture') + '/'; const config = { - root: join(__dirname, '..'), + root: new URL('..', import.meta.url).pathname, }; -const cloudcmd = require('../..'); const configManager = cloudcmd.createConfigManager(); -configManager('auth', false); -const {request} = require('serve-once')(cloudcmd, { +configManager('auth', false); +const {request} = serveOnce(cloudcmd, { config, configManager, }); +const fixtureDir = join(__dirname, '..', 'fixture') + '/'; + test('cloudcmd: rest: copy', async (t) => { const tmp = join(fixtureDir, 'tmp'); const files = { from: '/fixture/', to: '/fixture/tmp', - names: [ - 'copy.txt', - ], + names: ['copy.txt'], }; mkdirSync(tmp); @@ -40,4 +42,3 @@ test('cloudcmd: rest: copy', async (t) => { t.equal(body, 'copy: ok("["copy.txt"]")', 'should return result'); t.end(); }); - diff --git a/test/rest/fs.js b/test/rest/fs.js index 5c3b4f0b7e..721320a25d 100644 --- a/test/rest/fs.js +++ b/test/rest/fs.js @@ -1,9 +1,8 @@ -'use strict'; +import serveOnce from 'serve-once'; +import test from 'supertape'; +import {cloudcmd} from '#server/cloudcmd'; -const test = require('supertape'); - -const cloudcmd = require('../..'); -const {request} = require('serve-once')(cloudcmd, { +const {request} = serveOnce(cloudcmd, { config: { auth: false, }, @@ -20,3 +19,9 @@ test('cloudcmd: rest: fs: path', async (t) => { 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.js b/test/rest/move.js index d36f2e0405..696b36a31e 100644 --- a/test/rest/move.js +++ b/test/rest/move.js @@ -1,44 +1,15 @@ -'use strict'; - -const fs = require('fs'); - -const test = require('supertape'); -const {Volume} = require('memfs'); -const {ufs} = require('unionfs'); - -const mockRequire = require('mock-require'); -const {reRequire, stopAll} = mockRequire; -const serveOnce = require('serve-once'); - -const cloudcmdPath = '../../'; -const dir = cloudcmdPath + 'server/'; -const restPath = dir + 'rest'; - -const {assign} = Object; +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'; test('cloudcmd: rest: move', async (t) => { - const volume = { - '/fixture/move.txt': 'hello', - '/fixture/tmp/a.txt': 'a', - }; - - const vol = Volume.fromJSON(volume, '/'); + const move = new EventEmitter(); + const moveFiles = stub().returns(move); - const unionFS = ufs - .use(vol) - .use(fs); - - assign(unionFS, { - promises: fs.promises, - }); - mockRequire('fs', unionFS); - - reRequire('@cloudcmd/rename-files'); - reRequire('@cloudcmd/move-files'); - reRequire(restPath); - - const cloudcmd = reRequire(cloudcmdPath); const {createConfigManager} = cloudcmd; + cloudcmd.depStore('moveFiles', moveFiles); const configManager = createConfigManager(); configManager('auth', false); @@ -51,23 +22,23 @@ test('cloudcmd: rest: move', async (t) => { const files = { from: '/fixture/', to: '/fixture/tmp/', - names: [ - 'move.txt', - ], + names: ['move.txt'], }; - const {body} = await request.put(`/api/v1/move`, { - body: files, - }); + const emit = move.emit.bind(move); - stopAll(); + const [{body}] = await Promise.all([ + request.put(`/api/v1/move`, { + body: files, + }), + wait(1000, emit, 'end'), + ]); t.equal(body, 'move: ok("["move.txt"]")', 'should move'); t.end(); }); test('cloudcmd: rest: move: no from', async (t) => { - const cloudcmd = reRequire(cloudcmdPath); const {createConfigManager} = cloudcmd; const configManager = createConfigManager(); @@ -91,7 +62,6 @@ test('cloudcmd: rest: move: no from', async (t) => { }); test('cloudcmd: rest: move: no to', async (t) => { - const cloudcmd = reRequire(cloudcmdPath); const {createConfigManager} = cloudcmd; const configManager = createConfigManager(); diff --git a/test/rest/pack.js b/test/rest/pack.js index aff32c615d..8a97029922 100644 --- a/test/rest/pack.js +++ b/test/rest/pack.js @@ -1,34 +1,32 @@ -'use strict'; - -const fs = require('fs'); -const {join} = require('path'); -const {promisify} = require('util'); - -const {reRequire} = require('mock-require'); -const test = require('supertape'); -const tar = require('tar-stream'); -const gunzip = require('gunzip-maybe'); -const pullout = require('pullout'); - -const pathTarFixture = join(__dirname, '..', 'fixture/pack.tar.gz'); +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 serveOnce from 'serve-once'; +import {cloudcmd} from '#server/cloudcmd'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); const pathZipFixture = join(__dirname, '..', 'fixture/pack.zip'); -const cloudcmdPath = '../..'; -const fixture = { - tar: fs.readFileSync(pathTarFixture), - zip: fs.readFileSync(pathZipFixture), -}; +const pathTarFixture = join(__dirname, '..', 'fixture/pack.tar.gz'); const defaultOptions = { config: { auth: false, - root: join(__dirname, '..'), + root: new URL('..', import.meta.url).pathname, }, }; -const cloudcmd = require(cloudcmdPath); +const fixture = { + tar: fs.readFileSync(pathTarFixture), + zip: fs.readFileSync(pathZipFixture), +}; -const serveOnce = require('serve-once'); const {request} = serveOnce(cloudcmd, defaultOptions); const once = promisify((name, extract, fn) => { @@ -47,7 +45,6 @@ test('cloudcmd: rest: pack: tar: get', async (t) => { config, }; - const cloudcmd = reRequire(cloudcmdPath); const {request} = serveOnce(cloudcmd, defaultOptions); const {body} = await request.get(`/api/v1/pack/fixture/pack`, { @@ -57,14 +54,18 @@ test('cloudcmd: rest: pack: tar: get', async (t) => { const extract = tar.extract(); - body.pipe(gunzip()).pipe(extract); + body + .pipe(gunzip()) + .pipe(extract); const [, stream] = await once('entry', extract); const data = await pullout(stream); - const file = fs.readFileSync(__dirname + '/../fixture/pack', 'utf8'); + const file = fs.readFileSync(`${__dirname}/../fixture/pack`, 'utf8'); t.equal(file, data, 'should pack data'); t.end(); +}, { + timeout: 7000, }); test('cloudcmd: rest: pack: tar: put: file', async (t) => { @@ -76,7 +77,7 @@ test('cloudcmd: rest: pack: tar: put: file', async (t) => { config, }; - const name = String(Math.random()) + '.tar.gz'; + const name = `${Math.random()}.tar.gz`; const {request} = serveOnce(cloudcmd, defaultOptions); @@ -88,11 +89,13 @@ test('cloudcmd: rest: pack: tar: put: file', async (t) => { const file = fs.createReadStream(join(__dirname, '..', name)); const extract = tar.extract(); - file.pipe(gunzip()).pipe(extract); + file + .pipe(gunzip()) + .pipe(extract); const [, stream] = await once('entry', extract); const data = await pullout(stream, 'buffer'); - const result = fs.readFileSync(__dirname + '/../fixture/pack'); + const result = fs.readFileSync(`${__dirname}/../fixture/pack`); fs.unlinkSync(`${__dirname}/../${name}`); @@ -109,7 +112,8 @@ test('cloudcmd: rest: pack: tar: put: response', async (t) => { config, }; - const name = String(Math.random()) + '.tar.gz'; + const name = `${Math.random()}.tar.gz`; + const {body} = await request.put(`/api/v1/pack`, { options, body: getPackOptions(name), @@ -132,12 +136,10 @@ test('cloudcmd: rest: pack: tar: put: error', async (t) => { const {body} = await request.put(`/api/v1/pack`, { options, - body: getPackOptions('name', [ - 'not found', - ]), + body: getPackOptions('name', ['not found']), }); - t.ok(/^ENOENT: no such file or directory/.test(body), 'should return error'); + t.match(body, /^ENOENT: no such file or directory/, 'should return error'); t.end(); }); @@ -155,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(); }); @@ -168,16 +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(); }); @@ -190,7 +192,8 @@ 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, body: getPackOptions(name), @@ -214,20 +217,15 @@ test('cloudcmd: rest: pack: zip: put: error', async (t) => { const {body} = await request.put(`/api/v1/pack`, { options, - body: getPackOptions('name', [ - 'not found', - ]), + body: getPackOptions('name', ['not found']), }); - t.ok(/^ENOENT: no such file or directory/.test(body), 'should return error'); + t.match(body, /^ENOENT: no such file or directory/, 'should return error'); 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/rest/rename.js b/test/rest/rename.js index ed7e11c5aa..8dd8e1f847 100644 --- a/test/rest/rename.js +++ b/test/rest/rename.js @@ -1,18 +1,9 @@ -'use strict'; - -const fs = require('fs'); - -const test = require('supertape'); -const {Volume} = require('memfs'); -const {ufs} = require('unionfs'); - -const mockRequire = require('mock-require'); -const {reRequire, stopAll} = mockRequire; -const serveOnce = require('serve-once'); - -const cloudcmdPath = '../../'; -const dir = cloudcmdPath + 'server/'; -const restPath = dir + 'rest'; +import fs from 'node:fs'; +import test from 'supertape'; +import {Volume} from 'memfs'; +import {ufs} from 'unionfs'; +import serveOnce from 'serve-once'; +import {cloudcmd} from '#server/cloudcmd'; test('cloudcmd: rest: rename', async (t) => { const volume = { @@ -26,19 +17,13 @@ test('cloudcmd: rest: rename', async (t) => { .use(vol) .use(fs); - mockRequire('fs', unionFS); - - reRequire('@cloudcmd/rename-files'); - reRequire('@cloudcmd/move-files'); - reRequire(restPath); - - const cloudcmd = reRequire(cloudcmdPath); const {createConfigManager} = cloudcmd; const configManager = createConfigManager(); configManager('auth', false); configManager('root', '/'); + cloudcmd.depStore('fs', unionFS); const {request} = serveOnce(cloudcmd, { configManager, }); @@ -52,18 +37,15 @@ test('cloudcmd: rest: rename', async (t) => { body: files, }); - mockRequire.stop('fs'); + cloudcmd.depStore(); const expected = 'rename: ok("{"from":"/fixture/mv.txt","to":"/fixture/tmp/mv.txt"}")'; - stopAll(); - t.equal(body, expected, 'should move'); t.end(); }); test('cloudcmd: rest: rename: no from', async (t) => { - const cloudcmd = reRequire(cloudcmdPath); const {createConfigManager} = cloudcmd; const configManager = createConfigManager(); @@ -87,7 +69,6 @@ test('cloudcmd: rest: rename: no from', async (t) => { }); test('cloudcmd: rest: rename: no to', async (t) => { - const cloudcmd = reRequire(cloudcmdPath); const {createConfigManager} = cloudcmd; const configManager = createConfigManager(); @@ -111,4 +92,3 @@ test('cloudcmd: rest: rename: no to', async (t) => { t.equal(body, expected); t.end(); }); - diff --git a/test/server/columns.js b/test/server/columns.js deleted file mode 100644 index c57705d779..0000000000 --- a/test/server/columns.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const test = require('supertape'); -const fs = require('fs'); -const {reRequire} = require('mock-require'); -const columnsPath = '../../server/columns'; - -test('columns', (t) => { - const {NODE_ENV} = process.env; - process.env.NODE_ENV = ''; - const columns = reRequire(columnsPath); - - process.env.NODE_ENV = NODE_ENV; - - t.equal(columns[''], ''); - t.end(); -}); - -test('columns: dev', (t) => { - const {NODE_ENV} = process.env; - process.env.NODE_ENV = 'development'; - - const columns = reRequire(columnsPath); - const css = fs.readFileSync(`${__dirname}/../../css/columns/name-size-date.css`, 'utf8'); - - process.env.NODE_ENV = NODE_ENV; - - t.equal(columns['name-size-date'], css); - t.end(); -}); - diff --git a/test/server/console.js b/test/server/console.js index 8eb8bfdc56..56d35a3cb5 100644 --- a/test/server/console.js +++ b/test/server/console.js @@ -1,21 +1,20 @@ -'use strict'; +import {once} from 'node:events'; +import test from 'supertape'; +import io from 'socket.io-client'; +import {connect} from '../before.js'; +import {createConfig} from '../../server/config.js'; -const path = require('path'); -const {once} = require('events'); - -const test = require('supertape'); -const io = require('socket.io-client'); - -const configPath = path.join(__dirname, '../..', 'server', 'config'); -const {connect} = require('../before'); -const configFn = require(configPath).createConfig(); +const configFn = createConfig(); test('cloudcmd: console: enabled', async (t) => { const config = { console: true, }; - const {port, done} = await connect({config}); + const {port, done} = await connect({ + config, + }); + const socket = io(`http://localhost:${port}/console`); socket.emit('auth', configFn('username'), configFn('password')); @@ -34,7 +33,10 @@ test('cloudcmd: console: disabled', async (t) => { console: false, }; - const {port, done} = await connect({config}); + const {port, done} = await connect({ + config, + }); + const socket = io(`http://localhost:${port}/console`); const [error] = await once(socket, 'connect_error'); @@ -45,4 +47,3 @@ test('cloudcmd: console: disabled', async (t) => { t.equal(error.message, 'Invalid namespace', 'should emit error'); t.end(); }); - diff --git a/test/server/env.js b/test/server/env.js index c745428aac..323c0a99be 100644 --- a/test/server/env.js +++ b/test/server/env.js @@ -1,11 +1,10 @@ -'use strict'; - -const test = require('supertape'); -const env = require('../../server/env'); +import process from 'node:process'; +import {test} from 'supertape'; +import * as env from '../../server/env.js'; 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(); @@ -13,7 +12,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(); @@ -38,6 +37,7 @@ test('env: bool: true', (t) => { test('env: bool: undefined', (t) => { const {cloudcmd_terminal} = process.env; + process.env.cloudcmd_terminal = undefined; t.notOk(env.bool('terminal'), 'should be undefined'); @@ -45,4 +45,3 @@ test('env: bool: undefined', (t) => { process.env.cloudcmd_terminal = cloudcmd_terminal; t.end(); }); - diff --git a/test/server/modulas.js b/test/server/modulas.js index d8fc7e9098..5c7c6d9ead 100644 --- a/test/server/modulas.js +++ b/test/server/modulas.js @@ -1,17 +1,19 @@ -'use strict'; - -const {join} = require('path'); -const test = require('supertape'); -const stub = require('@cloudcmd/stub'); +import {createRequire} from 'node:module'; +import {dirname, join} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import serveOnce from 'serve-once'; +import {test, stub} from 'supertape'; +import {cloudcmd} from '#server/cloudcmd'; +import modulas from '../../server/modulas.js'; +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(`${cloudcmdPath}/server/modulas`); -const cloudcmd = require(cloudcmdPath); -const {request} = require('serve-once')(cloudcmd, { +const {request} = serveOnce(cloudcmd, { config: { auth: false, dropbox: false, @@ -26,6 +28,7 @@ test('cloudcmd: modules', async (t) => { }, }, }; + const options = { modules, }; @@ -40,7 +43,7 @@ test('cloudcmd: modules', async (t) => { options, }); - t.deepEqual(body, expected, 'should equal'); + t.deepEqual(body, expected); t.end(); }); @@ -72,9 +75,10 @@ test('cloudcmd: modules: no', (t) => { const url = '/json/modules.json'; const send = stub(); - fn({url}, {send}); + fn({url}, { + send, + }); t.calledWith(send, [localModules], 'should have been called with modules'); t.end(); }); - diff --git a/test/static.js b/test/static.js index f8f3e8a8a4..cdb6b9e7f2 100644 --- a/test/static.js +++ b/test/static.js @@ -1,18 +1,19 @@ -'use strict'; - -const test = require('supertape'); -const criton = require('criton'); - -const cloudcmd = require('..'); -const configFn = cloudcmd.createConfigManager(); +import {Buffer} from 'node:buffer'; +import serveOnce from 'serve-once'; +import test from 'supertape'; +import criton from 'criton'; +import {cloudcmd} from '#server/cloudcmd'; const config = { auth: false, }; -const {request} = require('serve-once')(cloudcmd, { + +const {request} = serveOnce(cloudcmd, { config, }); +const configFn = cloudcmd.createConfigManager(); + test('cloudcmd: static', async (t) => { const name = 'package.json'; const {body} = await request.get(`/${name}`, { @@ -42,6 +43,7 @@ test('cloudcmd: prefix: wrong', async (t) => { }; const name = Math.random(); + const {status} = await request.get(`/${name}`, { options, }); @@ -65,6 +67,7 @@ test('cloudcmd: /cloudcmd.js: auth: access denied', async (t) => { const config = { auth: true, }; + const options = { config, }; @@ -80,15 +83,20 @@ test('cloudcmd: /cloudcmd.js: auth: access denied', async (t) => { test('cloudcmd: /cloudcmd.js: auth: no password', async (t) => { const name = 'cloudcmd.js'; const username = 'hello'; + const config = { auth: true, username, }; + const options = { config, }; - const encoded = Buffer.from(`${username}:`).toString('base64'); + const encoded = Buffer + .from(`${username}:`) + .toString('base64'); + const authorization = `Basic ${encoded}`; const {status} = await request.get(`/${name}`, { @@ -107,11 +115,13 @@ test('cloudcmd: /cloudcmd.js: auth: access granted', async (t) => { const username = 'hello'; const password = 'world'; const algo = configFn('algo'); + const config = { auth: true, username, password: criton(password, algo), }; + const options = { config, }; @@ -140,4 +150,3 @@ test('cloudcmd: /logout', async (t) => { t.equal(status, 401, 'should return 401'); t.end(); }); - diff --git a/tmpl/config.hbs b/tmpl/config.hbs index 686836174b..6002cd8b96 100644 --- a/tmpl/config.hbs +++ b/tmpl/config.hbs @@ -34,7 +34,7 @@ Diff -
  • +
  • -
  • +
  • + +
  • +
  • -
  • +
  • @@ -66,6 +74,12 @@ Vim
  • +
  • + +
  • -
  • +
  • +
  • + +
  • - + \ No newline at end of file diff --git a/tmpl/fs/file.hbs b/tmpl/fs/file.hbs index 0eaa688746..8b50494faa 100644 --- a/tmpl/fs/file.hbs +++ b/tmpl/fs/file.hbs @@ -2,6 +2,7 @@ {{ name }} {{ size }} + {{ time }} {{ date }} {{ owner }} {{ mode }} diff --git a/webpack.config.js b/webpack.config.js index 301237b420..5d68439120 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,14 +1,10 @@ -'use strict'; +import {merge} from 'webpack-merge'; +import * as htmlConfig from './.webpack/html.js'; +import cssConfig from './.webpack/css.js'; +import jsConfig from './.webpack/js.js'; -const {merge} = require('webpack-merge'); - -const htmlConfig = require('./.webpack/html'); -const cssConfig = require('./.webpack/css'); -const jsConfig = require('./.webpack/js'); - -module.exports = merge([ +export default merge([ jsConfig, htmlConfig, cssConfig, ]); -