-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
fixtures.ts
344 lines (277 loc) · 10.1 KB
/
fixtures.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
import fs from 'fs-extra'
import _path from 'path'
import chokidar from 'chokidar'
import cachedir from 'cachedir'
import execa from 'execa'
import tempDir from 'temp-dir'
const root = _path.join(__dirname, '..')
const serverRoot = _path.join(__dirname, '../../packages/server/')
const projects = _path.join(root, 'projects')
const cyTmpDir = _path.join(tempDir, 'cy-projects')
// copy contents instead of deleting+creating new file, which can cause
// filewatchers to lose track of toFile.
const copyContents = (fromFile, toFile) => {
return Promise.all([
fs.open(toFile, 'w'),
fs.readFile(fromFile),
])
.then(([toFd, fromFileBuf]) => {
return fs.write(toFd, fromFileBuf)
.finally(() => {
return fs.close(toFd)
})
})
}
// copies all of the project fixtures
// to the cyTmpDir .projects in the root
export function scaffold () {
fs.copySync(projects, cyTmpDir)
}
/**
* Given a project name, copy the project's test files to the temp dir.
*/
export function scaffoldProject (project: string): void {
const from = _path.join(projects, project)
const to = _path.join(cyTmpDir, project)
fs.copySync(from, to)
}
/**
* Symlink the cached `node_modules` directory to the temp project directory's `node_modules`.
*/
async function symlinkNodeModulesFromCache (project: string, cacheDir: string): Promise<void> {
const from = _path.join(projectPath(project), 'node_modules')
try {
await fs.stat(cacheDir)
} catch (err) {
console.log(`📦 Creating a new node_modules cache dir at ${cacheDir}`)
await fs.mkdirp(cacheDir)
}
try {
await fs.symlink(cacheDir, from, 'junction')
} catch (err) {
if (err.code !== 'EEXIST') return
}
console.log(`📦 node_modules symlink created at ${from}`)
}
/**
* Given a package name, returns the path to the module directory on disk.
*/
function pathToPackage (pkg: string): string {
return _path.dirname(require.resolve(`${pkg}/package.json`))
}
/**
* Given a path to a `package.json`, convert any references to development
* versions of packages to absolute paths, so `yarn` will not reach out to
* the Internet to obtain these packages once it runs in the temp dir.
* @returns a list of dependency names that were updated
*/
async function makeWorkspacePackagesAbsolute (pathToPkgJson: string): Promise<string[]> {
const pkgJson = await fs.readJson(pathToPkgJson)
const updatedDeps: string[] = []
for (const deps of [pkgJson.dependencies, pkgJson.devDependencies, pkgJson.optionalDependencies]) {
for (const dep in deps) {
const version = deps[dep]
if (version.startsWith('file:')) {
const absPath = pathToPackage(dep)
console.log(`📦 Setting absolute path in package.json for ${dep}: ${absPath}.`)
deps[dep] = `file:${absPath}`
updatedDeps.push(dep)
}
}
}
await fs.writeJson(pathToPkgJson, pkgJson)
return updatedDeps
}
function getYarnCommand (opts: {
yarnV311: boolean
updateYarnLock: boolean
isCI: boolean
runScripts: boolean
}): string {
let cmd = `yarn install`
if (opts.yarnV311) {
// @see https://yarnpkg.com/cli/install
if (!opts.runScripts) cmd += ' --mode=skip-build'
if (!opts.updateYarnLock) cmd += ' --immutable'
return cmd
}
cmd += ' --prefer-offline'
if (!opts.runScripts) cmd += ' --ignore-scripts'
if (!opts.updateYarnLock) cmd += ' --frozen-lockfile'
// yarn v1 has a bug with integrity checking and local cache/dependencies
// @see https://github.com/yarnpkg/yarn/issues/6407
cmd += ' --update-checksums'
// in CircleCI, this offline cache can be used
if (opts.isCI) cmd += ` --cache-folder=~/.yarn-${process.platform} `
else cmd += ` --cache-folder=${_path.join(tempDir, 'cy-system-tests-yarn-cache', String(Date.now()))}`
return cmd
}
type Dependencies = Record<string, string>
/**
* Type for package.json files for system-tests example projects.
*/
type SystemTestPkgJson = {
/**
* By default, scaffolding will run `yarn install` if there is a `package.json`.
* This option, if set, disables that.
*/
_cySkipYarnInstall?: boolean
/**
* Run the yarn v2-style install command instead of yarn v1-style.
*/
_cyYarnV311?: boolean
/**
* By default, the automatic `yarn install` will not run postinstall scripts. This
* option, if set, will cause postinstall scripts to run for this project.
*/
_cyRunScripts?: boolean
dependencies?: Dependencies
devDependencies?: Dependencies
optionalDependencies?: Dependencies
}
/**
* Given a `system-tests` project name, detect and install the `node_modules`
* specified in the project's `package.json`. No-op if no `package.json` is found.
*/
export async function scaffoldProjectNodeModules (project: string, updateYarnLock: boolean = !!process.env.UPDATE_YARN_LOCK): Promise<void> {
const projectDir = projectPath(project)
const relativePathToMonorepoRoot = _path.relative(
_path.join(projects, project),
_path.join(root, '..'),
)
const projectPkgJsonPath = _path.join(projectDir, 'package.json')
const runCmd = async (cmd) => {
console.log(`📦 Running "${cmd}" in ${projectDir}`)
await execa.shell(cmd, { cwd: projectDir, stdio: 'inherit' })
}
const cacheDir = _path.join(cachedir('cy-system-tests-node-modules'), project, 'node_modules')
async function removeWorkspacePackages (packages: string[]): Promise<void> {
for (const dep of packages) {
const depDir = _path.join(cacheDir, dep)
await fs.remove(depDir)
}
}
try {
// this will throw and exit early if the package.json does not exist
const pkgJson: SystemTestPkgJson = require(projectPkgJsonPath)
console.log(`📦 Found package.json for project ${project}.`)
if (pkgJson._cySkipYarnInstall) {
return console.log(`📦 cySkipYarnInstall set in package.json, skipping yarn steps`)
}
if (!pkgJson.dependencies && !pkgJson.devDependencies && !pkgJson.optionalDependencies) {
return console.log(`📦 No dependencies found, skipping yarn steps`)
}
// 1. Ensure there is a cache directory set up for this test project's `node_modules`.
await symlinkNodeModulesFromCache(project, cacheDir)
// 2. Before running `yarn`, resolve workspace deps to absolute paths.
// This is required to fix `yarn install` for workspace-only packages.
const workspaceDeps = await makeWorkspacePackagesAbsolute(projectPkgJsonPath)
await removeWorkspacePackages(workspaceDeps)
// 3. Fix relative paths in temp dir's `yarn.lock`.
const relativePathToProjectDir = _path.relative(projectDir, _path.join(root, '..'))
const yarnLockPath = _path.join(projectDir, 'yarn.lock')
console.log('📦 Writing yarn.lock with fixed relative paths to temp dir')
try {
const yarnLock = (await fs.readFile(yarnLockPath, 'utf8'))
.replaceAll(relativePathToMonorepoRoot, relativePathToProjectDir)
await fs.writeFile(yarnLockPath, yarnLock)
} catch (err) {
if (err.code !== 'ENOENT' || !updateYarnLock) throw err
console.log('📦 No yarn.lock found, continuing')
}
// 4. Run `yarn install`.
const cmd = getYarnCommand({
updateYarnLock,
yarnV311: pkgJson._cyYarnV311,
isCI: !!process.env.CI,
runScripts: pkgJson._cyRunScripts,
})
await runCmd(cmd)
console.log(`📦 Copying yarn.lock and fixing relative paths for ${project}`)
// Replace workspace dependency paths in `yarn.lock` with tokens so it can be the same
// for all developers
const yarnLock = (await fs.readFile(yarnLockPath, 'utf8'))
.replaceAll(relativePathToProjectDir, relativePathToMonorepoRoot)
await fs.writeFile(_path.join(projects, project, 'yarn.lock'), yarnLock)
// 5. After `yarn install`, we must now symlink *over* all workspace dependencies, or else
// `require` calls from `yarn install`'d workspace deps to peer deps will fail.
await removeWorkspacePackages(workspaceDeps)
for (const dep of workspaceDeps) {
console.log(`📦 Symlinking workspace dependency: ${dep}`)
const depDir = _path.join(cacheDir, dep)
await fs.symlink(pathToPackage(dep), depDir, 'junction')
}
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') return
console.error(`⚠ An error occurred while installing the node_modules for ${project}.`)
console.error([err.message, err.stack].join('\n'))
throw err
}
}
export async function scaffoldCommonNodeModules () {
await Promise.all([
'@cypress/code-coverage',
'@cypress/webpack-dev-server',
'@packages/socket',
'@packages/ts',
'@tooling/system-tests',
'bluebird',
'chai',
'dayjs',
'debug',
'execa',
'fs-extra',
'https-proxy-agent',
'jimp',
'lazy-ass',
'lodash',
'proxyquire',
'react',
'semver',
'systeminformation',
'tslib',
'typescript',
].map(symlinkNodeModule))
}
export async function symlinkNodeModule (pkg) {
const from = _path.join(cyTmpDir, 'node_modules', pkg)
const to = pathToPackage(pkg)
await fs.ensureDir(_path.dirname(from))
try {
await fs.symlink(to, from, 'junction')
} catch (err) {
if (err.code === 'EEXIST') return
throw err
}
}
export function scaffoldWatch () {
const watchdir = _path.resolve(__dirname, '../projects')
console.log('watching files due to --no-exit', watchdir)
chokidar.watch(watchdir, {
})
.on('change', (srcFilepath, stats) => {
const tmpFilepath = _path.join(cyTmpDir, _path.relative(watchdir, srcFilepath))
return copyContents(srcFilepath, tmpFilepath)
})
.on('error', console.error)
}
// removes all of the project fixtures
// from the cyTmpDir .projects in the root
export function remove () {
return fs.removeSync(cyTmpDir)
}
// returns the path to project fixture
// in the cyTmpDir
export function project (...args) {
return this.projectPath.apply(this, args)
}
export function projectPath (name) {
return _path.join(cyTmpDir, name)
}
export function get (fixture, encoding: BufferEncoding = 'utf8') {
return fs.readFileSync(_path.join(serverRoot, 'test', 'support', 'fixtures', fixture), { encoding })
}
export function path (fixture) {
return _path.join(serverRoot, 'test', 'support', 'fixtures', fixture)
}
export default module.exports