Skip to content

Commit

Permalink
Merge pull request #101 from mvnpm/auto-deps
Browse files Browse the repository at this point in the history
Add Deps import support in AutoEntryPoint
  • Loading branch information
ia3andy authored Mar 13, 2024
2 parents 0dda344 + 83d5812 commit 2e5f4d9
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 114 deletions.
3 changes: 1 addition & 2 deletions src/main/java/io/mvnpm/esbuild/Bundler.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import io.mvnpm.esbuild.model.BundleResult;
import io.mvnpm.esbuild.model.EsBuildConfig;
import io.mvnpm.esbuild.model.ExecuteResult;
import io.mvnpm.esbuild.model.ScriptlessEntryPoint;
import io.mvnpm.esbuild.resolve.Resolver;

public class Bundler {
Expand Down Expand Up @@ -71,7 +70,7 @@ private static EsBuildConfig createBundle(BundleOptions bundleOptions, Path work
Files.createDirectories(dist);
esBuildConfig.setOutdir(dist.toString());
if (bundleOptions.getEntries() == null) {
bundleOptions.setEntries(List.of(new ScriptlessEntryPoint(getNodeModulesDir(workDir, bundleOptions))));
throw new IllegalArgumentException("At least one entry point is required");
}
final List<String> paths = bundleOptions.getEntries().stream().map(entry -> entry.process(workDir).toString()).toList();
esBuildConfig.setEntryPoint(paths.toArray(String[]::new));
Expand Down
111 changes: 89 additions & 22 deletions src/main/java/io/mvnpm/esbuild/model/AutoEntryPoint.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.mvnpm.esbuild.model;

import static io.mvnpm.esbuild.util.PathUtils.copyEntries;
import static java.util.Objects.requireNonNull;

import java.io.IOException;
import java.io.StringWriter;
Expand All @@ -11,55 +12,78 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;

public class AutoEntryPoint implements EntryPoint {
private static final Set<String> SCRIPTS = Set.of("js", "ts", "jsx", "tsx", "mjs", "mts", "cjs", "cts");
private final String name;
private final Path rootDir;
private final List<String> scripts;
private final List<Source> sources;
private final AutoDeps autoDeps;

public AutoEntryPoint(Path rootDir, String name, List<String> scripts) {
this.name = name;
this.rootDir = rootDir;
this.scripts = scripts;
private AutoEntryPoint(Path rootDir, String name, List<String> sources, AutoDeps autoDeps) {
this.name = requireNonNull(name, "name is required");
this.rootDir = requireNonNull(rootDir, "rootDir is required");
this.sources = requireNonNull(sources, "sources are required").stream().map(Source::of).toList();
this.autoDeps = autoDeps;
}

public static AutoEntryPoint withoutAutoDeps(Path rootDir, String name, List<String> sources) {
return new AutoEntryPoint(rootDir, name, sources, null);
}

public static AutoEntryPoint withAutoDeps(Path rootDir, String name, List<String> sources, AutoDeps autoDeps) {
return new AutoEntryPoint(rootDir, name, sources, autoDeps);
}

@Override
public Path process(Path workDir) {
try {
if (!Objects.equals(rootDir, workDir)) {
copyEntries(rootDir, scripts, workDir);
String content = autoImportsSources(workDir);
AutoDepsMode resolvedMode = autoDeps != null ? autoDeps.mode() : AutoDepsMode.NONE;
if (resolvedMode == AutoDepsMode.AUTO) {
resolvedMode = sources.stream().noneMatch(Source::isScript) ? AutoDepsMode.ALL : AutoDepsMode.STYLES;
}
return bundleScripts(workDir, name, scripts);
switch (resolvedMode) {
case ALL -> content += DependenciesAutoImports.dependenciesImports(autoDeps.nodeModulesDir,
autoDeps.idsPredicate, false);
case STYLES -> content += DependenciesAutoImports.dependenciesImports(autoDeps.nodeModulesDir,
autoDeps.idsPredicate, true);
case NONE -> {
}
}
return createEntryPoint(name, workDir, content);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private Path bundleScripts(Path workDir, String bundleName, List<String> scripts) throws IOException {
final String entryString = convert(workDir, scripts);
final Path entry = workDir.resolve("%s.js".formatted(bundleName));
Files.writeString(entry, entryString, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
static Path createEntryPoint(String name, Path workDir, String content) throws IOException {
final Path entry = workDir.resolve("%s.js".formatted(name));
Files.writeString(entry, content, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
return entry;
}

private String convert(Path workDir, List<String> scripts) {
protected String autoImportsSources(Path workDir) {
if (sources.isEmpty()) {
return "";
}
if (!Objects.equals(rootDir, workDir)) {
copyEntries(rootDir, sources.stream().map(Source::relativePath).toList(), workDir);
}
try (StringWriter sw = new StringWriter()) {
for (String script : scripts) {
final String fileName = Path.of(script).getFileName().toString();
final int index = fileName.lastIndexOf(".");
final String ext = fileName.substring(index + 1);
final boolean isScript = SCRIPTS.contains(ext);
sw.write("// Auto-generated imports for project sources\n");
for (Source source : sources) {
String line;
if (isScript) {
script = script.substring(0, script.lastIndexOf("."));
line = EXPORT.formatted(script);
if (source.isScript()) {
line = EXPORT.formatted(source.relativePathWithoutExt());
} else {
line = IMPORT.formatted(script);
line = IMPORT.formatted(source.relativePath());
}
sw.write(line);
sw.write("\n");
}
sw.write("\n");
return sw.toString();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
Expand All @@ -68,4 +92,47 @@ private String convert(Path workDir, List<String> scripts) {

private static final String EXPORT = "export * from \"./%s\";";
private static final String IMPORT = "import \"./%s\";";

record Source(String relativePath, String ext) {
public static Source of(String relativePath) {
final String ext = resolveExtension(relativePath);
return new Source(relativePath, ext);
}

public String relativePathWithoutExt() {
return relativePath.substring(0, relativePath.lastIndexOf("."));
}

public boolean isScript() {
return SCRIPTS.contains(ext);
}
}

public static boolean isScript(String relativePath) {
if (relativePath == null || relativePath.isBlank()) {
return false;
}
final String ext = resolveExtension(relativePath);
return SCRIPTS.contains(ext);
}

public static String resolveExtension(String relativePath) {
final String fileName = Path.of(relativePath).getFileName().toString();
final int index = fileName.lastIndexOf(".");
return fileName.substring(index + 1);
}

public enum AutoDepsMode {
ALL,
STYLES,
AUTO,
NONE
}

public record AutoDeps(AutoDepsMode mode, Path nodeModulesDir, Predicate<String> idsPredicate) {
public AutoDeps(AutoDepsMode mode, Path nodeModulesDir) {
this(mode, nodeModulesDir, id -> true);
}

}
}
1 change: 1 addition & 0 deletions src/main/java/io/mvnpm/esbuild/model/BundleOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,5 @@ public BundleOptions setNodeModulesDir(Path nodeModulesDir) {
this.nodeModulesDir = nodeModulesDir;
return this;
}

}
15 changes: 13 additions & 2 deletions src/main/java/io/mvnpm/esbuild/model/BundleOptionsBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

import io.mvnpm.esbuild.model.AutoEntryPoint.AutoDeps;
import io.mvnpm.esbuild.model.AutoEntryPoint.AutoDepsMode;

public class BundleOptionsBuilder {
private final BundleOptions options = new BundleOptions();
Expand All @@ -15,8 +19,14 @@ private static EsBuildConfig useDefaultConfig() {
return new EsBuildConfigBuilder().build();
}

public BundleOptionsBuilder addAutoEntryPoint(Path sourceDir, String name, List<String> scripts) {
return addEntryPoint(new AutoEntryPoint(sourceDir, name, scripts));
public BundleOptionsBuilder addAutoEntryPoint(Path sourceDir, String name, List<String> sources) {
return addEntryPoint(AutoEntryPoint.withoutAutoDeps(sourceDir, name, sources));
}

public BundleOptionsBuilder addAutoEntryPoint(Path sourceDir, String name, List<String> sources, AutoDepsMode mode,
Predicate<String> autoDepsIdsPredicate) {
return addEntryPoint(AutoEntryPoint.withAutoDeps(sourceDir, name, sources,
new AutoDeps(mode, options.getNodeModulesDir(), autoDepsIdsPredicate)));
}

public BundleOptionsBuilder addEntryPoint(Path rootDir, String script) {
Expand Down Expand Up @@ -68,4 +78,5 @@ public BundleOptionsBuilder withEsConfig(EsBuildConfig esBuildConfig) {
public BundleOptions build() {
return options;
}

}
78 changes: 78 additions & 0 deletions src/main/java/io/mvnpm/esbuild/model/DependenciesAutoImports.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.mvnpm.esbuild.model;

import static io.mvnpm.esbuild.install.WebDepsInstaller.getMvnpmInfoPath;
import static io.mvnpm.esbuild.model.AutoEntryPoint.isScript;
import static io.mvnpm.esbuild.util.JarInspector.PACKAGE_JSON;
import static java.util.function.Predicate.not;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.mvnpm.esbuild.install.MvnpmInfo;
import io.mvnpm.esbuild.install.WebDepsInstaller;

public class DependenciesAutoImports {
private static final String[] FIELDS = new String[] { "name", "module", "main", "style", "sass", "browser" };

private static final ObjectMapper objectMapper = new ObjectMapper();

static String dependenciesImports(Path nodeModulesDir, Predicate<String> idsPredicate, boolean onlyStyles) {
MvnpmInfo mvnpmInfo = WebDepsInstaller.readMvnpmInfo(getMvnpmInfoPath(nodeModulesDir));

Stream<Path> packageJsons = mvnpmInfo.installed().stream()
.filter(d -> idsPredicate.test(d.id()))
.flatMap(dependency -> dependency.dirs().stream().map(d -> nodeModulesDir.resolve(d).resolve(PACKAGE_JSON)))
.filter(Files::exists);

String entries = packageJsons.map(packageJson -> {
StringBuilder imports = new StringBuilder();
Map<String, String> data = readPackage(packageJson);
// Some packages have both style and script, so we need to check both
if (!data.getOrDefault("sass", "").isBlank()) {
imports.append(IMPORT.formatted(data.get("name") + "/" + data.get("sass"))).append("\n");
} else if (!data.getOrDefault("style", "").isBlank()) {
imports.append(IMPORT.formatted(data.get("name") + "/" + data.get("style"))).append("\n");
}
if (onlyStyles) {
return imports.toString();
}
// Based on this: https://esbuild.github.io/api/#platform
// Use module if browser is defined else use main if defined
if (isScript(data.get("module")) && !data.getOrDefault("browser", "").isBlank()) {
imports.append(IMPORT.formatted(data.get("name"))).append("\n");
} else if (isScript(data.get("main"))) {
imports.append(IMPORT.formatted(data.get("name"))).append("\n");
}
return imports.toString();
}).filter(not(String::isBlank)).collect(Collectors.joining(""));
return "// Auto-generated imports for web dependencies\n" + entries + "\n";
}

private static Map<String, String> readPackage(Path path) {
Map<String, String> contents = new HashMap<>(FIELDS.length);
try {
JsonNode object = objectMapper.readTree(path.toFile());
for (String field : FIELDS) {
if (object.has(field)) {
final JsonNode node = object.get(field);
contents.put(field, node.isValueNode() ? node.asText() : "[object]");
}
}
return contents;
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}

private static final String IMPORT = "import \"%s\";";
}
77 changes: 0 additions & 77 deletions src/main/java/io/mvnpm/esbuild/model/ScriptlessEntryPoint.java

This file was deleted.

5 changes: 0 additions & 5 deletions src/test/java/io/mvnpm/esbuild/BundlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,6 @@ public void shouldBundleMvnpmWithoutPackageJson() throws URISyntaxException, IOE
executeTest(List.of("/mvnpm/polymer-3.5.1.jar"), WebDependencyType.MVNPM, "application-mvnpm-importmap.js", true);
}

@Test
public void shouldBundleWithoutEntryPoint() throws URISyntaxException, IOException {
executeTest(List.of("/mvnpm/stimulus-3.2.1.jar"), WebDependencyType.MVNPM, null, true);
}

@Test
public void shouldBundle() throws URISyntaxException, IOException {
executeTest(List.of("/webjars/htmx.org-1.8.4.jar"), WebDependencyType.WEBJARS, "application-webjar.js", true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,10 @@ void testNoInfo() throws IOException {
new MvnpmInfo.InstalledDependency("org.something:hooks-0.4.9", List.of("@restart/hooks"))));
}

private List<WebDependency> getWebDependencies(List<String> jarNames) {
public static List<WebDependency> getWebDependencies(List<String> jarNames) {
return jarNames.stream().map(jarName -> {
try {
return new File(getClass().getResource(jarName).toURI()).toPath();
return new File(WebDepsInstallerTest.class.getResource(jarName).toURI()).toPath();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
Expand Down
Loading

0 comments on commit 2e5f4d9

Please sign in to comment.