diff --git a/.github/workflows/docker-io.yml b/.github/workflows/docker-io.yml index beff4589..8cbeb042 100644 --- a/.github/workflows/docker-io.yml +++ b/.github/workflows/docker-io.yml @@ -1,4 +1,6 @@ name: Docker IO +permissions: + contents: write on: - push jobs: @@ -49,7 +51,7 @@ jobs: with: context: . file: docker/Dockerfile.io - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 push: true tags: | coderaiser/cloudcmd:io diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1a85b29e..5c81d368 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,4 +1,6 @@ name: Docker CI +permissions: + contents: write on: push: tags: diff --git a/.madrun.js b/.madrun.js index 7452328c..efa8ce98 100644 --- a/.madrun.js +++ b/.madrun.js @@ -21,14 +21,14 @@ export default { 'start:dev', ]), 'lint:all': () => run('lint:progress'), - 'lint': () => 'redlint fix; putout . --rulesdir rules', + '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 --rulesdir rules --fix . && redlint fix`, + 'fix:lint': async () => `putout --fix . && redlint fix`, 'lint:stream': () => run('lint', '-f stream'), - 'test': () => [testEnv, `tape '{test,test-e2e}/**/*.js' '{bin,client,static,common,server}/**/*.spec.js' -f fail`], + 'test': () => [testEnv, `tape '{test}/**/*.js' '{bin,client,static,common,server}/**/*.spec.js' -f fail`], 'test:e2e': () => `tape 'test-e2e/**/*.js'`, 'test:client': () => `tape 'test/client/**/*.js'`, 'test:server': () => `tape 'test/**/*.js' 'server/**/*.spec.js' 'common/**/*.spec.js'`, @@ -39,7 +39,7 @@ export default { 'coverage': async () => [testEnv, `c8 ${await cutEnv('test')}`], 'coverage:report': () => 'c8 report --reporter=lcov', 'report': () => 'c8 report --reporter=lcov', - '6to5': () => [buildEnv, 'webpack --progress'], + '6to5': () => [buildEnv, 'rspack build --config rspack.config.js'], '6to5:client': () => run('6to5', '--mode production'), '6to5:client:dev': async () => await run('6to5', '--mode development', { NODE_ENV: 'development', diff --git a/.webpack/css.js b/.rspack/css.js similarity index 53% rename from .webpack/css.js rename to .rspack/css.js index 311290cc..10d28250 100644 --- a/.webpack/css.js +++ b/.rspack/css.js @@ -1,19 +1,18 @@ import {env} from 'node:process'; -import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; -import MiniCssExtractPlugin from 'mini-css-extract-plugin'; +import {rspack} from '@rspack/core'; +const {CssExtractRspackPlugin} = rspack; const isDev = env.NODE_ENV === 'development'; -const clean = (a) => a.filter(Boolean); -const plugins = clean([ - new MiniCssExtractPlugin({ +const plugins = [ + new CssExtractRspackPlugin({ filename: '[name].css', }), -]); +]; const rules = [{ test: /\.css$/i, - use: [MiniCssExtractPlugin.loader, { + use: [CssExtractRspackPlugin.loader, { loader: 'css-loader', options: { url: true, @@ -33,13 +32,7 @@ export default { optimization: { minimize: !isDev, minimizer: [ - new CssMinimizerPlugin({ - minimizerOptions: { - preset: ['default', { - svgo: false, - }], - }, - }), + new rspack.LightningCssMinimizerRspackPlugin(), ], }, }; diff --git a/.webpack/html.js b/.rspack/html.js similarity index 100% rename from .webpack/html.js rename to .rspack/html.js diff --git a/.webpack/js.js b/.rspack/js.js similarity index 81% rename from .webpack/js.js rename to .rspack/js.js index fdc88556..251d85f6 100644 --- a/.webpack/js.js +++ b/.rspack/js.js @@ -1,12 +1,15 @@ import {resolve, sep} from 'node:path'; +import {fileURLToPath} from 'node:url'; import {env} from 'node:process'; -import webpack from 'webpack'; -import WebpackBar from 'webpackbar'; +import {rspack} from '@rspack/core'; + +const resolveModule = (a) => fileURLToPath(import.meta.resolve(a)); const { EnvironmentPlugin, NormalModuleReplacementPlugin, -} = webpack; + ProvidePlugin, +} = rspack; const modules = './modules'; const dirModules = './client/modules'; @@ -22,27 +25,31 @@ 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) => a.endsWith('.spec.js'); -const options = { - babelrc: true, -}; -const rules = clean([ - !isDev && { - test: /\.[mc]?js$/, - exclude: /node_modules/, - loader: 'babel-loader', +// 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', + }, }, - isDev && { - test: /\.[mc]?js$/, - exclude: /node_modules/, - loader: 'babel-loader', - options, - }, -]); +}]; const plugins = [ new NormalModuleReplacementPlugin(/^node:/, (resource) => { @@ -52,8 +59,7 @@ const plugins = [ new EnvironmentPlugin({ NODE_ENV, }), - new WebpackBar(), - new webpack.ProvidePlugin({ + new ProvidePlugin({ process: 'process/browser', }), ]; @@ -99,9 +105,9 @@ export default { 'node:path': 'path', }, fallback: { - path: import.meta.resolve('path-browserify'), - process: import.meta.resolve('process/browser'), - util: import.meta.resolve('util'), + path: resolveModule('path-browserify'), + process: resolveModule('process/browser'), + util: resolveModule('util'), }, }, devtool, @@ -156,7 +162,7 @@ export default { }, plugins, performance: { - maxEntrypointSize: 600_000, + maxEntrypointSize: 800_000, maxAssetSize: 600_000, }, }; diff --git a/client/dom/current-file.spec.js b/client/dom/current-file.spec.js index 6bf0509b..85b18a29 100644 --- a/client/dom/current-file.spec.js +++ b/client/dom/current-file.spec.js @@ -19,14 +19,13 @@ test('current-file: setCurrentName: setAttribute', (t) => { currentFile.setCurrentName('hello', current); + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; + t.calledWith(setAttribute, [ 'data-name', 'js-file-aGVsbG8=', ], 'should call setAttribute'); - - globalThis.DOM = DOM; - globalThis.CloudCmd = CloudCmd; - t.end(); }); @@ -41,14 +40,13 @@ test('current-file: setCurrentName: setAttribute: cyrillic', (t) => { currentFile.setCurrentName('ай', current); + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; + t.calledWith(setAttribute, [ 'data-name', 'js-file-JUQwJUIwJUQwJUI5', ], 'should call setAttribute'); - - globalThis.DOM = DOM; - globalThis.CloudCmd = CloudCmd; - t.end(); }); @@ -76,11 +74,10 @@ test('current-file: emit', (t) => { currentFile.setCurrentName('hello', current); - t.calledWith(emit, ['current-file', current], 'should call emit'); - globalThis.DOM = DOM; globalThis.CloudCmd = CloudCmd; + t.calledWith(emit, ['current-file', current], 'should call emit'); t.end(); }); @@ -96,14 +93,12 @@ test('current-file: setCurrentName: return', (t) => { globalThis.CloudCmd = getCloudCmd(); const current = create(); - const result = currentFile.setCurrentName('hello', current); - t.equal(result, link, 'should return link'); - globalThis.DOM = DOM; globalThis.CloudCmd = CloudCmd; + t.equal(result, link, 'should return link'); t.end(); }); diff --git a/client/dom/dom-tree.spec.js b/client/dom/dom-tree.spec.js index 13f56f6d..53b376c7 100644 --- a/client/dom/dom-tree.spec.js +++ b/client/dom/dom-tree.spec.js @@ -1,7 +1,16 @@ -import test from 'supertape'; +import {test, stub} from 'supertape'; import {create} from 'auto-globals'; import {tryCatch} from 'try-catch'; -import {isContainClass} from './dom-tree.js'; +import { + isContainClass, + getByTag, + getById, + getByClass, + getByDataName, + getByClassAll, + hide, + show, +} from './dom-tree.js'; test('dom: isContainClass: no element', (t) => { const [e] = tryCatch(isContainClass); @@ -38,3 +47,106 @@ test('dom: isContainClass: contains: array', (t) => { t.calledWith(contains, [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/index.js b/client/dom/index.js index f6ce6bc7..a82b6f72 100644 --- a/client/dom/index.js +++ b/client/dom/index.js @@ -442,11 +442,7 @@ export function getFilenames(files) { if (name === '..') allFiles.shift(); - const names = allFiles.map((current) => { - return DOM.getCurrentName(current); - }); - - return names; + return allFiles.map(DOM.getCurrentName); } /** diff --git a/client/dom/load-remote.js b/client/dom/load-remote.js index 43af0cf5..5be8e327 100644 --- a/client/dom/load-remote.js +++ b/client/dom/load-remote.js @@ -33,13 +33,19 @@ export const loadRemote = callbackify(async (name, options) => { local = [module.local]; } - const localURL = local.map((url) => prefix + url); + const localURL = []; - const remoteURL = remoteTmpls.map((tmpl) => { - return rendy(tmpl, { + 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); diff --git a/client/key/binder.spec.js b/client/key/binder.spec.js new file mode 100644 index 00000000..f9d54fdd --- /dev/null +++ b/client/key/binder.spec.js @@ -0,0 +1,28 @@ +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/vim/find.spec.js b/client/key/vim/find.spec.js index 59c9a249..191a3652 100644 --- a/client/key/vim/find.spec.js +++ b/client/key/vim/find.spec.js @@ -1,6 +1,12 @@ import test from 'supertape'; import {getDOM} from './globals.fixture.js'; -import {_next, _previous} from './find.js'; +import { + _next, + _previous, + find, + findNext, + findPrevious, +} from './find.js'; globalThis.DOM = getDOM(); @@ -11,9 +17,39 @@ 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); t.equal(result, 1, 'should return 1'); 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/index.spec.js b/client/key/vim/index.spec.js index e1ca4eee..231c6577 100644 --- a/client/key/vim/index.spec.js +++ b/client/key/vim/index.spec.js @@ -1,6 +1,7 @@ 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'; globalThis.DOM = getDOM(); globalThis.CloudCmd = getCloudCmd(); @@ -675,3 +676,81 @@ test('cloudcmd: client: vim: rename', async (t) => { 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/modules/config/input.spec.js b/client/modules/config/input.spec.js index 537c1632..9d0866cd 100644 --- a/client/modules/config/input.spec.js +++ b/client/modules/config/input.spec.js @@ -1,5 +1,10 @@ -import {test} from 'supertape'; -import {convert} from './input.js'; +import {test, stub} from 'supertape'; +import { + convert, + getName, + getValue, + setValue, +} from './input.js'; test('cloudcmd: client: config: input: convert', (t) => { const result = convert({ @@ -13,3 +18,123 @@ test('cloudcmd: client: config: input: convert', (t) => { 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/view/index.spec.js b/client/modules/view/index.spec.js index 8d184548..abfbe3f4 100644 --- a/client/modules/view/index.spec.js +++ b/client/modules/view/index.spec.js @@ -31,8 +31,10 @@ test('cloudcmd: client: view: initConfig', (t) => { test('cloudcmd: client: view: initConfig: no options', (t) => { const config = _initConfig(); + const result = typeof config; + const expected = 'object'; - t.equal(typeof config, 'object'); + t.equal(result, expected); t.end(); }); diff --git a/client/sw/register.spec.js b/client/sw/register.spec.js index 47420ba7..52699f0b 100644 --- a/client/sw/register.spec.js +++ b/client/sw/register.spec.js @@ -65,7 +65,7 @@ test('sw: register: registerSW: http', async (t, {location, navigator}) => { test('sw: register: registerSW: https self-signed', async (t, {location, navigator}) => { Object.assign(location, { - protocol: 'https', + protocol: 'https:', hostname: 'self-signed.badssl.com', }); diff --git a/common/cloudfunc.spec.js b/common/cloudfunc.spec.js index dca24b5a..b679feeb 100644 --- a/common/cloudfunc.spec.js +++ b/common/cloudfunc.spec.js @@ -2,12 +2,17 @@ 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'; const templatePath = new URL('../tmpl/fs', import.meta.url).pathname; @@ -221,6 +226,97 @@ test('cloudfunc: _getDataName', (t) => { 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/', diff --git a/common/util.spec.js b/common/util.spec.js index 6a32848c..5a19a8be 100644 --- a/common/util.spec.js +++ b/common/util.spec.js @@ -5,6 +5,8 @@ import { getRegExp, escapeRegExp, getExt, + time, + timeEnd, } from '#common/util'; test('getExt: no extension', (t) => { @@ -106,11 +108,31 @@ test('util: getRegExp: no', (t) => { }); test('util: escapeRegExp: no str', (t) => { - t.equal(escapeRegExp(1), 1); + const result = escapeRegExp(1); + const expected = 1; + + t.equal(result, expected); t.end(); }); test('util: escapeRegExp', (t) => { - t.equal(escapeRegExp('#hello'), '\\#hello'); + const result = escapeRegExp('#hello'); + const expected = '\\#hello'; + + t.equal(result, expected); + t.end(); +}); + +test('util: time', (t) => { + const [error] = tryCatch(time, 'test'); + + 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/package.json b/package.json index 4c50b3fc..f680e519 100644 --- a/package.json +++ b/package.json @@ -151,10 +151,10 @@ "yargs-parser": "^22.0.0" }, "devDependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/core": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.21.0", - "@babel/preset-env": "^7.0.0", + "@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", "@cloudcmd/clipboard": "^2.0.0", "@cloudcmd/create-element": "^2.0.0", "@cloudcmd/modal": "^4.0.0", @@ -162,6 +162,8 @@ "@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", @@ -171,7 +173,6 @@ "clean-css-loader": "^4.2.1", "codegen.macro": "^4.0.0", "css-loader": "^7.1.2", - "css-minimizer-webpack-plugin": "^8.0.0", "css-modules-require-hook": "^4.2.3", "cssnano-preset-default": "^8.0.1", "domtokenlist-shim": "^1.2.0", @@ -190,7 +191,6 @@ "load.js": "^3.0.0", "madrun": "^13.0.0", "memfs": "^4.2.0", - "mini-css-extract-plugin": "^2.9.2", "minor": "^1.2.2", "morgan": "^1.6.1", "multi-rename": "^3.0.0", @@ -205,7 +205,7 @@ "request": "^2.76.0", "rimraf": "^6.0.1", "scroll-into-view-if-needed": "^3.0.4", - "serve-once": "^3.0.1", + "serve-once": "^4.0.1", "smalltalk": "^5.0.0", "style-loader": "^4.0.0", "superc8": "^12.6.0", @@ -215,10 +215,7 @@ "unionfs": "^4.0.0", "url-loader": "^4.0.0", "util": "^0.12.5", - "webpack": "^5.99.9", - "webpack-cli": "^7.0.2", - "webpack-merge": "^6.0.1", - "webpackbar": "^7.0.0" + "webpack-merge": "^6.0.1" }, "imports": { "#css/": "./css/", diff --git a/rspack.config.js b/rspack.config.js new file mode 100644 index 00000000..5dcb033d --- /dev/null +++ b/rspack.config.js @@ -0,0 +1,10 @@ +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/server/cloudcmd.spec.js b/server/cloudcmd.spec.js index 97949cd3..d79aa4c1 100644 --- a/server/cloudcmd.spec.js +++ b/server/cloudcmd.spec.js @@ -1,6 +1,6 @@ import path, {dirname} from 'node:path'; import {fileURLToPath} from 'node:url'; -import serveOnce from 'serve-once'; +import {serveOnce} from 'serve-once'; import {test, stub} from 'supertape'; import cloudcmd, { _isDev, @@ -10,6 +10,9 @@ import cloudcmd, { _initAuth, _getIndexPath, } from '#server/cloudcmd'; +import {connect} from '../test/before.js'; + +const noop = () => {}; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -158,17 +161,20 @@ test('cloudcmd: auth: accept: no auth', (t) => { test('cloudcmd: getIndexPath: production', (t) => { const isDev = false; - const name = path.join(__dirname, '..', 'dist', 'index.html'); - t.equal(_getIndexPath(isDev), name); + const result = _getIndexPath(isDev); + const expected = path.join(__dirname, '..', 'dist', 'index.html'); + + t.equal(result, expected); t.end(); }); test('cloudcmd: getIndexPath: development', (t) => { const isDev = true; - const name = path.join(__dirname, '..', 'dist-dev', 'index.html'); + const result = _getIndexPath(isDev); + const expected = path.join(__dirname, '..', 'dist-dev', 'index.html'); - t.equal(_getIndexPath(isDev), name); + t.equal(result, expected); t.end(); }); @@ -178,3 +184,64 @@ test('cloudcmd: sw', async (t) => { t.equal(status, 200, 'should return sw'); t.end(); }); + +test('cloudcmd: no params', (t) => { + const middle = cloudcmd(); + + t.ok(Array.isArray(middle), 'should return middleware list when no params passed'); + t.end(); +}); + +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/distribute/log.spec.js b/server/distribute/log.spec.js index be50dcc3..1c6d1aa3 100644 --- a/server/distribute/log.spec.js +++ b/server/distribute/log.spec.js @@ -32,3 +32,32 @@ test('distribute: log: config', (t) => { }, { 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.spec.js b/server/env.spec.js index d8065b2c..5a8765f4 100644 --- a/server/env.spec.js +++ b/server/env.spec.js @@ -54,3 +54,16 @@ test('cloudcmd: server: env: bool: number: 0', (t) => { 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.spec.js b/server/exit.spec.js index 3b454818..94d627ac 100644 --- a/server/exit.spec.js +++ b/server/exit.spec.js @@ -4,44 +4,46 @@ import exit from './exit.js'; test('cloudcmd: exit: process.exit', (t) => { const {exit: exitOriginal} = process; + const exitStub = stub(); - process.exit = stub(); + process.exit = exitStub; exit(); - t.calledWith(process.exit, [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 {error} = console; + const errorStub = stub(); - console.error = stub(); + console.error = errorStub; process.exit = stub(); exit('hello world'); - t.calledWith(console.error, ['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 {error} = console; + const errorStub = stub(); - console.error = stub(); + console.error = errorStub; process.exit = stub(); exit(Error('hello world')); - t.calledWith(console.error, ['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/markdown/index.spec.js b/server/markdown/index.spec.js index 466271bd..f705915e 100644 --- a/server/markdown/index.spec.js +++ b/server/markdown/index.spec.js @@ -3,7 +3,7 @@ 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 {serveOnce} from 'serve-once'; import {cloudcmd} from '#server/cloudcmd'; import markdown from './index.js'; diff --git a/server/rest/index.js b/server/rest/index.js index 68bec54e..c3a3eda0 100644 --- a/server/rest/index.js +++ b/server/rest/index.js @@ -32,6 +32,8 @@ const UserError = (msg) => { return error; }; +export const _UserError = UserError; + export default currify(({config, fs = _fs, moveFiles = _moveFiles}, request, response, next) => { const name = ponse.getPathName(request); const regExp = RegExp(`^${apiURL}`); @@ -153,6 +155,8 @@ function getPackReg(packer) { return /\.tar\.gz$/; } +export const _getPackReg = getPackReg; + function streamPack(cmd, response, packer) { const noop = () => {}; const filename = cmd.replace(getPackReg(packer), ''); @@ -172,6 +176,8 @@ function getCMD(cmd) { return cmd; } +export const _getCMD = getCMD; + const getMoveMsg = (names) => formatMsg('move', names); const getRenameMsg = (from, to) => { @@ -276,6 +282,8 @@ function rename(rootDir, from, to, fs, callback) { 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'); @@ -294,6 +302,8 @@ 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'); @@ -307,6 +317,8 @@ 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; @@ -317,6 +329,8 @@ function getPacker(operation, packer) { return jaguar.pack; } +export const _getPacker = getPacker; + function operation(op, packer, from, to, names, fn) { if (!fn) { fn = names; @@ -345,6 +359,8 @@ 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) diff --git a/server/rest/index.spec.js b/server/rest/index.spec.js index 0fd64d7c..9c672095 100644 --- a/server/rest/index.spec.js +++ b/server/rest/index.spec.js @@ -1,4 +1,4 @@ -import {test} from 'supertape'; +import {test, stub} from 'supertape'; import {tryToCatch} from 'try-to-catch'; import { _formatMsg, @@ -6,6 +6,11 @@ import { _isRootWin32, _isRootAll, _onPUT, + _UserError, + _getPackReg, + _getCMD, + _rename, + _getPacker, } from './index.js'; test('rest: formatMsg', (t) => { @@ -70,3 +75,104 @@ test('rest: onPUT: no callback', async (t) => { t.equal(e.message, 'callback should be a function!', 'should throw when no callback'); 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/route.spec.js b/server/route.spec.js index 7387a9fc..ed19fdc6 100644 --- a/server/route.spec.js +++ b/server/route.spec.js @@ -4,7 +4,7 @@ import {Readable} from 'node:stream'; import fs from 'node:fs'; import {tryToCatch} from 'try-to-catch'; import {test, stub} from 'supertape'; -import serveOnce from 'serve-once'; +import {serveOnce} from 'serve-once'; import {cloudcmd} from '#server/cloudcmd'; import {_getReadDir} from './route.js'; diff --git a/server/themes.spec.js b/server/themes.spec.js index 1fc95ec8..709896e8 100644 --- a/server/themes.spec.js +++ b/server/themes.spec.js @@ -30,3 +30,21 @@ test('themes: no args', (t) => { 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.spec.js b/server/user-menu.spec.js index bb78b4e8..7c5fcf2c 100644 --- a/server/user-menu.spec.js +++ b/server/user-menu.spec.js @@ -2,7 +2,7 @@ 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 {serveOnce} from 'serve-once'; import {putout} from 'putout'; import {userMenu} from './user-menu.js'; diff --git a/server/validate.spec.js b/server/validate.spec.js index 76243e30..639f185c 100644 --- a/server/validate.spec.js +++ b/server/validate.spec.js @@ -131,3 +131,26 @@ test('validate: theme: wrong', (t) => { 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/test-e2e/ratelimit.js b/test-e2e/ratelimit.js index 632916c5..54cdbca8 100644 --- a/test-e2e/ratelimit.js +++ b/test-e2e/ratelimit.js @@ -1,15 +1,10 @@ import process from 'node:process'; +import {spawn} from 'node:child_process'; import {test} from 'supertape'; -let i = 0; - test('cloudcmd: server: ratelimit: x-forwarded-for', async (t) => { const PORT = 3000; - - process.env.PORT = PORT; - process.env.CLOUDCMD_LOG = 0; - - await import('../bin/cloudcmd.js'); + const child = await run(PORT); const {status} = await fetch(`http://localhost:${PORT}`, { headers: { @@ -17,7 +12,7 @@ test('cloudcmd: server: ratelimit: x-forwarded-for', async (t) => { }, }); - process.kill(process.pid, 'SIGUSR1'); + child.kill(); t.notEqual(status, 500); t.end(); @@ -27,18 +22,34 @@ test('cloudcmd: server: ratelimit', async (t) => { const PORT = 3001; const STATUS = 429; - process.env.PORT = PORT; - process.env.CLOUDCMD_LOG = 0; - - await import(`../bin/cloudcmd.js?${i++}`); + const child = await run(PORT); for (let i = 0; i < 1000; i++) { await fetch(`http://localhost:${PORT}`); } const {status} = await fetch(`http://localhost:${PORT}`); - process.kill(process.pid, 'SIGUSR1'); + 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 970a4a4f..08a50f04 100644 --- a/test/before.js +++ b/test/before.js @@ -50,8 +50,7 @@ function before(options, fn = options) { })); server.listen(() => { - fn(server - .address().port, promisify(after)); + fn(server.address().port, promisify(after)); }); } diff --git a/test/rest/config.js b/test/rest/config.js index 737d422e..b45a0d18 100644 --- a/test/rest/config.js +++ b/test/rest/config.js @@ -1,4 +1,4 @@ -import serveOnce from 'serve-once'; +import {serveOnce} from 'serve-once'; import test from 'supertape'; import {cloudcmd} from '#server/cloudcmd'; diff --git a/test/rest/copy.js b/test/rest/copy.js index f11ef7d4..e6d8a03f 100644 --- a/test/rest/copy.js +++ b/test/rest/copy.js @@ -1,7 +1,7 @@ import {dirname, join} from 'node:path'; import {fileURLToPath} from 'node:url'; import {mkdirSync} from 'node:fs'; -import serveOnce from 'serve-once'; +import {serveOnce} from 'serve-once'; import test from 'supertape'; import {rimraf} from 'rimraf'; import {cloudcmd} from '#server/cloudcmd'; diff --git a/test/rest/fs.js b/test/rest/fs.js index 721320a2..db438a71 100644 --- a/test/rest/fs.js +++ b/test/rest/fs.js @@ -1,4 +1,4 @@ -import serveOnce from 'serve-once'; +import {serveOnce} from 'serve-once'; import test from 'supertape'; import {cloudcmd} from '#server/cloudcmd'; diff --git a/test/rest/move.js b/test/rest/move.js index 696b36a3..073c520f 100644 --- a/test/rest/move.js +++ b/test/rest/move.js @@ -1,7 +1,7 @@ import {EventEmitter} from 'node:events'; import wait from '@iocmd/wait'; import {test, stub} from 'supertape'; -import serveOnce from 'serve-once'; +import {serveOnce} from 'serve-once'; import {cloudcmd} from '#server/cloudcmd'; test('cloudcmd: rest: move', async (t) => { diff --git a/test/rest/pack.js b/test/rest/pack.js index 8a970299..4c7a49ed 100644 --- a/test/rest/pack.js +++ b/test/rest/pack.js @@ -6,7 +6,7 @@ import test from 'supertape'; import tar from 'tar-stream'; import gunzip from 'gunzip-maybe'; import pullout from 'pullout'; -import serveOnce from 'serve-once'; +import {serveOnce} from 'serve-once'; import {cloudcmd} from '#server/cloudcmd'; const __filename = fileURLToPath(import.meta.url); diff --git a/test/rest/rename.js b/test/rest/rename.js index 8dd8e1f8..eb773dba 100644 --- a/test/rest/rename.js +++ b/test/rest/rename.js @@ -2,7 +2,7 @@ import fs from 'node:fs'; import test from 'supertape'; import {Volume} from 'memfs'; import {ufs} from 'unionfs'; -import serveOnce from 'serve-once'; +import {serveOnce} from 'serve-once'; import {cloudcmd} from '#server/cloudcmd'; test('cloudcmd: rest: rename', async (t) => { diff --git a/test/server/env.js b/test/server/env.js index 323c0a99..34bc8959 100644 --- a/test/server/env.js +++ b/test/server/env.js @@ -4,34 +4,37 @@ import * as env from '../../server/env.js'; test('env: small', (t) => { process.env.cloudcmd_hello = 'world'; - t.equal(env.parse('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.parse('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.notOk(env.bool('terminal'), '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'; - t.ok(env.bool('terminal'), 'should be true'); - delete process.env.cloudcmd_terminal; + + t.ok(env.bool('terminal'), 'should be true'); t.end(); }); @@ -39,9 +42,10 @@ test('env: bool: undefined', (t) => { const {cloudcmd_terminal} = process.env; process.env.cloudcmd_terminal = undefined; - - t.notOk(env.bool('terminal'), '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 5c7c6d9e..0085449e 100644 --- a/test/server/modulas.js +++ b/test/server/modulas.js @@ -1,7 +1,7 @@ import {createRequire} from 'node:module'; import {dirname, join} from 'node:path'; import {fileURLToPath} from 'node:url'; -import serveOnce from 'serve-once'; +import {serveOnce} from 'serve-once'; import {test, stub} from 'supertape'; import {cloudcmd} from '#server/cloudcmd'; import modulas from '../../server/modulas.js'; diff --git a/test/static.js b/test/static.js index cdb6b9e7..0f0da5cb 100644 --- a/test/static.js +++ b/test/static.js @@ -1,5 +1,5 @@ import {Buffer} from 'node:buffer'; -import serveOnce from 'serve-once'; +import {serveOnce} from 'serve-once'; import test from 'supertape'; import criton from 'criton'; import {cloudcmd} from '#server/cloudcmd';