Skip to content

Commit

Permalink
feat: Lit templates in Vite dev and prod mode (#12204)
Browse files Browse the repository at this point in the history
* In dev mode, read templates from
-  {projectRoot}/frontend
-  {projectRoot}/node_modules/@vaadin/flow-frontend 
-  {projectRoot}/node_modules (if not "./"-prefixed)

In prod mode, copy templates from the above.
  • Loading branch information
Johannes Eriksson committed Nov 8, 2021
1 parent a04b2e5 commit ad39b06
Show file tree
Hide file tree
Showing 61 changed files with 1,168 additions and 653 deletions.
Expand Up @@ -26,6 +26,7 @@
import com.vaadin.flow.component.template.Id;
import com.vaadin.flow.di.Instantiator;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.internal.Template;
import com.vaadin.flow.internal.UsageStatistics;
import com.vaadin.flow.server.VaadinService;

Expand Down Expand Up @@ -55,7 +56,8 @@
* @author Vaadin Ltd
* @since
*/
public abstract class LitTemplate extends Component implements HasStyle {
public abstract class LitTemplate extends Component
implements HasStyle, Template {

static {
UsageStatistics.markAsUsed("flow/LitTemplate", null);
Expand Down
Expand Up @@ -17,15 +17,13 @@

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

import org.apache.commons.io.FilenameUtils;
import org.jsoup.UncheckedIOException;
import org.jsoup.nodes.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -34,17 +32,17 @@
import com.vaadin.flow.component.littemplate.BundleLitParser;
import com.vaadin.flow.component.littemplate.LitTemplate;
import com.vaadin.flow.component.littemplate.LitTemplateParser;
import com.vaadin.flow.function.DeploymentConfiguration;
import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.di.ResourceProvider;
import com.vaadin.flow.internal.AnnotationReader;
import com.vaadin.flow.internal.Pair;
import com.vaadin.flow.server.Constants;
import com.vaadin.flow.server.DependencyFilter;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.shared.ui.Dependency;
import com.vaadin.flow.shared.ui.LoadMode;

import elemental.json.JsonObject;

/**
* Lit template parser implementation.
* <p>
Expand All @@ -67,10 +65,6 @@ public class LitTemplateParserImpl implements LitTemplateParser {

private static final LitTemplateParser INSTANCE = new LitTemplateParserImpl();

private final HashMap<String, String> cache = new HashMap<>();
private final ReentrantLock templateSourceslock = new ReentrantLock();
private JsonObject jsonStats;

/**
* The default constructor. Protected in order to prevent direct
* instantiation, but not private in order to allow mocking/overrides for
Expand Down Expand Up @@ -107,14 +101,7 @@ public TemplateData getTemplateContent(Class<? extends LitTemplate> clazz,
}

String url = dependency.getUrl();
String source = getSourcesFromTemplate(tag, url);
if (source == null) {
try {
source = getSourcesFromStats(service, url);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
String source = getSourcesFromTemplate(service, tag, url);
if (source == null) {
continue;
}
Expand Down Expand Up @@ -173,6 +160,8 @@ private boolean dependencyHasTagName(Dependency dependency, String tag) {
/**
* Finds the JavaScript sources for given tag.
*
* @param service
* the Vaadin service
* @param tag
* the value of the {@link com.vaadin.flow.component.Tag}
* annotation, e.g. `my-component`
Expand All @@ -184,9 +173,23 @@ private boolean dependencyHasTagName(Dependency dependency, String tag) {
* @return the .js source which declares given custom element, or null if no
* such source can be found.
*/
protected String getSourcesFromTemplate(String tag, String url) {
InputStream content = getClass().getClassLoader()
.getResourceAsStream(url);
protected String getSourcesFromTemplate(VaadinService service, String tag,
String url) {
InputStream content = getResourceStream(service, url);
if (content == null) {
// Attempt to get the sources from dev server, if available
content = FrontendUtils.getFrontendFileFromDevModeHandler(service,
url);
}
if (content == null) {
// In production builds, template sources are stored in
// META-INF/VAADIN/config/templates
String pathWithoutPrefix = url.replaceFirst("^\\./", "");
String vaadinDirectory = Constants.VAADIN_SERVLET_RESOURCES
+ Constants.TEMPLATE_DIRECTORY;
String resourceUrl = vaadinDirectory + pathWithoutPrefix;
content = getResourceStream(service, resourceUrl);
}
if (content != null) {
getLogger().debug(
"Found sources from the tag '{}' in the template '{}'", tag,
Expand All @@ -196,67 +199,19 @@ protected String getSourcesFromTemplate(String tag, String url) {
return null;
}

private String getSourcesFromStats(VaadinService service, String url)
throws IOException {
templateSourceslock.lock();
try {
if (isStatsFileReadNeeded(service)) {
String content = FrontendUtils.getStatsContent(service);
if (content != null) {
resetCache(content);
}
}
if (!cache.containsKey(url) && jsonStats != null) {
cache.put(url, BundleLitParser.getSourceFromStatistics(url,
jsonStats, service));
private InputStream getResourceStream(VaadinService service, String url) {
ResourceProvider resourceProvider = service.getContext()
.getAttribute(Lookup.class).lookup(ResourceProvider.class);
URL resourceUrl = resourceProvider.getApplicationResource(url);
if (resourceUrl != null) {
try {
return resourceUrl.openStream();
} catch (IOException e) {
getLogger().warn("Exception accessing resource " + resourceUrl,
e);
}
return cache.get(url);
} finally {
templateSourceslock.unlock();
}
}

/**
* Check status to see if stats.json needs to be loaded and parsed.
* <p>
* Always load if jsonStats is null, never load again when we have a bundle
* as it never changes, always load a new stats if the hash has changed and
* we do not have a bundle.
*
* @param service
* the Vaadin service.
* @return {@code true} if we need to re-load and parse stats.json, else
* {@code false}
*/
private boolean isStatsFileReadNeeded(VaadinService service)
throws IOException {
assert templateSourceslock.isHeldByCurrentThread();
DeploymentConfiguration config = service.getDeploymentConfiguration();
if (jsonStats == null) {
return true;
} else if (usesBundleFile(config)) {
return false;
}
return !jsonStats.get("hash").asString()
.equals(FrontendUtils.getStatsHash(service));
}

/**
* Check if we are running in a mode without dev server and using a pre-made
* bundle file.
*
* @param config
* deployment configuration
* @return true if production mode or disabled dev server
*/
private boolean usesBundleFile(DeploymentConfiguration config) {
return config.isProductionMode() && !config.enableDevServer();
}

private void resetCache(String fileContents) {
assert templateSourceslock.isHeldByCurrentThread();
cache.clear();
jsonStats = BundleLitParser.parseJsonStatistics(fileContents);
return null;
}

private Logger getLogger() {
Expand Down
Expand Up @@ -39,9 +39,6 @@
import com.vaadin.flow.server.MockVaadinServletService;
import com.vaadin.flow.server.frontend.FrontendUtils;

import static com.vaadin.flow.server.Constants.STATISTICS_JSON_DEFAULT;
import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES;

public class LitTemplateParserImplTest {

private MockVaadinServletService service;
Expand Down Expand Up @@ -76,10 +73,10 @@ public void init() {

ResourceProvider resourceProvider = service.getContext()
.getAttribute(Lookup.class).lookup(ResourceProvider.class);
Mockito.when(resourceProvider.getApplicationResource(
VAADIN_SERVLET_RESOURCES + STATISTICS_JSON_DEFAULT))
.thenReturn(LitTemplateParserImplTest.class.getResource(
"/" + VAADIN_SERVLET_RESOURCES + "config/stats.json"));
Mockito.when(
resourceProvider.getApplicationResource(Mockito.anyString()))
.thenAnswer(invoc -> LitTemplateParserImpl.class
.getClassLoader().getResource(invoc.getArgument(0)));
}

@Test
Expand Down Expand Up @@ -180,9 +177,6 @@ public void getTemplateContent_sourceNotFoundInStatsFile_returnsNull() {
public void getTemplateContent_sourceFileWithFaultyTemplateGetter_returnsNull() {
// If the template getter can not be found it should result in no
// template element children
Mockito.when(configuration.getStringProperty(Mockito.anyString(),
Mockito.anyString()))
.thenReturn(VAADIN_SERVLET_RESOURCES + "config/stats.json");
LitTemplateParser.TemplateData templateContent = LitTemplateParserImpl
.getInstance()
.getTemplateContent(MyFaulty.class, "my-element", service);
Expand All @@ -194,9 +188,6 @@ public void getTemplateContent_sourceFileWithFaultyTemplateGetter_returnsNull()
public void getTemplateContent_renderIsDefinedInSuperClass_returnsNull() {
// If the template getter can not be found it should result in no
// template element children
Mockito.when(configuration.getStringProperty(Mockito.anyString(),
Mockito.anyString()))
.thenReturn(VAADIN_SERVLET_RESOURCES + "config/stats.json");
LitTemplateParser.TemplateData templateContent = LitTemplateParserImpl
.getInstance().getTemplateContent(MyFaulty.class,
"my-super-lit-element", service);
Expand All @@ -206,9 +197,6 @@ public void getTemplateContent_renderIsDefinedInSuperClass_returnsNull() {

@Test
public void getTemplateContent_nonLocalTemplate_rootElementParsed() {
Mockito.when(configuration.getStringProperty(Mockito.anyString(),
Mockito.anyString()))
.thenReturn(VAADIN_SERVLET_RESOURCES + "config/stats.json");
LitTemplateParser.TemplateData templateContent = LitTemplateParserImpl
.getInstance().getTemplateContent(HelloWorld.class,
HelloWorld.class.getAnnotation(Tag.class).value(),
Expand All @@ -223,9 +211,6 @@ public void getTemplateContent_nonLocalTemplate_rootElementParsed() {

@Test
public void getTemplateContent_nonLocalTemplateInTargetFolder_rootElementParsed() {
Mockito.when(configuration.getStringProperty(Mockito.anyString(),
Mockito.anyString()))
.thenReturn(VAADIN_SERVLET_RESOURCES + "config/stats.json");
LitTemplateParser.TemplateData templateContent = LitTemplateParserImpl
.getInstance().getTemplateContent(HelloWorld2.class,
HelloWorld2.class.getAnnotation(Tag.class).value(),
Expand All @@ -240,10 +225,6 @@ public void getTemplateContent_nonLocalTemplateInTargetFolder_rootElementParsed(

@Test
public void severalJsModuleAnnotations_theFirstFileDoesNotExist_fileWithContentIsChosen() {
Mockito.when(configuration.getStringProperty(Mockito.anyString(),
Mockito.anyString()))
.thenReturn(VAADIN_SERVLET_RESOURCES + "config/stats.json");

LitTemplateParser instance = LitTemplateParserImpl.getInstance();
LitTemplateParser.TemplateData templateContent = instance
.getTemplateContent(BrokenJsModuleAnnotation.class,
Expand All @@ -256,10 +237,6 @@ public void severalJsModuleAnnotations_theFirstFileDoesNotExist_fileWithContentI

@Test
public void severalJsModuleAnnotations_parserSelectsByName() {
Mockito.when(configuration.getStringProperty(Mockito.anyString(),
Mockito.anyString()))
.thenReturn(VAADIN_SERVLET_RESOURCES + "config/stats.json");

LitTemplateParser instance = LitTemplateParserImpl.getInstance();
LitTemplateParser.TemplateData templateContent = instance
.getTemplateContent(SeveralJsModuleAnnotations.class,
Expand Down Expand Up @@ -320,7 +297,7 @@ public class HelloWorld2 extends LitTemplate {
}

@Tag("my-lit-element-view")
@JsModule("./frontend/non-existant.js")
@JsModule("./frontend/non-existent.js")
@JsModule("./frontend/my-lit-element-view.js")
public class BrokenJsModuleAnnotation extends LitTemplate {
}
Expand Down

This file was deleted.

@@ -0,0 +1,24 @@
// Import an element
import { LitElement, html } from 'lit';

// Define an element class
export class MyLitElement extends LitElement {

// Define public API properties
// Define the element's template
render() {
return `
<style>
:host{
margin: 5px;
}
.response { margin-top: 10px; }
</style>
<div id="test" class="response">Web components like you, too.</div>
`;
}
}

// Register the element with the browser
customElements.define('my-element', MyLitElement);
@@ -0,0 +1,23 @@
// Import an element
import { LitElement, html } from 'lit';

// Define an element class
export class MyGreedyLitElement extends LitElement {

// Define the element's template
render() {
return html`
<style>
:host{
margin: 5px;
}
.response { margin-top: 10px; }
</style>
<div>\`Tag name doesn't match the JS module name<div>inner</div></div> <div id='test' class='response'>greedy</div>
`;}
static get styles() { return css`:host { background-color: pink } <span>incorrect content</span>`; }
}

// Register the element with the browser
customElements.define('my-greedy-element', MyGreedyLitElement);

0 comments on commit ad39b06

Please sign in to comment.