diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..5fcb804f --- /dev/null +++ b/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": [ + "@babel/preset-env" + ], + "plugins": [ + "module:babel-plugin-macros", + ] +} diff --git a/.babelrc.json b/.babelrc.json deleted file mode 100644 index 33d70c39..00000000 --- a/.babelrc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "presets": [ - "@babel/preset-env" - ], - "plugins": [ - "module:babel-plugin-macros", - "@babel/plugin-transform-optional-chaining" - ] -} diff --git a/.browserslistrc b/.browserslistrc index bde9f522..36cf4ddd 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,4 +1,5 @@ last 2 Chrome versions last 2 Safari versions Firefox ESR +maintained node versions not dead diff --git a/.cloudcmd.menu.js b/.cloudcmd.menu.js index 0e0126b6..010b6615 100644 --- a/.cloudcmd.menu.js +++ b/.cloudcmd.menu.js @@ -1,31 +1,25 @@ -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'), +'use strict'; + +module.exports = { + 'F2 - Rename file': async ({DOM}) => { + await DOM.renameCurrent(); + }, + 'D - Build Dev': async ({CloudCmd}) => { + await CloudCmd.TerminalRun.show({ + command: 'npm run build:client:dev', + autoClose: false, + closeMessage: 'Press any button to close Terminal', + }); + + CloudCmd.refresh(); + }, + 'P - Build Prod': async ({CloudCmd}) => { + await CloudCmd.TerminalRun.show({ + command: 'npm run build:client', + autoClose: true, + }); + + CloudCmd.refresh(); + }, }; -async function renameCurrent(DOM) { - await DOM.renameCurrent(); -} - -function run(command) { - return async ({CloudCmd, DOM}) => { - const {TerminalRun, config} = CloudCmd; - - const {CurrentInfo} = DOM; - const {dirPath} = CurrentInfo; - - const cwd = config('root') + dirPath; - - return await TerminalRun.show({ - cwd, - command, - closeMessage: 'Press any key to close Terminal', - autoClose: false, - }); - }; -} diff --git a/.dockerignore b/.dockerignore index 1c9d9d9a..4f398708 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,5 +19,6 @@ cssnano.config.js bin/release.js client +legacy server_ diff --git a/.editorconfig b/.editorconfig index 439abfd0..09fa9422 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 = false +trim_trailing_whitespace = true indent_style = space indent_size = 4 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..85c5eacd --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,34 @@ +'use strict'; + +module.exports = { + extends: [ + 'plugin:putout/recommended', + ], + plugins: [ + 'putout', + 'node', + ], + overrides: [{ + files: ['bin/release.js'], + rules: { + 'no-console': 'off', + 'node/shebang': 'off', + }, + extends: [ + 'plugin:node/recommended', + ], + }, { + files: ['bin/cloudcmd.js'], + rules: { + 'no-console': 'off', + }, + extends: [ + 'plugin:node/recommended', + ], + }, { + files: ['{client,common}/**/*.js'], + env: { + browser: true, + }, + }], +}; diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 9fdb6d3c..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -github: coderaiser -open_collective: cloudcmd -ko_fi: coderaiser diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..6cc34cc8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,12 @@ + + +* **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 deleted file mode 100644 index 5c1e7460..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,45 +0,0 @@ ---- - -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 deleted file mode 100644 index 5f41e73a..00000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 549a8874..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,21 +0,0 @@ ---- - -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 deleted file mode 100644 index 17bf5831..00000000 --- a/.github/ISSUE_TEMPLATE/issue_template.md +++ /dev/null @@ -1,24 +0,0 @@ -*** - -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 fb62bd82..98b1f1ad 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,5 +4,6 @@ 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 fix:lint` is OK +- [ ] `npm run codestyle` is OK - [ ] `npm test` is OK + diff --git a/.github/workflows/docker-io.yml b/.github/workflows/docker-io.yml deleted file mode 100644 index 8cbeb042..00000000 --- a/.github/workflows/docker-io.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Docker IO -permissions: - contents: write -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,linux/arm64 - 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 deleted file mode 100644 index 5c81d368..00000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Docker CI -permissions: - contents: write -on: - push: - tags: - - "*" -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 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: | - 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 base-image - uses: docker/build-push-action@v7 - with: - context: . - file: docker/Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: | - coderaiser/cloudcmd:latest - coderaiser/cloudcmd:${{ steps.build.outputs.version }} - ghcr.io/${{ github.repository }}:latest - ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }} - - name: Build and push alpine-image - uses: docker/build-push-action@v7 - with: - context: . - file: docker/Dockerfile.alpine - platforms: linux/amd64,linux/arm64 - push: true - tags: | - coderaiser/cloudcmd:alpine - coderaiser/cloudcmd:latest-alpine - coderaiser/cloudcmd:${{ steps.build.outputs.version }}-alpine - ghcr.io/${{ github.repository }}:latest-alpine - ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-alpine - - 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 429e5568..15f55f5e 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,53 +1,20 @@ name: Node CI -on: - - push -permissions: - contents: write + +on: [push] + jobs: build: + runs-on: ubuntu-latest - env: - NAME: cloudcmd - strategy: - matrix: - node-version: - - 22.x - - 24.x - - 26.x + steps: - - 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@v6 - with: - node-version: ${{ matrix.node-version }} - - name: Install Redrun - run: bun i redrun madrun -g --no-save - - name: Install - run: bun i --no-save - - name: Init Madrun - run: madrun --init - - 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@v10 - continue-on-error: true - with: - message: "chore: ${{ env.NAME }}: actions: lint ☘️" - - name: Build - run: redrun build - - name: Test - run: redrun test - - name: Coverage - run: redrun coverage coverage:report - - name: Coveralls - uses: coverallsapp/github-action@v2 - continue-on-error: true - with: - github-token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@master + - name: Use Node.js 12.x + uses: actions/setup-node@v1 + with: + version: 12.x + - name: npm install, build, and test + run: | + npm install + npm run build + npm test diff --git a/.gitignore b/.gitignore index 6aa86aa9..e5ad5994 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,18 @@ -*.swp -*.log -*.lock - -.nyc_output -.DS_Store -.idea - package-lock.json -npm-debug.log* - +yarn.lock +yarn-error.log node_modules +npm-debug.log* coverage + modules/execon modules/emitify + +.nyc_output + +*.swp +.putoutcache + dist dist-dev + diff --git a/.madrun.js b/.madrun.js index efa8ce98..ed82e78c 100644 --- a/.madrun.js +++ b/.madrun.js @@ -1,62 +1,178 @@ -import {run, cutEnv} from 'madrun'; -import {defineEnv} from 'supertape/env'; +'use strict'; -const testEnv = defineEnv({ - timeout: 7000, - css: true, -}); +const { + run, + parallel, + predefined, +} = require('madrun'); -const buildEnv = { - NODE_ENV: 'production', -}; +const {version} = require('./package'); -export default { +const names = [ + 'bin/cloudcmd.js', + 'client', + 'common', + 'server', + 'test', + 'bin/release.js', + 'webpack.config.js', + 'cssnano.config.js', + '.webpack', + '.eslintrc.js', + '.madrun.js', + '{client,server,common}/**/*.spec.js', +]; + +const {putout} = predefined; + +const env = 'THREAD_IT_COUNT=0'; +const dockerName = 'coderaiser/cloudcmd'; + +module.exports = { 'start': () => 'node bin/cloudcmd.js', - 'start:dev': async () => await run('start', null, { - NODE_ENV: 'development', - }), + 'start:dev': () => `NODE_ENV=development ${run('start')}`, 'build:start': () => run(['build:client', 'start']), - 'build:start:dev': () => run([ - 'build:client:dev', - 'start:dev', - ]), - 'lint:all': () => run('lint:progress'), - 'lint': () => 'redlint fix; putout .', - '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'), - 'fix:lint': async () => `putout --fix . && redlint fix`, - 'lint:stream': () => run('lint', '-f stream'), - 'test': () => [testEnv, `tape '{test}/**/*.js' '{bin,client,static,common,server}/**/*.spec.js' -f fail`], - 'test:e2e': () => `tape 'test-e2e/**/*.js'`, + 'build:start:dev': () => run(['build:client:dev', 'start:dev']), + 'lint:all': () => run(['lint', 'lint:css', 'spell']), + 'lint:base': () => putout({ + names, + }), + 'lint:css': () => 'stylelint css/*.css', + 'spell': () => 'yaspeller .', + 'fix:lint': () => run(['lint', 'lint:css'], '--fix'), + 'lint': () => run('lint:progress', '--cache'), + 'lint:progress': () => run('lint:base', '-f progress'), + 'lint:stream': () => run('lint:base', '-f stream'), + + 'test:base': () => { + const cmd = 'tape'; + const names = `'test/**/*.js' '{client,static,common,server}/**/*.spec.js'`; + + return `${cmd} ${names}`; + }, + + 'test': () => `${env} ${run('test:base')}`, 'test:client': () => `tape 'test/client/**/*.js'`, 'test:server': () => `tape 'test/**/*.js' 'server/**/*.spec.js' 'common/**/*.spec.js'`, - 'wisdom': async () => await run(['lint:all', 'build', 'test'], null, { - CI: 1, - }), + 'wisdom': () => run(['lint:all', 'build', 'test']), 'wisdom:type': () => 'bin/release.js', - 'coverage': async () => [testEnv, `c8 ${await cutEnv('test')}`], - 'coverage:report': () => 'c8 report --reporter=lcov', - 'report': () => 'c8 report --reporter=lcov', - '6to5': () => [buildEnv, 'rspack build --config rspack.config.js'], + 'docker:pull': () => 'docker pull node', + 'docker:pull:alpine': () => 'docker pull mhart/alpine-node', + 'docker:pull:arm32': () => 'docker pull arm32v7/node:slim', + 'docker:pull:arm64': () => 'docker pull arm64v8/node:slim', + 'docker:push': () => dockerPush('x64', version), + 'docker:push:latest': () => dockerPush('x64'), + 'docker:push:alpine': () => dockerPush('alpine', version), + 'docker:push:alpine:latest': () => dockerPush('alpine'), + 'docker:push:arm32': () => dockerPush('arm32', version), + 'docker:push:arm32:latest': () => dockerPush('arm32'), + 'docker:push:arm64': () => dockerPush('arm64', version), + 'docker:push:arm64:latest': () => dockerPush('arm64'), + 'docker:build': () => dockerBuild('docker/Dockerfile', 'x64', version), + 'docker:build:alpine': () => dockerBuild('docker/Dockerfile.alpine', 'alpine', version), + 'docker:build:arm32': () => dockerBuild('docker/arm/Dockerfile.arm32v7', 'arm32', version), + 'docker:build:arm64': () => dockerBuild('docker/arm/Dockerfile.arm64v8', 'arm64', version), + 'docker:manifest:create': () => { + const images = [ + `${dockerName}:latest`, + `${dockerName}:latest-x64`, + // `${dockerName}:latest-arm32`, + // `${dockerName}:latest-arm64`, + ].join(' '); + + return `docker manifest create ${images}`; + }, + 'docker:manifest:push': () => `docker manifest push ${dockerName}:latest`, + 'docker': () => run(['docker:x64', 'docker:alpine', 'docker:manifest:*']), + 'docker-ci': () => run(['build', 'docker-login', 'docker']), + 'docker-login': () => 'docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD', + + 'docker:x64': () => run([ + 'docker:pull', + 'docker:build', + 'docker:tag', + 'docker:push', + 'docker:push:latest', + ]), + + 'docker:alpine': () => run([ + 'docker:pull:alpine', + 'docker:build:alpine', + 'docker:tag:alpine', + 'docker:push:alpine', + 'docker:push:alpine:latest', + ]), + + /* + 'docker:arm32': () => run([ + 'docker:pull:arm32', + 'docker:build:arm32', + 'docker:tag:arm32', + 'docker:push:arm32', + 'docker:push:arm32:latest', + ]), + + 'docker:arm64': () => run([ + 'docker:pull:arm64', + 'docker:build:arm64', + 'docker:tag:arm64', + 'docker:push:arm64', + 'docker:push:arm64:latest', + ]), + */ + + 'docker:manifest': () => run([ + 'docker:manifest:create', + 'docker:manifest:push', + ]), + + 'docker:tag': () => dockerTag('x64', version), + 'docker:tag:alpine': () => dockerTag('alpine', version), + 'docker:tag:arm32': () => dockerTag('arm32', version), + 'docker:tag:arm64': () => dockerTag('arm64', version), + 'docker:rm:version': () => dockerRmi('x64', version), + 'docker:rm:latest': () => dockerRmi('x64'), + 'docker:rm:alpine': () => dockerRmi('alpine', version), + 'docker:rm:latest-alpine': () => dockerRmi('alpine'), + 'docker:rm:arm32': () => dockerRmi('arm32', version), + 'docker:rm:latest-arm32': () => dockerRmi('arm32'), + 'docker:rm:arm64': () => dockerRmi('arm64', version), + 'docker:rm:latest-arm64': () => dockerRmi('arm64'), + 'docker:rm-old': () => `${parallel('docker:rm:*')} || true`, + + 'coverage': () => `${env} nyc ${run('test:base')}`, + 'report': () => 'nyc report --reporter=text-lcov | coveralls', + '6to5': () => 'webpack --progress', '6to5:client': () => run('6to5', '--mode production'), - '6to5:client:dev': async () => await run('6to5', '--mode development', { - NODE_ENV: 'development', - }), + '6to5:client:dev': () => `NODE_ENV=development ${run('6to5', '--mode development')}`, 'pre6to5:client': () => 'rimraf dist', 'pre6to5:client:dev': () => 'rimraf dist-dev', 'watch:client': () => run('6to5:client', '--watch'), 'watch:client:dev': () => run('6to5:client:dev', '--watch'), 'watch:server': () => 'nodemon bin/cloudcmd.js', - 'watch:test': async () => [testEnv, `nodemon -w client -w server -w test -w common -x ${await cutEnv('test')}`], - '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'), + 'watch:lint': () => `nodemon -w client -w server -w webpack.config.js -x ${run('lint')}`, + 'watch:test': () => `nodemon -w client -w server -w test -w common -x ${run('test')}`, + 'watch:test:client': () => `nodemon -w client -w test/client -x ${run('test:client')}`, + 'watch:test:server': () => `nodemon -w client -w test/client -x ${run('test:server')}`, + 'watch:coverage': () => `nodemon -w server -w test -w common -x ${run('coverage')}`, + 'build': () => run('6to5:*'), 'build:client': () => run('6to5:client'), 'build:client:dev': () => run('6to5:client:dev'), 'heroku-postbuild': () => run('6to5:client'), }; + +function dockerPush(type, version = 'latest') { + return `docker push coderaiser/cloudcmd:${version}-${type}`; +} + +function dockerBuild(file, type, version) { + return `docker build -f ${file} -t coderaiser/cloudcmd:${version}-${type} .`; +} + +function dockerTag(type, version) { + return `docker tag coderaiser/cloudcmd:${version}-${type} coderaiser/cloudcmd:latest-${type}`; +} + +function dockerRmi(type, version = 'latest') { + return `docker rmi -f coderaiser/cloudcmd:${version}-${type}`; +} diff --git a/.npmignore b/.npmignore index b83b5923..f0c9ab36 100644 --- a/.npmignore +++ b/.npmignore @@ -1,29 +1,30 @@ -*.spec.* -*.config.* -*.fixture.js* -*.ai -*.cdr -*.eps -*.log -*.lock - .* - +*.spec.js +*.fixture.js* manifest.yml -docker-compose.yml -now.json -app.json -bower.json -deno.json -bin/release.* -img/logo/cloudcmd-hq.png -webpack.config.js - docker -test* +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 + +webpack.config.js + +*.ai +*.cdr +*.eps + diff --git a/.nycrc.json b/.nycrc.json deleted file mode 100644 index 1fe174f3..00000000 --- a/.nycrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "checkCoverage": false, - "all": false, - "exclude": [ - "**/*.spec.js", - "**/*.*.js", - "**/*.config.*", - "**/fixture", - "**/test/**" - ], - "branches": 100, - "lines": 100, - "functions": 100, - "statements": 100 -} diff --git a/.putout.json b/.putout.json index 7154775a..e8ed3cf0 100644 --- a/.putout.json +++ b/.putout.json @@ -1,60 +1,27 @@ { - "plugins": ["cloudcmd"], - "ignore": [ - "*.md", - "app.json", - "fontello.json", - "html", - "fixture*" - ], - "rules": { - "package-json/add-type": "off" - }, "match": { - ".filesystem.json": { - "nodejs/rename-file-cjs-to-js": "off" - }, - "base64": { - "types/convert-typeof-to-is-type": "off" - }, - "*.md": { - "nodejs/convert-commonjs-to-esm": "on" - }, - ".webpack": { - "webpack": "on" - }, "server": { - "nodejs/remove-process-exit": "on" + "remove-process-exit": true }, - "server/{server,exit}.js": { - "nodejs/remove-process-exit": "off" + "server/(server|exit).js": { + "remove-process-exit": false, + "remove-console": false }, - "server/{server,exit,terminal,distribute/log}.{js,mjs}": { - "remove-console": "off" + "server/(terminal|distribute/log).js": { + "remove-console": false }, - "client/{client,cloudcmd,load-module}.{js,mjs}": { - "remove-console": "off" + "client/(client|cloudcmd|load-module).js": { + "remove-console": false }, - "client": { - "nodejs": "off" - }, - "client/sw": { - "remove-console": "off" + "client/modules/config/index.js": { + "apply-shorthand-properties": [{ + "ignore": [ + "ONE_MINUTE" + ] + }] }, "test/common/cloudfunc.js": { - "remove-console": "off" - }, - "storage.js": { - "promises/remove-useless-async": "off" - }, - "docker.yml": { - "github/set-node-versions": "off" - }, - "vim.js": { - "merge-duplicate-functions": "off" - }, - "common": { - "nodejs/declare": "off" + "remove-console": false } } } diff --git a/.rspack/css.js b/.rspack/css.js deleted file mode 100644 index 10d28250..00000000 --- a/.rspack/css.js +++ /dev/null @@ -1,38 +0,0 @@ -import {env} from 'node:process'; -import {rspack} from '@rspack/core'; - -const {CssExtractRspackPlugin} = rspack; -const isDev = env.NODE_ENV === 'development'; - -const plugins = [ - new CssExtractRspackPlugin({ - filename: '[name].css', - }), -]; - -const rules = [{ - test: /\.css$/i, - use: [CssExtractRspackPlugin.loader, { - loader: 'css-loader', - options: { - url: true, - }, - }], -}, { - test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/, - type: 'asset/inline', -}]; - -export default { - mode: isDev ? 'development' : 'production', - plugins, - module: { - rules, - }, - optimization: { - minimize: !isDev, - minimizer: [ - new rspack.LightningCssMinimizerRspackPlugin(), - ], - }, -}; diff --git a/.rspack/html.js b/.rspack/html.js deleted file mode 100644 index e90038ac..00000000 --- a/.rspack/html.js +++ /dev/null @@ -1,36 +0,0 @@ -import {env} from 'node:process'; -import HtmlWebpackPlugin from 'html-webpack-plugin'; - -const isDev = env.NODE_ENV === 'development'; - -export const plugins = [ - new HtmlWebpackPlugin({ - inject: false, - template: 'html/index.html', - minify: !isDev && getMinifyHtmlOptions(), - }), -]; - -function getMinifyHtmlOptions() { - return { - 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, - - minifyJS: true, - }; -} diff --git a/.rspack/js.js b/.rspack/js.js deleted file mode 100644 index 251d85f6..00000000 --- a/.rspack/js.js +++ /dev/null @@ -1,173 +0,0 @@ -import {resolve, sep} from 'node:path'; -import {fileURLToPath} from 'node:url'; -import {env} from 'node:process'; -import {rspack} from '@rspack/core'; - -const resolveModule = (a) => fileURLToPath(import.meta.resolve(a)); - -const { - EnvironmentPlugin, - NormalModuleReplacementPlugin, - ProvidePlugin, -} = rspack; - -const modules = './modules'; -const dirModules = './client/modules'; -const dirCss = './css'; -const dirThemes = `${dirCss}/themes`; -const dirColumns = `${dirCss}/columns`; -const dir = './client'; -const {NODE_ENV} = env; -const isDev = NODE_ENV === 'development'; - -const rootDir = new URL('..', import.meta.url).pathname; -const dist = resolve(rootDir, 'dist'); -const distDev = resolve(rootDir, 'dist-dev'); -const devtool = isDev ? 'eval' : 'source-map'; - -const noParse = (a) => a.endsWith('.spec.js'); - -// codegen.macro is a babel-macro (build-time codegen), not supported by -// Rspack's native SWC transform, so client/sw/sw.js (the only file that -// uses it) keeps going through babel-loader. Everything else uses -// Rspack's built-in SWC loader, which is the main source of the speedup. -const rules = [{ - test: /sw\/sw\.js$/, - exclude: /node_modules/, - loader: 'babel-loader', -}, { - test: /\.[mc]?js$/, - exclude: [/node_modules/, /sw\/sw\.js$/], - loader: 'builtin:swc-loader', - options: { - jsc: { - parser: { - syntax: 'ecmascript', - }, - }, - env: { - targets: 'defaults', - }, - }, -}]; - -const plugins = [ - new NormalModuleReplacementPlugin(/^node:/, (resource) => { - resource.request = resource.request.replace(/^node:/, ''); - }), - new NormalModuleReplacementPlugin(/^putout$/, '@putout/bundle'), - new EnvironmentPlugin({ - NODE_ENV, - }), - new ProvidePlugin({ - process: 'process/browser', - }), -]; - -const splitChunks = { - 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, - }, - }, -}; - -export default { - resolve: { - symlinks: false, - alias: { - 'node:process': 'process', - 'node:path': 'path', - }, - fallback: { - path: resolveModule('path-browserify'), - process: resolveModule('process/browser'), - util: resolveModule('util'), - }, - }, - devtool, - optimization: { - splitChunks, - }, - entry: { - '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', - path: isDev ? distDev : dist, - pathinfo: isDev, - devtoolModuleFilenameTemplate, - publicPath: '/dist/', - }, - module: { - rules, - noParse, - }, - plugins, - performance: { - maxEntrypointSize: 800_000, - maxAssetSize: 600_000, - }, -}; - -function devtoolModuleFilenameTemplate(info) { - const resource = info.absoluteResourcePath.replace(rootDir + sep, ''); - return `file://cloudcmd/${resource}`; -} diff --git a/.stylelintrc.yml b/.stylelintrc.yml new file mode 100644 index 00000000..a94abc6b --- /dev/null +++ b/.stylelintrc.yml @@ -0,0 +1,17 @@ +extends: stylelint-config-standard +rules: + indentation: 4 + declaration-block-trailing-semicolon: always + declaration-colon-space-before: null + selector-list-comma-newline-after: null + comment-empty-line-before: null + number-leading-zero: null + number-no-trailing-zeros: null + string-quotes: single + function-url-quotes: never + no-eol-whitespace: null + font-family-name-quotes: always-unless-keyword + font-family-no-missing-generic-family-keyword: null + rule-empty-line-before: null + max-empty-lines: 2 + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..681a3b5e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,52 @@ +language: node_js +node_js: + - 14 + - 12 + - 10 + +cache: + npm: false + +matrix: + allow_failures: + - os: windows + +os: + - linux + - osx + - windows + +# https://docs.travis-ci.com/user/customizing-the-build/#git-end-of-line-conversion-control +# need for windows +git: + autocrlf: input + + +script: + - npm run lint && npm run build && npm run coverage && npm run report + +notifications: + email: + on_success: never + on_failure: change + +sudo: required + +services: + - docker + +before_deploy: + - echo '{"experimental":"enabled"}' | sudo tee /etc/docker/daemon.json + - mkdir -p $HOME/.docker + - echo '{"experimental":"enabled"}' | tee $HOME/.docker/config.json + - sudo service docker start + +deploy: + provider: script + script: npm run docker-ci + skip_cleanup: true + on: + node: 12 + condition: $TRAVIS_OS_NAME = linux + tags: true + all_branches: false diff --git a/.typos.toml b/.typos.toml deleted file mode 100644 index dc8af253..00000000 --- a/.typos.toml +++ /dev/null @@ -1,2 +0,0 @@ -[files] -extend-exclude = ["ChangeLog"] diff --git a/.webpack/css.js b/.webpack/css.js new file mode 100644 index 00000000..df2919b4 --- /dev/null +++ b/.webpack/css.js @@ -0,0 +1,81 @@ +'use strict'; + +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(), +]); + +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', + options: { + limit: 100000, + }, + }, +}]; + +module.exports = { + plugins, + module: { + rules, + }, +}; + +function getCSSList(dir) { + const base = (a) => basename(a, extname(a)); + const addDir = (name) => `${dir}/${name}`; + const rootDir = join(__dirname, '..'); + + return fs.readdirSync(`${rootDir}/css/${dir}`) + .map(base) + .map(addDir); +} + +function extract(extractPlugin) { + const {filename} = extractPlugin; + + return { + test: RegExp(`css/${filename}`), + use: extractPlugin.extract([ + 'css-loader', + ]), + }; +} + diff --git a/.webpack/html.js b/.webpack/html.js new file mode 100644 index 00000000..28e40c65 --- /dev/null +++ b/.webpack/html.js @@ -0,0 +1,43 @@ +'use strict'; + +const {env} = process; +const isDev = env.NODE_ENV === 'development'; + +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +const plugins = [ + new HtmlWebpackPlugin({ + inject: false, + template: 'html/index.html', + minify: !isDev && getMinifyHtmlOptions(), + }), +]; + +module.exports = { + plugins, +}; + +function getMinifyHtmlOptions() { + return { + 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, + + minifyJS: true, + }; +} + diff --git a/.webpack/js.js b/.webpack/js.js new file mode 100644 index 00000000..8f32f4bd --- /dev/null +++ b/.webpack/js.js @@ -0,0 +1,134 @@ +'use strict'; + +const { + resolve, + sep, + join, +} = require('path'); + +const {EnvironmentPlugin} = require('webpack'); + +const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin'); + +const dir = './client'; +const dirModules = './client/modules'; +const modules = './modules'; + +const {env} = process; +const {NODE_ENV} = env; +const isDev = NODE_ENV === 'development'; + +const rootDir = join(__dirname, '..'); +const dist = resolve(rootDir, 'dist'); +const distDev = resolve(rootDir, 'dist-dev'); +const devtool = isDev ? 'eval' : 'source-map'; + +const notEmpty = (a) => a; +const clean = (array) => array.filter(notEmpty); + +const noParse = (a) => /\.spec\.js$/.test(a); + +const options = { + babelrc: false, + plugins: [ + 'module:babel-plugin-macros', + ], +}; + +const rules = clean([ + !isDev && { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader', + }, + isDev && { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader', + options, + }]); + +const plugins = [ + new EnvironmentPlugin({ + NODE_ENV, + }), + + new ServiceWorkerWebpackPlugin({ + entry: join(__dirname, '..', 'client', 'sw', 'sw.js'), + excludes: ['*'], + }), +]; + +const splitChunks = { + name: 'cloudcmd.common', + chunks: 'all', +}; + +module.exports = { + resolve: { + symlinks: false, + }, + 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.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`, + }, + output: { + filename: '[name].js', + path: isDev ? distDev : dist, + pathinfo: isDev, + devtoolModuleFilenameTemplate, + publicPath: '/dist/', + }, + externals: [ + externals, + ], + module: { + rules, + noParse, + }, + plugins, + performance: { + maxEntrypointSize: 600000, + maxAssetSize: 600000, + }, +}; + +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/.yaspellerrc b/.yaspellerrc index 7e29119e..ad0eddc1 100644 --- a/.yaspellerrc +++ b/.yaspellerrc @@ -22,14 +22,12 @@ "Iptables", "JitSu", "Node", - "IO", "Olena", "TarZak", "Termux", "Zalitok", "WebSocket", "auth", - "autostart", "binded", "cd", "cloudcmd", @@ -46,7 +44,6 @@ "gz", "io", "js", - "linux", "maintainers", "markdown", "microservice", @@ -56,8 +53,6 @@ "nginx", "npm", "or io", - "patreon", - "rc", "refactor", "sexualized", "sslPort", @@ -66,7 +61,6 @@ "v0", "v1", "v2", - "yml", - "systemd" + "yml" ] } diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 0e2522dd..7e77a3a2 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo Examples of behavior that contributes to creating a positive environment include: -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members Examples of unacceptable behavior by participants include: -- The use of sexualized language or imagery and unwelcome sexual attention or advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6b6b4ef..c7c9b914 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,30 +1,27 @@ -## Commit - +Commit +--------------- 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. For example util, console, view, edit, style etc... **Subject text**: - - use imperative, present tense: “change” not “changed” nor “changes” - don't capitalize first letter - no dot (.) at the end - **Message body**: +**Message body**: - just as in use imperative, present tense: “change” not “changed” nor “changes” - includes motivation for the change and contrasts with previous behavior **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 c5e0fbca..1763c0da 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,1948 +1,3 @@ -2026.06.15, v19.19.1 - -feature: -- 78b206e0 client: key: Ctrl + L -> Ctrl + Shift + L: Log out (#466) -- b1492885 docker: io: NPM_CONFIG_PACKAGE_LOCK=false -- 76d7bf4d docker: typos, shellcheck: add - -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: -- (package) putout v23.0.0 - -2021.12.16, v15.9.12 - -fix: -- (client) loadDir: history enabled by default - -2021.12.09, v15.9.11 - -fix: -- (client) load: Upload stuck for zero byte files (#359) - -2021.12.04, v15.9.10 - -fix: -- (client) operation: move and copy not working (#358) -feature: -- (package) eslint-plugin-putout v12.2.0 - -2021.11.22, v15.9.9 - -feature: -- (package) putout v22.0.2 -- (package) yargs-parser v21.0.0 -- (package) yaspeller v8.0.0 -- (package) eslint-plugin-putout v11.1.0 -- (package) putout v21.1.2 -- (package) eslint v8.0.1 -- (package) eslint-plugin-putout v10.3.0 -- (package) find-up v6.1.0 -- (package) putout v20.5.1 -- (package) supertape v6.9.1 -- (package) eslint-plugin-putout v9.0.1 - -2021.08.09, v15.9.8 - -fix: -- (cloudcmd) args: snake case -> camelCase: yargs strip dashed - - -2021.06.27, v15.9.7 - -fix: -- (config) rm broadcast: cannot change config -- (view) close modals using ESC - -feature: -- (package) package-json v7.0.0 -- (config) add more descriptive titles - - -2021.06.13, v15.9.6 - -feature: -- (package) @cloudcmd/move-files v5.0.0 - - -2021.06.08, v15.9.5 - -fix: -- (load) put: replace #: single -> multiple -- (server) user-menu: e.message -> errorFind.message - -feature: -- (package) eslint-plugin-putout v8.0.1 -- (package) es6-promisify v7.0.0 -- (package) putout v18.0.1 -- (package) putout v17.0.0 - - -2021.04.05, v15.9.4 -feature: -- (package) socket.io-client v4.0.1 -- (package) open v8.0.5 -- (package) putout v16.4.0 - -2021.03.17, v15.9.3 - -fix: -- (cloudcmd) Docker latest-alpine (15.9.2) stopped with errors (#337) - - -2021.03.16, v15.9.2 - -fix: -- (route) unable to navigate between folders (#333) - -feature: -- (package) open v8.0.2 -- (package) socket.io-client v4.0.0 -- (package) socket.io v4.0.0 - - -2021.03.01, v15.9.1 - -feature: -- (cloudcmd) icons: add archive icon for small screens -- (cloudcmd) add archive-link image - - -2021.03.01, v15.9.0 - -feature: -- feature(cloudcmd) add archive icon -- feature(client) rm unused loadCurrentTime -- feature(package) win32 v6.0.0 -- feature(package) redzip v2.0.0 - - - -2021.02.27, v15.8.1 - -feature: -- (package) restafary v9.7.0 (#330) - - -2021.02.23, v15.8.0 - -feature: -- (cloudcmd) add ability to suggest option, when wrong provided -- (package) supertape v5.0.0 - - -2021.02.21, v15.7.1 - -fix: -- (cloudcmd) prefix env has no effect (#328) - - -2021.02.19, v15.7.0 - -fix: -- chore(github-actions) add ability to autofix lint - -feature: -- (package) @cloudcmd/move-files v4.0.0 -- (package) putout v15.0.0 -- (package) copymitter v6.0.0 -- (package) @cloudcmd/olark v3.0.2 -- (client) view: unknown file type: method: GET -> HEAD: speed up - - -2021.02.03, v15.6.0 - -feature: -- (client) view: add ability to detect file type when extension is missing (coderaiser/cloudcmd#287) - - -2021.02.02, v15.5.2 - -fix: -- (client) view: isAudio (#322) - - -2021.01.31, v15.5.1 - -fix: -- (cloudcmd) cannot create a directory (#319) - - -2021.01.30, v15.5.0 - -feature: -- (package) style-loader v2.0.0 -- (cloudcmd) improve error handling when viewing or editing a file -- (package) putout v14.0.0 -- (package) eslint-plugin-putout v7.0.0 - - -2021.01.26, v15.4.4 - -feature: -- (package) win32 v5.1.11 - - -2021.01.25, v15.4.3 - -feature: -- (package) restbox v3.0.0 -- (package) ponse v6.0.0 -- (package) files-io v4.0.0 - - -2021.01.21, v15.4.2 - -feature: -- (package) mellow v3.0.0 -- (route) tokenize -> wraptile - - -2021.01.20, v15.4.1 - -feature: -- (route) redzip -> win32 (#317) - - -2021.01.19, v15.4.0 - -feature: -- (cloudcmd) add abilit to parse markdown inside zip archives - - -2021.01.19, v15.3.4 - -fix: -- (docker) images: change alpine images to be part of master image (#316) -- (dom) bug in Firefox with Imagus extension enabled: change getCurrentName to read from `data-name` instead of `title` (#313) - -feature: -- (package) deepword v8.0.0 -- (package) console-io v13.0.0 -- (package) edward v13.0.0 -- (docker) add support of multi-arch builds (#291) - - -2021.01.18, v15.3.3 - -fix: -- (dom) bug in Firefox with Imagus extension enabled: change getCurrentName to read from `data-name` instead of `title` (#313) -- (client) dom: goToDirectory - - -2021.01.18, v15.3.2 - -fix: -- (client) dom: goToDirectory - - -2021.01.17, v15.3.1 - -fix: -- (view) view html, pdf - -feature: -- (package) dword v13.0.0 -- (modules) socket.io v3.1.0 - - -2021.01.17, v15.3.0 - -feature: -- (client) improve vim support: Esc - toggle vim support in editors -- (client) view: add ability to view raw files using Shift + F3 -- (client) view: show markdown without shift -- (key) add ability to toggle global vim config on the time of session - - -2021.01.16, v15.2.0 - -fix: -- (server) distribute: simplify colors - -feature: -- (cloudcmd) add ability to open html files using F3 -- (cloudcmd) add ability to toggle vim hotkes using Esc -- (package) @cloudcmd/fileop v5.0.0 - - -2021.01.07, v15.1.0 - -feature: -- (cloudcmd) add ability to read zip files like directories (#309) - - -2021.01.05, v15.0.4 - -feature: -- (package) flop v9.0.0 -- (package) restafary v9.0.1 - - -2021.01.05, v15.0.3 - -fix: -- (cloudcmd) validateRoot: import -> simport: forEachKey is not a function (#311) - -feature: -- (package) onezip v5.0.0 -- (package) socket.io v3.0.5 (socketio/socket.io#3720) - - -2020.12.31, v15.0.2 - -fix: -- (cloudcmd) criton: crash when password set (#310) - - -2020.12.30, v15.0.1 - -fix: -- (cloudcmd) css: media query for screen size with width more then 1600 (cloudcmd/console-io#6) -- (cloudcmd) client: build: optional chaining -- (cloudcmd) drop support of node < 14 - -feature: -- (package) putout v13.0.0 - - -2020.12.28, v15.0.0 - -fix: -- (cloudcmd) app, help: remove duplicate keys -- chore(cloudcmd) menu: add lint, :lint, test, coverage -- (cloudcmd) importListen prevent server from start (#295) - -feature: -- (package) auto-globals v2.0.0 -- (cloudcmd) convert to EcmaScriptModules -- (package) supertape v4.1.0 -- (package) supertape v3.10.0 -- (package) babel-plugin-macros v3.0.0 -- (package) putout v12.0.0 -- (package) supertape v3.1.3 -- (package) madrun v8.0.0 -- (package) gritty v6.0.0 -- (package) console-io v12.0.0 -- (package) socket.io-client v3.0.1 -- (package) socket.io v3.0.1 -- (package) putout v11.0.2 -- (package) eslint-plugin-putout v6.0.0 -- (package) markdown-it v12.0.0 -- (package) putout v10.0.0 -- (package) gritty v5.0.0 -- (distribute) import: do not import if env variable set -- (package) drop support of node < 12 -- (package) table v6.0.1 - - -2020.08.21, v14.9.3 - -fix: -- (client) dom: buffer: get, set -> getJson, setJson (#295) - -feature: -- (storage) rm unused removeMatch - - -2020.08.19, v14.9.2 - -fix: -- (cloudcmd) client: edit: json files (#294) - - -2020.08.16, v14.9.1 - -feature: -- (cloudcmd) add IO.createDirectory (https://github.com/coderaiser/putout/commit/b54b5486f438804120df261dafa8e6985201a4eb) - - -2020.08.14, v14.9.0 - -feature: -- (key) vim: add ability to navigate to next and previous using w and b -- (key) vim: add ability to navigate using to first and last file using ^ and $ - - -2020.08.14, v14.8.0 - -feature: -- (cloudcmd) IO.cp -> IO.copy -- (cloudcmd) IO.mv -> IO.move -- (package) find-up v5.0.0 - - -2020.08.11, v14.7.2 - -fix: -- (client) key: F3: search appears - - -2020.08.11, v14.7.1 - -fix: -- (view) init config: copy - - -2020.08.10, v14.7.0 - -fix: -- (client) user menu: click on user menu title -- test(cloudcmd) test/server/prefixer -> server/prefixer - -feature: -- (cloudcmd) rest: add rename -- (rest) mv: improved user errors -- (client) io: promisify -> async -- (package) webpack-merge v5.0.8 -- (package) eslint-plugin-putout v5.0.0 -- (package) putout v9.0.0 -- (package) madrun v7.0.0 - - 2020.05.20, v14.6.0 feature: @@ -6766,7 +4821,7 @@ fix: - (rest) onDelete: func(null, body) -> func - (rest) onStat: add var - (cloudcmd) change index path -- (server) start: url -> PREFIX +- (server) start: url -> URL - (server) start: SSLPort -> sslPort - (server) start: Port -> port - (dom) getCurrentDirPath: "," -> ";" @@ -7778,7 +5833,7 @@ with clicks on files. - Removed allowed from cache property in config. - Added ability to hide "Upload To" menu item if -no storage modules set up in modules.json. +no storage modules setted up in modules.json. - From now any file minifying only if last modified time was changed. @@ -7984,7 +6039,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 set up. +config.json only if it setted up. - Fixed bug with starting node from other then projects dir. @@ -8293,7 +6348,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 -set b char: "0b". +setted b char: "0b". - Added supporting of Russian language in directory names. diff --git a/HELP.md b/HELP.md index 2967f0d0..99362aa5 100644 --- a/HELP.md +++ b/HELP.md @@ -1,27 +1,25 @@ -# Cloud Commander v19.19.1 +# Cloud Commander v14.6.0 -### [Main][MainURL] [Blog][BlogURL] [Demo][DemoURL] [Deploy](#deploy) +### [Main][MainURL] [Blog][BlogURL] Live(![Heroku][Heroku_LIVE_IMG] [Heroku][HerokuURL]) -[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" +[MainURL]: http://cloudcmd.io "Main" +[BlogURL]: http://blog.cloudcmd.io "Blog" +[HerokuURL]: https://cloudcmd.herokuapp.com/ "Heroku" +[HEROKU_LIVE_IMG]: https://status.cloudcmd.io/host/cloudcmd.herokuapp.com/img/file.png "Heroku" -[DeployInstaPodsIMG]: https://img.shields.io/badge/deploy%20on-InstaPods-blue -[DeployInstaPodsURL]: https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd +[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" +[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" **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](https://cloudcmd.io/img/logo/cloudcmd.png "Cloud Commander") +![Cloud Commander](/img/logo/cloudcmd.png "Cloud Commander") -## Benefits +Benefits +--------------- - Open source (**MIT License**). - Has 2 classic panels. @@ -30,13 +28,14 @@ - 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] [Deepword][DEEPWORD] and [Qword][QWORD]. +- **3 built-in editors** with support of **syntax highlighting**: [Dword][DWORD], [Edward][EDWARD] and [Deepword][DEEPWORD]. - [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**. - Built-in archives extract: **zip**, **tar**, **gz**, **bz2**, **.tar.gz** and **.tar.bz2** (with help of [inly][INLY]). -## Installation +Installation +--------------- Installation is very simple: @@ -53,8 +52,8 @@ When in trouble, use: npm i cloudcmd -g --force ``` -## Usage - +Usage +--------------- To start the server, just run the global *npm* binary from your terminal: ```sh @@ -63,93 +62,88 @@ 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-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 +|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 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 PREFIX in your browser: +To begin using the web client, go to this URL in your browser: ``` http://localhost:8000 ``` -## Updating the app - +Updating the app +--------------- If you installed Cloud Commander with `npm`, stop the server. Then, reinstall it with: ```sh @@ -158,96 +152,87 @@ npm install cloudcmd -g Then, start the server again with `cloudcmd` and reload the page. -## Hot keys +Hot keys +--------------- -| Key |Operation -|:---------------------|:-------------------------------------------- -| `F1` | help -| `F2` | show `user menu` -| `F3` | view, change directory -| `Shift + F3` | view raw file, change directory -| `F4` | edit -| `F5` | copy -| `Alt` + `F5` | pack -| `F6` | rename/move -| `Shift` + `F6` | rename current file -| `F7` | new directory -| `Shift + F7` | new file -| `F8`, `Delete` | remove -| `Shift + Delete` | remove without prompt -| `F9` | menu -| `Alt` + `F9` | extract -| `F10` | config -| `*` | select/unselect all -| `+` | expand selection -| `-` | shrink selection -| `:` | open Command Line -| `Ctrl + X` | cut to buffer -| `Ctrl + C` | copy to buffer -| `Ctrl + V` | paste from buffer -| `Ctrl + Z` | clear buffer -| `Ctrl + P` | copy path -| `Ctrl + R` | refresh -| `Ctrl + D` | clear local storage -| `Ctrl + A` | select all files in a panel -| `Ctrl + M` | [rename selected files](https://github.com/coderaiser/cloudcmd/releases/tag/v12.1.0) in editor -| `Ctrl + U` | swap panels -| `Ctrl + F3` | sort by name -| `Ctrl + F5` | sort by date -| `Ctrl + F6` | sort by size -| `Ctrl + Shift + L` | logout -| `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`) +|Key |Operation +|:----------------------|:-------------------------------------------- +| `F1` | help +| `F2` | show `user menu` +| `F3` | view, change directory +| `Shift + F3` | view as markdown +| `F4` | edit +| `Shift + F4` | edit in "vim" mode +| `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 +| `Shift + Ctrl + M` | rename selected files in vim mode of 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 +| `Ctrl + Click` | open file on new tab ### 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 -| `md` | make directory -| `mf` | make file -| `tt` | show terminal -| `e` | edit file -| `cc` | copy -| `mm` | move -| `rr` | rename file +|Key |Operation +|:----------------------|:-------------------------------------------- +| `j` | navigate to next file +| `k` | navigate to previous file +| `dd` | remove current file +| `G` | navigate to bottom file +| `gg` | 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 Commands can be joined, for example: - - `5j` will navigate **5** files below current; - `d5j` will remove next **5** files; - `dG` will remove all files from current to bottom; -## Drag and drop - +Drag and drop +--------------- These file operations are accessible with "drag and drop". | Drag Mouse Button | Key | Origin | Destination |Operation @@ -257,12 +242,11 @@ These file operations are accessible with "drag and drop". | Left | | Panel | Desktop | download files | Left | | Desktop | Panel | upload files -## View - +View +--------------- ![View](/img/screen/view.png "View") ### Features - - View images. - View text files. - Play audio. @@ -275,8 +259,8 @@ These file operations are accessible with "drag and drop". | `F3` | open | `Esc` | close -## Edit - +Edit +--------------- ![Edit](/img/screen/edit.png "Edit") ### Hot keys @@ -289,8 +273,8 @@ These file operations are accessible with "drag and drop". For more details see [Edward hotkeys][EDWARD_KEYS]. -## Console - +Console +--------------- ![Console](/img/screen/console.png "Console") ### Hot keys @@ -303,8 +287,8 @@ For more details see [Edward hotkeys][EDWARD_KEYS]. For more details see [console hot keys](https://github.com/cloudcmd/console#hot-keys "Console Hot Keys"). -## Terminal - +Terminal +--------------- ![Terminal](/img/screen/terminal.png "Terminal") ### Install @@ -356,7 +340,8 @@ After that, you can use Cloud Commander's terminal in the same way as a normal s | `Shift` + `~` | open | `Shift` + `Esc` | close -## Environment Variables +Environment Variables +--------------- Every program executed in Cloud Commander's terminal has these environment variables: @@ -372,8 +357,8 @@ On Unix, you can use it this way: /home/coderaiser/cloudcmd/bin/cloudcmd.js ``` -## Configuration - +Configuration +--------------- ![Config](/img/screen/config.png "Config") ### Hot keys @@ -387,53 +372,52 @@ When you change any options, the `~/.cloudcmd.json` file is automatically update It can also be edited manually with any text editor. Here's a description of all options: -```json +```js { - "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 + "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 } ``` @@ -444,13 +428,10 @@ 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 themes -- `CLOUDCMD_THEME` - set themes "light" or "dark" -- `CLOUDCMD_MENU` - set menu "supermenu" or "aleman" +- `CLOUDCMD_COLUMNS` - set visible columns - `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 @@ -473,7 +454,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 @@ -482,11 +463,13 @@ When you press `F2` Cloud Commander will read a file `.cloudcmd.menu.js` by walk Let's consider example `user menu` works file: ```js -const RENAME_FILE = 'Rename file'; +const RENAME_FILE= 'Rename file'; -export default { - '__settings': { - select: [RENAME_FILE], +module.exports = { + __settings: { + select: [ + RENAME_FILE, + ], run: false, }, [`F2 - ${RENAME_FILE}`]: async ({DOM}) => { @@ -516,10 +499,7 @@ 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, @@ -562,13 +542,6 @@ Here you can find `API` that can be used in **User Menu**. **DOM** and **CloudCm - `TerminalRun` - module that shows `Terminal` with a `command` from options and closes terminal when everything is done. -**IO** Files API - -- `rename(from, to)` - rename `from` into `to` -- `move(from, to, names)` - rename files with a `names` `from` into `to`; -- `copy(from, to, names)` - copy files with a `names` `from` into `to`; -- `createDirectory(path)` - create directory with a `path`; - ### Distribute Being able to configure Cloud Commander remotely opens the doors to using it as microservice, and that's what the "distribute" options set out to do. @@ -633,8 +606,8 @@ The *export server* omits the following configuration fields: - `log` - `configDialog` -## Menu - +Menu +--------------- ![Menu](/img/screen/menu.png "Menu") Right-mouse click to show a context menu with these items: @@ -658,28 +631,19 @@ Right-mouse click to show a context menu with these items: ### Hot keys -| 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 +|Key |Operation +|:----------------------|:-------------------------------------------- +| `F9` | open +| `Esc` | close +One file panel +--------------- Cloud Commander can work in one panel mode when your screen size can't accommodate a second panel (such as on mobile or tablet), or via the `--one-file-panel` options flag. ![One file panel](/img/screen/one-file-panel.png "One file panel") -## Using as middleware +Using as middleware +--------------- Cloud Commander can be used as middleware for `node.js` applications based on [socket.io](http://socket.io "Socket.IO") and [express](http://expressjs.com "Express"): @@ -698,30 +662,29 @@ npm i cloudcmd express socket.io -S And create `index.js`: ```js -import http from 'node:http'; -import {cloudcmd} from 'cloudcmd'; -import {Server} from 'socket.io'; -import express from 'express'; +const http = require('http'); +const cloudcmd = require('cloudcmd'); +const io = require('socket.io'); +const app = require('express')(); -const app = express(); const port = 1337; const prefix = '/'; const server = http.createServer(app); -const socket = new Server(server, { - path: `${prefix}socket.io`, +const socket = io.listen(server, { + path: `${prefix}socket.io` }); const config = { - name: 'cloudcmd :)', + name: 'cloudcmd :)' }; const filePicker = { data: { FilePicker: { - key: 'key', - }, - }, + key: 'key' + } + } }; // override option from json/modules.json @@ -736,14 +699,14 @@ const { const configManager = createConfigManager({ configPath, -}); +}), app.use(prefix, cloudcmd({ - socket, // used by Config, Edit (optional) and Console (required) - config, // config data (optional) + socket, // used by Config, Edit (optional) and Console (required) + config, // config data (optional) modules, // optional configManager, // optional -})); +)); server.listen(port); ``` @@ -751,12 +714,11 @@ server.listen(port); Here is example with two `Config Managers`: ```js -import http from 'node:http'; -import cloudcmd from 'cloudcmd'; -import {Server} from 'socket.io'; -import express from 'express'; +const http = require('http'); +const cloudcmd = require('cloudcmd'); +const io = require('socket.io'); +const app = require('express')(); -const app = express(); const port = 8000; const prefix1 = '/1'; const prefix2 = '/2'; @@ -764,16 +726,15 @@ const prefix2 = '/2'; const {createConfigManager} = cloudcmd; const server = http.createServer(app); -const socket1 = new Server(server, { - path: `${prefix1}/socket.io`, +const socket1 = io.listen(server, { + path: `${prefix1}/socket.io` }); -const socket2 = new Server(server, { - path: `${prefix2}/socket.io`, +const socket2 = io.listen(server, { + path: `${prefix2}/socket.io` }); const configManager1 = createConfigManager(); - configManager1('name', '1'); const configManager2 = createConfigManager(); @@ -795,11 +756,9 @@ server.listen(port); If you want to enable authorization, you can pass credentials to Cloud Commander with a config. To generate a password, you can install `criton` with `npm i criton --save`, and use it (or any other way) to generate a hash of a password. ```js -import criton from 'criton'; +const criton = require('criton'); +const algo = 'sha512WithRSAEncryption'; // default -const algo = 'sha512WithRSAEncryption'; - -// default // you can generate a hash dynamically const password = criton('root', algo); @@ -813,8 +772,8 @@ const config = { algo, // optional auth, username, - password, -}; + pasword, +} ``` Now you're ready to go! @@ -831,8 +790,8 @@ cloudcmd --username name --password password --auth --save --no-server This command will create hash of your password and write it to `~/.cloudcmd.json`. -## Server - +Server +--------------- Standard practices dictate that no non-root process get to talk to the internet on a port less than 1024. Despite this, **I suggest you start Cloud Commander as a non-root process**. How can we get around this limitation? There's a couple of fast & easy ways. One of them is port forwarding: ### Iptables @@ -853,7 +812,6 @@ target prot opt source destination REDIRECT tcp -- anywhere anywhere tcp dpt:http redir ports 8000 REDIRECT tcp -- anywhere anywhere tcp dpt:https redir ports 4430 ``` - If you would want to get things back just clear rules (rule numbers **1** and **2**; in your list they could differ). ```sh @@ -862,7 +820,6 @@ iptables -t nat -D PREROUTING 1 ``` ### nginx - Get [nginx](http://nginx.org/ "nginx"). On Linux, you can run: ```sh @@ -913,6 +870,7 @@ For WebSocket support, (nginx >= v1.3.13) modify the `server` block like so: } ``` + If you need redirection from **http** to **https**, simply use: ```sh @@ -931,14 +889,14 @@ ln -s ./sites-available/io.cloudcmd.io ./sites-enabled /etc/init.d/nginx restart ``` -## Deploy +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 on InstaPods](https://instapods.com/deploy-button.svg)](https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd) - -## Docker +[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.png "Deploy to Heroku")]( https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd) +Docker +--------------- `Cloud Commander` can be used as [docker container](https://hub.docker.com/r/coderaiser/cloudcmd/ "Docker container") like so: ```sh @@ -986,414 +944,17 @@ While using Dropbox remember that there is no remote support for the console/ter - view - edit -## Automatically start cloudcmd on boot for `systemd` - -First, locate the command to run cloudcmd - -```sh -which cloudcmd -``` - -take note of the result and create a systemd entry by executing - -```sh -sudo nano /etc/systemd/system/cloudcmd.service -``` - -and use this template - -``` -[Unit] -Description = Cloud Commander - -[Service] -TimeoutStartSec = 0 -Restart = always -ExecStart = THE RESULT OF which cloudcmd WE'VE EXECUTED EARLIER -User = YOUR_USER - -[Install] -WantedBy = multi-user.target -``` - -Don't forget to change the line for `ExecStart` and `User` - -Save the changes and exit editor. - -You may now enable cloudcmd and set it to autostart on boot by running: - -```sh -sudo systemctl enable --now cloudcmd -``` - -## Automatically start cloudcmd on boot for `FreeBSD` - -First, locate the command to run cloudcmd - -``` -which cloudcmd -``` - -take note of the result and create a rc script - -``` -vi /usr/local/etc/rc.d/cloudcmd -``` - -and use this template - -``` -!/bin/sh -# -# PROVIDE: cloudcmd -# REQUIRE: LOGIN -# KEYWORD: shutdown - -# Author: IhatemyISP (ihatemyisp.net) -# Version: 1.0.0 - -# Description: -# This script runs Cloud Commander as a service under the supplied user on boot - -# 1) Place file in /usr/local/etc/rc.d/ -# 2) Add cloudcmd_enable="YES" to /etc/rc.conf -# 3) (Optional) To run as non-root, add cloudcmd_runAs="user" to /etc/rc.conf -# 4) (Optional) To pass Cloud Commander args, add cloudcmd_args="" to /etc/rc.conf - -# Freebsd rc library -. /etc/rc.subr - -# General Info -name="cloudcmd" # Safe name of program -program_name="cloudcmd" # Name of exec -title="CloudCommander" # Title to display in top/htop - -# RC.config vars -load_rc_config $name # Loading rc config vars -: ${cloudcmd_enable="NO"} # Default: Do not enable Cloud Commander -: ${cloudcmd_runAs="root"} # Default: Run Cloud Commander as root - -# Freebsd Setup -rcvar=cloudcmd_enable # Enables the rc.conf YES/NO flag -pidfile="/var/run/${program_name}.pid" # PID file location - -# Env Setup -export HOME=$( getent passwd "$cloudcmd_runAs" | cut -d: -f6 ) # Gets the home directory of the runAs user - -# Command Setup -exec_path="/usr/local/bin/${program_name}" # Path to the cloudcmd exec, /usr/local/bin/ when installed globally -output_file="/var/log/${program_name}.log" # Path to Cloud Commander output file - -# Command -command="/usr/sbin/daemon" -command_args="-r -t ${title} -u ${cloudcmd_runAs} -o ${output_file} -P ${pidfile} ${exec_path} ${cloudcmd_args}" - -# Loading Config -load_rc_config ${name} -run_rc_command "$1" -``` - -Enable autostart - -``` -echo cloudcmd_enable="YES" >> /etc/rc.conf -``` - -(Optional) Set user to run Cloud Commander as (default is root) - -``` -echo cloudcmd_runAs="user" >> /etc/rc.conf -``` - -Start the service (or just reboot) - -``` -service cloudcmd start -``` - -## Get involved +Get involved +--------------- There are a lot of ways to be involved in `Cloud Commander` development: -- support project on patreon: https://patreon.com/coderaiser; - if you find a bug or have an idea to share, [create an issue](https://github.com/coderaiser/cloudcmd/issues/new "Create issue"); - if you fixed a bug, typo or implemented a new feature, [create a pull request](https://github.com/coderaiser/cloudcmd/compare "Create pull request"); - if you know a language not currently translated, or would like to improve an existing translation, you can help with [site translations](https://github.com/coderaiser/cloudcmd/wiki "Cloud Commander community wiki"); -## Version history - -- *2026.06.15*, **[v19.19.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.19.1)** -- *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)** -- *2021.12.04*, **[v15.9.10](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.10)** -- *2021.11.22*, **[v15.9.9](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.9)** -- *2021.02.03*, **[v15.6.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.6.0)** -- *2021.02.02*, **[v15.5.2](//github.com/coderaiser/cloudcmd/releases/tag/v15.5.2)** -- *2021.01.31*, **[v15.5.1](//github.com/coderaiser/cloudcmd/releases/tag/v15.5.1)** -- *2021.01.30*, **[v15.5.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.5.0)** -- *2021.01.26*, **[v15.4.4](//github.com/coderaiser/cloudcmd/releases/tag/v15.4.4)** -- *2021.01.25*, **[v15.4.3](//github.com/coderaiser/cloudcmd/releases/tag/v15.4.3)** -- *2021.01.21*, **[v15.4.2](//github.com/coderaiser/cloudcmd/releases/tag/v15.4.2)** -- *2021.01.20*, **[v15.4.1](//github.com/coderaiser/cloudcmd/releases/tag/v15.4.1)** -- *2021.01.19*, **[v15.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.4.0)** -- *2021.01.19*, **[v15.3.4](//github.com/coderaiser/cloudcmd/releases/tag/v15.3.4)** -- *2021.01.17*, **[v15.3.1](//github.com/coderaiser/cloudcmd/releases/tag/v15.3.1)** -- *2021.01.17*, **[v15.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.3.0)** -- *2021.01.16*, **[v15.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.2.0)** -- *2021.01.07*, **[v15.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.1.0)** -- *2021.01.05*, **[v15.0.4](//github.com/coderaiser/cloudcmd/releases/tag/v15.0.4)** -- *2020.01.05*, **[v15.0.3](//github.com/coderaiser/cloudcmd/releases/tag/v15.0.3)** -- *2020.12.31*, **[v15.0.2](//github.com/coderaiser/cloudcmd/releases/tag/v15.0.2)** -- *2020.12.30*, **[v15.0.1](//github.com/coderaiser/cloudcmd/releases/tag/v15.0.1)** -- *2020.12.28*, **[v15.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.0.0)** -- *2020.08.21*, **[v14.9.3](//github.com/coderaiser/cloudcmd/releases/tag/v14.9.3)** -- *2020.08.19*, **[v14.9.2](//github.com/coderaiser/cloudcmd/releases/tag/v14.9.2)** -- *2020.08.16*, **[v14.9.1](//github.com/coderaiser/cloudcmd/releases/tag/v14.9.1)** -- *2020.08.14*, **[v14.9.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.9.0)** -- *2020.08.14*, **[v14.8.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.8.0)** -- *2020.08.11*, **[v14.7.2](//github.com/coderaiser/cloudcmd/releases/tag/v14.7.2)** -- *2020.08.11*, **[v14.7.1](//github.com/coderaiser/cloudcmd/releases/tag/v14.7.1)** -- *2020.08.10*, **[v14.7.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.7.0)** +Version history +--------------- - *2020.05.20*, **[v14.6.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.6.0)** - *2020.05.06*, **[v14.5.1](//github.com/coderaiser/cloudcmd/releases/tag/v14.5.1)** - *2020.05.04*, **[v14.5.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.5.0)** @@ -1754,11 +1315,12 @@ There are a lot of ways to be involved in `Cloud Commander` development: - *2012.07.11*, **[v0.1.1](//github.com/cloudcmd/archive/raw/master/cloudcmd-v0.1.1.zip)** - *2012.07.09*, **[v0.1.0](//github.com/cloudcmd/archive/raw/master/cloudcmd-v0.1.0.zip)** -## Special Thanks - -- [Olena Zalitok](https://www.linkedin.com/in/ozalitok-ux-ui/ "Olena Zalitok") for **logo** and **favicon**. +Special Thanks +--------------- +- [Olena Zalitok](https://zalitok.github.io/ "Olena Zalitok") for **logo** and **favicon**. - [TarZak](https://github.com/tarzak "TarZak") - - Russian and Ukrainian translations; - - config template and style; - - change order of directories and files; - - add ability to keep path and header when files are scrolling; + - Russian and Ukrainian translations; + - config template and style; + - change order of directories and files; + - add ability to keep path and header when files are scrolling; + diff --git a/LICENSE b/LICENSE index a0d7436b..50fc1d35 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ (The MIT License) -Copyright (c) 2012-2025 Coderaiser +Copyright (c) 2012-2019 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 dfe8d8c6..f903f022 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,25 @@ -# Cloud Commander v19.19.1 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![Deploy on InstaPods][DeployInstaPodsIMG]][DeployInstaPodsURL] +# Cloud Commander v14.6.0 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fcoderaiser%2Fcloudcmd.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fcoderaiser%2Fcloudcmd?ref=badge_shield) -### [Main][MainURL] [Blog][BlogURL] [Support][SupportURL] [Demo][DemoURL] +### [Main][MainURL] [Blog][BlogURL] Live([Heroku][HerokuURL]) -[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 -[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 +[NPM_INFO_IMG]: https://nodei.co/npm/cloudcmd.png +[MainURL]: http://cloudcmd.io "Main" +[BlogURL]: http://blog.cloudcmd.io "Blog" +[HerokuURL]: https://cloudcmd.herokuapp.com/ "Heroku" +[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 + +[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 **Cloud Commander** a file manager for the web with console and editor. @@ -27,7 +30,6 @@ ``` npm i cloudcmd -g ``` - ## Start For starting just type in console: @@ -45,8 +47,8 @@ Open url `http://localhost:8000` in browser. You will see something similar to this. ![View](https://cloudcmd.io/img/screen/view.png "View") -## Deploy +## Deploy `Cloud Commander` could be easily deployed to [Heroku][DeployURL]. [![Deploy][DeployIMG]][DeployURL] @@ -70,19 +72,17 @@ npm i cloudcmd express socket.io -S And create `index.js`: ```js -import http from 'node:http'; -import {cloudcmd} from 'cloudcmd'; -import {Server} from 'socket.io'; -import express from 'express'; - -const app = express(); +const http = require('http'); +const cloudcmd = require('cloudcmd'); +const io = require('socket.io'); +const app = require('express')(); const port = 1337; const prefix = '/'; const server = http.createServer(app); -const socket = new Server(server, { - path: `${prefix}socket.io`, +const socket = io.listen(server, { + path: `${prefix}/socket.io` }); const config = { @@ -93,8 +93,8 @@ const filePicker = { data: { FilePicker: { key: 'key', - }, - }, + } + } }; // override option from json/modules.json @@ -109,11 +109,11 @@ const { const configManager = createConfigManager({ configPath, -}); +}), app.use(prefix, cloudcmd({ - socket, // used by Config, Edit (optional) and Console (required) - config, // config data (optional) + socket, // used by Config, Edit (optional) and Console (required) + config, // config data (optional) modules, // optional configManager, // optional })); @@ -121,21 +121,12 @@ app.use(prefix, cloudcmd({ server.listen(port); ``` -## Docker - -The docker images are provided for multiple architectures and types. The following list shows all existing images: - -| Architecture | Type | -|----------------|--------------| -| amd64 | linux | -| arm64 (arm/v8) | linux | -| amd64 | linux-alpine | -| arm64 (arm/v8) | linux-alpine | - +Docker +--------------- `Cloud Commander` could be used as a [docker container](https://hub.docker.com/r/coderaiser/cloudcmd/ "Docker container") this way: ```sh -docker run -it --rm -v ~:/root -v /:/mnt/fs -w=/root -p 8000:8000 coderaiser/cloudcmd +docker run -t --rm -v ~:/root -v /:/mnt/fs -w=/root -p 8000:8000 coderaiser/cloudcmd ``` Config would be read from home directory, hosts root file system would be mount to `/mnt/fs`, @@ -161,15 +152,11 @@ When you create this file run: docker-compose up ``` -## Documentation - -More documentation you can find on https://cloudcmd.io/. - -## Get involved +Get involved +--------------- There is a lot ways to be involved in `Cloud Commander` development: -- support project on patreon: https://patreon.com/coderaiser; - if you find a bug or got idea to share [create an issue](https://github.com/coderaiser/cloudcmd/issues/new "Create issue"); - if you fixed a bug, typo or implemented new feature [create pull request](https://github.com/coderaiser/cloudcmd/compare "Create pull request"); - if you know languages you can help with [site translations](https://github.com/coderaiser/cloudcmd/wiki "Cloud Commander community wiki"); diff --git a/app.json b/app.json index 3dbe05a0..dbcdfac7 100644 --- a/app.json +++ b/app.json @@ -11,7 +11,6 @@ "folder", "orthodox" ], - "stack": "heroku-18", "env": { "NPM_CONFIG_PRODUCTION": { "description": "Keep false to install devDependencies and build frontend", @@ -63,6 +62,11 @@ "value": "", "required": false }, + "CLOUDCMD_CONFIG_DIALOG": { + "description": "show config dialog", + "value": "false", + "required": false + }, "CLOUDCMD_ONE_FILE_PANEL": { "description": "show one file panel", "value": "false", diff --git a/bin/cloudcmd.js b/bin/cloudcmd.js index 6ca6716c..005c7f1b 100755 --- a/bin/cloudcmd.js +++ b/bin/cloudcmd.js @@ -1,48 +1,37 @@ #!/usr/bin/env node -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 * 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', -}; +'use strict'; -process.on('unhandledRejection', exit); +const Info = require('../package'); +const DIR_SERVER = '../server/'; -const isUndefined = (a) => typeof a === 'undefined'; +const {promisify} = require('util'); +const tryToCatch = require('try-to-catch'); -const choose = (a, b) => { - if (isUndefined(a)) - return b; - - return a; -}; +const exit = require(DIR_SERVER + 'exit'); +const { + createConfig, + configPath, +} = require(DIR_SERVER + 'config'); const config = createConfig({ configPath, }); -const maybeRoot = (a) => { - if (a === '.') - return process.cwd(); +const env = require(DIR_SERVER + 'env'); +const prefixer = require(DIR_SERVER + '/prefixer'); + +const choose = (a, b) => { + if (a === undefined) + return b; return a; }; -const yargsOptions = { - configuration: { - 'strip-aliased': true, - 'strip-dashed': true, - }, - coerce: { - root: maybeRoot, - }, +process.on('unhandledRejection', exit); + +const {argv} = process; +const args = require('minimist')(argv.slice(2), { string: [ 'name', 'port', @@ -57,8 +46,6 @@ const yargsOptions = { 'terminal-path', 'terminal-command', 'columns', - 'menu', - 'theme', 'import-url', 'import-token', 'export-token', @@ -73,7 +60,6 @@ const yargsOptions = { 'open', 'config-dialog', 'config-auth', - 'config-port', 'console', 'sync-console-path', 'contact', @@ -83,7 +69,6 @@ const yargsOptions = { 'confirm-copy', 'confirm-move', 'show-config', - 'show-dot-files', 'show-file-name', 'vim', 'keys-panel', @@ -92,70 +77,65 @@ const yargsOptions = { 'import', 'import-listen', 'log', - 'zip', 'dropbox', ], default: { - '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')), + '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('cloudcmd_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'), - 'import-url': env.parse('import_url') || config('importUrl'), + 'import-url': env('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')), - '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-path': env('terminal_path') || config('terminalPath'), + 'terminal-command': env('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.parse('import_token') || config('importToken'), - 'export-token': env.parse('export_token') || config('exportToken'), + 'import-token': env('import_token') || config('importToken'), + 'export-token': env('export_token') || config('exportToken'), 'dropbox': config('dropbox'), - 'dropbox-token': config('dropboxToken') || '', + 'dropbox-token': config('dropboxToken'), }, alias: { - version: 'v', - help: 'h', - password: 'p', - online: 'o', - username: 'u', - save: 's', - auth: 'a', - config: 'c', + v: 'version', + h: 'help', + p: 'password', + o: 'online', + u: 'username', + s: 'save', + a: 'auth', + c: 'config', }, -}; - -const {argv} = process; -const args = parse(argv.slice(2), yargsOptions); + unknown: (cmd) => { + exit('\'%s\' is not a cloudcmd option. See \'cloudcmd --help\'.', cmd); + }, +}); if (args.version) version(); @@ -165,21 +145,8 @@ else main(); async function main() { - const {validateArgs} = await import('@putout/cli-validate-args'); - - const error = await validateArgs(args, [ - ...yargsOptions.boolean, - ...yargsOptions.string, - ]); - - if (error) - return exit(error); - if (args.repl) - await repl(); - - validate.columns(args.columns); - validate.theme(args.theme); + repl(); port(args.port); @@ -189,41 +156,37 @@ async function main() { config('open', args.open); config('username', args.username); config('console', args.console); - config('syncConsolePath', args.syncConsolePath); - config('showDotFiles', args.showDotFiles); - config('showFileName', args.showFileName); + config('syncConsolePath', args['sync-console-path']); + config('showFileName', args['show-file-name']); config('contact', args.contact); config('terminal', args.terminal); - config('terminalPath', args.terminalPath); - config('terminalCommand', args.terminalCommand); - config('terminalAutoRestart', args.terminalAutoRestart); + config('terminalPath', args['terminal-path']); + config('terminalCommand', args['terminal-command']); + config('terminalAutoRestart', args['terminal-auto-restart']); config('editor', args.editor); - config('menu', args.menu); config('prefix', prefixer(args.prefix)); - config('prefixSocket', prefixer(args.prefixSocket)); + config('prefixSocket', prefixer(args['prefix-socket'])); config('root', args.root || '/'); config('vim', args.vim); - config('theme', args.theme); config('columns', args.columns); config('log', args.log); - config('confirmCopy', args.confirmCopy); - config('confirmMove', args.confirmMove); - config('oneFilePanel', args.oneFilePanel); - config('configDialog', args.configDialog); - config('configAuth', args.configAuth); - config('configPort', args.configPort); - config('keysPanel', args.keysPanel); + config('confirmCopy', args['confirm-copy']); + config('confirmMove', args['confirm-move']); + config('oneFilePanel', args['one-file-panel']); + config('configDialog', args['config-dialog']); + config('configAuth', args['config-auth']); + config('keysPanel', args['keys-panel']); config('export', args.export); - config('exportToken', args.exportToken); + config('exportToken', args['export-token']); config('import', args.import); - config('importToken', args.importToken); - config('importListen', args.importListen); - config('importUrl', args.importUrl); + config('importToken', args['import-token']); + config('importListen', args['import-listen']); + config('importUrl', args['import-url']); - config('dropbox', args.dropbox); - config('dropboxToken', args.dropboxToken); + config('dropbox', args['dropbox']); + config('dropboxToken', args['dropbox-token'] || ''); - await readConfig(args.config); + readConfig(args.config); const options = { root: config('root'), @@ -232,44 +195,42 @@ async function main() { prefix: config('prefix'), prefixSocket: config('prefixSocket'), columns: config('columns'), - theme: config('theme'), - menu: config('menu'), }; - const password = env.parse('password') || args.password; + const password = env('password') || args.password; if (password) - config('password', await getPassword(password)); + config('password', getPassword(password)); validateRoot(options.root, config); - if (args.showConfig) - await showConfig(); + if (args['show-config']) + showConfig(); - const {distributeImport} = await import('../server/distribute/import.js'); - const importConfig = promisify(distributeImport); + const distribute = require('../server/distribute'); + const importConfig = promisify(distribute.import); - await start(options, config); + await importConfig(config); if (args.save) config.write(); + start(options, config); await tryToCatch(checkUpdate); - await importConfig(config); } function validateRoot(root, config) { + const validate = require(DIR_SERVER + 'validate'); validate.root(root, config); if (root === '/') return; - if (config('log')) - console.log(`root: ${root}`); + console.log(`root: ${root}`); } -async function getPassword(password) { - const {default: criton} = await import('criton'); +function getPassword(password) { + const criton = require('criton'); return criton(password, config('algo')); } @@ -277,11 +238,13 @@ function version() { console.log('v' + Info.version); } -async function start(options, config) { +function start(options, config) { + const SERVER = DIR_SERVER + 'server'; + if (!args.server) return; - const {default: server} = await import('../server/server.js'); + const server = require(SERVER); server(options, config); } @@ -294,38 +257,38 @@ function port(arg) { exit('cloudcmd --port: should be a number'); } -async function showConfig() { - const {showConfig} = await import('../server/show-config.js'); - const data = showConfig(config('*')); +function showConfig() { + const show = require('../server/show-config'); + const data = show(config('*')); console.log(data); } -async function readConfig(name) { +function readConfig(name) { if (!name) return; - const {default: forEachKey} = await import('for-each-key'); + const fs = require('fs'); + const tryCatch = require('try-catch'); + const jju = require('jju'); + const forEachKey = require('for-each-key'); - const data = await import(name, { - with: { - type: 'json', - }, + const readjsonSync = (name) => jju.parse(fs.readFileSync(name, 'utf8'), { + mode: 'json', }); + const [error, data] = tryCatch(readjsonSync, name); + + if (error) + return exit(error.message); + forEachKey(config, data); } -async function help() { - 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'); - +function help() { + const bin = require('../json/help'); + const forEachKey = require('for-each-key'); + const currify = require('currify'); const usage = 'Usage: cloudcmd [options]'; const url = Info.homepage; const log = currify((a, b, c) => console.log(a, b, c)); @@ -336,27 +299,28 @@ async function help() { console.log('\nGeneral help using Cloud Commander: <%s>', url); } -async function repl() { +function repl() { console.log('REPL mode enabled (telnet localhost 1337)'); - await import('../server/repl.js'); + require(DIR_SERVER + 'repl'); } async function checkUpdate() { - const {default: load} = await import('package-json'); - const {version} = await load(Info.name, 'latest'); + const load = require('package-json'); - await showUpdateInfo(version); + const {version} = await load(Info.name, 'latest'); + showUpdateInfo(version); } -async function showUpdateInfo(version) { +function showUpdateInfo(version) { if (version === Info.version) return; - const {default: chalk} = await import('chalk'); + const chalk = require('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 deleted file mode 100644 index d09541b9..00000000 --- a/bin/cloudcmd.spec.js +++ /dev/null @@ -1,26 +0,0 @@ -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.js b/bin/release.js index 86e1d714..f3e6bf93 100755 --- a/bin/release.js +++ b/bin/release.js @@ -1,72 +1,67 @@ #!/usr/bin/env node -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 shortdate from 'shortdate'; -import Info from '../package.json' with { - type: 'json', -}; +'use strict'; -const place = promisify(_place); +const DIR = '../'; +const Info = require(DIR + 'package'); -await main(); +const minor = require('minor'); +const place = require('place'); +const rendy = require('rendy'); +const shortdate = require('shortdate'); -async function main() { - const history = '## Version history\n\n'; +const ERROR = Error('ERROR: version is missing. release --patch|--minor|--major'); + +main((error) => { + if (error) + console.error(error.message); +}); + +function main(callback) { + 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; - const [error, versionNew] = await tryToCatch(cl); - - if (error) - return console.error(error); - - await replaceVersion('README.md', version, versionNew); - await replaceVersion('HELP.md', version, versionNew); - - const historyNew = history + - rendy(template, { - date: shortdate(), - version: versionNew, + cl((error, versionNew) => { + if (error) + return callback(error); + + replaceVersion('README.md', version, versionNew, callback); + replaceVersion('HELP.md', version, versionNew, () => { + const historyNew = history + rendy(template, { + date : shortdate(), + version : versionNew, + }); + + replaceVersion('HELP.md', history, historyNew, callback); }); - - await replaceVersion('HELP.md', history, historyNew); + }); } -async function replaceVersion(name, version, versionNew) { - const [error] = await tryToCatch(place, name, version, versionNew); - - if (error) - return console.error(error); - - console.log(`done: ${name}`); +function replaceVersion(name, version, versionNew, callback) { + place(name, version, versionNew, (error) => { + if (error) + return callback(error); + + callback(null, 'done: ' + name); + }); } -async function cl() { +function cl(callback) { const {argv} = process; const length = argv.length - 1; const last = process.argv[length]; const regExp = /^--(major|minor|patch)$/; const [, match] = last.match(regExp) || []; - console.log(last); - if (!regExp.test(last)) - throw Error('ERROR: version is missing. release --patch|--minor|--major'); + return callback(ERROR); - return getVersionNew(last, match); + callback(null, getVersionNew(last, match)); } function getVersionNew(last, match) { @@ -75,3 +70,4 @@ function getVersionNew(last, match) { return last.substr(3); } + diff --git a/client/client.js b/client/client.js index a0e3cb07..c553b1c6 100644 --- a/client/client.js +++ b/client/client.js @@ -1,59 +1,74 @@ -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, -} 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'; +'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 pascalCase = require('just-pascal-case'); +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 currify = require('currify'); const noJS = (a) => a.replace(/.js$/, ''); -const isDev = process.env.NODE_ENV === 'development'; +const { + apiURL, + formatMsg, + buildFromJSON, +} = require('../common/cloudfunc'); + +const loadModule = require('./load-module'); inherits(CloudCmdProto, Emitify); -export const createCloudCmd = ({DOM, Listeners}) => { - return new CloudCmdProto({ - DOM, - Listeners, - }); -}; +module.exports = new CloudCmdProto(DOM); load.addErrorListener((e, src) => { const msg = `file ${src} could not be loaded`; Images.show.error(msg); }); -function CloudCmdProto({DOM, Listeners}) { +function CloudCmdProto(DOM) { + let Listeners; + + const log = (...a) => { + if (!isDev ) + return; + + console.log(...a); + }; + Emitify.call(this); const CloudCmd = this; const Info = DOM.CurrentInfo; - const {Storage, Files} = DOM; + const { + Storage, + Files, + } = DOM; - this.log = () => { - if (!isDev) - return; - }; + this.log = log; this.prefix = ''; this.prefixSocket = ''; this.prefixURL = ''; + this.DIRCLIENT = '/dist/'; + this.DIRCLIENT_MODULES = this.DIRCLIENT + 'modules/'; - this.MIN_ONE_PANEL_WIDTH = DOM.getCSSVar('min-one-panel-width'); + this.MIN_ONE_PANEL_WIDTH = 1155; this.HOST = location.origin || location.protocol + '//' + location.host; + + const TITLE = 'Cloud Commander'; + this.TITLE = TITLE; + this.sort = { left: 'name', right: 'name', @@ -64,16 +79,28 @@ function CloudCmdProto({DOM, Listeners}) { right: 'asc', }; - this.changeDir = async (path, overrides = {}) => { + /** + * Функция привязываеться ко всем ссылкам и + * загружает содержимое каталогов + * + * @param params - { + * paramLink - ссылка + * needRefresh - необходимость обязательной загрузки данных с сервера + * panel + * } + * @param callback + */ + this.loadDir = async (params) => { + const p = params; + const refresh = p.isRefresh; + const { - isRefresh, panel, - history = true, + history, noCurrent, currentName, - } = overrides; + } = p; - const refresh = isRefresh; let panelChanged; if (!noCurrent && panel && panel !== Info.panel) { @@ -89,12 +116,11 @@ function CloudCmdProto({DOM, Listeners}) { Images.show.load(imgPosition, panel); /* загружаем содержимое каталога */ - await ajaxLoad(addSlashToEnd(path), { + await ajaxLoad(p.path, { refresh, history, noCurrent, currentName, - showDotFiles: CloudCmd.config('showDotFiles'), }, panel); }; @@ -107,8 +133,6 @@ function CloudCmdProto({DOM, Listeners}) { 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)); @@ -117,6 +141,7 @@ function CloudCmdProto({DOM, Listeners}) { * should be called from config.js only * after successful update on server */ + if (key === 'password') return; @@ -127,14 +152,22 @@ function CloudCmdProto({DOM, Listeners}) { CloudCmd.MIN_ONE_PANEL_WIDTH = Infinity; if (!document.body.scrollIntoViewIfNeeded) - await load.js(`${CloudCmd.DIR_MODULES}/polyfill.js`); + await load.js(prefix + CloudCmd.DIRCLIENT_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('/'); @@ -144,13 +177,12 @@ function CloudCmdProto({DOM, Listeners}) { const [kebabModule] = query; const module = noJS(pascalCase(kebabModule.slice(1))); - const [, file] = query; + const file = query[1]; const current = DOM.getCurrentByName(file); if (file && !current) { const msg = formatMsg('set current file', file, 'error'); CloudCmd.log(msg); - return; } @@ -172,7 +204,7 @@ function CloudCmdProto({DOM, Listeners}) { }; const initModules = async () => { - CloudCmd.Key = Key; + CloudCmd.Key = new CloudCmd.Key(); CloudCmd.Key.bind(); const [, modules] = await tryToCatch(Files.get, 'modules'); @@ -222,7 +254,7 @@ function CloudCmdProto({DOM, Listeners}) { }); const dirPath = DOM.getCurrentDirPath(); - + Listeners = CloudCmd.Listeners; Listeners.init(); const panels = getPanels(); @@ -236,7 +268,7 @@ function CloudCmdProto({DOM, Listeners}) { const data = await Storage.get(dirPath); if (!data) - await Storage.setJson(dirPath, getJsonFromFileTable()); + await Storage.set(dirPath, getJsonFromFileTable()); } function getPanels() { @@ -259,15 +291,19 @@ function CloudCmdProto({DOM, Listeners}) { }; 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?.noCurrent; + const noCurrent = options ? options.noCurrent : false; - await CloudCmd.changeDir(path, { + await CloudCmd.loadDir({ + path, isRefresh, history, panel, @@ -283,18 +319,22 @@ function CloudCmdProto({DOM, Listeners}) { * @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 json = dirStorage && await Storage.get(path); const name = options.currentName || Info.name; - const {noCurrent, refresh} = options; + const { + noCurrent, + refresh, + } = options; if (!refresh && json) return await createFileTable(json, panel, options); @@ -311,8 +351,7 @@ function CloudCmdProto({DOM, Listeners}) { const [, newObj] = await RESTful.read(path + query, 'json'); if (!newObj) - // that's OK, error handled by RESTful - return; + return; // that's OK, error handled by RESTful options.sort = sort; options.order = order; @@ -325,35 +364,28 @@ function CloudCmdProto({DOM, Listeners}) { if (!CloudCmd.config('dirStorage')) return; - Storage.setJson(path, newObj); + Storage.set(path, newObj); } /** * Функция строит файловую таблицу - * @param data - данные о файлах + * @param json - данные о файлах * @param panelParam - * @param options - history, noCurrent, showDotFiles + * @param history + * @param callback */ async function createFileTable(data, panelParam, options) { const { history, noCurrent, - showDotFiles, } = options; - const names = [ - 'file', - 'path', - 'link', - 'pathLink', - ]; + const names = ['file', 'path', 'link', '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); @@ -361,7 +393,10 @@ function CloudCmdProto({DOM, Listeners}) { const panel = panelParam || DOM.getPanel(); const {prefix} = CloudCmd; - const {dir, name} = Info; + const { + dir, + name, + } = Info; const {childNodes} = panel; let i = childNodes.length; @@ -370,13 +405,12 @@ function CloudCmdProto({DOM, Listeners}) { 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, - showDotFiles, - template: { + template : { file, path, pathLink, @@ -416,7 +450,9 @@ function CloudCmdProto({DOM, Listeners}) { const path = parentDirPath; - await CloudCmd.changeDir(path); + await CloudCmd.loadDir({ + path, + }); const current = DOM.getCurrentByName(dir); const [first] = DOM.getFiles(panel); @@ -426,3 +462,4 @@ function CloudCmdProto({DOM, Listeners}) { }); }; } + diff --git a/client/cloudcmd.js b/client/cloudcmd.js index f2416fd4..a1bcdacb 100644 --- a/client/cloudcmd.js +++ b/client/cloudcmd.js @@ -1,44 +1,42 @@ -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'; +'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'); const isDev = process.env.NODE_ENV === 'development'; -export default init; +const { + registerSW, + listenSW, +} = require('./sw/register'); -globalThis.CloudCmd = init; +// prevent additional loading of emitify +window.Emitify = require('emitify'); -async function init(config) { - globalThis.CloudCmd = createCloudCmd({ - DOM, - Listeners, - }); - globalThis.DOM = DOM; - globalThis.Util = Util; - globalThis.CloudFunc = CloudFunc; +module.exports = window.CloudCmd = (config) => { + window.Util = require('../common/util'); + window.CloudFunc = require('../common/cloudfunc'); - await register(config); + const DOM = require('./dom'); + + window.DOM = DOM; + window.CloudCmd = require('./client'); + + register(config); + + require('./listeners'); + require('./key'); + require('./sort'); - initSortPanel(); - globalThis.CloudCmd.sortPanel = sortPanel; const prefix = getPrefix(config.prefix); - globalThis.CloudCmd.init(prefix, config); - - if (globalThis.CloudCmd.config('menu') === 'aleman') - setTimeout(() => { - import('https://esm.sh/@putout/processor-html'); - import('https://esm.sh/@putout/bundle@5.5'); - }, 100); -} + window.CloudCmd.init(prefix, config); +}; function getPrefix(prefix) { if (!prefix) @@ -54,7 +52,7 @@ const onUpdateFound = wraptile(async (config) => { if (isDev) return; - const {DOM} = globalThis; + const {DOM} = window; const prefix = getPrefix(config.prefix); await load.js(`${prefix}/dist/cloudcmd.common.js`); @@ -63,7 +61,7 @@ const onUpdateFound = wraptile(async (config) => { console.log('cloudcmd: sw: updated'); DOM.Events.removeAll(); - globalThis.CloudCmd(config); + window.CloudCmd(config); }); async function register(config) { @@ -72,3 +70,4 @@ async function register(config) { listenSW(sw, 'updatefound', onUpdateFound(config)); } + diff --git a/client/dom/buffer.js b/client/dom/buffer.js index c6d20ccd..3569ebeb 100644 --- a/client/dom/buffer.js +++ b/client/dom/buffer.js @@ -1,124 +1,137 @@ -/* global CloudCmd*/ -import * as Storage from '#dom/storage'; -import tryToPromiseAll from '../../common/try-to-promise-all.js'; +'use strict'; -const CLASS = 'cut-file'; -const COPY = 'copy'; -const CUT = 'cut'; +/* global CloudCmd */ -function showMessage(msg) { - globalThis.DOM.Dialog.alert(msg); -} +const tryToPromiseAll = require('../../common/try-to-promise-all'); +const Storage = require('./storage'); +const DOM = require('./'); -function getNames() { - const {DOM} = globalThis; - const files = DOM.getActiveFiles(); +module.exports = new BufferProto(); + +function BufferProto() { + const Info = DOM.CurrentInfo; + const CLASS = 'cut-file'; + const COPY = 'copy'; + const CUT = 'cut'; + const Buffer = { + cut : callIfEnabled.bind(null, cut), + copy : callIfEnabled.bind(null, copy), + clear : callIfEnabled.bind(null, clear), + paste : callIfEnabled.bind(null, paste), + }; - return DOM.getFilenames(files); -} - -function addCutClass() { - const {DOM} = globalThis; - const files = DOM.getActiveFiles(); - - for (const element of files) { - element.classList.add(CLASS); + function showMessage(msg) { + DOM.Dialog.alert(msg); } -} - -function rmCutClass() { - const {DOM} = globalThis; - const files = DOM.getByClassAll(CLASS); - for (const element of files) { - element.classList.remove(CLASS); + function getNames() { + const files = DOM.getActiveFiles(); + const names = DOM.getFilenames(files); + + return names; } + + function addCutClass() { + const files = DOM.getActiveFiles(); + + for (const element of files) { + element.classList.add(CLASS); + } + } + + function rmCutClass() { + const files = DOM.getByClassAll(CLASS); + + for (const element of files) { + element.classList.remove(CLASS); + } + } + + function callIfEnabled(callback) { + const is = CloudCmd.config('buffer'); + + if (is) + return callback(); + + showMessage('Buffer disabled in config!'); + } + + async function readBuffer() { + const [e, cp, ct] = await tryToPromiseAll([ + Storage.get(COPY), + Storage.get(CUT), + ]); + + return [ + e, + cp, + ct, + ]; + } + + async function copy() { + const names = getNames(); + const from = Info.dirPath; + + await clear(); + + if (!names.length) + return; + + await Storage.remove(CUT); + await Storage.set(COPY, { + from, + names, + }); + } + + async function cut() { + const names = getNames(); + const from = Info.dirPath; + + await clear(); + + if (!names.length) + return; + + addCutClass(); + + await Storage.set(CUT, { + from, + names, + }); + } + + async function clear() { + await Storage.remove(COPY); + await Storage.remove(CUT); + + rmCutClass(); + } + + async function paste() { + const [error, cp, ct] = await readBuffer(); + + if (error || !cp && !ct) + return showMessage(error || 'Buffer is empty!'); + + const opStr = cp ? 'copy' : 'move'; + const data = cp || ct; + const {Operation} = CloudCmd; + const msg = 'Path is same!'; + const to = Info.dirPath; + + if (data.from === to) + return showMessage(msg); + + Operation.show(opStr, { + ...data, + to, + }); + + await clear(); + } + + return Buffer; } -const checkEnabled = (fn) => () => { - const is = CloudCmd.config('buffer'); - - if (is) - return fn(); - - showMessage('Buffer disabled in config!'); -}; - -async function readBuffer() { - const [e, cp, ct] = await tryToPromiseAll([ - Storage.getJson(COPY), - Storage.getJson(CUT), - ]); - - return [ - e, - cp, - ct, - ]; -} - -export const copy = checkEnabled(async () => { - const Info = globalThis.DOM.CurrentInfo; - const names = getNames(); - const from = Info.dirPath; - - await clear(); - - if (!names.length) - return; - - await Storage.remove(CUT); - await Storage.setJson(COPY, { - from, - names, - }); -}); - -export const cut = checkEnabled(async () => { - const Info = globalThis.DOM.CurrentInfo; - const names = getNames(); - const from = Info.dirPath; - - await clear(); - - if (!names.length) - return; - - addCutClass(); - - await Storage.setJson(CUT, { - from, - names, - }); -}); - -export const clear = checkEnabled(async () => { - await Storage.remove(COPY); - await Storage.remove(CUT); - - rmCutClass(); -}); - -export const paste = checkEnabled(async () => { - const Info = globalThis.DOM.CurrentInfo; - const [error, cp, ct] = await readBuffer(); - - if (error || !cp && !ct) - return showMessage(error || 'Buffer is empty!'); - - const opStr = cp ? 'copy' : 'move'; - const data = cp || ct; - const {Operation} = CloudCmd; - const msg = 'Path is same!'; - const to = Info.dirPath; - - if (data.from === to) - return showMessage(msg); - - Operation.show(opStr, { - ...data, - to, - }); - - await clear(); -}); diff --git a/client/dom/cmd.js b/client/dom/cmd.js deleted file mode 100644 index d228b315..00000000 --- a/client/dom/cmd.js +++ /dev/null @@ -1,83 +0,0 @@ -/* 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 b8113054..012d68dc 100644 --- a/client/dom/current-file.js +++ b/client/dom/current-file.js @@ -1,16 +1,28 @@ +'use strict'; + /* global DOM */ /* global CloudCmd */ -import createElement from '@cloudcmd/create-element'; -import {getTitle, FS} from '#common/cloudfunc'; -import {encode, decode} from '#common/entity'; + +const btoa = require('../../common/btoa'); +const createElement = require('@cloudcmd/create-element'); + +const { + encode, + decode, +} = require('../../common/entity'); + +const { + getTitle, + FS, +} = require('../../common/cloudfunc'); let Title; const CURRENT_FILE = 'current-file'; -const encodeNBSP = (a) => a?.replace('\xa0', ' '); -const decodeNBSP = (a) => a?.replace(' ', '\xa0'); +const NBSP_REG = RegExp(String.fromCharCode(160), 'g'); +const SPACE = ' '; -export const _CURRENT_FILE = CURRENT_FILE; +module.exports._CURRENT_FILE = CURRENT_FILE; /** * set name from current (or param) file @@ -18,7 +30,7 @@ export const _CURRENT_FILE = CURRENT_FILE; * @param name * @param current */ -export const setCurrentName = (name, current) => { +module.exports.setCurrentName = (name, current) => { const Info = DOM.CurrentInfo; const {link} = Info; const {prefix} = CloudCmd; @@ -29,7 +41,7 @@ export const setCurrentName = (name, current) => { link.href = dir + encoded; link.innerHTML = encoded; - current.setAttribute('data-name', createNameAttribute(name)); + current.setAttribute('data-name', 'js-file-' + btoa(encodeURI(name))); CloudCmd.emit('current-file', current); return link; @@ -40,48 +52,28 @@ export const setCurrentName = (name, current) => { * * @param currentFile */ -export const getCurrentName = (currentFile) => { +module.exports.getCurrentName = (currentFile) => { const current = currentFile || DOM.getCurrentFile(); if (!current) return ''; - return parseNameAttribute(current.getAttribute('data-name')); + const link = DOM.getCurrentLink(current); + + if (!link) + return ''; + + return decode(link.title) + .replace(NBSP_REG, SPACE); }; /** - * Generate a `data-name` attribute for the given filename - * @param name The string name to encode + * get current direcotory path */ -const createNameAttribute = (name) => { - const encoded = btoa(encodeURI(name)); - return `js-file-${encoded}`; -}; - -/** - * Parse a `data-name` attribute string back into the original filename - * @param attribute The string we wish to decode - */ -const parseNameAttribute = (attribute) => { - attribute = attribute.replace('js-file-', ''); - 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 directory path - */ -export const getCurrentDirPath = (panel = DOM.getPanel()) => { +module.exports.getCurrentDirPath = (panel = DOM.getPanel()) => { const path = DOM.getByDataName('js-path', panel); - return path.textContent; + return path.textContent + .replace(NBSP_REG, SPACE); }; /** @@ -89,31 +81,36 @@ export const getCurrentDirPath = (panel = DOM.getPanel()) => { * * @param currentFile - current file by default */ -export const getCurrentPath = (currentFile) => { +module.exports.getCurrentPath = (currentFile) => { const current = currentFile || DOM.getCurrentFile(); const [element] = DOM.getByTag('a', current); const {prefix} = CloudCmd; - return parseHrefAttribute(prefix, element.getAttribute('href')); + const path = element + .getAttribute('href') + .replace(RegExp('^' + prefix + FS), '') + .replace(NBSP_REG, SPACE); + + return decode(path); }; /** - * get current directory name + * get current direcotory name */ -export const getCurrentDirName = () => { - const href = DOM - .getCurrentDirPath() +module.exports.getCurrentDirName = () => { + const href = DOM.getCurrentDirPath() .replace(/\/$/, ''); const substr = href.substr(href, href.lastIndexOf('/')); + const ret = href.replace(substr + '/', '') || '/'; - return href.replace(`${substr}/`, '') || '/'; + return ret; }; /** - * get current directory path + * get current direcotory path */ -export const getParentDirPath = (panel) => { +module.exports.getParentDirPath = (panel) => { const path = DOM.getCurrentDirPath(panel); const dirName = DOM.getCurrentDirName() + '/'; const index = path.lastIndexOf(dirName); @@ -125,9 +122,9 @@ export const getParentDirPath = (panel) => { }; /** - * get not current directory path + * get not current direcotory path */ -export const getNotCurrentDirPath = () => { +module.exports.getNotCurrentDirPath = () => { const panel = DOM.getPanel({ active: false, }); @@ -140,20 +137,20 @@ export const getNotCurrentDirPath = () => { * * @currentFile */ -export const getCurrentFile = () => { +module.exports.getCurrentFile = () => { return DOM.getByClass(CURRENT_FILE); }; /** * get current file by name */ -export const getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => { - const dataName = 'js-file-' + btoa(encodeURI(encodeNBSP(name))); +module.exports.getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => { + const dataName = 'js-file-' + btoa(encodeURI(name)); return DOM.getByDataName(dataName, panel); }; /** - * private function that unset currentFile + * private function thet unset currentfile * * @currentFile */ @@ -169,7 +166,7 @@ function unsetCurrentFile(currentFile) { /** * unified way to set current file */ -export const setCurrentFile = (currentFile, options) => { +module.exports.setCurrentFile = (currentFile, options) => { const o = options; const currentFileWas = DOM.getCurrentFile(); @@ -198,7 +195,7 @@ export const setCurrentFile = (currentFile, options) => { * but it should be false * to prevent default behavior */ - if (!o || o.history) { + if (!o || o.history !== false) { const historyPath = path === '/' ? path : FS + path; DOM.setHistory(historyPath, null, historyPath); } @@ -206,7 +203,6 @@ export const setCurrentFile = (currentFile, options) => { /* scrolling to current file */ const CENTER = true; - DOM.scrollIntoViewIfNeeded(currentFile, CENTER); CloudCmd.emit('current-file', currentFile); @@ -216,7 +212,7 @@ export const setCurrentFile = (currentFile, options) => { return DOM; }; -export const setCurrentByName = (name) => { +this.setCurrentByName = (name) => { const current = DOM.getCurrentByName(name); return DOM.setCurrentFile(current); }; @@ -227,7 +223,7 @@ export const setCurrentByName = (name) => { * @param layer - element * @param - position {x, y} */ -export const getCurrentByPosition = ({x, y}) => { +module.exports.getCurrentByPosition = ({x, y}) => { const element = document.elementFromPoint(x, y); const getEl = (el) => { @@ -259,7 +255,7 @@ export const getCurrentByPosition = ({x, y}) => { * * @param currentFile */ -export const isCurrentFile = (currentFile) => { +module.exports.isCurrentFile = (currentFile) => { if (!currentFile) return false; @@ -271,7 +267,8 @@ export const isCurrentFile = (currentFile) => { * * @param name */ -export const setTitle = (name) => { + +module.exports.setTitle = (name) => { if (!Title) Title = DOM.getByTag('title')[0] || createElement('title', { innerHTML: name, @@ -288,24 +285,20 @@ export const setTitle = (name) => { * * @param currentFile */ -export const isCurrentIsDir = (currentFile) => { +module.exports.isCurrentIsDir = (currentFile) => { const current = currentFile || DOM.getCurrentFile(); - const path = DOM.getCurrentPath(current); const fileType = DOM.getCurrentType(current); - const isZip = path.endsWith('.zip'); - const isDir = /^directory(-link)?/.test(fileType); - - return isDir || isZip; + return /^directory(-link)?/.test(fileType); }; -export const getCurrentType = (currentFile) => { +module.exports.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 85b18a29..22441b36 100644 --- a/client/dom/current-file.spec.js +++ b/client/dom/current-file.spec.js @@ -1,72 +1,69 @@ -import {test, stub} from 'supertape'; -import {create} from 'auto-globals'; -import wraptile from 'wraptile'; -import * as currentFile from './current-file.js'; +'use strict'; +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 {_CURRENT_FILE} = currentFile; test('current-file: setCurrentName: setAttribute', (t) => { - const {DOM, CloudCmd} = globalThis; + const { + DOM, + CloudCmd, + } = global; - globalThis.DOM = getDOM(); - globalThis.CloudCmd = getCloudCmd(); + global.DOM = getDOM(); + global.CloudCmd = getCloudCmd(); const current = create(); const {setAttribute} = current; currentFile.setCurrentName('hello', current); - globalThis.DOM = DOM; - globalThis.CloudCmd = CloudCmd; + t.ok(setAttribute.calledWith('data-name', 'js-file-aGVsbG8='), 'should call setAttribute'); + + global.DOM = DOM; + global.CloudCmd = CloudCmd; - t.calledWith(setAttribute, [ - 'data-name', - 'js-file-aGVsbG8=', - ], 'should call setAttribute'); t.end(); }); test('current-file: setCurrentName: setAttribute: cyrillic', (t) => { - const {DOM, CloudCmd} = globalThis; + const { + DOM, + CloudCmd, + } = global; - globalThis.DOM = getDOM(); - globalThis.CloudCmd = getCloudCmd(); + global.DOM = getDOM(); + global.CloudCmd = getCloudCmd(); const current = create(); const {setAttribute} = current; currentFile.setCurrentName('ай', current); - globalThis.DOM = DOM; - globalThis.CloudCmd = CloudCmd; + t.ok(setAttribute.calledWith('data-name', 'js-file-JUQwJUIwJUQwJUI5'), 'should call setAttribute'); - t.calledWith(setAttribute, [ - 'data-name', - 'js-file-JUQwJUIwJUQwJUI5', - ], 'should call setAttribute'); - t.end(); -}); - -test('current-file: getCurrentName', (t) => { - const current = create(); - current.getAttribute.returns('js-file-Ymlu'); + global.DOM = DOM; + global.CloudCmd = CloudCmd; - const result = currentFile.getCurrentName(current); - - t.equal(result, 'bin'); t.end(); }); test('current-file: emit', (t) => { - const {DOM, CloudCmd} = globalThis; + const { + DOM, + CloudCmd, + } = global; const emit = stub(); - globalThis.DOM = getDOM(); - globalThis.CloudCmd = getCloudCmd({ + global.DOM = getDOM(); + global.CloudCmd = getCloudCmd({ emit, }); @@ -74,41 +71,47 @@ test('current-file: emit', (t) => { currentFile.setCurrentName('hello', current); - globalThis.DOM = DOM; - globalThis.CloudCmd = CloudCmd; + t.ok(emit.calledWith('current-file', current), 'should call emit'); + + global.DOM = DOM; + global.CloudCmd = CloudCmd; - t.calledWith(emit, ['current-file', current], 'should call emit'); t.end(); }); test('current-file: setCurrentName: return', (t) => { - const {DOM, CloudCmd} = globalThis; + const { + DOM, + CloudCmd, + } = global; const link = {}; - globalThis.DOM = getDOM({ + global.DOM = getDOM({ link, }); - globalThis.CloudCmd = getCloudCmd(); + global.CloudCmd = getCloudCmd(); const current = create(); + const result = currentFile.setCurrentName('hello', current); - globalThis.DOM = DOM; - globalThis.CloudCmd = CloudCmd; - t.equal(result, link, 'should return link'); + + global.DOM = DOM; + global.CloudCmd = CloudCmd; + t.end(); }); test('current-file: getParentDirPath: result', (t) => { - const {DOM} = globalThis; + const {DOM} = global; const getCurrentDirPath = returns('/D/Films/+++favorite films/'); const getCurrentDirName = returns('+++favorite films'); - globalThis.DOM = getDOM({ + global.DOM = getDOM({ getCurrentDirPath, getCurrentDirName, }); @@ -116,55 +119,65 @@ test('current-file: getParentDirPath: result', (t) => { const result = currentFile.getParentDirPath(); const expected = '/D/Films/'; - globalThis.DOM = DOM; + global.DOM = DOM; t.equal(result, expected, 'should return parent dir path'); t.end(); }); test('current-file: isCurrentFile: no', (t) => { - const {DOM, CloudCmd} = globalThis; + const { + DOM, + CloudCmd, + } = global; - globalThis.DOM = getDOM(); - globalThis.CloudCmd = getCloudCmd(); + global.DOM = getDOM(); + global.CloudCmd = getCloudCmd(); const result = currentFile.isCurrentFile(); + const expect = false; - globalThis.DOM = DOM; - globalThis.CloudCmd = CloudCmd; + global.DOM = DOM; + global.CloudCmd = CloudCmd; - t.notOk(result); + t.equal(result, expect, 'should equal'); t.end(); }); test('current-file: isCurrentFile', (t) => { - const {DOM, CloudCmd} = globalThis; + const { + DOM, + CloudCmd, + } = global; const isContainClass = stub(); - globalThis.DOM = getDOM({ + global.DOM = getDOM({ isContainClass, }); - globalThis.CloudCmd = getCloudCmd(); + global.CloudCmd = getCloudCmd(); const current = {}; currentFile.isCurrentFile(current); - globalThis.DOM = DOM; - globalThis.CloudCmd = CloudCmd; + global.DOM = DOM; + global.CloudCmd = CloudCmd; - t.calledWith(isContainClass, [current, _CURRENT_FILE], 'should call isContainClass'); + t.ok(isContainClass.calledWith(current, _CURRENT_FILE), 'should call isContainClass'); t.end(); }); test('current-file: getCurrentType', (t) => { - const {DOM, CloudCmd} = globalThis; + const { + DOM, + CloudCmd, + } = global; - globalThis.DOM = getDOM(); - globalThis.CloudCmd = getCloudCmd(); + global.DOM = getDOM(); + global.CloudCmd = getCloudCmd(); - const {getByDataName} = globalThis.DOM; + const {getByDataName} = global.DOM; getByDataName.returns({ className: 'mini-icon directory', @@ -174,129 +187,122 @@ test('current-file: getCurrentType', (t) => { currentFile.getCurrentType(current); - globalThis.DOM = DOM; - globalThis.CloudCmd = CloudCmd; + global.DOM = DOM; + global.CloudCmd = CloudCmd; - t.calledWith(getByDataName, ['js-type', current]); + t.ok(getByDataName.calledWith('js-type', current)); t.end(); }); test('current-file: isCurrentIsDir: getCurrentType', (t) => { - const {DOM, CloudCmd} = globalThis; + const { + DOM, + CloudCmd, + } = global; - globalThis.DOM = getDOM(); - globalThis.CloudCmd = getCloudCmd(); + global.DOM = getDOM(); + global.CloudCmd = getCloudCmd(); - const {getCurrentType} = globalThis.DOM; + const {getCurrentType} = global.DOM; const current = create(); currentFile.isCurrentIsDir(current); - globalThis.DOM = DOM; - globalThis.CloudCmd = CloudCmd; + global.DOM = DOM; + global.CloudCmd = CloudCmd; - t.calledWith(getCurrentType, [current]); + t.ok(getCurrentType.calledWith(current)); t.end(); }); test('current-file: isCurrentIsDir: directory', (t) => { - const {DOM, CloudCmd} = globalThis; + const { + DOM, + CloudCmd, + } = global; - globalThis.DOM = getDOM({ + global.DOM = getDOM({ getCurrentType: stub().returns('directory'), }); - globalThis.CloudCmd = getCloudCmd(); + global.CloudCmd = getCloudCmd(); const current = create(); const result = currentFile.isCurrentIsDir(current); - globalThis.DOM = DOM; - globalThis.CloudCmd = CloudCmd; + global.DOM = DOM; + global.CloudCmd = CloudCmd; t.ok(result); t.end(); }); test('current-file: isCurrentIsDir: directory-link', (t) => { - const {DOM, CloudCmd} = globalThis; + const { + DOM, + CloudCmd, + } = global; - globalThis.DOM = getDOM({ + global.DOM = getDOM({ getCurrentType: stub().returns('directory-link'), }); - globalThis.CloudCmd = getCloudCmd(); + global.CloudCmd = getCloudCmd(); const current = create(); const result = currentFile.isCurrentIsDir(current); - globalThis.DOM = DOM; - globalThis.CloudCmd = CloudCmd; + global.DOM = DOM; + global.CloudCmd = CloudCmd; t.ok(result); t.end(); }); test('current-file: isCurrentIsDir: file', (t) => { - const {DOM, CloudCmd} = globalThis; + const { + DOM, + CloudCmd, + } = global; - globalThis.DOM = getDOM({ + global.DOM = getDOM({ getCurrentType: stub().returns('file'), }); - globalThis.CloudCmd = getCloudCmd(); + global.CloudCmd = getCloudCmd(); const current = create(); const result = currentFile.isCurrentIsDir(current); - globalThis.DOM = DOM; - globalThis.CloudCmd = CloudCmd; + global.DOM = DOM; + global.CloudCmd = CloudCmd; t.notOk(result); t.end(); }); -const getCloudCmd = ({emit} = {}) => ({ - prefix: '', - emit: emit || stub(), -}); +function getCloudCmd({emit} = {}) { + return { + 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(); -}); - -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; - +function getDOM({ + link = {}, + getCurrentDirPath = stub(), + getCurrentDirName = stub(), + getByDataName = stub(), + isContainClass = stub(), + getCurrentType = stub(), +} = {}) { return { getCurrentDirPath, getCurrentDirName, - getCurrentPath, getByDataName, isContainClass, getCurrentType, @@ -306,3 +312,4 @@ function getDOM(overrides = {}) { }, }; } + diff --git a/client/dom/dialog.js b/client/dom/dialog.js index b1791d92..3fcf7fb6 100644 --- a/client/dom/dialog.js +++ b/client/dom/dialog.js @@ -1,18 +1,27 @@ -import {tryToCatch} from 'try-to-catch'; -import * as smalltalk from 'smalltalk'; +'use strict'; + +const tryToCatch = require('try-to-catch'); + +const { + alert, + prompt, + confirm, + progress, +} = require('smalltalk'); const title = 'Cloud Commander'; -export const alert = (...a) => smalltalk.alert(title, ...a, { +module.exports.alert = (...a) => alert(title, ...a, { cancel: false, }); -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.prompt = (...a) => tryToCatch(prompt, title, ...a); +module.exports.confirm = (...a) => tryToCatch(confirm, title, ...a); +module.exports.progress = (...a) => progress(title, ...a); -alert.noFiles = () => { - return smalltalk.alert(title, 'No files selected!', { +module.exports.alert.noFiles = () => { + return alert(title, 'No files selected!', { cancel: false, }); }; + diff --git a/client/dom/directory.js b/client/dom/directory.js index efbca119..a04f889b 100644 --- a/client/dom/directory.js +++ b/client/dom/directory.js @@ -1,19 +1,27 @@ -/* global DOM, CloudCmd */ -import philip from 'philip'; -import * as Dialog from '#dom/dialog'; -import {FS} from '#common/cloudfunc'; -import * as Images from '#dom/images'; +'use strict'; -export const uploadDirectory = (items) => { +/* 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) => { if (items.length) Images.show('top'); - const entries = Array - .from(items) - .map((item) => item.webkitGetAsEntry()); + const entries = Array.from(items).map((item) => { + return item.webkitGetAsEntry(); + }); - const dirPath = DOM.getCurrentDirPath(); - const path = dirPath.replace(/\/$/, ''); + const dirPath = getPathWhenRootEmpty(); + const path = dirPath + .replace(/\/$/, ''); const progress = Dialog.progress('Uploading...'); @@ -57,8 +65,15 @@ export const uploadDirectory = (items) => { uploader.on('end', CloudCmd.refresh); }; -const percent = (i, n, per = 100) => Math.round(i * per / n); +function percent(i, n, per = 100) { + return Math.round(i * per / n); +} -const uploadFile = (url, data) => DOM.load.put(url, data); +function uploadFile(url, data) { + return DOM.load.put(url, data); +} + +function uploadDir(url) { + return DOM.load.put(url + '?dir'); +} -const uploadDir = (url) => DOM.load.put(`${url}?dir`); diff --git a/client/dom/dom-tree.js b/client/dom/dom-tree.js index fad5ad40..3bef0e4f 100644 --- a/client/dom/dom-tree.js +++ b/client/dom/dom-tree.js @@ -1,15 +1,8 @@ -import currify from 'currify'; +'use strict'; -const DOM = { - show, - hide, - getByClass, - getByClassAll, - getByDataName, - getById, - getByTag, - isContainClass, -}; +const currify = require('currify'); + +const DOM = module.exports; /** * check class of element @@ -17,7 +10,7 @@ const DOM = { * @param element * @param className */ -export function isContainClass(element, className) { +const isContainClass = (element, className) => { if (!element) throw Error('element could not be empty!'); @@ -25,68 +18,67 @@ export function 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 */ -export function getByTag(tag, element = document) { +module.exports.getByTag = (tag, element = document) => { return element.getElementsByTagName(tag); -} +}; /** * Function search element by id - * @param id - * @param element + * @param Id - id */ -export function getById(id, element = document) { - return element.querySelector(`#${id}`); -} +module.exports.getById = (id, element = document) => { + return element.querySelector('#' + id); +}; /** * Function search first element by class name * @param className - className * @param element - element */ -export function getByClass(className, element = document) { +module.exports.getByClass = (className, element = document) => { return DOM.getByClassAll(className, element)[0]; -} +}; -export function getByDataName(attribute, element = document) { +module.exports.getByDataName = (attribute, element = document) => { const selector = '[' + 'data-name="' + attribute + '"]'; return element.querySelector(selector); -} +}; /** * Function search element by class name - * @param className - * @param element + * @param pClass - className + * @param element - element */ -export function getByClassAll(className, element) { +module.exports.getByClassAll = (className, element) => { return (element || document).getElementsByClassName(className); -} +}; /** * add class=hidden to element * * @param element */ -export function hide(element) { +module.exports.hide = (element) => { element.classList.add('hidden'); return DOM; -} +}; -export function show(element) { +module.exports.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 53b376c7..5090b28c 100644 --- a/client/dom/dom-tree.spec.js +++ b/client/dom/dom-tree.spec.js @@ -1,27 +1,19 @@ -import {test, stub} from 'supertape'; -import {create} from 'auto-globals'; -import {tryCatch} from 'try-catch'; -import { - isContainClass, - getByTag, - getById, - getByClass, - getByDataName, - getByClassAll, - hide, - show, -} from './dom-tree.js'; +'use strict'; + +const test = require('supertape'); +const {create} = require('auto-globals'); +const tryCatch = require('try-catch'); + +const {isContainClass} = require('./dom-tree'); test('dom: isContainClass: no element', (t) => { const [e] = tryCatch(isContainClass); - t.equal(e.message, 'element could not be empty!', 'should throw when no element'); t.end(); }); test('dom: isContainClass: no className', (t) => { const [e] = tryCatch(isContainClass, {}); - t.equal(e.message, 'className could not be empty!', 'should throw when no element'); t.end(); }); @@ -33,7 +25,7 @@ test('dom: isContainClass: contains', (t) => { const className = 'hello'; isContainClass(el, className); - t.calledWith(contains, [className], 'should call contains'); + t.ok(contains.calledWith(className), 'should call contains'); t.end(); }); @@ -42,111 +34,13 @@ 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.ok(contains.calledWith(className), 'should call contains'); t.end(); }); -test('dom: getByTag', (t) => { - const getElementsByTagName = stub(); - const element = { - getElementsByTagName, - }; - - getByTag('div', element); - - t.calledWith(getElementsByTagName, ['div'], 'should call getElementsByTagName'); - t.end(); -}); - -test('dom: getById', (t) => { - const querySelector = stub(); - const element = { - querySelector, - }; - - getById('my-id', element); - - t.calledWith(querySelector, ['#my-id'], 'should call querySelector with id selector'); - t.end(); -}); - -test('dom: getByClassAll', (t) => { - const getElementsByClassName = stub(); - const element = { - getElementsByClassName, - }; - - getByClassAll('my-class', element); - - t.calledWith(getElementsByClassName, ['my-class'], 'should call getElementsByClassName'); - t.end(); -}); - -test('dom: getByClass: calls getByClassAll', (t) => { - const element = { - getElementsByClassName: stub().returns(['first']), - }; - - const result = getByClass('my-class', element); - - t.equal(result, 'first', 'should return first element from class list'); - t.end(); -}); - -test('dom: getByDataName', (t) => { - const querySelector = stub(); - const element = { - querySelector, - }; - - getByDataName('hello', element); - - t.calledWith(querySelector, ['[data-name="hello"]'], 'should call querySelector with data-name selector'); - t.end(); -}); - -test('dom: hide', (t) => { - const add = stub(); - const element = { - classList: { - add, - }, - }; - - hide(element); - - t.calledWith(add, ['hidden'], 'should add hidden class'); - t.end(); -}); - -test('dom: show', (t) => { - const remove = stub(); - const element = { - classList: { - remove, - }, - }; - - show(element); - - t.calledWith(remove, ['hidden'], 'should remove hidden class'); - t.end(); -}); - -test('dom: getByClassAll: without element uses document', (t) => { - const getElementsByClassName = stub(); - const prevDocument = globalThis.document; - - globalThis.document = { - getElementsByClassName, - }; - - getByClassAll('my-class'); - - globalThis.document = prevDocument; - - t.calledWith(getElementsByClassName, ['my-class'], 'should fallback to document when no element'); - t.end(); -}); diff --git a/client/dom/events/event-store.js b/client/dom/events/event-store.js index 1cbfe7a4..6d9273c0 100644 --- a/client/dom/events/event-store.js +++ b/client/dom/events/event-store.js @@ -1,6 +1,8 @@ +'use strict'; + let list = []; -export const add = (el, name, fn) => { +module.exports.add = (el, name, fn) => { list.push([ el, name, @@ -8,8 +10,11 @@ export const add = (el, name, fn) => { ]); }; -export const clear = () => { +module.exports.clear = () => { list = []; }; -export const get = () => list; +module.exports.get = () => { + return list; +}; + diff --git a/client/dom/events/event-store.spec.js b/client/dom/events/event-store.spec.js index 6a623939..0ff1b5bf 100644 --- a/client/dom/events/event-store.spec.js +++ b/client/dom/events/event-store.spec.js @@ -1,5 +1,7 @@ -import {test} from 'supertape'; -import * as eventStore from './event-store.js'; +'use strict'; + +const test = require('supertape'); +const eventStore = require('./event-store'); test('event-store: get', (t) => { const el = {}; @@ -8,16 +10,11 @@ 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); + t.deepEqual(expected, result, 'should equal'); t.end(); }); @@ -32,6 +29,6 @@ test('event-store: clear', (t) => { const result = eventStore.get(); const expected = []; - t.deepEqual(result, expected); + t.deepEqual(expected, result, 'should equal'); t.end(); }); diff --git a/client/dom/events/index.js b/client/dom/events/index.js index c49d2333..195987da 100644 --- a/client/dom/events/index.js +++ b/client/dom/events/index.js @@ -1,203 +1,250 @@ -import itype from 'itype'; -import * as EventStore from './event-store.js'; +'use strict'; -/** - * safe add event listener - * - * @param type - * @param element - document by default - * @param listener - */ -export const add = (type, element, listener) => { - checkType(type); +const itype = require('itype'); +const EventStore = require('./event-store'); + +module.exports = new EventsProto(); + +function EventsProto() { + const Events = this; - parseArgs(type, element, listener, (element, args) => { - const [name, fn, options] = args; + const getEventOptions = (eventName) => { + if (eventName !== 'touchstart') + return false; - 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); + return { + passive: true, + }; }; - if (!listener) { - listener = element; - element = null; + 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; + } } - add(type, element, once); - - return Events; -}; - -/** - * safe remove event listener - * - * @param type - * @param listener - * @param element - document by default - */ -export const remove = (type, element, listener) => { - checkType(type); - - parseArgs(type, element, listener, (element, args) => { - element.removeEventListener(...args); - }); - - return Events; -}; - -/** - * remove all added event listeners - */ -export const removeAll = () => { - const events = EventStore.get(); - - for (const [el, name, fn] of events) - el.removeEventListener(name, fn); - - EventStore.clear(); -}; - -/** - * safe add event keydown listener - * - * @param args - */ -export const addKey = function(...args) { - return add('keydown', ...args); -}; - -/** - * safe remove event click listener - * - * @param args - */ -export const rmKey = function(...args) { - return Events.remove('keydown', ...args); -}; - -/** - * safe add event click listener - */ -export const addClick = function(...args) { - return Events.add('click', ...args); -}; - -/** - * safe remove event click listener - */ -export const rmClick = function(...args) { - return remove('click', ...args); -}; - -export const addContextMenu = function(...args) { - return add('contextmenu', ...args); -}; - -/** - * safe add load listener - */ -export const addLoad = function(...args) { - return add('load', ...args); -}; - -function checkType(type) { - if (!type) - throw Error('type could not be empty!'); -} - -const getEventOptions = (eventName) => { - if (eventName !== 'touchstart') - return false; - - return { - passive: true, + /** + * 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; }; -}; - -function parseArgs(eventName, element, listener, callback) { - let isFunc; - const args = [ - eventName, - element, - listener, - callback, - ]; - const EVENT_NAME = 1; - const ELEMENT = 0; - const type = itype(eventName); - - switch(type) { - default: - if (!type.endsWith('element')) - throw Error(`unknown eventName: ${type}`); + /** + * 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); + }; - parseArgs(args[EVENT_NAME], args[ELEMENT], listener, callback); - break; - - case 'string': - isFunc = itype.function(element); - - if (isFunc) { + if (!listener) { listener = element; element = null; } - if (!element) - element = window; + this.add(type, element, once); - callback(element, [ - eventName, - listener, - getEventOptions(eventName), - ]); - break; + return Events; + }; - case 'array': + /** + * safe remove event listener + * + * @param type + * @param listener + * @param element {document by default} + */ + this.remove = (type, element, listener) => { + checkType(type); - for (const name of eventName) { - parseArgs(name, element, listener, callback); - } + parseArgs(type, element, listener, (element, args) => { + element.removeEventListener(...args); + }); - break; + return Events; + }; - case 'object': + /** + * remove all added event listeners + * + * @param listener + */ + this.removeAll = () => { + const events = EventStore.get(); - for (const name of Object.keys(eventName)) { - const eventListener = eventName[name]; - - parseArgs(name, element, eventListener, callback); - } + for (const [el, name, fn] of events) + el.removeEventListener(name, fn); - break; + EventStore.clear(); + }; + + /** + * safe add event keydown listener + * + * @param listener + */ + this.addKey = function(...argsArr) { + const name = 'keydown'; + const args = [name, ...argsArr]; + + return Events.add(...args); + }; + + /** + * safe remove event click listener + * + * @param listener + */ + this.rmKey = function(...argsArr) { + const name = 'keydown'; + const args = [name, ...argsArr]; + + return Events.remove(...args); + }; + + /** + * safe add event click listener + * + * @param listener + */ + this.addClick = function(...argsArr) { + const name = 'click'; + const args = [name, ...argsArr]; + + return Events.add(...args); + }; + + /** + * safe remove event click listener + * + * @param listener + */ + this.rmClick = function(...argsArr) { + const name = 'click'; + const args = [name, ...argsArr]; + + return Events.remove(...args); + }; + + this.addContextMenu = function(...argsArr) { + const name = 'contextmenu'; + const args = [name, ...argsArr]; + + return Events.add(...args); + }; + + /** + * safe add event click listener + * + * @param listener + */ + this.addError = function(...argsArr) { + const name = 'error'; + const args = [name, ...argsArr]; + + return Events.add(...args); + }; + + /** + * safe add load click listener + * + * @param listener + */ + this.addLoad = function(...argsArr) { + const name = 'load'; + const args = [name, ...argsArr]; + + return Events.add(...args); + }; + + function checkType(type) { + if (!type) + throw Error('type could not be empty!'); } } -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 395ac0fa..8133c11d 100644 --- a/client/dom/files.js +++ b/client/dom/files.js @@ -1,29 +1,37 @@ /* global CloudCmd */ -import itype from 'itype'; -import {promisify} from 'es6-promisify'; -import * as load from '#dom/load'; -import * as RESTful from '#dom/rest'; + +'use strict'; + +const itype = require('itype'); +const currify = require('currify'); +const {promisify} = require('es6-promisify'); + +const load = require('./load'); +const RESTful = require('./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); -export const get = getFile; +const get = currify(getFile); +const unaryMap = (array, fn) => array.map((a) => fn(a)); -function getFile(name) { +module.exports.get = get; + +async function getFile(name) { const type = itype(name); check(name); if (type === 'string') - return getModule(name); + return await getModule(name); if (type === 'array') - return Promise.all(name.map(getFile)); + return Promise.all(unaryMap(name, get)); } function check(name) { @@ -31,9 +39,9 @@ function check(name) { throw Error('name could not be empty!'); } -function getModule(name) { - const regExpHTML = RegExp(FILES_HTML + '|' + FILES_HTML_ROOT); - const regExpJSON = RegExp(FILES_JSON); +async function getModule(name) { + const regExpHTML = new RegExp(FILES_HTML + '|' + FILES_HTML_ROOT); + const regExpJSON = new RegExp(FILES_JSON); const isHTML = regExpHTML.test(name); const isJSON = regExpJSON.test(name); @@ -45,13 +53,12 @@ function getModule(name) { return getConfig(); const path = getPath(name, isHTML, isJSON); - return getSystemFile(path); } function getPath(name, isHTML, isJSON) { let path; - const regExp = RegExp(FILES_HTML_ROOT); + const regExp = new RegExp(FILES_HTML_ROOT); const isRoot = regExp.test(name); if (isHTML) { @@ -69,8 +76,8 @@ function getPath(name, isHTML, isJSON) { } function showError(name) { - const str = `Wrong file name: ${name}`; - const error = Error(str); + const str = 'Wrong file name: ' + name; + const error = new Error(str); throw error; } @@ -134,3 +141,4 @@ function getTimeoutOnce(time) { }, time); }; } + diff --git a/client/dom/images.js b/client/dom/images.js index 75663e72..ac3592f2 100644 --- a/client/dom/images.js +++ b/client/dom/images.js @@ -1,13 +1,20 @@ /* global DOM */ -import createElement from '@cloudcmd/create-element'; + +'use strict'; + +const createElement = require('@cloudcmd/create-element'); + +const Images = module.exports; const LOADING = 'loading'; const HIDDEN = 'hidden'; const ERROR = 'error'; -const getLoadingType = () => isSVG() ? '-svg' : '-gif'; +function getLoadingType() { + return isSVG() ? '-svg' : '-gif'; +} -export const get = getElement; +module.exports.get = getElement; /** * check SVG SMIL animation support @@ -36,7 +43,7 @@ function getElement() { } /* Функция создаёт картинку загрузки */ -export const loading = () => { +module.exports.loading = () => { const element = getElement(); const {classList} = element; const loadingImage = LOADING + getLoadingType(); @@ -48,7 +55,7 @@ export const loading = () => { }; /* Функция создаёт картинку ошибки загрузки */ -export const error = () => { +module.exports.error = () => { const element = getElement(); const {classList} = element; const loadingImage = LOADING + getLoadingType(); @@ -59,21 +66,15 @@ export const error = () => { return element; }; -show.load = show; -show.error = (text) => { - const image = Images.error(); - - DOM.show(image); - image.title = text; - - return image; -}; +module.exports.show = show; +module.exports.show.load = show; +module.exports.show.error = error; /** * Function shows loading spinner * position = {top: true}; */ -export function show(position, panel) { +function show(position, panel) { const image = Images.loading(); const parent = image.parentElement; const refreshButton = DOM.getRefreshButton(panel); @@ -99,25 +100,34 @@ export function show(position, panel) { return image; } +function error(text) { + const image = Images.error(); + + DOM.show(image); + image.title = text; + + return image; +} + /** * hide load image */ -export function hide() { +module.exports.hide = () => { const element = Images.get(); DOM.hide(element); return Images; -} +}; -export const setProgress = (value, title) => { +module.exports.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; @@ -125,7 +135,7 @@ export const setProgress = (value, title) => { return Images; }; -export const clearProgress = () => { +module.exports.clearProgress = () => { const DATA = 'data-progress'; const element = Images.get(); @@ -138,12 +148,3 @@ export const 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 a82b6f72..9113ffe4 100644 --- a/client/dom/index.js +++ b/client/dom/index.js @@ -1,79 +1,29 @@ /* 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; +'use strict'; + +const tryToPromiseAll = require('../../common/try-to-promise-all'); + +const Util = require('../../common/util'); +const callbackify = require('../../common/callbackify'); + +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 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, -}); - -export const CurrentInfo = {}; + ...new CmdProto(), +}; DOM.Images = Images; DOM.load = load; @@ -82,730 +32,841 @@ DOM.RESTful = RESTful; DOM.IO = IO; DOM.Storage = Storage; DOM.Dialog = Dialog; -DOM.CurrentInfo = CurrentInfo; -export default DOM; +module.exports = DOM; -DOM.uploadDirectory = uploadDirectory; -DOM.Buffer = Buffer; -DOM.Events = Events; -const isString = (a) => typeof a === 'string'; +DOM.uploadDirectory = require('./directory'); +DOM.Buffer = require('./buffer'); +DOM.Events = require('./events'); -const TabPanel = { - 'js-left': null, - 'js-right': null, -}; +const loadRemote = require('./load-remote'); +const selectByPattern = require('./select-by-pattern'); -export function loadRemote(name, options, callback) { - _loadRemote(name, options, callback); - return DOM; -} - -export function loadSocket(callback) { - DOM.loadRemote('socket', { - name: 'io', - }, callback); +function CmdProto() { + const CurrentInfo = {}; - return DOM; -} - -/** - * create new folder - * - */ -export async function promptNewDir() { - await promptNew('directory'); -} - -/** - * create new file - * - * @typeName - * @type - */ -export async function promptNewFile() { - await promptNew('file'); -} - -async function promptNew(typeName) { - const {Dialog} = DOM; - const dir = DOM.getCurrentDirPath(); - const msg = `New ${typeName}` || 'File'; + const Cmd = this; + const SELECTED_FILE = 'selected-file'; + const TabPanel = { + 'js-left' : null, + 'js-right' : null, + }; - const getName = () => { - const name = DOM.getCurrentName(); + this.loadRemote = (name, options, callback) => { + loadRemote(name, options, callback); + return DOM; + }; + + this.loadSocket = function(callback) { + DOM.loadRemote('socket', { + name : 'io', + }, callback); + + return DOM; + }; + + /** + * create new folder + * + */ + this.promptNewDir = function() { + promptNew('directory', '?dir'); + }; + + /** + * create new file + * + * @typeName + * @type + */ + this.promptNewFile = () => { + promptNew('file'); + }; + + async function promptNew(typeName, type) { + const {Dialog} = DOM; + const dir = DOM.getCurrentDirPath(); + const msg = 'New ' + typeName || 'File'; + const getName = () => { + const name = DOM.getCurrentName(); + + if (name === '..') + return ''; + + return name; + }; + + const name = getName(); + + const [cancel, currentName] = await Dialog.prompt(msg, name); + + if (cancel) + return; + + const path = (type) => { + const result = dir + currentName; + + if (!type) + return result; + + return result + type; + }; + + await RESTful.write(path(type)); + await CloudCmd.refresh({ + currentName, + }); + } + + /** + * get current direcotory name + */ + this.getCurrentDirName = () => { + const href = DOM.getCurrentDirPath() + .replace(/\/$/, ''); + + const substr = href.substr(href, href.lastIndexOf('/')); + const ret = href.replace(substr + '/', '') || '/'; + + return ret; + }; + + /** + * get current direcotory path + */ + this.getParentDirPath = (panel) => { + const path = DOM.getCurrentDirPath(panel); + const dirName = DOM.getCurrentDirName() + '/'; + const index = path.lastIndexOf(dirName); + + if (path !== '/') + return path.slice(0, index); + + return path; + }; + + /** + * get not current direcotory path + */ + this.getNotCurrentDirPath = () => { + const panel = DOM.getPanel({active: false}); + const path = DOM.getCurrentDirPath(panel); + + return path; + }; + + /** + * get current file by name + */ + this.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 + */ + this.getSelectedFiles = () => { + const panel = DOM.getPanel(); + const selected = DOM.getByClassAll(SELECTED_FILE, panel); + + return Array.from(selected); + }; + + /* + * unselect all files + */ + this.unselectFiles = (files) => { + files = files || DOM.getSelectedFiles(); + + Array.from(files).forEach(DOM.toggleSelectedFile); + }; + + /** + * get all selected files or current when none selected + * + * @currentFile + */ + this.getActiveFiles = () => { + const current = DOM.getCurrentFile(); + const files = DOM.getSelectedFiles(); + const name = DOM.getCurrentName(current); + + if (!files.length && name !== '..') + return [current]; + + return files; + }; + + this.getCurrentDate = (currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + const date = DOM + .getByDataName('js-date', current) + .textContent; + + return date; + }; + + /** + * get size + * @currentFile + */ + this.getCurrentSize = (currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + /* если это папка - возвращаем слово dir вместо размера*/ + const size = DOM.getByDataName('js-size', current) + .textContent + .replace(/^<|>$/g, ''); + + return size; + }; + + /** + * get size + * @currentFile + */ + this.loadCurrentSize = callbackify(async (currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + const query = '?size'; + const link = DOM.getCurrentPath(current); + + Images.show.load(); if (name === '..') - return ''; + return; - return name; - }; - - const name = getName(); - const [cancel, currentName] = await Dialog.prompt(msg, name); - - if (cancel) - return; - - const path = `${dir}${currentName}`; - - if (typeName === 'directory') - await RESTful.createDirectory(path); - else - await RESTful.write(path); - - await CloudCmd.refresh({ - currentName, - }); -} - -/** - * get current directory name - */ -export function getCurrentDirName() { - const href = DOM - .getCurrentDirPath() - .replace(/\/$/, ''); - - const substr = href.substr(href, href.lastIndexOf('/')); - - return href.replace(`${substr}/`, '') || '/'; -} - -/** - * get current directory path - */ -export function getParentDirPath(panel) { - const path = DOM.getCurrentDirPath(panel); - const dirName = DOM.getCurrentDirName() + '/'; - const index = path.lastIndexOf(dirName); - - if (path !== '/') - return path.slice(0, index); - - return path; -} - -/** - * get not current directory path - */ -export function getNotCurrentDirPath() { - const panel = DOM.getPanel({ - active: false, + const [, size] = await RESTful.read(link + query); + + DOM.setCurrentSize(size, current); + Images.hide(); + + return current; }); - return DOM.getCurrentDirPath(panel); -} - -/* - * unselect all files - */ -export function unselectFiles(files) { - files = files || DOM.getSelectedFiles(); - - Array - .from(files) - .forEach(DOM.toggleSelectedFile); -} - -/** - * get all selected files or current when none selected - * - * @currentFile - */ -export function getActiveFiles() { - const current = DOM.getCurrentFile(); - const files = DOM.getSelectedFiles(); - const name = DOM.getCurrentName(current); - - if (!files.length && name !== '..') - return [current]; - - return files; -} - -export function getCurrentDate(currentFile) { - const current = currentFile || DOM.getCurrentFile(); - - return DOM.getByDataName('js-date', current).textContent; -} - -/** - * get size - * @currentFile - */ -export function getCurrentSize(currentFile) { - const current = currentFile || DOM.getCurrentFile(); - - /* если это папка - возвращаем слово dir вместо размера*/ - const size = DOM - .getByDataName('js-size', current) - .textContent - .replace(/^<|>$/g, ''); - - return size; -} - -/** - * get size - * @currentFile - */ -export async function loadCurrentSize(currentFile) { - const current = currentFile || DOM.getCurrentFile(); - const query = '?size'; - const link = DOM.getCurrentPath(current); - - Images.show.load(); - - if (name === '..') - return; - - const [, size] = await RESTful.read(link + query); - - DOM.setCurrentSize(size, current); - Images.hide(); - - return current; -} - -/** - * load hash - * @callback - * @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 - */ -export function setCurrentSize(size, currentFile) { - const current = currentFile || DOM.getCurrentFile(); - const sizeElement = DOM.getByDataName('js-size', current); - - sizeElement.textContent = size; -} - -/** - * @currentFile - */ -export function getCurrentMode(currentFile) { - const current = currentFile || DOM.getCurrentFile(); - const mode = DOM.getByDataName('js-mode', current); - - return mode.textContent; -} - -/** - * @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 - */ -export async function getCurrentData(currentFile) { - const {Dialog} = DOM; - const Info = DOM.CurrentInfo; - const current = currentFile || DOM.getCurrentFile(); - const path = DOM.getCurrentPath(current); - const isDir = DOM.isCurrentIsDir(current); - - if (Info.name === '..') { - Dialog.alert.noFiles(); - return [ - Error('No Files'), - ]; - } - - if (isDir) - return await RESTful.read(path); - - const [hashNew, hash] = await DOM.checkStorageHash(path); - - if (!hashNew) - return [ - Error(`Can't get hash of a file`), - ]; - - if (hash === hashNew) - return [null, await Storage.get(`${path}-data`)]; - - const [e, data] = await RESTful.read(path); - - if (e) - return [ - e, - null, - ]; - - 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 - */ -export function getRefreshButton(panel = DOM.getPanel()) { - return DOM.getByDataName('js-refresh', panel); -} - -export function getAllFiles() { - const panel = DOM.getPanel(); - const files = DOM.getFiles(panel); - const name = DOM.getCurrentName(files[0]); - - const from = (a) => a === '..' ? 1 : 0; - const i = from(name); - - return Array - .from(files) - .slice(i); -} - -/** - * open dialog with expand selection - */ -export async function expandSelection() { - const msg = 'expand'; - const {files} = CurrentInfo; - - await selectByPattern(msg, files); -} - -/** - * open dialog with shrink selection - */ -export async function shrinkSelection() { - const msg = 'shrink'; - const {files} = CurrentInfo; - - await selectByPattern(msg, files); -} - -/** - * setting history wrapper - */ -export function setHistory(data, title, url) { - const ret = globalThis.history; - const {prefix} = CloudCmd; - - url = prefix + url; - - if (ret) - history.pushState(data, title, url); - - return ret; -} - -/** - * get link from current (or param) file - * - * @param currentFile - current file by default - */ -export function getCurrentLink(currentFile) { - const current = currentFile || DOM.getCurrentFile(); - const link = DOM.getByTag('a', current); - - return link[0]; -} - -export function getFilenames(files) { - if (!files) - throw Error('AllFiles could not be empty'); - - const first = files[0] || DOM.getCurrentFile(); - const name = DOM.getCurrentName(first); - - const allFiles = Array.from(files); - - if (name === '..') - allFiles.shift(); - - return allFiles.map(DOM.getCurrentName); -} - -/** - * check storage hash - */ -export async function checkStorageHash(name) { - const nameHash = `${name}-hash`; - - if (!isString(name)) - throw Error('name should be a string!'); - - const [loadHash, storeHash] = await Promise.all([ - DOM.loadCurrentHash(), - Storage.get(nameHash), - ]); - - return [loadHash, storeHash]; -} - -/** - * save data to storage - * - * @param name - * @param data - * @param hash - */ -export async function saveDataToStorage(name, data, hash) { - const isDir = DOM.isCurrentIsDir(); - - if (isDir) - return; - - hash = hash || await DOM.loadCurrentHash(); - - const nameHash = `${name}-hash`; - const nameData = `${name}-data`; - - await Storage.set(nameHash, hash); - await Storage.set(nameData, data); - - return hash; -} - -export function getFM() { - const {parentElement} = DOM.getPanel(); - return parentElement; -} - -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} - */ -export function getPanel(options) { - let files; - let panel; - let isLeft; - let dataName = 'js-'; - - const current = DOM.getCurrentFile(); - - if (!current) { - panel = DOM.getByDataName('js-left'); - } else { - files = current.parentElement; - panel = files.parentElement; - isLeft = panel.getAttribute('data-name') === 'js-left'; - } - - /* if {active : false} getting passive panel */ - if (options && !options.active) { - dataName += isLeft ? 'right' : 'left'; - panel = DOM.getByDataName(dataName); - } - - /* if two panels showed - * then always work with passive - * panel + /** + * load hash + * @callback + * @currentFile */ - if (globalThis.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH) - panel = DOM.getByDataName('js-left'); - - if (!panel) - throw Error('can not find Active Panel!'); - - return panel; -} - -export function getFiles(element) { - const files = DOM.getByDataName('js-files', element); - return files.children || []; -} - -/** - * shows panel right or left (or active) - */ -export function showPanel(active) { - const panel = DOM.getPanel({ - active, - }); - - if (!panel) - return false; - - DOM.show(panel); - - return true; -} - -/** - * hides panel right or left (or active) - */ -export function hidePanel(active) { - const panel = DOM.getPanel({ - active, - }); - - if (!panel) - return false; - - return DOM.hide(panel); -} - -/** - * remove child of element - * @param child - * @param element - */ -export function remove(child, element) { - const parent = element || document.body; - - parent.removeChild(child); - - return DOM; -} - -/** - * remove current file from file table - * @param current - * - */ -export function deleteCurrent(current) { - if (!current) - DOM.getCurrentFile(); - - const parent = current?.parentElement; - const name = DOM.getCurrentName(current); - - if (current && name !== '..') { - const next = current.nextSibling; - const prev = current.previousSibling; + this.loadCurrentHash = async (currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + const query = '?hash'; + const link = DOM.getCurrentPath(current); - DOM.setCurrentFile(next || prev); - parent.removeChild(current); - } -} - -/** - * remove selected files from file table - * @Selected - */ -export function deleteSelected(selected) { - selected = selected || DOM.getSelectedFiles(); - - if (!selected) - return; - - selected.map(DOM.deleteCurrent); -} - -/** - * rename current file - * - * @currentFile - */ -export function scrollIntoViewIfNeeded(element, center = false) { - if (!element || !element.scrollIntoViewIfNeeded) - return; - - element.scrollIntoViewIfNeeded(center); -} - -/* scroll on one page */ -export function scrollByPages(element, pPages) { - const ret = element?.scrollByPages && pPages; - - if (ret) - element.scrollByPages(pPages); - - return ret; -} - -export function changePanel() { - const Info = CurrentInfo; - let panel = DOM.getPanel(); - - CloudCmd.emit('passive-dir', Info.dirPath); - - const panelPassive = DOM.getPanel({ - active: false, - }); - - let name = DOM.getCurrentName(); - const filesPassive = DOM.getFiles(panelPassive); - - let dataName = panel.getAttribute('data-name'); - - TabPanel[dataName] = name; - - panel = panelPassive; - dataName = panel.getAttribute('data-name'); - - name = TabPanel[dataName]; - - let files; - let current; - - if (name) { - current = DOM.getCurrentByName(name, panel); - - if (current) - files = current.parentElement; - } - - if (!files || !files.parentElement) { - current = DOM.getCurrentByName(name, panel); - - if (!current) - [current] = filesPassive; - } - - DOM.setCurrentFile(current, { - history: true, - }); - - CloudCmd.emit('active-dir', Info.dirPath); - - return DOM; -} - -export function getPackerExt(type) { - if (type === 'zip') - return '.zip'; - - return '.tar.gz'; -} - -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 prompt(msg, dirPath); - - if (cancel) - return; - - await changeDir(path); -} - -export async function duplicatePanel() { - const Info = CurrentInfo; - const {isDir} = Info; - const panel = Info.panelPassive; - const noCurrent = !Info.isOnePanel; - - const getPath = (isDir) => { - if (isDir) - return Info.path; - - return Info.dirPath; + const [, data] = await RESTful.read(link + query); + return data; }; - const path = getPath(isDir); + /** + * load current modification time of file + * @callback + * @currentFile + */ + this.loadCurrentTime = async (currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + const query = '?time'; + const link = DOM.getCurrentPath(current); + + const [, data] = await RESTful.read(link + query); + return data; + }; - await CloudCmd.changeDir(path, { - panel, - noCurrent, - }); + /** + * set size + * @currentFile + */ + this.setCurrentSize = (size, currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + const sizeElement = DOM.getByDataName('js-size', current); + + sizeElement.textContent = size; + }; + + /** + * @currentFile + */ + this.getCurrentMode = (currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + const mode = DOM.getByDataName('js-mode', current); + + return mode.textContent; + }; + + /** + * @currentFile + */ + this.getCurrentOwner = (currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + const owner = DOM.getByDataName('js-owner', current); + + return owner.textContent; + }; + + const mixArgs = (f) => (a, b) => f(b, a); + + /** + * unified way to get current file content + * + * @param callback + * @param currentFile + */ + this.getCurrentData = callbackify(mixArgs(async (callback, currentFile) => { + const {Dialog} = DOM; + const Info = DOM.CurrentInfo; + const current = currentFile || DOM.getCurrentFile(); + const path = DOM.getCurrentPath(current); + const isDir = DOM.isCurrentIsDir(current); + + if (Info.name === '..') { + Dialog.alert.noFiles(); + return callback(Error('No files selected!')); + } + + if (isDir) { + const [e, data] = await RESTful.read(path); + + if (e) + throw e; + + return data; + } + + const [hashNew, hash] = await DOM.checkStorageHash(path); + + if (hash === hashNew) + return await Storage.get(`${path}-data`); + + const [e, data] = await RESTful.read(path); + + if (e) + return; + + const ONE_MEGABYTE = 1024 * 1024 * 1024; + const {length} = data; + + if (hash && length < ONE_MEGABYTE) + await DOM.saveDataToStorage(path, data, hashNew); + + return data; + })); + + /** + * unified way to get RefreshButton + */ + this.getRefreshButton = (panel = DOM.getPanel()) => { + return DOM.getByDataName('js-refresh', panel); + }; + + /** + * select current file + * @param currentFile + */ + this.selectFile = (currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + + current.classList.add(SELECTED_FILE); + + return Cmd; + }; + + this.unselectFile = (currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + + current.classList.remove(SELECTED_FILE); + + return Cmd; + }; + + this.toggleSelectedFile = (currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + const name = DOM.getCurrentName(current); + + if (name === '..') + return Cmd; + + current.classList.toggle(SELECTED_FILE); + + return Cmd; + }; + + this.toggleAllSelectedFiles = () => { + DOM.getAllFiles().map(DOM.toggleSelectedFile); + + return Cmd; + }; + + this.selectAllFiles = () => { + DOM.getAllFiles().map(DOM.selectFile); + + return Cmd; + }; + + this.getAllFiles = () => { + const panel = DOM.getPanel(); + const files = DOM.getFiles(panel); + const name = DOM.getCurrentName(files[0]); + + const from = (a) => a === '..' ? 1 : 0; + const i = from(name); + + return Array.from(files).slice(i); + }; + + /** + * open dialog with expand selection + */ + this.expandSelection = () => { + const msg = 'expand'; + const {files} = CurrentInfo; + + selectByPattern(msg, files); + }; + + /** + * open dialog with shrink selection + */ + this.shrinkSelection = () => { + const msg = 'shrink'; + const {files} = CurrentInfo; + + selectByPattern(msg, files); + }; + + /** + * setting history wrapper + */ + this.setHistory = (data, title, url) => { + const ret = window.history; + const {prefix} = CloudCmd; + + url = prefix + url; + + if (ret) + history.pushState(data, title, url); + + return ret; + }; + + /** + * selected file check + * + * @param currentFile + */ + this.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 + */ + this.getCurrentLink = (currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + const link = DOM.getByTag('a', current); + + return link[0]; + }; + + this.getFilenames = (files) => { + if (!files) + throw Error('AllFiles could not be empty'); + + const first = files[0] || DOM.getCurrentFile(); + const name = DOM.getCurrentName(first); + + const allFiles = Array.from(files); + + if (name === '..') + allFiles.shift(); + + const names = allFiles.map((current) => { + return DOM.getCurrentName(current); + }); + + return names; + }; + + /** + * check storage hash + */ + this.checkStorageHash = async (name) => { + const nameHash = name + '-hash'; + + if (typeof name !== 'string') + throw Error('name should be a string!'); + + const [error, loadHash, storeHash] = await tryToPromiseAll([ + DOM.loadCurrentHash(), + Storage.get(nameHash), + ]); + + if (error) + throw error; + + return [loadHash, storeHash]; + }; + + /** + * save data to storage + * + * @param name + * @param data + * @param hash + * @param callback + */ + this.saveDataToStorage = async (name, data, hash) => { + const isDir = DOM.isCurrentIsDir(); + + if (isDir) + return; + + hash = hash || await DOM.loadCurrentHash(); + + const nameHash = name + '-hash'; + const nameData = name + '-data'; + + await Storage.set(nameHash, hash); + await Storage.set(nameData, data); + + return hash; + }; + + this.getFM = () => { + return DOM.getPanel().parentElement; + }; + + this.getPanelPosition = (panel) => { + panel = panel || DOM.getPanel(); + + return panel.dataset.name.replace('js-', ''); + }; + + /** function getting panel active, or passive + * @param options = {active: true} + */ + this.getPanel = (options) => { + let files; + let panel; + let isLeft; + let dataName = 'js-'; + + const current = DOM.getCurrentFile(); + + if (!current) { + panel = DOM.getByDataName('js-left'); + } else { + files = current.parentElement; + panel = files.parentElement; + isLeft = panel.getAttribute('data-name') === 'js-left'; + } + + /* if {active : false} getting passive panel */ + if (options && !options.active) { + dataName += isLeft ? 'right' : 'left'; + panel = DOM.getByDataName(dataName); + } + + /* if two panels showed + * then always work with passive + * panel + */ + if (window.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH) + panel = DOM.getByDataName('js-left'); + + if (!panel) + throw Error('can not find Active Panel!'); + + return panel; + }; + + this.getFiles = (element) => { + const files = DOM.getByDataName('js-files', element); + return files.children || []; + }; + + /** + * shows panel right or left (or active) + */ + this.showPanel = (active) => { + const panel = DOM.getPanel({active}); + + if (!panel) + return false; + + DOM.show(panel); + + return true; + }; + + /** + * hides panel right or left (or active) + */ + this.hidePanel = (active) => { + const panel = DOM.getPanel({ + active, + }); + + if (!panel) + return false; + + return DOM.hide(panel); + }; + + /** + * remove child of element + * @param pChild + * @param element + */ + this.remove = (child, element) => { + const parent = element || document.body; + + parent.removeChild(child); + + return DOM; + }; + + /** + * remove current file from file table + * @param current + * + */ + this.deleteCurrent = (current) => { + if (!current) + DOM.getCurrentFile(); + + const parent = current && current.parentElement; + const name = DOM.getCurrentName(current); + + if (current && name !== '..') { + const next = current.nextSibling; + const prev = current.previousSibling; + + DOM.setCurrentFile(next || prev); + parent.removeChild(current); + } + }; + + /** + * remove selected files from file table + * @Selected + */ + this.deleteSelected = (selected) => { + selected = selected || DOM.getSelectedFiles(); + + if (!selected) + return; + + selected.map(DOM.deleteCurrent); + }; + + /** + * rename current file + * + * @currentFile + */ + this.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 + */ + this.scrollIntoViewIfNeeded = function(element, center = false) { + if (!element || !element.scrollIntoViewIfNeeded) + return; + + element.scrollIntoViewIfNeeded(center); + }; + + /* scroll on one page */ + this.scrollByPages = (element, pPages) => { + const ret = element && element.scrollByPages && pPages; + + if (ret) + element.scrollByPages(pPages); + + return ret; + }; + + this.changePanel = () => { + const Info = CurrentInfo; + let panel = DOM.getPanel(); + + CloudCmd.emit('passive-dir', Info.dirPath); + + const panelPassive = DOM.getPanel({ + active: false, + }); + + let name = DOM.getCurrentName(); + const filesPassive = DOM.getFiles(panelPassive); + + let dataName = panel.getAttribute('data-name'); + + TabPanel[dataName] = name; + + panel = panelPassive; + dataName = panel.getAttribute('data-name'); + + name = TabPanel[dataName]; + + let files; + let current; + + if (name) { + current = DOM.getCurrentByName(name, panel); + + if (current) + files = current.parentElement; + } + + if (!files || !files.parentElement) { + current = DOM.getCurrentByName(name, panel); + + if (!current) + [current] = filesPassive; + } + + DOM.setCurrentFile(current, { + history: true, + }); + + CloudCmd.emit('active-dir', Info.dirPath); + + return DOM; + }; + + this.getPackerExt = (type) => { + if (type === 'zip') + return '.zip'; + + return '.tar.gz'; + }; + + this.goToDirectory = async () => { + const msg = 'Go to directory:'; + const {Dialog} = DOM; + const {dirPath} = CurrentInfo; + + const [ + cancel, + path = dirPath, + ] = await Dialog.prompt(msg, path); + + if (cancel) + return; + + await CloudCmd.loadDir({ + path, + }); + }; + + this.duplicatePanel = async () => { + const Info = CurrentInfo; + const {isDir} = Info; + const panel = Info.panelPassive; + const noCurrent = !Info.isOnePanel; + + const getPath = (isDir) => { + if (isDir) + return Info.path; + + return Info.dirPath; + }; + + const path = getPath(isDir); + + await CloudCmd.loadDir({ + path, + panel, + noCurrent, + }); + }; + + this.swapPanels = async () => { + const Info = CurrentInfo; + const { + panel, + files, + element, + panelPassive, + } = Info; + + const path = DOM.getCurrentDirPath(); + const dirPathPassive = DOM.getNotCurrentDirPath(); + + let currentIndex = files.indexOf(element); + + await CloudCmd.loadDir({ + path, + panel: panelPassive, + noCurrent: true, + }); + + await CloudCmd.loadDir({ + path: dirPathPassive, + panel, + }); + + const length = Info.files.length - 1; + + if (currentIndex > length) + currentIndex = length; + + const el = Info.files[currentIndex]; + + DOM.setCurrentFile(el); + }; + + this.CurrentInfo = CurrentInfo; + + this.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, + }); + + const filesPassive = DOM.getFiles(panelPassive); + const name = DOM.getCurrentName(current); + + /* eslint no-multi-spaces:0 */ + + 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'); + }; } -export async function swapPanels() { - const Info = CurrentInfo; - const { - panel, - files, - element, - panelPassive, - } = Info; - - const path = DOM.getCurrentDirPath(); - const dirPathPassive = DOM.getNotCurrentDirPath(); - - let currentIndex = files.indexOf(element); - - await CloudCmd.changeDir(path, { - panel: panelPassive, - noCurrent: true, - }); - - await CloudCmd.changeDir(dirPathPassive, { - panel, - }); - - const length = Info.files.length - 1; - - if (currentIndex > length) - currentIndex = length; - - const el = Info.files[currentIndex]; - - DOM.setCurrentFile(el); -} - -export function updateCurrentInfo(currentFile) { - const info = DOM.CurrentInfo; - const current = currentFile || DOM.getCurrentFile(); - const files = current.parentElement; - - const panelPassive = DOM.getPanel({ - active: false, - }); - - 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 = 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 deleted file mode 100644 index 268efe5f..00000000 --- a/client/dom/index.spec.js +++ /dev/null @@ -1,52 +0,0 @@ -import {test, stub} from 'supertape'; -import {getCSSVar, goToDirectory} from '#dom'; - -globalThis.CloudCmd = {}; - -test('cloudcmd: client: dom: goToDirectory', async (t) => { - const path = ''; - const changeDir = stub(); - const prompt = stub().returns([null, path]); - - await goToDirectory({ - prompt, - changeDir, - }); - - t.calledWith(changeDir, [path]); - t.end(); -}); - -test('cloudcmd: client: dom: getCSSVar', (t) => { - const body = {}; - const getPropertyValue = stub().returns(0); - - globalThis.getComputedStyle = stub().returns({ - getPropertyValue, - }); - const result = getCSSVar('hello', { - body, - }); - - delete globalThis.getComputedStyle; - - t.notOk(result); - t.end(); -}); - -test('cloudcmd: client: dom: getCSSVar: 1', (t) => { - const body = {}; - const getPropertyValue = stub().returns(1); - - globalThis.getComputedStyle = stub().returns({ - getPropertyValue, - }); - const result = getCSSVar('hello', { - body, - }); - - delete globalThis.getComputedStyle; - - t.ok(result); - t.end(); -}); diff --git a/client/dom/io.js b/client/dom/io.js new file mode 100644 index 00000000..f47ccd62 --- /dev/null +++ b/client/dom/io.js @@ -0,0 +1,216 @@ +'use strict'; + +/* global CloudCmd*/ + +const itype = require('itype'); +const {promisify} = require('es6-promisify'); + +const {FS} = require('../../common/cloudfunc'); + +const Images = require('./images'); +const load = require('./load'); + +const imgPosition = { + top: true, +}; + +module.exports._replaceHash = replaceHash; +function replaceHash(url) { + /* + * if we send ajax request - + * no need in hash so we escape # + */ + return url.replace(/#/g, '%23'); +} + +module.exports.delete = promisify((url, data, callback) => { + const isFunc = itype.function(data); + + if (!callback && isFunc) { + callback = data; + data = null; + } + + sendRequest({ + method : 'DELETE', + url : FS + url, + data, + callback, + imgPosition : { + top: !!data, + }, + }); +}); + +module.exports.patch = promisify((url, data, callback) => { + const isFunc = itype.function(data); + + if (!callback && isFunc) { + callback = data; + data = null; + } + + sendRequest({ + method: 'PATCH', + url: FS + url, + data, + callback, + imgPosition, + }); +}); + +module.exports.write = promisify((url, data, callback) => { + const isFunc = itype.function(data); + + if (!callback && isFunc) { + callback = data; + data = null; + } + + sendRequest({ + method: 'PUT', + url: FS + url, + data, + callback, + imgPosition, + }); +}); + +module.exports.read = promisify((url, dataType, callback) => { + const notLog = !url.includes('?'); + const isFunc = itype.function(dataType); + + if (!callback && isFunc) { + callback = dataType; + dataType = 'text'; + } + + sendRequest({ + method: 'GET', + url: FS + url, + callback, + notLog, + dataType, + }); +}); + +module.exports.cp = promisify((data, callback) => { + sendRequest({ + method: 'PUT', + url: '/cp', + data, + callback, + imgPosition, + }); +}); + +module.exports.pack = promisify((data, callback) => { + sendRequest({ + method: 'PUT', + url: '/pack', + data, + callback, + }); +}); + +module.exports.extract = promisify((data, callback) => { + sendRequest({ + method : 'PUT', + url : '/extract', + data, + callback, + }); +}); + +module.exports.mv = promisify((data, callback) => { + sendRequest({ + method : 'PUT', + url : '/mv', + data, + callback, + imgPosition, + }); +}); + +module.exports.Config = { + read: promisify((callback) => { + sendRequest({ + method: 'GET', + url: '/config', + callback, + imgPosition, + notLog: true, + }); + }), + + write: promisify((data, callback) => { + sendRequest({ + method: 'PATCH', + url: '/config', + data, + callback, + imgPosition, + }); + }), +}; + +module.exports.Markdown = { + read: promisify((url, callback) => { + sendRequest({ + method: 'GET', + url: '/markdown' + url, + callback, + imgPosition, + notLog: true, + }); + }), + + render: promisify((data, callback) => { + sendRequest({ + method: 'PUT', + url: '/markdown', + data, + callback, + imgPosition, + notLog: true, + }); + }), +}; + +function sendRequest(params) { + const p = params; + const {prefixURL} = CloudCmd; + + p.url = prefixURL + p.url; + p.url = encodeURI(p.url); + + p.url = replaceHash(p.url); + + load.ajax({ + method : p.method, + url : p.url, + data : p.data, + dataType : p.dataType, + error : (jqXHR) => { + const response = jqXHR.responseText; + + const { + statusText, + status, + } = jqXHR; + + const text = status === 404 ? response : statusText; + + p.callback(Error(text)); + }, + success: (data) => { + Images.hide(); + + if (!p.notLog) + CloudCmd.log(data); + + p.callback(null, data); + }, + }); +} + diff --git a/client/dom/io/send-request.spec.js b/client/dom/io.spec.js similarity index 55% rename from client/dom/io/send-request.spec.js rename to client/dom/io.spec.js index f8a960d8..c73efbac 100644 --- a/client/dom/io/send-request.spec.js +++ b/client/dom/io.spec.js @@ -1,11 +1,15 @@ -import {test} from 'supertape'; -import {_replaceHash} from './send-request.js'; +'use strict'; + +const test = require('supertape'); +const io = require('./io'); test('cloudcmd: client: io: replaceHash', (t) => { + const {_replaceHash} = io; const url = '/hello/####world'; const result = _replaceHash(url); const expected = '/hello/%23%23%23%23world'; - t.equal(result, expected); + t.equal(result, expected, 'should equal'); t.end(); }); + diff --git a/client/dom/io/index.js b/client/dom/io/index.js deleted file mode 100644 index 192769e6..00000000 --- a/client/dom/io/index.js +++ /dev/null @@ -1,166 +0,0 @@ -import {FS} from '#common/cloudfunc'; -import {sendRequest as _sendRequest} from './send-request.js'; - -const {assign} = Object; - -const imgPosition = { - top: true, -}; - -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); -}; - -export const patch = async (url, data) => { - return await _sendRequest({ - method: 'PATCH', - url: FS + url, - data, - imgPosition, - }); -}; - -export const write = async (url, data) => { - return await _sendRequest({ - method: 'PUT', - url: FS + url, - data, - imgPosition, - }); -}; - -export const createDirectory = async (url, overrides = {}) => { - const { - sendRequest = _sendRequest, - } = overrides; - - return await sendRequest({ - method: 'PUT', - url: `${FS}${url}?dir`, - imgPosition, - }); -}; - -export const read = async (url, dataType = 'text') => { - const notLog = !url.includes('?'); - - return await _sendRequest({ - method: 'GET', - url: FS + url, - notLog, - dataType, - }); -}; - -export const copy = async (from, to, names) => { - return await _sendRequest({ - method: 'PUT', - url: '/copy', - data: { - from, - to, - names, - }, - imgPosition, - }); -}; - -export const pack = async (data) => { - return await _sendRequest({ - method: 'PUT', - url: '/pack', - data, - }); -}; - -export const extract = async (data) => { - return await _sendRequest({ - method: 'PUT', - url: '/extract', - data, - }); -}; - -export const move = async (from, to, names) => { - return await _sendRequest({ - method: 'PUT', - url: '/move', - data: { - from, - to, - names, - }, - imgPosition, - }); -}; - -export const rename = async (from, to) => { - return await _sendRequest({ - method: 'PUT', - url: '/rename', - data: { - from, - to, - }, - imgPosition, - }); -}; - -export const Config = { - read: async () => { - return await _sendRequest({ - method: 'GET', - url: '/config', - imgPosition, - notLog: true, - }); - }, - - write: async (data) => { - return await _sendRequest({ - method: 'PATCH', - url: '/config', - data, - imgPosition, - }); - }, -}; - -export const Markdown = { - read: async (url) => { - return await _sendRequest({ - method: 'GET', - url: `/markdown${url}`, - imgPosition, - notLog: true, - }); - }, - - render: async (data) => { - return await _sendRequest({ - method: 'PUT', - url: '/markdown', - data, - imgPosition, - notLog: true, - }); - }, -}; diff --git a/client/dom/io/index.spec.js b/client/dom/io/index.spec.js deleted file mode 100644 index 8e9e2d08..00000000 --- a/client/dom/io/index.spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import {test, stub} from 'supertape'; -import * as io from './index.js'; - -test('client: dom: io', (t) => { - const sendRequest = stub(); - - io.createDirectory('/hello', { - sendRequest, - }); - - const expected = { - imgPosition: { - top: true, - }, - method: 'PUT', - url: '/fs/hello?dir', - }; - - 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 deleted file mode 100644 index 177d34b5..00000000 --- a/client/dom/io/send-request.js +++ /dev/null @@ -1,48 +0,0 @@ -/* global CloudCmd */ -import {promisify} from 'es6-promisify'; -import * as Images from '#dom/images'; -import * as load from '#dom/load'; - -export const sendRequest = promisify((params, callback) => { - const p = params; - const {prefixURL} = CloudCmd; - - p.url = prefixURL + p.url; - p.url = encodeURI(p.url); - - p.url = replaceHash(p.url); - - load.ajax({ - method: p.method, - url: p.url, - data: p.data, - dataType: p.dataType, - error: (jqXHR) => { - const response = jqXHR.responseText; - - const {statusText, status} = jqXHR; - - const text = status === 404 ? response : statusText; - - callback(Error(text)); - }, - success: (data) => { - Images.hide(); - - if (!p.notLog) - CloudCmd.log(data); - - callback(null, data); - }, - }); -}); - -export const _replaceHash = replaceHash; - -function replaceHash(url) { - /* - * if we send ajax request - - * no need in hash so we escape # - */ - return url.replace(/#/g, '%23'); -} diff --git a/client/dom/load-remote.js b/client/dom/load-remote.js index 5be8e327..b4e0aa22 100644 --- a/client/dom/load-remote.js +++ b/client/dom/load-remote.js @@ -1,58 +1,60 @@ -/* 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'; +'use strict'; -export const loadRemote = callbackify(async (name, options) => { +/* global CloudCmd */ + +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) => { const {prefix, config} = CloudCmd; const o = options; if (o.name && window[o.name]) - return; + return callback(); - 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 = []; - - for (const url of local) { - localURL.push(prefix + url); - } - - const remoteURL = []; - - for (const tmpl of remoteTmpls) { - remoteURL.push(rendy(tmpl, { - version, - })); - } - - if (online) { - const [e] = await tryToCatch(load.parallel, remoteURL); + Files.get('modules').then(async (modules) => { + const online = config('online') && navigator.onLine; + const module = findObjByNameInArr(modules.remote, name); - if (!e) - return; - } - - await load.parallel(localURL); -}); + 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) => { + return 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); + }); +}; + diff --git a/client/dom/load.js b/client/dom/load.js index 8c7011c1..9ba976fc 100644 --- a/client/dom/load.js +++ b/client/dom/load.js @@ -1,8 +1,12 @@ -import itype from 'itype'; -import jonny from 'jonny'; -import Emitify from 'emitify'; -import exec from 'execon'; -import * as Images from '#dom/images'; +'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; /** * Function gets id by src @@ -10,18 +14,17 @@ import * as Images from '#dom/images'; * * Example: http://domain.com/1.js -> 1_js */ -export function getIdBySrc(src) { +function getIdBySrc(src) { const isStr = itype.string(src); if (!isStr) return; - if (src.includes(':')) + if (~src.indexOf(':')) src += '-join'; const num = src.lastIndexOf('/') + 1; const sub = src.substr(src, num); - const id = src .replace(sub, '') .replace(/\./g, '-'); @@ -34,15 +37,15 @@ export function getIdBySrc(src) { * * @param params */ -export const ajax = (params) => { +module.exports.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); @@ -60,7 +63,7 @@ export const ajax = (params) => { if (!isArrayBuf && isObject || isArray) data = jonny.stringify(p.data); else - ({data} = p); + data = p.data; xhr.onreadystatechange = (event) => { const xhr = event.target; @@ -78,7 +81,7 @@ export const ajax = (params) => { return exec(p.error, xhr); const notText = p.dataType !== 'text'; - const isContain = type.includes(TYPE_JSON); + const isContain = ~type.indexOf(TYPE_JSON); let data = xhr.response; @@ -91,11 +94,12 @@ export const ajax = (params) => { xhr.send(data); }; -export const put = (url, body) => { +module.exports.put = (url, body) => { const emitter = Emitify(); const xhr = new XMLHttpRequest(); - url = encodeURI(url).replace(/#/g, '#'); + url = encodeURI(url) + .replace('#', '%23'); xhr.open('put', url, true); @@ -116,10 +120,8 @@ export const put = (url, body) => { if (!over) return; - if (xhr.status === OK) { - emitter.emit('progress', 100); + if (xhr.status === OK) return emitter.emit('end'); - } const error = Error(xhr.responseText); emitter.emit('error', error); @@ -129,3 +131,4 @@ export const put = (url, body) => { return emitter; }; + diff --git a/client/dom/operations/rename-current.js b/client/dom/operations/rename-current.js index 8b6536d3..401a4378 100644 --- a/client/dom/operations/rename-current.js +++ b/client/dom/operations/rename-current.js @@ -1,27 +1,23 @@ -/* 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'; +'use strict'; -export default async (current, overrides = {}) => { - const { - refresh = CloudCmd.refresh, - Dialog = _Dialog, - currentFile = _currentFile, - } = overrides; - - const { - isCurrentFile, - getCurrentName, - getCurrentFile, - getCurrentByName, - getCurrentType, - getCurrentDirPath, - setCurrentName, - } = currentFile; - +/* global CloudCmd */ + +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) => { if (!isCurrentFile(current)) current = getCurrentFile(); @@ -50,11 +46,12 @@ export default async (current, overrides = {}) => { return; const dirPath = getCurrentDirPath(); + const files = { + from : dirPath + from, + to : dirPath + to, + }; - const fromFull = `${dirPath}${from}`; - const toFull = `${dirPath}${to}`; - - const [e] = await RESTful.rename(fromFull, toFull); + const [e] = await RESTful.mv(files); if (e) return; @@ -62,5 +59,5 @@ export default async (current, overrides = {}) => { setCurrentName(to, current); Storage.remove(dirPath); - refresh(); + CloudCmd.refresh(); }; diff --git a/client/dom/operations/rename-current.spec.js b/client/dom/operations/rename-current.spec.js index a5ebb415..c90a29c8 100644 --- a/client/dom/operations/rename-current.spec.js +++ b/client/dom/operations/rename-current.spec.js @@ -1,26 +1,37 @@ -import {test, stub} from 'supertape'; -import renameCurrent from './rename-current.js'; +'use strict'; + +const test = require('supertape'); +const stub = require('@cloudcmd/stub'); +const mockRequire = require('mock-require'); + +const {reRequire} = mockRequire; test('cloudcmd: client: dom: renameCurrent: isCurrentFile', async (t) => { const current = {}; const isCurrentFile = stub(); - const currentFile = stubCurrentFile({ + mockRequire('../dialog', stubDialog()); + mockRequire('../current-file', stubCurrentFile({ isCurrentFile, - }); + })); - await renameCurrent(current, { - Dialog: stubDialog(), - currentFile, - }); + const renameCurrent = reRequire('./rename-current'); + await renameCurrent(current); - t.calledWith(isCurrentFile, [current], 'should call isCurrentFile'); + t.ok(isCurrentFile.calledWith(current), 'should call isCurrentFile'); t.end(); }); 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]); @@ -28,24 +39,23 @@ test('cloudcmd: client: dom: renameCurrent: file exist', async (t) => { const getCurrentByName = stub().returns(current); const getCurrentType = stub().returns('directory'); - const Dialog = stubDialog({ + mockRequire('../dialog', stubDialog({ confirm, prompt, - }); + })); - const currentFile = stubCurrentFile({ + mockRequire('../current-file', stubCurrentFile({ getCurrentByName, getCurrentType, - }); + })); - await renameCurrent(null, { - Dialog, - currentFile, - }); + const renameCurrent = reRequire('./rename-current'); + await renameCurrent(); const expected = 'Directory "hello" already exists. Proceed?'; + global.CloudCmd = CloudCmd; - t.calledWith(confirm, [expected], 'should call confirm'); + t.ok(confirm.calledWith(expected), 'should call confirm'); t.end(); }); @@ -84,3 +94,4 @@ const stubCurrentFile = (fns = {}) => { setCurrentName, }; }; + diff --git a/client/dom/rest.js b/client/dom/rest.js index 3cf2fb66..53fe28d8 100644 --- a/client/dom/rest.js +++ b/client/dom/rest.js @@ -1,8 +1,12 @@ -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'; +'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'); const handleError = (promise) => async (...args) => { const [e, data] = await tryToCatch(promise, ...args); @@ -18,23 +22,22 @@ const handleError = (promise) => async (...args) => { return [e, data]; }; -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); +module.exports.delete = handleError(IO.delete); +module.exports.patch = handleError(IO.patch); +module.exports.write = handleError(IO.write); +module.exports.read = handleError(IO.read); +module.exports.cp = handleError(IO.cp); +module.exports.pack = handleError(IO.pack); +module.exports.extract = handleError(IO.extract); +module.exports.mv = handleError(IO.mv); -export const Config = { +module.exports.Config = { read: handleError(IO.Config.read), write: handleError(IO.Config.write), }; -export const Markdown = { +module.exports.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 0bada6ef..eec89bb4 100644 --- a/client/dom/select-by-pattern.js +++ b/client/dom/select-by-pattern.js @@ -1,18 +1,22 @@ -import {alert, prompt} from '#dom/dialog'; -import {getRegExp} from '#common/util'; -import {getCurrentName} from './current-file.js'; -import { - isSelected, - toggleSelectedFile, -} from './cmd.js'; +'use strict'; let SelectType = '*.*'; -export const selectByPattern = async (msg, files) => { +const {getRegExp} = require('../../common/util'); +const { + alert, + prompt, +} = require('./dialog'); + +const DOM = require('.'); + +module.exports = 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) @@ -24,23 +28,24 @@ export const selectByPattern = async (msg, files) => { let matches = 0; for (const current of files) { - const name = getCurrentName(current); + const name = DOM.getCurrentName(current); if (name === '..' || !regExp.test(name)) continue; ++matches; - let selected = isSelected(current); + let isSelected = DOM.isSelected(current); const shouldSel = msg === 'expand'; if (shouldSel) - selected = !selected; + isSelected = !isSelected; - if (selected) - toggleSelectedFile(current); + if (isSelected) + DOM.toggleSelectedFile(current); } if (!matches) alert('No matches found!'); }; + diff --git a/client/dom/storage.js b/client/dom/storage.js index d5cf231f..a2de5201 100644 --- a/client/dom/storage.js +++ b/client/dom/storage.js @@ -1,26 +1,38 @@ +'use strict'; + +const tryCatch = require('try-catch'); + const {parse, stringify} = JSON; +const isObj = (a) => typeof a === 'object'; -export const set = (name, data) => { - localStorage.setItem(name, data); +module.exports.set = (name, data) => { + const primitive = !isObj(data) ? data : stringify(data); + + localStorage.setItem(name, primitive); }; -export const setJson = (name, data) => { - localStorage.setItem(name, stringify(data)); -}; - -export const get = (name) => { - return localStorage.getItem(name); -}; - -export const getJson = (name) => { +module.exports.get = async (name) => { const data = localStorage.getItem(name); - return parse(data); + const [, result = data] = tryCatch(parse, data); + + return result; }; -export const clear = () => { +module.exports.clear = () => { localStorage.clear(); }; -export const remove = (item) => { +module.exports.remove = (item) => { localStorage.removeItem(item); }; + +module.exports.removeMatch = (string) => { + const reg = RegExp('^' + string + '.*$'); + const test = (a) => reg.test(a); + const remove = (a) => localStorage.removeItem(a); + + Object.keys(localStorage) + .filter(test) + .forEach(remove); +}; + diff --git a/client/dom/storage.spec.js b/client/dom/storage.spec.js deleted file mode 100644 index c538952d..00000000 --- a/client/dom/storage.spec.js +++ /dev/null @@ -1,105 +0,0 @@ -import {test, stub} from 'supertape'; -import * as storage from '#dom/storage'; - -const {stringify} = JSON; - -test('cloudcmd: client: storage: set', async (t) => { - const {localStorage} = globalThis; - const setItem = stub(); - - globalThis.localStorage = { - setItem, - }; - - await storage.set('hello', 'world'); - globalThis.localStorage = localStorage; - - t.calledWith(setItem, ['hello', 'world'], 'should call setItem'); - t.end(); -}); - -test('cloudcmd: client: storage: get', async (t) => { - const {localStorage} = globalThis; - const getItem = stub().returns('world'); - - globalThis.localStorage = { - getItem, - }; - - const result = await storage.get('hello'); - - globalThis.localStorage = localStorage; - - t.equal(result, 'world'); - t.end(); -}); - -test('cloudcmd: client: storage: getJson', async (t) => { - const {localStorage} = globalThis; - const expected = { - hello: 'world', - }; - - const getItem = stub().returns(stringify(expected)); - - globalThis.localStorage = { - getItem, - }; - - const result = await storage.getJson('hello'); - - globalThis.localStorage = localStorage; - - t.deepEqual(result, expected); - t.end(); -}); - -test('cloudcmd: client: storage: setJson', async (t) => { - const {localStorage} = globalThis; - const data = { - hello: 'world', - }; - - const expected = stringify(data); - const setItem = stub(); - - globalThis.localStorage = { - setItem, - }; - - await storage.setJson('hello', data); - globalThis.localStorage = localStorage; - - t.calledWith(setItem, ['hello', expected]); - t.end(); -}); - -test('cloudcmd: client: storage: remove', async (t) => { - const {localStorage} = globalThis; - const removeItem = stub(); - - globalThis.localStorage = { - removeItem, - }; - - await storage.remove('hello'); - globalThis.localStorage = localStorage; - - t.calledWith(removeItem, ['hello'], 'should call removeItem'); - t.end(); -}); - -test('cloudcmd: client: storage: clear', async (t) => { - const {localStorage} = globalThis; - const clear = stub(); - - globalThis.localStorage = { - clear, - }; - - await storage.clear(); - globalThis.localStorage = localStorage; - - t.calledWithNoArgs(clear, 'should call clear'); - t.end(); -}); diff --git a/client/dom/upload-files.js b/client/dom/upload-files.js index 1c37fe1b..8b0e8b7a 100644 --- a/client/dom/upload-files.js +++ b/client/dom/upload-files.js @@ -1,19 +1,25 @@ +'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 loadFile = wraptile(_loadFile); +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); -export const uploadFiles = (dir, files) => { +const {getCurrentDirPath: getPathWhenRootEmpty} = require('.'); + +module.exports = (dir, files) => { if (!files) { files = dir; - dir = getCurrentDirPath(); + dir = getPathWhenRootEmpty(); } const n = files.length; @@ -49,8 +55,7 @@ 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) => { @@ -65,3 +70,4 @@ 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 ea90f366..747cae9d 100644 --- a/client/get-json-from-file-table.js +++ b/client/get-json-from-file-table.js @@ -1,13 +1,40 @@ +'use strict'; + /* global DOM */ + +const Info = DOM.CurrentInfo; + /** * Функция генерирует JSON из html-таблицы файлов и * используеться при первом заходе в корень */ -export const getJsonFromFileTable = () => { - const Info = DOM.CurrentInfo; +module.exports = () => { const path = DOM.getCurrentDirPath(); const infoFiles = Info.files || []; + const notParent = (current) => { + const name = DOM.getCurrentName(current); + return name !== '..'; + }; + + const parse = (current) => { + const name = DOM.getCurrentName(current); + const size = DOM.getCurrentSize(current); + const owner = DOM.getCurrentOwner(current); + const mode = DOM.getCurrentMode(current); + const date = DOM.getCurrentDate(current); + const type = DOM.getCurrentType(current); + + return { + name, + size, + mode, + owner, + date, + type, + }; + }; + const files = infoFiles .filter(notParent) .map(parse); @@ -20,25 +47,3 @@ export const getJsonFromFileTable = () => { 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 deleted file mode 100644 index 6aec97ea..00000000 --- a/client/key/binder.js +++ /dev/null @@ -1,15 +0,0 @@ -export const createBinder = () => { - let binded = false; - - return { - isBind() { - return binded; - }, - setBind() { - binded = true; - }, - unsetBind() { - binded = false; - }, - }; -}; diff --git a/client/key/binder.spec.js b/client/key/binder.spec.js deleted file mode 100644 index f9d54fdd..00000000 --- a/client/key/binder.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -import {test} from 'supertape'; -import {createBinder} from './binder.js'; - -test('client: key: binder: isBind: default', (t) => { - const binder = createBinder(); - - t.notOk(binder.isBind(), 'should not be bind by default'); - t.end(); -}); - -test('client: key: binder: setBind', (t) => { - const binder = createBinder(); - - binder.setBind(); - - t.ok(binder.isBind(), 'should be bind'); - t.end(); -}); - -test('client: key: binder: unsetBind', (t) => { - const binder = createBinder(); - - binder.setBind(); - binder.unsetBind(); - - t.notOk(binder.isBind(), 'should not be bind'); - t.end(); -}); diff --git a/client/key/index.js b/client/key/index.js index 283a7f11..d3578e30 100644 --- a/client/key/index.js +++ b/client/key/index.js @@ -1,544 +1,500 @@ /* 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'; +'use strict'; + +const Info = DOM.CurrentInfo; + +const exec = require('execon'); +const clipboard = require('@cloudcmd/clipboard'); + +const Events = require('../dom/events'); +const Buffer = require('../dom/buffer'); +const KEY = require('./key'); +const vim = require('./vim'); +const setCurrentByChar = require('./set-current-by-char'); +const fullstore = require('fullstore'); const Chars = fullstore(); -const toggleVim = (keyCode, overrides = {}) => { - const {_config, config} = overrides; - - if (!config('vim') && keyCode === KEY.ESC) - _config('vim', true); -}; - -const isUndefined = (a) => typeof a === 'undefined'; - Chars([]); -const {assign} = Object; -const binder = createBinder(); +KeyProto.prototype = KEY; +CloudCmd.Key = KeyProto; +const {loadDir} = CloudCmd; -const bind = () => { - Events.addKey(listener, true); - binder.setBind(); -}; - -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; +function KeyProto() { + let Binded; - const char = key || fromCharCode(keyIdentifier); - const symbol = getSymbol(shift, keyCode); + const Key = this; - return [symbol, char]; -} - -async function listener(event, overrides = {}) { - const { - config = CloudCmd.config, - _config = CloudCmd._config, - switchKey = _switchKey, - vim = _vim, - } = overrides; + this.isBind = () => { + return Binded; + }; - const {keyCode} = event; + this.setBind = () => { + Binded = true; + }; - // strange chrome bug calls listener twice - // in second time event misses a lot fields - if (isUndefined(event.altKey)) - return; + this.unsetBind = () => { + Binded = false; + }; - const alt = event.altKey; - const ctrl = event.ctrlKey; - const meta = event.metaKey; - const isBetween = keyCode >= KEY.ZERO && keyCode <= KEY.Z; - const isNumpad = /Numpad/.test(event.code); + this.bind = () => { + Events.addKey(listener, true); + Binded = true; + }; - const [symbol, char] = getChar(event); - - if (!binder.isBind()) - return; - - toggleVim(keyCode, { - config, - _config, - }); - - const isVim = config('vim'); - - if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || symbol)) - return setCurrentByChar(char, Chars); - - Chars([]); - await switchKey(event); - - if (keyCode >= KEY.F1 && keyCode <= KEY.F10) - return; - - if (isVim) - vim(char, event); -} - -function getSymbol(shift, keyCode) { - switch(keyCode) { - case KEY.DOT: - return '.'; - - case KEY.HYPHEN: - return shift ? '_' : '-'; - - case KEY.EQUAL: - return shift ? '+' : '='; + function getChar(event) { + /* + * event.keyIdentifier deprecated in chrome v51 + * but event.key is absent in chrome <= v51 + */ + + if (event.key) + return event.key; + + return fromCharCode(event.keyIdentifier); } - return ''; -} - -function fromCharCode(keyIdentifier) { - const code = keyIdentifier.substring(2); - const hex = parseInt(code, 16); - - return String.fromCharCode(hex); -} - -async function _switchKey(event) { - const Info = DOM.CurrentInfo; - let i; - let isSelected; - let prev; - let next; - let current = Info.element; - let dataName; - - const { - name, - panel, - path, - isDir, - } = Info; - - const { - Operation, - changeDir, - config, - } = CloudCmd; - - const {keyCode} = event; - - const alt = event.altKey; - const shift = event.shiftKey; - const ctrl = event.ctrlKey; - const meta = event.metaKey; - const ctrlMeta = ctrl || meta; - - if (current) { - prev = current.previousSibling; - next = current.nextSibling; + async function listener(event) { + const {keyCode} = event; + + // strange chrome bug calles listener twice + // in second time event misses a lot fields + if (typeof event.altKey === 'undefined') + return; + + const alt = event.altKey; + const ctrl = event.ctrlKey; + const shift = event.shiftKey; + const meta = event.metaKey; + const isBetween = keyCode >= KEY.ZERO && keyCode <= KEY.Z; + const isNumpad = /Numpad/.test(event.code); + + let char = getChar(event); + let isSymbol = ~['.', '_', '-', '+', '='].indexOf(char); + + if (!isSymbol) { + isSymbol = getSymbol(shift, keyCode); + + if (isSymbol) + char = isSymbol; + } + + if (!Key.isBind()) + return; + + const isVim = CloudCmd.config('vim'); + + if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || isSymbol)) + return setCurrentByChar(char, Chars); + + Chars([]); + await switchKey(event); + + if (keyCode >= KEY.F1 && keyCode <= KEY.F10) + return; + + if (isVim) + vim(char, event); } - switch(keyCode) { - case KEY.TAB: - DOM.changePanel(); - event.preventDefault(); - break; - - case KEY.INSERT: - DOM - .toggleSelectedFile(current) - .setCurrentFile(next); - break; - - case KEY.INSERT_MAC: - DOM - .toggleSelectedFile(current) - .setCurrentFile(next); - break; - - case KEY.DELETE: - if (shift) - Operation.show('delete:silent'); - else - Operation.show('delete'); + function getSymbol(shift, keyCode) { + switch(keyCode) { + case KEY.DOT: + return '.'; - break; - - case KEY.ASTERISK: - DOM.toggleAllSelectedFiles(current); - break; - - case KEY.PLUS: - DOM.expandSelection(); - event.preventDefault(); - break; - - case KEY.MINUS: - DOM.shrinkSelection(); - event.preventDefault(); - break; - - case KEY.F1: - CloudCmd.Help.show(); - event.preventDefault(); - break; - - case KEY.F2: - CloudCmd.UserMenu.show(); - break; - - case KEY.F3: - event.preventDefault(); + case KEY.HYPHEN: + return shift ? '_' : '-'; - if (Info.isDir) - await changeDir(path); - else if (shift) - CloudCmd.View.show(null, { - raw: true, - }); - else if (ctrlMeta) - CloudCmd.sortPanel('name'); - else - CloudCmd.View.show(); - - break; + case KEY.EQUAL: + return shift ? '+' : '='; + } + } - case KEY.F4: - if (config('vim')) - CloudCmd.EditFileVim.show(); - else - CloudCmd.EditFile.show(); + function fromCharCode(keyIdentifier) { + const code = keyIdentifier.substring(2); + const hex = parseInt(code, 16); + const char = String.fromCharCode(hex); - event.preventDefault(); - break; + return char; + } - case KEY.F5: - if (ctrlMeta) - CloudCmd.sortPanel('date'); - else if (alt) - Operation.show('pack'); - else - Operation.show('copy'); + async function switchKey(event) { + let i; + let isSelected; + let prev; + let next; + let current = Info.element; + let dataName; - event.preventDefault(); - break; - - case KEY.F6: - if (ctrlMeta) - CloudCmd.sortPanel('size'); - else if (shift) - DOM.renameCurrent(current); - else - Operation.show('move'); + const { + name, + panel, + path, + isDir, + } = Info; - event.preventDefault(); - break; - - case KEY.F7: - if (shift) - DOM.promptNewFile(); - else - DOM.promptNewDir(); + const {Operation} = CloudCmd; + const {keyCode} = event; - event.preventDefault(); - break; - - case KEY.F8: - Operation.show('delete'); - event.preventDefault(); - break; - - case KEY.F9: - if (alt) - Operation.show('extract'); - else - CloudCmd.Menu.show(); + const alt = event.altKey; + const shift = event.shiftKey; + const ctrl = event.ctrlKey; + const meta = event.metaKey; + const ctrlMeta = ctrl || meta; - event.preventDefault(); - break; - - case KEY.F10: - CloudCmd.Config.show(); - event.preventDefault(); - break; - - case KEY.TRA: - event.preventDefault(); + if (current) { + prev = current.previousSibling; + next = current.nextSibling; + } - if (shift) - return CloudCmd.Terminal.show(); - - CloudCmd.Konsole.show(); - break; - - case KEY.BRACKET_CLOSE: - CloudCmd.Konsole.show(); - event.preventDefault(); - break; - - case KEY.SPACE: - event.preventDefault(); - - if (!isDir || name === '..') - isSelected = true; - else - isSelected = DOM.isSelected(current); - - if (!isSelected) - await DOM.loadCurrentSize(current); - - DOM.toggleSelectedFile(current); - - break; - - case KEY.U: - if (ctrlMeta) { - DOM.swapPanels(); + switch(keyCode) { + case Key.TAB: + DOM.changePanel(); event.preventDefault(); - } + break; - break; - - /* navigation on file table: * - * in case of pressing button 'up', * - * select previous row */ - case KEY.UP: - if (shift) - DOM.toggleSelectedFile(current); + case Key.INSERT: + DOM .toggleSelectedFile(current) + .setCurrentFile(next); + break; - DOM.setCurrentFile(prev); - event.preventDefault(); - break; - - /* in case of pressing button 'down', * - * select next row */ - case KEY.DOWN: - if (shift) - DOM.toggleSelectedFile(current); + case Key.INSERT_MAC: + DOM .toggleSelectedFile(current) + .setCurrentFile(next); + break; - DOM.setCurrentFile(next); - event.preventDefault(); - break; - - case KEY.LEFT: - if (!alt) - return; - - event.preventDefault(); - - dataName = Info.panel.getAttribute('data-name'); - - if (dataName === 'js-right') - DOM.duplicatePanel(); - - break; - - case KEY.RIGHT: - if (!alt) - return; - - event.preventDefault(); - - dataName = Info.panel.getAttribute('data-name'); - - if (dataName === 'js-left') - DOM.duplicatePanel(); - - break; - - /* in case of pressing button 'Home', * - * go to top element */ - case KEY.HOME: - DOM.setCurrentFile(Info.first); - event.preventDefault(); - break; - - /* in case of pressing button 'End', select last element */ - case KEY.END: - DOM.setCurrentFile(Info.last); - event.preventDefault(); - break; - - /* если нажали клавишу page down проматываем экран */ - case KEY.PAGE_DOWN: - DOM.scrollByPages(panel, 1); - - for (i = 0; i < 30; i++) { - if (!current.nextSibling) - break; - - current = current.nextSibling; - } - - DOM.setCurrentFile(current); - event.preventDefault(); - break; - - /* если нажали клавишу page up проматываем экран */ - case KEY.PAGE_UP: - DOM.scrollByPages(panel, -1); - - for (i = 0; i < 30; i++) { - if (!current.previousSibling) - break; - - current = current.previousSibling; - } - - DOM.setCurrentFile(current); - event.preventDefault(); - break; - - case KEY.ENTER: - if (Info.isDir) - await changeDir(path); - else - CloudCmd.View.show(); - - break; - - case KEY.BACKSPACE: - CloudCmd.goToParentDir(); - event.preventDefault(); - break; - - case KEY.BACKSLASH: - if (ctrlMeta) - await changeDir('/'); - - break; - - case KEY.A: - if (ctrlMeta) { - DOM.selectAllFiles(); - event.preventDefault(); - } - - break; - - case KEY.G: - if (alt) { - DOM.goToDirectory(); - event.preventDefault(); - } - - break; - - case KEY.L: - if (ctrlMeta && shift) { - CloudCmd.logOut(); - event.preventDefault(); - } - - break; - - case KEY.M: - if (ctrlMeta) { - if (config('vim')) - CloudCmd.EditNamesVim.show(); + case Key.DELETE: + if (shift) + Operation.show('delete:silent'); else - CloudCmd.EditNames.show(); + Operation.show('delete'); + break; + + case Key.ASTERISK: + DOM.toggleAllSelectedFiles(current); + break; + + case Key.PLUS: + DOM.expandSelection(); + event.preventDefault(); + break; + + case Key.MINUS: + DOM.shrinkSelection(); + event.preventDefault(); + break; + + case Key.F1: + CloudCmd.Help.show(); + event.preventDefault(); + break; + + case Key.F2: + CloudCmd.UserMenu.show(); + break; + + case Key.F3: + if (Info.isDir) + await loadDir({ + path, + }); + else if (shift) + CloudCmd.Markdown.show(path); + else if (ctrlMeta) + CloudCmd.sortPanel('name'); + else + CloudCmd.View.show(); event.preventDefault(); - } + break; - break; - - case KEY.P: - if (!ctrlMeta) - return; - - event.preventDefault(); - clipboard - .writeText(Info.dirPath) - .catch(CloudCmd.log); - - break; - - /** - * обновляем страницу, - * загружаем содержимое каталога - * при этом данные берём всегда с - * сервера, а не из кэша - * (обновляем кэш) - */ - case KEY.R: - if (ctrlMeta) { - CloudCmd.log('reloading page...\n'); - CloudCmd.refresh(); + case Key.F4: + if (shift) + CloudCmd.EditFileVim.show(); + else + CloudCmd.EditFile.show(); + event.preventDefault(); - } + break; - 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; - - /* чистим хранилище */ - case KEY.D: - if (ctrlMeta) { - CloudCmd.log('clearing storage...'); - await DOM.Storage.clear(); - CloudCmd.log('storage cleared'); + case Key.F5: + if (ctrlMeta) + CloudCmd.sortPanel('date'); + else if (alt) + Operation.show('pack'); + else + Operation.show('copy'); + event.preventDefault(); - } + break; - break; - - case KEY.DOT: - if (meta && shift) { - const showDotFiles = !CloudCmd.config('showDotFiles'); - CloudCmd._config('showDotFiles', showDotFiles); - CloudCmd.refresh(); - await DOM.RESTful.Config.write({ - showDotFiles, + case Key.F6: + if (ctrlMeta) + CloudCmd.sortPanel('size'); + else if (shift) + DOM.renameCurrent(current); + else + Operation.show('move'); + + event.preventDefault(); + break; + + case Key.F7: + if (shift) + DOM.promptNewFile(); + else + DOM.promptNewDir(); + + event.preventDefault(); + break; + + case Key.F8: + Operation.show('delete'); + event.preventDefault(); + break; + + case Key.F9: + if (alt) + Operation.show('extract'); + else + CloudCmd.Menu.show(); + event.preventDefault(); + break; + + case Key.F10: + CloudCmd.Config.show(); + event.preventDefault(); + break; + + case Key.TRA: + event.preventDefault(); + + if (shift) + return CloudCmd.Terminal.show(); + + CloudCmd.Konsole.show(); + break; + + case KEY.BRACKET_CLOSE: + CloudCmd.Konsole.show(); + event.preventDefault(); + break; + + case Key.SPACE: + if (!isDir || name === '..') + isSelected = true; + else + isSelected = DOM.isSelected(current); + + exec.if(isSelected, () => { + DOM.toggleSelectedFile(current); + }, (callback) => { + DOM.loadCurrentSize(current, callback); }); - } + + event.preventDefault(); + break; - break; + case Key.U: + if (ctrlMeta) { + DOM.swapPanels(); + event.preventDefault(); + } + break; + + /* navigation on file table: * + * in case of pressing button 'up', * + * select previous row */ + case Key.UP: + if (shift) + DOM.toggleSelectedFile(current); + + DOM.setCurrentFile(prev); + event.preventDefault(); + break; + + /* in case of pressing button 'down', * + * select next row */ + case Key.DOWN: + if (shift) + DOM.toggleSelectedFile(current); + + DOM.setCurrentFile(next); + event.preventDefault(); + break; + + case Key.LEFT: + if (!alt) + return; + + event.preventDefault(); + + dataName = Info.panel.getAttribute('data-name'); + + if (dataName === 'js-right') + DOM.duplicatePanel(); + + break; + + case Key.RIGHT: + if (!alt) + return; + + event.preventDefault(); + + dataName = Info.panel.getAttribute('data-name'); + + if (dataName === 'js-left') + DOM.duplicatePanel(); + + break; + + /* in case of pressing button 'Home', * + * go to top element */ + case Key.HOME: + DOM.setCurrentFile(Info.first); + event.preventDefault(); + break; + + /* in case of pressing button 'End', select last element */ + case Key.END: + DOM.setCurrentFile(Info.last); + event.preventDefault(); + break; + + /* если нажали клавишу page down проматываем экран */ + case Key.PAGE_DOWN: + DOM.scrollByPages(panel, 1); + + for (i = 0; i < 30; i++) { + if (!current.nextSibling) + break; + + current = current.nextSibling; + } + + DOM.setCurrentFile(current); + event.preventDefault(); + break; + + /* если нажали клавишу page up проматываем экран */ + case Key.PAGE_UP: + DOM.scrollByPages(panel, -1); + + for (i = 0; i < 30; i++) { + if (!current.previousSibling) + break; + + current = current.previousSibling; + } + + DOM.setCurrentFile(current); + event.preventDefault(); + break; + + case Key.ENTER: + if (Info.isDir) + await loadDir({path}); + else + CloudCmd.View.show(); + break; + + case Key.BACKSPACE: + CloudCmd.goToParentDir(); + event.preventDefault(); + break; + + case Key.BACKSLASH: + if (ctrlMeta) + await loadDir({ + path: '/', + }); + break; + + case Key.A: + if (ctrlMeta) { + DOM.selectAllFiles(); + event.preventDefault(); + } + + break; + + case Key.G: + if (alt) { + DOM.goToDirectory(); + event.preventDefault(); + } + + break; + + case Key.M: + if (ctrlMeta) { + if (shift) + CloudCmd.EditNamesVim.show(); + else + CloudCmd.EditNames.show(); + + event.preventDefault(); + } + + break; + + case Key.P: + if (!ctrlMeta) + return; + + event.preventDefault(); + clipboard + .writeText(Info.dirPath) + .catch(CloudCmd.log); + + break; + /** + * обновляем страницу, + * загружаем содержимое каталога + * при этом данные берём всегда с + * сервера, а не из кэша + * (обновляем кэш) + */ + case Key.R: + if (ctrlMeta) { + CloudCmd.log('reloading page...\n'); + 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.D: + if (ctrlMeta) { + CloudCmd.log('clearing storage...'); + await DOM.Storage.clear(); + CloudCmd.log('storage cleared'); + event.preventDefault(); + } + break; + } } } + diff --git a/client/key/index.spec.js b/client/key/index.spec.js deleted file mode 100644 index bec92b31..00000000 --- a/client/key/index.spec.js +++ /dev/null @@ -1,56 +0,0 @@ -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 test = autoGlobals(supertape); -const {stub} = supertape; - -globalThis.DOM = getDOM(); -globalThis.CloudCmd = getCloudCmd(); - -test('cloudcmd: client: key: enable vim', async (t) => { - const vim = stub(); - const config = stub().returns(true); - const _config = stub(); - - const event = { - keyCode: ESC, - key: 'Escape', - altKey: false, - }; - - Key.setBind(); - - await _listener(event, { - vim, - config, - _config, - switchKey: stub(), - }); - - t.calledWith(vim, ['Escape', event]); - t.end(); -}); - -test('cloudcmd: client: key: disable vim', async (t) => { - const _config = stub(); - const config = stub(); - - const event = { - keyCode: ESC, - key: 'Escape', - altKey: false, - }; - - Key.setBind(); - await _listener(event, { - config, - _config, - switchKey: stub(), - }); - - t.calledWith(_config, ['vim', true]); - t.end(); -}); diff --git a/client/key/key.js b/client/key/key.js index 6772e34c..8db26d12 100644 --- a/client/key/key.js +++ b/client/key/key.js @@ -1,58 +1,81 @@ -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; +'use strict'; + +module.exports = { + BACKSPACE : 8, + TAB : 9, + ENTER : 13, + 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, +}; + diff --git a/client/key/set-current-by-char.js b/client/key/set-current-by-char.js index 92764f11..3fbf79cd 100644 --- a/client/key/set-current-by-char.js +++ b/client/key/set-current-by-char.js @@ -1,15 +1,18 @@ /* global DOM */ -import {escapeRegExp} from '#common/util'; -export default function setCurrentByChar(char, charStore) { - const Info = DOM.CurrentInfo; +'use strict'; + +const Info = DOM.CurrentInfo; +const {escapeRegExp} = require('../../common/util'); + +module.exports = function setCurrentByChar(char, charStore) { let firstByName; let skipCount = 0; - let set = false; + let setted = 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; @@ -28,14 +31,12 @@ export default 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) { - set = true; + setted = true; DOM.setCurrentFile(byName); - return true; } @@ -50,8 +51,9 @@ export default function setCurrentByChar(char, charStore) { .filter(not(isRoot)) .some(setCurrent); - if (!set) { + if (!setted) { DOM.setCurrentFile(firstByName); charStore([char]); } -} +}; + diff --git a/client/key/vim/find.js b/client/key/vim/find.js index e8db825a..a5f415d0 100644 --- a/client/key/vim/find.js +++ b/client/key/vim/find.js @@ -1,10 +1,12 @@ -import {fullstore} from 'fullstore'; -import limier from 'limier'; +'use strict'; + +const fullstore = require('fullstore'); +const limier = require('limier'); const searchStore = fullstore([]); const searchIndex = fullstore(0); -export const find = (value, names) => { +module.exports.find = (value, names) => { const result = limier(value, names); searchStore(result); @@ -13,7 +15,7 @@ export const find = (value, names) => { return result; }; -export const findNext = () => { +module.exports.findNext = () => { const names = searchStore(); const index = next(searchIndex(), names.length); @@ -21,7 +23,7 @@ export const findNext = () => { return names[searchIndex()]; }; -export const findPrevious = () => { +module.exports.findPrevious = () => { const names = searchStore(); const index = previous(searchIndex(), names.length); @@ -29,8 +31,8 @@ export const findPrevious = () => { return names[index]; }; -export const _next = next; -export const _previous = previous; +module.exports._next = next; +module.exports._previous = previous; function next(index, length) { if (index === length - 1) @@ -45,3 +47,4 @@ function previous(index, length) { return --index; } + diff --git a/client/key/vim/find.spec.js b/client/key/vim/find.spec.js index 191a3652..6dbe073d 100644 --- a/client/key/vim/find.spec.js +++ b/client/key/vim/find.spec.js @@ -1,14 +1,16 @@ -import test from 'supertape'; -import {getDOM} from './globals.fixture.js'; -import { +'use strict'; + +const test = require('supertape'); +const dir = './'; + +const {getDOM} = require('./globals.fixture'); + +global.DOM = getDOM(); + +const { _next, _previous, - find, - findNext, - findPrevious, -} from './find.js'; - -globalThis.DOM = getDOM(); +} = require(dir + 'find'); test('cloudcmd: client: vim: _next', (t) => { const result = _next(1, 2); @@ -17,13 +19,6 @@ test('cloudcmd: client: vim: _next', (t) => { t.end(); }); -test('cloudcmd: client: vim: _next: increment', (t) => { - const result = _next(0, 2); - - t.equal(result, 1, 'should return 1'); - t.end(); -}); - test('cloudcmd: client: vim: _previous', (t) => { const result = _previous(0, 2); @@ -31,25 +26,3 @@ test('cloudcmd: client: vim: _previous', (t) => { t.end(); }); -test('cloudcmd: client: vim: _previous: decrement', (t) => { - const result = _previous(1, 2); - - t.equal(result, 0, 'should return 0'); - t.end(); -}); - -test('cloudcmd: client: vim: findNext: after find', (t) => { - find('a', ['alpha', 'beta', 'apple']); - const result = findNext(); - - t.equal(result, 'beta', 'should return next found name'); - t.end(); -}); - -test('cloudcmd: client: vim: findPrevious: after find', (t) => { - find('a', ['alpha', 'beta', 'apple']); - const result = findPrevious(); - - t.equal(result, 'apple', 'should return previous found name'); - t.end(); -}); diff --git a/client/key/vim/globals.fixture.js b/client/key/vim/globals.fixture.js index c49894d7..b2134843 100644 --- a/client/key/vim/globals.fixture.js +++ b/client/key/vim/globals.fixture.js @@ -1,12 +1,13 @@ -const noop = () => {}; +'use strict'; -export const getDOM = () => { +module.exports.getDOM = () => { const prompt = Promise.resolve.bind(Promise); const CurrentInfo = { element: {}, files: [], }; + const noop = () => {}; const Buffer = { copy: noop, paste: noop, @@ -27,20 +28,16 @@ export const getDOM = () => { getCurrentName: noop, setCurrentByName: noop, toggleSelectedFile: noop, - prompNewDirectory: noop, - promptNewFile: noop, }; }; -export const getCloudCmd = () => { +module.exports.getCloudCmd = () => { const show = () => {}; return { - Operation: { + Operation: { show, }, - - config: noop, - _config: noop, }; }; + diff --git a/client/key/vim/index.js b/client/key/vim/index.js index 981f6f19..bc8e5b41 100644 --- a/client/key/vim/index.js +++ b/client/key/vim/index.js @@ -1,167 +1,102 @@ -import vim from './vim.js'; -import * as finder from './find.js'; -import { - setCurrent, - selectFileNotParent, -} from './set-current.js'; +'use strict'; -export default (key, event, overrides = {}) => { - const defaults = { - ...globalThis.DOM, - ...globalThis.CloudCmd, - }; - - const deps = { - ...defaults, - ...overrides, - }; - - const operations = getOperations(event, deps); - - vim(key, operations, deps); +/* 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, 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; - +const getOperations = (event) => { return { - makeFile: createMakeFile({ - promptNewFile, - preventDefault, - stopImmediatePropagation, - }), - findNext: createFindNext({ - setCurrentByName, - }), - findPrevious: createFindPrevious({ - setCurrentByName, - }), - escape: unselectFiles, - rename: () => { - event.preventDefault(); - renameCurrent(); - }, + escape: DOM.unselectFiles, remove: () => { - Operation.show('delete'); + CloudCmd.Operation.show('delete'); }, - operationCopy: () => { - event.preventDefault(); - Operation.show('copy'); - }, - operationMove: () => { - event.preventDefault(); - Operation.show('move'); - }, - - makeDirectory: () => { - event.stopImmediatePropagation(); - event.preventDefault(); - globalThis.DOM.promptNewDir(); - }, - - terminal: () => { - CloudCmd.Terminal.show(); - }, - - edit: () => { - CloudCmd.EditFileVim.show(); - }, - copy: () => { - Buffer.copy(); - unselectFiles(); + DOM.Buffer.copy(); + DOM.unselectFiles(); }, - select: () => { const current = Info.element; - toggleSelectedFile(current); + DOM.toggleSelectedFile(current); }, - - paste: Buffer.paste, - + paste: DOM.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', ''); + event.preventDefault(); + const [, value] = await Dialog.prompt('Find', ''); if (!value) return; - const names = Info.files.map(getCurrentName); + const names = Info.files.map(DOM.getCurrentName); const [result] = finder.find(value, names); - setCurrentByName(result); + DOM.setCurrentByName(result); + }, + findNext: () => { + const name = finder.findNext(); + DOM.setCurrentByName(name); + }, + findPrevious: () => { + const name = finder.findPrevious(); + DOM.setCurrentByName(name); }, }; }; -export const selectFile = selectFileNotParent; +module.exports.selectFile = selectFile; -const _createFindPrevious = (overrides = {}) => () => { - const {setCurrentByName} = overrides; - const name = finder.findPrevious(); +function selectFile(current) { + const name = DOM.getCurrentName(current); - setCurrentByName(name); -}; + if (name === '..') + return; + + DOM.selectFile(current); +} -const _createFindNext = (overrides = {}) => () => { - const {setCurrentByName} = overrides; - const name = finder.findNext(); +function setCurrent(sibling, {count, isVisual, isDelete}) { + let current = Info.element; + const select = isVisual ? selectFile : DOM.unselectFile; - setCurrentByName(name); -}; + select(current); + + 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'); +} -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 231c6577..e7048ca2 100644 --- a/client/key/vim/index.spec.js +++ b/client/key/vim/index.spec.js @@ -1,35 +1,46 @@ -import {test, stub} from 'supertape'; -import {getDOM, getCloudCmd} from './globals.fixture.js'; -import vim, {selectFile as vimSelectFile} from './index.js'; -import * as finder from './find.js'; +'use strict'; -globalThis.DOM = getDOM(); -globalThis.CloudCmd = getCloudCmd(); +const {join} = require('path'); -const {assign} = Object; -const {DOM} = globalThis; +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 {Buffer} = DOM; +const vim = require(pathVim); + test('cloudcmd: client: key: set next file: no', (t) => { - const element = {}; - const setCurrentFile = stub(); - const unselectFiles = stub(); - - const Info = { - element, + const element = { }; - vim('j', {}, { - Info, - setCurrentFile, - unselectFiles, - }); + const setCurrentFile = stub(); - t.calledWith(setCurrentFile, [element], 'should set next file'); + global.DOM.CurrentInfo.element = element; + global.DOM.setCurrentFile = setCurrentFile; + + vim('j', {}); + + t.ok(setCurrentFile.calledWith(element), 'should set next file'); t.end(); }); -test('cloudcmd: client: key: set next file current: j', async (t) => { +test('cloudcmd: client: key: set next file current', (t) => { const nextSibling = 'hello'; const element = { nextSibling, @@ -37,21 +48,17 @@ test('cloudcmd: client: key: set next file current: j', async (t) => { const setCurrentFile = stub(); - const Info = { - element, - }; + global.DOM.CurrentInfo.element = element; + global.DOM.setCurrentFile = setCurrentFile; - await vim('j', {}, { - Info, - setCurrentFile, - unselectFiles: stub(), - }); + vim('j', {}); + + t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file'); - t.calledWith(setCurrentFile, [nextSibling], 'should set next file'); t.end(); }); -test('cloudcmd: client: key: set next file current: mjj', (t) => { +test('cloudcmd: client: key: set next file current', (t) => { const nextSibling = 'hello'; const element = { nextSibling, @@ -59,21 +66,15 @@ test('cloudcmd: client: key: set next file current: mjj', (t) => { const setCurrentFile = stub(); - const Info = { - element, - }; + global.DOM.CurrentInfo.element = element; + global.DOM.setCurrentFile = setCurrentFile; - const deps = { - Info, - setCurrentFile, - unselectFiles: stub(), - }; + vim('m', {}); + vim('j', {}); + vim('j', {}); - vim('m', {}, deps); - vim('j', {}, deps); - vim('j', {}, deps); + t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file'); - t.calledWith(setCurrentFile, [nextSibling], 'should set next file'); t.end(); }); @@ -85,44 +86,38 @@ test('cloudcmd: client: key: set next file current: g', (t) => { const setCurrentFile = stub(); - const Info = { - element, - }; + global.DOM.CurrentInfo.element = element; + global.DOM.setCurrentFile = setCurrentFile; - const deps = { - Info, - setCurrentFile, - unselectFiles: stub(), - }; + vim('g', {}); + vim('j', {}); - vim('g', {}, deps); - vim('j', {}, deps); + t.ok(setCurrentFile.calledWith(nextSibling), 'should ignore g'); - t.calledWith(setCurrentFile, [nextSibling], 'should ignore g'); t.end(); }); 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, - }; - - const deps = { - setCurrentFile, - Info, - unselectFiles: stub(), - }; + global.DOM.CurrentInfo.element = element; + global.DOM.setCurrentFile = setCurrentFile; const event = {}; - vim('2', event, deps); - vim('j', event, deps); + vim('2', event); + vim('j', event); + + t.ok(setCurrentFile.calledWith(last), 'should set next file'); - t.calledWith(setCurrentFile, [last], 'should set next file'); t.end(); }); @@ -131,39 +126,25 @@ test('cloudcmd: client: key: select +2 files from current before delete', (t) => const nextSibling = { nextSibling: last, }; - const element = { nextSibling, }; const setCurrentFile = stub(); - const Info = { - element, - }; - - const Operation = { - show: stub(), - }; - - const selectFile = stub(); - const getCurrentName = stub().returns('x'); + global.DOM.CurrentInfo.element = element; + global.DOM.setCurrentFile = setCurrentFile; + global.DOM.selectFile = stub(); + global.DOM.getCurrentName = () => false; + global.CloudCmd.Operation.show = stub(); const event = {}; - const deps = { - Info, - setCurrentFile, - selectFile, - getCurrentName, - Operation, - }; + vim('d', event); + vim('2', event); + vim('j', event); - vim('d', event, deps); - vim('2', event, deps); - vim('j', event, deps); - - t.calledWith(setCurrentFile, [last], 'should set next file'); + t.ok(setCurrentFile.calledWith(last), 'should set next file'); t.end(); }); @@ -172,7 +153,6 @@ test('cloudcmd: client: key: delete +2 files from current', (t) => { const nextSibling = { nextSibling: last, }; - const element = { nextSibling, }; @@ -180,26 +160,20 @@ test('cloudcmd: client: key: delete +2 files from current', (t) => { const setCurrentFile = stub(); const show = stub(); - const deps = { - Info: { - element, - }, - Operation: { - show, - }, - setCurrentFile, - selectFile: stub(), - getCurrentName: stub().returns('x'), - unselectFiles: stub(), - }; + global.DOM.CurrentInfo.element = element; + global.DOM.setCurrentFile = setCurrentFile; + global.DOM.selectFile = stub(); + global.DOM.getCurrentName = () => false; + global.CloudCmd.Operation.show = show; const event = {}; - vim('d', event, deps); - vim('2', event, deps); - vim('j', event, deps); + vim('d', event); + vim('2', event); + vim('j', event); + + t.ok(show.calledWith('delete'), 'should call delete'); - t.calledWith(show, ['delete'], 'should call delete'); t.end(); }); @@ -210,84 +184,49 @@ test('cloudcmd: client: key: set previous file current', (t) => { }; const setCurrentFile = stub(); - const unselectFiles = stub(); - const Info = { - element, - }; + global.DOM.CurrentInfo.element = element; + global.DOM.setCurrentFile = setCurrentFile; - const deps = { - Info, - setCurrentFile, - unselectFiles, - }; + vim('k', {}); - vim('k', {}, deps); - - t.calledWith(setCurrentFile, [previousSibling], 'should set previous file'); + t.ok(setCurrentFile.calledWith(previousSibling), 'should set previous file'); t.end(); }); test('cloudcmd: client: key: copy: no', (t) => { const copy = stub(); - vim('y', {}, { - unselectFiles: stub(), - Buffer: { - copy, - }, - }); + Buffer.copy = copy; + + vim('y', {}); + + 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: {}, - }; - const toggleSelectedFile = stub(); - const unselectFiles = stub(); + Buffer.copy = copy; - const deps = { - Info, - unselectFiles, - toggleSelectedFile, - Buffer: { - copy, - }, - }; + vim('v', {}); + vim('y', {}); - vim('v', {}, deps); - vim('y', {}, deps); - - t.calledWithNoArgs(copy, 'should copy files'); + t.ok(copy.calledWith(), 'should copy files'); t.end(); }); test('cloudcmd: client: key: copy: unselectFiles', (t) => { const unselectFiles = stub(); - const Info = { - element: {}, - }; - const toggleSelectedFile = stub(); + DOM.unselectFiles = unselectFiles; - const deps = { - Info, - unselectFiles, - toggleSelectedFile, - Buffer: { - copy: stub(), - }, - }; + vim('v', {}); + vim('y', {}); - vim('v', {}, deps); - vim('y', {}, deps); - - t.calledWithNoArgs(unselectFiles, 'should unselect files'); + t.ok(unselectFiles.calledWith(), 'should unselect files'); t.end(); }); @@ -296,91 +235,63 @@ test('cloudcmd: client: key: paste', (t) => { Buffer.paste = paste; - vim('p', {}, { - Buffer, - }); + vim('p', {}); + + 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); - vimSelectFile(current, { - selectFile, - getCurrentName, - }); - - t.notCalled(selectFile, 'should not call selectFile'); + t.notOk(getCurrentName.called, 'should not call selectFile'); t.end(); }); test('cloudcmd: client: key: selectFile', (t) => { const selectFile = stub(); - const getCurrentName = stub().returns('x'); + + DOM.selectFile = selectFile; + DOM.getCurrentName = (a) => a.name; + const current = {}; - vimSelectFile(current, { - selectFile, - getCurrentName, - }); + vim.selectFile(current); - t.calledWith(selectFile, [current], 'should call selectFile'); + t.ok(selectFile.calledWith(current), 'should call selectFile'); t.end(); }); -test('cloudcmd: client: key: set last file current: shift + g', async (t) => { +test('cloudcmd: client: key: set last file current', (t) => { const last = 'last'; const nextSibling = { nextSibling: last, }; - const element = { nextSibling, }; const setCurrentFile = stub(); - await vim('G', {}, { - Info: { - element, - }, - setCurrentFile, - unselectFiles: stub(), - }); + global.DOM.CurrentInfo.element = element; + global.DOM.setCurrentFile = setCurrentFile; + + vim('G', {}); + + t.ok(setCurrentFile.calledWith(last), 'should set last file'); - t.calledWith(setCurrentFile, [last], 'should set last file'); t.end(); }); -test('cloudcmd: client: key: set last file current: $', (t) => { - const last = 'last'; - const nextSibling = { - nextSibling: last, - }; - - const element = { - nextSibling, - }; - - const setCurrentFile = stub(); - - vim('$', {}, { - Info: { - element, - }, - setCurrentFile, - unselectFiles: stub(), - }); - - t.calledWith(setCurrentFile, [last], 'should set last file'); - t.end(); -}); - -test('cloudcmd: client: key: set first file current: gg', (t) => { +test('cloudcmd: client: key: set first file current', (t) => { const first = 'first'; const previousSibling = { previousSibling: first, @@ -390,121 +301,66 @@ test('cloudcmd: client: key: set first file current: gg', (t) => { previousSibling, }; - const Operation = { - show: stub(), - }; - - const unselectFiles = stub(); const setCurrentFile = stub(); - const deps = { - Operation, - unselectFiles, - setCurrentFile, - Info: { - element, - }, - }; + global.DOM.CurrentInfo.element = element; + global.DOM.setCurrentFile = setCurrentFile; - vim('g', {}, deps); - vim('g', {}, deps); + vim('g', {}); + vim('g', {}); - t.calledWith(setCurrentFile, [first], 'should set first file'); - t.end(); -}); - -test('cloudcmd: client: key: set first file current: ^', async (t) => { - const first = 'first'; - const previousSibling = { - previousSibling: first, - }; + t.ok(setCurrentFile.calledWith(first), 'should set first file'); - const element = { - previousSibling, - }; - - const Operation = { - show: stub(), - }; - - const unselectFiles = stub(); - const setCurrentFile = stub(); - - const deps = { - setCurrentFile, - Info: { - element, - }, - unselectFiles, - Operation, - }; - - await vim('^', {}, deps); - - t.calledWith(setCurrentFile, [first], 'should set first file'); t.end(); }); test('cloudcmd: client: key: visual', (t) => { - const element = {}; - const toggleSelectedFile = stub(); - - const Info = { - element, + const element = { }; - vim('v', {}, { - Info, - toggleSelectedFile, - }); + const toggleSelectedFile = stub(); + + global.DOM.CurrentInfo.element = element; + global.DOM.toggleSelectedFile = toggleSelectedFile; + + vim('v', {}); + + t.ok(toggleSelectedFile.calledWith(element), 'should toggle selection'); - t.calledWith(toggleSelectedFile, [element], 'should toggle selection'); t.end(); }); test('cloudcmd: client: key: ESC', (t) => { - const element = {}; - const unselectFiles = stub(); - - const Info = { - element, + const element = { }; - vim('Escape', null, { - Info, - unselectFiles, - }); + const unselectFiles = stub(); - t.calledWithNoArgs(unselectFiles, 'should toggle selection'); + global.DOM.CurrentInfo.element = element; + global.DOM.unselectFiles = unselectFiles ; + + vim('Escape'); + + t.ok(unselectFiles.calledWith(), 'should toggle selection'); t.end(); }); -test('cloudcmd: client: key: Enter', async (t) => { +test('cloudcmd: client: key: Enter', (t) => { const nextSibling = 'hello'; const element = { nextSibling, }; - const unselectFiles = stub(); const setCurrentFile = stub(); - const Info = { - element, - }; + DOM.CurrentInfo.element = element; + DOM.setCurrentFile = setCurrentFile; - await vim('Enter', null, { - Info, - setCurrentFile, - unselectFiles, - }); + vim('Enter'); - await vim('j', null, { - Info, - setCurrentFile, - unselectFiles, - }); + vim('j'); - t.calledWith(setCurrentFile, [nextSibling], 'should set next file'); + t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file'); t.end(); }); @@ -512,245 +368,50 @@ test('cloudcmd: client: key: /', (t) => { const preventDefault = stub(); const element = {}; - const Info = { - element, - files: [], - }; + DOM.CurrentInfo.element = element; + DOM.getCurrentName = () => ''; - const getCurrentName = stub().returns(''); - - const event = { + vim('/', { preventDefault, - }; - - const prompt = stub().returns([]); - - vim('/', event, { - getCurrentName, - Info, - prompt, }); - 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.ok(preventDefault.calledWith(), 'should call preventDefault'); t.end(); }); test('cloudcmd: client: key: n', (t) => { const findNext = stub(); - const createFindNext = stub().returns(findNext); - const event = {}; - - vim('n', event, { - createFindNext, + mockRequire(pathFind, { + findNext, }); - t.calledWithNoArgs(findNext, 'should call findNext'); + const vim = reRequire(pathVim); + const event = {}; + + vim('n', event); + + stopAll(pathFind); + + t.ok(findNext.calledWith(), 'should call findNext'); t.end(); }); test('cloudcmd: client: key: N', (t) => { const findPrevious = stub(); - const createFindPrevious = stub().returns(findPrevious); + + mockRequire(pathFind, { + findPrevious, + }); + + const vim = reRequire(dir + 'vim'); const event = {}; - vim('N', event, { - createFindPrevious, - }); + vim('N', event); - t.calledWithNoArgs(findPrevious); + stopAll(pathFind); + + t.ok(findPrevious.calledWith(), 'should call findPrevious'); t.end(); }); -test('cloudcmd: client: key: make directory', async (t) => { - const {DOM} = globalThis; - - assign(DOM, { - promptNewDir: stub(), - }); - - const event = { - stopImmediatePropagation: stub(), - preventDefault: stub(), - }; - - await vim('m', event); - await vim('d', event); - - t.calledWithNoArgs(DOM.promptNewDir); - t.end(); -}); - -test('cloudcmd: client: key: make file', (t) => { - const promptNewFile = stub(); - - const event = { - stopImmediatePropagation: stub(), - preventDefault: stub(), - }; - - vim('m', event); - vim('f', event, { - promptNewFile, - }); - - t.calledWithNoArgs(promptNewFile); - t.end(); -}); - -test('cloudcmd: client: vim: terminal', (t) => { - const CloudCmd = { - Terminal: { - show: stub(), - }, - }; - - const 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 = {}; - - await vim('e', event); - - 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(); -}); - -test('cloudcmd: client: key: cc: operationCopy', (t) => { - const show = stub(); - const preventDefault = stub(); - - const Operation = { - show, - }; - - const event = { - preventDefault, - }; - - vim('c', event, { - Operation, - }); - - vim('c', event, { - Operation, - }); - - t.calledWith(show, ['copy'], 'should show copy operation'); - t.end(); -}); - -test('cloudcmd: client: key: mm: operationMove', (t) => { - const show = stub(); - const preventDefault = stub(); - - const Operation = { - show, - }; - - const event = { - preventDefault, - }; - - vim('m', event, { - Operation, - }); - - vim('m', event, { - Operation, - }); - - t.calledWith(show, ['move'], 'should show move operation'); - t.end(); -}); - -test('cloudcmd: client: key: n: findNext: real', (t) => { - const setCurrentByName = stub(); - - finder.find('a', ['alpha', 'beta', 'apple']); - - const event = {}; - - vim('n', event, { - setCurrentByName, - }); - - t.calledWith(setCurrentByName, ['beta'], 'should set current by next found name'); - t.end(); -}); - -test('cloudcmd: client: key: N: findPrevious: real', (t) => { - const setCurrentByName = stub(); - - finder.find('a', ['alpha', 'beta', 'apple']); - - const event = {}; - - vim('N', event, { - setCurrentByName, - }); - - t.calledWith(setCurrentByName, ['apple'], 'should set current by previous found name'); - t.end(); -}); diff --git a/client/key/vim/set-current.js b/client/key/vim/set-current.js deleted file mode 100644 index a2fc8646..00000000 --- a/client/key/vim/set-current.js +++ /dev/null @@ -1,33 +0,0 @@ -/* 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 95a27caf..cee0293a 100644 --- a/client/key/vim/vim.js +++ b/client/key/vim/vim.js @@ -1,5 +1,6 @@ -import {fullstore} from 'fullstore'; +'use strict'; +const fullstore = require('fullstore'); const store = fullstore(''); const visual = fullstore(false); @@ -20,11 +21,10 @@ const rmFirst = (a) => { const noop = () => {}; -export default (key, operations = {}) => { +module.exports = (key, operations) => { const prevStore = store(); const isVisual = visual(); const value = store(prevStore.concat(key)); - const { escape = noop, moveNext = noop, @@ -36,13 +36,6 @@ export default (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') @@ -51,11 +44,10 @@ export default (key, operations = {}) => { if (key === 'Escape') { visual(false); escape(); - return end(); } - if (key === 'j' || key === 'w') { + if (key === 'j') { const { count, isDelete, @@ -71,7 +63,7 @@ export default (key, operations = {}) => { return end(); } - if (key === 'k' || key === 'b') { + if (key === 'k') { const { count, isDelete, @@ -87,8 +79,11 @@ export default (key, operations = {}) => { return end(); } - if (value === 'gg' || key === '^') { - const {isDelete, isVisual} = handleDelete(prevStore); + if (/^gg$/.test(value)) { + const { + isDelete, + isVisual, + } = handleDelete(prevStore); movePrevious({ count: Infinity, @@ -99,49 +94,13 @@ export default (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(); } - if (key === 'G' || key === '$') { + if (key === 'G') { moveNext({ count: Infinity, isVisual, @@ -156,7 +115,6 @@ export default (key, operations = {}) => { stopVisual(); copy(); - return end(); } @@ -168,7 +126,6 @@ export default (key, operations = {}) => { if (/^v$/i.test(key)) { visual(!visual()); select(); - return end(); } @@ -186,9 +143,6 @@ export default (key, operations = {}) => { findPrevious(); return end(); } - - if (key === ' ') - return end(); }; function handleDelete(prevStore) { @@ -218,3 +172,4 @@ function getNumber(value) { return parseInt(value); } + diff --git a/client/key/vim/vim.spec.js b/client/key/vim/vim.spec.js index 02fd52d4..5355807d 100644 --- a/client/key/vim/vim.spec.js +++ b/client/key/vim/vim.spec.js @@ -1,5 +1,7 @@ -import {test, stub} from 'supertape'; -import vim from './vim.js'; +'use strict'; + +const test = require('supertape'); +const vim = require('./vim'); test('vim: no operations', (t) => { const result = vim('hello', {}); @@ -8,93 +10,3 @@ 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(); - - vim('^', { - movePrevious, - }); - - const expected = { - count: Infinity, - isVisual: false, - isDelete: false, - }; - - t.calledWith(movePrevious, [expected], 'should call movePrevious'); - 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(); - - vim('w', { - moveNext, - }); - - const expected = { - count: 1, - isVisual: false, - isDelete: false, - }; - - t.calledWith(moveNext, [expected], 'should call moveNext'); - t.end(); -}); - -test('vim: b', (t) => { - const movePrevious = stub(); - - vim('b', { - movePrevious, - }); - - const expected = { - count: 1, - isVisual: false, - isDelete: false, - }; - - t.calledWith(movePrevious, [expected], 'should call movePrevious'); - t.end(); -}); diff --git a/client/listeners/get-index.js b/client/listeners/get-index.js index 45ce85e8..d7870608 100644 --- a/client/listeners/get-index.js +++ b/client/listeners/get-index.js @@ -1,10 +1,11 @@ -import currify from 'currify'; +'use strict'; -export const getIndex = currify((array, item) => { +module.exports = (array, item) => { const index = array.indexOf(item); if (!~index) return 0; return index; -}); +}; + diff --git a/client/listeners/get-range.js b/client/listeners/get-range.js index 839c2287..ef1ea0f8 100644 --- a/client/listeners/get-range.js +++ b/client/listeners/get-range.js @@ -1,4 +1,6 @@ -export const getRange = (indexFrom, indexTo, files) => { +'use strict'; + +module.exports = (indexFrom, indexTo, files) => { if (indexFrom < indexTo) return files.slice(indexFrom, indexTo + 1); @@ -7,3 +9,4 @@ export const getRange = (indexFrom, indexTo, files) => { return [files[indexFrom]]; }; + diff --git a/client/listeners/get-range.spec.js b/client/listeners/get-range.spec.js deleted file mode 100644 index 77c950f4..00000000 --- a/client/listeners/get-range.spec.js +++ /dev/null @@ -1,52 +0,0 @@ -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 result = getRange(0, 1, files); - - t.deepEqual(result, expected, 'should return range'); - t.end(); -}); - -test('cloudcmd: client: listeners: getRange: reverse', (t) => { - const expected = [ - 'hello', - 'world', - ]; - - const files = [ - ...expected, - 'how', - 'come', - ]; - - const result = getRange(1, 0, files); - - t.deepEqual(result, expected, 'should return range'); - t.end(); -}); - -test('cloudcmd: client: listeners: getRange: one', (t) => { - const expected = ['hello']; - 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 babc1e0d..cf0044bb 100644 --- a/client/listeners/index.js +++ b/client/listeners/index.js @@ -1,30 +1,36 @@ /* global DOM, CloudCmd */ -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'; + +'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'); const NBSP_REG = RegExp(String.fromCharCode(160), 'g'); const SPACE = ' '; -export async function init() { +module.exports.init = () => { contextMenu(); dragndrop(); unload(); pop(); resize(); + config(); header(); - await config(); -} +}; + +CloudCmd.Listeners = module.exports; const unselect = (event) => { - const isMac = /Mac/.test(globalThis.navigator.platform); + const isMac = /Mac/.test(window.navigator.platform); const { shiftKey, metaKey, @@ -42,9 +48,14 @@ const execAll = currify((funcs, event) => { fn(event); }); +const Info = DOM.CurrentInfo; +const {Events} = DOM; const EventsFiles = { mousedown: exec.with(execIfNotUL, setCurrentFileByEvent), - click: execAll([onClick, exec.with(execIfNotMobile, unselect)]), + click: execAll([ + onClick, + unselect, + ]), dragstart: exec.with(execIfNotUL, onDragStart), dblclick: exec.with(execIfNotUL, onDblClick), touchstart: exec.with(execIfNotUL, onTouch), @@ -55,7 +66,6 @@ let EXT; function header() { const fm = DOM.getFM(); const isDataset = (el) => el.dataset; - const isPanel = (el) => { return /^js-(left|right)$/.test(el.dataset.name); }; @@ -67,7 +77,8 @@ 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; @@ -90,58 +101,58 @@ function getPath(el, path = []) { async function config() { const [, config] = await tryToCatch(DOM.Files.get, 'config'); - const type = config?.packer; + const type = config && config.packer; EXT = DOM.getPackerExt(type); } -export const initKeysPanel = () => { +module.exports.initKeysPanel = () => { const keysElement = DOM.getById('js-keyspanel'); if (!keysElement) return; - Events.addClick(keysElement, (event) => { - const {target} = event; + Events.addClick(keysElement, ({target}) => { const {id} = target; - const operation = (name) => { const {Operation} = CloudCmd; + const fn = Operation.show.bind(null, name); - return Operation.show.bind(null, name); + return fn; }; 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': () => { - event.stopPropagation(); - CloudCmd.Menu.show(); - }, - 'f10': CloudCmd.Config.show, - '~': CloudCmd.Konsole.show, - 'shift~': CloudCmd.Terminal.show, - 'contact': CloudCmd.Contact.show, + 'f1' : CloudCmd.Help.show, + 'f2' : initF2, + '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, }; exec(clickFuncs[id]); }); }; +function initF2() { + CloudCmd.UserMenu.show(); +} + const getPanel = (side) => { if (!itype.string(side)) return side; - return DOM.getByDataName(`js-${side}`); + return DOM.getByDataName('js-' + side); }; -export const setOnPanel = (side) => { +module.exports.setOnPanel = (side) => { const panel = getPanel(side); const filesElement = DOM.getByDataName('js-files', panel); @@ -157,7 +168,6 @@ function getPathListener(panel) { } function isNoCurrent(panel) { - const Info = DOM.CurrentInfo; const infoPanel = Info.panel; if (!infoPanel) @@ -176,13 +186,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; @@ -204,26 +214,19 @@ async function onPathElementClick(panel, event) { const {href} = element; const path = decodePath(href); - await CloudCmd.changeDir(path, { + await CloudCmd.loadDir({ + 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; @@ -238,14 +241,14 @@ function onClick(event) { } function toggleSelect(key, files) { - const isMac = /Mac/.test(globalThis.navigator.platform); + const isMac = /Mac/.test(window.navigator.platform); if (!key) throw Error('key should not be undefined!'); const [file] = files; - if (isMac && key.meta) + if (isMac && key.meta || key.ctrl) return DOM.toggleSelectedFile(file); if (key.shift) @@ -253,7 +256,6 @@ 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); @@ -263,16 +265,21 @@ function changePanel(element) { } async function onDblClick(event) { - event.preventDefault(); - const current = getLIElement(event.target); const isDir = DOM.isCurrentIsDir(current); const path = DOM.getCurrentPath(current); - if (!isDir) - return CloudCmd.View.show(); - - await CloudCmd.changeDir(path); + if (isDir) { + await CloudCmd.loadDir({ + path: path === '/' ? '/' : path + '/', + }); + + event.preventDefault(); + } else { + CloudCmd.View.show(); + + event.preventDefault(); + } } async function onTouch(event) { @@ -287,7 +294,9 @@ async function onTouch(event) { if (!isCurrent) return; - await CloudCmd.changeDir(DOM.getCurrentPath(current)); + await CloudCmd.loadDir({ + path: DOM.getCurrentPath(current), + }); } /* @@ -295,7 +304,6 @@ 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; @@ -312,8 +320,8 @@ function onDragStart(event) { event.dataTransfer.setData( 'DownloadURL', - 'application/octet-stream' + ':' + name + - ':' + + 'application/octet-stream' + ':' + + name + ':' + link, ); } @@ -336,7 +344,6 @@ function getULElement(element) { } function setCurrentFileByEvent(event) { - const Info = DOM.CurrentInfo; const BUTTON_LEFT = 0; const key = { @@ -400,7 +407,10 @@ function dragndrop() { }; const onDrop = (event) => { - const {files, items} = event.dataTransfer; + const { + files, + items, + } = event.dataTransfer; const {length: filesCount} = files; @@ -410,10 +420,7 @@ 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); @@ -425,7 +432,7 @@ function dragndrop() { }; /** - * In macOS Chrome dropEffect = 'none' + * In Mac OS Chrome dropEffect = 'none' * so drop do not firing up when try * to upload file from download bar */ @@ -450,9 +457,9 @@ function dragndrop() { } function unload() { - Events.add(['unload', 'beforeunload'], (event) => { + DOM.Events.add(['unload', 'beforeunload'], (event) => { const {Key} = CloudCmd; - const isBind = Key?.isBind(); + const isBind = Key && Key.isBind(); if (isBind) return; @@ -470,8 +477,8 @@ function pop() { return CloudCmd.route(location.hash); const history = false; - - await CloudCmd.changeDir(path, { + await CloudCmd.loadDir({ + path, history, }); }); @@ -479,8 +486,7 @@ function pop() { function resize() { Events.add('resize', () => { - const Info = DOM.CurrentInfo; - const is = globalThis.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH; + const is = window.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH; if (!is) return; @@ -500,3 +506,4 @@ function resize() { DOM.changePanel(); }); } + diff --git a/client/load-module.js b/client/load-module.js index 7fc2c328..5111259c 100644 --- a/client/load-module.js +++ b/client/load-module.js @@ -1,16 +1,19 @@ -/* 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'; +'use strict'; +/* global CloudCmd */ + +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} */ -export const loadModule = (params) => { +module.exports = function loadModule(params) { if (!params) return; @@ -22,24 +25,24 @@ export const loadModule = (params) => { if (CloudCmd[name]) return; - CloudCmd[name] = async () => { + CloudCmd[name] = () => { exec(doBefore); + const {prefix} = CloudCmd; + const pathFull = prefix + CloudCmd.DIRCLIENT_MODULES + path + '.js'; - 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; + 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; + }); }; CloudCmd[name].show = async (...args) => { @@ -49,8 +52,9 @@ export const loadModule = (params) => { const [e, a] = await tryToCatch(m); if (e) - return; + return console.error(e); - return await a.show(...args); + await a.show(...args); }; }; + diff --git a/client/modules/cloud.js b/client/modules/cloud.js index cfe0d708..59c203c9 100644 --- a/client/modules/cloud.js +++ b/client/modules/cloud.js @@ -1,32 +1,31 @@ /* global CloudCmd, filepicker */ -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'; + +'use strict'; + +const exec = require('execon'); +const currify = require('currify'); +const load = require('load.js'); 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; -CloudCmd[Name] = { - init, - uploadFile, - saveFile, -}; - -export async function init() { +module.exports.init = async () => { const [modules] = await loadFiles(); const {key} = modules.data.FilePicker; filepicker.setKey(key); Images.hide(); -} +}; -export function uploadFile(filename, data) { +module.exports.uploadFile = (filename, data) => { const mimetype = ''; filepicker.store(data, { @@ -35,14 +34,17 @@ export function uploadFile(filename, data) { }, (fpFile) => { filepicker.exportFile(fpFile, log, log); }); -} +}; -export function saveFile(callback) { +module.exports.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); @@ -54,7 +56,7 @@ function _upload(callback, file) { }); } -function loadFiles() { +async function loadFiles() { const js = '//api.filepicker.io/v2/filepicker.js'; return Promise.all([ @@ -62,3 +64,4 @@ function loadFiles() { load.js(js), ]); } + diff --git a/client/modules/command-line.js b/client/modules/command-line.js deleted file mode 100644 index 7a1c3993..00000000 --- a/client/modules/command-line.js +++ /dev/null @@ -1,31 +0,0 @@ -/* 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 dfa4c105..61cb3eaa 100644 --- a/client/modules/config/index.js +++ b/client/modules/config/index.js @@ -1,23 +1,28 @@ -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'; +'use strict'; -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); @@ -41,25 +46,28 @@ let Template; const loadCSS = load.css; -export async function init() { +module.exports.init = async () => { if (!CloudCmd.config('configDialog')) return; showLoad(); - const {DIR_DIST} = CloudCmd; + const {prefix} = CloudCmd; [Template] = await Promise.all([ Files.get('config-tmpl'), loadSocket(), - loadCSS(`${DIR_DIST}/config.css`), + loadCSS(prefix + '/dist/config.css'), CloudCmd.View(), ]); initSocket(); -} +}; -const {config, Key} = CloudCmd; +const { + config, + Key, +} = CloudCmd; let Element; @@ -69,20 +77,24 @@ function getHost() { origin, protocol, } = location; + const href = origin || `${protocol}//${host}`; - return origin || `${protocol}//${host}`; + return href; } function initSocket() { const href = getHost(); - const {prefixSocket, prefix} = CloudCmd; + const { + prefixSocket, + prefix, + } = CloudCmd; const ONE_MINUTE = 60 * 1000; - const socket = globalThis.io.connect(href + prefixSocket + '/config', { + const socket = io.connect(href + prefixSocket + '/config', { reconnectionAttempts: Infinity, reconnectionDelay: ONE_MINUTE, - path: `${prefix}/socket.io`, + path: prefix + '/socket.io', }); const save = (data) => { @@ -113,7 +125,9 @@ function authCheck(socket) { Config.save = saveHttp; -export async function show() { +module.exports.show = show; + +async function show() { if (!CloudCmd.config('configDialog')) return; @@ -128,27 +142,21 @@ async function fillTemplate() { const { editor, - menu, packer, columns, - theme, configAuth, - configPort, ...obj } = input.convert(config); - obj[`${menu}-selected`] = 'selected'; - obj[`${editor}-selected`] = 'selected'; - obj[`${packer}-selected`] = 'selected'; - obj[`${columns}-selected`] = 'selected'; - obj[`${theme}-selected`] = 'selected'; + obj[editor + '-selected'] = 'selected'; + obj[packer + '-selected'] = 'selected'; + obj[columns + '-selected'] = 'selected'; obj.configAuth = configAuth ? '' : 'hidden'; - obj.configPort = configPort ? '' : 'hidden'; const innerHTML = rendy(Template, obj); Element = createElement('form', { - className: 'config', + className : 'config', innerHTML, }); @@ -165,20 +173,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, }); } -export function hide() { +module.exports.hide = hide; + +function hide() { CloudCmd.View.hide(); } @@ -220,7 +228,7 @@ function onAuthChange(checked) { const elUsername = input.getElementByName('username', Element); const elPassword = input.getElementByName('password', Element); - elUsername.disabled = !checked; + elUsername.disabled = elPassword.disabled = !checked; } @@ -230,18 +238,13 @@ function onNameChange(name) { })); } -async function onKey({keyCode, target}) { +function onKey({keyCode, target}) { switch(keyCode) { case Key.ESC: return hide(); case Key.ENTER: - return await onChange(target); + return onChange(target); } } -CloudCmd[Name] = { - init, - show, - hide, -}; diff --git a/client/modules/config/input.js b/client/modules/config/input.js index 34a4be69..013908d2 100644 --- a/client/modules/config/input.js +++ b/client/modules/config/input.js @@ -1,17 +1,23 @@ -import {encode} from '#common/entity'; +'use strict'; -const isBool = (a) => typeof a === 'boolean'; -const isString = (a) => typeof a === 'string'; +const currify = require('currify'); -const {keys} = Object; +const isType = currify((type, object, name) => { + return typeof object[name] === type; +}); -export function getElementByName(selector, element) { +const isBool = isType('boolean'); + +module.exports.getElementByName = getElementByName; + +function getElementByName(selector, element) { const str = `[data-name="js-${selector}"]`; - return element.querySelector(str); + return element + .querySelector(str); } -export const getName = (element) => { +module.exports.getName = (element) => { const name = element .getAttribute('data-name') .replace(/^js-/, ''); @@ -19,21 +25,17 @@ export const getName = (element) => { return name; }; -export const convert = (config) => { - const result = config; +module.exports.convert = (config) => { + const result = { + ...config, + }; + const array = Object.keys(config); - for (const name of keys(config)) { + const filtered = array.filter(isBool(config)); + + for (const name of filtered) { const item = config[name]; - - if (isBool(item)) { - result[name] = setState(item); - continue; - } - - if (isString(item)) { - result[name] = encode(item); - continue; - } + result[name] = setState(item); } return result; @@ -46,7 +48,7 @@ function setState(state) { return ''; } -export const getValue = (name, element) => { +module.exports.getValue = (name, element) => { const el = getElementByName(name, element); const {type} = el; @@ -62,7 +64,7 @@ export const getValue = (name, element) => { } }; -export const setValue = (name, value, element) => { +module.exports.setValue = (name, value, element) => { const el = getElementByName(name, element); const {type} = el; @@ -76,3 +78,4 @@ export const setValue = (name, value, element) => { break; } }; + diff --git a/client/modules/config/input.spec.js b/client/modules/config/input.spec.js deleted file mode 100644 index 9d0866cd..00000000 --- a/client/modules/config/input.spec.js +++ /dev/null @@ -1,140 +0,0 @@ -import {test, stub} from 'supertape'; -import { - convert, - getName, - getValue, - setValue, -} 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(); -}); - -test('cloudcmd: client: config: input: convert: bool', (t) => { - const result = convert({ - auth: true, - }); - - const expected = { - auth: ' checked', - }; - - t.deepEqual(result, expected); - t.end(); -}); - -test('cloudcmd: client: config: input: convert: bool false', (t) => { - const result = convert({ - auth: false, - }); - - const expected = { - auth: '', - }; - - t.deepEqual(result, expected); - t.end(); -}); - -test('cloudcmd: client: config: input: getName', (t) => { - const getAttribute = stub().returns('js-hello'); - const element = { - getAttribute, - }; - - const result = getName(element); - - t.equal(result, 'hello', 'should strip js- prefix'); - t.end(); -}); - -test('cloudcmd: client: config: input: getValue: checkbox', (t) => { - const querySelector = stub().returns({ - type: 'checkbox', - checked: true, - }); - - const element = { - querySelector, - }; - - const result = getValue('auth', element); - - t.ok(result, 'should return checked value'); - t.end(); -}); - -test('cloudcmd: client: config: input: getValue: number', (t) => { - const querySelector = stub().returns({ - type: 'number', - value: '42', - }); - - const element = { - querySelector, - }; - - const result = getValue('port', element); - - t.equal(result, 42, 'should return number'); - t.end(); -}); - -test('cloudcmd: client: config: input: getValue: default', (t) => { - const querySelector = stub().returns({ - type: 'text', - value: 'hello', - }); - - const element = { - querySelector, - }; - - const result = getValue('name', element); - - t.equal(result, 'hello', 'should return value as is'); - t.end(); -}); - -test('cloudcmd: client: config: input: setValue: checkbox', (t) => { - const el = { - type: 'checkbox', - checked: false, - }; - - const querySelector = stub().returns(el); - const element = { - querySelector, - }; - - setValue('auth', true, element); - - t.ok(el.checked, 'should set checked'); - t.end(); -}); - -test('cloudcmd: client: config: input: setValue: default', (t) => { - const el = { - type: 'text', - value: 'old', - }; - - const querySelector = stub().returns(el); - const element = { - querySelector, - }; - - setValue('name', 'new', element); - - t.equal(el.value, 'new', 'should set value'); - t.end(); -}); diff --git a/client/modules/contact.js b/client/modules/contact.js index 48c7bd19..abcfd003 100644 --- a/client/modules/contact.js +++ b/client/modules/contact.js @@ -1,34 +1,36 @@ /* global CloudCmd */ /* global DOM */ -import olark from '@cloudcmd/olark'; -import * as Images from '#dom/images'; -CloudCmd.Contact = { - init, - show, - hide, -}; +'use strict'; + +CloudCmd.Contact = exports; + +const olark = require('@cloudcmd/olark'); +const Images = require('../dom/images'); const {Events} = DOM; const {Key} = CloudCmd; -export function init() { +module.exports.show = show; +module.exports.hide = hide; + +module.exports.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); -} +}; -export function show() { +function show() { Key.unsetBind(); Images.hide(); olark('api.box.expand'); } -export function hide() { +function hide() { Key.setBind(); olark('api.box.hide'); } @@ -37,3 +39,4 @@ 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 16d557ae..fe8198af 100644 --- a/client/modules/edit-file-vim.js +++ b/client/modules/edit-file-vim.js @@ -1,12 +1,10 @@ -import * as Events from '#dom/events'; +'use strict'; -const {CloudCmd} = globalThis; +/* global CloudCmd */ -CloudCmd.EditFileVim = { - init, - show, - hide, -}; +CloudCmd.EditFileVim = exports; + +const Events = require('../dom/events'); const {Key} = CloudCmd; @@ -18,29 +16,34 @@ const ConfigView = { }, }; -export async function init() { +module.exports.init = async () => { await CloudCmd.EditFile(); -} +}; -export async function show() { +module.exports.show = () => { Events.addKey(listener); - const editFile = await CloudCmd.EditFile.show(ConfigView); - - editFile + CloudCmd.EditFile + .show(ConfigView) .getEditor() .setKeyMap('vim'); -} +}; -export function hide() { +module.exports.hide = hide; + +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 83a07e36..f9b29041 100644 --- a/client/modules/edit-file.js +++ b/client/modules/edit-file.js @@ -1,19 +1,20 @@ -/* global CloudCmd, DOM*/ -import Format from 'format-io'; -import {fullstore} from 'fullstore'; -import exec from 'execon'; -import {supermenu} from 'supermenu'; +'use strict'; -CloudCmd.EditFile = { - init, - show, - hide, - isChanged, -}; +/* global CloudCmd, DOM*/ + +CloudCmd.EditFile = exports; + +const Format = require('format-io'); +const fullstore = require('fullstore'); +const exec = require('execon'); +const supermenu = require('supermenu'); const Info = DOM.CurrentInfo; -const {Dialog, Images} = DOM; +const { + Dialog, + Images, +} = DOM; const {config} = CloudCmd; @@ -23,13 +24,13 @@ let MSG_CHANGED; const isLoading = fullstore(); const ConfigView = { - beforeClose: async () => { + beforeClose: () => { exec.ifExist(Menu, 'hide'); - await isChanged(); + isChanged(); }, }; -export async function init() { +module.exports.init = async () => { isLoading(true); await CloudCmd.Edit(); @@ -39,7 +40,7 @@ export async function init() { setListeners(editor); isLoading(false); -} +}; function getName() { const {name, isDir} = Info; @@ -50,7 +51,7 @@ function getName() { return name; } -export async function show(options) { +module.exports.show = (options) => { if (isLoading()) return; @@ -68,30 +69,30 @@ export async function show(options) { .getEditor() .setOption('keyMap', 'default'); - const [error, data] = await Info.getData(); - - if (error) { - Images.hide(); - return CloudCmd.Edit; - } - - const {path} = Info; - const name = getName(); - - setMsgChanged(name); - - CloudCmd.Edit - .getEditor() - .setValueFirst(path, data) - .setModeForPath(name) - .enableKey(); - - CloudCmd.Edit.show(optionsEdit); + Info.getData((error, data) => { + const {path} = Info; + const name = getName(); + + if (error) + return Images.hide(); + + setMsgChanged(name); + + CloudCmd.Edit + .getEditor() + .setValueFirst(path, data) + .setModeForPath(name) + .enableKey(); + + CloudCmd.Edit.show(optionsEdit); + }); return CloudCmd.Edit; -} +}; -export function hide() { +module.exports.hide = hide; + +function hide() { CloudCmd.Edit.hide(); } @@ -148,36 +149,40 @@ 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 + '?'; } -export async function isChanged() { +module.exports.isChanged = isChanged; + +async function isChanged() { const editor = CloudCmd.Edit.getEditor(); const is = editor.isChanged(); @@ -191,3 +196,4 @@ export async function isChanged() { editor.save(); } + diff --git a/client/modules/edit-names-vim.js b/client/modules/edit-names-vim.js index 8b3b2f89..66087294 100644 --- a/client/modules/edit-names-vim.js +++ b/client/modules/edit-names-vim.js @@ -1,12 +1,10 @@ -import * as Events from '#dom/events'; +'use strict'; -const {CloudCmd} = globalThis; +/* global CloudCmd */ -CloudCmd.EditNamesVim = { - init, - show, - hide, -}; +CloudCmd.EditNamesVim = exports; + +const Events = require('../dom/events'); const {Key} = CloudCmd; const ConfigView = { @@ -17,28 +15,34 @@ const ConfigView = { }, }; -export async function init() { +module.exports.init = async () => { await CloudCmd.EditNames(); -} +}; -export function show() { +module.exports.show = () => { Events.addKey(listener); CloudCmd.EditNames .show(ConfigView) .getEditor() .setKeyMap('vim'); -} +}; -export function hide() { +module.exports.hide = hide; + +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 7bc29054..e26e9ad8 100644 --- a/client/modules/edit-names.js +++ b/client/modules/edit-names.js @@ -1,37 +1,40 @@ -import {tryToCatch} from 'try-to-catch'; -import exec from 'execon'; -import {supermenu} from 'supermenu'; -import {multiRename} from 'multi-rename'; +'use strict'; -const {CloudCmd, DOM} = globalThis; +/* global CloudCmd, DOM */ -CloudCmd.EditNames = { - init, - show, - hide, - isChanged, -}; +CloudCmd.EditNames = exports; + +const currify = require('currify'); +const exec = require('execon'); +const supermenu = require('supermenu'); +const multiRename = require('multi-rename'); + +const reject = Promise.reject.bind(Promise); const Info = DOM.CurrentInfo; const {Dialog} = DOM; +const refresh = currify(_refresh); +const rename = currify(_rename); + let Menu; const ConfigView = { - beforeClose: async () => { + beforeClose: () => { exec.ifExist(Menu, 'hide'); DOM.Events.remove('keydown', keyListener); - await isChanged(); + isChanged(); }, }; -export async function init() { +module.exports.init = async () => { await CloudCmd.Edit(); - setListeners(); -} + const editor = CloudCmd.Edit.getEditor(); + setListeners(editor); +}; -export function show(options) { +module.exports.show = (options) => { const names = getActiveNames().join('\n'); const config = { ...ConfigView, @@ -53,7 +56,7 @@ export function show(options) { CloudCmd.Edit.show(config); return CloudCmd.Edit; -} +}; async function keyListener(event) { const ctrl = event.ctrlKey; @@ -61,15 +64,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; - } - if (ctrlMeta && event.keyCode === Key.P) { + else if (ctrlMeta && event.keyCode === Key.P) { const [, pattern] = await Dialog.prompt('Apply pattern:', '[n][e]'); pattern && applyPattern(pattern); } + + event.preventDefault(); } function applyPattern(pattern) { @@ -83,7 +86,9 @@ function getActiveNames() { return DOM.getFilenames(DOM.getActiveFiles()); } -export function hide() { +module.exports.hide = hide; + +function hide() { CloudCmd.Edit.hide(); } @@ -93,7 +98,7 @@ function setListeners() { DOM.Events.addOnce('contextmenu', element, setMenu); } -async function applyNames() { +function applyNames() { const dir = Info.dirPath; const from = getActiveNames(); const nameIndex = from.indexOf(Info.name); @@ -105,18 +110,15 @@ async function applyNames() { const root = CloudCmd.config('root'); - const response = rename(dir, from, to, root); - const [error] = await tryToCatch(refresh, to, nameIndex, response); - - if (error) - alert(error); + Promise.resolve(root) + .then(rename(dir, from, to)) + .then(refresh(to, nameIndex)) + .catch(alert); } -function refresh(to, nameIndex, res) { - if (res.status === 404) { - const error = res.text(); - throw error; - } +function _refresh(to, nameIndex, res) { + if (res.status === 404) + return res.text().then(reject); const currentName = to[nameIndex]; @@ -132,7 +134,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; @@ -159,7 +161,6 @@ function setMenu(event) { return; const editor = CloudCmd.Edit.getEditor(); - const options = { beforeShow: (params) => { params.x -= 18; @@ -172,29 +173,31 @@ function setMenu(event) { }; const menuData = { - 'Save Ctrl+S': async () => { - await applyNames(); + 'Save Ctrl+S' : () => { + 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,14 +208,16 @@ function setMenu(event) { Menu.show(position.x, position.y); } -export async function isChanged() { +module.exports.isChanged = isChanged; + +async function isChanged() { const editor = CloudCmd.Edit.getEditor(); const msg = 'Apply new names?'; if (!editor.isChanged()) return; - const [cancel] = await Dialog.confirm(msg); - - !cancel && await applyNames(); + const [, names] = await Dialog.confirm(msg); + names && applyNames(names); } + diff --git a/client/modules/edit.js b/client/modules/edit.js index 593a11cb..1807145b 100644 --- a/client/modules/edit.js +++ b/client/modules/edit.js @@ -1,28 +1,19 @@ /* 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 isFn = (a) => typeof a === 'function'; +const {promisify} = require('es6-promisify'); +const tryToCatch = require('try-to-catch'); +const createElement = require('@cloudcmd/create-element'); +const load = require('load.js'); const loadJS = load.js; +const {MAX_FILE_SIZE: maxSize} = require('../../common/cloudfunc'); +const {time, timeEnd} = require('../../common/util'); + const Name = 'Edit'; -CloudCmd[Name] = { - init, - show, - hide, - getEditor, - getElement, -}; +CloudCmd[Name] = exports; const EditorName = CloudCmd.config('editor'); @@ -38,20 +29,19 @@ const ConfigView = { }, }; -export async function init() { +module.exports.init = async () => { const element = create(); await CloudCmd.View(); await loadFiles(element); -} +}; function create() { const element = createElement('div', { - style: montag` - width: 100%; - height: 100%; - font-family: "Droid Sans Mono"; - `, + style: + 'width : 100%;' + + 'height : 100%;' + + 'font-family: "Droid Sans Mono";', notAppend: true, }); @@ -61,8 +51,8 @@ function create() { } function checkFn(name, fn) { - if (!isFn(fn)) - throw Error(`${name} should be a function!`); + if (typeof fn !== 'function') + throw Error(name + ' should be a function!'); } function initConfig(options = {}) { @@ -76,32 +66,41 @@ function initConfig(options = {}) { checkFn('options.afterShow', options.afterShow); + const afterShow = {config}; + config.afterShow = () => { - ConfigView.afterShow(); + afterShow(); options.afterShow(); }; return config; } -export function show(options) { +module.exports.show = (options) => { if (Loading) return; CloudCmd.View.show(Element, initConfig(options)); - getEditor().setOptions({ - fontSize: 16, - }); + getEditor() + .setOptions({ + fontSize: 16, + }); +}; + +module.exports.getEditor = getEditor; + +function getEditor() { + return editor; } -export function getElement() { +module.exports.getElement = () => { return Element; -} +}; -export function hide() { +module.exports.hide = () => { CloudCmd.View.hide(); -} +}; const loadFiles = async (element) => { const prefix = `${CloudCmd.prefix}/${EditorName}`; @@ -109,7 +108,7 @@ const loadFiles = async (element) => { const prefixSocket = `${CloudCmd.prefixSocket}/${EditorName}`; const url = `${prefix}/${EditorName}.js`; - time(`${Name} load`); + time(Name + ' load'); await loadJS(url); @@ -121,7 +120,8 @@ 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 74657a90..5e859115 100644 --- a/client/modules/help.js +++ b/client/modules/help.js @@ -1,27 +1,31 @@ -import * as Images from '#dom/images'; +'use strict'; -const {CloudCmd} = globalThis; +/* global CloudCmd */ -CloudCmd.Help = { - init, - show, - hide, +CloudCmd.Help = exports; + +const Images = require('../dom/images'); + +module.exports.init = () => { + Images.show.load('top'); }; -export function init() { - Images.show.load('top'); -} +module.exports.show = show; +module.exports.hide = hide; -export function show() { +function show() { const positionLoad = 'top'; const relative = true; - CloudCmd.Markdown.show('/HELP.md', { - positionLoad, - relative, - }); + CloudCmd + .Markdown + .show('/HELP.md', { + positionLoad, + relative, + }); } -export function hide() { +function hide() { CloudCmd.View.hide(); } + diff --git a/client/modules/konsole.js b/client/modules/konsole.js index e9a465af..867bf8b2 100644 --- a/client/modules/konsole.js +++ b/client/modules/konsole.js @@ -1,21 +1,23 @@ +'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 {Dialog, CurrentInfo: Info} = DOM; +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 rmLastSlash = (a) => a.replace(/\/$/, '') || '/'; @@ -29,7 +31,7 @@ const Name = 'Konsole'; let Element; let Loaded; -export async function init() { +module.exports.init = async () => { if (!config('console')) return; @@ -38,34 +40,42 @@ export async function init() { await CloudCmd.View(); await load(); await create(); -} +}; -export function hide() { +module.exports.hide = () => { CloudCmd.View.hide(); -} +}; -export const clear = () => { +module.exports.clear = () => { konsole.clear(); }; -const getPrefix = () => CloudCmd.prefix + '/console'; +function getPrefix() { + return CloudCmd.prefix + '/console'; +} function getPrefixSocket() { return CloudCmd.prefixSocket + '/console'; } -const getEnv = () => ({ - ACTIVE_DIR: DOM.getCurrentDirPath.bind(DOM), - PASSIVE_DIR: DOM.getNotCurrentDirPath.bind(DOM), - CURRENT_NAME: DOM.getCurrentName.bind(DOM), - CURRENT_PATH: () => Info.path, -}); +function getEnv() { + return { + ACTIVE_DIR: DOM.getCurrentDirPath.bind(DOM), + PASSIVE_DIR: DOM.getNotCurrentDirPath.bind(DOM), + CURRENT_NAME: DOM.getCurrentName.bind(DOM), + CURRENT_PATH: () => { + return Info.path; + }, + }; +} async function onPath(path) { if (Info.dirPath === path) return; - await CloudCmd.changeDir(path); + await CloudCmd.loadDir({ + path, + }); } const getDirPath = () => { @@ -111,7 +121,7 @@ function authCheck(konsole) { }); } -export function show(callback) { +module.exports.show = (callback) => { if (!Loaded) return; @@ -124,20 +134,21 @@ export function 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 c94c64e8..523814c7 100644 --- a/client/modules/markdown.js +++ b/client/modules/markdown.js @@ -1,34 +1,44 @@ -import createElement from '@cloudcmd/create-element'; -import * as Images from '#dom/images'; -import {Markdown} from '#dom/rest'; -import {alert} from '#dom/dialog'; +'use strict'; -const {CloudCmd} = globalThis; +/* global CloudCmd */ -CloudCmd.Markdown = { - init, - show, - hide, -}; +const {promisify} = require('es6-promisify'); +const tryToCatch = require('try-to-catch'); -export async function init() { +CloudCmd.Markdown = exports; + +const createElement = require('@cloudcmd/create-element'); + +const Images = require('../dom/images'); +const {Markdown} = require('../dom/rest'); +const {alert} = require('../dom/dialog'); + +const read = promisify(Markdown.read); + +module.exports.init = async () => { Images.show.load('top'); await CloudCmd.View(); -} +}; -export function hide() { +module.exports.show = show; + +module.exports.hide = () => { CloudCmd.View.hide(); -} +}; -export async function show(name, options = {}) { - const {positionLoad, relative} = options; +async function show(name, options = {}) { + const relativeQuery = '?relative'; + const { + positionLoad, + relative, + } = options; Images.show.load(positionLoad); if (relative) - name += '?relative'; + name += relativeQuery; - const [error, innerHTML] = await Markdown.read(name); + const [error, innerHTML] = await tryToCatch(read, name); Images.hide(); if (error) @@ -37,7 +47,6 @@ export async function show(name, options = {}) { }); const className = 'help'; - const div = createElement('div', { className, innerHTML, @@ -45,3 +54,4 @@ export async function show(name, options = {}) { CloudCmd.View.show(div); } + diff --git a/client/modules/menu/index.js b/client/modules/menu.js similarity index 71% rename from client/modules/menu/index.js rename to client/modules/menu.js index 7235591e..66a4dbcc 100644 --- a/client/modules/menu/index.js +++ b/client/modules/menu.js @@ -1,12 +1,20 @@ -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'; +/* global CloudCmd, DOM */ -const {CloudCmd, DOM} = globalThis; -const {config, Key} = CloudCmd; +'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 { Buffer, @@ -23,54 +31,45 @@ let MenuShowedName; let MenuContext; let MenuContextFile; -export const ENABLED = false; +module.exports.ENABLED = false; -CloudCmd.Menu = { - init, - show, - hide, -}; +CloudCmd.Menu = exports; -export async function init() { - const {isAuth, menuDataFile} = getFileMenuData(); +module.exports.init = () => { + const { + isAuth, + menuDataFile, + } = getFileMenuData(); const fm = DOM.getFM(); const menuData = getMenuData(isAuth); + const options = getOptions({type: 'context'}); + const optionsFile = getOptions({type: 'file'}); - 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 = supermenu(fm, options, menuData); + MenuContextFile = supermenu(fm, optionsFile, menuDataFile); MenuContext.addContextMenuListener(); MenuContextFile.addContextMenuListener(); Events.addKey(listener); -} +}; -export function hide() { +module.exports.hide = hide; + +function hide() { MenuContext.hide(); MenuContextFile.hide(); } -export function show(position) { +module.exports.show = (position) => { const {x, y} = getPosition(position); MenuContext.show(x, y); MenuContextFile.show(x, y); Images.hide(); -} +}; function getPosition(position) { if (position) @@ -106,11 +105,9 @@ function getOptions({type}) { } const options = { - icon: true, - infiniteScroll: false, - beforeClose: Key.setBind, - beforeHide: Key.setBind, - beforeShow: exec.with(beforeShow, func), + icon : true, + beforeClose : Key.setBind, + beforeShow : exec.with(beforeShow, func), beforeClick, name, }; @@ -129,7 +126,6 @@ function getMenuData(isAuth) { CloudCmd.Upload.show(); }, 'Upload From Cloud': uploadFromCloud, - 'Toggle File Selection': DOM.toggleSelectedFile, '(Un)Select All': DOM.toggleAllSelectedFiles, }; @@ -141,16 +137,14 @@ function getMenuData(isAuth) { function getFileMenuData() { const isAuth = CloudCmd.config('auth'); + const show = wrap((name) => { + CloudCmd[name].show(); + }); const menuBottom = getMenuData(isAuth); const menuTop = { - 'View': () => { - CloudCmd.View.show(); - }, - 'Edit': () => { - const name = config('vim') ? 'EditFileVim' : 'EditFile'; - CloudCmd[name].show(); - }, + 'View': show('View'), + 'Edit': show('EditFile'), 'Rename': () => { setTimeout(DOM.renameCurrent, 100); }, @@ -200,26 +194,16 @@ 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 !~[].indexOf.call(elements, el); + return is; } function beforeShow(callback, params) { - Key.unsetBind(); - - const { - name, - position = { - x: params.x, - y: params.y, - }, - } = params; - - const {x, y} = position; - + const {name} = params; const el = DOM.getCurrentByPosition({ - x, - y, + x: params.x, + y: params.y, }); const menuName = getMenuNameByEl(el); @@ -235,23 +219,26 @@ function beforeShow(callback, params) { exec(callback); if (isShow) - isShow = isPath(x, y); + isShow = isPath(params.x, params.y); return isShow; } -const beforeClick = (name) => MenuShowedName !== name; +function beforeClick(name) { + return MenuShowedName !== name; +} -async function _uploadTo(nameModule) { - const [error, data] = await Info.getData(); +function _uploadTo(nameModule) { + Info.getData((error, data) => { + if (error) + return; + + const {name} = Info; + + CloudCmd.execFromModule(nameModule, 'uploadFile', name, data); + }); - if (error) - return; - - const {name} = Info; - - CloudCmd.execFromModule(nameModule, 'uploadFile', name, data); - CloudCmd.log(`Uploading to ${name}...`); + CloudCmd.log('Uploading to ' + name + '...'); } function uploadFromCloud() { @@ -264,9 +251,7 @@ function uploadFromCloud() { if (e) return; - await CloudCmd.refresh({ - currentName, - }); + await CloudCmd.refresh({currentName}); }); } @@ -289,14 +274,13 @@ 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, '#'); + const encodedPath = encodeURI(path).replace(/#/g, '%23'); const id = getIdBySrc(path); let src; @@ -307,7 +291,7 @@ function download(type) { src = prefixURL + FS + encodedPath + '?download'; const element = createElement('iframe', { - id: id + '-' + date, + id : id + '-' + date, async: false, className: 'hidden', src, @@ -336,17 +320,21 @@ function getCurrentPosition() { } function listener(event) { - const {F9, ESC} = Key; + const { + F9, + ESC, + } = Key; const key = event.keyCode; const isBind = Key.isBind(); - if (key === ESC) { - Key.setBind(); - return hide(); - } + if (!isBind) + return; - if (isBind && key === F9) { + if (key === ESC) + return hide(); + + if (key === F9) { const position = getCurrentPosition(); MenuContext.show(position.x, position.y); diff --git a/client/modules/menu/cloudmenu.js b/client/modules/menu/cloudmenu.js deleted file mode 100644 index 8f7c1e68..00000000 --- a/client/modules/menu/cloudmenu.js +++ /dev/null @@ -1,31 +0,0 @@ -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/operation/format.js b/client/modules/operation/format.js index c639799e..373d069c 100644 --- a/client/modules/operation/format.js +++ b/client/modules/operation/format.js @@ -1,6 +1,9 @@ -export const format = (operation, from, to) => { +'use strict'; + +module.exports = (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 63445888..a27e9daa 100644 --- a/client/modules/operation/get-next-current-name.js +++ b/client/modules/operation/get-next-current-name.js @@ -1,9 +1,11 @@ -import currify from 'currify'; +'use strict'; -const not = currify((array, value) => !array.includes(value)); +const currify = require('currify'); + +const not = currify((array, value) => !~array.indexOf(value)); const notOneOf = currify((a, b) => a.filter(not(b))); -export const getNextCurrentName = (currentName, names, removedNames) => { +module.exports = (currentName, names, removedNames) => { const i = names.indexOf(currentName); const nextNames = notOneOf(names, removedNames); @@ -14,3 +16,4 @@ export const getNextCurrentName = (currentName, names, removedNames) => { return nextNames[length - 1]; }; + diff --git a/client/modules/operation/index.js b/client/modules/operation/index.js index f349f7d8..dc80b8f8 100644 --- a/client/modules/operation/index.js +++ b/client/modules/operation/index.js @@ -1,19 +1,26 @@ -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'; +/* global CloudCmd */ +/* global Util */ +/* global DOM */ +/* global fileop */ -const {DOM, CloudCmd} = globalThis; +'use strict'; + +const currify = require('currify'); +const wraptile = require('wraptile'); +const {promisify} = require('es6-promisify'); +const exec = require('execon'); +const load = require('load.js'); +const tryToCatch = require('try-to-catch'); + +const {encode} = require('../../../common/entity'); +const removeExtension = require('./remove-extension'); +const setListeners = require('./set-listeners'); +const getNextCurrentName = require('./get-next-current-name'); const removeQuery = (a) => a.replace(/\?.*/, ''); const Name = 'Operation'; +CloudCmd[Name] = exports; const {config} = CloudCmd; const {Dialog, Images} = DOM; @@ -45,7 +52,7 @@ const noFilesCheck = () => { return is; }; -export const init = promisify((callback) => { +module.exports.init = promisify((callback) => { showLoad(); exec.series([ @@ -54,10 +61,13 @@ export const init = promisify((callback) => { if (config('dropbox')) return callback(); - const {prefix, prefixSocket} = CloudCmd; + const { + prefix, + prefixSocket, + } = CloudCmd; await loadAll(); - await initOperations(prefix, prefixSocket, callback); + initOperations(prefix, prefixSocket, callback); }, (callback) => { Loaded = true; @@ -72,7 +82,7 @@ function _authCheck(spawn, ok) { const alertDialog = wraptile(Dialog.alert); spawn.on('accept', accept(spawn)); - spawn.on('reject', alertDialog('Wrong credentials!')); + spawn.on('reject', alertDialog ('Wrong credentials!')); spawn.emit('auth', config('username'), config('password')); } @@ -84,11 +94,7 @@ const onConnect = currify((fn, operator) => { async function initOperations(prefix, socketPrefix, fn) { socketPrefix = `${socketPrefix}/fileop`; - const operator = await globalThis.fileop({ - prefix, - socketPrefix, - }); - + const operator = await fileop({prefix, socketPrefix}); operator.on('connect', authCheck(operator, onConnect(fn))); } @@ -103,8 +109,7 @@ function setOperations(operator) { to, }); - operator - .tar(from, to, names) + operator.tar(from, to, names) .then(listen); }; @@ -118,8 +123,7 @@ function setOperations(operator) { to, }); - operator - .zip(from, to, names) + operator.zip(from, to, names) .then(listen); }; @@ -133,8 +137,7 @@ function setOperations(operator) { from, }); - operator - .remove(from, files) + operator.remove(from, files) .then(listen); }; @@ -148,8 +151,7 @@ function setOperations(operator) { names, }); - operator - .copy(from, to, names) + operator.copy(from, to, names) .then(listen); }; @@ -162,8 +164,7 @@ function setOperations(operator) { to, }); - operator - .move(from, to, names) + operator.move(from, to, names) .then(listen); }; @@ -177,8 +178,7 @@ function setOperations(operator) { to, }); - operator - .extract(from, to) + operator.extract(from, to) .then(listen); }; } @@ -190,11 +190,11 @@ function getPacker(type) { return packTarFn; } -export function hide() { +module.exports.hide = () => { CloudCmd.View.hide(); -} +}; -export function show(operation, data) { +module.exports.show = (operation, data) => { if (!Loaded) return; @@ -215,7 +215,7 @@ export function show(operation, data) { if (operation === 'extract') return Operation.extract(); -} +}; Operation.copy = processFiles({ type: 'copy', @@ -225,9 +225,13 @@ Operation.move = processFiles({ type: 'move', }); -Operation.delete = promptDelete; +Operation.delete = () => { + promptDelete(); +}; -Operation.deleteSilent = deleteSilent; +Operation.deleteSilent = () => { + deleteSilent(); +}; Operation.pack = () => { const isZip = config('packer') === 'zip'; @@ -269,12 +273,13 @@ async function promptDelete() { } else { const current = DOM.getCurrentFile(); const isDir = DOM.isCurrentIsDir(current); - const getType = (isDir) => isDir ? 'directory' : 'file'; + const getType = (isDir) => { + return isDir ? 'directory' : 'file'; + }; const type = getType(isDir) + ' '; const name = DOM.getCurrentName(current); - msg = msgAsk + msgSel + type + name + '?'; } @@ -334,22 +339,20 @@ async function _processFiles(options, data) { let names = []; + /* eslint no-multi-spaces: 0 */ if (data) { - ({ - from, - to, - names, - } = data); - - ({panel} = Info); + from = data.from; + to = data.to; + names = data.names; + panel = Info.panel; } 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) @@ -386,14 +389,10 @@ 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(); @@ -407,7 +406,15 @@ async function _processFiles(options, data) { operation(files, async () => { await DOM.Storage.remove(from); - const {panel, panelPassive} = Info; + const { + panel, + panelPassive, + } = Info; + + const setCurrent = () => { + const currentName = name || data.names[0]; + DOM.setCurrentByName(currentName); + }; if (!Info.isOnePanel) CloudCmd.refresh({ @@ -415,9 +422,7 @@ async function _processFiles(options, data) { noCurrent: true, }); - CloudCmd.refresh({ - panel, - }); + CloudCmd.refresh({panel}, setCurrent); }); } } @@ -425,7 +430,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) { @@ -433,7 +438,10 @@ 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); @@ -447,7 +455,7 @@ function twopack(operation, type) { case 'extract': op = extractFn; - fileFrom = { + fileFrom = { from: path, to: dirPath, }; @@ -460,7 +468,7 @@ function twopack(operation, type) { op = getPacker(type); if (names.length > 1) - currentName = Info.dir; + currentName = Info.dir; currentName += DOM.getPackerExt(type); @@ -488,23 +496,17 @@ 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); + return Dialog.prompt(msg, to); } -globalThis.CloudCmd[Name] = { - init, - hide, - show, -}; - async function loadAll() { - const {prefix} = globalThis.CloudCmd; + const {prefix} = CloudCmd; const file = `${prefix}/fileop/fileop.js`; const [error] = await tryToCatch(load.js, file); @@ -514,3 +516,4 @@ async function loadAll() { Loaded = true; } + diff --git a/client/modules/operation/remove-extension.js b/client/modules/operation/remove-extension.js index e6ce2c46..52772c30 100644 --- a/client/modules/operation/remove-extension.js +++ b/client/modules/operation/remove-extension.js @@ -1,17 +1,20 @@ -import {getExt} from '#common/util'; +'use strict'; -export const removeExtension = (name) => { +const {getExt} = require('../../../common/util'); + +module.exports = (name) => { const ext = getExtension(name); return name.replace(ext, ''); }; function getExtension(name) { - if (name.endsWith('.tar.gz')) + if (/\.tar\.gz$/.test(name)) return '.tar.gz'; - if (name.endsWith('.tar.bz2')) + if (/\.tar\.bz2$/.test(name)) 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 fa11e9e2..0cf0aacc 100644 --- a/client/modules/operation/remove-extension.spec.js +++ b/client/modules/operation/remove-extension.spec.js @@ -1,5 +1,7 @@ -import test from 'supertape'; -import {removeExtension} from './remove-extension.js'; +'use strict'; + +const test = require('supertape'); +const removeExtension = require(`./remove-extension`); test('cloudcmd: client: modules: operation: removeExtension: .tar.gz', (t) => { const name = 'hello'; @@ -24,3 +26,4 @@ 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 e54e7d14..df83d243 100644 --- a/client/modules/operation/set-listeners.js +++ b/client/modules/operation/set-listeners.js @@ -1,11 +1,18 @@ +'use strict'; + /* global DOM */ -import forEachKey from 'for-each-key'; -import wraptile from 'wraptile'; -import {format} from './format.js'; -const {Dialog, Images} = DOM; +const { + Dialog, + Images, +} = DOM; -export const setListeners = (options) => (emitter) => { +const forEachKey = require('for-each-key'); +const wraptile = require('wraptile'); + +const format = require('./format'); + +module.exports = (options) => (emitter) => { const { operation, callback, @@ -15,11 +22,13 @@ export const setListeners = (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, @@ -48,20 +57,21 @@ export const setListeners = (options) => (emitter) => { forEachKey(removeListener, listeners); progress.remove(); - callback(); + if (lastError || done) + callback(); }, error: async (error) => { + lastError = error; + if (noContinue) { listeners.end(error); Dialog.alert(error); progress.remove(); - return; } - const [cancel] = await Dialog.confirm(`${error} - Continue?`); + const [cancel] = await Dialog.confirm(error + '\n Continue?'); if (!done && !cancel) return emitter.continue(); @@ -73,3 +83,4 @@ export const setListeners = (options) => (emitter) => { forEachKey(on, listeners); }; + diff --git a/client/modules/polyfill.js b/client/modules/polyfill.js index 90b62689..e4fe9fcf 100644 --- a/client/modules/polyfill.js +++ b/client/modules/polyfill.js @@ -1,15 +1,11 @@ -import _scrollIntoViewIfNeeded from 'scroll-into-view-if-needed'; +'use strict'; -globalThis.DOM = globalThis.DOM || {}; +/* global DOM */ -export const scrollIntoViewIfNeeded = (el, overrides = {}) => { - const { - scroll = _scrollIntoViewIfNeeded, - } = overrides; - - return scroll(el, { - block: 'nearest', - }); -}; +require('domtokenlist-shim'); + +const scrollIntoViewIfNeeded = require('scroll-into-view-if-needed').default; +DOM.scrollIntoViewIfNeeded = (el) => scrollIntoViewIfNeeded(el, { + block: 'nearest', +}); -globalThis.DOM.scrollIntoViewIfNeeded = scrollIntoViewIfNeeded; diff --git a/client/modules/polyfill.spec.js b/client/modules/polyfill.spec.js index 740947eb..af2fbce6 100644 --- a/client/modules/polyfill.spec.js +++ b/client/modules/polyfill.spec.js @@ -1,20 +1,31 @@ -import {test, stub} from 'supertape'; -import {scrollIntoViewIfNeeded} from './polyfill.js'; +'use strict'; -test('cloudcmd: client: polyfill: scrollIntoViewIfNeeded', (t) => { +const test = require('supertape'); +const mockRequire = require('mock-require'); +const stub = require('@cloudcmd/stub'); + +test('cloudcmd: client: polyfill: scrollIntoViewIfNeaded', (t) => { + const {DOM} = global; const scroll = stub(); const el = {}; - scrollIntoViewIfNeeded(el, { - scroll, + global.DOM = {}; + + mockRequire('scroll-into-view-if-needed', { + default: scroll, }); + mockRequire.reRequire('./polyfill'); + + global.DOM.scrollIntoViewIfNeeded(el); + mockRequire.stop('scroll-into-view-if-neaded'); + global.DOM = DOM; + const args = [ el, { block: 'nearest', - }, - ]; + }]; - t.calledWith(scroll, args, 'should call scrollIntoViewIfNeeded'); + t.ok(scroll.calledWith(...args), 'should call scrollIntoViewIfNeaded'); t.end(); }); diff --git a/client/modules/terminal-run.js b/client/modules/terminal-run.js index 029415ea..cc2df175 100644 --- a/client/modules/terminal-run.js +++ b/client/modules/terminal-run.js @@ -1,21 +1,25 @@ -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'; +'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'); const {Dialog} = DOM; -const {CloudCmd} = globalThis; -const {Key, config} = CloudCmd; +const { + Key, + config, +} = CloudCmd; -CloudCmd.TerminalRun = { - init, - show, - hide, -}; +CloudCmd.TerminalRun = exports; let Loaded; let Terminal; @@ -33,14 +37,14 @@ const loadAll = async () => { const [e] = await tryToCatch(load.parallel, [js, css]); if (e) { - const src = e.target.src.replace(globalThis.location.href, ''); + const src = e.target.src.replace(window.location.href, ''); return Dialog.alert(`file ${src} could not be loaded`); } Loaded = true; }; -export async function init() { +module.exports.init = async () => { if (!config('terminal')) return; @@ -48,15 +52,11 @@ export async function init() { await CloudCmd.View(); await loadAll(); -} +}; -export async function show(options = {}) { - return await runTerminal(options); -} - -const runTerminal = promisify((options, fn) => { +module.exports.show = promisify((options = {}, fn) => { if (!Loaded) - return fn(null, -1); + return; if (!config('terminal')) return; @@ -73,22 +73,28 @@ const runTerminal = promisify((options, fn) => { }); }); -export function hide() { +module.exports.hide = hide; + +function hide() { CloudCmd.View.hide(); } -const getPrefix = () => CloudCmd.prefix + '/gritty'; +function getPrefix() { + return CloudCmd.prefix + '/gritty'; +} function getPrefixSocket() { return CloudCmd.prefixSocket + '/gritty'; } -const getEnv = () => ({ - ACTIVE_DIR: DOM.getCurrentDirPath, - PASSIVE_DIR: DOM.getNotCurrentDirPath, - CURRENT_NAME: DOM.getCurrentName, - CURRENT_PATH: DOM.getCurrentPath, -}); +function getEnv() { + return { + ACTIVE_DIR: DOM.getCurrentDirPath, + PASSIVE_DIR: DOM.getNotCurrentDirPath, + CURRENT_NAME: DOM.getCurrentName, + CURRENT_PATH: DOM.getCurrentPath, + }; +} function create(createOptions) { const { @@ -110,7 +116,7 @@ function create(createOptions) { let commandExit = false; - const {socket, terminal} = globalThis.gritty(document.body, options); + const {socket, terminal} = gritty(document.body, options); Socket = socket; Terminal = terminal; @@ -121,8 +127,9 @@ function create(createOptions) { if (commandExit) hide(); - if (shiftKey && keyCode === Key.ESC) + if (shiftKey && keyCode === Key.ESC) { hide(); + } }); Socket.on('exit', (code) => { @@ -145,3 +152,4 @@ function authCheck(spawn) { Dialog.alert('Wrong credentials!'); }); } + diff --git a/client/modules/terminal.js b/client/modules/terminal.js index 186ee0b2..09d67b4c 100644 --- a/client/modules/terminal.js +++ b/client/modules/terminal.js @@ -1,22 +1,26 @@ -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'; +'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'); const loadParallel = load.parallel; -const {CloudCmd} = globalThis; - const {Dialog} = DOM; -const {Key, config} = globalThis.CloudCmd; +const { + Key, + config, +} = CloudCmd; -CloudCmd.Terminal = { - init, - show, - hide, -}; +CloudCmd.Terminal = exports; let Loaded; let Terminal; @@ -32,14 +36,14 @@ const loadAll = async () => { const [e] = await tryToCatch(loadParallel, [js, css]); if (e) { - const src = e.target.src.replace(globalThis.location.href, ''); + const src = e.target.src.replace(window.location.href, ''); return Dialog.alert(`file ${src} could not be loaded`); } Loaded = true; }; -export async function init() { +module.exports.init = async () => { if (!config('terminal')) return; @@ -47,25 +51,32 @@ export async function init() { await CloudCmd.View(); await loadAll(); - create(); -} + await create(); +}; -export function hide() { +module.exports.show = show; +module.exports.hide = hide; + +function hide() { CloudCmd.View.hide(); } -const getPrefix = () => CloudCmd.prefix + '/gritty'; +function getPrefix() { + return CloudCmd.prefix + '/gritty'; +} function getPrefixSocket() { return CloudCmd.prefixSocket + '/gritty'; } -const getEnv = () => ({ - ACTIVE_DIR: DOM.getCurrentDirPath, - PASSIVE_DIR: DOM.getNotCurrentDirPath, - CURRENT_NAME: DOM.getCurrentName, - CURRENT_PATH: DOM.getCurrentPath, -}); +function getEnv() { + return { + ACTIVE_DIR: DOM.getCurrentDirPath, + PASSIVE_DIR: DOM.getNotCurrentDirPath, + CURRENT_NAME: DOM.getCurrentName, + CURRENT_PATH: DOM.getCurrentPath, + }; +} function create() { const options = { @@ -75,7 +86,7 @@ function create() { fontFamily: 'Droid Sans Mono', }; - const {socket, terminal} = globalThis.gritty(document.body, options); + const {socket, terminal} = gritty(document.body, options); Socket = socket; Terminal = terminal; @@ -83,8 +94,9 @@ 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)); @@ -98,7 +110,7 @@ function authCheck(spawn) { }); } -export function show() { +function show() { if (!Loaded) return; @@ -111,3 +123,4 @@ export function show() { }, }); } + diff --git a/client/modules/upload.js b/client/modules/upload.js index 8433fbf5..868428ad 100644 --- a/client/modules/upload.js +++ b/client/modules/upload.js @@ -1,21 +1,23 @@ /* 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'; -CloudCmd.Upload = { - init, - show, - hide, -}; +'use strict'; -export async function init() { +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'); + +module.exports.init = async () => { Images.show.load('top'); await CloudCmd.View(); -} +}; -export async function show() { +module.exports.show = show; +module.exports.hide = hide; + +async function show() { Images.show.load('top'); const innerHTML = await Files.get('upload'); @@ -47,7 +49,7 @@ export async function show() { }); } -export function hide() { +function hide() { CloudCmd.View.hide(); } @@ -64,3 +66,4 @@ 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 aa3a1076..2df17696 100644 --- a/client/modules/user-menu/get-user-menu.js +++ b/client/modules/user-menu/get-user-menu.js @@ -1,4 +1,6 @@ -export const getUserMenu = (menuFn) => { +'use strict'; + +module.exports = (menuFn) => { const module = {}; const fn = Function('module', menuFn); @@ -6,3 +8,4 @@ export const getUserMenu = (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 606eb820..5da03054 100644 --- a/client/modules/user-menu/get-user-menu.spec.js +++ b/client/modules/user-menu/get-user-menu.spec.js @@ -1,5 +1,7 @@ -import test from 'supertape'; -import {getUserMenu} from './get-user-menu.js'; +'use strict'; + +const test = require('supertape'); +const getUserMenu = require('./get-user-menu'); test('user-menu: getUserMenu', (t) => { const menu = `module.exports = { @@ -13,6 +15,7 @@ test('user-menu: getUserMenu', (t) => { const [key] = Object.keys(result); - t.equal(key, 'F2 - Rename file'); + t.equal(key, 'F2 - Rename file', 'should equal'); t.end(); }); + diff --git a/client/modules/user-menu/index.js b/client/modules/user-menu/index.js index 60d32d95..50138c62 100644 --- a/client/modules/user-menu/index.js +++ b/client/modules/user-menu/index.js @@ -1,49 +1,47 @@ -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'; +'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'); const loadCSS = load.css; const sourceStore = fullstore(); -const { - CloudCmd, - DOM, - CloudFunc, -} = globalThis; - const Name = 'UserMenu'; - -CloudCmd[Name] = { - init, - show, - hide, -}; +CloudCmd[Name] = module.exports; const {Key} = CloudCmd; -export async function init() { +module.exports.init = async () => { await Promise.all([ loadCSS(`${CloudCmd.prefix}/dist/user-menu.css`), CloudCmd.View(), ]); -} +}; + +module.exports.show = show; +module.exports.hide = hide; const {CurrentInfo} = DOM; -export async function show() { +async function show() { Images.show.load('top'); const {dirPath} = CurrentInfo; @@ -54,10 +52,7 @@ export async function show() { Images.hide(); if (error) - return Dialog.alert(getCodeFrame({ - error, - source, - })); + return Dialog.alert(getCodeFrame({error, source})); sourceStore(source); @@ -74,17 +69,15 @@ export async function show() { const button = createElement('button', { className: 'cloudcmd-user-menu-button', innerText: 'User Menu', - notAppend: true, }); const select = createElement('select', { className: 'cloudcmd-user-menu', innerHTML: fillTemplate(names), - notAppend: true, size: 10, }); - button.addEventListener('click', onButtonClick(userMenu, select)); + button.addEventListener('click', onButtonClick(items, select)); select.addEventListener('dblclick', onDblClick(userMenu)); select.addEventListener('keydown', onKeyDown({ keys, @@ -109,7 +102,7 @@ function fillTemplate(options) { return result.join(''); } -export function hide() { +function hide() { CloudCmd.View.hide(); } @@ -123,7 +116,10 @@ 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(); @@ -134,8 +130,7 @@ const onKeyDown = currify(async ({keys, userMenu}, e) => { if (keyCode === Key.ESC) return hide(); - - if (keyCode === Key.ENTER) + else if (keyCode === Key.ENTER) value = userMenu[target.value]; else if (keys[keyName]) value = keys[keyName]; @@ -151,7 +146,6 @@ const runUserMenu = async (fn) => { const [error] = await tryToCatch(fn, { DOM, CloudCmd, - CloudFunc, tryToCatch, }); @@ -159,7 +153,6 @@ const runUserMenu = async (fn) => { return; const source = sourceStore(); - return Dialog.alert(getCodeFrame({ error, source, @@ -173,7 +166,6 @@ function getCodeFrame({error, source}) { return error.message; const [line, column] = parseError(error); - const start = { line, column, @@ -190,3 +182,4 @@ function getCodeFrame({error, source}) { return `
${frame}
`; } + diff --git a/client/modules/user-menu/navigate.js b/client/modules/user-menu/navigate.js index 0fa4d672..67c12e0a 100644 --- a/client/modules/user-menu/navigate.js +++ b/client/modules/user-menu/navigate.js @@ -1,53 +1,35 @@ -import {fullstore} from 'fullstore'; -import { +'use strict'; + +const { J, K, UP, DOWN, -} from '../../key/key.js'; +} = require('../../key/key.js'); -const store = fullstore(1); -const isDigit = (a) => /^\d+$/.test(a); - -export const navigate = (el, {key, keyCode}) => { - if (isDigit(key)) - store(Number(key)); +module.exports = (el, {keyCode}) => { + if (keyCode === DOWN || keyCode === J) + return down(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); - } + if (keyCode === UP || keyCode === K) + return up(el); }; -function down(el, count) { +function down(el) { const {length} = el; if (el.selectedIndex === length - 1) el.selectedIndex = 0; else - el.selectedIndex += count; - - if (el.selectedIndex < 0) - el.selectedIndex = length - 1; + ++el.selectedIndex; } -function up(el, count) { +function up(el) { const {length} = el; if (!el.selectedIndex) el.selectedIndex = length - 1; else - el.selectedIndex -= count; - - if (el.selectedIndex < 0) - el.selectedIndex = 0; + --el.selectedIndex; } + diff --git a/client/modules/user-menu/navigate.spec.js b/client/modules/user-menu/navigate.spec.js index 6539c8e9..7a5ba70b 100644 --- a/client/modules/user-menu/navigate.spec.js +++ b/client/modules/user-menu/navigate.spec.js @@ -1,11 +1,14 @@ -import test from 'supertape'; -import {navigate} from './navigate.js'; -import { +'use strict'; + +const test = require('supertape'); +const navigate = require('./navigate'); + +const { UP, DOWN, J, K, -} from '../../key/key.js'; +} = require('../../key/key.js'); test('cloudcmd: user-menu: navigate: DOWN', (t) => { const el = { @@ -105,59 +108,3 @@ 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 23d605e0..190c4ff6 100644 --- a/client/modules/user-menu/parse-error.js +++ b/client/modules/user-menu/parse-error.js @@ -1,11 +1,19 @@ +'use strict'; + const isNumber = (a) => typeof a === 'number'; -export const parseError = (error) => { - const {lineNumber, columnNumber} = error; +module.exports = (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); @@ -19,3 +27,4 @@ export const parseError = (error) => { Number(column), ]; }; + diff --git a/client/modules/user-menu/parse-error.spec.js b/client/modules/user-menu/parse-error.spec.js index 5ec8da45..ebd21553 100644 --- a/client/modules/user-menu/parse-error.spec.js +++ b/client/modules/user-menu/parse-error.spec.js @@ -1,5 +1,7 @@ -import test from 'supertape'; -import {parseError} from './parse-error.js'; +'use strict'; + +const test = require('supertape'); +const parseError = require('./parse-error'); test('user-menu: parse-error', (t) => { const result = parseError({ @@ -13,7 +15,7 @@ test('user-menu: parse-error', (t) => { t.end(); }); -test('user-menu: parse-error: stack', (t) => { +test('user-menu: parse-error', (t) => { const stack = ` ReferenceError: s is not defined at eval (eval at module.exports (get-user-menu.js:NaN), :1:2) @@ -22,10 +24,7 @@ test('user-menu: parse-error: stack', (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 e4ad39e3..78ce016f 100644 --- a/client/modules/user-menu/parse-user-menu.js +++ b/client/modules/user-menu/parse-user-menu.js @@ -1,6 +1,8 @@ +'use strict'; + const {entries, assign} = Object; -export const parseUserMenu = (userMenu) => { +module.exports = (userMenu) => { const names = []; const keys = {}; const items = {}; @@ -12,8 +14,9 @@ export const parseUserMenu = (userMenu) => { continue; } - if (str.startsWith('_')) + if (/^_/.test(str)) { continue; + } names.push(str); const [key, name] = str.split(' - '); @@ -29,3 +32,4 @@ export const parseUserMenu = (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 fb763209..3cdd72cd 100644 --- a/client/modules/user-menu/parse-user-menu.spec.js +++ b/client/modules/user-menu/parse-user-menu.spec.js @@ -1,20 +1,19 @@ -import {test, stub} from 'supertape'; -import {parseUserMenu} from './parse-user-menu.js'; +'use strict'; + +const test = require('supertape'); +const stub = require('@cloudcmd/stub'); +const parse = require('./parse-user-menu'); test('cloudcmd: user menu: parse', (t) => { const fn = stub(); const __settings = {}; - - const result = parseUserMenu({ + const result = parse({ __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 51e9fe02..463396d2 100644 --- a/client/modules/user-menu/run.js +++ b/client/modules/user-menu/run.js @@ -1,5 +1,8 @@ -export const runSelected = async (selectedItems, items, runUserMenu) => { +'use strict'; + +module.exports.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 86738712..e2f18b47 100644 --- a/client/modules/user-menu/run.spec.js +++ b/client/modules/user-menu/run.spec.js @@ -1,10 +1,15 @@ -import {test, stub} from 'supertape'; -import {runSelected} from './run.js'; +'use strict'; + +const test = require('supertape'); +const stub = require('@cloudcmd/stub'); +const {runSelected} = require('./run'); test('cloudcmd: client: user menu: run', async (t) => { const runUserMenu = stub(); const fn = stub(); - const selected = ['hello']; + const selected = [ + 'hello', + ]; const items = { hello: fn, @@ -12,6 +17,7 @@ test('cloudcmd: client: user menu: run', async (t) => { await runSelected(selected, items, runUserMenu); - t.calledWith(runUserMenu, [fn]); + t.ok(runUserMenu.calledWith(fn)); t.end(); }); + diff --git a/client/modules/view/index.js b/client/modules/view.js similarity index 50% rename from client/modules/view/index.js rename to client/modules/view.js index 6626f1aa..e192dcf9 100644 --- a/client/modules/view/index.js +++ b/client/modules/view.js @@ -1,90 +1,77 @@ -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, -} from './types.js'; +'use strict'; + +/* global CloudCmd, DOM */ + +require('../../css/view.css'); + +const itype = require('itype'); +const rendy = require('rendy'); +const exec = require('execon'); +const currify = require('currify'); +const tryToCatch = require('try-to-catch'); + +const modal = require('@cloudcmd/modal'); +const createElement = require('@cloudcmd/create-element'); + +const {time} = require('../../common/util'); +const {FS} = require('../../common/cloudfunc'); + +const Files = require('../dom/files'); +const Events = require('../dom/events'); +const load = require('load.js'); +const Images = require('../dom/images'); + +const {encode} = require('../../common/entity'); -const CloudCmd = globalThis.CloudCmd || {}; -const DOM = globalThis.DOM || {}; -const isString = (a) => typeof a === 'string'; -const {assign} = Object; const {isArray} = Array; - +const testRegExp = currify((name, reg) => reg.test(name)); const lifo = currify((fn, el, cb, name) => fn(name, el, cb)); -const series = wraptile((...a) => { - for (const f of a) - f(); -}); -const isFn = (a) => typeof a === 'function'; - -const noop = () => {}; const addEvent = lifo(Events.add); +const getRegExp = (ext) => RegExp(`\\.${ext}$`, 'i'); const loadCSS = load.css; +module.exports.show = show; +module.exports.hide = hide; + let Loading = false; const Name = 'View'; - -CloudCmd[Name] = { - init, - show, - hide, -}; +CloudCmd[Name] = module.exports; const Info = DOM.CurrentInfo; const {Key} = CloudCmd; - -const basename = (a) => a - .split('/') - .pop(); +const basename = (a) => a.split('/').pop(); let El; let TemplateAudio; let Overlay; const Config = { - beforeShow: () => { + beforeShow: (callback) => { Images.hide(); Key.unsetBind(); + exec(callback); }, - - beforeClose: () => { + beforeClose: (callback) => { Events.rmKey(listener); Key.setBind(); + exec(callback); }, - - afterShow: () => { + afterShow: (callback) => { El.focus(); + exec(callback); }, - onOverlayClick, - afterClose: noop, - autoSize: false, - + afterClose : exec, + autoSize : false, helpers: { title: {}, }, }; -export const _Config = Config; - -export async function init() { +module.exports.init = async () => { await loadAll(); const events = [ @@ -92,13 +79,10 @@ export async function init() { 'contextmenu', ]; - events.forEach(addEvent( - Overlay, - onOverlayClick, - )); -} + events.forEach(addEvent(Overlay, onOverlayClick)); +}; -export async function show(data, options = {}) { +async function show(data, options) { const prefixURL = CloudCmd.prefixURL + FS; if (Loading) @@ -107,7 +91,7 @@ export async function show(data, options = {}) { if (!options || options.bindKeys !== false) Events.addKey(listener); - El = _createElement('div', { + El = createElement('div', { className: 'view', notAppend: true, }); @@ -120,43 +104,31 @@ export async function show(data, options = {}) { else El.append(data); - _modal.open(El, initConfig(options)); + modal.open(El, initConfig(Config, options)); return; } Images.show.load(); const path = prefixURL + Info.path; - const type = options.raw ? '' : await getType(path); + const type = getType(path); switch(type) { default: - return await viewFile(); - - case 'markdown': - return await CloudCmd.Markdown.show(Info.path); - - case 'html': - return viewHtml(path); + return viewFile(); case 'image': - return viewImage(Info.path, prefixURL); + return viewImage(prefixURL); case 'media': - return await viewMedia(path); + return viewMedia(path); case 'pdf': return viewPDF(path); } } -export const _createIframe = createIframe; - -function createIframe(src, overrides = {}) { - const { - createElement = _createElement, - } = overrides; - +function viewPDF(src) { const element = createElement('iframe', { src, width: '100%', @@ -167,25 +139,14 @@ function createIframe(src, overrides = {}) { element.contentWindow.addEventListener('keydown', listener); }); - return element; -} - -export const _viewHtml = viewHtml; - -function viewHtml(src, overrides = {}) { - const {modal = _modal} = overrides; - modal.open(createIframe(src), Config); -} - -function viewPDF(src) { - const element = createIframe(src); - - const options = assign({}, Config); + const options = { + ...Config, + }; if (CloudCmd.config('showFileName')) options.title = Info.name; - _modal.open(element, options); + modal.open(element, options); } async function viewMedia(path) { @@ -206,80 +167,82 @@ async function viewMedia(path) { }, }; - _modal.open(element, allConfig); + modal.open(element, allConfig); } -async function viewFile() { - const [error, data] = await Info.getData(); - - if (error) - return Images.hide(); - - const element = document.createTextNode(data); - const options = Config; - - if (CloudCmd.config('showFileName')) - options.title = Info.name; - - El.append(element); - _modal.open(El, options); +function viewFile() { + Info.getData((error, data) => { + if (error) + return Images.hide(); + + const element = document.createTextNode(data); + const options = { + ...Config, + }; + + if (CloudCmd.config('showFileName')) + options.title = Info.name; + + El.append(element); + modal.open(El, options); + }); } -const copy = (a) => assign({}, a); - -export const _initConfig = initConfig; - -function initConfig(options) { - const config = copy(Config); +function initConfig(Config, options) { + const config = { + ...Config, + }; if (!options) return config; const names = Object.keys(options); - for (const name of names) { - const isConfig = Boolean(config[name]); + const isConfig = !!config[name]; const item = options[name]; + const isFunc = itype.function(item); - if (!isFn(item) || !isConfig) { + if (!isFunc || !isConfig) { config[name] = options[name]; continue; } - const fn = config[name]; - - config[name] = series(fn, item); + const func = config[name]; + config[name] = () => { + exec.series([func, item]); + }; } return config; } -export function hide() { - _modal.close(); +function hide() { + modal.close(); } -function viewImage(path, prefixURL) { - const isSupportedImage = (a) => isImage(a) || a === path; - const makeTitle = (path) => ({ - href: `${prefixURL}${path}`, - title: encode(basename(path)), - }); +function viewImage(prefixURL) { + const makeTitle = (path) => { + return { + href: prefixURL + path, + title: encode(basename(path)), + }; + }; const names = Info.files .map(DOM.getCurrentPath) - .filter(isSupportedImage); + .filter(isImage); - 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 : {}, }, }; @@ -288,7 +251,48 @@ function viewImage(path, prefixURL) { ...imageConfig, }; - _modal.open(titles, config); + modal.open(titles, config); +} + +function isImage(name) { + const images = [ + 'jp(e|g|eg)', + 'gif', + 'png', + 'bmp', + 'webp', + 'svg', + 'ico', + ]; + + return images + .map(getRegExp) + .some(testRegExp(name)); +} + +function isMedia(name) { + return isAudio(name) || isVideo(name); +} + +function isAudio(name) { + return /\.(mp3|ogg|m4a)$/i.test(name); +} + +function isVideo(name) { + return /\.(mp4|avi|webm)$/i.test(name); +} + +const isPDF = (name) => /\.(pdf)$/i.test(name); + +function getType(name) { + if (isPDF(name)) + return 'pdf'; + + if (isImage(name)) + return 'image'; + + if (isMedia(name)) + return 'media'; } async function getMediaElement(src) { @@ -313,7 +317,7 @@ async function getMediaElement(src) { name, }); - const element = _createElement('div', { + const element = createElement('div', { innerHTML, }); @@ -321,17 +325,21 @@ async function getMediaElement(src) { } function check(src) { - if (!isString(src)) + if (typeof src !== 'string') throw Error('src should be a string!'); } +/** + * function loads css and js of FancyBox + * @callback - executes, when everything loaded + */ async function loadAll() { - const {DIR_DIST} = CloudCmd; + const {prefix} = CloudCmd; - time(`${Name} load`); + time(Name + ' load'); Loading = true; - await loadCSS(`${DIR_DIST}/view.css`); + await loadCSS(`${prefix}/dist/view.css`); Loading = false; } @@ -350,10 +358,13 @@ function setCurrentByPosition(position) { if (!element) return; - const {files, filesPassive} = Info; + const { + files, + filesPassive, + } = Info; - const isFiles = files.includes(element); - const isFilesPassive = filesPassive.includes(element); + const isFiles = ~files.indexOf(element); + const isFilesPassive = ~filesPassive.indexOf(element); if (!isFiles && !isFilesPassive) return; @@ -370,3 +381,4 @@ function listener({keyCode}) { if (keyCode === Key.ESC) hide(); } + diff --git a/client/modules/view/get-type.js b/client/modules/view/get-type.js deleted file mode 100644 index a061399f..00000000 --- a/client/modules/view/get-type.js +++ /dev/null @@ -1,51 +0,0 @@ -import currify from '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) => a.endsWith('.html'); -const isMarkdown = (a) => /.\.md$/.test(a); - -export default (name) => { - if (isPDF(name)) - return 'pdf'; - - if (isImage(name)) - return 'image'; - - if (isMedia(name)) - return 'media'; - - if (isHTML(name)) - return 'html'; - - if (isMarkdown(name)) - return 'markdown'; -}; - -function isImage(name) { - const images = [ - 'jp(e|g|eg)', - 'gif', - 'png', - 'bmp', - 'webp', - 'svg', - 'ico', - ]; - - return images - .map(getRegExp) - .some(testRegExp(name)); -} - -function isMedia(name) { - return isAudio(name) || isVideo(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.spec.js b/client/modules/view/index.spec.js deleted file mode 100644 index abfbe3f4..00000000 --- a/client/modules/view/index.spec.js +++ /dev/null @@ -1,98 +0,0 @@ -import autoGlobals from 'auto-globals'; -import {stub} from '@cloudcmd/stub'; -import {test as tape} from 'supertape'; -import { - _initConfig, - _viewHtml, - _Config, - _createIframe, -} from './index.js'; - -const test = autoGlobals(tape); - -test('cloudcmd: client: view: initConfig', (t) => { - let config; - let i = 0; - - const afterClose = () => ++i; - const options = { - afterClose, - }; - - config = _initConfig(options); - config.afterClose(); - - config = _initConfig(options); - config.afterClose(); - - t.equal(i, 2, 'should not change default config'); - t.end(); -}); - -test('cloudcmd: client: view: initConfig: no options', (t) => { - const config = _initConfig(); - const result = typeof config; - const expected = 'object'; - - t.equal(result, expected); - t.end(); -}); - -test('cloudcmd: client: view: html', (t) => { - const open = stub(); - const modal = { - open, - }; - - const src = '/hello.html'; - - _viewHtml(src, { - modal, - }); - - const [first] = open.args; - const [arg] = first; - - t.deepEqual(first, [arg, _Config]); - t.end(); -}); - -test('cloudcmd: client: view: createIframe', (t) => { - const addEventListener = stub(); - const el = { - addEventListener, - }; - - const createElement = stub().returns(el); - const src = '/hello.html'; - - _createIframe(src, { - createElement, - }); - - const expected = { - src, - height: '100%', - width: '100%', - }; - - t.calledWith(createElement, ['iframe', expected]); - t.end(); -}); - -test('cloudcmd: client: view: createIframe: returns', (t) => { - const addEventListener = stub(); - const el = { - addEventListener, - }; - - const createElement = stub().returns(el); - - const src = '/hello.html'; - 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 deleted file mode 100644 index 320a4bc8..00000000 --- a/client/modules/view/types.js +++ /dev/null @@ -1,74 +0,0 @@ -import {extname} from 'node:path'; -import currify from 'currify'; - -export const isAudio = (name) => /\.(mp3|ogg|m4a|flac)$/i.test(name); - -const testRegExp = currify((name, reg) => reg.test(name)); -const getRegExp = (ext) => RegExp(`\\.${ext}$`, 'i'); - -const isPDF = (a) => /\.pdf$/i.test(a); -const isHTML = (a) => a.endsWith('.html'); -const isMarkdown = (a) => /.\.md$/.test(a); - -export const getType = async (path) => { - const ext = extname(path); - - if (!ext) - path = await detectType(path); - - if (isPDF(path)) - return 'pdf'; - - if (isImage(path)) - return 'image'; - - if (isMedia(path)) - return 'media'; - - if (isHTML(path)) - return 'html'; - - if (isMarkdown(path)) - return 'markdown'; -}; - -export function isImage(name) { - const images = [ - 'jp(e|g|eg)', - 'gif', - 'png', - 'bmp', - 'webp', - 'svg', - 'ico', - ]; - - return images - .map(getRegExp) - .some(testRegExp(name)); -} - -function isMedia(name) { - return isAudio(name) || isVideo(name); -} - -function isVideo(name) { - return /\.(mp4|avi|webm)$/i.test(name); -} - -export const _detectType = detectType; - -async function detectType(path) { - const {headers} = await fetch(path, { - method: 'HEAD', - }); - - for (const [name, value] of headers) { - if (name === 'content-type') - return `.${value - .split('/') - .pop()}`; - } - - return ''; -} diff --git a/client/modules/view/types.spec.js b/client/modules/view/types.spec.js deleted file mode 100644 index f0542c18..00000000 --- a/client/modules/view/types.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import {test, stub} from 'supertape'; -import {isAudio, _detectType} from './types.js'; - -test('cloudcmd: client: view: types: isAudio', (t) => { - const result = isAudio('hello.mp3'); - - t.ok(result); - 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'); - - t.notOk(result); - t.end(); -}); - -test('cloudcmd: client: view: types: detectType', async (t) => { - const fetch = stub().returns({ - headers: [], - }); - - globalThis.fetch = fetch; - await _detectType('/hello'); - - const expected = ['/hello', { - method: 'HEAD', - }]; - - t.calledWith(fetch, expected); - t.end(); -}); - -test('cloudcmd: client: view: types: detectType: found', async (t) => { - globalThis.fetch = stub().returns({ - headers: [ - ['content-type', 'image/png'], - ], - }); - const result = await _detectType('/hello'); - - t.equal(result, '.png'); - t.end(); -}); diff --git a/client/sort.js b/client/sort.js index ed0971f3..f46f61a2 100644 --- a/client/sort.js +++ b/client/sort.js @@ -1,32 +1,39 @@ -/* global CloudCmd */ -import {fullstore} from 'fullstore'; -import DOM from '#dom'; +'use strict'; -const sortPrevious = fullstore(); +/* global CloudCmd */ +const DOM = require('./dom'); + +const Info = DOM.CurrentInfo; + +const { + sort, + order, +} = CloudCmd; + +const position = DOM.getPanelPosition(); + +let sortPrevious = sort[position]; const {getPanel} = DOM; -export const initSortPanel = () => { - const {sort} = CloudCmd; - const position = DOM.getPanelPosition(); +CloudCmd.sortPanel = (name, panel = getPanel()) => { + const position = panel + .dataset + .name + .replace('js-', ''); - sortPrevious(sort[position]); -}; - -export const sortPanel = (name, panel = getPanel()) => { - const {sort, order} = CloudCmd; - const Info = DOM.CurrentInfo; - const position = panel.dataset.name.replace('js-', ''); - - if (name !== sortPrevious()) - order[position] = 'asc'; - else if (order[position] === 'asc') - order[position] = 'desc'; - else + if (name !== sortPrevious) { order[position] = 'asc'; + } else { + if (order[position] === 'asc') + order[position] = 'desc'; + else + order[position] = 'asc'; + } - sortPrevious(name); + sortPrevious = sort[position] = name; + const noCurrent = position !== Info.panelPosition; CloudCmd.refresh({ @@ -34,3 +41,4 @@ export const sortPanel = (name, panel = getPanel()) => { noCurrent, }); }; + diff --git a/client/sw/register.js b/client/sw/register.js index 928d63c9..90c307f7 100644 --- a/client/sw/register.js +++ b/client/sw/register.js @@ -1,10 +1,13 @@ -import {tryToCatch} from 'try-to-catch'; +'use strict'; -export const listenSW = (sw, ...args) => { - sw?.addEventListener(...args); +module.exports.registerSW = registerSW; +module.exports.unregisterSW = unregisterSW; + +module.exports.listenSW = (sw, ...args) => { + sw && sw.addEventListener(...args); }; -export async function registerSW(prefix) { +async function registerSW(prefix) { if (!navigator.serviceWorker) return; @@ -14,17 +17,10 @@ export async function registerSW(prefix) { if (!isHTTPS && !isLocalhost) return; - const {serviceWorker} = navigator; - const register = serviceWorker.register.bind(serviceWorker); - const [e, sw] = await tryToCatch(register, `${prefix}/sw.js`); - - if (e) - return null; - - return sw; + return navigator.serviceWorker.register(`${prefix}/sw.js`); +} +async function unregisterSW(prefix) { + const reg = await registerSW(prefix); + reg && reg.unregister(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 52699f0b..883d4959 100644 --- a/client/sw/register.spec.js +++ b/client/sw/register.spec.js @@ -1,16 +1,16 @@ -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'; +'use strict'; + +const autoGlobals = require('auto-globals'); +const tape = require('supertape'); 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, @@ -18,11 +18,12 @@ test('sw: listen', (t) => { listenSW(sw, 'hello', 'world'); - t.calledWith(addEventListener, ['hello', 'world'], 'should call addEventListener'); + t.ok(addEventListener.calledWith('hello', 'world'), 'should call addEventListener'); t.end(); }); test('sw: lesten: no sw', (t) => { + const {listenSW} = reRequire('./register'); const [e] = tryCatch(listenSW, null, 'hello', 'world'); t.notOk(e, 'should not throw'); @@ -30,6 +31,8 @@ test('sw: lesten: no sw', (t) => { }); test('sw: register: registerSW: no serviceWorker', async (t, {navigator}) => { + const {registerSW} = reRequire('./register'); + delete navigator.serviceWorker; await registerSW(); @@ -43,9 +46,11 @@ test('sw: register: registerSW: no https', async (t, {location, navigator}) => { location.protocol = 'http:'; + const {registerSW} = reRequire('./register'); + await registerSW(); - t.notCalled(register, 'should not call register'); + t.notOk(register.called, 'should not call register'); t.end(); }); @@ -57,24 +62,11 @@ test('sw: register: registerSW: http', async (t, {location, navigator}) => { const {register} = navigator.serviceWorker; + const {registerSW} = reRequire('./register'); + await registerSW(); - t.notCalled(register, 'should not call register'); - 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.notOk(register.called, 'should not call register'); t.end(); }); @@ -82,9 +74,11 @@ 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'); + t.ok(register.calledWith('/hello/sw.js'), 'should call register'); t.end(); }); @@ -96,8 +90,11 @@ 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.ok(register.calledWith('/hello/sw.js'), 'should call register'); t.end(); }); + diff --git a/client/sw/sw.js b/client/sw/sw.js index 5b5643a3..3c849bd8 100644 --- a/client/sw/sw.js +++ b/client/sw/sw.js @@ -1,7 +1,8 @@ -import process from 'node:process'; -import codegen from 'codegen.macro'; -import {tryToCatch} from 'try-to-catch'; -import currify from 'currify'; +'use strict'; + +const codegen = require('codegen.macro'); +const tryToCatch = require('try-to-catch'); +const currify = require('currify'); const isDev = process.env.NODE_ENV === 'development'; @@ -14,7 +15,7 @@ const respondWith = currify((f, e) => { const {url} = request; const pathname = getPathName(url); - if (url.endsWith('/') || /\^\/fs/.test(pathname)) + if (/\/$/.test(url) || /\^\/fs/.test(pathname)) return; if (!isGet(request)) @@ -23,7 +24,7 @@ const respondWith = currify((f, e) => { if (!isBasic(request)) return; - if (pathname.startsWith('/api')) + if (/^\/api/.test(pathname)) return; if (/^socket.io/.test(pathname)) @@ -48,14 +49,14 @@ const getRequest = (a, request) => { return createRequest('/'); }; -globalThis.addEventListener('install', wait(onInstall)); -globalThis.addEventListener('fetch', respondWith(onFetch)); -globalThis.addEventListener('activate', wait(onActivate)); +self.addEventListener('install', wait(onInstall)); +self.addEventListener('fetch', respondWith(onFetch)); +self.addEventListener('activate', wait(onActivate)); async function onActivate() { console.info(`cloudcmd: sw: activate: ${NAME}`); - await globalThis.clients.claim(); + await self.clients.claim(); const keys = await caches.keys(); const deleteCache = caches.delete.bind(caches); @@ -65,7 +66,7 @@ async function onActivate() { async function onInstall() { console.info(`cloudcmd: sw: install: ${NAME}`); - await globalThis.skipWaiting(); + await self.skipWaiting(); } async function onFetch(event) { @@ -96,3 +97,4 @@ async function addToCache(request, response) { const cache = await caches.open(NAME); return cache.put(request, response); } + diff --git a/common/btoa.js b/common/btoa.js new file mode 100644 index 00000000..bf26630b --- /dev/null +++ b/common/btoa.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = (str) => { + if (typeof btoa === 'function') + return btoa(str); + + return Buffer + .from(str) + .toString('base64'); +}; + diff --git a/common/callbackify.js b/common/callbackify.js index 71160ac7..f661eed7 100644 --- a/common/callbackify.js +++ b/common/callbackify.js @@ -1,9 +1,14 @@ +'use strict'; + const success = (f) => (data) => f(null, data); -export default (promise) => (...a) => { - const fn = a.pop(); - - promise(...a) - .then(success(fn)) - .catch(fn); +module.exports = (promise) => { + return (...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 d1586d5a..0b7f6d8c 100644 --- a/common/callbackify.spec.js +++ b/common/callbackify.spec.js @@ -1,26 +1,31 @@ -import {promisify} from 'node:util'; -import {tryToCatch} from 'try-to-catch'; -import {test, stub} from 'supertape'; -import callbackify from './callbackify.js'; +'use strict'; -test('cloudcmd: common: callbackify: error', async (t) => { - const promise = stub().rejects(Error('hello')); +const test = require('supertape'); +const callbackify = require('./callbackify'); + +test('cloudcmd: common: callbackify: error', (t) => { + const promise = async () => { + throw Error('hello'); + }; const fn = callbackify(promise); - const newPromise = promisify(fn); - const [error] = await tryToCatch(newPromise); - t.equal(error.message, 'hello'); - t.end(); + fn((e) => { + t.equal(e.message, 'hello'); + t.end(); + }); }); -test('cloudcmd: common: callbackify', async (t) => { - const promise = stub().resolves('hi'); +test('cloudcmd: common: callbackify', (t) => { + const promise = async () => { + return 'hi'; + }; const fn = callbackify(promise); - const promiseAgain = promisify(fn); - const data = await promiseAgain(); - t.equal(data, 'hi'); - t.end(); + fn((e, data) => { + t.equal(data, 'hi'); + t.end(); + }); }); + diff --git a/common/cloudfunc.js b/common/cloudfunc.js index 7884d069..81124e2a 100644 --- a/common/cloudfunc.js +++ b/common/cloudfunc.js @@ -1,47 +1,50 @@ -import {rendy} from 'rendy'; -import currify from 'currify'; -import {fullstore} from 'fullstore'; -import {encode} from '#common/entity'; +'use strict'; -const id = (a) => a; +const rendy = require('rendy'); +const currify = require('currify'); +const store = require('fullstore'); +const {encode} = require('./entity'); +const btoa = require('./btoa'); + +const getHeaderField = currify(_getHeaderField); + +/* КОНСТАНТЫ (общие для клиента и сервера)*/ + +/* название программы */ const NAME = 'Cloud Commander'; - -export const dateFormatter = fullstore(id); -export const getHeaderField = currify(_getHeaderField); -export const FS = '/fs'; - -const Path = fullstore(); +const FS = '/fs'; +const Path = store(); Path('/'); -const filterOutDotFiles = ({showDotFiles}) => ({name}) => { - if (showDotFiles) - return true; - - return !name.startsWith('.'); -}; +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; -export const apiURL = '/api/v1'; -export const MAX_FILE_SIZE = 500 * 1024; - -export const formatMsg = (msg, name, status) => { +module.exports.formatMsg = (msg, name, status) => { status = status || 'ok'; name = name || ''; if (name) - name = `("${name}")`; + name = '("' + name + '")'; - return `${msg}: ${status}${name}`; + return msg + ': ' + status + name; }; /** * Функция возвращает заголовок веб страницы * @path */ -export const getTitle = (options) => { +module.exports.getTitle = (options) => { options = options || {}; - const {path = Path(), name} = options; + const { + path = Path(), + name, + } = options; const array = [ name || NAME, @@ -57,7 +60,7 @@ export const getTitle = (options) => { * возвращаеться массив каталогов * @param url - адрес каталога */ -export function getPathLink(url, prefix, template) { +function getPathLink(url, prefix, template) { if (!url) throw Error('url could not be empty!'); @@ -68,67 +71,57 @@ export function getPathLink(url, prefix, template) { .split('/') .slice(1, -1); - const allNames = [ - '/', - ...names, - ]; - - const lines = []; - const n = allNames.length; + const allNames = ['/', ...names]; + const length = allNames.length - 1; let path = '/'; - for (let i = 0; i < n; i++) { - const name = allNames[i]; - const isLast = i === n - 1; + const pathHTML = allNames.map((name, index) => { + const isLast = index === length; - if (i) - path += `${name}/`; + if (index) + path += name + '/'; - if (i && isLast) { - lines.push(`${name}/`); - continue; - } + if (index && isLast) + return name + '/'; - const slash = i ? '/' : ''; + const slash = index ? '/' : ''; - lines.push(rendy(template, { + return rendy(template, { path, name, slash, prefix, - })); - } + }); + }).join(''); - return lines.join(''); + return pathHTML; } -export function _getDataName(name) { +const getDataName = (name) => { const encoded = btoa(encodeURI(name)); return `data-name="js-file-${encoded}" `; -} +}; /** * Функция строит таблицу файлв из JSON-информации о файлах * @param params - информация о файлах * */ -export const buildFromJSON = (params) => { +module.exports.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; /* @@ -138,9 +131,9 @@ export const buildFromJSON = (params) => { const htmlPath = getPathLink(path, prefix, template.pathLink); let fileTable = rendy(template.path, { - link: prefix + FS + path, - fullPath: path, - path: htmlPath, + link : prefix + FS + path, + fullPath : path, + path : htmlPath, }); const owner = 'owner'; @@ -151,17 +144,15 @@ export const buildFromJSON = (params) => { const name = getFieldName('name'); const size = getFieldName('size'); const date = getFieldName('date'); - const time = getFieldName('time'); const header = rendy(templateFile, { - tag: 'div', - attribute: 'data-name="js-fm-header" ', - className: 'fm-header', - type: '', + tag : 'div', + attribute : 'data-name="js-fm-header" ', + className : 'fm-header', + type : '', name, size, date, - time, owner, mode, }); @@ -169,7 +160,7 @@ export const buildFromJSON = (params) => { /* сохраняем путь */ Path(path); - fileTable += `${header}
    `; + fileTable += header + '
      '; /* Если мы не в корне */ if (path !== '/') { @@ -178,32 +169,28 @@ export const buildFromJSON = (params) => { const linkResult = rendy(template.link, { link, - title: '..', - name: '..', + title : '..', + name : '..', }); - const dataName = _getDataName('..'); - const attribute = `draggable="true" ${dataName}`; + const dataName = 'data-name="js-file-.." '; + const attribute = 'draggable="true" ' + dataName; /* Сохраняем путь к каталогу верхнего уровня*/ fileTable += rendy(template.file, { - tag: 'li', + tag : 'li', attribute, - className: '', - type: 'directory', - name: linkResult, - size: '<dir>', - date: '--.--.----', - time: '--:--:--', - owner: '.', - mode: '--- --- ---', + className : '', + type : 'directory', + name : linkResult, + size : '<dir>', + date : '--.--.----', + owner : '.', + mode : '--- --- ---', }); } fileTable += files - .filter(filterOutDotFiles({ - showDotFiles, - })) .map(updateField) .map((file) => { const name = encode(file.name); @@ -213,7 +200,6 @@ export const buildFromJSON = (params) => { type, mode, date, - time, owner, size, } = file; @@ -225,7 +211,7 @@ export const buildFromJSON = (params) => { attribute: getAttribute(file.type), }); - const dataName = _getDataName(file.name); + const dataName = getDataName(file.name); const attribute = `draggable="true" ${dataName}`; return rendy(templateFile, { @@ -235,26 +221,25 @@ export const buildFromJSON = (params) => { type, name: linkResult, size, - date: formatDate(date), - time, + date, owner, mode, }); - }) - .join(''); + }).join(''); fileTable += '
    '; return fileTable; }; -const updateField = (file) => ({ - ...file, - date: file.date || '--.--.----', - time: file.time || '--:--:--', - owner: file.owner || 'root', - size: getSize(file), -}); +function updateField(file) { + return { + ...file, + date: file.date || '--.--.----', + owner: file.owner || 'root', + size: getSize(file), + }; +} function getAttribute(type) { if (type === 'directory') @@ -263,10 +248,14 @@ function getAttribute(type) { return 'target="_blank" '; } -export const _getSize = getSize; - -function getSize({size, type}) { - if (type === 'directory') +module.exports._getSize = getSize; +function getSize(file) { + const { + size, + type, + } = file; + + if (/^directory$/.test(type)) return '<dir>'; if (/link/.test(type)) @@ -287,7 +276,7 @@ function _getHeaderField(sort, order, name) { return `${name}${arrow}`; } -export function getDotDot(path) { +function getDotDot(path) { // убираем последний слеш и каталог в котором мы сейчас находимся const lastSlash = path.substr(path, path.lastIndexOf('/')); const dotDot = lastSlash.substr(lastSlash, lastSlash.lastIndexOf('/')); @@ -297,3 +286,4 @@ export function getDotDot(path) { return dotDot; } + diff --git a/common/cloudfunc.spec.js b/common/cloudfunc.spec.js index b679feeb..217d9ca5 100644 --- a/common/cloudfunc.spec.js +++ b/common/cloudfunc.spec.js @@ -1,101 +1,11 @@ -import {readFileSync} from 'node:fs'; -import test from 'supertape'; -import {montag} from 'montag'; -import * as cheerio from 'cheerio'; -import {tryCatch} from 'try-catch'; -import { - _getSize, - getPathLink, - buildFromJSON, - _getDataName, - dateFormatter, - formatMsg, - getTitle, - getDotDot, - getHeaderField, -} from '#common/cloudfunc'; +'use strict'; -const templatePath = new URL('../tmpl/fs', import.meta.url).pathname; - -const template = { - pathLink: readFileSync(`${templatePath}/pathLink.hbs`, 'utf8'), - path: readFileSync(`${templatePath}/path.hbs`, 'utf8'), - file: readFileSync(`${templatePath}/file.hbs`, 'utf8'), - link: readFileSync(`${templatePath}/link.hbs`, 'utf8'), -}; - -test('cloudfunc: buildFromJSON: ..', (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, - }); - - const $ = cheerio.load(html); - const el = $('[data-name="js-file-Li4="]'); - - const result = el - .find('[data-name="js-name"]') - .text(); - - const expected = '..'; - - t.equal(result, expected); - t.end(); -}); - -test('cloudfunc: getPathLink: /', (t) => { - const {pathLink} = template; - const result = getPathLink('/', '', pathLink); - - const expected = montag` - / - `; - - t.equal(result, expected); - t.end(); -}); - -test('cloudfunc: getPathLink: /hello/world', (t) => { - const {pathLink} = template; - const result = getPathLink('/hello/world', '', pathLink); - - const expected = montag` - /hello/ - `; - - t.equal(result, expected); - t.end(); -}); - -test('cloudfunc: getPathLink: prefix', (t) => { - const {pathLink} = template; - const result = getPathLink('/hello/world', '/cloudcmd', pathLink); - - const expected = montag` - /hello/ - `; - - t.equal(result, expected); - t.end(); -}); +const test = require('supertape'); +const {_getSize} = require('./cloudfunc'); test('cloudfunc: getSize: dir', (t) => { const type = 'directory'; const size = 0; - const result = _getSize({ type, size, @@ -103,14 +13,13 @@ test('cloudfunc: getSize: dir', (t) => { const expected = '<dir>'; - t.equal(result, expected); + t.equal(result, expected, 'should equal'); t.end(); }); test('cloudfunc: getSize: link: dir', (t) => { const type = 'directory-link'; const size = 0; - const result = _getSize({ type, size, @@ -118,14 +27,13 @@ test('cloudfunc: getSize: link: dir', (t) => { const expected = '<link>'; - t.equal(result, expected); + t.equal(result, expected, 'should equal'); t.end(); }); test('cloudfunc: getSize: link: file', (t) => { const type = 'file-link'; const size = 0; - const result = _getSize({ type, size, @@ -133,14 +41,13 @@ test('cloudfunc: getSize: link: file', (t) => { const expected = '<link>'; - t.equal(result, expected); + t.equal(result, expected, 'should equal'); t.end(); }); test('cloudfunc: getSize: file', (t) => { const type = 'file'; const size = '100.00kb'; - const result = _getSize({ type, size, @@ -148,217 +55,6 @@ test('cloudfunc: getSize: file', (t) => { const expected = '100.00kb'; - 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: formatMsg: name', (t) => { - const result = formatMsg('hello', 'world'); - const expected = 'hello: ok("world")'; - - t.equal(result, expected, 'should format message with name'); - t.end(); -}); - -test('cloudfunc: formatMsg: no name', (t) => { - const result = formatMsg('hello'); - const expected = 'hello: ok'; - - t.equal(result, expected, 'should format message without name'); - t.end(); -}); - -test('cloudfunc: getTitle: no options', (t) => { - const result = getTitle(); - - t.ok(result, 'should return a title string even without options'); - t.end(); -}); - -test('cloudfunc: getTitle: with name', (t) => { - const result = getTitle({ - name: 'MyName', - }); - - t.match(result, 'MyName', 'should return title with name'); - t.end(); -}); - -test('cloudfunc: getHeaderField: sort not name', (t) => { - const result = getHeaderField('size', 'asc', 'name'); - - t.equal(result, 'name', 'should return plain name when sort does not match'); - t.end(); -}); - -test('cloudfunc: getHeaderField: sort name asc', (t) => { - const result = getHeaderField('name', 'asc', 'name'); - - t.equal(result, 'name', 'should return plain name when name asc'); - t.end(); -}); - -test('cloudfunc: getDotDot: root', (t) => { - const result = getDotDot('/'); - - t.equal(result, '/', 'should return / for root path'); - t.end(); -}); - -test('cloudfunc: getPathLink: /a/b/c', (t) => { - const {pathLink} = template; - const result = getPathLink('/a/b/c/', '', pathLink); - - t.ok(result, 'should build path link for 3-segment path'); - t.end(); -}); - -test('cloudfunc: getPathLink: no url', (t) => { - const [error] = tryCatch(getPathLink); - - t.equal(error.message, 'url could not be empty!', 'should throw when url is empty'); - t.end(); -}); - -test('cloudfunc: getPathLink: no template', (t) => { - const [error] = tryCatch(getPathLink, '/'); - - t.equal(error.message, 'template could not be empty!', 'should throw when template is empty'); - t.end(); -}); - -test('cloudfunc: getHeaderField: sort name desc', (t) => { - const result = getHeaderField('name', 'desc', 'name'); - const expected = 'name↓'; - - t.equal(result, expected, 'should return name with down arrow'); - t.end(); -}); - -test('cloudfunc: getDotDot: normal path', (t) => { - const result = getDotDot('/hello/world/'); - const expected = '/hello'; - - t.equal(result, expected, 'should return parent directory'); - 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.equal(result, expected, 'should equal'); t.end(); }); diff --git a/common/datetime.js b/common/datetime.js index d7982ab4..131c27fb 100644 --- a/common/datetime.js +++ b/common/datetime.js @@ -1,6 +1,8 @@ -import shortdate from 'shortdate'; +'use strict'; -export default (date) => { +const shortdate = require('shortdate'); + +module.exports = (date) => { date = date || new Date(); check(date); diff --git a/common/datetime.spec.js b/common/datetime.spec.js index e3edb2ef..da81f65e 100644 --- a/common/datetime.spec.js +++ b/common/datetime.spec.js @@ -1,6 +1,7 @@ -import {test} from 'supertape'; -import {tryCatch} from 'try-catch'; -import datetime from './datetime.js'; +'use strict'; + +const test = require('supertape'); +const datetime = require('./datetime'); test('common: datetime', (t) => { const dateStr = 'Fri, 17 Aug 2018 10:56:48'; @@ -8,26 +9,26 @@ test('common: datetime', (t) => { const expected = '2018.08.17 10:56:48'; - t.equal(result, expected); + t.equals(result, expected, 'should equal'); t.end(); }); test('common: datetime: no arg', (t) => { - const {Date} = globalThis; + const {Date} = global; let called = false; - - globalThis.Date = class extends Date { + const myDate = class extends Date { constructor() { super(); called = true; } }; + global.Date = myDate; + datetime(); - globalThis.Date = Date; - + global.Date = Date; t.ok(called, 'should call new Date'); t.end(); }); @@ -38,13 +39,14 @@ test('common: 0 before number', (t) => { const expected = '2018.08.17 10:56:08'; - t.equal(result, expected); + t.equals(result, expected, 'should equal'); t.end(); }); test('common: datetime: wrong args', (t) => { - const [error] = tryCatch(datetime, {}); + const fn = () => datetime({}); - t.equal(error.message, 'date should be instanceof Date!', 'should throw'); + t.throws(fn, /date should be instanceof Date!/, 'should throw'); t.end(); }); + diff --git a/common/entity.js b/common/entity.js index 690bbd7a..bad8ef77 100644 --- a/common/entity.js +++ b/common/entity.js @@ -1,14 +1,15 @@ +'use strict'; + const Entities = { + ' ': ' ', '<': '<', '>': '>', '"': '"', - '{': '{', - '}': '}', }; const keys = Object.keys(Entities); -export const encode = (str) => { +module.exports.encode = (str) => { for (const code of keys) { const char = Entities[code]; const reg = RegExp(char, 'g'); @@ -19,7 +20,7 @@ export const encode = (str) => { return str; }; -export const decode = (str) => { +module.exports.decode = (str) => { for (const code of keys) { const char = Entities[code]; const reg = RegExp(code, 'g'); @@ -29,3 +30,4 @@ export const decode = (str) => { return str; }; + diff --git a/common/omit.js b/common/omit.js deleted file mode 100644 index 284c2f5b..00000000 --- a/common/omit.js +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index ff70541c..00000000 --- a/common/omit.spec.js +++ /dev/null @@ -1,18 +0,0 @@ -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 1dcfa3a8..c8a54eab 100644 --- a/common/try-to-promise-all.js +++ b/common/try-to-promise-all.js @@ -1,8 +1,9 @@ -import {tryToCatch} from 'try-to-catch'; +'use strict'; +const tryToCatch = require('try-to-catch'); const all = Promise.all.bind(Promise); -export default async (a) => { +module.exports = async (a) => { const [e, result = []] = await tryToCatch(all, a); return [ @@ -10,3 +11,4 @@ export default async (a) => { ...result, ]; }; + diff --git a/common/try-to-promise-all.spec.js b/common/try-to-promise-all.spec.js index eafb036c..628bc931 100644 --- a/common/try-to-promise-all.spec.js +++ b/common/try-to-promise-all.spec.js @@ -1,25 +1,24 @@ -import {test} from 'supertape'; -import tryToPromiseAll from './try-to-promise-all.js'; +'use strict'; + +const test = require('supertape'); +const tryToPromiseAll = require('./try-to-promise-all'); const resolve = Promise.resolve.bind(Promise); const reject = Promise.reject.bind(Promise); -test('commons: try-to-promise-all', async (t) => { +test('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('commons: try-to-promise-all: error', async (t) => { +test('try-to-promise-all: error', async (t) => { const [e] = await tryToPromiseAll([ reject('a'), ]); @@ -27,3 +26,4 @@ test('commons: try-to-promise-all: error', async (t) => { t.equal(e, 'a'); t.end(); }); + diff --git a/common/util.js b/common/util.js index 422b3c22..8551b1ef 100644 --- a/common/util.js +++ b/common/util.js @@ -1,9 +1,9 @@ -import exec from 'execon'; +'use strict'; -const isString = (a) => typeof a === 'string'; +const exec = require('execon'); -export const escapeRegExp = (str) => { - const isStr = isString(str); +module.exports.escapeRegExp = (str) => { + const isStr = typeof str === 'string'; if (isStr) str = str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); @@ -14,24 +14,25 @@ export const escapeRegExp = (str) => { /** * get regexp from wild card */ -export const getRegExp = (wildcard) => { - const escaped = `^${wildcard // search from start of line +module.exports.getRegExp = (wildcard) => { + const escaped = '^' + wildcard // search from start of line .replace(/\./g, '\\.') .replace(/\*/g, '.*') - .replace('?', '.?')}$`; + .replace('?', '.?') + '$'; // search to end of line - // search to end of line return RegExp(escaped); }; +module.exports.exec = exec; + /** * function gets file extension * * @param name * @return ext */ -export const getExt = (name) => { - const isStr = isString(name); +module.exports.getExt = (name) => { + const isStr = typeof name === 'string'; if (!isStr) return ''; @@ -45,18 +46,18 @@ export const getExt = (name) => { }; /** - * find object by name in array + * find object by name in arrray * * @param array * @param name */ -export const findObjByNameInArr = (array, name) => { +module.exports.findObjByNameInArr = (array, name) => { let ret; if (!Array.isArray(array)) throw Error('array should be array!'); - if (!isString(name)) + if (typeof name !== 'string') throw Error('name should be string!'); array.some((item) => { @@ -88,7 +89,7 @@ export const findObjByNameInArr = (array, name) => { * start timer * @param name */ -export const time = (name) => { +module.exports.time = (name) => { exec.ifExist(console, 'time', [name]); }; @@ -96,6 +97,7 @@ export const time = (name) => { * stop timer * @param name */ -export const timeEnd = (name) => { +module.exports.timeEnd = (name) => { exec.ifExist(console, 'timeEnd', [name]); }; + diff --git a/common/util.spec.js b/common/util.spec.js index 5a19a8be..c9227b46 100644 --- a/common/util.spec.js +++ b/common/util.spec.js @@ -1,18 +1,18 @@ -import test from 'supertape'; -import {tryCatch} from 'try-catch'; -import { +'use strict'; + +const test = require('supertape'); +const {reRequire} = require('mock-require'); +const Util = require('./util'); +const { findObjByNameInArr, getRegExp, escapeRegExp, - getExt, - time, - timeEnd, -} from '#common/util'; +} = Util; test('getExt: no extension', (t) => { const EXT = ''; - const name = 'file-without-extension'; - const ext = getExt(name); + const name = 'file-withot-extension'; + const ext = Util.getExt(name); t.equal(ext, EXT, 'should return "" when extension is none'); t.end(); @@ -21,30 +21,27 @@ test('getExt: no extension', (t) => { test('getExt: return extension', (t) => { const EXT = '.png'; const name = 'picture.png'; - const ext = getExt(name); + const ext = Util.getExt(name); t.equal(ext, EXT, 'should return ".png" in files "picture.png"'); t.end(); }); test('util: getExt: no name', (t) => { - const ext = getExt(); + const ext = Util.getExt(); t.equal(ext, '', 'should return empty string'); t.end(); }); test('util: findObjByNameInArr: no array', (t) => { - const [error] = tryCatch(findObjByNameInArr); - - t.equal(error.message, 'array should be array!', 'should throw when no array'); + t.throws(findObjByNameInArr, /array should be array!/, 'should throw when no array'); t.end(); }); test('util: findObjByNameInArr: no name', (t) => { - const [error] = tryCatch(findObjByNameInArr, []); - - t.equal(error.message, 'name should be string!', 'should throw when no array'); + const fn = () => findObjByNameInArr([]); + t.throws(fn, /name should be string!/, 'should throw when no name'); t.end(); }); @@ -54,7 +51,9 @@ test('util: findObjByNameInArr: object', (t) => { name, }; - const array = [obj]; + const array = [ + obj, + ]; const result = findObjByNameInArr(array, name); @@ -65,7 +64,6 @@ test('util: findObjByNameInArr: object', (t) => { test('util: findObjByNameInArr: array', (t) => { const name = 'hello'; const data = 'abc'; - const item = { name, data, @@ -76,8 +74,10 @@ test('util: findObjByNameInArr: array', (t) => { }; const array = [ - name, - [obj, item], + name, [ + obj, + item, + ], ]; const result = findObjByNameInArr(array, name); @@ -87,9 +87,9 @@ test('util: findObjByNameInArr: array', (t) => { }); test('util: getRegExp', (t) => { - const reg = getRegExp('help?o.*'); + const reg = getRegExp('hel?o.*'); - t.deepEqual(reg, /^help.?o\..*$/, 'should return regexp'); + t.deepEqual(reg, RegExp('^hel.?o\\..*$'), 'should return regexp'); t.end(); }); @@ -103,36 +103,29 @@ test('util: getRegExp: dots', (t) => { test('util: getRegExp: no', (t) => { const reg = getRegExp(''); - t.deepEqual(reg, /^$/, 'should return regexp'); + t.deepEqual(reg, RegExp('^$'), 'should return regexp'); t.end(); }); test('util: escapeRegExp: no str', (t) => { - const result = escapeRegExp(1); - const expected = 1; - - t.equal(result, expected); + t.equal(escapeRegExp(1), 1, 'should equal'); t.end(); }); test('util: escapeRegExp', (t) => { - const result = escapeRegExp('#hello'); - const expected = '\\#hello'; - - t.equal(result, expected); + t.equal(escapeRegExp('#hello'), '\\#hello', 'should equal'); t.end(); }); -test('util: time', (t) => { - const [error] = tryCatch(time, 'test'); +test('util: scope', (t) => { + global.window = {}; + + reRequire('./util'); + + t.pass('should set window in scope'); + + delete global.window; - t.notOk(error, 'should not throw'); t.end(); }); -test('util: timeEnd', (t) => { - const [error] = tryCatch(timeEnd, 'test'); - - t.notOk(error, 'should not throw'); - t.end(); -}); diff --git a/css/columns/name-size-date-time.css b/css/columns/name-size-date-time.css deleted file mode 100644 index f46e63f8..00000000 --- a/css/columns/name-size-date-time.css +++ /dev/null @@ -1,26 +0,0 @@ -.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 9248f700..196a2b7f 100644 --- a/css/columns/name-size-date.css +++ b/css/columns/name-size-date.css @@ -19,7 +19,3 @@ width: 19%; } -.time { - display: none; -} - diff --git a/css/columns/name-size-time.css b/css/columns/name-size-time.css deleted file mode 100644 index 1bbd59de..00000000 --- a/css/columns/name-size-time.css +++ /dev/null @@ -1,24 +0,0 @@ -.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 c02472a5..e58d1844 100644 --- a/css/columns/name-size.css +++ b/css/columns/name-size.css @@ -18,8 +18,3 @@ .date { display: none; } - -.time { - display: none; -} - diff --git a/css/config.css b/css/config.css index 4804374a..ab456d97 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,24 +18,15 @@ padding: 0 12px; font-size: 16px; line-height: 1.5; - color: var(--column-color); - background: var(--internal-background); + color: #555; 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-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -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 { @@ -53,15 +44,9 @@ .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 rgba(0, 0, 0, 0.075), 0 0 1px rgba(102, 175, 233, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 1px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 1px rgba(102, 175, 233, 0.6); } .config .form-control:focus:invalid:focus { @@ -72,8 +57,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 6728c589..42602618 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 ca4a1181..253b3593 100644 --- a/css/icons.css +++ b/css/icons.css @@ -1,144 +1,139 @@ .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::before { - font-family: 'Fontello'; - content: '\e807 '; + 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 '; + 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 '; -} - -.icon-toggle-file-selection::before { - font-family: 'Fontello'; - content: '\e81f '; + font-family : 'Fontello'; + content : '\e811 '; } .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 6b901a8e..6008c2a9 100644 --- a/css/main.css +++ b/css/main.css @@ -1,7 +1,8 @@ -@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); +@import './urls.css'; +@import './reset.css'; +@import './style.css'; +@import './icons.css'; +@import './help.css'; +@import './query.css'; +@import './supports.css'; + diff --git a/css/nojs.css b/css/nojs.css index 350ac4e6..1df4bd12 100644 --- a/css/nojs.css +++ b/css/nojs.css @@ -1,5 +1,4 @@ -.path-icon, -.keyspanel { +.path-icon, .keyspanel { display: none; } diff --git a/css/query.css b/css/query.css index 509f1b45..eb320351 100644 --- a/css/query.css +++ b/css/query.css @@ -1,31 +1,4 @@ -@media only screen and (width >= 1600px) { - .name { - width: 40%; - } - - .size { - width: 10%; - } - - .date { - width: 15%; - } - - .owner { - width: 12%; - } - - .mode { - width: 15%; - } -} - -:root { - --min-one-panel-width: 1155px; - --is-mobile: 0; -} - -@media only screen and (height <= 900px) and (width <= 600px) { +@media only screen and (max-height: 900px) and (max-width: 600px) { .fm { height: 85%; } @@ -35,7 +8,7 @@ } } -@media only screen and (height >= 550px) and (width <= 600px) { +@media only screen and (min-height: 550px) and (max-width: 600px) { .fm { height: 80%; } @@ -45,49 +18,43 @@ } } -@media only screen and (height <= 750px) and (width <= 600px) { +@media only screen and (max-height: 750px) and (max-width: 600px) { .fm { height: 75%; } } -@media only screen and (height <= 450px) and (width <= 600px) { +@media only screen and (max-height: 450px) and (max-width: 600px) { .fm { height: 75%; } } -@media only screen and (width <= 600px) { - :root { - --is-mobile: 1; - } -} - -@media only screen and (height <= 550px) and (width <= 600px) { +@media only screen and (max-height: 550px) and (max-width: 600px) { .fm { height: 65%; } } -@media only screen and (height <= 550px) and (width <= 550px) { +@media only screen and (max-height: 550px) and (max-width: 550px) { .fm { height: 70%; } } -@media only screen and (height >= 850px) and (width >= 650px) { +@media only screen and (min-height: 850px) and (min-width: 650px) { .fm { height: 95%; } } -@media only screen and (height <= 850px) { +@media only screen and (max-height: 850px) { .files { height: 90%; } } -@media only screen and (height <= 700px) and (width >= 600px) { +@media only screen and (max-height: 700px) and (min-width: 600px) { .fm { height: 85%; } @@ -97,13 +64,13 @@ } } -@media only screen and (height <= 450px) { +@media only screen and (max-height: 450px) { .fm { height: 65%; } } -@media only screen and (height <= 640px) and (width <= 360px) { +@media only screen and (max-height: 640px) and (max-width: 360px) { .fm { height: 75%; } @@ -114,8 +81,8 @@ } /* iphone 6 landscape */ -@media only screen and (device-width >= 375px) and (device-width <= 667px) and (orientation: landscape), - only screen and (height <= 360px) and (width <= 640px) { +@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) { .fm { height: 55%; } @@ -125,14 +92,14 @@ } } -@media only screen and (width <= 600px) { +@media only screen and (max-width: 600px) { .panel { font-size: 26px; } /* текущий файл под курсором */ .current-file { - background-color: var(--border-color); + background-color: var(--color-transparent); color: white; } /* делаем иконки под курсом белыми */ @@ -140,30 +107,25 @@ color: white; } - .file::before { - color: rgb(26 224 124 / 56%); + .file::before, .file-link::before { + color: rgba(26, 224, 124, 0.56); content: '\e80d'; } - .file-link::before { - color: rgb(26 224 124 / 56%); - content: '\e81d'; + .current-file .file::before, .file-link::before { + color: white; } /* меняем иконки на шрифтовые */ .mini-icon { - color: rgb(246 224 124 / 56%); - font: 16px 'Fontello'; + color : rgba(246, 224, 124, 0.56); + font : 16px 'Fontello'; background-image: none; - padding: 1%; + padding : 1%; } - .size, - .date, - .owner, - .time, - .mode { - display: none !important; + .size, .date, .owner, .mode { + display: none; } .name { @@ -171,30 +133,14 @@ display: inline-block; } - .directory::before { + .directory::before, .directory-link::before { content: '\e807'; } - .directory-link::before { - content: '\e81e'; - } - - .file, - .file-link { + .file, .file-link { background-image: none; } - .archive, - .archive-link { - background-image: none; - } - - .archive::before, - .archive-link { - color: rgb(26 224 124 / 56%); - content: '\e81d'; - } - /* убираем заголовок */ .fm-header { display: none; @@ -206,26 +152,24 @@ } } -@media only screen and (width >= 601px) and (width <= 785px) { +@media only screen and (min-width: 601px) and (max-width: 785px) { .cmd-button { width: 13%; } } -@media only screen and (width >= 786px) and (width <= 1155px) { +@media only screen and (min-width: 786px) and (max-width: 1155px) { .cmd-button { width: 10%; } } -@media only screen and (width <= 1155px) { +@media only screen and (max-width: 1155px) { .panel { width: 98%; } /* если правая панель не помещаеться - прячем её */ - .panel-right, - .cmd-button#f5, - .cmd-button#f6 { + .panel-right, .cmd-button#f5, .cmd-button#f6 { display: none; } } @@ -238,8 +182,7 @@ border: none; } - .keyspanel, - .panel-right { + .keyspanel, .panel-right { display: none; } diff --git a/css/style.css b/css/style.css index 10820444..5bcd53fb 100644 --- a/css/style.css +++ b/css/style.css @@ -1,19 +1,22 @@ +:root { + --color: rgba(49, 123, 249); + --color-transparent: rgba(49, 123, 249, 0.4); +} + html { - height: 94%; + height : 94%; } body { - width: 100%; - height: 95%; - overflow: hidden; - background-color: var(--background); + width : 100%; + height : 95%; + overflow : hidden; + background-color : white; } -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 { @@ -24,34 +27,31 @@ code { display: none; } -.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; +.fm, .keyspanel { + cursor : default; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -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; - color: var(--column-color); + -webkit-user-select : initial; + -moz-user-select : initial; + -ms-user-select : initial; + -o-user-select : initial; + user-select : text; } -.panel, -.cmd-button { +.panel, .cmd-button { border: 1px solid; - border-color: var(--border-color); + border-color: var(--color-transparent); } .icon { - margin-left: 0.5%; + margin-left : 0.5%; cursor: default; } @@ -61,7 +61,7 @@ code { .path-icon { position: relative; - color: var(--icon-color); + color: #222; } .path-icon:active { @@ -69,41 +69,41 @@ 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(--link-color); - content: attr(data-progress); + position : relative; + bottom : 5px; + left : 16px; + font-size : 10px; + color : var(--color); + content : attr(data-progress); } .cmd-button { width: 5%; height: 30px; margin: 20px 2px 0; - color: var(--icon-color); - background-color: var(--button-background); + color: #222; + background-color: white; transition: ease 0.1s; } @@ -114,7 +114,7 @@ code { .cmd-button:active { color: white; - background-color: var(--link-color); + background-color: var(--color); transition: ease 0.1s; } @@ -126,8 +126,7 @@ a { text-decoration: none; } -a:hover, -a:active { +a:hover, a:active { color: #06e; text-decoration: none; } @@ -151,14 +150,13 @@ a:active { } .fm { - width: 98%; + width : 98%; height: 90%; margin: 26px auto 0; } .fm-header { font-weight: bold; - color: var(--column-color); } .panel-left { @@ -166,30 +164,15 @@ a:active { } .current-file { - box-shadow: 0 0 0 1px var(--border-color) inset; + box-shadow: 0 0 0 1px var(--color-transparent) inset; } .cut-file { opacity: 0.7; } -.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); +.selected-file, .selected-file .name > a { + color: rgb(254, 159, 224); } .panel-right { @@ -207,55 +190,54 @@ a:active { } .selected-panel { - border-color: rgb(254 159 224 / 40%); + border-color: rgba(254, 159, 224, .40); } .keyspanel { 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: 12%; - + display : inline-block; + width : 13%; /* when inline-block * vertical align should be * set top to prevent additional * spaces behind lines */ - vertical-align: top; - color: var(--column-color); + vertical-align : top; } .mode { float: right; - width: 22%; - color: var(--column-color); + width: 18%; } .reduce-text { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + overflow : hidden; + text-overflow : ellipsis; + white-space : nowrap; } .files { @@ -270,3 +252,4 @@ a:active { .files li { overflow: hidden; } + diff --git a/css/supports.css b/css/supports.css index ea2084d2..bf470c2d 100644 --- a/css/supports.css +++ b/css/supports.css @@ -1,6 +1,6 @@ @supports (overflow: overlay) { .files { - overflow-y: auto; + overflow-y: overlay; } .fm-header { diff --git a/css/terminal.css b/css/terminal.css index e7b1fea6..20cf94b4 100644 --- a/css/terminal.css +++ b/css/terminal.css @@ -1,3 +1,4 @@ .terminal { height: 100%; } + diff --git a/css/themes/dark.css b/css/themes/dark.css deleted file mode 100644 index 311a8093..00000000 --- a/css/themes/dark.css +++ /dev/null @@ -1,49 +0,0 @@ -: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 deleted file mode 100644 index e95eb441..00000000 --- a/css/themes/light.css +++ /dev/null @@ -1,9 +0,0 @@ -: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 be90e235..b53c3626 100644 --- a/css/urls.css +++ b/css/urls.css @@ -17,23 +17,20 @@ } @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 { @@ -52,14 +49,6 @@ background-image: url(../img/file-link.png); } -.archive { - background-image: url(../img/archive.png); -} - -.archive-link { - background-image: url(../img/archive-link.png); -} - .loading-svg { background: url(../img/spinner.svg); } @@ -67,3 +56,4 @@ .loading-gif { background: url(../img/spinner.gif); } + diff --git a/css/user-menu.css b/css/user-menu.css index 792747f4..7ce0e971 100644 --- a/css/user-menu.css +++ b/css/user-menu.css @@ -1,7 +1,6 @@ .cloudcmd-user-menu { font-size: 16px; font-family: 'Droid Sans Mono', 'Ubuntu Mono', 'Consolas', monospace; - border: 0; } .cloudcmd-user-menu:focus { @@ -9,7 +8,7 @@ } .cloudcmd-user-menu > option:checked { - box-shadow: 20px -20px 0 2px var(--selected-menu-item-color) inset; + box-shadow: 20px -20px 0 2px var(--color) inset; } .cloudcmd-user-menu-button { @@ -21,3 +20,4 @@ border: 0; overflow: auto; } + diff --git a/css/view.css b/css/view.css index b6fb20ed..4a02cd15 100644 --- a/css/view.css +++ b/css/view.css @@ -15,12 +15,11 @@ } .view-overlay { - display: block; - background: rgb(255 255 255 / 10%); + display : block; + background : rgba(255, 255, 255, 0.1); } -.media, -video { +.media, video { width: 100%; } diff --git a/cssnano.config.js b/cssnano.config.js new file mode 100644 index 00000000..588d0e46 --- /dev/null +++ b/cssnano.config.js @@ -0,0 +1,15 @@ +'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 deleted file mode 100644 index 64c1bde2..00000000 --- a/deno.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "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 0fb72a34..e358cb83 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: "2" +version: '2' services: web: ports: @@ -7,3 +7,4 @@ services: - ~:/root - /:/mnt/fs image: coderaiser/cloudcmd + diff --git a/docker/Dockerfile b/docker/Dockerfile index 7c5d7b64..a24b5e91 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,29 +1,25 @@ 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/cloudcmd +COPY . /usr/src/app WORKDIR / -ENV cloudcmd_terminal=true -ENV cloudcmd_terminal_path=gritty -ENV cloudcmd_open=false +ENV cloudcmd_terminal true +ENV cloudcmd_terminal_path gritty +ENV cloudcmd_open false EXPOSE 8000 -ENTRYPOINT ["/usr/src/cloudcmd/bin/cloudcmd.js"] +ENTRYPOINT ["/usr/src/app/bin/cloudcmd.js"] + diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 5e38ab8a..3c85fce5 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -1,34 +1,29 @@ 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 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 && \ +RUN npm config set package-lock false && \ + npm install --production && \ + apk update && \ + apk add --no-cache bash make g++ python && \ + npm i gritty && \ + npm cache clean --force && \ + apk del make g++ python && \ rm -rf /usr/include /tmp/* /var/cache/apk/* -COPY . /usr/src/cloudcmd +COPY . /usr/src/app WORKDIR / -ENV cloudcmd_terminal=true -ENV cloudcmd_terminal_path=gritty -ENV cloudcmd_open=false -ENV cloudcmd_vim=true +ENV cloudcmd_terminal true +ENV cloudcmd_terminal_path gritty +ENV cloudcmd_open false EXPOSE 8000 -ENTRYPOINT ["/usr/src/cloudcmd/bin/cloudcmd.js"] +ENTRYPOINT ["/usr/src/app/bin/cloudcmd.js"] + diff --git a/docker/Dockerfile.io b/docker/Dockerfile.io deleted file mode 100644 index d5a24459..00000000 --- a/docker/Dockerfile.io +++ /dev/null @@ -1,113 +0,0 @@ -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 deleted file mode 100644 index c6f273b8..00000000 --- a/docker/Dockerfile.slim +++ /dev/null @@ -1,39 +0,0 @@ -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/docker/arm/Dockerfile.arm32v7 b/docker/arm/Dockerfile.arm32v7 new file mode 100644 index 00000000..ca249cae --- /dev/null +++ b/docker/arm/Dockerfile.arm32v7 @@ -0,0 +1,31 @@ +# Dockerfile to be used in an ARMv7 environment like Raspverry PI 4 +FROM arm32v7/node:slim +LABEL maintainer="Coderaiser, tea-mo903" + +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +COPY package.json /usr/src/app/ + +RUN apt-get update && apt-get -y install python3 make g++ + +ENV PYTHON python3 + +RUN npm config set package-lock false && \ + npm install --production && \ + npm i gritty && \ + npm cache clean --force + +RUN apt-get -y remove --purge python3 make g++ + +COPY . /usr/src/app + +WORKDIR / + +ENV cloudcmd_terminal true +ENV cloudcmd_terminal_path gritty +ENV cloudcmd_open false + +EXPOSE 8000 + +ENTRYPOINT ["/usr/src/app/bin/cloudcmd.js"] diff --git a/docker/arm/Dockerfile.arm64v8 b/docker/arm/Dockerfile.arm64v8 new file mode 100644 index 00000000..e27e2e68 --- /dev/null +++ b/docker/arm/Dockerfile.arm64v8 @@ -0,0 +1,31 @@ +# Dockerfile to be used in an ARMv7 environment like Raspverry PI 4 +FROM arm64v8/node:slim +LABEL maintainer="Coderaiser, tea-mo903" + +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +COPY package.json /usr/src/app/ + +RUN apt-get update && apt-get -y install python3 make g++ + +ENV PYTHON python3 + +RUN npm config set package-lock false && \ + npm install --production && \ + npm i gritty && \ + npm cache clean --force + +RUN apt-get -y remove --purge python3 make g++ + +COPY . /usr/src/app + +WORKDIR / + +ENV cloudcmd_terminal true +ENV cloudcmd_terminal_path gritty +ENV cloudcmd_open false + +EXPOSE 8000 + +ENTRYPOINT ["/usr/src/app/bin/cloudcmd.js"] diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 22add654..00000000 --- a/eslint.config.js +++ /dev/null @@ -1,31 +0,0 @@ -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 e144bd82..c886783f 100644 Binary files a/font/fontello.eot and b/font/fontello.eot differ diff --git a/font/fontello.json b/font/fontello.json index c8bad6be..354b93cc 100644 --- a/font/fontello.json +++ b/font/fontello.json @@ -180,24 +180,6 @@ "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 ff664681..ac6e533b 100644 --- a/font/fontello.svg +++ b/font/fontello.svg @@ -1,7 +1,7 @@ -Copyright (C) 2024 by original authors @ fontello.com +Copyright (C) 2019 by original authors @ fontello.com @@ -63,12 +63,6 @@ - - - - - - - + \ No newline at end of file diff --git a/font/fontello.ttf b/font/fontello.ttf index bfec5c43..57d3d331 100644 Binary files a/font/fontello.ttf and b/font/fontello.ttf differ diff --git a/font/fontello.woff b/font/fontello.woff index a23a854d..fd4d9d12 100644 Binary files a/font/fontello.woff and b/font/fontello.woff differ diff --git a/font/fontello.woff2 b/font/fontello.woff2 index f0210ad6..dae85710 100644 Binary files a/font/fontello.woff2 and b/font/fontello.woff2 differ diff --git a/html/index.html b/html/index.html index d366ed1c..00ae977e 100644 --- a/html/index.html +++ b/html/index.html @@ -8,19 +8,16 @@ - + - -
    {{ fm }}
    +
    {{ fm }}
    @@ -41,15 +38,5 @@ - diff --git a/img/archive-link.png b/img/archive-link.png deleted file mode 100644 index b134c2b8..00000000 Binary files a/img/archive-link.png and /dev/null differ diff --git a/img/archive.png b/img/archive.png deleted file mode 100644 index fd4bbccd..00000000 Binary files a/img/archive.png and /dev/null differ diff --git a/img/favicon/favicon-256.png b/img/favicon/favicon-256.png deleted file mode 100644 index 4e01d43c..00000000 Binary files a/img/favicon/favicon-256.png and /dev/null differ diff --git a/img/logo/cloudcmd-hq.png b/img/logo/cloudcmd-hq.png deleted file mode 100644 index 8e4d1d3b..00000000 Binary files a/img/logo/cloudcmd-hq.png and /dev/null differ diff --git a/json/config.json b/json/config.json index 3bbb65f4..54e78bfe 100644 --- a/json/config.json +++ b/json/config.json @@ -5,10 +5,9 @@ "password": "2b64f2e3f9fee1942af9ff60d40aa5a719db33b8ba8dd4864bb4f11e25ca2bee00907de32a59429602336cac832c8f2eeff5177cc14c864dd116c8bf6ca5d9a9", "algo": "sha512WithRSAEncryption", "editor": "edward", - "menu": "aleman", "packer": "tar", "diff": true, - "zip": true, + "zip" : true, "buffer": true, "dirStorage": false, "online": false, @@ -24,7 +23,6 @@ "confirmMove": true, "configDialog": true, "configAuth": true, - "configPort": true, "oneFilePanel": false, "console": true, "syncConsolePath": false, @@ -32,12 +30,10 @@ "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, @@ -48,3 +44,4 @@ "dropbox": false, "dropboxToken": "" } + diff --git a/json/help.json b/json/help.json index 44f79248..84c6dbf9 100644 --- a/json/help.json +++ b/json/help.json @@ -8,7 +8,6 @@ "-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\"", @@ -20,12 +19,10 @@ "--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", @@ -35,7 +32,6 @@ "--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,11 +49,11 @@ "--no-name ": "set default tab name in web browser", "--no-one-file-panel ": "show two file panels", "--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", @@ -69,7 +65,6 @@ "--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 6ed586d1..d8df33c9 100644 --- a/json/modules.json +++ b/json/modules.json @@ -11,7 +11,6 @@ "markdown", "config", "contact", - "command-line", "upload", "operation", "konsole", @@ -22,7 +21,7 @@ ], "remote": [{ "name": "socket", - "version": "4.0.1", + "version": "2.2.0", "local": "/socket.io/socket.io.js", "remote": "https://cdnjs.cloudflare.com/ajax/libs/socket.io/{{ version }}/socket.io.js" }], diff --git a/man/cloudcmd.1 b/man/cloudcmd.1 index dab36a2c..781e01e1 100644 --- a/man/cloudcmd.1 +++ b/man/cloudcmd.1 @@ -31,10 +31,8 @@ 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", "deepword" or "qword" - --menu set menu: "supermenu" or "aleman" + --editor set editor: "dword", "edward" or "deepword" --packer set packer: "tar" or "zip" --root set root directory --prefix set url prefix @@ -49,7 +47,6 @@ 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 @@ -58,7 +55,6 @@ 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 @@ -69,7 +65,6 @@ 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 @@ -82,7 +77,6 @@ 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 c10c360d..97b2810f 100644 --- a/manifest.yml +++ b/manifest.yml @@ -1,3 +1,4 @@ +--- applications: .: name: cloudcmd @@ -6,7 +7,7 @@ applications: info: mem: 512M description: Node.js Application - exec: null + exec: url: ${name}.${target-base} mem: 128M instances: 2 diff --git a/package.json b/package.json index f680e519..d1d893d2 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,13 @@ { "name": "cloudcmd", - "version": "19.19.1", - "type": "module", + "version": "14.6.0", "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+https://github.com/coderaiser/cloudcmd.git" + "url": "git://github.com/coderaiser/cloudcmd.git" }, - "main": "server/cloudcmd.js", "keywords": [ "console", "terminal", @@ -41,6 +38,12 @@ "bin": { "cloudcmd": "bin/cloudcmd.js" }, + "nyc": { + "exclude": [ + "**/*.spec.js", + "test" + ] + }, "scripts": { "start": "madrun start", "start:dev": "madrun start:dev", @@ -48,196 +51,183 @@ "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", + "lint:css": "madrun lint:css", + "spell": "madrun spell", "fix:lint": "madrun fix:lint", - "lint:stream": "madrun lint:stream", + "lint:progress": "madrun lint:progress", "test": "madrun test", - "test:e2e": "madrun test:e2e", "test:client": "madrun test:client", "test:server": "madrun test:server", "wisdom": "madrun wisdom", "wisdom:type": "madrun wisdom:type", + "docker:pull:node": "madrun docker:pull:node", + "docker:pull:alpine": "madrun docker:pull:alpine", + "docker:push": "madrun docker:push", + "docker:push:latest": "madrun docker:push:latest", + "docker:push:alpine": "madrun docker:push:alpine", + "docker:push:alpine:latest": "madrun docker:push:alpine:latest", + "docker:build": "madrun docker:build", + "docker:build:alpine": "madrun docker:build:alpine", + "docker": "madrun docker", + "docker-ci": "madrun docker-ci", + "docker-login": "madrun docker-login", + "docker:alpine": "madrun docker:alpine", + "docker:tag": "madrun docker:tag", + "docker:tag:alpine": "madrun docker:tag:alpine", + "docker:rm:version": "madrun docker:rm:version", + "docker:rm:latest": "madrun docker:rm:latest", + "docker:rm:alpine": "madrun docker:rm:alpine", + "docker:rm:latest-alpine": "madrun docker:rm:latest-alpine", + "docker:rm-old": "madrun docker:rm-old", "coverage": "madrun coverage", - "coverage:report": "madrun coverage:report", "report": "madrun report", "6to5": "madrun 6to5", "6to5:client": "madrun 6to5:client", "6to5:client:dev": "madrun 6to5:client:dev", + "pre6to5:client": "madrun pre6to5:client", + "pre6to5:client:dev": "madrun pre6to5:client:dev", "watch:client": "madrun watch:client", "watch:client:dev": "madrun watch:client:dev", "watch:server": "madrun watch:server", + "watch:lint": "madrun watch:lint", "watch:test": "madrun watch:test", "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" + "heroku-postbuild": "madrun heroku-postbuild", + "putout": "madrun putout" }, "directories": { "man": "man" }, "subdomain": "cloudcmd", "dependencies": { - "@cloudcmd/dropbox": "^5.0.1", - "@cloudcmd/fileop": "^9.0.7", - "@cloudcmd/move-files": "^8.0.0", + "@cloudcmd/dropbox": "^4.0.1", + "@cloudcmd/fileop": "^4.0.0", + "@cloudcmd/move-files": "^3.0.0", "@cloudcmd/read-files-sync": "^2.0.0", - "@putout/cli-validate-args": "^2.0.0", - "@putout/plugin-cloudcmd": "^5.2.0", - "aleman": "^2.0.1", "apart": "^2.0.0", - "chalk": "^5.3.0", + "chalk": "^4.0.0", "compression": "^1.7.4", - "console-io": "^15.0.1", - "copymitter": "^10.3.0", + "console-io": "^11.0.0", + "copymitter": "^5.0.0", "criton": "^2.0.0", "currify": "^4.0.0", "deepmerge": "^4.0.0", - "deepword": "^11.0.0", - "dword": "^16.0.0", - "edward": "^16.0.0", - "es6-promisify": "^7.0.0", + "deepword": "^7.0.0", + "dword": "^12.0.0", + "edward": "^12.0.0", + "es6-promisify": "^6.0.2", "execon": "^1.2.0", - "express": "^5.1.0", - "express-rate-limit": "^8.5.2", - "files-io": "^4.0.0", - "find-up": "^8.0.0", + "express": "^4.13.0", + "files-io": "^3.0.0", + "find-up": "^4.0.0", + "flop": "^8.0.0", "for-each-key": "^2.0.0", "format-io": "^2.0.0", - "fullstore": "^4.0.0", - "http-auth": "^4.2.1", - "inly": "^5.0.0", + "fullstore": "^3.0.0", + "http-auth": "^4.1.2", + "inly": "^4.0.0", "jaguar": "^6.0.0", "jju": "^1.3.0", "jonny": "^3.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", + "markdown-it": "^11.0.0", + "mellow": "^2.0.0", + "minimist": "^1.2.0", "nomine": "^4.0.0", + "object.omit": "^3.0.0", "once": "^1.4.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", + "onezip": "^4.0.0", + "open": "^7.0.0", + "package-json": "^6.0.0", + "ponse": "^5.0.0", + "pullout": "^4.0.0", + "putout": "^8.0.0", + "rendy": "^3.0.0", + "restafary": "^8.0.0", + "restbox": "^2.0.0", "shortdate": "^2.0.0", - "socket.io": "^4.0.0", - "socket.io-client": "^4.0.1", + "socket.io": "^2.0.3", + "socket.io-client": "^2.1.1", "squad": "^3.0.0", - "table": "^6.0.1", - "try-catch": "^4.0.4", - "try-to-catch": "^4.0.0", + "table": "^5.0.2", + "thread-it": "^1.1.0", + "try-catch": "^3.0.0", + "try-to-catch": "^3.0.0", "tryrequire": "^3.0.0", - "win32": "^8.0.0", "wraptile": "^3.0.0", - "writejson": "^3.0.0", - "yargs-parser": "^22.0.0" + "writejson": "^3.0.0" }, "devDependencies": { - "@babel/code-frame": "^8.0.0", - "@babel/core": "^8.0.1", - "@babel/plugin-transform-optional-chaining": "^8.0.1", - "@babel/preset-env": "^8.0.2", + "@babel/code-frame": "^7.5.5", + "@babel/core": "^7.0.0", + "@babel/preset-env": "^7.0.0", "@cloudcmd/clipboard": "^2.0.0", "@cloudcmd/create-element": "^2.0.0", - "@cloudcmd/modal": "^4.0.0", - "@cloudcmd/olark": "^3.0.2", - "@cloudcmd/stub": "^5.0.0", - "@iocmd/wait": "^2.1.0", - "@putout/eslint-flat": "^4.0.0", - "@rspack/cli": "^2.1.4", - "@rspack/core": "^2.1.4", - "@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", - "cheerio": "^1.0.0-rc.5", - "clean-css-loader": "^4.2.1", + "@cloudcmd/modal": "^2.0.0", + "@cloudcmd/olark": "^3.0.0", + "@cloudcmd/stub": "^3.0.0", + "auto-globals": "^1.7.0", + "babel-loader": "^8.0.0", + "babel-plugin-macros": "^2.2.1", + "clean-css-loader": "^2.0.0", "codegen.macro": "^4.0.0", - "css-loader": "^7.1.2", - "css-modules-require-hook": "^4.2.3", - "cssnano-preset-default": "^8.0.1", + "coveralls": "^3.0.0", + "css-loader": "^3.0.0", "domtokenlist-shim": "^1.2.0", "emitify": "^4.0.1", - "eslint": "^10.0.0", - "eslint-plugin-putout": "^31.0.0", - "globals": "^17.0.0", - "gritty": "^10.2.0", + "eslint": "^7.0.0-rc.0", + "eslint-plugin-node": "^11.0.0", + "eslint-plugin-putout": "^4.0.0", + "extract-text-webpack-plugin": "^4.0.0-alpha.0", + "gritty": "^4.7.0", "gunzip-maybe": "^1.3.1", - "html-webpack-plugin": "^5.6.3", + "html-looks-like": "^1.0.2", + "html-webpack-plugin": "^4.0.1", "inherits": "^2.0.3", - "itype": "^3.0.1", - "just-capitalize": "^3.2.0", - "just-pascal-case": "^3.2.0", + "just-capitalize": "^1.0.0", + "just-pascal-case": "^1.1.0", "limier": "^3.0.0", "load.js": "^3.0.0", - "madrun": "^13.0.0", - "memfs": "^4.2.0", + "madrun": "^6.0.0", + "memfs": "^3.0.1", "minor": "^1.2.2", + "mock-require": "^3.0.1", "morgan": "^1.6.1", - "multi-rename": "^3.0.0", - "nodemon": "^3.0.1", - "path-browserify": "^1.0.1", - "philip": "^3.0.0", + "multi-rename": "^2.0.0", + "nodemon": "^2.0.1", + "nyc": "^15.0.0", + "optimize-css-assets-webpack-plugin": "^5.0.0", + "philip": "^2.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": "^6.0.1", - "scroll-into-view-if-needed": "^3.0.4", - "serve-once": "^4.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", + "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": "^1.0.0", + "stylelint": "^13.0.0", + "stylelint-config-standard": "^20.0.0", + "supermenu": "^4.0.1", + "supertape": "^2.0.0", + "tar-stream": "^2.0.0", "unionfs": "^4.0.0", "url-loader": "^4.0.0", - "util": "^0.12.5", - "webpack-merge": "^6.0.1" - }, - "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" + "webpack": "^4.0.0", + "webpack-cli": "^3.0.1", + "webpack-merge": "^4.1.2", + "yaspeller": "^7.0.0" }, "engines": { - "node": ">=22" + "node": ">=10" }, "license": "MIT", + "main": "server/cloudcmd.js", "publishConfig": { "access": "public" } diff --git a/robots.txt b/robots.txt deleted file mode 100644 index 1f53798b..00000000 --- a/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: / diff --git a/rspack.config.js b/rspack.config.js deleted file mode 100644 index 5dcb033d..00000000 --- a/rspack.config.js +++ /dev/null @@ -1,10 +0,0 @@ -import {merge} from 'webpack-merge'; -import * as htmlConfig from './.rspack/html.js'; -import cssConfig from './.rspack/css.js'; -import jsConfig from './.rspack/js.js'; - -export default merge([ - jsConfig, - htmlConfig, - cssConfig, -]); diff --git a/rules/split-autoglobals-with-tape.js b/rules/split-autoglobals-with-tape.js deleted file mode 100644 index 5c3c3eb2..00000000 --- a/rules/split-autoglobals-with-tape.js +++ /dev/null @@ -1,8 +0,0 @@ -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 09da0c2a..69fc85ee 100644 --- a/server/auth.js +++ b/server/auth.js @@ -1,11 +1,12 @@ -import httpAuth from 'http-auth'; -import criton from 'criton'; -import currify from 'currify'; +'use strict'; +const httpAuth = require('http-auth'); +const criton = require('criton'); +const currify = require('currify'); const middle = currify(_middle); const check = currify(_check); -export default (config) => { +module.exports = (config) => { const auth = httpAuth.basic({ realm: 'Cloud Commander', }, check(config)); @@ -15,13 +16,11 @@ export default (config) => { function _middle(config, authentication, req, res, next) { const is = config('auth'); - const {originalUrl} = req; - if (!is || originalUrl.startsWith('/public/')) + if (!is) return next(); - const success = () => next(); - + const success = () => next(/* success */); return authentication.check(success)(req, res); } @@ -39,3 +38,4 @@ function _check(config, username, password, callback) { callback(sameName && samePass); } + diff --git a/server/cloudcmd.js b/server/cloudcmd.js index 2e751520..5fe0d0f6 100644 --- a/server/cloudcmd.js +++ b/server/cloudcmd.js @@ -1,63 +1,59 @@ -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'; +'use strict'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const {assign} = Object; -const DIR = `${__dirname}/`; -const DIR_ROOT = join(DIR, '..'); +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 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; const clean = (a) => a.filter(notEmpty); -const isUndefined = (a) => typeof a === 'undefined'; -const isFn = (a) => typeof a === 'function'; - -export default cloudcmd; - -export function cloudcmd(params) { +module.exports = (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) { @@ -66,7 +62,7 @@ export function cloudcmd(params) { if (/root/.test(name)) validate.root(value, config); - if (/editor|packer|themes|menu/.test(name)) + if (/editor|packer|columns/.test(name)) validate[name](value); if (/prefix/.test(name)) @@ -87,41 +83,37 @@ export function cloudcmd(params) { socket: p.socket, }); - return cloudcmdMiddle({ + return cloudcmd({ modules, config, }); -} - -const depStore = createDepStore(); - -export const createConfigManager = createConfig; -export { - configPath, }; -export const _getIndexPath = getIndexPath; +module.exports.createConfigManager = createConfig; +module.exports.configPath = configPath; + +module.exports._getIndexPath = getIndexPath; function defaultValue(config, name, options) { const value = options[name]; const previous = config(name); - if (isUndefined(value)) + if (typeof value === 'undefined') return previous; return value; } -export const _getPrefix = getPrefix; - +module.exports._getPrefix = getPrefix; function getPrefix(prefix) { - if (isFn(prefix)) + if (typeof prefix === 'function') return prefix() || ''; return prefix || ''; } -export function _initAuth(config, accept, reject, username, password) { +module.exports._initAuth = _initAuth; +function _initAuth(config, accept, reject, username, password) { if (!config('auth')) return accept(); @@ -139,61 +131,57 @@ function listen({prefixSocket, socket, config}) { const auth = initAuth(config); prefixSocket = getPrefix(prefixSocket); - config.listen(socket, auth); + + if (config.listen) + config.listen(socket, auth); edward.listen(socket, { root, auth, - prefixSocket: `${prefixSocket}/edward`, + prefixSocket: prefixSocket + '/edward', }); dword.listen(socket, { root, auth, - prefixSocket: `${prefixSocket}/dword`, - }); - - qword.listen(socket, { - root, - auth, - prefixSocket: `${prefixSocket}/qword`, + prefixSocket: prefixSocket + '/dword', }); 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'), }); - distributeExport(config, socket); + distribute.export(config, socket); } -function cloudcmdMiddle({modules, config}) { +function cloudcmd({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.createStatic({ + const ponseStatic = ponse.static({ cache, root: DIR_ROOT, }); @@ -205,7 +193,9 @@ function cloudcmdMiddle({modules, config}) { config('console') && konsole({ online, }), + config('terminal') && terminal(config, {}), + edward({ root, online, @@ -214,6 +204,7 @@ function cloudcmdMiddle({modules, config}) { dropbox, dropboxToken, }), + dword({ root, online, @@ -222,14 +213,7 @@ function cloudcmdMiddle({modules, config}) { dropbox, dropboxToken, }), - qword({ - root, - online, - diff, - zip, - dropbox, - dropboxToken, - }), + deepword({ root, online, @@ -238,36 +222,38 @@ function cloudcmdMiddle({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, ]); @@ -281,11 +267,9 @@ function logout(req, res, next) { res.sendStatus(401); } -export const _isDev = isDev; -export const _replaceDist = replaceDist; - +module.exports._replaceDist = replaceDist; function replaceDist(url) { - if (!isDev()) + if (!isDev) return url; return url.replace(/^\/dist\//, '/dist-dev/'); @@ -302,17 +286,11 @@ function setUrl(req, res, next) { function setSW(req, res, next) { const {url} = req; - const isSW = /^\/sw\.[mc]?js(\.map)?$/.test(url); + const isSW = /^\/sw\.js(\.map)?$/.test(url); - if (isSW) { - const url = req.url.replace(/[cm]js/, 'js'); + if (isSW) req.url = replaceDist(`/dist${url}`); - } next(); } -assign(cloudcmd, { - depStore, - createConfigManager, -}); diff --git a/server/cloudcmd.spec.js b/server/cloudcmd.spec.js index d79aa4c1..ae4208db 100644 --- a/server/cloudcmd.spec.js +++ b/server/cloudcmd.spec.js @@ -1,23 +1,22 @@ -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, +'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 { createConfigManager, _getPrefix, _initAuth, - _getIndexPath, -} from '#server/cloudcmd'; -import {connect} from '../test/before.js'; +} = cloudcmd; -const noop = () => {}; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const {request} = serveOnce(cloudcmd, { +const {request} = require('serve-once')(cloudcmd, { config: { auth: false, dropbox: false, @@ -53,7 +52,7 @@ test('cloudcmd: getPrefix', (t) => { const value = 'hello'; const result = _getPrefix(value); - t.equal(result, value); + t.equal(result, value, 'should equal'); t.end(); }); @@ -62,7 +61,7 @@ test('cloudcmd: getPrefix: function', (t) => { const fn = () => value; const result = _getPrefix(fn); - t.equal(result, value); + t.equal(result, value, 'should equal'); t.end(); }); @@ -71,34 +70,37 @@ test('cloudcmd: getPrefix: function: empty', (t) => { const fn = () => value; const result = _getPrefix(fn); - t.equal(result, ''); + t.equal(result, '', 'should equal'); t.end(); }); test('cloudcmd: replaceDist', (t) => { - const currentIsDev = _isDev(); + const {NODE_ENV} = process.env; + process.env.NODE_ENV = 'development'; + + const {_replaceDist} = reRequire(cloudcmdPath); - _isDev(true); const url = '/dist/hello'; const result = _replaceDist(url); const expected = '/dist-dev/hello'; - _isDev(currentIsDev); + process.env.NODE_ENV = NODE_ENV; - t.equal(result, expected); + t.equal(result, expected, 'should equal'); t.end(); }); test('cloudcmd: replaceDist: !isDev', (t) => { const url = '/dist/hello'; + const cloudcmdPath = DIR + 'cloudcmd'; - const currentIsDev = _isDev(); - _isDev(false); + const reset = cleanNodeEnv(); + const {_replaceDist} = reRequire(cloudcmdPath); const result = _replaceDist(url); - _isDev(currentIsDev); + reset(); - t.equal(result, url); + t.equal(result, url, 'should equal'); t.end(); }); @@ -110,7 +112,6 @@ test('cloudcmd: auth: reject', (t) => { const username = 'root'; const password = 'toor'; - config('auth', true); config('username', username); config('password', password); @@ -161,87 +162,35 @@ test('cloudcmd: auth: accept: no auth', (t) => { test('cloudcmd: getIndexPath: production', (t) => { const isDev = false; + const name = path.join(__dirname, '..', 'dist', 'index.html'); - const result = _getIndexPath(isDev); - const expected = path.join(__dirname, '..', 'dist', 'index.html'); - - t.equal(result, expected); + t.equal(cloudcmd._getIndexPath(isDev), name); t.end(); }); test('cloudcmd: getIndexPath: development', (t) => { const isDev = true; - const result = _getIndexPath(isDev); - const expected = path.join(__dirname, '..', 'dist-dev', 'index.html'); + const name = path.join(__dirname, '..', 'dist-dev', 'index.html'); - t.equal(result, expected); + t.equal(cloudcmd._getIndexPath(isDev), name); t.end(); }); test('cloudcmd: sw', async (t) => { - const {status} = await request.get('/sw.mjs'); + const {status} = await request.get('/sw.js'); t.equal(status, 200, 'should return sw'); t.end(); }); -test('cloudcmd: no params', (t) => { - const middle = cloudcmd(); +function cleanNodeEnv() { + const {NODE_ENV} = process.env; + process.env.NODE_ENV = ''; - t.ok(Array.isArray(middle), 'should return middleware list when no params passed'); - t.end(); -}); + const reset = () => { + process.env.NODE_ENV = NODE_ENV; + }; + + return reset; +} -test('cloudcmd: listen: terminal', async (t) => { - const {done} = await connect({ - config: { - terminal: true, - }, - }); - - await done(); - - t.pass('should listen with terminal enabled'); - t.end(); -}); - -test('cloudcmd: middle: dropbox', async (t) => { - const {port, done} = await connect({ - config: { - dropbox: true, - dropboxToken: 'hello', - }, - }); - - const response = await fetch(`http://localhost:${port}/api/v1/dropbox/nonexistent`); - - await done(); - - t.ok(response.status, 'should mount dropbox route'); - t.end(); -}); - -test('cloudcmd: logout', async (t) => { - const {status} = await request.get('/logout'); - - t.equal(status, 401, 'should return 401 for /logout'); - t.end(); -}); - -test('cloudcmd: modules', (t) => { - const middle = cloudcmd({ - modules: { - hello: noop, - }, - }); - - t.ok(Array.isArray(middle), 'should return middleware list with modules'); - t.end(); -}); - -test('cloudcmd: setUrl: cloudcmd.js', async (t) => { - const {status} = await request.get('/cloudcmd.js'); - - t.equal(status, 200, 'should serve cloudcmd.js'); - t.end(); -}); diff --git a/server/columns.js b/server/columns.js index eff63957..ac257560 100644 --- a/server/columns.js +++ b/server/columns.js @@ -1,16 +1,9 @@ -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'; +'use strict'; -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 path = require('path'); +const fs = require('fs'); +const readFilesSync = require('@cloudcmd/read-files-sync'); +const isMap = (a) => /\.map$/.test(a); const not = (fn) => (a) => !fn(a); const defaultColumns = { @@ -18,27 +11,19 @@ const defaultColumns = { 'name-size-date-owner-mode': '', }; -const _isDev = fullstore(process.env.NODE_ENV === 'development'); +const isDev = process.env.NODE_ENV === 'development'; const getDist = (isDev) => isDev ? 'dist-dev' : 'dist'; -export const isDev = _isDev; +const dist = getDist(isDev); +const columnsDir = path.join(__dirname, '..', dist, 'columns'); -export const getColumns = ({isDev = _isDev()} = {}) => { - const columns = readFilesSyncMemo(isDev); - - return { - ...columns, - ...defaultColumns, - }; +const names = fs.readdirSync(columnsDir) + .filter(not(isMap)); + +const columns = readFilesSync(columnsDir, names, 'utf8'); + +module.exports = { + ...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 deleted file mode 100644 index 533cfe75..00000000 --- a/server/columns.spec.js +++ /dev/null @@ -1,40 +0,0 @@ -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 77c4a6af..674afe3a 100644 --- a/server/config.js +++ b/server/config.js @@ -1,41 +1,41 @@ -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', -}; +'use strict'; -const isUndefined = (a) => typeof a === 'undefined'; +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'); 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 key = (a) => Object.keys(a).pop(); + +const ConfigPath = path.join(DIR, 'json/config.json'); +const ConfigHome = path.join(HOME, '.cloudcmd.json'); const connection = currify(_connection); - -const connectionWrapped = wraptile(_connection); - +const connectionWraped = wraptile(_connection); const middle = currify(_middle); const readjsonSync = (name) => { @@ -44,6 +44,8 @@ const readjsonSync = (name) => { }); }; +const rootConfig = readjsonSync(ConfigPath); + function read(filename) { if (!filename) return rootConfig; @@ -59,7 +61,8 @@ function read(filename) { }; } -export const configPath = path.join(HOME, '.cloudcmd.json'); +module.exports.createConfig = createConfig; +module.exports.configPath = ConfigHome; const manageListen = currify((manage, socket, auth) => { if (!manage('configDialog')) @@ -77,15 +80,17 @@ function initWrite(filename, configManager) { return resolve; } -export function createConfig({configPath} = {}) { +function createConfig({configPath} = {}) { const config = {}; const changeEmitter = new Emitter(); const configManager = (key, value) => { if (key === '*') - return config; + return { + ...config, + }; - if (isUndefined(value)) + if (value === undefined) return config[key]; config[key] = value; @@ -104,16 +109,15 @@ export function createConfig({configPath} = {}) { }; configManager.unsubscribe = (fn) => { - changeEmitter.off('change', fn); + // replace to off on node v10 + changeEmitter.removeListener('change', fn); }; return configManager; } -const write = (filename, config) => { - return writejson(filename, config('*'), { - mode: 0o600, - }); +const write = async (filename, config) => { + return writejson(filename, config('*'), {mode: 0o600}); }; function _connection(manage, socket) { @@ -131,12 +135,12 @@ function _connection(manage, socket) { const send = () => { const data = CloudFunc.formatMsg('config', key(json)); + socket.broadcast.send(json); socket.send(json); socket.emit('log', data); }; - manage - .write() + manage.write() .then(send) .catch(emit(socket, 'err')); }); @@ -145,18 +149,17 @@ 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(connectionWrapped(manage, socket), reject)); + socket.on('auth', auth(connectionWraped(manage, socket), reject)); }); } -async function _middle(manage, req, res, next) { +function _middle(manage, req, res, next) { const noConfigDialog = !manage('configDialog'); if (req.url !== `${apiURL}/config`) @@ -173,7 +176,7 @@ async function _middle(manage, req, res, next) { .status(404) .send('Config is disabled'); - await patch(manage, req, res); + patch(manage, req, res); break; default: @@ -185,17 +188,16 @@ function get(manage, request, response) { const data = jonny.stringify(manage('*')); ponse.send(data, { - name: 'config.json', + name : 'config.json', request, response, - cache: false, + cache : false, }); } async function patch(manage, request, response) { const name = 'config.json'; const cache = false; - const options = { name, request, @@ -231,8 +233,7 @@ function traverse([manage, json]) { } } -export const _cryptoPass = cryptoPass; - +module.exports._cryptoPass = cryptoPass; function cryptoPass(manage, json) { const algo = manage('algo'); @@ -241,10 +242,9 @@ 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 92b0646c..a9c6ad69 100644 --- a/server/config.spec.js +++ b/server/config.spec.js @@ -1,14 +1,22 @@ -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'; +'use strict'; -const require = createRequire(import.meta.url); -const fixture = require('./config.fixture.json'); +const test = require('supertape'); +const stub = require('@cloudcmd/stub'); +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(); @@ -19,9 +27,7 @@ test('config: manage: get', async (t) => { const configManager = createConfig(); const {done} = await connect({ - config: { - editor, - }, + config: {editor}, configManager, }); @@ -31,15 +37,13 @@ test('config: manage: get', async (t) => { t.end(); }); -test('config: manage: get: config', async (t) => { +test('config: manage: get', async (t) => { const editor = 'deepword'; const conf = { editor, }; - const {done} = await connect({ - config: conf, - }); + const {done} = await connect({config: conf}); config('editor', 'dword'); done(); @@ -92,14 +96,13 @@ test('config: middle: no', (t) => { const res = null; const url = `${apiURL}/config`; const method = 'POST'; - const req = { url, method, }; middle(req, res, next); - - t.calledWithNoArgs(next, 'should call next'); + t.ok(next.calledWith(), 'should call next'); t.end(); }); + diff --git a/server/depstore.js b/server/depstore.js deleted file mode 100644 index c5fefaa9..00000000 --- a/server/depstore.js +++ /dev/null @@ -1,13 +0,0 @@ -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 5e1b54d4..7b2e3641 100644 --- a/server/distribute/export.js +++ b/server/distribute/export.js @@ -1,8 +1,13 @@ -import currify from 'currify'; -import wraptile from 'wraptile'; -import squad from 'squad'; -import {omit} from '#common/omit'; -import log, { +'use strict'; + +const currify = require('currify'); +const wraptile = require('wraptile'); +const squad = require('squad'); +const omit = require('object.omit'); + +const log = require('./log'); + +const { exportStr, connectedStr, disconnectedStr, @@ -10,8 +15,8 @@ import log, { makeColor, getMessage, getDescription, - logWrapped, -} from './log.js'; + logWraped, +} = log; const omitList = [ 'auth', @@ -33,7 +38,7 @@ const omitList = [ const omitConfig = (config) => omit(config, omitList); -export const distributeExport = (config, socket) => { +module.exports = (config, socket) => { if (!config('export')) return; @@ -41,12 +46,14 @@ export const distributeExport = (config, socket) => { const distributePrefix = `${prefix}/distribute`; const isLog = config('log'); - const onError = squad(logWrapped(isLog, exportStr), getMessage); + const onError = squad( + logWraped(isLog, exportStr), + getMessage, + ); - const onConnectError = squad(logWrapped(isLog, exportStr), getDescription); + const onConnectError = squad(logWraped(isLog, exportStr), getDescription); - socket - .of(distributePrefix) + socket.of(distributePrefix) .on('connection', onConnection(push, config)) .on('error', onError) .on('connect_error', onConnectError); @@ -83,11 +90,7 @@ 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('*'))); @@ -122,3 +125,4 @@ 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 ca6155a8..181f22c7 100644 --- a/server/distribute/export.spec.js +++ b/server/distribute/export.spec.js @@ -1,10 +1,12 @@ -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'; +'use strict'; -const config = Config.createConfig(); +const {once} = require('events'); + +const test = require('supertape'); +const io = require('socket.io-client'); + +const {connect} = require('../../test/before'); +const config = require('../config').createConfig(); test('distribute: export', async (t) => { const defaultConfig = { @@ -20,7 +22,7 @@ test('distribute: export', async (t) => { configManager: config, }); - const url = `http://localhost:${port}/distribute?port=1111`; + const url = `http://localhost:${port}/distribute?port=${1111}`; const socket = io.connect(url); await once(socket, 'connect'); @@ -51,7 +53,7 @@ test('distribute: export: config', async (t) => { config: defaultConfig, }); - const url = `http://localhost:${port}/distribute?port=1111`; + const url = `http://localhost:${port}/distribute?port=${1111}`; const socket = io.connect(url); socket.once('connect', () => { @@ -66,3 +68,4 @@ 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 f6cbe614..792114e1 100644 --- a/server/distribute/import.js +++ b/server/distribute/import.js @@ -1,16 +1,14 @@ -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'; +'use strict'; -const isUndefined = (a) => typeof a === 'undefined'; +const currify = require('currify'); +const wraptile = require('wraptile'); +const squad = require('squad'); +const fullstore = require('fullstore'); -const noop = () => {}; -const forEachKey = currify(_forEachKey); +const io = require('socket.io-client'); +const forEachKey = currify(require('for-each-key')); + +const log = require('./log'); const { importStr, @@ -18,16 +16,15 @@ const { disconnectedStr, tokenRejectedStr, authTryStr, + makeColor, + stringToRGB, getMessage, getDescription, - logWrapped, + logWraped, } = 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,21 +33,23 @@ const wrapApply = (f, disconnect) => (status) => () => f(null, { const closeIfNot = wraptile((socket, is) => !is && socket.close()); const addUrl = currify((url, a) => `${url}: ${a}`); +const getColorUrl = (url, name) => { + if (!name) + return url; + + return makeColor(url, stringToRGB(name)); +}; + 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('connect_error', listeners.onConnectError); - socket.removeListener('reject', listeners.onReject); - - if (listeners.onChange) - socket.removeListener('change', listeners.onChange); + socket.removeListener('connection_error', listeners.onError); }); const canceled = (f) => f(null, { status: 'canceled', - disconnect: noop, + disconnect: () => {}, }); const done = wraptile((fn, store) => fn(null, { @@ -63,16 +62,7 @@ const emitAuth = wraptile((importUrl, config, socket) => { socket.emit('auth', config('importToken')); }); -const updateConfig = currify((config, data) => { - for (const [key, value] of entries(data)) { - if (!isUndefined(env.parse(key))) - continue; - - config(key, value); - } -}); - -export const distributeImport = (config, options, fn) => { +module.exports = (config, options, fn) => { fn = fn || options; if (!config('import')) @@ -90,48 +80,60 @@ export const distributeImport = (config, options, fn) => { }); const url = `${importUrl}/distribute?${query}`; - const socket = io.connect(url, { ...options, rejectUnauthorized: false, }); const superFn = wrapApply(fn, socket.close.bind(socket)); - const colorUrl = importUrl; + const colorUrl = getColorUrl(importUrl, name); const close = closeIfNot(socket, importListen); const statusStore = fullstore(); - const statusStoreWrapped = wraptile(statusStore); + const statusStoreWraped = wraptile(statusStore); - const onConfig = squad(close, logWrapped(isLog, importStr, `config received from ${colorUrl}`), statusStoreWrapped('received'), updateConfig(config)); + const onConfig = squad( + close, + logWraped(isLog, importStr, `config received from ${colorUrl}`), + statusStoreWraped('received'), + forEachKey(config), + ); - const onError = squad(superFn('error'), logWrapped(isLog, config, importStr), addUrl(colorUrl), getMessage); + const onError = squad( + superFn('error'), + logWraped(isLog, config, importStr), + addUrl(colorUrl), + getMessage, + ); - const onConnectError = squad(superFn('connect_error'), logWrapped(isLog, importStr), addUrl(colorUrl), getDescription); + const onConnectError = squad( + superFn('connect_error'), + logWraped(isLog, importStr), + addUrl(colorUrl), + getDescription, + ); const onConnect = emitAuth(importUrl, config, socket); - 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(...[ + const onAccept = logWraped(isLog, importStr, `${connectedStr} to ${colorUrl}`); + const onDisconnect = squad( done(fn, statusStore), - logWrapped(isLog, importStr, `${disconnectedStr} from ${colorUrl}`), + logWraped(isLog, importStr, `${disconnectedStr} from ${colorUrl}`), rmListeners(socket, { - onConnect, - onAccept, - onConfig, onError, - onConnectError, - onReject, - onChange, + onConnect, + onConfig, }), - ]); + ); + + const onChange = squad( + logWraped(isLog, importStr), + config, + ); + + const onReject = squad( + superFn('reject'), + logWraped(isLog, importStr, tokenRejectedStr), + ); socket.on('connect', onConnect); socket.on('accept', onAccept); @@ -158,3 +160,4 @@ 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 10e246be..51f82de1 100644 --- a/server/distribute/import.spec.js +++ b/server/distribute/import.spec.js @@ -1,19 +1,16 @@ -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'; +'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'); const distribute = { - import: promisify(distributeImport), + import: promisify(require('./import')), }; const config = createConfigManager(); -process.on('unhandledRejection', console.log); - test('distribute: import: canceled', async (t) => { const {done} = await connect({ config: { @@ -28,7 +25,7 @@ test('distribute: import: canceled', async (t) => { await done(); - t.equal(status, 'canceled'); + t.equal(status, 'canceled', 'should equal'); t.end(); }); @@ -72,7 +69,7 @@ test('distribute: import: received', async (t) => { const {status} = await distribute.import(configManager); await done(); - t.equal(status, 'received'); + t.equal(status, 'received', 'should equal'); t.end(); }); @@ -96,7 +93,7 @@ test('distribute: import: received: auth: reject', async (t) => { const {status} = await distribute.import(configManager); await done(); - t.equal(status, 'reject'); + t.equal(status, 'reject', 'should equal'); t.end(); }); @@ -120,7 +117,7 @@ test('distribute: import: received: auth: accept', async (t) => { const {status} = await distribute.import(configManager); await done(); - t.equal(status, 'received'); + t.equal(status, 'received', 'should equal'); t.end(); }); @@ -142,7 +139,7 @@ test('distribute: import: received: no name', async (t) => { const {status} = await distribute.import(configManager); await done(); - t.equal(status, 'received'); + t.equal(status, 'received', 'should equal'); t.end(); }); @@ -166,7 +163,7 @@ test('distribute: import: error', async (t) => { await done(); - t.equal(status, 'connect_error'); + t.equal(status, 'connect_error', 'should equal'); t.end(); }); @@ -188,98 +185,9 @@ test('distribute: import: config:change: no export', async (t) => { await done(); - t.equal(status, 'connect_error'); + t.equal(status, 'connect_error', 'should equal'); t.end(); }); -test('distribute: import: env', async (t) => { - const configManager = createConfigManager(); - const configManagerImport = createConfigManager(); - - const exporter = await connect({ - configManager, - config: { - name: 'bill', - import: false, - importListen: false, - export: true, - exportToken: 'a', - log: false, - editor: 'edward', - }, - }); - - const importer = await connect({ - configManager: configManagerImport, - config: { - name: 'jack', - import: true, - importToken: 'a', - export: false, - importListen: false, - log: false, - editor: 'deepword', - }, - }); - - process.env.cloudcmd_editor = 'some editor'; - - configManagerImport('importUrl', `http://localhost:${exporter.port}`); - - await distribute.import(configManagerImport); - - await importer.done(); - await exporter.done(); - - delete process.env.cloudcmd_editor; - - const result = configManagerImport('editor'); - const expected = 'deepword'; - - t.equal(result, expected); - t.end(); -}); +process.on('unhandledRejection', console.log); -test('distribute: import: no env', async (t) => { - const configManager = createConfigManager(); - const configManagerImport = createConfigManager(); - - const exporter = await connect({ - configManager, - config: { - name: 'bill', - import: false, - importListen: false, - export: true, - exportToken: 'a', - log: false, - editor: 'edward', - }, - }); - - const importer = await connect({ - configManager: configManagerImport, - config: { - name: 'jack', - import: true, - importToken: 'a', - export: false, - importListen: false, - log: false, - editor: 'deepword', - }, - }); - - configManagerImport('importUrl', `http://localhost:${exporter.port}`); - - await distribute.import(configManagerImport); - - await importer.done(); - await exporter.done(); - - const result = configManagerImport('editor'); - const expected = 'edward'; - - t.equal(result, expected); - t.end(); -}); diff --git a/server/distribute/index.js b/server/distribute/index.js new file mode 100644 index 00000000..9fae4b04 --- /dev/null +++ b/server/distribute/index.js @@ -0,0 +1,4 @@ +'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 acea5759..f08fc58c 100644 --- a/server/distribute/log.js +++ b/server/distribute/log.js @@ -1,27 +1,30 @@ -import wraptile from 'wraptile'; -import chalk from 'chalk'; -import datetime from '../../common/datetime.js'; +'use strict'; -const {assign} = Object; +const wraptile = require('wraptile'); +const chalk = require('chalk'); + +const datetime = require('../../common/datetime'); const log = (isLog, name, msg) => isLog && console.log(`${datetime()} -> ${name}: ${msg}`); +const makeColor = (a, color) => chalk.rgb(color || stringToRGB(a))(a); +const getMessage = (e) => e.message || e; +const getDescription = (e) => `${e.type}: ${e.description}`; -export const makeColor = (a) => chalk.blue(a); -export const getMessage = (e) => e.message || e; -export 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; -export default log; +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'); -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) { +function stringToRGB(a) { return [ a.charCodeAt(0), a.length, @@ -29,7 +32,9 @@ export function stringToRGB(a) { ]; } -const add = (a, b) => a + b.charCodeAt(0); +const add = (a, b) => { + return a + b.charCodeAt(0); +}; function crc(a) { return a @@ -37,16 +42,3 @@ 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 1c6d1aa3..83c7a963 100644 --- a/server/distribute/log.spec.js +++ b/server/distribute/log.spec.js @@ -1,12 +1,14 @@ -import test from 'supertape'; -import log from './log.js'; -import {createConfig} from '../config.js'; +'use strict'; + +const test = require('supertape'); +const log = require('./log'); +const {createConfig} = require('../config'); test('distribute: log: getMessage', (t) => { const e = 'hello'; const result = log.getMessage(e); - t.equal(e, result); + t.equal(e, result, 'should equal'); t.end(); }); @@ -16,7 +18,7 @@ test('distribute: log: getMessage: message', (t) => { message, }); - t.equal(result, message); + t.equal(result, message, 'should equal'); t.end(); }); @@ -28,36 +30,5 @@ test('distribute: log: config', (t) => { log('log', 'test message'); config('log', logOriginal); - t.end(); -}, { - checkAssertionsCount: false, -}); - -test('distribute: log: stringToRGB', (t) => { - const result = log.stringToRGB('abc'); - - t.deepEqual(result, [97, 3, 294], 'should return [charCode, length, crc]'); - t.end(); -}); - -test('distribute: log: makeColor', (t) => { - const result = log.makeColor('hello'); - - t.match(result, 'hello', 'should return colored string containing the input'); - t.end(); -}); - -test('distribute: log: getDescription', (t) => { - const message = 'some error'; - const result = log.getDescription({ - message, - }); - - t.equal(result, message, 'should return message from error object'); - t.end(); -}); - -test('distribute: log: connectedStr', (t) => { - t.ok(log.connectedStr, 'should have connectedStr'); t.end(); }); diff --git a/server/env.js b/server/env.js index 86865307..2a601481 100644 --- a/server/env.js +++ b/server/env.js @@ -1,27 +1,23 @@ -import {env} from 'node:process'; -import snake from 'just-snake-case'; +'use strict'; +const {env} = process; const up = (a) => a.toUpperCase(); -export const bool = (name) => { +module.exports = parse; +module.exports.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; }; -export const parse = (name) => { - const small = `cloudcmd_${snake(name)}`; +function parse(name) { + const small = `cloudcmd_${name}`; const big = up(small); return env[big] || env[small]; -}; +} + diff --git a/server/env.spec.js b/server/env.spec.js index 5a8765f4..2e040e90 100644 --- a/server/env.spec.js +++ b/server/env.spec.js @@ -1,6 +1,7 @@ -import process from 'node:process'; -import {test} from 'supertape'; -import * as env from './env.js'; +'use strict'; + +const test = require('supertape'); +const env = require('./env'); test('cloudcmd: server: env: bool: upper case first', (t) => { const { @@ -20,50 +21,3 @@ test('cloudcmd: server: env: bool: upper case first', (t) => { t.end(); }); -test('cloudcmd: server: env: bool: snake_case', (t) => { - process.env.cloudcmd_config_auth = 'true'; - - const result = env.bool('configAuth'); - - 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(); -}); - -test('cloudcmd: server: env: bool: zero uppercase', (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 ebe5ad8f..f01cd130 100644 --- a/server/exit.js +++ b/server/exit.js @@ -1,10 +1,11 @@ -import process from 'node:process'; +'use strict'; -const getMessage = (a) => a?.message || a; +const getMessage = (a) => a && a.message || a; -export default (...args) => { +module.exports = (...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 94d627ac..85ee3a94 100644 --- a/server/exit.spec.js +++ b/server/exit.spec.js @@ -1,49 +1,49 @@ -import process from 'node:process'; -import {test, stub} from 'supertape'; -import exit from './exit.js'; +'use strict'; + +const test = require('supertape'); +const exit = require('./exit'); +const stub = require('@cloudcmd/stub'); test('cloudcmd: exit: process.exit', (t) => { - const {exit: exitOriginal} = process; - const exitStub = stub(); - - process.exit = exitStub; + const {exit:exitOriginal} = process; + process.exit = stub(); exit(); + t.ok(process.exit.calledWith(1), 'should call process.exit'); process.exit = exitOriginal; - t.calledWith(exitStub, [1], 'should call process.exit'); t.end(); }); test('cloudcmd: exit: console.error', (t) => { - const {exit: exitOriginal} = process; + const {exit:exitOriginal} = process; const {error} = console; - const errorStub = stub(); - console.error = errorStub; + console.error = stub(); process.exit = stub(); exit('hello world'); + t.ok(console.error.calledWith('hello world'), 'should call console.error'); process.exit = exitOriginal; console.error = error; - t.calledWith(errorStub, ['hello world'], 'should call console.error'); t.end(); }); test('cloudcmd: exit.error: console.error: error', (t) => { - const {exit: exitOriginal} = process; + const {exit:exitOriginal} = process; const {error} = console; - const errorStub = stub(); - console.error = errorStub; + console.error = stub(); process.exit = stub(); exit(Error('hello world')); + t.ok(console.error.calledWith('hello world'), 'should call console.error'); + process.exit = exitOriginal; console.error = error; - t.calledWith(errorStub, ['hello world'], 'should call console.error'); t.end(); }); + diff --git a/server/fixture-user-menu/io-cp-fix.js b/server/fixture-user-menu/io-cp-fix.js deleted file mode 100644 index 52f9c0b0..00000000 --- a/server/fixture-user-menu/io-cp-fix.js +++ /dev/null @@ -1,3 +0,0 @@ -async function copy() { - await IO.copy(dirPath, mp3Dir, mp3Names); -} diff --git a/server/fixture-user-menu/io-cp.js b/server/fixture-user-menu/io-cp.js deleted file mode 100644 index 0547933c..00000000 --- a/server/fixture-user-menu/io-cp.js +++ /dev/null @@ -1,7 +0,0 @@ -async function copy() { - await IO.cp({ - from: dirPath, - to: mp3Dir, - names: mp3Names, - }); -} diff --git a/server/fixture-user-menu/io-mv-fix.js b/server/fixture-user-menu/io-mv-fix.js deleted file mode 100644 index d95ccf3f..00000000 --- a/server/fixture-user-menu/io-mv-fix.js +++ /dev/null @@ -1,3 +0,0 @@ -async function move() { - await IO.move(dirPath, mp3Dir, mp3Names); -} diff --git a/server/fixture-user-menu/io-mv.js b/server/fixture-user-menu/io-mv.js deleted file mode 100644 index 56884891..00000000 --- a/server/fixture-user-menu/io-mv.js +++ /dev/null @@ -1,7 +0,0 @@ -async function move() { - await IO.mv({ - from: dirPath, - to: mp3Dir, - names: mp3Names, - }); -} diff --git a/server/markdown/fixture/markdown.zip b/server/markdown/fixture/markdown.zip deleted file mode 100644 index 41e0b976..00000000 Binary files a/server/markdown/fixture/markdown.zip and /dev/null differ diff --git a/server/markdown/index.js b/server/markdown/index.js index 3f80ff03..47acd8b8 100644 --- a/server/markdown/index.js +++ b/server/markdown/index.js @@ -1,32 +1,35 @@ -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'; +'use strict'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const isString = (a) => typeof a === 'string'; +const {readFile} = require('fs').promises; +const {join} = require('path'); +const {callbackify} = require('util'); + +const pullout = require('pullout'); +const ponse = require('ponse'); +const threadIt = require('thread-it'); + +const parse = threadIt(join(__dirname, 'worker')); + +const root = require('../root'); + +threadIt.init(); // warm up parse(''); -const DIR_ROOT = `${__dirname}/../../`; +const DIR_ROOT = __dirname + '/../../'; -export default callbackify(async (name, rootDir, request) => { +module.exports = callbackify(async (name, rootDir, request) => { check(name, request); const {method} = request; switch(method) { case 'GET': - return await onGET(request, name, rootDir); + return onGET(request, name, rootDir); case 'PUT': - return await onPUT(request); + return onPUT(request); } }); @@ -40,10 +43,9 @@ function parseName(query, name, rootDir) { } async function onGET(request, name, root) { - const query = getQuery(request); + const query = ponse.getQuery(request); const fileName = parseName(query, name, root); - const stream = await read(fileName); - const data = await pullout(stream); + const data = await readFile(fileName, 'utf8'); return parse(data); } @@ -54,9 +56,10 @@ async function onPUT(request) { } function check(name, request) { - if (!isString(name)) + if (typeof name !== 'string') 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 f705915e..3e6859d1 100644 --- a/server/markdown/index.spec.js +++ b/server/markdown/index.spec.js @@ -1,38 +1,39 @@ -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'; +'use strict'; +const fs = require('fs'); +const path = require('path'); +const test = require('supertape'); +const {promisify} = require('util'); + +const tryToCatch = require('try-to-catch'); + +const markdown = require('.'); + +const _markdown = promisify(markdown); +const fixtureDir = path.join(__dirname, 'fixture'); +const cloudcmd = require('../..'); const config = { auth: false, }; const configManager = cloudcmd.createConfigManager(); -const {request} = serveOnce(cloudcmd, { +const {request} = require('serve-once')(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.match(body, 'ENOENT', 'should not found'); + t.ok(/ENOENT/.test(body), 'should not found'); t.end(); }); test('cloudcmd: markdown: relative: error', async (t) => { const {body} = await request.get('/api/v1/markdown/not-found?relative'); - t.match(body, 'ENOENT', 'should not found'); + t.ok(/ENOENT/.test(body), 'should not found'); t.end(); }); @@ -44,8 +45,8 @@ test('cloudcmd: markdown: relative', async (t) => { }); test('cloudcmd: markdown: put', async (t) => { - const md = join(fixtureDir, 'markdown.md'); - const html = join(fixtureDir, 'markdown.html'); + const md = path.join(fixtureDir, 'markdown.md'); + const html = path.join(fixtureDir, 'markdown.html'); const mdStream = fs.createReadStream(md); const htmlFile = fs.readFileSync(html, 'utf8'); @@ -59,7 +60,7 @@ test('cloudcmd: markdown: put', async (t) => { }); test('cloudcmd: markdown: put: error', async (t) => { - const md = join(fixtureDir, 'markdown-not-exist.md'); + const md = path.join(fixtureDir, 'markdown-not-exist.md'); const name = 'hello'; const mdStream = fs.createReadStream(md); @@ -69,7 +70,7 @@ test('cloudcmd: markdown: put: error', async (t) => { const [e] = await tryToCatch(_markdown, name, '/', mdStream); - t.match(e.message, 'ENOENT: no such file or directory', 'should emit error'); + t.ok(e.message.includes('ENOENT: no such file or directory'), 'should emit error'); t.end(); }); @@ -87,42 +88,3 @@ test('cloudcmd: markdown: no request', async (t) => { t.end(); }); -test('cloudcmd: markdown', async (t) => { - const configManager = cloudcmd.createConfigManager(); - const fixtureDir = new URL('fixture', import.meta.url).pathname; - - const config = { - auth: false, - root: fixtureDir, - }; - - const {request} = serveOnce(cloudcmd, { - config, - configManager, - }); - - const {body} = await request.get('/api/v1/markdown/markdown.md'); - - t.equal(body, '

    hello

    \n'); - t.end(); -}); - -test('cloudcmd: markdown: zip', async (t) => { - const configManager = cloudcmd.createConfigManager(); - const fixtureDir = new URL('fixture', import.meta.url).pathname; - - const config = { - auth: false, - root: fixtureDir, - }; - - const {request} = serveOnce(cloudcmd, { - config, - configManager, - }); - - const {body} = await request.get('/api/v1/markdown/markdown.zip/markdown.md'); - - t.equal(body, '

    hello

    \n'); - t.end(); -}); diff --git a/server/markdown/worker.js b/server/markdown/worker.js index ccac2aee..ae2a95fd 100644 --- a/server/markdown/worker.js +++ b/server/markdown/worker.js @@ -1,5 +1,4 @@ -import createMarkdownIt from 'markdown-it'; +'use strict'; -const markdownIt = createMarkdownIt(); - -export default (a) => markdownIt.render(a); +const markdownIt = require('markdown-it')(); +module.exports = (a) => markdownIt.render(a); diff --git a/server/modulas.js b/server/modulas.js index 7d38df99..1b32dffa 100644 --- a/server/modulas.js +++ b/server/modulas.js @@ -1,9 +1,9 @@ -import deepmerge from 'deepmerge'; -import originalModules from '../json/modules.json' with { - type: 'json', -}; +'use strict'; -export default (modules) => { +const deepmerge = require('deepmerge'); +const originalModules = require('../json/modules'); + +module.exports = (modules) => { const result = deepmerge(originalModules, modules || {}); return (req, res, next) => { @@ -13,3 +13,4 @@ export default (modules) => { res.send(result); }; }; + diff --git a/server/prefixer.js b/server/prefixer.js index 87a83411..3b5f3a95 100644 --- a/server/prefixer.js +++ b/server/prefixer.js @@ -1,14 +1,15 @@ -const isString = (a) => typeof a === 'string'; +'use strict'; -export default (value) => { - if (!isString(value)) +module.exports = (value) => { + if (typeof value !== 'string') return ''; if (value.length === 1) return ''; - if (value && !value.includes('/')) - return `/${value}`; + if (value && !~value.indexOf('/')) + return '/' + value; return value; }; + diff --git a/server/repl.js b/server/repl.js index 46d58a15..43497a14 100644 --- a/server/repl.js +++ b/server/repl.js @@ -1,25 +1,25 @@ -import process from 'node:process'; -import net from 'node:net'; -import repl from 'node:repl'; +'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); -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 c3a3eda0..27074b92 100644 --- a/server/rest/index.js +++ b/server/rest/index.js @@ -1,51 +1,44 @@ -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'; +'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'); -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; -}; - -export const _UserError = UserError; - -export default currify(({config, fs = _fs, moveFiles = _moveFiles}, request, response, next) => { +module.exports = currify((config, 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({fs, config, moveFiles}, request, response); + rest(config, request, response); }); -function rest({fs, config, moveFiles}, request, response) { +function rest(config, request, response) { const name = ponse.getPathName(request); const params = { request, @@ -53,7 +46,7 @@ function rest({fs, config, moveFiles}, request, response) { name: name.replace(apiURL, '') || '/', }; - sendData(params, {fs, config, moveFiles}, (error, options, data) => { + sendData(params, config, (error, options, data) => { params.gzip = !error; if (!data) { @@ -64,17 +57,17 @@ function rest({fs, config, moveFiles}, request, response) { if (options.name) params.name = options.name; - if (!isUndefined(options.gzip)) + if (options.gzip !== undefined) params.gzip = options.gzip; if (options.query) params.query = options.query; - if (error?.code) + if (error && error.code) return ponse.sendError(error, params); if (error) - return ponse.sendError(error, params); + return ponse.sendError(error.stack, params); ponse.send(data, params); }); @@ -83,13 +76,11 @@ function rest({fs, config, moveFiles}, request, response) { /** * getting data on method and command * - * @param params {name, method, body, request, response} - * @param config {} - * @param callback + * @param params {name, method, body, requrest, response} */ -function sendData(params, {fs, config, moveFiles}, callback) { +function sendData(params, config, callback) { const p = params; - const isMD = p.name.startsWith('/markdown'); + const isMD = RegExp('^/markdown').test(p.name); const rootDir = config('root'); if (isMD) @@ -106,11 +97,10 @@ function sendData(params, {fs, config, moveFiles}, callback) { .then((body) => { onPUT({ name: p.name, - fs, - moveFiles, config, body, - }, callback); + callback, + }); }) .catch(callback); } @@ -126,10 +116,9 @@ function onGET(params, config, callback) { if (p.name[0] === '/') cmd = p.name.replace('/', ''); - if (cmd.startsWith('pack')) { + if (/^pack/.test(cmd)) { cmd = cmd.replace(/^pack/, ''); streamPack(root(cmd, rootDir), p.response, packer); - return; } @@ -155,13 +144,10 @@ function getPackReg(packer) { return /\.tar\.gz$/; } -export const _getPackReg = getPackReg; - function streamPack(cmd, response, packer) { const noop = () => {}; const filename = cmd.replace(getPackReg(packer), ''); const dir = path.dirname(filename); - const names = [ path.basename(filename), ]; @@ -176,22 +162,15 @@ function getCMD(cmd) { return cmd; } -export const _getCMD = getCMD; - -const getMoveMsg = (names) => formatMsg('move', names); - -const getRenameMsg = (from, to) => { - const msg = formatMsg('rename', { - from, - to, - }); +const getMoveMsg = (files) => { + const data = !files.names ? files : files.names.slice(); + const msg = formatMsg('move', data); return msg; }; -export const _onPUT = onPUT; - -function onPUT({name, fs, moveFiles, config, body}, callback) { +module.exports._onPUT = onPUT; +function onPUT({name, config, body, callback}) { checkPut(name, body, callback); const cmd = getCMD(name); @@ -199,37 +178,27 @@ function onPUT({name, fs, moveFiles, config, body}, callback) { const rootDir = config('root'); switch(cmd) { - case 'move': { - const { - from, - to, - names, - } = files; + case 'mv': { + if (!files.from || !files.to) + return callback(body); - if (!from) - return callback(UserError('"from" should be filled')); - - if (!to) - return callback(UserError('"to" should be filled')); - - if (isRootAll(rootDir, [to, from])) + if (isRootAll(rootDir, [files.to, files.from])) return callback(getWin32RootMsg()); - const msg = getMoveMsg(names); + const msg = getMoveMsg(files); const fn = swap(callback, msg); - const fromRooted = root(from, rootDir); - const toRooted = root(to, rootDir); + const from = root(files.from, rootDir); + const to = root(files.to, rootDir); + const {names} = files; - return moveFiles(fromRooted, toRooted, names) + if (!names) + return fs.rename(from, to, fn); + + return moveFiles(from, to, names) .on('error', fn) .on('end', fn); - } - - case 'rename': - return rename(rootDir, files.from, files.to, fs, callback); - - case 'copy': + } case 'cp': if (!files.from || !files.names || !files.to) return callback(body); @@ -266,24 +235,6 @@ function onPUT({name, fs, moveFiles, config, body}, callback) { } } -function rename(rootDir, from, to, fs, callback) { - if (!from) - return callback(UserError('"from" should be filled')); - - if (!to) - return callback(UserError('"to" should be filled')); - - const msg = getRenameMsg(from, to); - const fn = swap(callback, msg); - - const fromRooted = root(from, rootDir); - const toRooted = root(to, rootDir); - - return fs.rename(fromRooted, toRooted, fn); -} - -export const _rename = rename; - function pack(from, to, names, config, fn) { const rootDir = config('root'); const packer = config('packer'); @@ -302,8 +253,6 @@ function pack(from, to, names, config, fn) { operation('pack', packer, from, to, names, fn); } -export const _pack = pack; - function extract(from, to, config, fn) { const rootDir = config('root'); @@ -317,8 +266,6 @@ function extract(from, to, config, fn) { operation('extract', config('packer'), from, to, fn); } -export const _extract = extract; - function getPacker(operation, packer) { if (operation === 'extract') return inly; @@ -329,8 +276,6 @@ function getPacker(operation, packer) { return jaguar.pack; } -export const _getPacker = getPacker; - function operation(op, packer, from, to, names, fn) { if (!fn) { fn = names; @@ -346,7 +291,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', () => { @@ -359,8 +304,6 @@ function operation(op, packer, from, to, names, fn) { }); } -export const _operation = operation; - function copy(from, to, names, fn) { copymitter(from, to, names) .on('error', fn) @@ -380,15 +323,20 @@ const isRootWin32 = currify((root, path) => { return isWin32 && isRoot && isConfig; }); -export const _isRootWin32 = isRootWin32; -export const _isRootAll = isRootAll; +module.exports._isRootWin32 = isRootWin32; +module.exports._isRootAll = isRootAll; -export const _getWin32RootMsg = getWin32RootMsg; +function isRootAll(root, names) { + return names.some(isRootWin32(root)); +} + +module.exports._getWin32RootMsg = getWin32RootMsg; function getWin32RootMsg() { const message = 'Could not copy from/to root on windows!'; + const error = Error(message); - return Error(message); + return error; } function parseData(data) { @@ -400,20 +348,22 @@ function parseData(data) { return json.stringify(data); } -export const _formatMsg = formatMsg; +module.exports._formatMsg = formatMsg; function formatMsg(msg, data, status) { const value = parseData(data); + return CloudFunc.formatMsg(msg, value, status); } function checkPut(name, body, callback) { - if (!isString(name)) + if (typeof name !== 'string') throw Error('name should be a string!'); - if (!isString(body)) + if (typeof body !== 'string') throw Error('body should be a string!'); - if (!isFn(callback)) + if (typeof callback !== 'function') throw Error('callback should be a function!'); } + diff --git a/server/rest/index.spec.js b/server/rest/index.spec.js index 9c672095..bd0c9645 100644 --- a/server/rest/index.spec.js +++ b/server/rest/index.spec.js @@ -1,17 +1,15 @@ -import {test, stub} from 'supertape'; -import {tryToCatch} from 'try-to-catch'; -import { +'use strict'; + +const test = require('supertape'); +const tryToCatch = require('try-to-catch'); + +const { _formatMsg, _getWin32RootMsg, _isRootWin32, _isRootAll, _onPUT, - _UserError, - _getPackReg, - _getCMD, - _rename, - _getPacker, -} from './index.js'; +} = require('.'); test('rest: formatMsg', (t) => { const result = _formatMsg('hello', 'world'); @@ -52,7 +50,6 @@ test('rest: isRootAll', (t) => { test('rest: onPUT: no args', async (t) => { const [e] = await tryToCatch(_onPUT, {}); - t.equal(e.message, 'name should be a string!', 'should throw when no args'); t.end(); }); @@ -76,103 +73,3 @@ test('rest: onPUT: no callback', async (t) => { t.end(); }); -test('rest: UserError: message', (t) => { - const result = _UserError('hello'); - - t.equal(result.message, 'hello', 'should set message'); - t.end(); -}); - -test('rest: UserError: code', (t) => { - const result = _UserError('hello'); - - t.equal(result.code, 'EUSER', 'should set code'); - t.end(); -}); - -test('rest: getPackReg: zip', (t) => { - const result = _getPackReg('zip'); - - t.match('file.zip', result, 'should match .zip'); - t.end(); -}); - -test('rest: getPackReg: tar', (t) => { - const result = _getPackReg('tar'); - - t.match('file.tar.gz', result, 'should match .tar.gz'); - t.end(); -}); - -test('rest: getCMD: with slash', (t) => { - const result = _getCMD('/move'); - - t.equal(result, 'move', 'should strip leading slash'); - t.end(); -}); - -test('rest: getCMD: no slash', (t) => { - const result = _getCMD('move'); - - t.equal(result, 'move', 'should return as is'); - t.end(); -}); - -test('rest: rename: no from', (t) => { - const callback = stub(); - - _rename('/', null, 'to', null, callback); - - const msg = '"from" should be filled'; - - t.calledWith(callback, [ - _UserError(msg), - ], 'should return UserError'); - t.end(); -}); - -test('rest: rename: no to', (t) => { - const callback = stub(); - - _rename('/', 'from', null, null, callback); - - const msg = '"to" should be filled'; - - t.calledWith(callback, [ - _UserError(msg), - ], 'should return UserError'); - t.end(); -}); - -test('rest: rename: success', (t) => { - const callback = stub(); - const fs = { - rename: stub(), - }; - - _rename('/root', 'from', 'to', fs, callback); - - t.calledWith(fs.rename, ['/root/from', '/root/to', callback], 'should call fs.rename'); - t.end(); -}); - -test('rest: getPacker: extract', (t) => { - const result = _getPacker('extract', 'zip'); - - t.equal(typeof result, 'function', 'should return function'); - t.end(); -}); - -test('rest: getPacker: pack zip', (t) => { - const result = _getPacker('pack', 'zip'); - - t.equal(typeof result, 'function', 'should return function'); - t.end(); -}); - -test('rest: getPacker: pack tar', (t) => { - const result = _getPacker('pack', 'tar'); - - t.equal(typeof result, 'function', 'should return function'); - t.end(); -}); diff --git a/server/rest/info.js b/server/rest/info.js index 8cf0e1d7..13758b21 100644 --- a/server/rest/info.js +++ b/server/rest/info.js @@ -1,18 +1,17 @@ -import process from 'node:process'; -import format from 'format-io'; -import info from '../../package.json' with { - type: 'json', -}; +'use strict'; -const {version} = info; +const format = require('format-io'); + +const {version} = require('../../package'); const getMemory = () => { const {rss} = process.memoryUsage(); return format.size(rss); }; -export default (prefix) => ({ +module.exports = (prefix) => ({ version, prefix, memory: getMemory(), }); + diff --git a/server/rest/info.spec.js b/server/rest/info.spec.js index cac19e53..a067cc68 100644 --- a/server/rest/info.spec.js +++ b/server/rest/info.spec.js @@ -1,11 +1,14 @@ -import process from 'node:process'; -import {test, stub} from 'supertape'; -import info from './info.js'; +'use strict'; + +const test = require('supertape'); +const info = require('./info'); +const stub = require('@cloudcmd/stub'); test('cloudcmd: rest: info', (t) => { const {memoryUsage} = process; - const _memoryUsage = stub().returns({}); + const _memoryUsage = stub() + .returns({}); process.memoryUsage = _memoryUsage; @@ -13,6 +16,7 @@ test('cloudcmd: rest: info', (t) => { process.memoryUsage = memoryUsage; - t.calledWithNoArgs(_memoryUsage, 'should call memoryUsage'); + t.ok(_memoryUsage.calledWith(), 'should call memoryUsage'); t.end(); }); + diff --git a/server/root.js b/server/root.js index b563d77d..fcbfcf7e 100644 --- a/server/root.js +++ b/server/root.js @@ -1,5 +1,8 @@ -import mellow from 'mellow'; +'use strict'; -export default (dir, root, {webToWin = mellow.webToWin} = {}) => { - return webToWin(dir, root || '/'); +const mellow = require('mellow'); + +module.exports = (dir, root) => { + return mellow.pathToWin(dir, root || '/'); }; + diff --git a/server/root.spec.js b/server/root.spec.js index 2d9dd768..a8c65079 100644 --- a/server/root.spec.js +++ b/server/root.spec.js @@ -1,16 +1,35 @@ -import {test, stub} from 'supertape'; -import root from './root.js'; +'use strict'; + +const test = require('supertape'); +const stub = require('@cloudcmd/stub'); +const mockRequire = require('mock-require'); +const {reRequire} = mockRequire; + +const pathConfig = './config'; +const pathRoot = './root'; test('cloudcmd: root: mellow', (t) => { - const webToWin = stub(); + const config = stub().returns(''); + const pathToWin = stub(); + const mellow = { + pathToWin, + }; + + mockRequire('mellow', mellow); + mockRequire(pathConfig, config); + + const root = reRequire(pathRoot); const dir = 'hello'; const dirRoot = '/'; - root(dir, '', { - webToWin, - }); + root(dir); - t.calledWith(webToWin, [dir, dirRoot], 'should call mellow'); + mockRequire.stop('mellow'); + mockRequire.stopAll(pathConfig); + reRequire(pathRoot); + + t.ok(pathToWin.calledWith(dir, dirRoot), 'should call mellow'); t.end(); }); + diff --git a/server/route.js b/server/route.js index 7844ce09..200149c0 100644 --- a/server/route.js +++ b/server/route.js @@ -1,25 +1,25 @@ -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'; +'use strict'; -const require = createRequire(import.meta.url); -const {stringify} = JSON; -const {FS} = CloudFunc; +const DIR_SERVER = './'; +const DIR_COMMON = '../common/'; + +const {realpath} = require('fs').promises; + +const {read} = require('flop'); +const ponse = require('ponse'); +const rendy = require('rendy'); +const format = require('format-io'); +const currify = require('currify'); +const tryToCatch = require('try-to-catch'); +const once = require('once'); + +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); const sendIndex = (params, data) => { const ponseParams = { @@ -30,42 +30,40 @@ const sendIndex = (params, data) => { ponse.send(data, ponseParams); }; -const onceRequire = once(require); -const getPrefix = (config) => prefixer(config('prefix')); +const {FS} = CloudFunc; -const getReadDir = (config, {win32 = _win32} = {}) => { +const Columns = require(`${DIR_SERVER}/columns`); +const Template = require(`${DIR_SERVER}/template`); + +const tokenize = (fn, a) => (b) => fn(a, b); +const getReadDir = (config) => { if (!config('dropbox')) - return win32.read; + return read; const {readDir} = onceRequire('@cloudcmd/dropbox'); - return wraptile(readDir, config('dropboxToken')); + return tokenize(readDir, config('dropboxToken')); }; /** * routing of server queries */ -export default currify((config, options, request, response, next) => { +module.exports = 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); }); -export const _getReadDir = getReadDir; +module.exports._getReadDir = getReadDir; async function route({config, options, request, response}) { const name = ponse.getPathName(request); const gzip = true; - const p = { request, response, @@ -78,37 +76,26 @@ async function route({config, options, request, response}) { const rootName = name.replace(CloudFunc.FS, '') || '/'; const fullPath = root(rootName, config('root')); - if (fullPath.indexOf(config('root'))) - return ponse.sendError(Error(`Path '${fullPath}' beyond root '${config('root')}'`), p); + const read = getReadDir(config); + const [error, dir] = await tryToCatch(read, fullPath); + const {html} = options; - const {html, win32} = options; - - const read = getReadDir(config, { - win32, - }); - - const [error, stream] = await tryToCatch(read, fullPath, { - root: config('root'), - }); - - if (error) - return ponse.sendError(error, p); - - if (stream.type === 'directory') { - const {files} = stream; - + if (!error) return sendIndex(p, buildIndex(config, html, { - files, + ...dir, path: format.addSlashToEnd(rootName), })); - } - const {contentLength} = stream; + if (error.code !== 'ENOTDIR') + return ponse.sendError(error, p); - response.setHeader('Content-Length', contentLength); - response.setHeader('Content-Type', contentType(extname(fullPath))); + const [realPathError, pathReal] = await tryToCatch(realpath, fullPath); - await pipe([stream, response]); + ponse.sendFile({ + ...p, + name: realPathError ? name : pathReal, + gzip: false, + }); } /** @@ -134,30 +121,34 @@ 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', - content: panel, - className: !oneFilePanel ? '' : 'panel-single', + side : 'left', + content : panel, + className : !oneFilePanel ? '' : 'panel-single', }); let right = ''; if (!oneFilePanel) right = rendy(Template.panel, { - side: 'right', - content: panel, - className: '', + side : 'right', + content : panel, + className : '', }); const name = config('name'); @@ -168,9 +159,8 @@ function indexProcessing(config, options) { }), fm: left + right, prefix: getPrefix(config), - config: stringify(config('*')), - columns: getColumns()[config('columns')], - themes: getThemes()[config('theme')], + config: JSON.stringify(config('*')), + columns: Columns[config('columns')], }); return data; @@ -181,7 +171,6 @@ function buildIndex(config, html, data) { data, prefix: getPrefix(config), template: Template, - showDotFiles: config('showDotFiles'), }); return indexProcessing(config, { @@ -190,8 +179,7 @@ function buildIndex(config, html, data) { }); } -export const _hideKeysPanel = hideKeysPanel; - +module.exports._hideKeysPanel = hideKeysPanel; function hideKeysPanel(html) { const keysPanel = '
    { const options = { config: { @@ -36,7 +36,7 @@ test('cloudcmd: route: buttons: no console', async (t) => { options, }); - t.match(body, 'icon-console none', 'should hide console'); + t.ok(/icon-console none/.test(body), 'should hide console'); t.end(); }); @@ -70,7 +70,7 @@ test('cloudcmd: route: buttons: no config', async (t) => { options, }); - t.match(body, 'icon-config none', 'should hide config'); + t.ok(/icon-config none/.test(body), 'should hide config'); t.end(); }); @@ -87,7 +87,7 @@ test('cloudcmd: route: buttons: no contact', async (t) => { options, }); - t.match(body, 'icon-contact none', 'should hide contact'); + t.ok(/icon-contact none/.test(body), 'should hide contact'); t.end(); }); @@ -104,11 +104,28 @@ test('cloudcmd: route: buttons: one file panel: move', async (t) => { options, }); - t.match(body, 'icon-move none', 'should hide move button'); + t.ok(/icon-move none/.test(body), 'should hide move button'); t.end(); }); -test('cloudcmd: route: buttons: one file panel: copy', async (t) => { +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.end(); +}); + +test('cloudcmd: route: buttons: one file panel: move', async (t) => { const config = { oneFilePanel: true, }; @@ -121,7 +138,7 @@ test('cloudcmd: route: buttons: one file panel: copy', async (t) => { options, }); - t.match(body, 'icon-copy none', 'should hide copy button'); + t.ok(/icon-copy none/.test(body), 'should hide copy button'); t.end(); }); @@ -138,7 +155,7 @@ test('cloudcmd: route: keys panel: hide', async (t) => { options, }); - t.match(body, 'keyspanel hidden', 'should hide keyspanel'); + t.ok(/keyspanel hidden/.test(body), 'should hide keyspanel'); t.end(); }); @@ -198,113 +215,126 @@ test('cloudcmd: route: not found', async (t) => { options, }); - t.match(body, 'ENOENT: no such file or directory', 'should return error'); + t.ok(~body.indexOf('ENOENT: no such file or directory'), 'should return error'); + t.end(); +}); + +test('cloudcmd: route: realpath: error', async (t) => { + const error = 'realpath error'; + const {realpath} = fs.promises; + + fs.promises.realpath = async () => { + throw error; + }; + + const config = { + root: fixtureDir, + }; + + const options = { + config, + }; + + reRequire(routePath); + const cloudcmd = reRequire(cloudcmdPath); + + const {request} = serveOnce(cloudcmd, { + config: defaultConfig, + }); + + const {body} = await request.get('/fs/empty-file', { + options, + }); + + fs.promises.realpath = realpath; + + t.ok(/^ENOENT/.test(body), 'should return error'); t.end(); }); test('cloudcmd: route: sendIndex: encode', async (t) => { const name = '">'; - const nameEncoded = '"><svg onload=alert(3);>'; - const path = '/'; - + const nameEncoded = '"><svg onload=alert(3);>'; const files = [{ name, }]; - const stream = Readable.from(stringify({ + const read = async (path) => ({ path, files, - })); - - assign(stream, { - path, - files, - type: 'directory', }); - const read = stub().resolves(stream); - - cloudcmd.depStore('win32', { + mockRequire('flop', { read, }); + reRequire(routePath); + const cloudcmd = reRequire(cloudcmdPath); + const {request} = serveOnce(cloudcmd, { configManager: createConfigManager(), }); const {body} = await request.get('/'); - cloudcmd.depStore(); + stopAll(); - t.match(body, nameEncoded, 'should encode name'); + t.ok(body.includes(nameEncoded), 'should encode name'); t.end(); }); test('cloudcmd: route: sendIndex: encode: not encoded', async (t) => { const name = '">'; - const path = '/'; - const files = [{ name, }]; - const stream = Readable.from(stringify({ + const read = async (path) => ({ path, files, - })); - - assign(stream, { - path, - files, - type: 'directory', }); - const read = stub().resolves(stream); - - cloudcmd.depStore('win32', { + mockRequire('flop', { read, }); + reRequire(routePath); + const cloudcmd = reRequire(cloudcmdPath); + const {request} = serveOnce(cloudcmd); const {body} = await request.get('/'); - cloudcmd.depStore(); + stopAll(); t.notOk(body.includes(name), 'should not put not encoded name'); t.end(); }); test('cloudcmd: route: sendIndex: ddos: render', async (t) => { - const name = `$$$'"`; - const path = '/'; - + const name = '$$$\'"'; const files = [{ name, }]; - const stream = Readable.from(stringify({ + const read = async (path) => ({ path, files, - })); - - assign(stream, { - path, - files, - type: 'directory', }); - const read = stub().resolves(stream); - - cloudcmd.depStore('win32', { + mockRequire('flop', { read, }); + + reRequire(routePath); + const cloudcmd = reRequire(cloudcmdPath); + const {request} = serveOnce(cloudcmd, { config: defaultConfig, }); const {status} = await request.get('/'); - cloudcmd.depStore(); + stopAll(); t.equal(status, 200, 'should not hang up'); t.end(); @@ -323,11 +353,11 @@ test('cloudcmd: route: buttons: no terminal', async (t) => { options, }); - t.match(body, 'icon-terminal none', 'should hide terminal'); + t.ok(/icon-terminal none/.test(body), 'should hide terminal'); t.end(); }); -test('cloudcmd: route: no terminal: /fs', async (t) => { +test('cloudcmd: route: no termianl: /fs', async (t) => { const config = { terminal: false, }; @@ -338,12 +368,11 @@ test('cloudcmd: route: no terminal: /fs', async (t) => { }; const {request} = serveOnce(cloudcmd); - const {body} = await request.get('/fs', { options, }); - t.match(body, 'icon-terminal none', 'should hide terminal'); + t.ok(/icon-terminal none/.test(body), 'should hide terminal'); t.end(); }); @@ -361,14 +390,14 @@ test('cloudcmd: route: buttons: terminal: can not load', async (t) => { options, }); - t.match(body, 'icon-terminal none', 'should not enable terminal'); + t.ok(/icon-terminal none/.test(body), 'should not enable terminal'); t.end(); }); test('cloudcmd: route: buttons: terminal', async (t) => { const config = { terminal: true, - terminalPath: 'gritty', + terminalPath: 'console-io', }; const options = { @@ -393,7 +422,6 @@ test('cloudcmd: route: buttons: contact', async (t) => { }; const {request} = serveOnce(cloudcmd); - const {body} = await request.get('/', { options, }); @@ -407,54 +435,12 @@ 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.match(e.message, 'API', 'should contain word token in message'); + t.ok(/token/.test(e.message), 'should contain word token in message'); t.end(); }); -test('cloudcmd: route: content length', async (t) => { - const options = { - root: fixtureDir, - }; - - const {headers} = await request.get('/route.js', { - options, - }); - - const result = headers.get('content-length'); - - t.ok(result); - t.end(); -}); - -test('cloudcmd: route: read: root', async (t) => { - const stream = Readable.from('hello'); - - stream.contentLength = 5; - - const read = stub().returns(stream); - cloudcmd.depStore('win32', { - read, - }); - - const configManager = createConfigManager(); - const root = '/hello'; - - configManager('root', root); - - const {request} = serveOnce(cloudcmd, { - configManager, - }); - - await request.get('/fs/route.js'); - cloudcmd.depStore(); - - const expected = ['/hello/route.js', { - root, - }]; - - t.calledWith(read, expected); - t.end(); -}); diff --git a/server/server.js b/server/server.js index 06b6464a..394b62e1 100644 --- a/server/server.js +++ b/server/server.js @@ -1,74 +1,69 @@ -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'; +'use strict'; -const RATE_LIMIT = 1000; -const RATE_WINDOW = 15 * 60 * 1000; +const DIR_SERVER = './'; +const cloudcmd = require(DIR_SERVER + 'cloudcmd'); -const bind = (f, self) => f.bind(self); +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 shutdown = wraptile(async (config, promises) => { - if (config('log')) - console.log('closing cloudcmd...'); - +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...'); await Promise.all(promises); + threadIt.terminate(); process.exit(0); }); -const promisifySelf = squad(promisify, bind); - -const exitPort = two(exit, 'cloudcmd --port: %s'); +const opn = require('open'); +const express = require('express'); +const io = require('socket.io'); +const tryRequire = require('tryrequire'); const logger = tryRequire('morgan'); -export default async (options, config) => { +module.exports = 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 (config('log') && logger) + if (logger) app.use(logger('dev')); if (prefix) - app.get('/', (req, res) => res.redirect(`${prefix}/`)); + app.get('/', (req, res) => res.redirect(prefix + '/')); - const socketServer = new Server(server, { + const socketServer = io(server, { path: `${prefix}/socket.io`, }); - 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, configManager: config, })); - if (port < 0 || port > 65_535) + if (port < 0 || port > 65535) return exitPort('port number could be 1..65535, 0 means any available port'); const listen = promisifySelf(server.listen, server); @@ -78,23 +73,22 @@ export default async (options, config) => { server.on('error', exitPort); await listen(port, ip); - const close = shutdown(config, [closeServer, closeSocket]); - + const close = shutdown([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}/`; - - if (config('log')) - console.log(`url: ${url}`); + console.log(`url: ${url}`); if (!config('open')) return; - const [openError] = await tryToCatch(opn, url); + const [openError] = await tryToCatch(opn, url, { + url: true, + }); if (openError) console.error('cloudcmd --open:', openError.message); }; + diff --git a/server/show-config.js b/server/show-config.js index f151dd10..c7ee2dec 100644 --- a/server/show-config.js +++ b/server/show-config.js @@ -1,17 +1,16 @@ -import { +'use strict'; + +const { table, getBorderCharacters, -} from 'table'; +} = require('table'); -export const showConfig = (config) => { +module.exports = (config) => { check(config); - const data = Object - .keys(config) - .map((name) => [ - name, - config[name], - ]); + const data = Object.keys(config).map((name) => { + return [name, config[name]]; + }); if (!data.length) return ''; @@ -34,3 +33,4 @@ function check(config) { if (typeof config !== 'object') throw Error('config should be an object!'); } + diff --git a/server/template.js b/server/template.js index 0e705e79..150b63fd 100644 --- a/server/template.js +++ b/server/template.js @@ -1,9 +1,8 @@ -import path, {dirname} from 'node:path'; -import {fileURLToPath} from 'node:url'; -import readFilesSync from '@cloudcmd/read-files-sync'; +'use strict'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const path = require('path'); +const readFilesSync = require('@cloudcmd/read-files-sync'); const templatePath = path.join(__dirname, '..', 'tmpl/fs'); -export default readFilesSync(templatePath, 'utf8'); +module.exports = readFilesSync(templatePath, 'utf8'); + diff --git a/server/terminal.js b/server/terminal.js index c6d80df9..9d1e9c35 100644 --- a/server/terminal.js +++ b/server/terminal.js @@ -1,7 +1,6 @@ -import {createRequire} from 'node:module'; -import {tryCatch} from 'try-catch'; +'use strict'; -const require = createRequire(import.meta.url); +const tryCatch = require('try-catch'); const noop = (req, res, next) => { next && next(); @@ -9,18 +8,11 @@ const noop = (req, res, next) => { noop.listen = noop; -const parseDefault = (a) => a.default || a; -const _getModule = (a) => parseDefault(require(a)); - -export default (config, arg, overrides = {}) => { - const { - getModule = _getModule, - } = overrides; - +module.exports = (config, arg) => { if (!config('terminal')) return noop; - const [e, terminalModule] = tryCatch(getModule, config('terminalPath')); + const [e, terminalModule] = tryCatch(require, config('terminalPath')); if (!e && !arg) return terminalModule; @@ -33,3 +25,4 @@ export default (config, arg, overrides = {}) => { return noop; }; + diff --git a/server/terminal.spec.js b/server/terminal.spec.js index 22f20282..2b6d8f99 100644 --- a/server/terminal.spec.js +++ b/server/terminal.spec.js @@ -1,6 +1,13 @@ -import {test, stub} from 'supertape'; -import {createConfigManager} from '#server/cloudcmd'; -import terminal from './terminal.js'; +'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'); test('cloudcmd: terminal: disabled', (t) => { const config = createConfigManager(); @@ -25,19 +32,18 @@ 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); - terminal(config, arg, { - getModule, - }); + mockRequire(terminalPath, term); - t.calledWith(term, [arg], 'should call terminal'); + const terminal = require(terminalPath); + terminal(arg); + + t.ok(term.calledWith(arg), 'should call terminal'); t.end(); }); test('cloudcmd: terminal: enabled: no string', (t) => { - const {log: originalLog} = console; + const {log:originalLog} = console; const log = stub(); console.log = log; @@ -49,25 +55,26 @@ test('cloudcmd: terminal: enabled: no string', (t) => { console.log = originalLog; - const msg = `cloudcmd --terminal: Cannot find module 'hello'`; + const msg = 'cloudcmd --terminal: Cannot find module \'hello\''; const [arg] = log.args[0]; - t.match(arg, RegExp(msg), 'should call with msg'); + t.ok(arg.includes(msg), 'should call with msg'); t.end(); }); test('cloudcmd: terminal: no arg', (t) => { const gritty = {}; - const getModule = stub().returns(gritty); - const config = createConfigManager(); + mockRequire('gritty', gritty); + const config = createConfigManager(); config('terminal', true); config('terminalPath', 'gritty'); - const result = terminal(config, '', { - getModule, - }); + const result = terminal(config); - t.equal(result, gritty); + mockRequire.stop('gritty'); + + t.equal(result, gritty, 'should equal'); t.end(); }); + diff --git a/server/theme.js b/server/theme.js deleted file mode 100644 index d66623b5..00000000 --- a/server/theme.js +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 709896e8..00000000 --- a/server/themes.spec.js +++ /dev/null @@ -1,50 +0,0 @@ -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(); -}); - -test('themes: production', (t) => { - const themes = getThemes({ - isDev: false, - }); - - t.ok(themes.dark, 'should have dark theme'); - t.end(); -}); - -test('themes: production: light', (t) => { - const themes = getThemes({ - isDev: false, - }); - - t.ok(themes.light, 'should have light theme'); - t.end(); -}); diff --git a/server/user-menu.js b/server/user-menu.js index 53599553..4917327b 100644 --- a/server/user-menu.js +++ b/server/user-menu.js @@ -1,19 +1,27 @@ -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'; +'use strict'; + +const {homedir} = require('os'); +const {readFile} = require('fs').promises; + +const {join} = require('path'); + +const tryToCatch = require('try-to-catch'); +const currify = require('currify'); +const findUp = require('find-up'); +const threadIt = require('thread-it'); +const {codeframe} = require('putout'); +const putout = threadIt(require.resolve('putout')); + +threadIt.init(); // warm up worker cache transpile(''); -const PREFIX = '/api/v1/user-menu'; +const URL = '/api/v1/user-menu'; +const DEFAULT_MENU_PATH = join(__dirname, '../static/user-menu.js'); -export const userMenu = currify(async ({menuName, readFile = _readFile, config}, req, res, next) => { - if (!req.url.startsWith(PREFIX)) +module.exports = currify(async ({menuName}, req, res, next) => { + if (req.url.indexOf(URL)) return next(); const {method} = req; @@ -23,33 +31,26 @@ export const userMenu = currify(async ({menuName, readFile = _readFile, config}, req, res, menuName, - readFile, - config, }); next(); }); -async function onGET({req, res, menuName, readFile, config}) { +async function onGET({req, res, menuName}) { const {dir} = req.query; - const url = req.url.replace(PREFIX, ''); + const url = req.url.replace(URL, ''); if (url === '/default') - return await sendDefaultMenu(res, { - readFile, - }); + return sendDefaultMenu(res); - const {findUp} = await import('find-up'); - - const cwd = join(config('root'), dir); - const [errorFind, currentMenuPath] = await tryToCatch(findUp, [menuName], { - cwd, - }); + const [errorFind, currentMenuPath] = await tryToCatch(findUp, [ + menuName, + ], {cwd: dir}); if (errorFind && errorFind.code !== 'ENOENT') return res .status(404) - .send(errorFind.message); + .send(e.message); const homeMenuPath = join(homedir(), menuName); const menuPath = currentMenuPath || homeMenuPath; @@ -61,9 +62,7 @@ async function onGET({req, res, menuName, readFile, config}) { .send(e.message); if (e) - return await sendDefaultMenu(res, { - readFile, - }); + return sendDefaultMenu(res); const [parseError, result] = await transpile(source); @@ -78,12 +77,12 @@ async function onGET({req, res, menuName, readFile, config}) { } function getError(error, source) { - return montag` + return ` const e = Error(\`
    ${codeframe({
    -            error,
    -            source,
    -            highlightCode: false,
    -        })}
    \`); + error, + source, + highlightCode: false, + })}\`); e.code = 'frame'; @@ -91,35 +90,17 @@ function getError(error, source) { `; } -async function sendDefaultMenu(res, {readFile}) { - const menu = await getDefaultMenu({ - readFile, +function sendDefaultMenu(res) { + res.sendFile(DEFAULT_MENU_PATH, { + cacheControl: false, }); - - res - .type('js') - .send(menu); } -async function transpile(source) { - return await tryToCatch(putout, source, { - rules: { - 'nodejs/convert-esm-to-commonjs': 'on', - }, +function transpile(source) { + return tryToCatch(putout, source, { plugins: [ - 'nodejs', - 'cloudcmd', + 'convert-esm-to-commonjs', + 'strict-mode', ], }); } - -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 7c5fcf2c..2400e404 100644 --- a/server/user-menu.spec.js +++ b/server/user-menu.spec.js @@ -1,34 +1,20 @@ -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'; +'use strict'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const fs = require('fs'); +const {join} = require('path'); +const test = require('supertape'); +const serveOnce = require('serve-once'); +const threadIt = require('thread-it'); + +const userMenu = require('./user-menu'); const {request} = serveOnce(userMenu); + const userMenuPath = join(__dirname, '..', '.cloudcmd.menu.js'); - -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'); -const fixtureCopyFixName = join(fixtureDir, 'io-cp-fix.js'); - -const fixtureMove = readFileSync(fixtureMoveName, 'utf8'); -const fixtureMoveFix = readFileSync(fixtureMoveFixName, 'utf8'); -const fixtureCopy = readFileSync(fixtureCopyName, 'utf8'); -const fixtureCopyFix = readFileSync(fixtureCopyFixName, 'utf8'); +const userMenuFile = fs.readFileSync(userMenuPath, 'utf8'); test('cloudcmd: user menu', async (t) => { - const config = stub().returns(''); - const userMenuFile = getUserMenuFile(); - const options = { - config, menuName: '.cloudcmd.menu.js', }; @@ -36,94 +22,9 @@ test('cloudcmd: user menu', async (t) => { options, }); - t.equal(userMenuFile, body); + threadIt.terminate(); + + t.equal(userMenuFile, body, 'should equal'); 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 {request} = serveOnce(userMenu); - - const {body} = await request.get(`/api/v1/user-menu?dir=${__dirname}`, { - options, - }); - - 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.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 {request} = serveOnce(userMenu); - - const {body} = await request.get(`/api/v1/user-menu?dir=${__dirname}`, { - options, - }); - - 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(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 e01d0505..e87ce5a5 100644 --- a/server/validate.js +++ b/server/validate.js @@ -1,18 +1,12 @@ -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'; +'use strict'; -const isString = (a) => typeof a === 'string'; +const tryCatch = require('try-catch'); -export const root = (dir, config, overrides = {}) => { - const { - exit = _exit, - statSync = _statSync, - } = overrides; - - if (!isString(dir)) +const exit = require('./exit'); +const columns = require('./columns'); + +module.exports.root = (dir, config) => { + if (typeof dir !== 'string') throw Error('dir should be a string'); if (dir === '/') @@ -21,43 +15,31 @@ export const root = (dir, config, overrides = {}) => { if (config('dropbox')) return; + const {statSync} = require('fs'); const [error] = tryCatch(statSync, dir); if (error) return exit('cloudcmd --root: %s', error.message); }; -export const editor = (name, {exit = _exit} = {}) => { - const reg = /^(dword|edward|deepword|qword)$/; +module.exports.editor = (name) => { + const reg = /^(dword|edward|deepword)$/; if (!reg.test(name)) - exit(`cloudcmd --editor: could be "dword", "edward", "deepword" or "qword" only, received: "${name}"`); + exit('cloudcmd --editor: could be "dword", "edward" or "deepword" only'); }; -export const menu = (name, {exit = _exit} = {}) => { - const reg = /^(supermenu|aleman)$/; - - if (!reg.test(name)) - exit('cloudcmd --menu: could be "supermenu" or "aleman" only'); -}; - -export const packer = (name, {exit = _exit} = {}) => { +module.exports.packer = (name) => { const reg = /^(tar|zip)$/; if (!reg.test(name)) exit('cloudcmd --packer: could be "tar" or "zip" only'); }; -export const columns = (type, overrides = {}) => { - const { - exit = _exit, - getColumns = _getColumns, - } = overrides; - +module.exports.columns = (type) => { const addQuotes = (a) => `"${a}"`; - const all = Object - .keys(getColumns()) + .keys(columns) .concat(''); const names = all @@ -65,27 +47,7 @@ export const columns = (type, overrides = {}) => { .map(addQuotes) .join(', '); - if (!all.includes(type)) + if (!~all.indexOf(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 639f185c..a82fe004 100644 --- a/server/validate.spec.js +++ b/server/validate.spec.js @@ -1,17 +1,29 @@ -import {test, stub} from 'supertape'; -import {tryCatch} from 'try-catch'; -import {cloudcmd} from '#server/cloudcmd'; -import * as validate from './validate.js'; +'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} = 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); 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(); }); @@ -21,7 +33,7 @@ test('validate: root: config', (t) => { validate.root('/hello', config); - t.calledWith(config, ['dropbox'], 'should call config'); + t.ok(config.calledWith('dropbox'), 'should call config'); t.end(); }); @@ -29,128 +41,97 @@ test('validate: root: /', (t) => { const fn = stub(); validate.root('/', fn); - t.notCalled(fn, 'should not call fn'); + t.notOk(fn.called, 'should not call fn'); t.end(); }); test('validate: root: stat', (t) => { - const config = stub(); - const error = 'ENOENT'; - const statSync = stub().throws(Error(error)); - const exit = stub(); + const fn = stub(); + const {statSync} = fs; - validate.root('hello', config, { - statSync, - exit, - }); + const error = 'ENOENT'; + fs.statSync = () => { + throw Error(error); + }; + + mockRequire(exitPath, fn); + + const {root} = reRequire(validatePath); + + root('hello', fn); const msg = 'cloudcmd --root: %s'; + fs.statSync = statSync; - t.calledWith(exit, [msg, error], 'should call fn'); + mockRequire.stop(exitPath); + t.ok(fn.calledWith(msg, error), 'should call fn'); t.end(); }); test('validate: packer: not valid', (t) => { - const exit = stub(); + const fn = stub(); + + mockRequire(exitPath, fn); + + const {packer} = reRequire(validatePath); const msg = 'cloudcmd --packer: could be "tar" or "zip" only'; - validate.packer('hello', { - exit, - }); + packer('hello'); - t.calledWith(exit, [msg], 'should call fn'); + mockRequire.stop(exitPath); + + t.ok(fn.calledWith(msg), 'should call fn'); t.end(); }); test('validate: editor: not valid', (t) => { - const exit = stub(); - const msg = 'cloudcmd --editor: could be "dword", "edward", "deepword" or "qword" only, received: "hello"'; + const fn = stub(); - validate.editor('hello', { - exit, - }); + mockRequire(exitPath, fn); - t.calledWith(exit, [msg], 'should call fn'); + const {editor} = reRequire(validatePath); + const msg = 'cloudcmd --editor: could be "dword", "edward" or "deepword" only'; + + editor('hello'); + + mockRequire.stop(exitPath); + + t.ok(fn.calledWith(msg), 'should call fn'); t.end(); }); test('validate: columns', (t) => { - const exit = stub(); + const fn = stub(); + mockRequire(exitPath, fn); - validate.columns('name-size-date', { - exit, - }); + const {columns} = require(validatePath); - t.notCalled(exit, 'should not call exit'); + columns('name-size-date'); + + mockRequire.stop(exitPath); + + t.notOk(fn.called, 'should not call exit'); t.end(); }); test('validate: columns: wrong', (t) => { - const getColumns = stub().returns({ + const fn = stub(); + + mockRequire(exitPath, fn); + mockRequire(columnsPath, { 'name-size-date': '', 'name-size': '', }); - const exit = stub(); + const {columns} = reRequire(validatePath); const msg = 'cloudcmd --columns: can be only one of: "name-size-date", "name-size"'; - validate.columns('hello', { - exit, - getColumns, - }); + columns('hello'); - t.calledWith(exit, [msg], 'should call exit'); + mockRequire.stop(exitPath); + mockRequire.stop(columnsPath); + + t.ok(fn.calledWith(msg), 'should call exit'); t.end(); }); -test('validate: theme', (t) => { - const exit = stub(); - - validate.theme('dark', { - 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(); -}); - -test('validate: menu: not valid', (t) => { - const exit = stub(); - const msg = 'cloudcmd --menu: could be "supermenu" or "aleman" only'; - - validate.menu('hello', { - exit, - }); - - t.calledWith(exit, [msg], 'should call fn'); - t.end(); -}); - -test('validate: menu: valid', (t) => { - const exit = stub(); - - validate.menu('supermenu', { - exit, - }); - - t.notCalled(exit, 'should not call fn'); - t.end(); -}); diff --git a/static/user-menu.js b/static/user-menu.js index 1252aca9..7566c80f 100644 --- a/static/user-menu.js +++ b/static/user-menu.js @@ -1,76 +1,25 @@ -const RENAME_FILE = 'Rename file'; -const CDN = 'https://cdn.jsdelivr.net'; -const CDN_GH = `${CDN}/gh/cloudcmd/user-menu@1.2.4`; +'use strict'; -export default { - '__settings': { - select: [RENAME_FILE], +const RENAME_FILE= 'Rename file'; + +module.exports = { + __settings: { + 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 {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 {CurrentInfo} = DOM; 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, @@ -78,7 +27,7 @@ export default { CloudCmd, }); }, - 'D - Compare directories': ({DOM}) => { + 'D - Compare directories': async ({DOM}) => { const { CurrentInfo, getFilenames, @@ -124,12 +73,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 await res.text(); + return data; } -export const _selectNames = selectNames; - +module.exports._selectNames = selectNames; function selectNames(names, panel, {selectFile, getCurrentByName}) { for (const name of names) { const file = getCurrentByName(name, panel); @@ -137,8 +86,7 @@ function selectNames(names, panel, {selectFile, getCurrentByName}) { } } -export const _compare = compare; - +module.exports._compare = compare; function compare(a, b) { const result = []; @@ -152,17 +100,3 @@ 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 76f3a517..b493cc81 100644 --- a/static/user-menu.spec.js +++ b/static/user-menu.spec.js @@ -1,14 +1,16 @@ -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'; +'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'); -const test = autoGlobals(tape); const {create} = autoGlobals; -const {_data} = defaultMenu; +const {_data} = defaultMenu; const reject = wraptile(async (a) => { throw Error(a); }); @@ -27,89 +29,6 @@ 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(); @@ -122,62 +41,7 @@ test('cloudcmd: static: user menu: IO.write', async (t) => { }); const path = '/.cloudcmd.menu.js'; - - t.calledWith(write, [path, _data], 'should call IO.write'); - 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.ok(write.calledWith(path, _data), 'should call IO.write'); t.end(); }); @@ -192,7 +56,7 @@ test('cloudcmd: static: user menu: refresh', async (t) => { CloudCmd, }); - t.calledWithNoArgs(refresh, 'should call CloudCmd.refresh'); + t.ok(refresh.calledWith(), 'should call CloudCmd.refresh'); t.end(); }); @@ -208,8 +72,7 @@ test('cloudcmd: static: user menu: setCurrentByName', async (t) => { }); const fileName = '.cloudcmd.menu.js'; - - t.calledWith(setCurrentByName, [fileName], 'should call DOM.setCurrentByName'); + t.ok(setCurrentByName.calledWith(fileName), 'should call DOM.setCurrentByName'); t.end(); }); @@ -242,27 +105,27 @@ test('cloudcmd: static: user menu: no EditFile.show', async (t) => { CloudCmd, }); - t.notCalled(EditFile.show, 'should not call EditFile.show'); + t.notOk(EditFile.show.called, 'should not call EditFile.show'); t.end(); }); -test('cloudcmd: static: user menu: compare directories', (t) => { +test('cloudcmd: static: user menu: compare directories', async (t) => { const name = 'D - Compare directories'; const DOM = getDOM(); const CloudCmd = getCloudCmd(); - defaultMenu[name]({ + await defaultMenu[name]({ DOM, CloudCmd, }); - const {files} = DOM.CurrentInfo; - - t.calledWith(DOM.getFilenames, [files], 'should call getFilenames'); + const {files} = DOM.CurrentInfo.files; + t.ok(DOM.getFilenames.calledWith(files), 'should call getFilenames'); t.end(); }); -test('cloudcmd: static: user menu: compare directories: select names', (t) => { +test('cloudcmd: static: user menu: compare directories: select names', async (t) => { + const {_selectNames} = defaultMenu; const selectFile = stub(); const file = {}; const getCurrentByName = stub().returns(file); @@ -275,11 +138,12 @@ test('cloudcmd: static: user menu: compare directories: select names', (t) => { getCurrentByName, }); - t.calledWith(selectFile, [file], 'should call selectFile'); + t.ok(selectFile.calledWith(file), 'should call selectFile'); t.end(); }); -test('cloudcmd: static: user menu: compare directories: select names: getCurrentByName', (t) => { +test('cloudcmd: static: user menu: compare directories: select names: getCurrentByName', async (t) => { + const {_selectNames} = defaultMenu; const selectFile = stub(); const getCurrentByName = stub(); @@ -292,18 +156,21 @@ test('cloudcmd: static: user menu: compare directories: select names: getCurrent getCurrentByName, }); - t.calledWith(getCurrentByName, [name, panel], 'should call selectFile'); + t.ok(getCurrentByName.calledWith(name, panel), 'should call selectFile'); t.end(); }); -test('cloudcmd: static: user menu: compare directories: select names: compare', (t) => { +test('cloudcmd: static: user menu: compare directories: select names: compare', async (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); + t.deepEqual(result, expected, 'should equal'); t.end(); }); @@ -312,21 +179,16 @@ function getDOM() { write: stub(), }; - const Dialog = { - confirm: stub(), - }; - const CurrentInfo = { dirPath: '/', files: [], - filesPassive: [], + filesPasive: [], panel: create(), panelPassive: create(), }; return { IO, - Dialog, CurrentInfo, setCurrentByName: stub(), getFilenames: stub().returns([]), @@ -335,9 +197,12 @@ function getDOM() { }; } -const getCloudCmd = () => ({ - refresh: stub(), - EditFile: { - show: stub(), - }, -}); +function getCloudCmd() { + return { + refresh: stub(), + EditFile: { + show: stub(), + }, + }; +} + diff --git a/test-e2e/ratelimit.js b/test-e2e/ratelimit.js deleted file mode 100644 index 54cdbca8..00000000 --- a/test-e2e/ratelimit.js +++ /dev/null @@ -1,55 +0,0 @@ -import process from 'node:process'; -import {spawn} from 'node:child_process'; -import {test} from 'supertape'; - -test('cloudcmd: server: ratelimit: x-forwarded-for', async (t) => { - const PORT = 3000; - const child = await run(PORT); - - const {status} = await fetch(`http://localhost:${PORT}`, { - headers: { - 'X-Forwarded-For': '127.0.0.1', - }, - }); - - child.kill(); - - t.notEqual(status, 500); - t.end(); -}); - -test('cloudcmd: server: ratelimit', async (t) => { - const PORT = 3001; - const STATUS = 429; - - const child = await run(PORT); - - for (let i = 0; i < 1000; i++) { - await fetch(`http://localhost:${PORT}`); - } - - const {status} = await fetch(`http://localhost:${PORT}`); - child.kill(); - - t.equal(status, STATUS); - t.end(); -}); - -function run(port) { - return new Promise((resolve, reject) => { - const child = spawn(new URL('../bin/cloudcmd.js', import.meta.url).pathname, [], { - env: { - ...process.env, - PORT: port, - }, - stdio: ['ignore', 'pipe', 'pipe'], - }); - - child.stdout.on('data', (a) => { - if (a.toString().includes('url')) - resolve(child); - }); - - child.on('error', reject); - }); -} diff --git a/test/before.js b/test/before.js index 08a50f04..78ad0b73 100644 --- a/test/before.js +++ b/test/before.js @@ -1,25 +1,23 @@ -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'; +'use strict'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +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'); 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); -export default before; +module.exports = before; function before(options, fn = options) { const { @@ -31,7 +29,6 @@ function before(options, fn = options) { const app = express(); const server = http.createServer(app); - const after = (cb) => { if (currentConfig) writejson.sync(pathConfig, currentConfig); @@ -39,7 +36,7 @@ function before(options, fn = options) { server.close(cb); }; - const socket = new Server(server); + const socket = io.listen(server); app.use(cloudcmd({ socket, @@ -54,16 +51,16 @@ function before(options, fn = options) { }); } -export const connect = promisify((options, fn = options) => { +module.exports.connect = promisify((options, fn = options) => { before(options, (port, done) => { - fn(null, { - port, - done, - }); + fn(null, {port, done}); }); }); -const defaultConfig = () => ({ - auth: false, - root: __dirname, -}); +function defaultConfig() { + return { + auth: false, + root: __dirname, + }; +} + diff --git a/client/listeners/get-index.spec.js b/test/client/listeners/get-index.js similarity index 65% rename from client/listeners/get-index.spec.js rename to test/client/listeners/get-index.js index 0380cdc1..0f13acd6 100644 --- a/client/listeners/get-index.spec.js +++ b/test/client/listeners/get-index.js @@ -1,5 +1,9 @@ -import test from 'supertape'; -import {getIndex} from './get-index.js'; +'use strict'; + +const test = require('supertape'); + +const dir = '../../../client/listeners'; +const getIndex = require(`${dir}/get-index`); test('cloudcmd: client: listeners: getIndex: not found', (t) => { const array = ['hello']; @@ -9,11 +13,9 @@ 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/test/client/listeners/get-range.js b/test/client/listeners/get-range.js new file mode 100644 index 00000000..80816eca --- /dev/null +++ b/test/client/listeners/get-range.js @@ -0,0 +1,34 @@ +'use strict'; + +const test = require('supertape'); + +const dir = '../../../client/listeners'; +const getRange = require(`${dir}/get-range`); + +test('cloudcmd: client: listeners: getRange: direct', (t) => { + const expected = ['hello', 'world']; + const files = [...expected, 'how', 'come']; + const result = getRange(0, 1, files); + + t.deepEqual(expected, result, 'should return range'); + t.end(); +}); + +test('cloudcmd: client: listeners: getRange: reverse', (t) => { + const expected = ['hello', 'world']; + const files = [...expected, 'how', 'come']; + const result = getRange(1, 0, files); + + t.deepEqual(expected, result, 'should return range'); + t.end(); +}); + +test('cloudcmd: client: listeners: getRange: one', (t) => { + const expected = ['hello']; + const files = [...expected, 'how', 'come']; + const result = getRange(0, 0, files); + + t.deepEqual(expected, result, 'should return range'); + t.end(); +}); + diff --git a/test/common/btoa.js b/test/common/btoa.js new file mode 100644 index 00000000..9a82d5e4 --- /dev/null +++ b/test/common/btoa.js @@ -0,0 +1,31 @@ +'use strict'; + +const test = require('supertape'); +const stub = require('@cloudcmd/stub'); + +const btoa = require('../../common/btoa'); + +test('btoa: browser', (t) => { + const btoaOriginal = global.btoa; + const str = 'hello'; + + global.btoa = stub(); + + btoa(str); + + t.ok(global.btoa.calledWith(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(); +}); + diff --git a/test/common/cloudfunc.html b/test/common/cloudfunc.html index a032b107..c5f9a5b2 100644 --- a/test/common/cloudfunc.html +++ b/test/common/cloudfunc.html @@ -2,15 +2,13 @@ name size - time date owner mode -
    • +
    • .. <dir> - --:--:-- --.--.---- . --- --- --- @@ -18,7 +16,6 @@ applnk <dir> - --:--:-- 21.02.2016 root rwx r-x r-x @@ -26,8 +23,7 @@ ай 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 86f93908..14ee8f69 100644 --- a/test/common/cloudfunc.js +++ b/test/common/cloudfunc.js @@ -1,21 +1,28 @@ -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'; +'use strict'; -const __filename = fileURLToPath(import.meta.url); +const fs = require('fs'); -const __dirname = dirname(__filename); -const DIR = `${__dirname}/../../`; +const DIR = __dirname + '/../../'; +const COMMONDIR = DIR + 'common/'; +const TMPLDIR = DIR + 'tmpl/'; -const TMPLDIR = `${DIR}tmpl/`; +const { + time, + timeEnd, +} = require(COMMONDIR + 'util'); -const FS_DIR = `${TMPLDIR}fs/`; -const EXPECT_PATH = `${__dirname}/cloudfunc.html`; +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 addHBS = (a) => `${a}.hbs`; const TMPL = [ @@ -26,61 +33,61 @@ const TMPL = [ ].map(addHBS); const data = { - path: '/etc/X11/', - files: [{ + path : '/etc/X11/', + files : [{ name: 'applnk', size: '4.0.0kb', date: '21.02.2016', - uid: 0, + uid : 0, mode: 'rwx r-x r-x', type: 'directory', }, { name: 'ай', size: '1.30kb', date: 0, - uid: 0, + uid : 0, mode: 'rwx r-x r-x', type: 'file', }], }; -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({ - prefix: '', + prefix : '', data, template, }); - Expect += fs - .readFileSync(EXPECT_PATH, 'utf8') - .slice(0, -1); + Expect += expect; let i; - const isNotOk = Expect .split('') .some((item, number) => { const ret = result[number] !== item; - if (ret) + if (ret) { i = number; + } return ret; }); @@ -88,11 +95,19 @@ 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'); + t.equal(Expect, result, 'should be equal rendered json data'); + + htmlLooksLike(Expect, result); + t.end(); }); @@ -107,7 +122,7 @@ test('cloudfunc: formatMsg', (t) => { t.end(); }); -test('cloudfunc: formatMsg: no name', (t) => { +test('cloudfunc: formatMsg', (t) => { const msg = 'hello'; const name = null; const status = 'ok'; @@ -119,15 +134,16 @@ test('cloudfunc: formatMsg: no name', (t) => { }); test('cloudfunc: getTitle', (t) => { - const result = CloudFunc.getTitle({ - path: '/', - }); + const CloudFunc = reRequire(CloudFuncPath); + + const result = CloudFunc.getTitle(); 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({ @@ -139,6 +155,7 @@ test('cloudfunc: getTitle: no name', (t) => { }); test('cloudfunc: getTitle: name, path', (t) => { + const CloudFunc = reRequire(CloudFuncPath); const name = 'hello'; const path = '/hello/world'; @@ -164,18 +181,16 @@ test('cloudfunc: getHeaderField', (t) => { }); test('cloudfunc: getPathLink: no url', (t) => { - const [error] = tryCatch(CloudFunc.getPathLink); - - t.ok(error, 'should throw when no url'); + t.throws(CloudFunc.getPathLink, 'should throw when no url'); t.end(); }); test('cloudfunc: getPathLink: no template', (t) => { const url = 'http://abc.com'; const prefix = ''; - const [error] = tryCatch(CloudFunc.getPathLink, url, prefix); + const fn = () => CloudFunc.getPathLink(url, prefix); - t.ok(error, 'should throw when no template'); + t.throws(fn, 'should throw when no template'); t.end(); }); @@ -192,3 +207,4 @@ test('cloudfunc: getDotDot: two levels deep', (t) => { t.equal(dotDot, '/home', 'should return up level'); t.end(); }); + diff --git a/common/entity.spec.js b/test/common/entity.js similarity index 53% rename from common/entity.spec.js rename to test/common/entity.js index 79313f7c..8e38f46a 100644 --- a/common/entity.spec.js +++ b/test/common/entity.js @@ -1,34 +1,29 @@ -import {test} from 'supertape'; -import * as entity from '#common/entity'; +'use strict'; + +const test = require('supertape'); +const entity = require('../../common/entity'); test('cloudcmd: entity: encode', (t) => { const result = entity.encode(' '); - const expected = '<hello> '; - - t.equal(result, expected, 'should encode entity'); - t.end(); -}); - -test('cloudcmd: entity: {{}}', (t) => { - const result = entity.encode('{{}}'); - const expected = '{{}}'; + const expected = '<hello> '; 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 quote', (t) => { +test('cloudcmd: entity: encode', (t) => { const result = entity.encode('"hello"'); const expected = '"hello"'; t.equal(result, expected, 'should encode entity'); t.end(); }); + diff --git a/test/fixture/copy.txt b/test/fixture/copy.txt deleted file mode 100644 index ce013625..00000000 --- a/test/fixture/copy.txt +++ /dev/null @@ -1 +0,0 @@ -hello diff --git a/server/fixture/route.js b/test/fixture/cp.txt similarity index 100% rename from server/fixture/route.js rename to test/fixture/cp.txt diff --git a/test/rest/config.js b/test/rest/config.js index b45a0d18..e2b8fb3e 100644 --- a/test/rest/config.js +++ b/test/rest/config.js @@ -1,9 +1,11 @@ -import {serveOnce} from 'serve-once'; -import test from 'supertape'; -import {cloudcmd} from '#server/cloudcmd'; +'use strict'; +const test = require('supertape'); + +const cloudcmd = require('../..'); const configManager = cloudcmd.createConfigManager(); -const {request} = serveOnce(cloudcmd, { + +const {request} = require('serve-once')(cloudcmd, { config: { auth: false, }, @@ -102,3 +104,4 @@ 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 deleted file mode 100644 index e6d8a03f..00000000 --- a/test/rest/copy.js +++ /dev/null @@ -1,44 +0,0 @@ -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 __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const config = { - root: new URL('..', import.meta.url).pathname, -}; - -const configManager = cloudcmd.createConfigManager(); - -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'], - }; - - mkdirSync(tmp); - - const {body} = await request.put(`/api/v1/copy`, { - body: files, - }); - - rimraf.sync(tmp); - - t.equal(body, 'copy: ok("["copy.txt"]")', 'should return result'); - t.end(); -}); diff --git a/test/rest/cp.js b/test/rest/cp.js new file mode 100644 index 00000000..e3bce034 --- /dev/null +++ b/test/rest/cp.js @@ -0,0 +1,43 @@ +'use strict'; + +const {mkdirSync} = require('fs'); +const {join} = require('path'); +const test = require('supertape'); +const rimraf = require('rimraf'); + +const fixtureDir = join(__dirname, '..', 'fixture') + '/'; +const config = { + root: join(__dirname, '..'), +}; + +const cloudcmd = require('../..'); +const configManager = cloudcmd.createConfigManager(); +configManager('auth', false); + +const {request} = require('serve-once')(cloudcmd, { + config, + configManager, +}); + +test('cloudcmd: rest: cp', async (t) => { + const tmp = join(fixtureDir, 'tmp'); + const files = { + from: '/fixture/', + to: '/fixture/tmp', + names: [ + 'cp.txt', + ], + }; + + mkdirSync(tmp); + + const {body} = await request.put(`/api/v1/cp`, { + body: files, + }); + + rimraf.sync(tmp); + + t.equal(body, 'copy: ok("["cp.txt"]")', 'should return result'); + t.end(); +}); + diff --git a/test/rest/fs.js b/test/rest/fs.js index db438a71..8fd0497d 100644 --- a/test/rest/fs.js +++ b/test/rest/fs.js @@ -1,8 +1,9 @@ -import {serveOnce} from 'serve-once'; -import test from 'supertape'; -import {cloudcmd} from '#server/cloudcmd'; +'use strict'; -const {request} = serveOnce(cloudcmd, { +const test = require('supertape'); + +const cloudcmd = require('../..'); +const {request} = require('serve-once')(cloudcmd, { config: { auth: false, }, @@ -15,13 +16,7 @@ test('cloudcmd: rest: fs: path', async (t) => { const {path} = body; - t.equal(path, '/', 'should dir path be "/"'); + t.equal('/', path, 'should dir path be "/"'); t.end(); }); -test('cloudcmd: path traversal beyond root', async (t) => { - const {body} = await request.get('/fs..%2f..%2fetc/passwd'); - - t.match(body, 'beyond root', 'should return beyond root message'); - t.end(); -}); diff --git a/test/rest/move.js b/test/rest/move.js deleted file mode 100644 index 073c520f..00000000 --- a/test/rest/move.js +++ /dev/null @@ -1,87 +0,0 @@ -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 move = new EventEmitter(); - const moveFiles = stub().returns(move); - - const {createConfigManager} = cloudcmd; - cloudcmd.depStore('moveFiles', moveFiles); - - const configManager = createConfigManager(); - configManager('auth', false); - configManager('root', '/'); - - const {request} = serveOnce(cloudcmd, { - configManager, - }); - - const files = { - from: '/fixture/', - to: '/fixture/tmp/', - names: ['move.txt'], - }; - - const emit = move.emit.bind(move); - - 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 {createConfigManager} = cloudcmd; - - const configManager = createConfigManager(); - configManager('auth', false); - configManager('root', '/'); - - const {request} = serveOnce(cloudcmd, { - configManager, - }); - - const files = {}; - - const {body} = await request.put(`/api/v1/move`, { - body: files, - }); - - const expected = '"from" should be filled'; - - t.equal(body, expected); - t.end(); -}); - -test('cloudcmd: rest: move: no to', async (t) => { - const {createConfigManager} = cloudcmd; - - const configManager = createConfigManager(); - configManager('auth', false); - configManager('root', '/'); - - const {request} = serveOnce(cloudcmd, { - configManager, - }); - - const files = { - from: '/', - }; - - const {body} = await request.put(`/api/v1/move`, { - body: files, - }); - - const expected = '"to" should be filled'; - - t.equal(body, expected); - t.end(); -}); diff --git a/test/rest/mv.js b/test/rest/mv.js new file mode 100644 index 00000000..fe75773c --- /dev/null +++ b/test/rest/mv.js @@ -0,0 +1,114 @@ +'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} = mockRequire; +const serveOnce = require('serve-once'); + +const cloudcmdPath = '../../'; +const dir = cloudcmdPath + 'server/'; +const restPath = dir + 'rest'; + +const {assign} = Object; + +test('cloudcmd: rest: mv', async (t) => { + const volume = { + '/fixture/mv.txt': 'hello', + '/fixture/tmp/a.txt': 'a', + }; + + const vol = Volume.fromJSON(volume, '/'); + + 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; + + const configManager = createConfigManager(); + configManager('auth', false); + configManager('root', '/'); + + const {request} = serveOnce(cloudcmd, { + configManager, + }); + + const files = { + from: '/fixture/', + to: '/fixture/tmp/', + names: [ + 'mv.txt', + ], + }; + + const {body} = await request.put(`/api/v1/mv`, { + body: files, + }); + + mockRequire.stop('fs'); + + t.equal(body, 'move: ok("["mv.txt"]")', 'should move'); + t.end(); +}); + +test('cloudcmd: rest: mv: rename', async (t) => { + const volume = { + '/fixture/mv.txt': 'hello', + '/fixture/tmp/a.txt': 'a', + }; + + const vol = Volume.fromJSON(volume, '/'); + + const unionFS = ufs + .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', '/'); + + const {request} = serveOnce(cloudcmd, { + configManager, + }); + + const files = { + from: '/fixture/mv.txt', + to: '/fixture/tmp/mv.txt', + }; + + const {body} = await request.put(`/api/v1/mv`, { + body: files, + }); + + mockRequire.stop('fs'); + + const expected = 'move: ok("{"from":"/fixture/mv.txt","to":"/fixture/tmp/mv.txt"}")'; + + t.equal(body, expected, 'should move'); + t.end(); +}); + diff --git a/test/rest/pack.js b/test/rest/pack.js index 4c7a49ed..aff32c61 100644 --- a/test/rest/pack.js +++ b/test/rest/pack.js @@ -1,32 +1,34 @@ -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'; +'use strict'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const pathZipFixture = join(__dirname, '..', 'fixture/pack.zip'); +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'); - -const defaultOptions = { - config: { - auth: false, - root: new URL('..', import.meta.url).pathname, - }, -}; +const pathZipFixture = join(__dirname, '..', 'fixture/pack.zip'); +const cloudcmdPath = '../..'; const fixture = { tar: fs.readFileSync(pathTarFixture), zip: fs.readFileSync(pathZipFixture), }; +const defaultOptions = { + config: { + auth: false, + root: join(__dirname, '..'), + }, +}; + +const cloudcmd = require(cloudcmdPath); + +const serveOnce = require('serve-once'); const {request} = serveOnce(cloudcmd, defaultOptions); const once = promisify((name, extract, fn) => { @@ -45,6 +47,7 @@ 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`, { @@ -54,18 +57,14 @@ 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) => { @@ -77,7 +76,7 @@ test('cloudcmd: rest: pack: tar: put: file', async (t) => { config, }; - const name = `${Math.random()}.tar.gz`; + const name = String(Math.random()) + '.tar.gz'; const {request} = serveOnce(cloudcmd, defaultOptions); @@ -89,13 +88,11 @@ 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}`); @@ -112,8 +109,7 @@ test('cloudcmd: rest: pack: tar: put: response', async (t) => { config, }; - const name = `${Math.random()}.tar.gz`; - + const name = String(Math.random()) + '.tar.gz'; const {body} = await request.put(`/api/v1/pack`, { options, body: getPackOptions(name), @@ -136,10 +132,12 @@ 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.match(body, /^ENOENT: no such file or directory/, 'should return error'); + t.ok(/^ENOENT: no such file or directory/.test(body), 'should return error'); t.end(); }); @@ -157,7 +155,7 @@ test('cloudcmd: rest: pack: zip: get', async (t) => { type: 'buffer', }); - t.equal(body.length, 145, 'should pack data'); + t.equal(body.length, fixture.zip.length, 'should pack data'); t.end(); }); @@ -170,16 +168,16 @@ test('cloudcmd: rest: pack: zip: put: file', async (t) => { config, }; - const name = `${Math.random()}.zip`; - + const name = String(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, 136, 'should create archive'); + t.equal(fixture.zip.length, file.length, 'should create archive'); t.end(); }); @@ -192,8 +190,7 @@ test('cloudcmd: rest: pack: zip: put: response', async (t) => { config, }; - const name = `${Math.random()}.zip`; - + const name = String(Math.random()) + '.zip'; const {body} = await request.put(`/api/v1/pack`, { options, body: getPackOptions(name), @@ -217,15 +214,20 @@ 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.match(body, /^ENOENT: no such file or directory/, 'should return error'); + t.ok(/^ENOENT: no such file or directory/.test(body), 'should return error'); t.end(); }); -const getPackOptions = (to, names = ['pack']) => ({ - to, - names, - from: '/fixture', -}); +function getPackOptions(to, names = ['pack']) { + return { + to, + names, + from: '/fixture', + }; +} + diff --git a/test/rest/rename.js b/test/rest/rename.js deleted file mode 100644 index eb773dba..00000000 --- a/test/rest/rename.js +++ /dev/null @@ -1,94 +0,0 @@ -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 = { - '/fixture/mv.txt': 'hello', - '/fixture/tmp/a.txt': 'a', - }; - - const vol = Volume.fromJSON(volume, '/'); - - const unionFS = ufs - .use(vol) - .use(fs); - - const {createConfigManager} = cloudcmd; - const configManager = createConfigManager(); - - configManager('auth', false); - configManager('root', '/'); - - cloudcmd.depStore('fs', unionFS); - const {request} = serveOnce(cloudcmd, { - configManager, - }); - - const files = { - from: '/fixture/mv.txt', - to: '/fixture/tmp/mv.txt', - }; - - const {body} = await request.put(`/api/v1/rename`, { - body: files, - }); - - cloudcmd.depStore(); - - const expected = 'rename: ok("{"from":"/fixture/mv.txt","to":"/fixture/tmp/mv.txt"}")'; - - t.equal(body, expected, 'should move'); - t.end(); -}); - -test('cloudcmd: rest: rename: no from', async (t) => { - const {createConfigManager} = cloudcmd; - - const configManager = createConfigManager(); - configManager('auth', false); - configManager('root', '/'); - - const {request} = serveOnce(cloudcmd, { - configManager, - }); - - const files = {}; - - const {body} = await request.put(`/api/v1/rename`, { - body: files, - }); - - const expected = '"from" should be filled'; - - t.equal(body, expected); - t.end(); -}); - -test('cloudcmd: rest: rename: no to', async (t) => { - const {createConfigManager} = cloudcmd; - - const configManager = createConfigManager(); - configManager('auth', false); - configManager('root', '/'); - - const {request} = serveOnce(cloudcmd, { - configManager, - }); - - const files = { - from: '/', - }; - - const {body} = await request.put(`/api/v1/rename`, { - body: files, - }); - - const expected = '"to" should be filled'; - - t.equal(body, expected); - t.end(); -}); diff --git a/test/server/columns.js b/test/server/columns.js new file mode 100644 index 00000000..9ccb0e66 --- /dev/null +++ b/test/server/columns.js @@ -0,0 +1,31 @@ +'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[''], '', 'should equal'); + 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, 'should equal'); + t.end(); +}); + diff --git a/test/server/console.js b/test/server/console.js index 56d35a3c..104a7ff1 100644 --- a/test/server/console.js +++ b/test/server/console.js @@ -1,20 +1,21 @@ -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'; +'use strict'; -const configFn = createConfig(); +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(); 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')); @@ -33,17 +34,15 @@ 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'); + const [error] = await once(socket, 'error'); socket.close(); await done(); - t.equal(error.message, 'Invalid namespace', 'should emit error'); + t.equal(error, 'Invalid namespace', 'should emit error'); t.end(); }); + diff --git a/test/server/env.js b/test/server/env.js index 34bc8959..2357ef63 100644 --- a/test/server/env.js +++ b/test/server/env.js @@ -1,51 +1,48 @@ -import process from 'node:process'; -import {test} from 'supertape'; -import * as env from '../../server/env.js'; +'use strict'; + +const test = require('supertape'); +const env = require('../../server/env'); test('env: small', (t) => { process.env.cloudcmd_hello = 'world'; + t.equal(env('hello'), 'world', 'should parse string from env'); delete process.env.cloudcmd_hello; - - t.equal(env.parse('hello'), 'world', 'should parse string from env'); t.end(); }); test('env: big', (t) => { process.env.CLOUDCMD_HELLO = 'world'; + t.equal(env('hello'), 'world', 'should parse string from env'); delete process.env.CLOUDCMD_HELLO; - - t.equal(env.parse('hello'), 'world', 'should parse string from env'); t.end(); }); test('env: bool: false', (t) => { process.env.cloudcmd_terminal = 'false'; + t.equal(env.bool('terminal'), false, 'should return false'); delete process.env.cloudcmd_terminal; - - t.notOk(env.bool('terminal'), 'should return false'); t.end(); }); test('env: bool: true', (t) => { process.env.cloudcmd_terminal = 'true'; - delete process.env.cloudcmd_terminal; + t.equal(env.bool('terminal'), true, 'should be true'); - t.ok(env.bool('terminal'), 'should be true'); + delete process.env.cloudcmd_terminal; t.end(); }); test('env: bool: undefined', (t) => { const {cloudcmd_terminal} = process.env; - process.env.cloudcmd_terminal = undefined; + + t.equal(env.bool('terminal'), undefined, 'should be undefined'); + process.env.cloudcmd_terminal = cloudcmd_terminal; - - const result = env.bool('terminal'); - - t.notOk(result); t.end(); }); + diff --git a/test/server/modulas.js b/test/server/modulas.js index 0085449e..d9aa464f 100644 --- a/test/server/modulas.js +++ b/test/server/modulas.js @@ -1,19 +1,17 @@ -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'; +'use strict'; + +const {join} = require('path'); +const test = require('supertape'); +const stub = require('@cloudcmd/stub'); -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 {request} = serveOnce(cloudcmd, { +const localModules = require(modulesPath); +const modulas = require(`${cloudcmdPath}/server/modulas`); + +const cloudcmd = require(cloudcmdPath); +const {request} = require('serve-once')(cloudcmd, { config: { auth: false, dropbox: false, @@ -28,7 +26,6 @@ test('cloudcmd: modules', async (t) => { }, }, }; - const options = { modules, }; @@ -43,7 +40,7 @@ test('cloudcmd: modules', async (t) => { options, }); - t.deepEqual(body, expected); + t.deepEqual(body, expected, 'should equal'); t.end(); }); @@ -75,10 +72,9 @@ 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.ok(send.calledWith(localModules), 'should have been called with modules'); t.end(); }); + diff --git a/server/prefixer.spec.js b/test/server/prefixer.js similarity index 78% rename from server/prefixer.spec.js rename to test/server/prefixer.js index 3ea77dfb..a0cdfcd7 100644 --- a/server/prefixer.spec.js +++ b/test/server/prefixer.js @@ -1,5 +1,9 @@ -import {test} from 'supertape'; -import prefixer from './prefixer.js'; +'use strict'; + +const test = require('supertape'); + +const dir = '../../server/'; +const prefixer = require(dir + 'prefixer'); test('prefixer: prefix without a slash', (t) => { t.equal(prefixer('hello'), '/hello', 'should add slash'); diff --git a/server/show-config.spec.js b/test/server/show-config.js similarity index 57% rename from server/show-config.spec.js rename to test/server/show-config.js index b25e56a2..cb8cd66f 100644 --- a/server/show-config.spec.js +++ b/test/server/show-config.js @@ -1,22 +1,20 @@ -import {test} from 'supertape'; -import {tryCatch} from 'try-catch'; -import {showConfig} from './show-config.js'; +'use strict'; + +const test = require('supertape'); +const showConfig = require('../../server/show-config'); test('cloudcmd: show-config: no arguments', (t) => { - const [error] = tryCatch(showConfig); - - t.equal(error.message, 'config could not be empty!', 'should throw when no config'); + t.throws(showConfig, /config could not be empty!/, 'should throw when no config'); t.end(); }); test('cloudcmd: show-config: bad arguments', (t) => { - const [error] = tryCatch(showConfig, 'hello'); - - t.equal(error.message, 'config should be an object!', 'should throw when config not object'); + const fn = () => showConfig('hello'); + t.throws(fn, /config should be an object!/, 'should throw when config not object'); t.end(); }); -test('cloudcmd: show-config: empty: return', (t) => { +test('cloudcmd: show-config: return', (t) => { t.equal(showConfig({}), '', 'should return string'); t.end(); }); @@ -35,3 +33,4 @@ test('cloudcmd: show-config: return', (t) => { t.equal(showConfig(config), result, 'should return table'); t.end(); }); + diff --git a/test/static.js b/test/static.js index 0f0da5cb..f8f3e8a8 100644 --- a/test/static.js +++ b/test/static.js @@ -1,19 +1,18 @@ -import {Buffer} from 'node:buffer'; -import {serveOnce} from 'serve-once'; -import test from 'supertape'; -import criton from 'criton'; -import {cloudcmd} from '#server/cloudcmd'; +'use strict'; + +const test = require('supertape'); +const criton = require('criton'); + +const cloudcmd = require('..'); +const configFn = cloudcmd.createConfigManager(); const config = { auth: false, }; - -const {request} = serveOnce(cloudcmd, { +const {request} = require('serve-once')(cloudcmd, { config, }); -const configFn = cloudcmd.createConfigManager(); - test('cloudcmd: static', async (t) => { const name = 'package.json'; const {body} = await request.get(`/${name}`, { @@ -43,7 +42,6 @@ test('cloudcmd: prefix: wrong', async (t) => { }; const name = Math.random(); - const {status} = await request.get(`/${name}`, { options, }); @@ -67,7 +65,6 @@ test('cloudcmd: /cloudcmd.js: auth: access denied', async (t) => { const config = { auth: true, }; - const options = { config, }; @@ -83,20 +80,15 @@ 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}`, { @@ -115,13 +107,11 @@ 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, }; @@ -150,3 +140,4 @@ 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 6002cd8b..b7a3e757 100644 --- a/tmpl/config.hbs +++ b/tmpl/config.hbs @@ -1,5 +1,5 @@
    -
  • +
  • -
  • +
  • -
  • +
  • -
  • - -
  • -
  • - -
  • -
  • +
  • -
  • +
  • -
  • - -
  • -
  • +
  • -
  • +
  • -
  • +
  • -
  • +
  • -
  • +
  • -
  • +
  • -
  • +
  • -
  • +
  • -
  • +
  • -
  • - -
  • -
  • +
  • -
  • +
  • -
  • +
  • -
  • +
  • -
\ No newline at end of file + diff --git a/tmpl/fs/file.hbs b/tmpl/fs/file.hbs index 8b50494f..0eaa6887 100644 --- a/tmpl/fs/file.hbs +++ b/tmpl/fs/file.hbs @@ -2,7 +2,6 @@ {{ name }} {{ size }} - {{ time }} {{ date }} {{ owner }} {{ mode }} diff --git a/webpack.config.js b/webpack.config.js index 5d684391..b663db21 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,10 +1,14 @@ -import {merge} from 'webpack-merge'; -import * as htmlConfig from './.webpack/html.js'; -import cssConfig from './.webpack/css.js'; -import jsConfig from './.webpack/js.js'; +'use strict'; -export default merge([ +const merge = require('webpack-merge'); + +const htmlConfig = require('./.webpack/html'); +const cssConfig = require('./.webpack/css'); +const jsConfig = require('./.webpack/js'); + +module.exports = merge([ jsConfig, htmlConfig, cssConfig, ]); +