Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cac59cf
BREAKING: require node>=22, bump ecmaVersion to 2023
mbaumgartl Aug 7, 2026
65dbb95
ci: test against node 24 and 26
mbaumgartl Aug 7, 2026
65780d2
refactor: replace hasOwnProperty.call() with Object.hasOwn()
mbaumgartl Aug 7, 2026
8e98a80
chore: update eslint and related dependencies
mbaumgartl Aug 7, 2026
2ed1266
fix: remove useless variable assignments flagged by eslint
mbaumgartl Aug 7, 2026
d8253c9
refactor: re-reject requests with malformed Content-Type header
mbaumgartl Aug 7, 2026
7ec6c70
chore: update dependencies
mbaumgartl Aug 7, 2026
ac51046
refactor: replace content-type dependency with node:util's MIMEType
mbaumgartl Aug 10, 2026
4fd0eda
refactor: prefix core Node module requires with "node:"
mbaumgartl Aug 10, 2026
9db2b80
refactor: replace manual directory walk with fs.glob in config-loader
mbaumgartl Aug 10, 2026
8f97ba7
refactor: parse resource configs while walking config directory
mbaumgartl Aug 10, 2026
1431d57
refactor: load resource instances while walking config directory
mbaumgartl Aug 10, 2026
032f4c1
refactor: simplify config directory existence check
mbaumgartl Aug 10, 2026
b104eba
test: back url-parser request mocks with real Readable streams
mbaumgartl Aug 12, 2026
ca9ae9f
test: use local request variable in POST payload tests
mbaumgartl Aug 12, 2026
7f378ed
test: cover the request stream error path in url-parser
mbaumgartl Aug 12, 2026
e227c14
test: cover url-parser event handling against a real http.Server
mbaumgartl Aug 12, 2026
dc55538
refactor: use named capture groups when parsing the request path
mbaumgartl Aug 12, 2026
e7d8d9b
docs: fix httpToFloraRequest jsdoc parameter types
mbaumgartl Aug 12, 2026
3fd0a4f
refactor: clear postTimeout via close event instead of at each call site
mbaumgartl Aug 12, 2026
e004c06
refactor: use once for event listeners that fire at most once
mbaumgartl Aug 12, 2026
394470d
refactor: use for-of loop for GET query parameters
mbaumgartl Aug 12, 2026
8a73296
refactor: use for-of loop for urlencoded payload parameters
mbaumgartl Aug 12, 2026
c18b609
refactor: find first useless text node instead of filter+forEach
mbaumgartl Aug 12, 2026
1a27e57
refactor: use object spread in copyXmlAttributes
mbaumgartl Aug 12, 2026
f23d887
refactor: replace strRepeat helper with String.prototype.repeat
mbaumgartl Aug 12, 2026
75a33ea
refactor: use spread instead of Array.prototype.push.apply
mbaumgartl Aug 12, 2026
df8ea0c
refactor: use object spread instead of Object.assign({}, ...)
mbaumgartl Aug 12, 2026
763e854
refactor: dedupe dataSourceAttributes using Set instead of indexOf fi…
mbaumgartl Aug 12, 2026
18d85e7
refactor: replace indexOf(...) === -1 checks with Array.includes
mbaumgartl Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: ['18', '20', '22', '23']
node: ['22', '24', '26']
steps:
- uses: actions/checkout@v7
- name: Setup node ${{ matrix.node }}
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = [
globals: {
...globals.node
},
ecmaVersion: 2020,
ecmaVersion: 2023,
sourceType: 'commonjs'
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/docker/config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const path = require('path');
const path = require('node:path');

module.exports = {
resourcesPath: path.join(__dirname, 'resources'),
Expand Down
2 changes: 1 addition & 1 deletion examples/docker/worker.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const flora = require('flora');

const server = new flora.Server(require('path').join(__dirname, 'config.js'));
const server = new flora.Server(require('node:path').join(__dirname, 'config.js'));

server.run();
2 changes: 1 addition & 1 deletion examples/express/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This is an example for an adapter between Flora and Express.
```js
const express = require('express');
const flora = require('flora');
const path = require('path');
const path = require('node:path');
const floraExpress = require('./');

// Flora
Expand Down
4 changes: 2 additions & 2 deletions examples/express/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const errors = require('@florajs/errors');
const flora = require('flora');
const { URL } = require('url');
const { URL } = require('node:url');

module.exports = function (api) {
function sendResponse(response, httpRequest, httpResponse) {
Expand Down Expand Up @@ -49,7 +49,7 @@ module.exports = function (api) {
if (matches[3]) opts.format = matches[3];

parsedUrl.searchParams.forEach((value, param) => {
if (!Object.prototype.hasOwnProperty.call(opts, param)) {
if (!Object.hasOwn(opts, param)) {
if (parsedUrl.searchParams.getAll(param).length > 1) {
return next(new errors.RequestError(`Duplicate parameter "${param}" in URL`));
}
Expand Down
2 changes: 1 addition & 1 deletion examples/extensions/config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const path = require('path');
const path = require('node:path');

class EmptyDataSource {
async process(/* request */) {
Expand Down
2 changes: 1 addition & 1 deletion examples/extensions/server.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const path = require('path');
const path = require('node:path');
const flora = require('flora');

const server = new flora.Server(path.join(__dirname, 'config.js'));
Expand Down
2 changes: 1 addition & 1 deletion examples/native/native.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const path = require('path');
const path = require('node:path');
const flora = require('flora');

/*
Expand Down
2 changes: 1 addition & 1 deletion examples/plugins/master.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const path = require('path');
const path = require('node:path');
const flora = require('flora');

/*
Expand Down
2 changes: 1 addition & 1 deletion examples/plugins/worker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const path = require('path');
const path = require('node:path');
const flora = require('flora');

/*
Expand Down
2 changes: 1 addition & 1 deletion examples/simple/config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const path = require('path');
const path = require('node:path');

module.exports = {
exec: path.join(__dirname, 'worker.js'),
Expand Down
2 changes: 1 addition & 1 deletion examples/simple/master.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const path = require('path');
const path = require('node:path');
const flora = require('flora');

const master = new flora.Master(path.join(__dirname, 'config.js'));
Expand Down
2 changes: 1 addition & 1 deletion examples/simple/worker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const path = require('path');
const path = require('node:path');
const flora = require('flora');

const server = new flora.Server(path.join(__dirname, 'config.example.js'));
Expand Down
7 changes: 3 additions & 4 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ class Api extends PromiseEventEmitter {
const resource = this.getResource(request.resource);
if (!resource) throw new NotFoundError(`Unknown resource "${request.resource}" in request`);

if (!resource.actions || !Object.prototype.hasOwnProperty.call(resource.actions, request.action)) {
if (!resource.actions || !Object.hasOwn(resource.actions, request.action)) {
throw new RequestError(`Action "${request.action}" is not implemented`);
}

Expand All @@ -200,7 +200,7 @@ class Api extends PromiseEventEmitter {
throw new RequestError(`Invalid format "${request.format}" for action "${request.action}"`);
}
if (
!Object.prototype.hasOwnProperty.call(resource.actions[request.action], method) ||
!Object.hasOwn(resource.actions[request.action], method) ||
typeof resource.actions[request.action][method] !== 'function'
) {
throw new RequestError(`Invalid format "${request.format}" for action "${request.action}"`);
Expand Down Expand Up @@ -277,8 +277,7 @@ class Api extends PromiseEventEmitter {
* @returns {*}
*/
getPlugin(name) {
if (!Object.prototype.hasOwnProperty.call(this.plugins, name))
throw new Error(`Plugin "${name}" is not registered`);
if (!Object.hasOwn(this.plugins, name)) throw new Error(`Plugin "${name}" is not registered`);
return this.plugins[name];
}
}
Expand Down
20 changes: 7 additions & 13 deletions lib/ascii-art-profile.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
'use strict';

function strRepeat(str, count) {
return new Array(count + 1).join(str);
}

module.exports = function asciiArtProfile(profile, totalDuration, width) {
if (totalDuration <= 0) return ['total duration = ' + totalDuration + 'ms!? Wow ... that was fast :-)'];
if (width < 10) width = 10;
Expand All @@ -12,14 +8,12 @@ module.exports = function asciiArtProfile(profile, totalDuration, width) {
const beforeChar = '.';
const afterChar = '.';
let endChar = '';
let beforeWidth = 0;
let beginChar = '';
let durationChar = '#';
let durationWidth = 0;
let afterWidth = 0;
let description = '';
let afterWidth;
let description;

beforeWidth = Math.round((measure.startTime * width) / totalDuration);
let beforeWidth = Math.round((measure.startTime * width) / totalDuration);
if (beforeWidth > width) {
beforeWidth = width;
beginChar = '>';
Expand All @@ -29,6 +23,7 @@ module.exports = function asciiArtProfile(profile, totalDuration, width) {
beginChar = '<';
}

let durationWidth;
if (measure.duration !== null) {
durationWidth = Math.round((measure.duration * width) / totalDuration);
if (durationWidth > width - beforeWidth) {
Expand All @@ -50,7 +45,6 @@ module.exports = function asciiArtProfile(profile, totalDuration, width) {
} else {
durationWidth = width - beforeWidth;
durationChar = '?';
afterWidth = 0;
description = ' (' + measure.name + ' - still running!)';
}

Expand All @@ -63,11 +57,11 @@ module.exports = function asciiArtProfile(profile, totalDuration, width) {
}

return (
strRepeat(beforeChar, beforeWidth) +
beforeChar.repeat(beforeWidth) +
beginChar +
strRepeat(durationChar, durationWidth) +
durationChar.repeat(durationWidth) +
endChar +
strRepeat(afterChar, afterWidth) +
afterChar.repeat(afterWidth) +
description
);
});
Expand Down
2 changes: 1 addition & 1 deletion lib/cast.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class Cast {
}

if (value === null) return value;
if (Object.prototype.hasOwnProperty.call(casts, opts.type)) return casts[opts.type](value, opts, this.api);
if (Object.hasOwn(casts, opts.type)) return casts[opts.type](value, opts, this.api);

return value;
}
Expand Down
114 changes: 29 additions & 85 deletions lib/config-loader.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,9 @@
'use strict';

const fs = require('node:fs/promises');
const path = require('path');
const path = require('node:path');

/**
* Read config files from directory recursively.
*
* @param {string} configDirectory
* @param {string} resourceName
* @param {object} resources
* @return {Array}
* @private
*/
async function walk(configDirectory, resourceName, resources) {
resourceName = resourceName || '';
resources = resources || {};

for (const fileName of await fs.readdir(path.join(configDirectory, resourceName))) {
const subResourceName = (resourceName !== '' ? resourceName + '/' : '') + fileName;
const absoluteFilePath = path.join(configDirectory, subResourceName);
const stat = await fs.stat(absoluteFilePath);
if (stat && stat.isDirectory()) {
await walk(configDirectory, subResourceName, resources);
} else if (resourceName !== '') {
if (fileName.startsWith('config.')) {
if (!resources[resourceName]) resources[resourceName] = {};
resources[resourceName].configFile = absoluteFilePath;
}
if (fileName === 'index.js') {
if (!resources[resourceName]) resources[resourceName] = {};
resources[resourceName].instanceFile = absoluteFilePath;
}
}
}

return resources;
}
const { ImplementationError } = require('@florajs/errors');

/**
* Load resource configs from config directory.
Expand All @@ -53,71 +21,47 @@ module.exports = async function configLoader(api, options) {
},
...options
};
let resources;

const configDirectory = path.resolve(cfg.directory);
const configParsers = cfg.parsers;

if (
!(await fs
.access(configDirectory)
.then(() => true)
.catch(() => false))
) {
throw new Error(`Config directory "${configDirectory}" does not exist`);
}

try {
resources = await walk(configDirectory);
await fs.access(configDirectory);
} catch (err) {
err.message = 'Error reading resource directory tree: ' + err.message;
throw err;
throw new ImplementationError(`Cannot access config directory "${configDirectory}"`, { cause: err });
}

// parse all configs
await Promise.all(
Object.keys(resources).map(async (resourceName) => {
const file = resources[resourceName].configFile;
if (!file) return null;
const resources = {};
for await (const entry of fs.glob('*/**/{config.*,index.js}', { cwd: configDirectory, withFileTypes: true })) {
if (!entry.isFile()) continue;

const resourceName = path.relative(configDirectory, entry.parentPath).split(path.sep).join('/');
const absoluteFilePath = path.join(entry.parentPath, entry.name);

const extension = path.extname(file);
const type = extension.substring(1);
resources[resourceName] ??= {};

if (entry.name.startsWith('config.')) {
const type = path.extname(entry.name).substring(1);
const parseConfig = configParsers[type];

if (!parseConfig) return Promise.reject(new Error(`No "${type}" config parser registered`));
if (!parseConfig) throw new ImplementationError(`No "${type}" config parser registered`);

api.log.trace('Parsing config for resource ' + resourceName);
api.log.trace(`Parsing config for resource ${resourceName}`);
try {
resources[resourceName].config = await parseConfig(file);
delete resources[resourceName].configFile;
} catch (e) {
e.message = `Error parsing resource "${resourceName}": ${e.message}`;
throw e;
resources[resourceName].config = await parseConfig(absoluteFilePath);
} catch (err) {
throw new ImplementationError(`Error parsing resource "${resourceName}"`, { cause: err });
}
})
);

// load all resources
await Promise.all(
Object.keys(resources).map((resourceName) => {
if (!resources[resourceName].instanceFile) return null;

return new Promise((resolve, reject) => {
api.log.trace('Loading resource ' + resourceName);
const resourceFunction = require(resources[resourceName].instanceFile);
if (typeof resourceFunction !== 'function') {
return reject(
new Error(`Resource does not export a function: ${resources[resourceName].instanceFile}`)
);
}
resources[resourceName].instance = resourceFunction(api);
}

delete resources[resourceName].instanceFile;
resolve();
});
})
);
if (entry.name === 'index.js') {
api.log.trace(`Loading resource ${resourceName}`);
const resourceFunction = require(absoluteFilePath);
if (typeof resourceFunction !== 'function') {
throw new ImplementationError(`Resource does not export a function: ${absoluteFilePath}`);
}
resources[resourceName].instance = resourceFunction(api);
}
}

// done
return resources;
};
4 changes: 2 additions & 2 deletions lib/config-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function parseMap(map, context) {
* @private
*/
function checkWhitelist(str, whitelist, context) {
if (whitelist.indexOf(str) === -1) {
if (!whitelist.includes(str)) {
throw new ImplementationError(
'Invalid "' + str + '" (allowed: ' + whitelist.join(', ') + ')' + context.errorContext
);
Expand Down Expand Up @@ -642,7 +642,7 @@ function prepareDataSources(attrNode, context) {
}

// make attributes unique:
dataSourceAttributes = dataSourceAttributes.filter((value, index, self) => self.indexOf(value) === index);
dataSourceAttributes = [...new Set(dataSourceAttributes)];

try {
dataSourceInstance.prepare(dataSource, dataSourceAttributes);
Expand Down
Loading