|
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