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

fix(cli): support windows paths in the init command #2260

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
})
Loading