Open dsyer opened 2 years ago
Here's a prototype (https://github.com/dsyer/npm-resolver) that looks for webjars and falls back to unpkg.com if it can't find one:
@RestController
public class NpmVersionResolver {
private static final Log logger = LogFactory.getLog(NpmVersionResolver.class);
private static final Set<String> ALERTS = new HashSet<>();
private static final String PROPERTIES_ROOT = "META-INF/maven/";
private static final String RESOURCE_ROOT = "META-INF/resources/webjars/";
private static final String NPM = "org.webjars.npm/";
private static final String PLAIN = "org.webjars/";
private static final String POM_PROPERTIES = "/pom.properties";
private static final String PACKAGE_JSON = "/package.json";
@GetMapping("/npm/{webjar}")
public ResponseEntity<Void> module(@PathVariable String webjar) {
String path = findWebJarResourcePath(webjar, "/");
if (path == null) {
path = findUnpkgPath(webjar, "");
return ResponseEntity.status(HttpStatus.FOUND).location(URI.create(path)).build();
}
return ResponseEntity.status(HttpStatus.FOUND).location(URI.create("/webjars/" + path)).build();
}
@GetMapping("/npm/{webjar}/{*remainder}")
public ResponseEntity<Void> remainder(@PathVariable String webjar, @PathVariable String remainder) {
if (webjar.startsWith("@")) {
int index = remainder.indexOf("/",1);
String path = index < 0 ? remainder.substring(1) : remainder.substring(1, index);
webjar = webjar.substring(1) + "__" + path;
if (index < 0 || index == remainder.length() - 1) {
return module(webjar);
}
remainder = remainder.substring(index);
}
String path = findWebJarResourcePath(webjar, remainder);
if (path == null) {
if (version(webjar) == null) {
path = findUnpkgPath(webjar, remainder);
} else {
return ResponseEntity.notFound().build();
}
return ResponseEntity.status(HttpStatus.FOUND).location(URI.create(path)).build();
}
return ResponseEntity.status(HttpStatus.FOUND).location(URI.create("/webjars/" + path)).build();
}
private String findUnpkgPath(String webjar, String remainder) {
if (!StringUtils.hasText(remainder)) {
remainder = "";
} else if (!remainder.startsWith("/")) {
remainder = "/" + remainder;
}
if (webjar.contains("__")) {
webjar = "@" + webjar.replace("__", "/");
}
if (logger.isInfoEnabled() && !ALERTS.contains(webjar)) {
ALERTS.add(webjar);
logger.info("Resolving webjar to unpkg.com: " + webjar);
}
return "https://unpkg.com/" + webjar + remainder;
}
@Nullable
protected String findWebJarResourcePath(String webjar, String path) {
if (webjar.length() > 0) {
String version = version(webjar);
if (version != null) {
String partialPath = path(webjar, version, path);
if (partialPath != null) {
String webJarPath = webjar + "/" + version + partialPath;
return webJarPath;
}
}
}
return null;
}
private String path(String webjar, String version, String path) {
if (path.equals("/")) {
String module = module(webjar, version, path);
if (module != null) {
return module;
} else {
return null;
}
}
if (path.equals("/main.js")) {
String module = module(webjar, version, path);
if (module != null) {
return module;
}
}
if (new ClassPathResource(RESOURCE_ROOT + webjar + "/" + version + path).isReadable()) {
return path;
}
return null;
}
private String module(String webjar, String version, String path) {
Resource resource = new ClassPathResource(RESOURCE_ROOT + webjar + "/" + version + PACKAGE_JSON);
if (resource.isReadable()) {
try {
JsonParser parser = JsonParserFactory.getJsonParser();
Map<String, Object> map = parser
.parseMap(StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8));
if (!path.equals("/main.js") && map.containsKey("module")) {
return "/" + (String) map.get("module");
}
if (!map.containsKey("main") && map.containsKey("jspm")) {
String stem = resolve(map, "jspm.directories.lib", "dist");
String main = resolve(map, "jspm.main", "index.js");
return "/" + stem + "/" + main + (main.endsWith(".js") ? "" : ".js");
}
return "/" + (String) map.get("main");
} catch (IOException e) {
}
}
return null;
}
private static String resolve(Map<String, Object> map, String path, String defaultValue) {
Map<String, Object> sub = map;
String[] elements = StringUtils.delimitedListToStringArray(path, ".");
for (int i = 0; i < elements.length - 1; i++) {
@SuppressWarnings("unchecked")
Map<String, Object> tmp = (Map<String, Object>) sub.get(elements[i]);
sub = tmp;
if (sub == null) {
return defaultValue;
}
}
return (String) sub.getOrDefault(elements[elements.length - 1], defaultValue);
}
private String version(String webjar) {
Resource resource = new ClassPathResource(PROPERTIES_ROOT + NPM + webjar + POM_PROPERTIES);
if (!resource.isReadable()) {
resource = new ClassPathResource(PROPERTIES_ROOT + PLAIN + webjar + POM_PROPERTIES);
}
if (resource.isReadable()) {
Properties properties;
try {
properties = PropertiesLoaderUtils.loadProperties(resource);
return properties.getProperty("version");
} catch (IOException e) {
}
}
return null;
}
}
JavaScript in the browser has come a long way and native support for ECMAScript (aka es6 or esm) modules is becoming ubiquitous. Even without explicit support there is a shim that lets you use modules in all browsers with a one line import. The thing that is missing for most Spring Boot apps is ease of use in importing those modules into an HTML page or template.
Note that
<script type="importmap">
is a standard feature in browsers, but there is no uniform way to write the paths because the modules can come from literally any URL. Something convention-driven seems to make sense, and since we also support webjars in other ways, something with webjars appeals to me too.I would like to be able to do this in HTML (note the
/npm/*
paths in the map values - arbitrary, but convenient):and then be able to do this (i.e. just use modules like you do in Node.js):
The thing that would enable this is probably best implemented as a
@RequestMapping
.