From 68b8f670af569cbe0dab03b8c2842336cf979c8a Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sat, 4 Dec 2021 20:11:41 -0500 Subject: [PATCH 1/4] Support and test TypeScript and ESM --- .eslintignore | 1 + LICENSE | 2 +- bin/_hpal | 8 +++ lib/commands/run.js | 26 +++------ lib/helpers.js | 38 ++++++------- package.json | 13 ++++- .../package.json | 3 + .../project-a/.gitkeep | 0 .../project-b/.hc.mjs | 5 ++ test/closet/run-command-esm/package.json | 3 + test/closet/run-command-esm/server.mjs | 35 ++++++++++++ test/closet/run-command-ts/package.json | 3 + test/closet/run-command-ts/server.ts | 25 +++++++++ test/index.js | 56 +++++++++++++++++-- 14 files changed, 174 insertions(+), 44 deletions(-) create mode 100644 test/closet/non-ambiguous-hc-file-cwd-esm/package.json create mode 100644 test/closet/non-ambiguous-hc-file-cwd-esm/project-a/.gitkeep create mode 100644 test/closet/non-ambiguous-hc-file-cwd-esm/project-b/.hc.mjs create mode 100644 test/closet/run-command-esm/package.json create mode 100644 test/closet/run-command-esm/server.mjs create mode 100644 test/closet/run-command-ts/package.json create mode 100644 test/closet/run-command-ts/server.ts diff --git a/.eslintignore b/.eslintignore index fb868c4..4ccb186 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ test/closet/haute-couture-broken/node_modules/haute-couture.js test/closet/new/** +test/closet/run-command-ts/** diff --git a/LICENSE b/LICENSE index 52ea7b6..95c69a0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2017-2020, Devin Ivy and collaborators +Copyright (c) 2017-2021, Devin Ivy and collaborators Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/bin/_hpal b/bin/_hpal index 3a87c2f..25b7d01 100755 --- a/bin/_hpal +++ b/bin/_hpal @@ -1,11 +1,19 @@ #!/usr/bin/env node 'use strict'; +const Bounce = require('@hapi/bounce'); const Hpal = require('..'); const DisplayError = require('../lib/display-error'); (async () => { + try { + require('ts-node').register(); // Support .hc.ts files, etc. + } + catch (err) { + Bounce.ignore(err, { code: 'MODULE_NOT_FOUND' }); + } + try { await Hpal.start({ diff --git a/lib/commands/run.js b/lib/commands/run.js index 3b12470..939703a 100644 --- a/lib/commands/run.js +++ b/lib/commands/run.js @@ -1,6 +1,7 @@ 'use strict'; const Path = require('path'); +const Mo = require('mo-walk'); const PkgDir = require('pkg-dir'); const Helpers = require('../helpers'); const DisplayError = require('../display-error'); @@ -92,28 +93,17 @@ internals.getServer = async (root, ctx) => { const path = Path.join(root, 'server'); - try { - - const srv = require(path); - - if (typeof srv.deployment !== 'function') { - throw new DisplayError(ctx.colors.red(`No server found! To run commands the current project must export { deployment: async () => server } from ${root}/server.`)); - } - - const server = await srv.deployment(); - - await server.initialize(); + const [srv] = await Mo.tryToResolve(path) || []; - return server; + if (!srv || typeof srv.deployment !== 'function') { + throw new DisplayError(ctx.colors.red(`No server found! To run commands the current project must export { deployment: async () => server } from ${root}/server.`)); } - catch (err) { - if (err.code === 'MODULE_NOT_FOUND' && err.message.includes(`'${path}'`)) { - throw new DisplayError(ctx.colors.red(`No server found! To run commands the current project must export { deployment: async () => server } from ${root}/server.`)); - } + const server = await srv.deployment(); - throw err; - } + await server.initialize(); + + return server; }; internals.kebabize = (str) => str.replace(/[A-Z]/g, (m) => (`-${m}`).toLowerCase()); diff --git a/lib/helpers.js b/lib/helpers.js index 22cb0cc..30eadbe 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -6,6 +6,7 @@ const Util = require('util'); const AsyncHooks = require('async_hooks'); const ChildProcess = require('child_process'); const Mkdirp = require('mkdirp'); +const Mo = require('mo-walk'); const Toys = require('@hapipal/toys'); const Bounce = require('@hapi/bounce'); const Glob = require('glob'); @@ -27,29 +28,26 @@ exports.mkdirp = Mkdirp; exports.getAmendments = async (root, ctx, { amendmentsRequired = true } = {}) => { - // Fail early to avoid getAmendmentFile(), which can be expensive (globbing) + // Fail early to avoid getHcDirectory(), which can be expensive (globbing) const hc = internals.getHauteCouture(root, ctx); - let amendmentFile; + let hcDirectory; try { - amendmentFile = await internals.getAmendmentFile(root, ctx); + hcDirectory = await internals.getHcDirectory(root, ctx); } catch (err) { - Bounce.ignore(err, DisplayError); - if (amendmentsRequired) { throw err; } - - amendmentFile = null; } - const overrides = (amendmentFile !== null) ? require(amendmentFile) : {}; + const resolved = hcDirectory && await Mo.tryToResolve(Path.join(hcDirectory, '.hc')); + const overrides = resolved ? Mo.getDefaultExport(...resolved) : {}; const amendments = hc.amendments(overrides); - return { amendments, file: amendmentFile }; + return { amendments, file: resolved ? resolved[1] : null }; }; exports.withAsyncStorage = (command, run) => { @@ -91,7 +89,7 @@ internals.getHauteCouture = (root, { colors }) => { internals.glob = Util.promisify((pattern, opts, cb) => new Glob.Glob(pattern, opts, cb)); -internals.getAmendmentFile = async (root, ctx) => { +internals.getHcDirectory = async (root, ctx) => { const { colors, options: { cwd } } = ctx; @@ -113,8 +111,6 @@ internals.getAmendmentFile = async (root, ctx) => { const isAncestorPathOrNoRelation = (pathA, pathB) => isAncestorPath(pathA, pathB) || !isChildPath(pathA, pathB); - const makeFilename = (path) => Path.resolve(path, '.hc.js'); - const cull = (paths, predicate) => { let i = 0; @@ -127,27 +123,29 @@ internals.getAmendmentFile = async (root, ctx) => { if (paths.length > 1) { const relativize = (path) => Path.relative(cwd, path); - const filenames = paths.map(makeFilename).map(relativize); - throw new DisplayError(colors.red(`It's ambiguous which directory containing a .hc.js file to use: ${filenames.join(', ')}`)); + const pathnames = paths.map(relativize); + throw new DisplayError(colors.red(`It's ambiguous which directory containing a .hc.* file to use: ${pathnames.join(', ')}`)); } return paths[0]; }; - const amendmentFiles = await internals.glob('**/.hc.js', { + const unique = (arr) => [...new Set(arr)]; + + const amendmentFiles = await internals.glob(`**/.hc.{${Mo.defaultExtensions.join(',')}}`, { cwd: root, absolute: true, ignore: 'node_modules/**' }); - const amendmentPaths = amendmentFiles.map(Path.dirname); + const amendmentPaths = unique(amendmentFiles.map(Path.dirname)); // Prefer nearest ancestor... const ancestorPaths = amendmentPaths.filter((path) => isAncestorPath(path, cwd)); if (ancestorPaths.length) { - return makeFilename(cull(ancestorPaths, isChildPath)); + return cull(ancestorPaths, isChildPath); } // ... then nearest (unambiguous) child... @@ -155,14 +153,14 @@ internals.getAmendmentFile = async (root, ctx) => { const childPaths = amendmentPaths.filter((path) => isChildPath(path, cwd)); if (childPaths.length) { - return makeFilename(cull(childPaths, isAncestorPathOrNoRelation)); + return cull(childPaths, isAncestorPathOrNoRelation); } // ... then any (unambiguous) side-paths! if (amendmentPaths.length) { - return makeFilename(cull(amendmentPaths, isAncestorPathOrNoRelation)); + return cull(amendmentPaths, isAncestorPathOrNoRelation); } - throw new DisplayError(colors.red('There\'s no directory in this project containing a .hc.js file.')); + throw new DisplayError(colors.red('There\'s no directory in this project containing a .hc.* file.')); }; diff --git a/package.json b/package.json index 0d99b28..f03c5e7 100644 --- a/package.json +++ b/package.json @@ -47,18 +47,29 @@ "marked": "1.x.x", "marked-terminal": "4.x.x", "mkdirp": "1.x.x", + "mo-walk": ">=1.1.0 <2", "pkg-dir": "5.x.x", "pluralize": "8.x.x", "supports-color": "7.x.x" }, + "peerDependencies": { + "ts-node": "10.x.x" + }, + "peerDependenciesMeta": { + "@hapi/nes": { + "ts-node": true + } + }, "devDependencies": { "@hapi/boom": "9.x.x", "@hapi/code": "8.x.x", "@hapi/hapi": "20.x.x", "@hapi/lab": "24.x.x", "@hapipal/haute-couture": "4.x.x", + "@types/hapi__hapi": "20.x.x", "coveralls": "3.x.x", "rimraf": "3.x.x", - "strip-ansi": "6.x.x" + "strip-ansi": "6.x.x", + "ts-node": "10.x.x" } } diff --git a/test/closet/non-ambiguous-hc-file-cwd-esm/package.json b/test/closet/non-ambiguous-hc-file-cwd-esm/package.json new file mode 100644 index 0000000..3502f5e --- /dev/null +++ b/test/closet/non-ambiguous-hc-file-cwd-esm/package.json @@ -0,0 +1,3 @@ +{ + "name": "non-ambiguous-hc-file-cwd-esm" +} diff --git a/test/closet/non-ambiguous-hc-file-cwd-esm/project-a/.gitkeep b/test/closet/non-ambiguous-hc-file-cwd-esm/project-a/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/closet/non-ambiguous-hc-file-cwd-esm/project-b/.hc.mjs b/test/closet/non-ambiguous-hc-file-cwd-esm/project-b/.hc.mjs new file mode 100644 index 0000000..e2c4d19 --- /dev/null +++ b/test/closet/non-ambiguous-hc-file-cwd-esm/project-b/.hc.mjs @@ -0,0 +1,5 @@ +import * as HauteCouture from '@hapipal/haute-couture'; + +export default { + pluggums: HauteCouture.amendment('plugins') +}; diff --git a/test/closet/run-command-esm/package.json b/test/closet/run-command-esm/package.json new file mode 100644 index 0000000..ca9cb2f --- /dev/null +++ b/test/closet/run-command-esm/package.json @@ -0,0 +1,3 @@ +{ + "name": "run-command-esm" +} diff --git a/test/closet/run-command-esm/server.mjs b/test/closet/run-command-esm/server.mjs new file mode 100644 index 0000000..67d69d6 --- /dev/null +++ b/test/closet/run-command-esm/server.mjs @@ -0,0 +1,35 @@ +import * as Hapi from '@hapi/hapi'; + +export const deployment = async () => { + + const server = Hapi.server(); + + const register = (srv) => { + + srv.expose('commands', { + someCommand: (rootServer, args, root, ctx) => { + + ctx.options.cmd = [rootServer, args, root, ctx]; + + const stop = rootServer.stop; + rootServer.stop = async () => { + + rootServer.stop = stop; + + await rootServer.stop(); + + rootServer.stopped = true; + }; + } + }); + }; + + const plugin = { + name: 'x', + register + }; + + await server.register(plugin); + + return server; +}; diff --git a/test/closet/run-command-ts/package.json b/test/closet/run-command-ts/package.json new file mode 100644 index 0000000..4ebc23b --- /dev/null +++ b/test/closet/run-command-ts/package.json @@ -0,0 +1,3 @@ +{ + "name": "run-command-ts" +} diff --git a/test/closet/run-command-ts/server.ts b/test/closet/run-command-ts/server.ts new file mode 100644 index 0000000..db891f1 --- /dev/null +++ b/test/closet/run-command-ts/server.ts @@ -0,0 +1,25 @@ +import * as Hapi from '@hapi/hapi'; + +export const deployment = async () => { + + const server = Hapi.server(); + + const register = (srv: Hapi.Server) => { + + srv.expose('commands', { + someCommand: () => { + + console.log('some-command was run!'); + } + }); + }; + + const plugin = { + name: 'x', + register + }; + + await server.register(plugin); + + return server; +}; diff --git a/test/index.js b/test/index.js index f5ad79e..c69c33a 100644 --- a/test/index.js +++ b/test/index.js @@ -138,7 +138,7 @@ describe('hpal', () => { expect(result.err).to.be.instanceof(DisplayError); expect(result.output).to.equal(''); - expect(result.errorOutput).to.contain('There\'s no directory in this project containing a .hc.js file.'); + expect(result.errorOutput).to.contain('There\'s no directory in this project containing a .hc.* file.'); }); it('errors when there\'s no package.json file found.', async () => { @@ -156,7 +156,7 @@ describe('hpal', () => { expect(result.err).to.be.instanceof(DisplayError); expect(result.output).to.equal(''); - expect(normalize(result.errorOutput)).to.contain('It\'s ambiguous which directory containing a .hc.js file to use: project-a/.hc.js, project-b/.hc.js'); + expect(normalize(result.errorOutput)).to.contain('It\'s ambiguous which directory containing a .hc.* file to use: project-a, project-b'); expect(result.errorOutput.match(/,/g)).to.have.length(1); }); @@ -166,7 +166,7 @@ describe('hpal', () => { expect(result1.err).to.be.instanceof(DisplayError); expect(result1.output).to.equal(''); - expect(normalize(result1.errorOutput)).to.contain('It\'s ambiguous which directory containing a .hc.js file to use: ../project-a/.hc.js, ../project-b/.hc.js'); + expect(normalize(result1.errorOutput)).to.contain('It\'s ambiguous which directory containing a .hc.* file to use: ../project-a, ../project-b'); expect(result1.errorOutput.match(/,/g)).to.have.length(1); const OrigGlob = Glob.Glob; @@ -192,7 +192,7 @@ describe('hpal', () => { expect(Glob.Glob.notOriginal).to.not.exist(); expect(result2.err).to.be.instanceof(DisplayError); expect(result2.output).to.equal(''); - expect(normalize(result2.errorOutput)).to.contain('It\'s ambiguous which directory containing a .hc.js file to use: ../project-b/.hc.js, ../project-a/.hc.js'); + expect(normalize(result2.errorOutput)).to.contain('It\'s ambiguous which directory containing a .hc.* file to use: ../project-b, ../project-a'); expect(result2.errorOutput.match(/,/g)).to.have.length(1); }); @@ -230,6 +230,23 @@ describe('hpal', () => { expect(contents).to.startWith('\'use strict\';'); }); + it('succeeds when finding a .hc.mjs file from a cwd deep in the project.', async (flags) => { + + const fileCleanup = makeFileCleanup(); + fileCleanup.files.push('non-ambiguous-hc-file-cwd-esm/project-b/routes'); + flags.onCleanup = async () => await fileCleanup.cleanup(); + + const result = await RunUtil.cli(['make', 'route'], 'non-ambiguous-hc-file-cwd-esm/project-a'); + + expect(result.err).to.not.exist(); + expect(normalize(result.output)).to.contain('Wrote ../project-b/routes/index.js'); + expect(result.errorOutput).to.equal(''); + + const contents = await read('non-ambiguous-hc-file-cwd-esm/project-b/routes/index.js'); + + expect(contents).to.startWith('\'use strict\';'); + }); + it('errors when haute-couture cannot be found.', async () => { const result = await RunUtil.cli(['make', 'route'], 'no-haute-couture'); @@ -1347,6 +1364,18 @@ describe('hpal', () => { expect(result5.errorOutput).to.equal(''); }); + it('supports .hc.mjs for haute-couture-related matches.', async (flags) => { + + const mockWreck = mockWreckGet(); + flags.onCleanup = mockWreck.cleanup; + + const result = await RunUtil.cli(['docs', 'pluggums'], 'non-ambiguous-hc-file-cwd-esm'); + + expect(result.err).to.not.exist(); + expect(StripAnsi(result.output)).to.contain('# server.register('); // Matched using haute-couture manifest + expect(result.errorOutput).to.equal(''); + }); + it('does not match based on custom method.', async (flags) => { const mockWreck = mockWreckGet(); @@ -1701,6 +1730,16 @@ describe('hpal', () => { expect(result.errorOutput).to.equal(''); expect(result.options.cmd).to.equal({ start: undefined, stop: undefined }); }); + + it('supports server.mjs file.', async () => { + + const result = await RunUtil.cli(['run', 'x:some-command'], 'run-command-esm'); + + expect(result.err).to.not.exist(); + expect(result.errorOutput).to.equal(''); + expect(result.output).to.contain('Running x:some-command...'); + expect(result.output).to.contain('Complete!'); + }); }); }); @@ -1723,5 +1762,14 @@ describe('hpal', () => { expect(result.output).to.contain('["--experimental-repl-await","--use_strict"]'); expect(result.errorOutput).to.equal(''); }); + + it('support TypeScript files.', async () => { + + const result = await RunUtil.bin(['run', 'x:some-command'], `${__dirname}/closet/run-command-ts`); + + expect(result.code).to.equal(0); + expect(result.output).to.contain('some-command was run!'); + expect(result.errorOutput).to.equal(''); + }); }); }); From 776a2f5b8696f57792408da6d0981fbc4f280c50 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sat, 4 Dec 2021 20:22:07 -0500 Subject: [PATCH 2/4] Fix typo in tests, reorder assertions to get more useful output in case of failure --- test/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/index.js b/test/index.js index c69c33a..7138bb6 100644 --- a/test/index.js +++ b/test/index.js @@ -1763,13 +1763,13 @@ describe('hpal', () => { expect(result.errorOutput).to.equal(''); }); - it('support TypeScript files.', async () => { + it('supports TypeScript files.', async () => { const result = await RunUtil.bin(['run', 'x:some-command'], `${__dirname}/closet/run-command-ts`); + expect(result.errorOutput).to.equal(''); expect(result.code).to.equal(0); expect(result.output).to.contain('some-command was run!'); - expect(result.errorOutput).to.equal(''); }); }); }); From 238fe929ce32b7d294eb2ece0a69efd044d4b8d4 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sat, 4 Dec 2021 20:37:39 -0500 Subject: [PATCH 3/4] Fix typescript peer/dev dependencies --- package.json | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index f03c5e7..ee31b58 100644 --- a/package.json +++ b/package.json @@ -53,11 +53,15 @@ "supports-color": "7.x.x" }, "peerDependencies": { - "ts-node": "10.x.x" + "ts-node": "10.x.x", + "typescript": "*" }, "peerDependenciesMeta": { - "@hapi/nes": { - "ts-node": true + "ts-node": { + "optional": true + }, + "typescript": { + "optional": true } }, "devDependencies": { @@ -70,6 +74,7 @@ "coveralls": "3.x.x", "rimraf": "3.x.x", "strip-ansi": "6.x.x", - "ts-node": "10.x.x" + "ts-node": "10.x.x", + "typescript": "4.x.x" } } From 2e26e887112515f5ad4d6435898d85250a7f7c4e Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sat, 4 Dec 2021 20:56:49 -0500 Subject: [PATCH 4/4] Account for slow typescript test --- test/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/index.js b/test/index.js index 7138bb6..12b248c 100644 --- a/test/index.js +++ b/test/index.js @@ -1763,7 +1763,7 @@ describe('hpal', () => { expect(result.errorOutput).to.equal(''); }); - it('supports TypeScript files.', async () => { + it('supports TypeScript files.', { timeout: 5000 }, async () => { // Slow likely due to TypeScript init const result = await RunUtil.bin(['run', 'x:some-command'], `${__dirname}/closet/run-command-ts`);