diff --git a/flow-lit-template/src/main/java/com/vaadin/flow/component/littemplate/LitTemplate.java b/flow-lit-template/src/main/java/com/vaadin/flow/component/littemplate/LitTemplate.java index 0ea65432955..3d5620b4b94 100644 --- a/flow-lit-template/src/main/java/com/vaadin/flow/component/littemplate/LitTemplate.java +++ b/flow-lit-template/src/main/java/com/vaadin/flow/component/littemplate/LitTemplate.java @@ -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; @@ -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); diff --git a/flow-lit-template/src/main/java/com/vaadin/flow/component/littemplate/internal/LitTemplateParserImpl.java b/flow-lit-template/src/main/java/com/vaadin/flow/component/littemplate/internal/LitTemplateParserImpl.java index b7b6177f9d9..0632a8fc2dd 100644 --- a/flow-lit-template/src/main/java/com/vaadin/flow/component/littemplate/internal/LitTemplateParserImpl.java +++ b/flow-lit-template/src/main/java/com/vaadin/flow/component/littemplate/internal/LitTemplateParserImpl.java @@ -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; @@ -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. *

@@ -67,10 +65,6 @@ public class LitTemplateParserImpl implements LitTemplateParser { private static final LitTemplateParser INSTANCE = new LitTemplateParserImpl(); - private final HashMap 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 @@ -107,14 +101,7 @@ public TemplateData getTemplateContent(Class 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; } @@ -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` @@ -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, @@ -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. - *

- * 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() { diff --git a/flow-lit-template/src/test/java/com/vaadin/flow/component/littemplate/internal/LitTemplateParserImplTest.java b/flow-lit-template/src/test/java/com/vaadin/flow/component/littemplate/internal/LitTemplateParserImplTest.java index 7a68be037fc..4138fd42801 100644 --- a/flow-lit-template/src/test/java/com/vaadin/flow/component/littemplate/internal/LitTemplateParserImplTest.java +++ b/flow-lit-template/src/test/java/com/vaadin/flow/component/littemplate/internal/LitTemplateParserImplTest.java @@ -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; @@ -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 @@ -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); @@ -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); @@ -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(), @@ -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(), @@ -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, @@ -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, @@ -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 { } diff --git a/flow-lit-template/src/test/resources/META-INF/VAADIN/config/stats.json b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/stats.json deleted file mode 100644 index eadcddbc6c6..00000000000 --- a/flow-lit-template/src/test/resources/META-INF/VAADIN/config/stats.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "hash": "64bb80639ef116681818", - "assetsByChunkName" :{ - "bundle": "build/vaadin-bundle-1111.cache.js", - "export": "build/vaadin-export-2222.cache.js" - }, - "modules": [ - { - "name": "../node_modules/@vaadin/flow-frontend/src/hello-world-lit.js", - "source": "// Import an element\nimport { LitElement, html } from 'lit';\n\n// Define an element class\n export class HelloWorld extends LitElement {\n\n // Define the element's template\n render() {\n return html`\n \n

Tag name doesn't match the JS module name
inner
Web components like you, too.
\n `;\n }\n}\n\n// Register the element with the browser\ncustomElements.define('hello-world-lit', HelloWorld);" - }, - { - "name": "../target/flow-frontend/src/hello-world2.js", - "source": "// Import an element\nimport { LitElement, html } from 'lit';\n\n// Define an element class\n export class HelloWorld extends LitElement {\n\n // Define the element's template\n render() {\n return html`\n \n
Tag name doesn't match the JS module name
inner
Web components like you, too.
\n `;\n }\n}\n\n// Register the element with the browser\ncustomElements.define('hello-world-lit', HelloWorld);" - }, - { - "name": "./frontend/MyElementFaultyMethods.js", - "source": "// Import an element\nimport { LitElement, html } from 'lit';\n\n// Define an element class\nexport class MyLitElement extends LitElement {\n\n // Define public API properties\n // Define the element's template\n render() {\n return `\n \n
Web components like you, too.
\n `;\n }\n}\n\n// Register the element with the browser\ncustomElements.define('my-element', MyLitElement);" - }, - { - "name": "./frontend/MySuperLitElement.js", - "source": "// Import an element\nimport { LitElement, html } from 'lit'; \nimport { SimpleLitTemplateShadowRoot } from './MyLitElement.js';\n export class MySuperLitElement extends MyLitElement { createRenderRoot() { return this; }} customElements.define('my-super-lit-element', MySuperLitElement);" - }, - { - "id": "./frontend/my-form.ts", - "name": "./frontend/my-form.ts", - "source": "import { html, LitElement } from 'lit';\r\nimport { customElement } from 'lit/decorators.js';\r\n// @customElement(\"my-form\")\r\nexport class MyFormElement extends LitElement {\r\n render() {\r\n return html `\n

Hello

\n \n `;\r\n }\r\n}\r\ncustomElements.define(\"my-form\", MyFormElement);\r\n" - } - ] - , - - "chunks" : [ - { - "modules": [ - { - "name": "./frontend/MyLitElement.js", - "source": "// Import an element\nimport { LitElement, html } from 'lit';\n\n// Define an element class\n export class MyLitElement extends LitElement {\n\n // Define the element's template\n render() {\n return html`\n \n
Tag name doesn't match the JS module name
inner
Web components like you, too.
\n `;\n }\n}\n\n// Register the element with the browser\ncustomElements.define('my-element', MyLitElement);" - } - , - { - "name": "./frontend/MyGreedyLitElement.js", - "source": "// Import an element\nimport { LitElement, html } from 'lit';\n\n// Define an element class\n export class MyGreedyLitElement extends LitElement {\n\n // Define the element's template\n render() {\n return html`\n \n
\\`Tag name doesn't match the JS module name
inner
greedy
\n `;}\n static get styles() { return css`:host { background-color: pink } incorrect content`; }\n}\n\n// Register the element with the browser\ncustomElements.define('my-greedy-element', MyGreedyLitElement);" - } - ] - } - ] -} diff --git a/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/MyElementFaultyMethods.js b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/MyElementFaultyMethods.js new file mode 100644 index 00000000000..d96d821d7ac --- /dev/null +++ b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/MyElementFaultyMethods.js @@ -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 ` + +
Web components like you, too.
+ `; + } +} + +// Register the element with the browser +customElements.define('my-element', MyLitElement); diff --git a/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/MyGreedyLitElement.js b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/MyGreedyLitElement.js new file mode 100644 index 00000000000..88880f52d70 --- /dev/null +++ b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/MyGreedyLitElement.js @@ -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` + +
\`Tag name doesn't match the JS module name
inner
greedy
+ `;} + static get styles() { return css`:host { background-color: pink } incorrect content`; } +} + +// Register the element with the browser +customElements.define('my-greedy-element', MyGreedyLitElement); diff --git a/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/MyLitElement.js b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/MyLitElement.js new file mode 100644 index 00000000000..56ef44e3da5 --- /dev/null +++ b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/MyLitElement.js @@ -0,0 +1,23 @@ +// Import an element +import { LitElement, html } from 'lit'; + +// Define an element class +export class MyLitElement extends LitElement { + + // Define the element's template + render() { + return html` + +
Tag name doesn't match the JS module name
inner
Web components like you, too.
+ `; + } +} + +// Register the element with the browser +customElements.define('my-element', MyLitElement); diff --git a/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/MySuperLitElement.js b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/MySuperLitElement.js new file mode 100644 index 00000000000..71d7e2fd981 --- /dev/null +++ b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/MySuperLitElement.js @@ -0,0 +1,4 @@ +// Import an element +import { LitElement, html } from 'lit'; +import { SimpleLitTemplateShadowRoot } from './MyLitElement.js'; +export class MySuperLitElement extends MyLitElement { createRenderRoot() { return this; }} customElements.define('my-super-lit-element', MySuperLitElement); diff --git a/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/my-form.ts b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/my-form.ts new file mode 100644 index 00000000000..543c6bb2c21 --- /dev/null +++ b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/my-form.ts @@ -0,0 +1,12 @@ +import { html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +// @customElement("my-form") +export class MyFormElement extends LitElement { + render() { + return html ` +

Hello

+ + `; + } +} +customElements.define("my-form", MyFormElement); diff --git a/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/src/hello-world-lit.js b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/src/hello-world-lit.js new file mode 100644 index 00000000000..346e87687e5 --- /dev/null +++ b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/src/hello-world-lit.js @@ -0,0 +1,23 @@ +// Import an element +import { LitElement, html } from 'lit'; + +// Define an element class +export class HelloWorld extends LitElement { + + // Define the element's template + render() { + return html` + +
Tag name doesn't match the JS module name
inner
Web components like you, too.
+ `; + } +} + +// Register the element with the browser +customElements.define('hello-world-lit', HelloWorld); diff --git a/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/src/hello-world2.js b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/src/hello-world2.js new file mode 100644 index 00000000000..346e87687e5 --- /dev/null +++ b/flow-lit-template/src/test/resources/META-INF/VAADIN/config/templates/src/hello-world2.js @@ -0,0 +1,23 @@ +// Import an element +import { LitElement, html } from 'lit'; + +// Define an element class +export class HelloWorld extends LitElement { + + // Define the element's template + render() { + return html` + +
Tag name doesn't match the JS module name
inner
Web components like you, too.
+ `; + } +} + +// Register the element with the browser +customElements.define('hello-world-lit', HelloWorld); diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java index 19bd6ed0eed..29d89bd8340 100644 --- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java +++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java @@ -155,12 +155,9 @@ public static void prepareFrontend(PluginAdapterBase adapter) .withHomeNodeExecRequired(adapter.requireHomeNodeExec()) .setJavaResourceFolder(adapter.javaResourceFolder()) .withProductionMode(adapter.productionMode()); - // If building a jar project copy jar artifact contents now as we - // might not be able to read files from jar path. - if (adapter.isJarProject()) { - builder.copyResources(adapter.getJarFiles()); - } + // Copy jar artifact contents in TaskCopyFrontendFiles + builder.copyResources(adapter.getJarFiles()); try { builder.build().execute(); @@ -300,7 +297,7 @@ public static void runNodeUpdater(PluginAdapterBuild adapter) .enablePackagesUpdate(true) .useByteCodeScanner(adapter.optimizeBundle()) .withFlowResourcesFolder(flowResourcesFolder) - .copyResources(jarFiles) + .copyResources(jarFiles).copyTemplates(true) .copyLocalResources( adapter.frontendResourcesDirectory()) .enableImportsUpdate(true) diff --git a/flow-polymer-template/src/main/java/com/vaadin/flow/component/polymertemplate/NpmTemplateParser.java b/flow-polymer-template/src/main/java/com/vaadin/flow/component/polymertemplate/NpmTemplateParser.java index 68f1d85ba91..e8a08451800 100644 --- a/flow-polymer-template/src/main/java/com/vaadin/flow/component/polymertemplate/NpmTemplateParser.java +++ b/flow-polymer-template/src/main/java/com/vaadin/flow/component/polymertemplate/NpmTemplateParser.java @@ -19,14 +19,11 @@ 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; @@ -34,17 +31,15 @@ import com.vaadin.flow.component.dependency.JsModule; import com.vaadin.flow.di.Lookup; import com.vaadin.flow.di.ResourceProvider; -import com.vaadin.flow.function.DeploymentConfiguration; 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; - /** * Npm template parser implementation. *

@@ -75,10 +70,6 @@ public class NpmTemplateParser implements TemplateParser { private static final TemplateParser INSTANCE = new NpmTemplateParser(); - private final HashMap cache = new HashMap<>(); - private final ReentrantLock lock = 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 @@ -117,13 +108,6 @@ public TemplateData getTemplateContent( String url = dependency.getUrl(); String source = getSourcesFromTemplate(service, tag, url); - if (source == null) { - try { - source = getSourcesFromStats(service, url); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } if (source == null) { continue; } @@ -195,16 +179,20 @@ private boolean dependencyHasTagName(Dependency dependency, String tag) { */ protected String getSourcesFromTemplate(VaadinService service, String tag, String url) { - Lookup lookup = service.getContext().getAttribute(Lookup.class); - ResourceProvider resourceProvider = lookup - .lookup(ResourceProvider.class); - InputStream content = null; - try { - URL appResource = resourceProvider.getApplicationResource(url); - content = appResource == null ? null : appResource.openStream(); - } catch (IOException exception) { - getLogger().warn("Coudln't get resource for the template '{}'", url, - exception); + 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( @@ -215,67 +203,19 @@ protected String getSourcesFromTemplate(VaadinService service, String tag, return null; } - private String getSourcesFromStats(VaadinService service, String url) - throws IOException { - try { - lock.lock(); - if (isStatsFileReadNeeded(service)) { - String content = FrontendUtils.getStatsContent(service); - if (content != null) { - resetCache(content); - } + 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); } - if (!cache.containsKey(url) && jsonStats != null) { - cache.put(url, BundleParser.getSourceFromStatistics(url, - jsonStats, service)); - } - return cache.get(url); - } finally { - lock.unlock(); - } - } - - /** - * Check status to see if stats.json needs to be loaded and parsed. - *

- * 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} - */ - protected boolean isStatsFileReadNeeded(VaadinService service) - throws IOException { - assert lock.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 lock.isHeldByCurrentThread(); - cache.clear(); - jsonStats = BundleParser.parseJsonStatistics(fileContents); + return null; } private Logger getLogger() { diff --git a/flow-polymer-template/src/main/java/com/vaadin/flow/component/polymertemplate/PolymerTemplate.java b/flow-polymer-template/src/main/java/com/vaadin/flow/component/polymertemplate/PolymerTemplate.java index e83e4bb9fe5..0ef47c9a755 100644 --- a/flow-polymer-template/src/main/java/com/vaadin/flow/component/polymertemplate/PolymerTemplate.java +++ b/flow-polymer-template/src/main/java/com/vaadin/flow/component/polymertemplate/PolymerTemplate.java @@ -24,6 +24,7 @@ import com.vaadin.flow.component.dependency.JsModule; import com.vaadin.flow.component.template.Id; import com.vaadin.flow.dom.Element; +import com.vaadin.flow.internal.Template; import com.vaadin.flow.internal.UsageStatistics; import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.templatemodel.TemplateModel; @@ -55,7 +56,7 @@ */ @Deprecated public abstract class PolymerTemplate - extends AbstractTemplate { + extends AbstractTemplate implements Template { static { UsageStatistics.markAsUsed("flow/PolymerTemplate", null); diff --git a/flow-polymer-template/src/test/java/com/vaadin/flow/component/polymertemplate/BundleParserTest.java b/flow-polymer-template/src/test/java/com/vaadin/flow/component/polymertemplate/BundleParserTest.java index ac0f8126f7f..5e5ede3bb9b 100644 --- a/flow-polymer-template/src/test/java/com/vaadin/flow/component/polymertemplate/BundleParserTest.java +++ b/flow-polymer-template/src/test/java/com/vaadin/flow/component/polymertemplate/BundleParserTest.java @@ -1,20 +1,14 @@ package com.vaadin.flow.component.polymertemplate; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.Properties; import java.util.stream.Stream; -import org.apache.commons.io.IOUtils; import org.jsoup.nodes.Element; import org.junit.Assert; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import org.mockito.Mockito; - import com.vaadin.flow.di.Instantiator; import com.vaadin.flow.function.DeploymentConfiguration; import com.vaadin.flow.server.MockVaadinServletService; @@ -23,27 +17,11 @@ import elemental.json.Json; import elemental.json.JsonObject; -import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES; - public class BundleParserTest { - private static final String statsFile = VAADIN_SERVLET_RESOURCES - + "config/stats.json"; - - private static JsonObject stats; - private MockVaadinServletService service; private DeploymentConfiguration configuration; - @BeforeClass - public static void initClass() throws IOException { - InputStream stream = BundleParserTest.class.getClassLoader() - .getResourceAsStream(statsFile); - String statsFileContents = IOUtils.toString(stream, - StandardCharsets.UTF_8); - stats = BundleParser.parseJsonStatistics(statsFileContents); - } - @Before public void init() { configuration = Mockito.mock(DeploymentConfiguration.class); @@ -70,51 +48,6 @@ public void init() { service.init(instantiator); } - @Test - public void nonLocalTemplate_sourcesShouldBeFound() { - final String source = BundleParser.getSourceFromStatistics( - "./src/hello-world.js", stats, service); - Assert.assertNotNull("Source expected in stats.json", source); - } - - @Test - public void nonLocalTemplate_sourcesShouldBeFoundInTargetFolder() { - final String source = BundleParser.getSourceFromStatistics( - "./src/hello-world2.js", stats, service); - Assert.assertNotNull("Source expected in stats.json", source); - } - - @Test - public void nonLocalTemplate_windowsPath_sourcesShouldBeFoundInTargetFolder() { - Mockito.when(configuration.getFlowResourcesFolder()).thenReturn( - "target\\" + FrontendUtils.DEFAULT_FLOW_RESOURCES_FOLDER); - final String source = BundleParser.getSourceFromStatistics( - "./src/hello-world2.js", stats, service); - Assert.assertNotNull("Source expected in stats.json", source); - } - - @Test - public void frontendPrefix_sourcesShouldBeFound() { - final String source = BundleParser.getSourceFromStatistics( - "./frontend/src/hello-world.js", stats, service); - Assert.assertNotNull("Source expected in stats.json", source); - } - - @Test - public void typeScriptExtension_sourcesShouldBeFound() { - final String source = BundleParser.getSourceFromStatistics( - "./frontend/my-form.ts", stats, service); - Assert.assertNotNull("TypeScript sources expected in stats.json", - source); - } - - @Test - public void frontendProtocol_sourcesShouldBeFound() { - final String source = BundleParser.getSourceFromStatistics( - "frontend:///src/hello-world.js", stats, service); - Assert.assertNotNull("Source expected in stats.json", source); - } - @Test public void startsWithSingleLetterDirector_sourcesShouldNotBeFound() { // This test exposes a common error in String#replaceFirst (unescaped diff --git a/flow-polymer-template/src/test/java/com/vaadin/flow/component/polymertemplate/NpmTemplateParserTest.java b/flow-polymer-template/src/test/java/com/vaadin/flow/component/polymertemplate/NpmTemplateParserTest.java index bc6e40dfddd..7fd52dd408a 100644 --- a/flow-polymer-template/src/test/java/com/vaadin/flow/component/polymertemplate/NpmTemplateParserTest.java +++ b/flow-polymer-template/src/test/java/com/vaadin/flow/component/polymertemplate/NpmTemplateParserTest.java @@ -41,9 +41,6 @@ import com.vaadin.flow.server.frontend.FrontendUtils; import com.vaadin.flow.templatemodel.TemplateModel; -import static com.vaadin.flow.server.Constants.STATISTICS_JSON_DEFAULT; -import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES; - public class NpmTemplateParserTest { private MockVaadinServletService service; @@ -86,7 +83,7 @@ public void init() throws Exception { } @Test - public void should_FindCorrectDataInStats() { + public void should_FindCorrectDataInTemplate() { Mockito.when(configuration.isProductionMode()).thenReturn(true); TemplateParser instance = NpmTemplateParser.getInstance(); TemplateParser.TemplateData templateContent = instance @@ -108,14 +105,6 @@ public void should_FindCorrectDataInStats() { @Test public void getTemplateContent_polymer2TemplateStyleInsertion_contentParsedCorrectly() { - ResourceProvider resourceProvider = service.getContext() - .getAttribute(Lookup.class).lookup(ResourceProvider.class); - Mockito.when(resourceProvider.getApplicationResource( - VAADIN_SERVLET_RESOURCES + STATISTICS_JSON_DEFAULT)) - .thenReturn(NpmTemplateParser.class - .getResource("/" + VAADIN_SERVLET_RESOURCES - + "config/no-html-template.json")); - TemplateParser parser = NpmTemplateParser.getInstance(); TemplateData data = parser.getTemplateContent( NoHtmlTemplateContent.class, "no-html-template", service); @@ -128,14 +117,6 @@ public void getTemplateContent_polymer2TemplateStyleInsertion_contentParsedCorre @Test public void getTemplateContent_polymer2TemplateStyleInsertion_severalDomModules_correctTemplateContentIsChosen() { - ResourceProvider resourceProvider = service.getContext() - .getAttribute(Lookup.class).lookup(ResourceProvider.class); - Mockito.when(resourceProvider.getApplicationResource( - VAADIN_SERVLET_RESOURCES + STATISTICS_JSON_DEFAULT)) - .thenReturn(NpmTemplateParser.class - .getResource("/" + VAADIN_SERVLET_RESOURCES - + "config/no-html-template.json")); - TemplateParser parser = NpmTemplateParser.getInstance(); TemplateData data = parser.getTemplateContent( SeveralDomModulesTemplateContent.class, @@ -148,7 +129,7 @@ public void getTemplateContent_polymer2TemplateStyleInsertion_severalDomModules_ } @Test - public void shouldnt_UseStats_when_LocalFileTemplateExists() { + public void should_use_LocalFileTemplate() { TemplateParser instance = NpmTemplateParser.getInstance(); TemplateParser.TemplateData templateContent = instance .getTemplateContent(LikeableView.class, "likeable-element-view", @@ -187,18 +168,6 @@ public void getTypescriptTemplateContent_templateExists_getTemplateContent() { @Test(expected = IllegalStateException.class) public void should_throwException_when_LocalFileNotFound() { - ResourceProvider resourceProvider = service.getContext() - .getAttribute(Lookup.class).lookup(ResourceProvider.class); - Mockito.when(resourceProvider.getApplicationResource( - VAADIN_SERVLET_RESOURCES + STATISTICS_JSON_DEFAULT)) - .thenReturn(NpmTemplateParser.class - .getResource("/META-INF/resources/foo-bar.json")); - TemplateParser instance = NpmTemplateParser.getInstance(); - instance.getTemplateContent(FooView.class, "foo-view", service); - } - - @Test(expected = IllegalStateException.class) - public void should_throwException_when_ResourceNotFoundInStatsFile() { TemplateParser instance = NpmTemplateParser.getInstance(); instance.getTemplateContent(FooView.class, "foo-view", service); } @@ -248,14 +217,7 @@ public void nonLocalTemplate_shouldParseCorrectly() { } @Test - public void bableStats_shouldAlwaysParseCorrectly() { - ResourceProvider resourceProvider = service.getContext() - .getAttribute(Lookup.class).lookup(ResourceProvider.class); - Mockito.when(resourceProvider.getApplicationResource( - VAADIN_SERVLET_RESOURCES + STATISTICS_JSON_DEFAULT)) - .thenReturn(NpmTemplateParser.class - .getResource("/" + VAADIN_SERVLET_RESOURCES - + "config/babel_stats.json")); + public void shouldParseTemplateCorrectly() { TemplateParser instance = NpmTemplateParser.getInstance(); TemplateParser.TemplateData templateContent = instance .getTemplateContent(MyComponent.class, "my-component", service); @@ -308,13 +270,6 @@ public void bableStats_shouldAlwaysParseCorrectly() { */ @Test public void hierarchicalTemplate_templateHasChild_childHasCorrectPosition() { - ResourceProvider resourceProvider = service.getContext() - .getAttribute(Lookup.class).lookup(ResourceProvider.class); - Mockito.when(resourceProvider.getApplicationResource( - VAADIN_SERVLET_RESOURCES + STATISTICS_JSON_DEFAULT)) - .thenReturn(NpmTemplateParser.class - .getResource("/" + VAADIN_SERVLET_RESOURCES - + "config/template-in-template-stats.json")); TemplateParser instance = NpmTemplateParser.getInstance(); TemplateParser.TemplateData templateContent = instance .getTemplateContent(ParentTemplate.class, "parent-template", diff --git a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/babel_stats.json b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/babel_stats.json deleted file mode 100644 index 782aeae5c76..00000000000 --- a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/babel_stats.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "hash": "e251827e9f087691baf2", - "modules": [ - { - "name": "./my-component.js?babel-target=es6", - "source": "import { PolymerElement } from '@polymer/polymer/polymer-element.js';\nimport { html } from '@polymer/polymer/lib/utils/html-tag.js';\n\nclass MyComponentElement extends PolymerElement {\n static get template() {\n return html`\n \n

\n`;\n }\n\n static get is() {\n return 'my-component';\n }\n\n}\n\ncustomElements.define(MyComponentElement.is, MyComponentElement);" - } - ] -} \ No newline at end of file diff --git a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/no-html-template.json b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/no-html-template.json deleted file mode 100644 index 78424661fd6..00000000000 --- a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/no-html-template.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "modules": [ - { - "name": "./no-html-template.js", - "source": "/* innerHtml = ``; */ import { PolymerElement } from '@polymer/polymer/polymer-element.js'; const $_documentContainer = document.createElement('template'); $_documentContainer.innerHTML = ` `;" - }, - { - "name": "./several-dom-modules-template.js", - "source": "/* innerHtml = ``; */ import { PolymerElement } from '@polymer/polymer/polymer-element.js'; const $_documentContainer = document.createElement('template'); a.innerHtml =``;$_documentContainer.innerHTML = ' ';" - } - ], - "hash": "foo" -} diff --git a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/stats.json b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/stats.json deleted file mode 100644 index 09c428b4f99..00000000000 --- a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/stats.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "hash": "64bb80639ef116681818", - "assetsByChunkName" :{ - "bundle": "build/vaadin-bundle-1111.cache.js", - "export": "build/vaadin-export-2222.cache.js" - }, - "modules": [ - { - "name": "../node_modules/@vaadin/flow-frontend/src/hello-world.js", - "source": "import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';\nimport '@polymer/paper-input/paper-input.js';\n\nclass HelloWorld extends PolymerElement {\n static get template() {\n return html`\n
\n \n \n
[[greeting]]
\n
`;\n }\n\n static get is() {\n return 'hello-world';\n }\n\n}\n\ncustomElements.define(HelloWorld.is, HelloWorld);" - }, - { - "name": "../target/flow-frontend/src/hello-world2.js", - "source": "import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';\nimport '@polymer/paper-input/paper-input.js';\n\nclass HelloWorld extends PolymerElement {\n static get template() {\n return html`\n
\n \n \n
[[greeting]]
\n
`;\n }\n\n static get is() {\n return 'hello-world';\n }\n\n}\n\ncustomElements.define(HelloWorld.is, HelloWorld);" - }, - { - "name": "./frontend/LikeableElementBrokenHtml.js", - "source": "// Import an element\nimport '@polymer/paper-checkbox/paper-checkbox.js';\n\n// Import the PolymerElement base class and html helper\nimport {PolymerElement, html} from '@polymer/polymer';\n\n// Define an element class\nclass LikeableElement extends PolymerElement {\n\n // Define public API properties\n static get properties() { return { liked: Boolean }}\n\n // Define the element's template\n static get template() {\n return html`\n \n I like web components!\n\n
Web components like you, too.
\n `;\n }\n}\n\n// Register the element with the browser\ncustomElements.define('likeable-element', LikeableElement);" - }, - { - "id": "./frontend/my-form.ts", - "name": "./frontend/my-form.ts", - "source": "import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';\n\nclass MyFormElement extends PolymerElement {\n static get template() {\n return html`\n

Hello

\n `;\n }\n\n static get is() {\n return 'my-form';\n }\n\n}\n\ncustomElements.define(MyFormElement.is, MyFormElement);\r\n" - } - ] - , - - "chunks" : [ - { - "modules": [ - { - "name": "./frontend/LikeableElement.js", - "source": "// Import an element\nimport '@polymer/paper-checkbox/paper-checkbox.js';\n\n// Import the PolymerElement base class and html helper\nimport {PolymerElement, html} from '@polymer/polymer';\n\n// Define an element class\nclass LikeableElement extends PolymerElement {\n\n // Define public API properties\n static get properties() { return { liked: Boolean }}\n\n // Define the element's template\n static get template() {\n return html`\n \n
Tag name doesn't match the JS module name
I like web components!\n\n
Web components like you, too.
\n `;\n }\n}\n\n// Register the element with the browser\ncustomElements.define('likeable-element', LikeableElement);" - } - ] - } - ] -} diff --git a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/template-in-template-stats.json b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/template-in-template-stats.json deleted file mode 100644 index a4dfcb7970a..00000000000 --- a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/template-in-template-stats.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "hash": "a60ab8b9c567ee11738c", - "modules": [ - { - "name": "./ParentTemplate.js", - "source": "import { PolymerElement } from '@polymer/polymer/polymer-element.js';\nimport { html } from '@polymer/polymer/lib/utils/html-tag.js';\nimport './ChildTemplate.js';\n\nclass ParentTemplate extends PolymerElement {\n static get is() {\n return 'parent-template';\n }\n\n static get template() {\n return html`\n
Parent Template
\n
\n
Placeholder
\n \n \n \n
\n \n `;\n }\n\n}\n\ncustomElements.define(ParentTemplate.is, ParentTemplate);" - } - ] -} \ No newline at end of file diff --git a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/ParentTemplate.js b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/ParentTemplate.js new file mode 100644 index 00000000000..c651357c658 --- /dev/null +++ b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/ParentTemplate.js @@ -0,0 +1,30 @@ +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import './ChildTemplate.js'; + +class ParentTemplate extends PolymerElement { + static get is() { + return 'parent-template'; + } + + static get template() { + return html` +
Parent Template
+
+
Placeholder
+ + + +
+ + `; + } + +} + +customElements.define(ParentTemplate.is, ParentTemplate); diff --git a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/LikeableElement.js b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/LikeableElement.js new file mode 100644 index 00000000000..d6daa4abc5e --- /dev/null +++ b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/LikeableElement.js @@ -0,0 +1,31 @@ +// Import an element +import '@polymer/paper-checkbox/paper-checkbox.js'; + +// Import the PolymerElement base class and html helper +import {PolymerElement, html} from '@polymer/polymer'; + +// Define an element class +class LikeableElement extends PolymerElement { + + // Define public API properties + static get properties() { return { liked: Boolean }} + + // Define the element's template + static get template() { + return html` + +
Tag name doesn't match the JS module name
I like web components! + +
Web components like you, too.
+ `; + } +} + +// Register the element with the browser +customElements.define('likeable-element', LikeableElement); diff --git a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/LikeableElementBrokenHtml.js b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/LikeableElementBrokenHtml.js new file mode 100644 index 00000000000..a176b99a587 --- /dev/null +++ b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/LikeableElementBrokenHtml.js @@ -0,0 +1,30 @@ +// Import an element +import '@polymer/paper-checkbox/paper-checkbox.js'; + +// Import the PolymerElement base class and html helper +import {PolymerElement, html} from '@polymer/polymer'; + +// Define an element class +class LikeableElement extends PolymerElement { + + // Define public API properties + static get properties() { return { liked: Boolean }} + + // Define the element's template + static get template() { + return html` + + I like web components! + +
Web components like you, too.
+ `; + } +} + +// Register the element with the browser +customElements.define('likeable-element', LikeableElement); diff --git a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/my-form.ts b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/my-form.ts new file mode 100644 index 00000000000..7226de3fdae --- /dev/null +++ b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/frontend/my-form.ts @@ -0,0 +1,16 @@ +import { PolymerElement, html } from '@polymer/polymer/polymer-element.js'; + +class MyFormElement extends PolymerElement { + static get template() { + return html` +

Hello

+ `; + } + + static get is() { + return 'my-form'; + } + +} + +customElements.define(MyFormElement.is, MyFormElement); diff --git a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/my-component.js b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/my-component.js new file mode 100644 index 00000000000..9d5627a4cd5 --- /dev/null +++ b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/my-component.js @@ -0,0 +1,18 @@ +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; + +class MyComponentElement extends PolymerElement { + static get template() { + return html` + +
+`; + } + + static get is() { + return 'my-component'; + } + +} + +customElements.define(MyComponentElement.is, MyComponentElement); diff --git a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/no-html-template.js b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/no-html-template.js new file mode 100644 index 00000000000..486a7749c07 --- /dev/null +++ b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/no-html-template.js @@ -0,0 +1,7 @@ +/* innerHtml = ``; */ +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; +const $_documentContainer = document.createElement('template'); +$_documentContainer.innerHTML = + ` + + `; diff --git a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/several-dom-modules-template.js b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/several-dom-modules-template.js new file mode 100644 index 00000000000..e8063bbe2b1 --- /dev/null +++ b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/several-dom-modules-template.js @@ -0,0 +1,5 @@ +/* innerHtml = ``; */ +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; +const $_documentContainer = document.createElement('template'); +a.innerHtml =``; +$_documentContainer.innerHTML = ' '; diff --git a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/src/hello-world.js b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/src/hello-world.js new file mode 100644 index 00000000000..2e45151e58c --- /dev/null +++ b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/src/hello-world.js @@ -0,0 +1,20 @@ +import { PolymerElement, html } from '@polymer/polymer/polymer-element.js'; +import '@polymer/paper-input/paper-input.js'; + +class HelloWorld extends PolymerElement { + static get template() { + return html` +
+ + +
[[greeting]]
+
`; + } + + static get is() { + return 'hello-world'; + } + +} + +customElements.define(HelloWorld.is, HelloWorld); diff --git a/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/src/hello-world2.js b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/src/hello-world2.js new file mode 100644 index 00000000000..2e45151e58c --- /dev/null +++ b/flow-polymer-template/src/test/resources/META-INF/VAADIN/config/templates/src/hello-world2.js @@ -0,0 +1,20 @@ +import { PolymerElement, html } from '@polymer/polymer/polymer-element.js'; +import '@polymer/paper-input/paper-input.js'; + +class HelloWorld extends PolymerElement { + static get template() { + return html` +
+ + +
[[greeting]]
+
`; + } + + static get is() { + return 'hello-world'; + } + +} + +customElements.define(HelloWorld.is, HelloWorld); diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/DevModeHandler.java b/flow-server/src/main/java/com/vaadin/flow/internal/DevModeHandler.java index 04b7d0f6db5..0181dc61c5b 100644 --- a/flow-server/src/main/java/com/vaadin/flow/internal/DevModeHandler.java +++ b/flow-server/src/main/java/com/vaadin/flow/internal/DevModeHandler.java @@ -17,6 +17,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; @@ -72,4 +73,11 @@ boolean serveDevModeRequest(HttpServletRequest request, * Stop the dev-server. */ void stop(); + + /** + * Gets the project root folder. + * + * @return the project root folder + */ + File getProjectRoot(); } diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/Template.java b/flow-server/src/main/java/com/vaadin/flow/internal/Template.java new file mode 100644 index 00000000000..68a24546c91 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/internal/Template.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2021 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.internal; + +import java.io.Serializable; + +/** + * Marker interface for (Lit and Polymer) templates. All frontend files linked + * by implementors (with {@link com.vaadin.flow.component.dependency.JsModule}) + * will be copied to {@code META-INF/VAADIN/config/templates}. + * + * @author Vaadin Ltd + */ +public interface Template extends Serializable { +} diff --git a/flow-server/src/main/java/com/vaadin/flow/server/Constants.java b/flow-server/src/main/java/com/vaadin/flow/server/Constants.java index 2ee7d62ef0e..004d18210f5 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/Constants.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/Constants.java @@ -225,6 +225,13 @@ public final class Constants implements Serializable { public static final String STATISTICS_JSON_DEFAULT = Constants.VAADIN_CONFIGURATION + "stats.json"; + /** + * Default resource directory to place template sources in. This is used + * used for Vite production mode instead of a stats.json file. + */ + public static final String TEMPLATE_DIRECTORY = Constants.VAADIN_CONFIGURATION + + "templates/"; + /** * Name of the npm main file. */ diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java index d94a5d1ad59..0ecceaafc01 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java @@ -17,6 +17,7 @@ import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; @@ -438,52 +439,6 @@ public static ProcessBuilder createProcessBuilder(List command) { return processBuilder; } - /** - * Gets the content of the stats.json file produced by webpack. - * - * Note: Caches the stats.json when external stats is enabled - * or stats.json is provided from the class path. To clear the - * cache use {@link #clearCachedStatsContent(VaadinService)}. - * - * @param service - * the vaadin service. - * @return the content of the file as a string, null if not found. - * @throws IOException - * on error reading stats file. - */ - public static String getStatsContent(VaadinService service) - throws IOException { - DeploymentConfiguration config = service.getDeploymentConfiguration(); - InputStream content = null; - - try { - if (!config.isProductionMode() && config.enableDevServer()) { - Optional devModeHandler = DevModeHandlerManager - .getDevModeHandler(service); - if (!devModeHandler.isPresent()) { - throw new WebpackConnectionException( - "DevModeHandlerManager implementation missing. Include the " - + "com.vaadin:vaadin-dev-server dependency."); - } - content = getStatsFromWebpack(devModeHandler.get()); - } - - if (config.isStatsExternal()) { - content = getStatsFromExternalUrl(config.getExternalStatsUrl(), - service.getContext()); - } - - if (content == null) { - content = getStatsFromClassPath(service); - } - return content != null - ? IOUtils.toString(content, StandardCharsets.UTF_8) - : null; - } finally { - IOUtils.closeQuietly(content); - } - } - /** * Clears the stats.json cache within this * {@link VaadinContext}. @@ -549,44 +504,6 @@ private static InputStream getFileFromClassPath(VaadinService service, return stream; } - /** - * Get the latest hash for the stats file in development mode. This is - * requested from the webpack-dev-server. - *

- * In production mode and disabled dev server mode an empty string is - * returned. - * - * @param service - * the Vaadin service. - * @return hash string for the stats.json file, empty string if none found - * @throws IOException - * if an I/O error occurs while creating the input stream. - */ - public static String getStatsHash(VaadinService service) - throws IOException { - Optional devModeHandler = DevModeHandlerManager - .getDevModeHandler(service); - if (devModeHandler.isPresent()) { - HttpURLConnection statsConnection = devModeHandler.get() - .prepareConnection("/stats.hash", "GET"); - if (statsConnection - .getResponseCode() != HttpURLConnection.HTTP_OK) { - throw new WebpackConnectionException(String.format( - NO_CONNECTION, "getting the stats content hash.")); - } - return streamToString(statsConnection.getInputStream()) - .replaceAll("\"", ""); - } - - return ""; - } - - private static InputStream getStatsFromWebpack( - DevModeHandler devModeHandler) throws IOException { - return getResourceFromWebpack(devModeHandler, "/stats.json", - "downloading stats.json"); - } - private static InputStream getResourceFromWebpack( DevModeHandler devModeHandler, String resource, String exceptionMessage) throws IOException { @@ -699,6 +616,59 @@ private static InputStream getFileFromDevModeHandler( .getInputStream(); } + /** + * Get the contents of a frontend file from the running dev server. + * + * @param service + * the Vaadin service. + * @param path + * the file path. + * @return an input stream for reading the file contents; null if there is + * no such file or the dev server is not running. + */ + public static InputStream getFrontendFileFromDevModeHandler( + VaadinService service, String path) { + Optional devModeHandler = DevModeHandlerManager + .getDevModeHandler(service); + if (devModeHandler.isPresent()) { + try { + File frontendFile = resolveFrontendPath( + devModeHandler.get().getProjectRoot(), path); + return frontendFile == null ? null + : new FileInputStream(frontendFile); + } catch (IOException e) { + throw new UncheckedIOException("Error reading file " + path, e); + } + } + return null; + } + + /** + * Looks up the front file at the given path. If the path starts with + * {@code ./}, first look in {@code projectRoot/frontend}, then in + * {@code projectRoot/node_modules/@vaadin/flow-frontend}. If the path does + * not start with {@code ./}, look in {@code node_modules} instead. + * + * @param projectRoot + * the project root folder. + * @param path + * the file path. + * @return an existing {@link File} , or null if the file doesn't exist. + */ + public static File resolveFrontendPath(File projectRoot, String path) { + File localFrontendFolder = new File(projectRoot, + FrontendUtils.FRONTEND); + File nodeModulesFolder = new File(projectRoot, NODE_MODULES); + File flowFrontendFolder = new File(nodeModulesFolder, + "@vaadin/" + DEFAULT_FLOW_RESOURCES_FOLDER); + List candidateParents = path.startsWith("./") + ? Arrays.asList(localFrontendFolder, flowFrontendFolder) + : Arrays.asList(nodeModulesFolder, localFrontendFolder, + flowFrontendFolder); + return candidateParents.stream().map(parent -> new File(parent, path)) + .filter(File::exists).findFirst().orElse(null); + } + /** * Load the asset chunks from stats.json. We will only read the * file until we have reached the assetsByChunkName json and return that as diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java index b0e6e769b93..17643fb5c0f 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java @@ -110,6 +110,8 @@ public static class Builder implements Serializable { private boolean requireHomeNodeExec; + private boolean copyTemplates = false; + /** * Directory for npm and folders and files. */ @@ -348,6 +350,20 @@ public Builder copyResources(Set jars) { return this; } + /** + * Sets whether copy templates to + * {@code META-INF/VAADIN/config/templates}. + * + * @param copyTemplates + * whether to copy templates + * + * @return the builder + */ + public Builder copyTemplates(boolean copyTemplates) { + this.copyTemplates = copyTemplates; + return this; + } + /** * Sets whether to collect and package * {@link com.vaadin.flow.component.WebComponentExporter} dependencies. @@ -675,7 +691,8 @@ protected FeatureFlags getFeatureFlags() { TaskUpdateWebpack.class, TaskUpdateVite.class, TaskUpdateImports.class, - TaskUpdateThemeImport.class + TaskUpdateThemeImport.class, + TaskCopyTemplateFiles.class )); // @formatter:on @@ -803,6 +820,11 @@ private NodeTasks(Builder builder) { frontendDependencies.getThemeDefinition(), builder.frontendDirectory, builder.fusionClientAPIFolder)); } + + if (builder.copyTemplates) { + commands.add(new TaskCopyTemplateFiles(classFinder, + builder.npmFolder, builder.resourceOutputDirectory)); + } } private void addBootstrapTasks(Builder builder) { diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskCopyTemplateFiles.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskCopyTemplateFiles.java new file mode 100644 index 00000000000..719c2e49f2f --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskCopyTemplateFiles.java @@ -0,0 +1,82 @@ +/* + * Copyright 2000-2021 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.util.HashSet; +import java.util.Set; + +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.internal.Template; +import com.vaadin.flow.server.Constants; +import com.vaadin.flow.server.ExecutionFailedException; +import com.vaadin.flow.server.frontend.scanner.ClassFinder; + +/** + * Copies template files to the target folder so as to be available for parsing + * at runtime in production mode. + *

+ * For internal use only. May be renamed or removed in a future release. + */ +public class TaskCopyTemplateFiles implements FallibleCommand { + + private final ClassFinder classFinder; + private final File projectDirectory; + private final File resourceOutputDirectory; + + TaskCopyTemplateFiles(ClassFinder classFinder, File projectDirectory, + File resourceOutputDirectory) { + this.classFinder = classFinder; + this.projectDirectory = projectDirectory; + this.resourceOutputDirectory = resourceOutputDirectory; + } + + @Override + public void execute() throws ExecutionFailedException { + Set> classes = new HashSet<>(); + classes.addAll(classFinder.getSubTypesOf(Template.class)); + for (Class clazz : classes) { + for (JsModule jsmAnnotation : clazz + .getAnnotationsByType(JsModule.class)) { + String path = jsmAnnotation.value(); + File source = FrontendUtils + .resolveFrontendPath(projectDirectory, path); + if (source == null) { + throw new ExecutionFailedException( + "Unable to locate file " + path); + } + File templateDirectory = new File(resourceOutputDirectory, + Constants.TEMPLATE_DIRECTORY); + File target = new File(templateDirectory, path).getParentFile(); + target.mkdirs(); + try { + FileUtils.copyFileToDirectory(source, target); + } catch (IOException e) { + throw new ExecutionFailedException(e); + } + } + } + } + + Logger log() { + return LoggerFactory.getLogger(getClass()); + } +} diff --git a/flow-server/src/main/resources/webpack.generated.js b/flow-server/src/main/resources/webpack.generated.js index e0a62c4740d..7a4d34e57c3 100644 --- a/flow-server/src/main/resources/webpack.generated.js +++ b/flow-server/src/main/resources/webpack.generated.js @@ -236,15 +236,9 @@ module.exports = { devServer: { hot: false, // disable HMR client: false, // disable wds client as we handle reloads and errors better - // webpack-dev-server serves ./ , webpack-generated, and java webapp + // webpack-dev-server serves ./, webpack-generated, and java webapp static: [outputFolder, path.resolve(__dirname, 'src', 'main', 'webapp')], onAfterSetupMiddleware: function (devServer) { - devServer.app.get(`/stats.json`, function (req, res) { - res.json(stats); - }); - devServer.app.get(`/stats.hash`, function (req, res) { - res.json(stats.hash.toString()); - }); devServer.app.get(`/assetsByChunkName`, function (req, res) { res.json(stats.assetsByChunkName); }); diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendUtilsTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendUtilsTest.java index bd4be8caded..34c8303fd35 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendUtilsTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendUtilsTest.java @@ -289,42 +289,6 @@ public void parseManifestJson_returnsValidPaths() { manifestPaths.contains("/index.html")); } - @Test - public void getStatsContent_getStatsFromClassPath_delegateToGetApplicationResource() - throws IOException { - VaadinServletService service = mockServletService(); - - ResourceProvider provider = mockResourceProvider(service); - - FrontendUtils.getStatsContent(service); - - VaadinServlet servlet = service.getServlet(); - - Mockito.verify(provider).getApplicationResource("foo"); - } - - @Test - public void getStatsContent_getStatsFromDevServerWithNoImplementation_throwsException() { - VaadinServletService service = mockServletService(); - - DeploymentConfiguration config = Mockito - .mock(DeploymentConfiguration.class); - - Mockito.when(service.getDeploymentConfiguration()).thenReturn(config); - - Mockito.when(config.isProductionMode()).thenReturn(false); - Mockito.when(config.enableDevServer()).thenReturn(true); - - WebpackConnectionException exception = Assert.assertThrows( - WebpackConnectionException.class, - () -> FrontendUtils.getStatsContent(service)); - - Assert.assertEquals( - "DevModeHandlerManager implementation missing. Include " - + "the com.vaadin:vaadin-dev-server dependency.", - exception.getMessage()); - } - @Test public void getStatsAssetsByChunkName_getStatsFromClassPath_delegateToGetApplicationResource() throws IOException { @@ -339,54 +303,6 @@ public void getStatsAssetsByChunkName_getStatsFromClassPath_delegateToGetApplica Mockito.verify(provider).getApplicationResource("foo"); } - @Test - public void getStatsContent_getStatsFromClassPath_populatesStatsCache() - throws IOException, ServiceException { - VaadinService service = setupStatsAssetMocks("ValidStats.json"); - - assertNull("Stats cache should not be present", - service.getContext().getAttribute(CACHE_KEY)); - - // Populates cache - FrontendUtils.getStatsContent(service); - - assertNotNull("Stats cache should be created", - service.getContext().getAttribute(CACHE_KEY)); - } - - @Test // #10893 - public void newLineCharacterInJsonStats_readStreamIsJsonParsable() - throws IOException, ServiceException { - VaadinService service = setupStatsAssetMocks( - "specialCharacterValue.json"); - - assertNull("Stats cache should not be present", - service.getContext().getAttribute(CACHE_KEY)); - - // Load file from classpath and populate cache - String statsContent = FrontendUtils.getStatsContent(service); - - try { - // Json parsing should not throw for loaded stats. - Json.parse(statsContent); - } catch (JsonException jsonException) { - Assert.fail("Json loaded from class path was not parsable json"); - } - - assertNotNull("Stats cache should be created", - service.getContext().getAttribute(CACHE_KEY)); - - // Load cached stats. - statsContent = FrontendUtils.getStatsContent(service); - - try { - // Json parsing should not throw for cached stats. - Json.parse(statsContent); - } catch (JsonException jsonException) { - Assert.fail("Json loaded from cache was not parsable json"); - } - } - @Test public void getStatsAssetsByChunkName_getStatsFromClassPath_populatesStatsCache() throws IOException, ServiceException { @@ -413,7 +329,7 @@ public void clearCachedStatsContent_clearsCache() FrontendUtils.clearCachedStatsContent(service); // Populates cache - FrontendUtils.getStatsContent(service); + FrontendUtils.getStatsAssetsByChunkName(service); // Clears cache FrontendUtils.clearCachedStatsContent(service); diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeTasksExecutionTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeTasksExecutionTest.java index 0dfd08afdde..5c17434030e 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeTasksExecutionTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeTasksExecutionTest.java @@ -32,6 +32,7 @@ public void init() throws Exception { NodeTasks.Builder builder = new NodeTasks.Builder( Mockito.mock(Lookup.class), null, TARGET); builder.useV14Bootstrap(true); + builder.withProductionMode(false); nodeTasks = builder.build(); diff --git a/flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java b/flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java index 7391fc85f40..686703f3cd8 100644 --- a/flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java +++ b/flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java @@ -222,6 +222,7 @@ protected Stream getExcludedPatterns() { "com\\.vaadin\\.flow\\.server\\.frontend\\.TaskInstallWebpackPlugins", "com\\.vaadin\\.flow\\.server\\.frontend\\.TaskUpdateThemeImport", "com\\.vaadin\\.flow\\.server\\.frontend\\.EndpointGeneratorTaskFactory", + "com\\.vaadin\\.flow\\.server\\.frontend\\.TaskCopyTemplateFiles", // Node downloader classes "com\\.vaadin\\.flow\\.server\\.frontend\\.installer\\.DefaultArchiveExtractor", diff --git a/flow-tests/test-frontend/addon-with-templates/pom.xml b/flow-tests/test-frontend/addon-with-templates/pom.xml new file mode 100644 index 00000000000..09f52ee41b1 --- /dev/null +++ b/flow-tests/test-frontend/addon-with-templates/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + com.vaadin + test-frontend + 9.0-SNAPSHOT + + + addon-with-templates + Add-on containing a Lit template-based component + + + + + com.vaadin + flow-bom + ${project.version} + pom + import + + + + + + + com.vaadin + flow-server + + + com.vaadin + flow-lit-template + + + + + + diff --git a/flow-tests/test-frontend/addon-with-templates/src/main/java/org/vaadin/example/addon/AddonLitComponent.java b/flow-tests/test-frontend/addon-with-templates/src/main/java/org/vaadin/example/addon/AddonLitComponent.java new file mode 100644 index 00000000000..d01fc160ad5 --- /dev/null +++ b/flow-tests/test-frontend/addon-with-templates/src/main/java/org/vaadin/example/addon/AddonLitComponent.java @@ -0,0 +1,21 @@ +package org.vaadin.example.addon; + +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.littemplate.LitTemplate; +import com.vaadin.flow.component.template.Id; + +@Tag(AddonLitComponent.TAG) +@JsModule("./AddonLitComponent.ts") +public class AddonLitComponent extends LitTemplate { + + public static final String TAG = "addon-lit-component"; + + @Id("label") + private Span label; + + public void setLabel(String value) { + label.setText(value); + } +} diff --git a/flow-tests/test-frontend/addon-with-templates/src/main/resources/META-INF/frontend/AddonLitComponent.ts b/flow-tests/test-frontend/addon-with-templates/src/main/resources/META-INF/frontend/AddonLitComponent.ts new file mode 100644 index 00000000000..8487dea0311 --- /dev/null +++ b/flow-tests/test-frontend/addon-with-templates/src/main/resources/META-INF/frontend/AddonLitComponent.ts @@ -0,0 +1,12 @@ +import { html, LitElement } from 'lit'; + +class AddonLitComponent extends LitElement { + render() { + return html`

+

Add-on component

+ Default +
`; + } +} + +customElements.define('addon-lit-component', AddonLitComponent); diff --git a/flow-tests/test-frontend/pom.xml b/flow-tests/test-frontend/pom.xml index 8ee9e15ea5a..e2ee97035b9 100644 --- a/flow-tests/test-frontend/pom.xml +++ b/flow-tests/test-frontend/pom.xml @@ -12,6 +12,11 @@ pom + + addon-with-templates + vite-test-assets + + vite-basics vite-production diff --git a/flow-tests/test-frontend/vite-basics/frontend/templates/LitComponent.ts b/flow-tests/test-frontend/vite-basics/frontend/templates/LitComponent.ts new file mode 100644 index 00000000000..091c05ab623 --- /dev/null +++ b/flow-tests/test-frontend/vite-basics/frontend/templates/LitComponent.ts @@ -0,0 +1,12 @@ +import { html, LitElement } from 'lit'; + +class LitComponent extends LitElement { + render() { + return html`
+

Local Lit component

+ Default +
`; + } +} + +customElements.define('lit-component', LitComponent); diff --git a/flow-tests/test-frontend/vite-basics/frontend/templates/PolymerComponent.ts b/flow-tests/test-frontend/vite-basics/frontend/templates/PolymerComponent.ts new file mode 100644 index 00000000000..2313d0f8e1d --- /dev/null +++ b/flow-tests/test-frontend/vite-basics/frontend/templates/PolymerComponent.ts @@ -0,0 +1,17 @@ +import {PolymerElement,html} from '@polymer/polymer/polymer-element.js'; + +class PolymerComponent extends PolymerElement { + + static get template() { + return html`
+

Local Polymer component

+ Default +
`; + } + + static get is() { + return 'polymer-component'; + } +} + +customElements.define(PolymerComponent.is, PolymerComponent); diff --git a/flow-tests/test-frontend/vite-basics/pom.xml b/flow-tests/test-frontend/vite-basics/pom.xml index a0572e42c7c..8cc73c50114 100644 --- a/flow-tests/test-frontend/vite-basics/pom.xml +++ b/flow-tests/test-frontend/vite-basics/pom.xml @@ -38,6 +38,19 @@ flow-test-lumo ${project.version} + + com.vaadin + vite-test-assets + ${project.version} + + + com.vaadin + vite-test-assets + ${project.version} + test + tests + test-jar + @@ -67,30 +80,14 @@ + + org.apache.maven.plugins + maven-failsafe-plugin + + com.vaadin:vite-test-assets + + - - - production - - - - com.vaadin - flow-maven-plugin - - - - build-frontend - - - - - true - - - - - - diff --git a/flow-tests/test-frontend/vite-production/frontend/templates/LitComponent.ts b/flow-tests/test-frontend/vite-production/frontend/templates/LitComponent.ts new file mode 100644 index 00000000000..091c05ab623 --- /dev/null +++ b/flow-tests/test-frontend/vite-production/frontend/templates/LitComponent.ts @@ -0,0 +1,12 @@ +import { html, LitElement } from 'lit'; + +class LitComponent extends LitElement { + render() { + return html`
+

Local Lit component

+ Default +
`; + } +} + +customElements.define('lit-component', LitComponent); diff --git a/flow-tests/test-frontend/vite-production/frontend/templates/PolymerComponent.ts b/flow-tests/test-frontend/vite-production/frontend/templates/PolymerComponent.ts new file mode 100644 index 00000000000..2313d0f8e1d --- /dev/null +++ b/flow-tests/test-frontend/vite-production/frontend/templates/PolymerComponent.ts @@ -0,0 +1,17 @@ +import {PolymerElement,html} from '@polymer/polymer/polymer-element.js'; + +class PolymerComponent extends PolymerElement { + + static get template() { + return html`
+

Local Polymer component

+ Default +
`; + } + + static get is() { + return 'polymer-component'; + } +} + +customElements.define(PolymerComponent.is, PolymerComponent); diff --git a/flow-tests/test-frontend/vite-production/pom.xml b/flow-tests/test-frontend/vite-production/pom.xml index 1529935f6b4..f320d37c715 100644 --- a/flow-tests/test-frontend/vite-production/pom.xml +++ b/flow-tests/test-frontend/vite-production/pom.xml @@ -30,6 +30,19 @@ flow-test-lumo ${project.version} + + com.vaadin + vite-test-assets + ${project.version} + + + com.vaadin + vite-test-assets + ${project.version} + test + tests + test-jar + @@ -54,6 +67,13 @@ org.eclipse.jetty jetty-maven-plugin + + org.apache.maven.plugins + maven-failsafe-plugin + + com.vaadin:vite-test-assets + + diff --git a/flow-tests/test-frontend/vite-production/vite.config.ts b/flow-tests/test-frontend/vite-production/vite.config.ts new file mode 100644 index 00000000000..4d6a0222e3e --- /dev/null +++ b/flow-tests/test-frontend/vite-production/vite.config.ts @@ -0,0 +1,9 @@ +import { UserConfigFn } from 'vite'; +import { overrideVaadinConfig } from './vite.generated'; + +const customConfig: UserConfigFn = (env) => ({ + // Here you can add custom Vite parameters + // https://vitejs.dev/config/ +}); + +export default overrideVaadinConfig(customConfig); diff --git a/flow-tests/test-frontend/vite-production/vite.generated.ts b/flow-tests/test-frontend/vite-production/vite.generated.ts new file mode 100644 index 00000000000..f6829b47f8a --- /dev/null +++ b/flow-tests/test-frontend/vite-production/vite.generated.ts @@ -0,0 +1,119 @@ +/** + * NOTICE: this is an auto-generated file + * + * This file has been generated by the `flow:prepare-frontend` maven goal. + * This file will be overwritten on every run. Any custom changes should be made to vite.config.ts + */ +import path from 'path'; +import * as net from 'net'; + +import { processThemeResources } from '@vaadin/application-theme-plugin/theme-handle.js'; +import settings from './target/vaadin-dev-server-settings.json'; +import { UserConfigFn } from 'vite'; +import { defineConfig } from 'vite'; + +const frontendFolder = path.resolve(__dirname, settings.frontendFolder); +const themeFolder = path.resolve(frontendFolder, settings.themeFolder); +const buildFolder = path.resolve(__dirname, settings.frontendBundleOutput); + +const projectStaticAssetsFolders = [ + path.resolve(__dirname, 'src', 'main', 'resources', 'META-INF', 'resources'), + path.resolve(__dirname, 'src', 'main', 'resources', 'static'), + frontendFolder +]; + +// Folders in the project which can contain application themes +const themeProjectFolders = projectStaticAssetsFolders.map((folder) => path.resolve(folder, settings.themeFolder)); + +const themeOptions = { + devMode: false, + // The following matches folder 'target/flow-frontend/themes/' + // (not 'frontend/themes') for theme in JAR that is copied there + themeResourceFolder: path.resolve(__dirname, settings.themeResourceFolder), + themeProjectFolders: themeProjectFolders, + projectStaticAssetsOutputFolder: path.resolve(__dirname, settings.staticOutput), + frontendGeneratedFolder: path.resolve(frontendFolder, settings.generatedFolder) +}; + +// Block debug and trace logs. +console.trace = () => {}; +console.debug = () => {}; + +function updateTheme(contextPath: string) { + const themePath = path.resolve(themeFolder); + if (contextPath.startsWith(themePath)) { + const changed = path.relative(themePath, contextPath); + + console.debug('Theme file changed', changed); + + if (changed.startsWith(settings.themeName)) { + processThemeResources(themeOptions, console); + } + } +} + +function runWatchDog(watchDogPort) { + const client = net.Socket(); + client.setEncoding('utf8'); + client.on('error', function () { + console.log('Watchdog connection error. Terminating vite process...'); + client.destroy(); + process.exit(0); + }); + client.on('close', function () { + client.destroy(); + runWatchDog(watchDogPort); + }); + + client.connect(watchDogPort, 'localhost'); +} + +export const vaadinConfig: UserConfigFn = (env) => { + if (env.mode === 'development' && process.env.watchDogPort) { + // Open a connection with the Java dev-mode handler in order to finish + // vite when it exits or crashes. + runWatchDog(process.env.watchDogPort); + } + return { + root: 'frontend', + base: env.mode === 'production' ? '' : '/VAADIN/', + resolve: { + alias: { + themes: themeFolder, + Frontend: frontendFolder + } + }, + build: { + outDir: buildFolder, + assetsDir: 'VAADIN/build', + rollupOptions: { + input: { + main: path.resolve(frontendFolder, 'index.html'), + generated: path.resolve(frontendFolder, 'generated/vaadin.ts') + }, + output: { + // Produce only one chunk that gets imported into index.html + manualChunks: () => 'everything.js' + } + } + }, + plugins: [ + { + name: 'custom-theme', + config() { + processThemeResources(themeOptions, console); + }, + handleHotUpdate(context) { + updateTheme(path.resolve(context.file)); + } + } + ] + }; +}; + +export const overrideVaadinConfig = (customConfig: UserConfigFn) => { + return defineConfig((env) => ({ + ...vaadinConfig(env), + ...customConfig(env) + })); +}; diff --git a/flow-tests/test-frontend/vite-test-assets/pom.xml b/flow-tests/test-frontend/vite-test-assets/pom.xml new file mode 100644 index 00000000000..1a5ec339d9e --- /dev/null +++ b/flow-tests/test-frontend/vite-test-assets/pom.xml @@ -0,0 +1,96 @@ + + + + + + test-frontend + com.vaadin + 9.0-SNAPSHOT + + 4.0.0 + + vite-test-assets + jar + + Vite test assets + + + + + com.vaadin + flow-bom + ${project.version} + pom + import + + + + + + + junit + junit + + + com.vaadin + flow-lit-template + + + com.vaadin + flow-polymer-template + + + com.vaadin + flow-html-components-testbench + + + com.vaadin + addon-with-templates + ${project.version} + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + + ${project.artifactId} + + + + + test-jar + + + + + + + diff --git a/flow-tests/test-frontend/vite-test-assets/src/main/java/com/vaadin/viteapp/views/template/LitComponent.java b/flow-tests/test-frontend/vite-test-assets/src/main/java/com/vaadin/viteapp/views/template/LitComponent.java new file mode 100644 index 00000000000..418beee9621 --- /dev/null +++ b/flow-tests/test-frontend/vite-test-assets/src/main/java/com/vaadin/viteapp/views/template/LitComponent.java @@ -0,0 +1,21 @@ +package com.vaadin.viteapp.views.template; + +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.littemplate.LitTemplate; +import com.vaadin.flow.component.template.Id; + +@Tag(LitComponent.TAG) +@JsModule("./templates/LitComponent.ts") +public class LitComponent extends LitTemplate { + + public static final String TAG = "lit-component"; + + @Id("label") + private Span label; + + public void setLabel(String value) { + label.setText(value); + } +} diff --git a/flow-tests/test-frontend/vite-test-assets/src/main/java/com/vaadin/viteapp/views/template/PolymerComponent.java b/flow-tests/test-frontend/vite-test-assets/src/main/java/com/vaadin/viteapp/views/template/PolymerComponent.java new file mode 100644 index 00000000000..b1a02ff426a --- /dev/null +++ b/flow-tests/test-frontend/vite-test-assets/src/main/java/com/vaadin/viteapp/views/template/PolymerComponent.java @@ -0,0 +1,21 @@ +package com.vaadin.viteapp.views.template; + +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.polymertemplate.PolymerTemplate; +import com.vaadin.flow.component.template.Id; +import com.vaadin.flow.templatemodel.TemplateModel; + +@Tag(PolymerComponent.TAG) +@JsModule("./templates/PolymerComponent.ts") +public class PolymerComponent extends PolymerTemplate { + public static final String TAG = "polymer-component"; + + @Id("label") + private Span label; + + public void setLabel(String value) { + label.setText(value); + } +} diff --git a/flow-tests/test-frontend/vite-test-assets/src/main/java/com/vaadin/viteapp/views/template/TemplateView.java b/flow-tests/test-frontend/vite-test-assets/src/main/java/com/vaadin/viteapp/views/template/TemplateView.java new file mode 100644 index 00000000000..eff192b192e --- /dev/null +++ b/flow-tests/test-frontend/vite-test-assets/src/main/java/com/vaadin/viteapp/views/template/TemplateView.java @@ -0,0 +1,36 @@ +package com.vaadin.viteapp.views.template; + +import org.vaadin.example.addon.AddonLitComponent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Input; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.router.Route; + +@Route(TemplateView.ROUTE) +public class TemplateView extends Div { + + public static final String ROUTE = "template"; + + public TemplateView() { + LitComponent litComponent = new LitComponent(); + add(litComponent); + + PolymerComponent polymerComponent = new PolymerComponent(); + add(polymerComponent); + + AddonLitComponent addonLitComponent = new AddonLitComponent(); + add(addonLitComponent); + + Input setLabelInput = new Input(); + add(setLabelInput); + + NativeButton setLabelButton = new NativeButton("Set labels"); + setLabelButton.addClickListener(e -> { + String newLabel = setLabelInput.getValue(); + litComponent.setLabel(newLabel); + polymerComponent.setLabel(newLabel); + addonLitComponent.setLabel(newLabel); + }); + add(setLabelButton); + } +} diff --git a/flow-tests/test-frontend/vite-test-assets/src/test/java/com/vaadin/viteapp/TemplateIT.java b/flow-tests/test-frontend/vite-test-assets/src/test/java/com/vaadin/viteapp/TemplateIT.java new file mode 100644 index 00000000000..0fb11e21d2d --- /dev/null +++ b/flow-tests/test-frontend/vite-test-assets/src/test/java/com/vaadin/viteapp/TemplateIT.java @@ -0,0 +1,54 @@ +package com.vaadin.viteapp; + +import io.github.bonigarcia.wdm.WebDriverManager; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.vaadin.example.addon.AddonLitComponent; +import com.vaadin.flow.component.html.testbench.InputTextElement; +import com.vaadin.flow.component.html.testbench.NativeButtonElement; +import com.vaadin.flow.component.html.testbench.SpanElement; +import com.vaadin.flow.testutil.ChromeBrowserTest; +import com.vaadin.viteapp.views.template.LitComponent; +import com.vaadin.viteapp.views.template.PolymerComponent; +import com.vaadin.viteapp.views.template.TemplateView; + +public class TemplateIT extends ChromeBrowserTest { + @BeforeClass + public static void driver() { + WebDriverManager.chromedriver().setup(); + } + + @Before + public void openView() { + getDriver().get(getRootURL() + "/" + TemplateView.ROUTE); + waitForDevServer(); + getCommandExecutor().waitForVaadin(); + } + + @Test + public void testElementIdMapping() { + final String initialValue = "Default"; + + SpanElement litSpan = $(LitComponent.TAG).first().$(SpanElement.class) + .first(); + Assert.assertEquals(initialValue, litSpan.getText()); + + SpanElement polymerSpan = $(PolymerComponent.TAG).first() + .$(SpanElement.class).first(); + Assert.assertEquals(initialValue, polymerSpan.getText()); + + SpanElement addonLitSpan = $(AddonLitComponent.TAG).first() + .$(SpanElement.class).first(); + Assert.assertEquals(initialValue, addonLitSpan.getText()); + + final String newLabel = "New label"; + $(InputTextElement.class).first().setValue(newLabel); + $(NativeButtonElement.class).first().click(); + + Assert.assertEquals(newLabel, litSpan.getText()); + Assert.assertEquals(newLabel, polymerSpan.getText()); + Assert.assertEquals(newLabel, addonLitSpan.getText()); + } +} diff --git a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/AbstractDevServerRunner.java b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/AbstractDevServerRunner.java index ba0edbf3873..34caa893ded 100644 --- a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/AbstractDevServerRunner.java +++ b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/AbstractDevServerRunner.java @@ -235,8 +235,8 @@ protected void validateFiles() throws ExecutionFailedException { // Skip checks if we have a dev server already running File binary = getServerBinary(); File config = getServerConfig(); - if (!getNpmFolder().exists()) { - getLogger().warn("No project folder '{}' exists", getNpmFolder()); + if (!getProjectRoot().exists()) { + getLogger().warn("No project folder '{}' exists", getProjectRoot()); throw new ExecutionFailedException(START_FAILURE + " the target execution folder doesn't exist."); } @@ -308,7 +308,7 @@ protected void validateFiles() throws ExecutionFailedException { protected Process doStartDevServer() { ApplicationConfiguration config = getApplicationConfiguration(); ProcessBuilder processBuilder = new ProcessBuilder() - .directory(getNpmFolder()); + .directory(getProjectRoot()); boolean useHomeNodeExec = config.getBooleanProperty( InitParameters.REQUIRE_HOME_NODE_EXECUTABLE, false); @@ -318,7 +318,7 @@ protected Process doStartDevServer() { InitParameters.SERVLET_PARAMETER_GLOBAL_PNPM, false); FrontendToolsSettings settings = new FrontendToolsSettings( - getNpmFolder().getAbsolutePath(), + getProjectRoot().getAbsolutePath(), () -> FrontendUtils.getVaadinHomeDirectory().getAbsolutePath()); settings.setForceAlternativeNode(useHomeNodeExec); settings.setAutoUpdate(nodeAutoUpdate); @@ -339,7 +339,7 @@ protected Process doStartDevServer() { FrontendUtils.console(FrontendUtils.GREEN, START); if (getLogger().isDebugEnabled()) { getLogger().debug(FrontendUtils.commandToString( - getNpmFolder().getAbsolutePath(), command)); + getProjectRoot().getAbsolutePath(), command)); } processBuilder.command(command); @@ -421,12 +421,8 @@ protected void triggerLiveReload() { } } - /** - * Gets the project root folder. - * - * @return the project root folder - */ - protected File getNpmFolder() { + @Override + public File getProjectRoot() { return npmFolder; } diff --git a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/ViteHandler.java b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/ViteHandler.java index 5b74412964c..61307721faf 100644 --- a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/ViteHandler.java +++ b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/ViteHandler.java @@ -93,12 +93,12 @@ protected String getServerName() { @Override protected File getServerBinary() { - return new File(getNpmFolder(), VITE_SERVER); + return new File(getProjectRoot(), VITE_SERVER); } @Override protected File getServerConfig() { - return new File(getNpmFolder(), FrontendUtils.VITE_CONFIG); + return new File(getProjectRoot(), FrontendUtils.VITE_CONFIG); } @Override diff --git a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/WebpackHandler.java b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/WebpackHandler.java index 7e36f6b1a31..5c8def813d6 100644 --- a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/WebpackHandler.java +++ b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/WebpackHandler.java @@ -186,12 +186,12 @@ protected String getServerName() { @Override protected File getServerBinary() { - return new File(getNpmFolder(), WEBPACK_SERVER); + return new File(getProjectRoot(), WEBPACK_SERVER); } @Override protected File getServerConfig() { - return new File(getNpmFolder(), FrontendUtils.WEBPACK_CONFIG); + return new File(getProjectRoot(), FrontendUtils.WEBPACK_CONFIG); } @Override diff --git a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/WebpackHandlerTest.java b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/WebpackHandlerTest.java index 8536c88d5ab..6c21a02ad13 100644 --- a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/WebpackHandlerTest.java +++ b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/WebpackHandlerTest.java @@ -370,7 +370,7 @@ public void should_GetStatsJson_From_Webpack() throws Exception { waitForDevServer(); assertEquals(statsContent, - FrontendUtils.getStatsContent(vaadinService)); + FrontendUtils.getStatsAssetsByChunkName(vaadinService)); } @Test