Skip to content

Commit

Permalink
feat: support multiple directories in "msw.workerDirectory" (#1832)
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed Jan 15, 2024
1 parent 86a22ac commit 4dbf99a
Show file tree
Hide file tree
Showing 6 changed files with 502 additions and 252 deletions.
14 changes: 11 additions & 3 deletions cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,29 @@ const yargs = require('yargs')
yargs
.usage('$0 <cmd> [args]')
.command(
'init <publicDir>',
'init',
'Initializes Mock Service Worker at the specified directory',
(yargs) => {
yargs

.positional('publicDir', {
type: 'string',
description: 'Relative path to the public directory',
required: true,
demandOption: false,
normalize: true,
})
.example('init', 'msw init public')
.option('save', {
type: 'boolean',
description: 'Save the worker directory in your package.json',
})
.option('cwd', {
type: 'string',
description: 'Custom current worker directory',
normalize: true,
})
.example('msw init')
.example('msw init ./public')
.example('msw init ./static --save')
},
require('./init'),
)
Expand Down
199 changes: 126 additions & 73 deletions cli/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,116 +6,169 @@ const inquirer = require('inquirer')
const invariant = require('./invariant')
const { SERVICE_WORKER_BUILD_PATH } = require('../config/constants')

const CWD = process.cwd()

module.exports = async function init(args) {
const { publicDir, save } = args
const [, publicDir] = args._
const CWD = args.cwd || process.cwd()

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) || [],
)

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)
printSuccessMessage([publicDir])

if (args.save) {
// Only save the public path if it's not already saved in "package.json".
if (!savedWorkerDirectories.includes(relativePublicDir)) {
saveWorkerDirectory(packageJsonPath, relativePublicDir)
}
}
// Explicitly check if "save" was not provided (was null).
// You can also provide the "--no-save" option, and then "args.save"
// will equal to false.
else if (args.save == null) {
console.log(`\
${chalk.cyan(
'INFO',
)} In order to ease the future updates to the worker script,
we recommend saving the path to the worker directory in your package.json.`)

// If the "--save" flag was not provided, prompt to save
// the public path.
promptWorkerDirectoryUpdate(
`Do you wish to save "${relativePublicDir}" as the worker directory?`,
packageJsonPath,
relativePublicDir,
)
}

return
}

// Calling "init" without a public directory but with the "--save" flag
// is 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".',
)

// If the public directory was not provided, check any existing
// paths in "msw.workerDirectory". When called without the public
// directory, the "init" command must copy the worker script
// to all the paths stored in "msw.workerDirectory".
if (savedWorkerDirectories.length > 0) {
const copyResults = await Promise.allSettled(
savedWorkerDirectories.map((destination) => {
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]
})
}),
)
const successfulPaths = copyResults
.filter((result) => result.status === 'fulfilled')
.map((result) => result.value)
const failedPathsWithErrors = copyResults
.filter((result) => result.status === 'rejected')
.map((result) => result.reason)

// Notify about failed copies, if any.
if (failedPathsWithErrors.length > 0) {
printFailureMessage(failedPathsWithErrors)
}

// Notify about successful copies, if any.
if (successfulPaths.length > 0) {
printSuccessMessage(successfulPaths)
}
}
}

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

function toAbsolute(maybeAbsolutePath, cwd) {
return path.isAbsolute(maybeAbsolutePath)
? maybeAbsolutePath
: path.resolve(cwd, maybeAbsolutePath)
}

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 = path.isAbsolute(publicDir)
? publicDir
: path.resolve(CWD, publicDir)
const relativePublicDir = path.relative(CWD, absolutePublicDir)
const dirExists = fs.existsSync(absolutePublicDir)
const absolutePublicDir = toAbsolute(destination, cwd)

if (!dirExists) {
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 create a Service Worker at "%s": directory does not exist and could not be created.\nMake sure to include a relative path to the root directory of your server.\n\nSee the original error below:\n%s',
'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,
)
}

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

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

fs.copyFileSync(SERVICE_WORKER_BUILD_PATH, swDestFilepath)

console.log(`
${chalk.green('Service Worker successfully created!')}
${chalk.gray(swDestFilepath)}
return swDestFilepath
}

Continue by creating a mocking definition module in your application:
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/mocks')}
${chalk.cyan.bold('https://mswjs.io/docs/getting-started')}
`)
}

const packageJsonPath = path.resolve(CWD, 'package.json')
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
const { msw: mswConfig } = packageJson

if (mswConfig && mswConfig.workerDirectory === relativePublicDir) {
return
}

// When called `msw init --no-save` do not show the save suggestions.
if (save === false) {
return
}

if (!mswConfig) {
console.log(`\
${chalk.cyan('INFO')} In order to ease the future updates to the worker script,
we recommend saving the path to the worker directory in your package.json.
`)

if (save) {
return saveWorkerDirectory(packageJsonPath, relativePublicDir)
}

return promptWorkerDirectoryUpdate(
`Do you wish to save "${relativePublicDir}" as the worker directory?`,
packageJsonPath,
relativePublicDir,
)
}

if (mswConfig.workerDirectory !== relativePublicDir) {
console.log(
`\
${chalk.yellowBright(
'WARN',
)} The "msw.workerDirectory" in your package.json ("%s")
is different from the worker directory used right now ("%s").
`,
mswConfig.workerDirectory,
relativePublicDir,
)

if (save) {
return saveWorkerDirectory(packageJsonPath, relativePublicDir)
}

return promptWorkerDirectoryUpdate(
`Do you wish to use "${relativePublicDir}" instead of "${mswConfig.workerDirectory}" as the worker directory?`,
packageJsonPath,
relativePublicDir,
)
}
function printFailureMessage(pathsWithErrors) {
console.error(`\
${chalk.red('Copying the worker script failed at following paths:')}
${pathsWithErrors
.map(([path, error]) => chalk.gray(` - ${path}`) + '\n' + ` ${error}`)
.join('\n\n')}
`)
}

function saveWorkerDirectory(packageJsonPath, publicDir) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))

console.log(
chalk.gray('Writing "msw.workerDirectory" to "%s"...'),
chalk.gray('Updating "msw.workerDirectory" at "%s"...'),
packageJsonPath,
)

const prevWorkerDirectory = Array.prototype.concat(
(packageJson.msw && packageJson.msw.workerDirectory) || [],
)
const nextWorkerDirectory = Array.from(
new Set(prevWorkerDirectory).add(publicDir),
)

const nextPackageJson = Object.assign({}, packageJson, {
msw: {
workerDirectory: publicDir,
workerDirectory: nextWorkerDirectory,
},
})

Expand Down
28 changes: 8 additions & 20 deletions config/scripts/postinstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ const { execSync } = require('child_process')
// NPM stores the parent project directory in the "INIT_CWD" env variable.
const parentPackageCwd = process.env.INIT_CWD

function postinstall() {
// 1. Check if "package.json" has "msw.workerDirectory" property set.
function postInstall() {
const packageJson = JSON.parse(
fs.readFileSync(path.resolve(parentPackageCwd, 'package.json'), 'utf8'),
)
Expand All @@ -17,32 +16,21 @@ function postinstall() {
return
}

// 2. Check if the worker directory is an existing path.
const { workerDirectory } = packageJson.msw
const absoluteWorkerDirectory = path.resolve(
parentPackageCwd,
workerDirectory,
)

if (!fs.existsSync(absoluteWorkerDirectory)) {
return console.error(
`[MSW] Failed to automatically update the worker script at "%s": given path does not exist.`,
workerDirectory,
)
}

// 3. Update the worker script.
const cliExecutable = path.resolve(process.cwd(), 'cli/index.js')

try {
execSync(`node ${cliExecutable} init ${absoluteWorkerDirectory}`, {
/**
* @note Call the "init" command directly. It will now copy the worker script
* to all saved paths in "msw.workerDirectory"
*/
execSync(`node ${cliExecutable} init`, {
cwd: parentPackageCwd,
})
} catch (error) {
console.error(
`[MSW] Failed to automatically update the worker script:\n${error}`,
`[MSW] Failed to automatically update the worker script.\n\n${error}`,
)
}
}

postinstall()
postInstall()
Loading

0 comments on commit 4dbf99a

Please sign in to comment.