@netlify/cache-utils 4.1.5-rc → 4.1.6-rc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/dir.js ADDED
@@ -0,0 +1,8 @@
1
+ import { resolve } from 'path'
2
+
3
+ // Retrieve the cache directory location
4
+ export const getCacheDir = function ({ cacheDir = DEFAULT_CACHE_DIR, cwd = '.' } = {}) {
5
+ return resolve(cwd, cacheDir)
6
+ }
7
+
8
+ const DEFAULT_CACHE_DIR = '.netlify/cache/'
package/lib/expire.js ADDED
@@ -0,0 +1,15 @@
1
+ // Retrieve the expiration date when caching a file
2
+ export const getExpires = function (ttl) {
3
+ if (!Number.isInteger(ttl) || ttl < 1) {
4
+ return
5
+ }
6
+
7
+ return Date.now() + ttl * SECS_TO_MSECS
8
+ }
9
+
10
+ const SECS_TO_MSECS = 1e3
11
+
12
+ // Check if a file about to be restored is expired
13
+ export const checkExpires = function (expires) {
14
+ return expires !== undefined && Date.now() > expires
15
+ }
package/lib/fs.js ADDED
@@ -0,0 +1,60 @@
1
+ import { promises as fs } from 'fs'
2
+ import { basename, dirname } from 'path'
3
+
4
+ import cpy from 'cpy'
5
+ import { globby } from 'globby'
6
+ import { isNotJunk } from 'junk'
7
+ import { moveFile } from 'move-file'
8
+
9
+ // Move or copy a cached file/directory from/to a local one
10
+ export const moveCacheFile = async function (src, dest, move) {
11
+ // Moving is faster but removes the source files locally
12
+ if (move) {
13
+ return moveFile(src, dest, { overwrite: false })
14
+ }
15
+
16
+ const { srcGlob, cwd } = await getSrcGlob(src)
17
+ return cpy(srcGlob, dirname(dest), { cwd, parents: true, overwrite: false })
18
+ }
19
+
20
+ // Non-existing files and empty directories are always skipped
21
+ export const hasFiles = async function (src) {
22
+ const { srcGlob, cwd, isDir } = await getSrcGlob(src)
23
+ return srcGlob !== undefined && !(await isEmptyDir({ srcGlob, cwd, isDir }))
24
+ }
25
+
26
+ // Replicates what `cpy` is doing under the hood.
27
+ const isEmptyDir = async function ({ srcGlob, cwd, isDir }) {
28
+ if (!isDir) {
29
+ return false
30
+ }
31
+
32
+ const files = await globby(srcGlob, { cwd })
33
+ const filteredFiles = files.filter((file) => isNotJunk(basename(file)))
34
+ return filteredFiles.length === 0
35
+ }
36
+
37
+ // Get globbing pattern with files to move/copy
38
+ const getSrcGlob = async function (src) {
39
+ const srcStat = await getStat(src)
40
+
41
+ if (srcStat === undefined) {
42
+ return {}
43
+ }
44
+
45
+ const isDir = srcStat.isDirectory()
46
+ const srcBasename = basename(src)
47
+ const cwd = dirname(src)
48
+
49
+ if (isDir) {
50
+ return { srcGlob: `${srcBasename}/**`, cwd, isDir }
51
+ }
52
+
53
+ return { srcGlob: srcBasename, cwd, isDir }
54
+ }
55
+
56
+ const getStat = async function (src) {
57
+ try {
58
+ return await fs.stat(src)
59
+ } catch {}
60
+ }
package/lib/hash.js ADDED
@@ -0,0 +1,36 @@
1
+ import { createHash } from 'crypto'
2
+ import { createReadStream } from 'fs'
3
+
4
+ import getStream from 'get-stream'
5
+ import { locatePath } from 'locate-path'
6
+
7
+ // Caching a big directory like `node_modules` is slow. However those can
8
+ // sometime be represented by a digest file such as `package-lock.json`. If this
9
+ // has not changed, we don't need to save cache again.
10
+ export const getHash = async function (digests, move) {
11
+ // Moving files is faster than computing hashes
12
+ if (move || digests.length === 0) {
13
+ return
14
+ }
15
+
16
+ const digestPath = await locatePath(digests)
17
+ if (digestPath === undefined) {
18
+ return
19
+ }
20
+
21
+ const hash = await hashFile(digestPath)
22
+ return hash
23
+ }
24
+
25
+ // Hash a file's contents
26
+ const hashFile = async function (path) {
27
+ const contentStream = createReadStream(path, 'utf8')
28
+ const hashStream = createHash(HASH_ALGO, { encoding: 'hex' })
29
+ contentStream.pipe(hashStream)
30
+ const hash = await getStream(hashStream)
31
+ return hash
32
+ }
33
+
34
+ // We need a hashing algoritm that's as fast as possible.
35
+ // Userland CRC32 implementations are actually slower than Node.js SHA1.
36
+ const HASH_ALGO = 'sha1'
package/lib/list.js ADDED
@@ -0,0 +1,29 @@
1
+ import { join } from 'path'
2
+
3
+ import readdirp from 'readdirp'
4
+
5
+ import { getCacheDir } from './dir.js'
6
+ import { isManifest } from './manifest.js'
7
+ import { getBases } from './path.js'
8
+
9
+ // List all cached files/directories, at the top-level
10
+ export const list = async function ({ cacheDir, cwd: cwdOpt, depth = DEFAULT_DEPTH } = {}) {
11
+ const bases = await getBases(cwdOpt)
12
+ const cacheDirA = getCacheDir({ cacheDir, cwd: cwdOpt })
13
+ const files = await Promise.all(bases.map(({ name, base }) => listBase({ name, base, cacheDir: cacheDirA, depth })))
14
+ const filesA = files.flat()
15
+ return filesA
16
+ }
17
+
18
+ const DEFAULT_DEPTH = 1
19
+
20
+ // TODO: the returned paths are missing the Windows drive
21
+ const listBase = async function ({ name, base, cacheDir, depth }) {
22
+ const files = await readdirp.promise(`${cacheDir}/${name}`, { fileFilter, depth, type: 'files_directories' })
23
+ const filesA = files.map(({ path }) => join(base, path))
24
+ return filesA
25
+ }
26
+
27
+ const fileFilter = function ({ basename }) {
28
+ return !isManifest(basename)
29
+ }
package/lib/main.js ADDED
@@ -0,0 +1,103 @@
1
+ import del from 'del'
2
+
3
+ import { getCacheDir } from './dir.js'
4
+ import { moveCacheFile, hasFiles } from './fs.js'
5
+ import { list } from './list.js'
6
+ import { getManifestInfo, writeManifest, removeManifest, isExpired } from './manifest.js'
7
+ import { parsePath } from './path.js'
8
+
9
+ export { getCacheDir } from './dir.js'
10
+ export { list } from './list.js'
11
+
12
+ // Cache a file
13
+ const saveOne = async function (
14
+ path,
15
+ { move = DEFAULT_MOVE, ttl = DEFAULT_TTL, digests = [], cacheDir, cwd: cwdOpt } = {},
16
+ ) {
17
+ const { srcPath, cachePath } = await parsePath({ path, cacheDir, cwdOpt })
18
+
19
+ if (!(await hasFiles(srcPath))) {
20
+ return false
21
+ }
22
+
23
+ const { manifestInfo, identical } = await getManifestInfo({ cachePath, move, ttl, digests })
24
+ if (identical) {
25
+ return true
26
+ }
27
+
28
+ await del(cachePath, { force: true })
29
+ await moveCacheFile(srcPath, cachePath, move)
30
+ await writeManifest(manifestInfo)
31
+
32
+ return true
33
+ }
34
+
35
+ // Restore a cached file
36
+ const restoreOne = async function (path, { move = DEFAULT_MOVE, cacheDir, cwd: cwdOpt } = {}) {
37
+ const { srcPath, cachePath } = await parsePath({ path, cacheDir, cwdOpt })
38
+
39
+ if (!(await hasFiles(cachePath))) {
40
+ return false
41
+ }
42
+
43
+ if (await isExpired(cachePath)) {
44
+ return false
45
+ }
46
+
47
+ await del(srcPath, { force: true })
48
+ await moveCacheFile(cachePath, srcPath, move)
49
+
50
+ return true
51
+ }
52
+
53
+ // Remove the cache of a file
54
+ const removeOne = async function (path, { cacheDir, cwd: cwdOpt } = {}) {
55
+ const { cachePath } = await parsePath({ path, cacheDir, cwdOpt })
56
+
57
+ if (!(await hasFiles(cachePath))) {
58
+ return false
59
+ }
60
+
61
+ await del(cachePath, { force: true })
62
+ await removeManifest(cachePath)
63
+
64
+ return true
65
+ }
66
+
67
+ // Check if a file is cached
68
+ const hasOne = async function (path, { cacheDir, cwd: cwdOpt } = {}) {
69
+ const { cachePath } = await parsePath({ path, cacheDir, cwdOpt })
70
+
71
+ return (await hasFiles(cachePath)) && !(await isExpired(cachePath))
72
+ }
73
+
74
+ const DEFAULT_MOVE = false
75
+ const DEFAULT_TTL = undefined
76
+
77
+ // Allow each of the main functions to take either a single path or an array of
78
+ // paths as arguments
79
+ const allowMany = async function (func, paths, ...args) {
80
+ if (!Array.isArray(paths)) {
81
+ return func(paths, ...args)
82
+ }
83
+
84
+ const results = await Promise.all(paths.map((path) => func(path, ...args)))
85
+ return results.some(Boolean)
86
+ }
87
+
88
+ export const save = allowMany.bind(null, saveOne)
89
+ export const restore = allowMany.bind(null, restoreOne)
90
+ export const remove = allowMany.bind(null, removeOne)
91
+ export const has = allowMany.bind(null, hasOne)
92
+
93
+ // Change `opts` default values
94
+ export const bindOpts = function (opts) {
95
+ return {
96
+ save: (paths, optsA) => save(paths, { ...opts, ...optsA }),
97
+ restore: (paths, optsA) => restore(paths, { ...opts, ...optsA }),
98
+ remove: (paths, optsA) => remove(paths, { ...opts, ...optsA }),
99
+ has: (paths, optsA) => has(paths, { ...opts, ...optsA }),
100
+ list: (optsA) => list({ ...opts, ...optsA }),
101
+ getCacheDir: (optsA) => getCacheDir({ ...opts, ...optsA }),
102
+ }
103
+ }
@@ -0,0 +1,71 @@
1
+ import { promises as fs } from 'fs'
2
+ import { dirname } from 'path'
3
+
4
+ import del from 'del'
5
+ import { pathExists } from 'path-exists'
6
+
7
+ import { getExpires, checkExpires } from './expire.js'
8
+ import { getHash } from './hash.js'
9
+
10
+ // Retrieve cache manifest of a file to cache, which contains the file/directory
11
+ // contents hash and the `expires` date.
12
+ export const getManifestInfo = async function ({ cachePath, move, ttl, digests }) {
13
+ const manifestPath = getManifestPath(cachePath)
14
+ const expires = getExpires(ttl)
15
+ const hash = await getHash(digests, move)
16
+ const manifest = { expires, hash }
17
+ const manifestString = `${JSON.stringify(manifest, null, 2)}\n`
18
+ const identical = await isIdentical({ hash, manifestPath, manifestString })
19
+ return { manifestInfo: { manifestPath, manifestString }, identical }
20
+ }
21
+
22
+ // Whether the cache manifest has changed
23
+ const isIdentical = async function ({ hash, manifestPath, manifestString }) {
24
+ if (hash === undefined || !(await pathExists(manifestPath))) {
25
+ return false
26
+ }
27
+
28
+ const oldManifestString = await fs.readFile(manifestPath, 'utf8')
29
+ return oldManifestString === manifestString
30
+ }
31
+
32
+ // Persist the cache manifest to filesystem
33
+ export const writeManifest = async function ({ manifestPath, manifestString }) {
34
+ await fs.mkdir(dirname(manifestPath), { recursive: true })
35
+ await fs.writeFile(manifestPath, manifestString)
36
+ }
37
+
38
+ // Remove the cache manifest from filesystem
39
+ export const removeManifest = async function (cachePath) {
40
+ const manifestPath = getManifestPath(cachePath)
41
+ await del(manifestPath, { force: true })
42
+ }
43
+
44
+ // Retrieve the cache manifest filepath
45
+ const getManifestPath = function (cachePath) {
46
+ return `${cachePath}${CACHE_EXTENSION}`
47
+ }
48
+
49
+ export const isManifest = function (filePath) {
50
+ return filePath.endsWith(CACHE_EXTENSION)
51
+ }
52
+
53
+ const CACHE_EXTENSION = '.netlify.cache.json'
54
+
55
+ // Check whether a file/directory is expired by checking its cache manifest
56
+ export const isExpired = async function (cachePath) {
57
+ const manifestPath = getManifestPath(cachePath)
58
+ if (!(await pathExists(manifestPath))) {
59
+ return false
60
+ }
61
+
62
+ const { expires } = await readManifest(cachePath)
63
+ return checkExpires(expires)
64
+ }
65
+
66
+ const readManifest = async function (cachePath) {
67
+ const manifestPath = getManifestPath(cachePath)
68
+ const manifestString = await fs.readFile(manifestPath)
69
+ const manifest = JSON.parse(manifestString)
70
+ return manifest
71
+ }
package/lib/path.js ADDED
@@ -0,0 +1,107 @@
1
+ import { homedir } from 'os'
2
+ import { resolve, isAbsolute, join, sep } from 'path'
3
+
4
+ import { getCacheDir } from './dir.js'
5
+ import { safeGetCwd } from './utils/cwd.js'
6
+
7
+ // Find the paths of the file before/after caching
8
+ export const parsePath = async function ({ path, cacheDir, cwdOpt }) {
9
+ const srcPath = await getSrcPath(path, cwdOpt)
10
+ const cachePath = await getCachePath({ srcPath, cacheDir, cwdOpt })
11
+ return { srcPath, cachePath }
12
+ }
13
+
14
+ // Retrieve absolute path to the local file to cache/restore
15
+ const getSrcPath = async function (path, cwdOpt) {
16
+ const cwd = await safeGetCwd(cwdOpt)
17
+ const srcPath = resolvePath(path, cwd)
18
+ checkSrcPath(srcPath, cwd)
19
+ return srcPath
20
+ }
21
+
22
+ const resolvePath = function (path, cwd) {
23
+ if (isAbsolute(path)) {
24
+ return resolve(path)
25
+ }
26
+
27
+ if (cwd !== '') {
28
+ return resolve(cwd, path)
29
+ }
30
+
31
+ throw new Error(`Current directory does not exist: ${cwd}`)
32
+ }
33
+
34
+ // Caching the whole repository creates many issues:
35
+ // - It caches many directories that are not related to Gatsby but take lots of
36
+ // space, such as node_modules
37
+ // - It caches directories that are not meant to restored across builds. For
38
+ // example .git (beside being big).
39
+ // - It undoes any build operations inside the repository that might have
40
+ // happened before this plugin starts restoring the cache, leading to
41
+ // conflicts with other plugins, Netlify Build or the buildbot.
42
+ const checkSrcPath = function (srcPath, cwd) {
43
+ if (cwd !== '' && isParentPath(srcPath, cwd)) {
44
+ throw new Error(`Cannot cache ${srcPath} because it is the current directory (${cwd}) or a parent directory`)
45
+ }
46
+ }
47
+
48
+ // Note: srcPath and cwd are already normalized and absolute
49
+ const isParentPath = function (srcPath, cwd) {
50
+ return `${cwd}${sep}`.startsWith(`${srcPath}${sep}`)
51
+ }
52
+
53
+ const getCachePath = async function ({ srcPath, cacheDir, cwdOpt }) {
54
+ const cacheDirA = getCacheDir({ cacheDir, cwd: cwdOpt })
55
+ const { name, relPath } = await findBase(srcPath, cwdOpt)
56
+ const cachePath = join(cacheDirA, name, relPath)
57
+ return cachePath
58
+ }
59
+
60
+ // The cached path is the path relative to the base which can be either the
61
+ // current directory, the home directory or the root directory. Each is tried
62
+ // in order.
63
+ const findBase = async function (srcPath, cwdOpt) {
64
+ const bases = await getBases(cwdOpt)
65
+ const srcPathA = normalizeWindows(srcPath)
66
+ return bases.map(({ name, base }) => parseBase(name, base, srcPathA)).find(Boolean)
67
+ }
68
+
69
+ // Windows drives are problematic:
70
+ // - they cannot be used in `relPath` since directories cannot be called `C:`
71
+ // for example.
72
+ // - they make comparing between drives harder
73
+ // For the moment, we simply strip them. This does mean two files with the same
74
+ // paths but inside different drives would be cached together, which is not
75
+ // correct.
76
+ // This also means `cache.list()` always assumes the source files are on the
77
+ // current drive.
78
+ // TODO: fix it.
79
+ const normalizeWindows = function (srcPath) {
80
+ return srcPath.replace(WINDOWS_DRIVE_REGEX, '\\')
81
+ }
82
+
83
+ const WINDOWS_DRIVE_REGEX = /^[a-zA-Z]:\\/
84
+
85
+ // This logic works when `base` and `path` are on different Windows drives
86
+ const parseBase = function (name, base, srcPath) {
87
+ if (srcPath === base || !srcPath.startsWith(base)) {
88
+ return
89
+ }
90
+
91
+ const relPath = srcPath.replace(base, '')
92
+ return { name, relPath }
93
+ }
94
+
95
+ export const getBases = async function (cwdOpt) {
96
+ const cwdBase = await getCwdBase(cwdOpt)
97
+ return [...cwdBase, { name: 'home', base: homedir() }, { name: 'root', base: sep }]
98
+ }
99
+
100
+ const getCwdBase = async function (cwdOpt) {
101
+ const cwd = await safeGetCwd(cwdOpt)
102
+ if (cwd === '') {
103
+ return []
104
+ }
105
+
106
+ return [{ name: 'cwd', base: cwd }]
107
+ }
@@ -0,0 +1,23 @@
1
+ import { normalize } from 'path'
2
+ import process from 'process'
3
+
4
+ import { pathExists } from 'path-exists'
5
+
6
+ // Like `process.cwd()` but safer when current directory is wrong
7
+ export const safeGetCwd = async function (cwdOpt) {
8
+ try {
9
+ const cwd = getCwdValue(cwdOpt)
10
+
11
+ if (!(await pathExists(cwd))) {
12
+ return ''
13
+ }
14
+
15
+ return cwd
16
+ } catch {
17
+ return ''
18
+ }
19
+ }
20
+
21
+ const getCwdValue = function (cwdOpt = process.cwd()) {
22
+ return normalize(cwdOpt)
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/cache-utils",
3
- "version": "4.1.5-rc",
3
+ "version": "4.1.6-rc",
4
4
  "description": "Utility for caching files in Netlify Build",
5
5
  "type": "module",
6
6
  "exports": "./lib/main.js",