Skip to content

Commit

Permalink
fix(cli): support windows paths in the init command (#2260)
Browse files Browse the repository at this point in the history
Co-authored-by: Artem Zakharchenko <kettanaito@gmail.com>
  • Loading branch information
ivanfernandez2646 and kettanaito authored Sep 4, 2024
1 parent 6b2a7e6 commit ba285b8
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 37 deletions.
1 change: 0 additions & 1 deletion cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ yargs
'Initializes Mock Service Worker at the specified directory',
(yargs) => {
yargs

.positional('publicDir', {
type: 'string',
description: 'Relative path to the public directory',
Expand Down
90 changes: 59 additions & 31 deletions cli/init.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
const { until } = require('@open-draft/until')
const confirm = require('@inquirer/confirm').default
const invariant = require('./invariant')
const { SERVICE_WORKER_BUILD_PATH } = require('../config/constants')

module.exports = async function init(args) {
const [, publicDir] = args._
const CWD = args.cwd || process.cwd()
const publicDir = args._[1] ? normalizePath(args._[1]) : undefined

const packageJsonPath = path.resolve(CWD, 'package.json')
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
const savedWorkerDirectories = Array.prototype.concat(
(packageJson.msw && packageJson.msw.workerDirectory) || [],
)
const savedWorkerDirectories = Array.prototype
.concat((packageJson.msw && packageJson.msw.workerDirectory) || [])
.map(normalizePath)

if (publicDir) {
// If the public directory was provided, copy the worker script
// to that directory only. Even if there are paths stored in "msw.workerDirectory",
// those will not be touched.
await copyWorkerScript(publicDir, CWD)
const relativePublicDir = toRelative(publicDir, CWD)
const relativePublicDir = path.relative(CWD, publicDir)
printSuccessMessage([publicDir])

if (args.save) {
Expand Down Expand Up @@ -52,8 +51,7 @@ module.exports = async function init(args) {
return
}

// Calling "init" without a public directory but with the "--save" flag
// is no-op.
// Calling "init" without a public directory but with the "--save" flag is a no-op.
invariant(
args.save == null,
'Failed to copy the worker script: cannot call the "init" command without a public directory but with the "--save" flag. Either drop the "--save" flag to copy the worker script to all paths listed in "msw.workerDirectory", or add an explicit public directory to the command, like "npx msw init ./public".',
Expand All @@ -69,7 +67,7 @@ module.exports = async function init(args) {
return copyWorkerScript(destination, CWD).catch((error) => {
// Inject the absolute destination path onto the copy function rejections
// so it's available in the failed paths array below.
throw [toAbsolute(destination, CWD), error]
throw [toAbsolutePath(destination, CWD), error]
})
}),
)
Expand All @@ -92,51 +90,61 @@ module.exports = async function init(args) {
}
}

function toRelative(absolutePath, cwd) {
return path.relative(cwd, absolutePath)
}

function toAbsolute(maybeAbsolutePath, cwd) {
/**
* @param {string} maybeAbsolutePath
* @param {string} cwd
* @returns {string}
*/
function toAbsolutePath(maybeAbsolutePath, cwd) {
return path.isAbsolute(maybeAbsolutePath)
? maybeAbsolutePath
: path.resolve(cwd, maybeAbsolutePath)
}

/**
* @param {string} destination
* @param {string} cwd
* @returns {Promise<string>}
*/
async function copyWorkerScript(destination, cwd) {
// When running as a part of "postinstall" script, "cwd" equals the library's directory.
// The "postinstall" script resolves the right absolute public directory path.
const absolutePublicDir = toAbsolute(destination, cwd)
const absolutePublicDir = toAbsolutePath(destination, cwd)

if (!fs.existsSync(absolutePublicDir)) {
// Try to create the directory if it doesn't exist
const createDirectoryResult = await until(() =>
fs.promises.mkdir(absolutePublicDir, { recursive: true }),
)

invariant(
createDirectoryResult.error == null,
'Failed to copy the worker script at "%s": directory does not exist and could not be created.\nMake sure to include a relative path to the public directory of your application.\n\nSee the original error below:\n%s',
absolutePublicDir,
createDirectoryResult.error,
)
await fs.promises
.mkdir(absolutePublicDir, { recursive: true })
.catch((error) => {
throw new Error(
invariant(
false,
'Failed to copy the worker script at "%s": directory does not exist and could not be created.\nMake sure to include a relative path to the public directory of your application.\n\nSee the original error below:\n\n%s',
absolutePublicDir,
error,
),
)
})
}

console.log('Copying the worker script at "%s"...', absolutePublicDir)

const serviceWorkerFilename = path.basename(SERVICE_WORKER_BUILD_PATH)
const swDestFilepath = path.resolve(absolutePublicDir, serviceWorkerFilename)
const workerFilename = path.basename(SERVICE_WORKER_BUILD_PATH)
const workerDestinationPath = path.resolve(absolutePublicDir, workerFilename)

fs.copyFileSync(SERVICE_WORKER_BUILD_PATH, swDestFilepath)
fs.copyFileSync(SERVICE_WORKER_BUILD_PATH, workerDestinationPath)

return swDestFilepath
return workerDestinationPath
}

/**
* @param {Array<string>} paths
*/
function printSuccessMessage(paths) {
console.log(`
${chalk.green('Worker script successfully copied!')}
${paths.map((path) => chalk.gray(` - ${path}\n`))}
Continue by describing the network in your application:
${chalk.cyan.bold('https://mswjs.io/docs/getting-started')}
`)
Expand All @@ -151,6 +159,10 @@ ${pathsWithErrors
`)
}

/**
* @param {string} packageJsonPath
* @param {string} publicDir
*/
function saveWorkerDirectory(packageJsonPath, publicDir) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))

Expand Down Expand Up @@ -179,6 +191,12 @@ function saveWorkerDirectory(packageJsonPath, publicDir) {
)
}

/**
* @param {string} message
* @param {string} packageJsonPath
* @param {string} publicDir
* @returns {void}
*/
function promptWorkerDirectoryUpdate(message, packageJsonPath, publicDir) {
return confirm({
theme: {
Expand All @@ -191,3 +209,13 @@ function promptWorkerDirectoryUpdate(message, packageJsonPath, publicDir) {
}
})
}

/**
* Normalizes the given path, replacing ambiguous path separators
* with the platform-specific path separator.
* @param {string} input Path to normalize.
* @returns {string}
*/
function normalizePath(input) {
return input.replace(/[\\|\/]+/g, path.sep)
}
2 changes: 1 addition & 1 deletion cli/invariant.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const chalk = require('chalk')

module.exports = function (predicate, message, ...args) {
module.exports = function invariant(predicate, message, ...args) {
if (!predicate) {
console.error(chalk.red(message), ...args)
process.exit(1)
Expand Down
88 changes: 84 additions & 4 deletions test/node/msw-api/cli/init.node.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* @vitest-environment node
*/
// @vitest-environment node
import fs from 'fs'
import path from 'node:path'
import { createTeardown } from 'fs-teardown'
import { fromTemp } from '../../../support/utils'

Expand Down Expand Up @@ -34,7 +33,6 @@ afterEach(() => {
})

afterAll(async () => {
vi.restoreAllMocks()
await fsMock.cleanup()
})

Expand Down Expand Up @@ -378,3 +376,85 @@ test('prints the list of failed paths to copy', async () => {
expect.stringContaining(copyFileError.message),
)
})

test('supports a mix of unix/windows paths in "workerDirectory"', async () => {
await fsMock.create({
'package.json': JSON.stringify({
name: 'example',
msw: {
// Use a mix of different path styles to emulate multiple developers
// working from different operating systems.
workerDirectory: [
path.win32.join('public', 'windows-style'),
'unix/style',
],
},
}),
})

const initCommand = await init([''])

expect(initCommand.stderr).toBe('')
expect(
fs.existsSync(fsMock.resolve('public/windows-style/mockServiceWorker.js')),
).toBe(true)
expect(fs.existsSync(fsMock.resolve('unix/style/mockServiceWorker.js'))).toBe(
true,
)

const normalizedPaths = readJson(fsMock.resolve('package.json')).msw
.workerDirectory

// Expect normalized paths
expect(normalizedPaths).toContain('public\\windows-style')
expect(normalizedPaths).toContain('unix/style')
})

test('copies the script only to provided windows path in args', async () => {
await fsMock.create({
'package.json': JSON.stringify({
name: 'example',
msw: {
workerDirectory: ['unix/style'],
},
}),
})

const initCommand = await init([
`"${path.win32.join('.', 'windows-style', 'new-folder')}"`,
'--save',
])

expect(initCommand.stderr).toBe('')
expect(
fs.existsSync(
fsMock.resolve('windows-style/new-folder/mockServiceWorker.js'),
),
).toBe(true)
expect(fs.existsSync(fsMock.resolve('unix/style/mockServiceWorker.js'))).toBe(
false,
)
})

test('copies the script only to provided unix path in args', async () => {
await fsMock.create({
'package.json': JSON.stringify({
name: 'example',
msw: {
workerDirectory: [path.win32.join('windows-style', 'new-folder')],
},
}),
})

const initCommand = await init(['./unix/style', '--save'])

expect(initCommand.stderr).toBe('')
expect(fs.existsSync(fsMock.resolve('unix/style/mockServiceWorker.js'))).toBe(
true,
)
expect(
fs.existsSync(
fsMock.resolve('windows-style/new-folder/mockServiceWorker.js'),
),
).toBe(false)
})

0 comments on commit ba285b8

Please sign in to comment.