|
1 | 1 | #!/usr/bin/env node
|
2 | 2 |
|
3 |
| -'use strict' |
4 |
| - |
5 | 3 | /*!
|
6 | 4 | * Script to update version number references in the project.
|
7 |
| - * Copyright 2017 The Bootstrap Authors |
8 |
| - * Copyright 2017 Twitter, Inc. |
9 |
| - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
| 5 | + * Copyright 2017-2021 The Bootstrap Authors |
| 6 | + * Copyright 2017-2021 Twitter, Inc. |
| 7 | + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
10 | 8 | */
|
11 | 9 |
|
12 |
| -/* global Set */ |
| 10 | +'use strict' |
13 | 11 |
|
14 |
| -const fs = require('fs') |
| 12 | +const fs = require('fs').promises |
15 | 13 | const path = require('path')
|
16 |
| -const sh = require('shelljs') |
17 |
| -sh.config.fatal = true |
18 |
| -const sed = sh.sed |
| 14 | +const globby = require('globby') |
| 15 | + |
| 16 | +const VERBOSE = process.argv.includes('--verbose') |
| 17 | +const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-run') |
| 18 | + |
| 19 | +// These are the filetypes we only care about replacing the version |
| 20 | +const GLOB = [ |
| 21 | + '**/*.{css,html,js,json,md,scss,txt,yml}' |
| 22 | +] |
| 23 | +const GLOBBY_OPTIONS = { |
| 24 | + cwd: path.join(__dirname, '..'), |
| 25 | + gitignore: true |
| 26 | +} |
| 27 | +const EXCLUDED_FILES = [ |
| 28 | + 'CHANGELOG.md' |
| 29 | +] |
19 | 30 |
|
20 | 31 | // Blame TC39... https://github.com/benjamingr/RegExp.escape/issues/37
|
21 |
| -RegExp.quote = (string) => string.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&') |
22 |
| -RegExp.quoteReplacement = (string) => string.replace(/[$]/g, '$$') |
| 32 | +function regExpQuote(string) { |
| 33 | + return string.replace(/[$()*+-.?[\\\]^{|}]/g, '\\$&') |
| 34 | +} |
| 35 | + |
| 36 | +function regExpQuoteReplacement(string) { |
| 37 | + return string.replace(/\$/g, '$$') |
| 38 | +} |
23 | 39 |
|
24 |
| -const DRY_RUN = false |
| 40 | +async function replaceRecursively(file, oldVersion, newVersion) { |
| 41 | + const originalString = await fs.readFile(file, 'utf8') |
| 42 | + const newString = originalString.replace( |
| 43 | + new RegExp(regExpQuote(oldVersion), 'g'), regExpQuoteReplacement(newVersion) |
| 44 | + ) |
25 | 45 |
|
26 |
| -function walkAsync(directory, excludedDirectories, fileCallback, errback) { |
27 |
| - if (excludedDirectories.has(path.parse(directory).base)) { |
| 46 | + // No need to move any further if the strings are identical |
| 47 | + if (originalString === newString) { |
28 | 48 | return
|
29 | 49 | }
|
30 |
| - fs.readdir(directory, (err, names) => { |
31 |
| - if (err) { |
32 |
| - errback(err) |
33 |
| - return |
34 |
| - } |
35 |
| - names.forEach((name) => { |
36 |
| - const filepath = path.join(directory, name) |
37 |
| - fs.lstat(filepath, (err, stats) => { |
38 |
| - if (err) { |
39 |
| - process.nextTick(errback, err) |
40 |
| - return |
41 |
| - } |
42 |
| - if (stats.isSymbolicLink()) { |
43 |
| - return |
44 |
| - } |
45 |
| - else if (stats.isDirectory()) { |
46 |
| - process.nextTick(walkAsync, filepath, excludedDirectories, fileCallback, errback) |
47 |
| - } |
48 |
| - else if (stats.isFile()) { |
49 |
| - process.nextTick(fileCallback, filepath) |
50 |
| - } |
51 |
| - }) |
52 |
| - }) |
53 |
| - }) |
54 |
| -} |
55 | 50 |
|
56 |
| -function replaceRecursively(directory, excludedDirectories, allowedExtensions, original, replacement) { |
57 |
| - original = new RegExp(RegExp.quote(original), 'g') |
58 |
| - replacement = RegExp.quoteReplacement(replacement) |
59 |
| - const updateFile = !DRY_RUN ? (filepath) => { |
60 |
| - if (allowedExtensions.has(path.parse(filepath).ext)) { |
61 |
| - sed('-i', original, replacement, filepath) |
62 |
| - } |
63 |
| - } : (filepath) => { |
64 |
| - if (allowedExtensions.has(path.parse(filepath).ext)) { |
65 |
| - console.log(`FILE: ${filepath}`) |
66 |
| - } |
67 |
| - else { |
68 |
| - console.log(`EXCLUDED:${filepath}`) |
69 |
| - } |
| 51 | + if (VERBOSE) { |
| 52 | + console.log(`FILE: ${file}`) |
70 | 53 | }
|
71 |
| - walkAsync(directory, excludedDirectories, updateFile, (err) => { |
72 |
| - console.error('ERROR while traversing directory!:') |
73 |
| - console.error(err) |
74 |
| - process.exit(1) |
75 |
| - }) |
| 54 | + |
| 55 | + if (DRY_RUN) { |
| 56 | + return |
| 57 | + } |
| 58 | + |
| 59 | + await fs.writeFile(file, newString, 'utf8') |
76 | 60 | }
|
77 | 61 |
|
78 |
| -function main(args) { |
79 |
| - if (args.length !== 2) { |
80 |
| - console.error('USAGE: change-version old_version new_version') |
| 62 | +async function main(args) { |
| 63 | + const [oldVersion, newVersion] = args |
| 64 | + |
| 65 | + if (!oldVersion || !newVersion) { |
| 66 | + console.error('USAGE: change-version old_version new_version [--verbose] [--dry[-run]]') |
81 | 67 | console.error('Got arguments:', args)
|
82 | 68 | process.exit(1)
|
83 | 69 | }
|
84 |
| - const oldVersion = args[0] |
85 |
| - const newVersion = args[1] |
86 |
| - const EXCLUDED_DIRS = new Set([ |
87 |
| - '.git', |
88 |
| - 'node_modules', |
89 |
| - 'vendor' |
90 |
| - ]) |
91 |
| - const INCLUDED_EXTENSIONS = new Set([ |
92 |
| - // This extension whitelist is how we avoid modifying binary files |
93 |
| - '', |
94 |
| - '.css', |
95 |
| - '.html', |
96 |
| - '.js', |
97 |
| - '.json', |
98 |
| - '.md', |
99 |
| - '.scss', |
100 |
| - '.txt', |
101 |
| - '.yml' |
102 |
| - ]) |
103 |
| - replaceRecursively('.', EXCLUDED_DIRS, INCLUDED_EXTENSIONS, oldVersion, newVersion) |
| 70 | + |
| 71 | + // Strip any leading `v` from arguments because otherwise we will end up with duplicate `v`s |
| 72 | + [oldVersion, newVersion].map(arg => arg.startsWith('v') ? arg.slice(1) : arg) |
| 73 | + |
| 74 | + try { |
| 75 | + const files = await globby(GLOB, GLOBBY_OPTIONS, EXCLUDED_FILES) |
| 76 | + |
| 77 | + await Promise.all(files.map(file => replaceRecursively(file, oldVersion, newVersion))) |
| 78 | + } catch (error) { |
| 79 | + console.error(error) |
| 80 | + process.exit(1) |
| 81 | + } |
104 | 82 | }
|
105 | 83 |
|
106 | 84 | main(process.argv.slice(2))
|
0 commit comments