Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ESM and stopping at index files while recursing #84

Merged
merged 4 commits into from Dec 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions API.md
Expand Up @@ -33,6 +33,7 @@ When `composeOptions` is specified then it may define the following properties:
There are many amendments that haute-couture uses to provide its default behavior, as described in [Files and directories](#files-and-directories). In the case that `amendments` defines an amendment at a place for which haute-couture has a default, the contents of `amendment` will override the default. If `amendments` contains a key whose value is `false`, that place specified by that key will be ignored by haute-couture. You may also specify the key [`HauteCouture.default` or `'$default'`](#hautecouturedefault) to define defaults for all items. The following are amendment settings that can be changed through defaults:

- `recursive` - this option causes files to be picked-up recursively within their directory rather than just files that live directly under `place`. Flip this to `false` if you would prefer not to have a nested folder structure, e.g. `routes/users/login.js` versus `routes/users-login.js`.
- `stopAtIndexes` - when used with the `recursive` option, setting this option to `true` causes files adjacent to an index file (e.g. `index.js`) to not be recursed. For example, of the files `routes/users/index.js` and `routes/users/schema.js`, the first would be seen by haute-couture and the second would be excluded.
- `include` - may be a function `(filename, path) => Boolean` or a RegExp where `filename` (a filename without extension) and `path` (a file's path relative to `place`) are particular to files under `place`. When this option is used, a file will only be used as a call when the function returns `true` or the RegExp matches `path`.
- `exclude` - takes a function or RegExp, identically to `include`. When this option is used, a file will only be used as a call when the function returns `false` or the RegExp does not match `path`. This option defaults to exclude any file that lives under a directory named `helpers/`.
- `meta` - an object containing any meta information not required by haute-couture or haute, primarily for integration with other tools.
Expand Down Expand Up @@ -352,6 +353,7 @@ A haute manifest item describes the mapping of a file/directory's place and cont
- each value exported by the files within `place` when `place` is a directory without an index file (e.g. `plugins/vision.js`, `plugins/inert.js`).
- `useFilename` - (optional) when `list` is `true` and `place` is a directory without an index file, then this option allows one to use the name of the each file within `place` to modify its contents. Should be a function with signature `function(filename, value, path)` that receives the file's `filename` (without file extension); its contents at `value`; and the file's path relative to `place`. The function should return a new value to be used as arguments for hapi plugin API call.
- `recursive` - when `true` and `list` is in effect, this option causes files to be picked-up recursively within `place` rather than just files that live directly under `place`.
- `stopAtIndexes` - when used with the `recursive` option, setting this option to `true` causes files adjacent to an index file (e.g. `index.js`) to not be recursed.
- `include` - may be a function `(filename, path) => Boolean` or a RegExp where `filename` (a filename without extension) and `path` (a file's path relative to `place`) are particular to files under `place`. When this option is used, a file will only be used as a call when the function returns `true` or the RegExp matches `path`.
- `exclude` - takes a function or RegExp, identically to `include`. When this option is used, a file will only be used as a call when the function returns `false` or the RegExp does not match `path`. This option defaults to exclude any file that lives under a directory named `helpers/`.
- `meta` - an object containing any meta information not required by haute-couture or haute, primarily for integration with other tools.
22 changes: 7 additions & 15 deletions lib/index.js
Expand Up @@ -3,6 +3,7 @@
const Path = require('path');
const Hoek = require('@hapi/hoek');
const ParentModule = require('parent-module');
const Mo = require('mo-walk');
const Haute = require('haute');
const Manifest = require('./manifest');

Expand All @@ -21,13 +22,13 @@ exports.manifest = Manifest.manifest;
exports.compose = async (server, options, { dirname, amendments } = {}) => {

dirname = dirname || Path.dirname(ParentModule());
amendments = amendments || internals.maybeGetHcFile(dirname);
amendments = amendments || await internals.maybeGetHcFile(dirname);

// Protect from usage such as { name, register: HauteCouture.compose }
Hoek.assert(Path.relative(Path.resolve(dirname, '..', '..', '..'), dirname) !== Path.join('@hapi', 'hapi', 'lib'), 'You may not have called HauteCouture.compose(), which is necessary to determine the correct dirname. Consider using HauteCouture.composeWith() for your purposes.');

const manifest = Manifest.manifest(amendments, dirname);
const calls = Haute.calls('server', manifest);
const calls = await Haute.calls('server', manifest);

return await Haute.run(calls, server, options);
};
Expand All @@ -39,20 +40,11 @@ exports.composeWith = ({ dirname, amendments } = {}) => {
return (server, options) => exports.compose(server, options, { dirname, amendments });
};

internals.maybeGetHcFile = (dirname) => {
internals.maybeGetHcFile = async (dirname) => {

const path = Path.join(dirname, '.hc');
const resolved = await Mo.tryToResolve(Path.join(dirname, '.hc'));

try {
const resolved = require.resolve(path);
return exports.getDefaultExport(require(resolved), resolved);
}
catch (err) {
// Must be an error specifically from trying to require the passed (normalized) path
// Note that these error messages are of the form "Cannot find module '${path}'".
// In node v12 this is followed by a "Require stack", which is why we're testing for
// the module path specifically wrapped in single quotes.
Hoek.assert(err.code === 'MODULE_NOT_FOUND' && err.message.includes(`'${path}'`), err);
return;
if (resolved) {
return exports.getDefaultExport(...resolved);
}
};
4 changes: 3 additions & 1 deletion lib/manifest.js
Expand Up @@ -137,9 +137,11 @@ internals.extractFromPath = (path, values) => {
// E.g. a/b/c.d.js -> a-b-c.d
internals.normalizePath = (p) => {

const basename = Path.basename(p, Path.extname(p));

return Path.join(
Path.dirname(p),
Path.basename(p, Path.extname(p))
basename === 'index' ? '' : basename
)
.split(Path.sep)
.join('-');
Expand Down
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -39,7 +39,8 @@
"dependencies": {
"@hapi/hoek": "9.x.x",
"@hapi/topo": "5.x.x",
"haute": ">=3.2.x <4",
"haute": "4.x.x",
"mo-walk": ">=1.1.0 <2",
"parent-module": "2.x.x"
},
"peerDependencies": {
Expand Down
6 changes: 6 additions & 0 deletions test/closet/decorations-deep/decorations/y/index.js
@@ -0,0 +1,6 @@
'use strict';

module.exports = {
type: 'toolkit',
method: () => null
};
7 changes: 7 additions & 0 deletions test/closet/hc-file-esm/.hc.mjs
@@ -0,0 +1,7 @@
export default {
methods: false,
controllers: {
method: 'method',
list: true
}
};
12 changes: 12 additions & 0 deletions test/closet/hc-file-esm/controllers.mjs
@@ -0,0 +1,12 @@
export default [
{
name: 'controllerOne',
method: () => 'controller-one',
options: {}
},
{
name: 'controllerTwo',
method: () => 'controller-two',
options: {}
}
];
12 changes: 12 additions & 0 deletions test/closet/hc-file-esm/methods.mjs
@@ -0,0 +1,12 @@
export default [
{
name: 'methodOne',
method: () => 'method-one',
options: {}
},
{
name: 'methodTwo',
method: () => 'method-two',
options: {}
}
];
7 changes: 7 additions & 0 deletions test/closet/stop-at-indexes/routes/helpers/index.js
@@ -0,0 +1,7 @@
'use strict';

exports.createRoute = (name) => ({
method: 'get',
path: `/${name}`,
handler: () => name
});
5 changes: 5 additions & 0 deletions test/closet/stop-at-indexes/routes/item-one.js
@@ -0,0 +1,5 @@
'use strict';

const Helpers = require('./helpers');

module.exports = Helpers.createRoute('item-one');
5 changes: 5 additions & 0 deletions test/closet/stop-at-indexes/routes/one/b/item-one.js
@@ -0,0 +1,5 @@
'use strict';

const Helpers = require('../../helpers');

module.exports = Helpers.createRoute('one/b/item-one');
5 changes: 5 additions & 0 deletions test/closet/stop-at-indexes/routes/one/index.js
@@ -0,0 +1,5 @@
'use strict';

const Helpers = require('../helpers');

module.exports = Helpers.createRoute('one/index');
5 changes: 5 additions & 0 deletions test/closet/stop-at-indexes/routes/one/item-one.js
@@ -0,0 +1,5 @@
'use strict';

const Helpers = require('../helpers');

module.exports = Helpers.createRoute('one/item-one');
5 changes: 5 additions & 0 deletions test/closet/stop-at-indexes/routes/two/e/item-one.js
@@ -0,0 +1,5 @@
'use strict';

const Helpers = require('../../helpers');

module.exports = Helpers.createRoute('two/e/item-one');
5 changes: 5 additions & 0 deletions test/closet/stop-at-indexes/routes/two/item-one.js
@@ -0,0 +1,5 @@
'use strict';

const Helpers = require('../helpers');

module.exports = Helpers.createRoute('two/item-one');
51 changes: 49 additions & 2 deletions test/index.js
Expand Up @@ -559,7 +559,9 @@ describe('HauteCouture', () => {

expect(server.decorations).to.equal({
handler: [],
toolkit: [],
toolkit: [
'y' // y/index ({ type })
],
request: [
'serverY', // server/request.y
'bySelfHasConfiguredFull', // x/server/has-configured-full ({ type, property })
Expand All @@ -569,8 +571,8 @@ describe('HauteCouture', () => {
'xW' // x/response.w
],
server: [
'xU', // server/x/u
'x', // server/x
'xU', // server/x/u
'xZ' // x/server/z
]
});
Expand Down Expand Up @@ -723,6 +725,32 @@ describe('HauteCouture', () => {
]);
});

it('recurses with stopAtIndexes.', async () => {

const server = Hapi.server();

const plugin = {
name: 'my-recursive-plugin',
register: HauteCouture.composeWith({
dirname: `${__dirname}/closet/stop-at-indexes`,
amendments: {
$default: {
stopAtIndexes: true
}
}
})
};

await server.register(plugin);

expect(server.table().map((r) => r.settings.id)).to.equal([
'item-one',
'one',
'two-item-one',
'two-e-item-one'
]);
});

describe('amendment()', () => {

it('fetches default amendment by place.', () => {
Expand Down Expand Up @@ -1187,6 +1215,25 @@ describe('HauteCouture', () => {
expect(server.methods.methodTwo).to.not.exist();
});

it('supports .hc.mjs files.', async (flags) => {

const server = Hapi.server();

const plugin = {
name: 'my-hc-plugin',
register: HauteCouture.composeWith({
dirname: `${__dirname}/closet/hc-file-esm`
})
};

await server.register(plugin);

expect(server.methods.controllerOne()).to.equal('controller-one');
expect(server.methods.controllerTwo()).to.equal('controller-two');
expect(server.methods.methodOne).to.not.exist();
expect(server.methods.methodTwo).to.not.exist();
});

it('supports .hc.ts files.', async (flags) => {

// Emulate ts-node
Expand Down