-
Notifications
You must be signed in to change notification settings - Fork 160
/
NodeUpdater.java
493 lines (428 loc) · 18.3 KB
/
NodeUpdater.java
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
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
/*
* Copyright 2000-2020 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.flow.server.frontend;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vaadin.flow.server.Constants;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;
import com.vaadin.flow.server.frontend.scanner.FrontendDependencies;
import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
import elemental.json.Json;
import elemental.json.JsonException;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
import static com.vaadin.flow.server.Constants.COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT;
import static com.vaadin.flow.server.Constants.PACKAGE_JSON;
import static com.vaadin.flow.server.Constants.RESOURCES_FRONTEND_DEFAULT;
import static com.vaadin.flow.server.frontend.FrontendUtils.FLOW_NPM_PACKAGE_NAME;
import static com.vaadin.flow.server.frontend.FrontendUtils.NODE_MODULES;
import static elemental.json.impl.JsonUtil.stringify;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Base abstract class for frontend updaters that needs to be run when in
* dev-mode or from the flow maven plugin.
* <p>
* For internal use only. May be renamed or removed in a future release.
*
* @since 2.0
*/
public abstract class NodeUpdater implements FallibleCommand {
/**
* Relative paths of generated should be prefixed with this value, so they
* can be correctly separated from {projectDir}/frontend files.
*/
static final String GENERATED_PREFIX = "GENERATED/";
static final String DEPENDENCIES = "dependencies";
static final String VAADIN_DEP_KEY = "vaadin";
static final String HASH_KEY = "hash";
static final String DEV_DEPENDENCIES = "devDependencies";
private static final String DEP_LICENSE_KEY = "license";
private static final String DEP_LICENSE_DEFAULT = "UNLICENSED";
private static final String DEP_NAME_KEY = "name";
private static final String DEP_NAME_DEFAULT = "no-name";
private static final String DEP_MAIN_KEY = "main";
protected static final String DEP_NAME_FLOW_DEPS = "@vaadin/flow-deps";
protected static final String DEP_NAME_FLOW_JARS = "@vaadin/flow-frontend";
protected static final String DEP_NAME_FORM_JARS = "@vaadin/form";
private static final String FORM_FOLDER = "form";
private static final String DEP_MAIN_VALUE = "index";
private static final String DEP_VERSION_KEY = "version";
private static final String DEP_VERSION_DEFAULT = "1.0.0";
private static final String ROUTER_VERSION = "1.7.2";
protected static final String POLYMER_VERSION = "3.2.0";
/**
* Base directory for {@link Constants#PACKAGE_JSON},
* {@link FrontendUtils#WEBPACK_CONFIG}, {@link FrontendUtils#NODE_MODULES}.
*/
protected final File npmFolder;
/**
* The path to the {@link FrontendUtils#NODE_MODULES} directory.
*/
protected final File nodeModulesFolder;
/**
* Base directory for flow generated files.
*/
protected final File generatedFolder;
/**
* Base directory for flow dependencies coming from jars.
*/
protected final File flowResourcesFolder;
/**
* Base directory for form dependencies coming from jars.
*/
protected final File formResourcesFolder;
/**
* The {@link FrontendDependencies} object representing the application
* dependencies.
*/
protected final FrontendDependenciesScanner frontDeps;
protected String buildDir;
final ClassFinder finder;
boolean modified;
/**
* Constructor.
*
* @param finder
* a reusable class finder
* @param frontendDependencies
* a reusable frontend dependencies
* @param npmFolder
* folder with the `package.json` file
* @param generatedPath
* folder where flow generated files will be placed.
* @param flowResourcesPath
* folder where flow dependencies will be copied to.
* @param buildDir
* the used build directory
*/
protected NodeUpdater(ClassFinder finder,
FrontendDependenciesScanner frontendDependencies, File npmFolder,
File generatedPath, File flowResourcesPath, String buildDir) {
this.frontDeps = frontendDependencies;
this.finder = finder;
this.npmFolder = npmFolder;
this.nodeModulesFolder = new File(npmFolder, NODE_MODULES);
this.generatedFolder = generatedPath;
this.flowResourcesFolder = flowResourcesPath;
this.formResourcesFolder = new File(flowResourcesPath, FORM_FOLDER);
this.buildDir = buildDir;
}
private File getPackageJsonFile() {
return new File(npmFolder, PACKAGE_JSON);
}
static Set<String> getGeneratedModules(File directory,
Set<String> excludes) {
if (!directory.exists()) {
return Collections.emptySet();
}
final Function<String, String> unixPath = str -> str.replace("\\", "/");
final URI baseDir = directory.toURI();
return FileUtils.listFiles(directory, new String[] { "js" }, true)
.stream().filter(file -> {
String path = unixPath.apply(file.getPath());
if (path.contains("/node_modules/")) {
return false;
}
return excludes.stream().noneMatch(
postfix -> path.endsWith(unixPath.apply(postfix)));
})
.map(file -> GENERATED_PREFIX + unixPath
.apply(baseDir.relativize(file.toURI()).getPath()))
.collect(Collectors.toSet());
}
String resolveResource(String importPath) {
String resolved = importPath;
if (!importPath.startsWith("@")) {
// We only should check here those paths starting with './' when all
// flow components
// have the './' prefix
String resource = resolved.replaceFirst("^\\./+", "");
if (hasMetaInfResource(resource)) {
if (!resolved.startsWith("./")) {
log().warn(
"Use the './' prefix for files in JAR files: '{}', please update your component.",
importPath);
}
resolved = FLOW_NPM_PACKAGE_NAME + resource;
}
}
return resolved;
}
private boolean hasMetaInfResource(String resource) {
return finder.getResource(
RESOURCES_FRONTEND_DEFAULT + "/" + resource) != null
|| finder.getResource(COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT
+ "/" + resource) != null;
}
JsonObject getPackageJson() throws IOException {
JsonObject packageJson = getJsonFileContent(getPackageJsonFile());
if (packageJson == null) {
packageJson = Json.createObject();
packageJson.put(DEP_NAME_KEY, DEP_NAME_DEFAULT);
packageJson.put(DEP_LICENSE_KEY, DEP_LICENSE_DEFAULT);
}
addVaadinDefaultsToJson(packageJson);
addWebpackPlugins(packageJson);
return packageJson;
}
private void addWebpackPlugins(JsonObject packageJson) {
final List<String> plugins = WebpackPluginsUtil.getPlugins();
Path targetFolder = Paths.get(npmFolder.toString(), buildDir,
WebpackPluginsUtil.PLUGIN_TARGET);
JsonObject devDependencies;
if (packageJson.hasKey(DEV_DEPENDENCIES)) {
devDependencies = packageJson.getObject(DEV_DEPENDENCIES);
} else {
devDependencies = Json.createObject();
packageJson.put(DEV_DEPENDENCIES, devDependencies);
}
plugins.stream().filter(plugin -> targetFolder.toFile().exists())
.forEach(plugin -> {
String pluginTarget = "./" + (npmFolder.toPath()
.relativize(targetFolder).toString() + "/" + plugin)
.replace('\\', '/');
devDependencies.put("@vaadin/" + plugin, pluginTarget);
});
}
JsonObject getResourcesPackageJson() throws IOException {
JsonObject packageJson = getJsonFileContent(
new File(flowResourcesFolder, PACKAGE_JSON));
if (packageJson == null) {
packageJson = Json.createObject();
packageJson.put(DEP_NAME_KEY, DEP_NAME_FLOW_JARS);
packageJson.put(DEP_LICENSE_KEY, DEP_LICENSE_DEFAULT);
packageJson.put(DEP_MAIN_KEY, DEP_MAIN_VALUE);
packageJson.put(DEP_VERSION_KEY, DEP_VERSION_DEFAULT);
}
return packageJson;
}
JsonObject getFormResourcesPackageJson() throws IOException {
JsonObject packageJson = getJsonFileContent(
new File(formResourcesFolder, PACKAGE_JSON));
if (packageJson == null) {
packageJson = Json.createObject();
packageJson.put(DEP_NAME_KEY, DEP_NAME_FORM_JARS);
packageJson.put(DEP_LICENSE_KEY, DEP_LICENSE_DEFAULT);
packageJson.put(DEP_MAIN_KEY, DEP_MAIN_VALUE);
packageJson.put(DEP_VERSION_KEY, DEP_VERSION_DEFAULT);
packageJson.put("sideEffects", false);
}
return packageJson;
}
static JsonObject getJsonFileContent(File packageFile) throws IOException {
JsonObject jsonContent = null;
if (packageFile.exists()) {
String fileContent = FileUtils.readFileToString(packageFile,
UTF_8.name());
try {
jsonContent = Json.parse(fileContent);
} catch (JsonException e) {
throw new JsonException(String
.format("Cannot parse package file '%s'", packageFile));
}
}
return jsonContent;
}
static void addVaadinDefaultsToJson(JsonObject json) {
JsonObject vaadinPackages = computeIfAbsent(json, VAADIN_DEP_KEY,
Json::createObject);
computeIfAbsent(vaadinPackages, DEPENDENCIES, () -> {
final JsonObject dependencies = Json.createObject();
getDefaultDependencies().forEach(dependencies::put);
return dependencies;
});
computeIfAbsent(vaadinPackages, DEV_DEPENDENCIES, () -> {
final JsonObject devDependencies = Json.createObject();
getDefaultDevDependencies().forEach(devDependencies::put);
return devDependencies;
});
computeIfAbsent(vaadinPackages, HASH_KEY, () -> Json.create(""));
}
private static <T extends JsonValue> T computeIfAbsent(
JsonObject jsonObject, String key, Supplier<T> valueSupplier) {
T result = jsonObject.get(key);
if (result == null) {
result = valueSupplier.get();
jsonObject.put(key, result);
}
return result;
}
static Map<String, String> getDefaultDependencies() {
Map<String, String> defaults = new HashMap<>();
defaults.put("@vaadin/router", ROUTER_VERSION);
defaults.put("@polymer/polymer", POLYMER_VERSION);
defaults.put("lit-element", "2.5.1");
defaults.put("lit-html", "1.4.1");
return defaults;
}
static Map<String, String> getDefaultDevDependencies() {
Map<String, String> defaults = new HashMap<>();
defaults.put("html-webpack-plugin", "4.5.1");
defaults.put("typescript", "4.2.3");
defaults.put("ts-loader", "8.0.12");
defaults.put("fork-ts-checker-webpack-plugin", "6.2.1");
defaults.put("webpack", "4.46.0");
defaults.put("webpack-cli", "3.3.11");
defaults.put("webpack-dev-server", "3.11.0");
defaults.put("compression-webpack-plugin", "4.0.1");
defaults.put("extra-watch-webpack-plugin", "1.0.3");
defaults.put("webpack-merge", "4.2.2");
defaults.put("css-loader", "4.2.1");
defaults.put("extract-loader", "5.1.0");
defaults.put("lit-css-loader", "0.0.4");
defaults.put("file-loader", "6.2.0");
defaults.put("loader-utils", "2.0.0");
final String WORKBOX_VERSION = "6.1.0";
defaults.put("workbox-webpack-plugin", WORKBOX_VERSION);
defaults.put("workbox-core", WORKBOX_VERSION);
defaults.put("workbox-precaching", WORKBOX_VERSION);
defaults.put("glob", "7.1.6");
defaults.put("webpack-manifest-plugin", "3.0.0");
defaults.put("@types/validator", "13.1.0");
defaults.put("validator", "13.1.17");
// Constructable style sheets is only implemented for chrome,
// polyfill needed for FireFox et.al. at the moment
defaults.put("construct-style-sheets-polyfill", "2.4.16");
// Forcing chokidar version for now until new babel version is available
// check out https://github.com/babel/babel/issues/11488
defaults.put("chokidar", "^3.5.0");
return defaults;
}
/**
* Updates default dependencies and development dependencies to
* package.json.
*
* @param packageJson
* package.json json object to update with dependencies
* @return true if items were added or removed from the {@code packageJson}
*/
boolean updateDefaultDependencies(JsonObject packageJson) {
int added = 0;
for (Map.Entry<String, String> entry : getDefaultDependencies()
.entrySet()) {
added += addDependency(packageJson, DEPENDENCIES, entry.getKey(),
entry.getValue());
}
for (Map.Entry<String, String> entry : getDefaultDevDependencies()
.entrySet()) {
added += addDependency(packageJson, DEV_DEPENDENCIES,
entry.getKey(), entry.getValue());
}
if (added > 0) {
log().info("Added {} default dependencies to main package.json",
added);
}
return added > 0;
}
int addDependency(JsonObject json, String key, String pkg, String version) {
Objects.requireNonNull(json, "Json object need to be given");
Objects.requireNonNull(key, "Json sub object needs to be give.");
Objects.requireNonNull(pkg, "dependency package needs to be defined");
JsonObject vaadinDeps = json.getObject(VAADIN_DEP_KEY);
if (!json.hasKey(key)) {
json.put(key, Json.createObject());
}
json = json.get(key);
vaadinDeps = vaadinDeps.getObject(key);
if (vaadinDeps.hasKey(pkg)) {
if (version == null) {
version = vaadinDeps.getString(pkg);
}
return handleExistingVaadinDep(json, pkg, version, vaadinDeps);
} else {
vaadinDeps.put(pkg, version);
if (!json.hasKey(pkg) || new FrontendVersion(version)
.isNewerThan(toVersion(json, pkg))) {
json.put(pkg, version);
log().debug("Added \"{}\": \"{}\" line.", pkg, version);
return 1;
}
}
return 0;
}
private int handleExistingVaadinDep(JsonObject json, String pkg,
String version, JsonObject vaadinDeps) {
boolean added = false;
if (json.hasKey(pkg)) {
FrontendVersion packageVersion = toVersion(json, pkg);
FrontendVersion newVersion = new FrontendVersion(version);
FrontendVersion vaadinVersion = toVersion(vaadinDeps, pkg);
// Vaadin and package.json versions are the same, but dependency
// updates (can be up or down)
if (vaadinVersion.isEqualTo(packageVersion)
&& !vaadinVersion.isEqualTo(newVersion)) {
json.put(pkg, version);
added = true;
// if vaadin and package not the same, but new version is newer
// update package version.
} else if (newVersion.isNewerThan(packageVersion)) {
json.put(pkg, version);
added = true;
}
} else {
json.put(pkg, version);
added = true;
}
// always update vaadin version to the latest set version
vaadinDeps.put(pkg, version);
if (added) {
log().debug("Added \"{}\": \"{}\" line.", pkg, version);
}
return added ? 1 : 0;
}
private static FrontendVersion toVersion(JsonObject json, String key) {
return new FrontendVersion(json.getString(key));
}
String writePackageFile(JsonObject packageJson) throws IOException {
return writePackageFile(packageJson, new File(npmFolder, PACKAGE_JSON));
}
String writeResourcesPackageFile(JsonObject packageJson)
throws IOException {
return writePackageFile(packageJson,
new File(flowResourcesFolder, PACKAGE_JSON));
}
String writeFormResourcesPackageFile(JsonObject packageJson)
throws IOException {
return writePackageFile(packageJson,
new File(formResourcesFolder, PACKAGE_JSON));
}
String writePackageFile(JsonObject json, File packageFile)
throws IOException {
log().debug("writing file {}.", packageFile.getAbsolutePath());
FileUtils.forceMkdirParent(packageFile);
String content = stringify(json, 2) + "\n";
FileUtils.writeStringToFile(packageFile, content, UTF_8.name());
return content;
}
Logger log() {
return LoggerFactory.getLogger(this.getClass());
}
}