diff --git a/API.md b/API.md index f1e6be0..7f1597c 100644 --- a/API.md +++ b/API.md @@ -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. @@ -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. diff --git a/lib/index.js b/lib/index.js index 11d6498..e3f87b4 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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'); @@ -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); }; @@ -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); } }; diff --git a/lib/manifest.js b/lib/manifest.js index 0693923..79be866 100644 --- a/lib/manifest.js +++ b/lib/manifest.js @@ -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('-'); diff --git a/package.json b/package.json index 0d2e322..5d9c86c 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/test/closet/decorations-deep/decorations/y/index.js b/test/closet/decorations-deep/decorations/y/index.js new file mode 100644 index 0000000..f6ab304 --- /dev/null +++ b/test/closet/decorations-deep/decorations/y/index.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = { + type: 'toolkit', + method: () => null +}; diff --git a/test/closet/hc-file-esm/.hc.mjs b/test/closet/hc-file-esm/.hc.mjs new file mode 100644 index 0000000..8549d71 --- /dev/null +++ b/test/closet/hc-file-esm/.hc.mjs @@ -0,0 +1,7 @@ +export default { + methods: false, + controllers: { + method: 'method', + list: true + } +}; diff --git a/test/closet/hc-file-esm/controllers.mjs b/test/closet/hc-file-esm/controllers.mjs new file mode 100644 index 0000000..106005d --- /dev/null +++ b/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: {} + } +]; diff --git a/test/closet/hc-file-esm/methods.mjs b/test/closet/hc-file-esm/methods.mjs new file mode 100644 index 0000000..6299f97 --- /dev/null +++ b/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: {} + } +]; diff --git a/test/closet/stop-at-indexes/routes/helpers/index.js b/test/closet/stop-at-indexes/routes/helpers/index.js new file mode 100644 index 0000000..e2b71c5 --- /dev/null +++ b/test/closet/stop-at-indexes/routes/helpers/index.js @@ -0,0 +1,7 @@ +'use strict'; + +exports.createRoute = (name) => ({ + method: 'get', + path: `/${name}`, + handler: () => name +}); diff --git a/test/closet/stop-at-indexes/routes/item-one.js b/test/closet/stop-at-indexes/routes/item-one.js new file mode 100644 index 0000000..3b305ae --- /dev/null +++ b/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'); diff --git a/test/closet/stop-at-indexes/routes/one/b/item-one.js b/test/closet/stop-at-indexes/routes/one/b/item-one.js new file mode 100644 index 0000000..15a2530 --- /dev/null +++ b/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'); diff --git a/test/closet/stop-at-indexes/routes/one/index.js b/test/closet/stop-at-indexes/routes/one/index.js new file mode 100644 index 0000000..68e1c64 --- /dev/null +++ b/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'); diff --git a/test/closet/stop-at-indexes/routes/one/item-one.js b/test/closet/stop-at-indexes/routes/one/item-one.js new file mode 100644 index 0000000..ffc50f7 --- /dev/null +++ b/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'); diff --git a/test/closet/stop-at-indexes/routes/two/e/item-one.js b/test/closet/stop-at-indexes/routes/two/e/item-one.js new file mode 100644 index 0000000..af92345 --- /dev/null +++ b/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'); diff --git a/test/closet/stop-at-indexes/routes/two/item-one.js b/test/closet/stop-at-indexes/routes/two/item-one.js new file mode 100644 index 0000000..0d93e21 --- /dev/null +++ b/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'); diff --git a/test/index.js b/test/index.js index e4e2184..f83439c 100644 --- a/test/index.js +++ b/test/index.js @@ -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 }) @@ -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 ] }); @@ -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.', () => { @@ -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