Skip to content

Commit cd288c5

Browse files
committed
feat: support Windows-style paths
1 parent fb30994 commit cd288c5

7 files changed

+82
-78
lines changed

jest.config.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ module.exports = {
3434
setupFilesAfterEnv: ['./test/setup.ts'],
3535
collectCoverageFrom: ['src/**/*.ts?(x)', 'external-scripts/**/*.ts?(x)'],
3636
// ? Make sure jest-haste-map doesn't try to parse and cache fixtures
37-
modulePathIgnorePatterns: ['<rootDir>/test/fixtures'],
37+
modulePathIgnorePatterns: ['<rootDir>/test/fixtures']
3838
// ? Tell Jest to transpile node_modules (for ESM interop)
39-
transformIgnorePatterns: []
39+
//transformIgnorePatterns: []
4040
};

package-lock.json

+32-42
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,10 @@
9494
"debug": "^4.3.5",
9595
"merge-descriptors": "^2.0.0",
9696
"named-app-errors": "^4.0.2",
97-
"pkg-up": "^5.0.0",
97+
"package-up": "^5.0.0",
9898
"supports-color": "^8.1.1",
99-
"yargs": "^17.7.2"
99+
"yargs": "^17.7.2",
100+
"semver": "^7.6.3"
100101
},
101102
"devDependencies": {
102103
"@arethetypeswrong/cli": "^0.15.3",
@@ -196,7 +197,6 @@
196197
"remark-tight-comments": "^2.0.0",
197198
"remark-validate-links": "^13.0.1",
198199
"semantic-release": "https://xunn.at/[email protected]",
199-
"semver": "^7.6.3",
200200
"sort-package-json": "^2.10.0",
201201
"spellchecker": "^3.7.1",
202202
"text-extensions": "^3.0.0",

src/discover.ts

+30-22
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import assert from 'node:assert';
22
import fs from 'node:fs/promises';
33
import path from 'node:path';
4-
import url from 'node:url';
4+
import { pathToFileURL, fileURLToPath } from 'node:url';
55
import { isNativeError, isPromise } from 'node:util/types';
66

77
import makeVanillaYargs from 'yargs/yargs';
@@ -67,38 +67,38 @@ suppressNodeWarnings('ExperimentalWarning');
6767
* `PreExecutionContext::execute` is called.
6868
*/
6969
export async function discoverCommands(
70-
basePath: string,
70+
basePath_: string,
7171
context: ExecutionContext
7272
): Promise<void> {
7373
// ! Invariant: first command to be discovered, if any, is the root command.
7474
let alreadyLoadedRootCommand = false;
7575

76-
const basePath_ = basePath?.startsWith?.('file://')
77-
? url.fileURLToPath(basePath)
78-
: basePath;
76+
const basePath = basePath_?.startsWith?.('file://')
77+
? fileURLToPath(basePath_)
78+
: basePath_;
7979

8080
const debug = context.debug.extend('discover');
8181
const debug_load = debug.extend('load');
8282

83-
debug('ensuring base path directory exists and is readable: "%O"', basePath_);
83+
debug('ensuring base path directory exists and is readable: "%O"', basePath);
8484

8585
try {
86-
await fs.access(basePath_, fs.constants.R_OK);
87-
if (!(await fs.stat(basePath_)).isDirectory()) {
86+
await fs.access(basePath, fs.constants.R_OK);
87+
if (!(await fs.stat(basePath)).isDirectory()) {
8888
// ? This will be caught and re-thrown as an AssertionFailedError 👍🏿
8989
throw new Error('path is not a directory');
9090
}
9191
} catch (error) {
92-
debug.error('failed due to invalid base path "%O": %O', basePath_, error);
92+
debug.error('failed due to invalid base path "%O": %O', basePath, error);
9393
throw new AssertionFailedError(
94-
ErrorMessage.AssertionFailureBadConfigurationPath(basePath_)
94+
ErrorMessage.AssertionFailureBadConfigurationPath(basePath)
9595
);
9696
}
9797

98-
debug('searching upwards for nearest package.json file starting at %O', basePath_);
98+
debug('searching upwards for nearest package.json file starting at %O', basePath);
9999

100100
const pkg = {
101-
path: await (await import('pkg-up')).pkgUp({ cwd: basePath_ }),
101+
path: await (await import('package-up')).packageUp({ cwd: basePath }),
102102
name: undefined as string | undefined,
103103
version: undefined as string | undefined
104104
};
@@ -136,9 +136,9 @@ export async function discoverCommands(
136136
}
137137
}
138138

139-
debug('beginning configuration module auto-discovery at %O', basePath_);
139+
debug('beginning configuration module auto-discovery at %O', basePath);
140140

141-
await discover(basePath_);
141+
await discover(basePath);
142142

143143
debug('configuration module auto-discovery completed');
144144

@@ -151,7 +151,7 @@ export async function discoverCommands(
151151
debug_load.message('%O', context.commands);
152152
} else {
153153
throw new AssertionFailedError(
154-
ErrorMessage.AssertionFailureNoConfigurationLoaded(basePath_)
154+
ErrorMessage.AssertionFailureNoConfigurationLoaded(basePath)
155155
);
156156
}
157157

@@ -347,14 +347,21 @@ export async function discoverCommands(
347347
while (maybeConfigPaths.length) {
348348
try {
349349
const maybeConfigPath = maybeConfigPaths.shift()!;
350-
debug_('attempting to load configuration file: %O', maybeConfigPath);
350+
const maybeConfigFileURL = pathToFileURL(maybeConfigPath).toString();
351+
352+
debug_(
353+
'attempting to load configuration file url: %O (real path: %O)',
354+
maybeConfigFileURL,
355+
maybeConfigPath
356+
);
351357

352358
let maybeImportedConfig: ImportedConfigurationModule | undefined = undefined;
353359

354360
try {
355-
// TODO: defer importing the command/config until later?
361+
// TODO: Defer importing the command/config until later (will require
362+
// TODO: major refactor)
356363
// eslint-disable-next-line no-await-in-loop
357-
maybeImportedConfig = await import(maybeConfigPath);
364+
maybeImportedConfig = await import(maybeConfigFileURL);
358365
} catch (error) {
359366
if (
360367
isUnknownFileExtensionError(error) ||
@@ -375,7 +382,7 @@ export async function discoverCommands(
375382
const meta = {
376383
hasChildren: false,
377384
filename: path.basename(maybeConfigPath),
378-
filepath: maybeConfigPath,
385+
filepath: maybeConfigFileURL,
379386
parentDirName: path.basename(path.dirname(maybeConfigPath))
380387
} as ProgramMetadata;
381388

@@ -407,7 +414,8 @@ export async function discoverCommands(
407414

408415
if (typeof maybeImportedConfig === 'function') {
409416
debug_('configuration returned a function');
410-
// TODO: defer calling the command() function until it is needed...
417+
// TODO: Defer invoking default export until later (will require
418+
// TODO: major refactor)
411419
// eslint-disable-next-line no-await-in-loop
412420
rawConfig = await maybeImportedConfig(context);
413421
} else {
@@ -1321,10 +1329,10 @@ function defaultCommandHandler() {
13211329
}
13221330

13231331
/**
1324-
* Replace all the ASCII#32 space characters in a string with hyphens.
1332+
* Replace all space characters in a string with hyphens.
13251333
*/
13261334
function replaceSpaces(str: string) {
1327-
return str.replaceAll(' ', '-');
1335+
return str.replaceAll(/\s/g, '-');
13281336
}
13291337

13301338
/**

test/integration/integration-node-smoke.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ it('supports both CJS and ESM (js, mjs, cjs) configuration files in node CJS mod
8181
'..',
8282
'fixtures',
8383
'several-files-cjs-esm'
84-
)}', 'js cjs');`
84+
).replaceAll('\\', '/')}', 'js cjs');`
8585
}
8686
}
8787
);
@@ -111,7 +111,7 @@ export default runProgram('${join(
111111
'..',
112112
'fixtures',
113113
'several-files-cjs-esm'
114-
)}', 'js mjs');`
114+
).replaceAll('\\', '/')}', 'js mjs');`
115115
}
116116
}
117117
);

test/integration/integration-node.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ describe("import {...} from '@black-flag/core'", () => {
115115
'..',
116116
'fixtures',
117117
'one-file-index'
118-
)}').then(({execute}) => execute(['--version']));`
118+
).replaceAll('\\', '/')}').then(({execute}) => execute(['--version']));`
119119
}
120120
}
121121
);
@@ -138,7 +138,7 @@ describe("import {...} from '@black-flag/core'", () => {
138138
'..',
139139
'fixtures',
140140
'one-file-index'
141-
)}', '--version');`
141+
).replaceAll('\\', '/')}', '--version');`
142142
}
143143
}
144144
);
@@ -388,7 +388,7 @@ console.log(require('@black-flag/core/util').isPreExecutionContext({}) === false
388388
initialFileContents: {
389389
'src/index.cjs': `
390390
require('@black-flag/core/util').makeRunner({
391-
commandModulePath: '${join(__dirname, '..', 'fixtures', 'one-file-index')}'
391+
commandModulePath: '${join(__dirname, '..', 'fixtures', 'one-file-index').replaceAll('\\', '/')}'
392392
})('--help');`
393393
}
394394
}

test/unit-index.test.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
// ! OSes and Node.js versions walk the filesystem in different orders!
88

99
import assert from 'node:assert';
10+
import fsPromises from 'node:fs/promises';
1011

1112
import { engines as packageEngines } from 'package';
1213

@@ -140,10 +141,15 @@ describe('::configureProgram', () => {
140141
it('uses "name" from directory or filename without extension as root program name if no name is provided and no package.json is found', async () => {
141142
expect.hasAssertions();
142143

143-
const pkgUpNamespace = await import('pkg-up');
144-
jest
145-
.spyOn(pkgUpNamespace, 'pkgUp')
146-
.mockImplementation(() => Promise.resolve(undefined));
144+
jest.spyOn(fsPromises, 'stat').mockImplementation(async (path) => {
145+
const realResult = fsPromises.lstat(path);
146+
147+
if (path.toString().endsWith('package.json')) {
148+
return Promise.resolve({ ...(await realResult), isFile: () => false });
149+
}
150+
151+
return realResult;
152+
});
147153

148154
await withMocks(async () => {
149155
const context = await bf.configureProgram(

0 commit comments

Comments
 (0)