/
fix-imports.js
252 lines (233 loc) · 8.33 KB
/
fix-imports.js
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
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview A script that automatically adds imports of things that have
* been moved out of the Blockly namespace to developers' local files.
*/
'use strict';
import {createSubCommand, extractRequiredInfo} from './command.js';
import {readFileSync, statSync, writeFileSync} from 'fs';
export const fixImports = createSubCommand(
'fix-imports',
'>=9',
'Add imports for modules that have been moved out of the Blockly namespace',
)
.option(
'-i, --in-place [suffix]',
'fix imports in-place, optionally create backup files with the ' +
'given suffix. Otherwise output to stdout',
)
.action(function () {
// TODO (#1211): In the future we should use the fromVersion and toVersion
// so that we can support doing this across multiple versions. But for
// now we just need it to work for v9.
const {fileNames} = extractRequiredInfo(this);
fileNames.forEach((name) => {
if (statSync(name).isDirectory()) return;
const contents = readFileSync(name, 'utf8');
const migratedContents = migrateContents(contents);
const inPlace = this.opts().inPlace;
if (inPlace) {
if (typeof inPlace == 'string') {
writeFileSync(name + inPlace, contents);
}
writeFileSync(name, migratedContents);
process.stderr.write(`Migrated renamings in ${name}\n`);
} else {
process.stdout.write(migratedContents + '\n');
}
});
});
/**
* @typedef {{
* import: string,
* oldIdentifier?: string,
* newIdentifier: string,
* newImport: string,
* newRequire: string,
* }}
*/
let MigrationData;
// TODO (#1211): Make this database format more robust.
/** @type {Array<MigrationData>} */
const database = [
{
import: 'blockly/dart',
newIdentifier: 'dartGenerator',
newImport: `import {dartGenerator} from 'blockly/dart';`,
newRequire: `const {dartGenerator} = require('blockly/dart');`,
},
{
import: 'blockly/javascript',
oldIdentifier: 'Blockly.JavaScript',
newIdentifier: 'javascriptGenerator',
newImport: `import {javascriptGenerator} from 'blockly/javascript';`,
newRequire: `const {javascriptGenerator} = require('blockly/javascript');`,
},
{
import: 'blockly/lua',
newIdentifier: 'luaGenerator',
newImport: `import {luaGenerator} from 'blockly/lua';`,
newRequire: `const {luaGenerator} = require('blockly/lua');`,
},
{
import: 'blockly/php',
newIdentifier: 'phpGenerator',
newImport: `import {phpGenerator} from 'blockly/php';`,
newRequire: `const {phpGenerator} = require('blockly/php');`,
},
{
import: 'blockly/python',
newIdentifier: 'pythonGenerator',
newImport: `import {pythonGenerator} from 'blockly/python';`,
newRequire: `const {pythonGenerator} = require('blockly/python');`,
},
{
import: 'blockly/blocks',
oldIdentifier: 'Blockly.libraryBlocks',
newIdentifier: 'libraryBlocks',
newImport: `import * as libraryBlocks from 'blockly/blocks';`,
newRequire: `const libraryBlocks = require('blockly/blocks');`,
},
];
/**
* Migrates the contents of a particular file, renaming references and
* adding/updating imports.
* @param {string} contents The string contents of the file to migrate.
* @returns {string} The migrated contents of the file.
*/
function migrateContents(contents) {
let newContents = contents;
for (const migrationData of database) {
newContents = fixImport(newContents, migrationData);
}
return newContents;
}
/**
* Migrates a particular import in a particular file. Renames references to
* where the import used to exist on the namespace tree, and adds/updates
* imports.
* @param {string} contents The string contents of the file to migrate.
* @param {MigrationData} migrationData Data defining what to migrate and how.
* @returns {string} The migrated contents of the file.
*/
function fixImport(contents, migrationData) {
const identifier = getIdentifier(contents, migrationData);
if (!identifier) return contents;
const newContents = replaceReferences(contents, migrationData, identifier);
if (newContents !== contents) return addImport(newContents, migrationData);
return contents;
}
/**
* Returns the identifier a given import is assigned to.
* @param {string} contents The string contents of the file to migrate.
* @param {MigrationData} migrationData Data defining what to migrate and how.
* @returns {string} The identifier associated with the import associated with the
* migration data.
*/
function getIdentifier(contents, migrationData) {
const importMatch = contents.match(
new RegExp(`\\s(\\S*)\\s+from\\s+['"]${migrationData.import}['"]`),
);
if (importMatch) return importMatch[1];
const requireMatch = contents.match(
new RegExp(`(\\S*)\\s+=\\s+require\\(['"]${migrationData.import}['"]\\)`),
);
if (requireMatch) return requireMatch[1];
return migrationData.oldIdentifier;
}
/**
* Replaces references to where an import used to exist on the namespace tree
* with references to the actual import (if any references are found).
* @param {string} contents The string contents of the file to migrate.
* @param {MigrationData} migrationData Data defining what to migrate and how.
* @param {string} identifier The string to be replaced in the file contents.
* @returns {string} The migrated contents of the file.
*/
function replaceReferences(contents, migrationData, identifier) {
return contents.replace(dottedIdentifier, (match) => {
if (match.startsWith(identifier)) {
return migrationData.newIdentifier + match.slice(identifier.length);
}
return match;
});
}
/**
* Replaces the any existing import with the new import, or if no import is
* found, inserts a new one after the 'blockly' import.
* @param {string} contents The string contents of the file to migrate.
* @param {MigrationData} migrationData Data defining what to migrate and how.
* @returns {string} The migrated contents of the file.
*/
function addImport(contents, migrationData) {
const importRegExp = createImportRegExp(migrationData.import);
const importMatch = contents.match(importRegExp);
if (importMatch) {
return contents.replace(
importRegExp,
importMatch[1] + migrationData.newImport,
);
}
const requireRegExp = createRequireRegExp(migrationData.import);
const requireMatch = contents.match(requireRegExp);
if (requireMatch) {
return contents.replace(
requireRegExp,
requireMatch[1] + migrationData.newRequire,
);
}
const blocklyImportMatch = contents.match(createImportRegExp('blockly'));
if (blocklyImportMatch) {
const match = blocklyImportMatch;
return (
contents.slice(0, match.index + match[0].length) +
'\n' +
migrationData.newImport +
contents.slice(match.index + match[0].length)
);
}
const blocklyRequireMatch = contents.match(createRequireRegExp('blockly'));
if (blocklyRequireMatch) {
const match = blocklyRequireMatch;
return (
contents.slice(0, match.index + match[0].length) +
'\n' +
migrationData.newRequire +
contents.slice(match.index + match[0].length)
);
}
// Should never happen, but return something so we can keep going if it does.
return contents;
}
/**
* Returns a regular expression that matches an import statement for the given
* import identifier.
* @param {string} importIdent The identifier of the import to match.
* @returns {RegExp} The regular expression.
*/
function createImportRegExp(importIdent) {
return new RegExp(`(\\s*)import\\s+.+\\s+from\\s+['"]${importIdent}['"];`);
}
/**
* Returns a regular expression that matches a require statement for the given
* identifier.
* @param {string} importIdent The identifer of the import to match.
* @returns {RegExp} The regular expression.
*/
function createRequireRegExp(importIdent) {
return new RegExp(
`(\\s*)(const|let|var)\\s+.*\\s+=\\s+require\\(['"]${importIdent}['"]\\);`,
);
}
/**
* RegExp matching a dotted identifier path like "foo.bar.baz". Note
* that this only matches such paths containing at least one dot, as
* we expect to be looking for string like "Blockly.<something>" and
* don't want to try to rename every single variable and every word
* that appears in each comment!
*/
const dottedIdentifier =
/[A-Za-z$_][A-Za-z0-9$_]*(\.[A-Za-z$_][A-Za-z0-9$_]*)+/g;