Skip to content

Commit 5bd7a92

Browse files
authored
Merge pull request #1668 from Haehnchen/feature/webencore
support webencore inside "encore_entry_*" twig functions
2 parents 264c6aa + ec866ec commit 5bd7a92

File tree

7 files changed

+340
-0
lines changed

7 files changed

+340
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package fr.adrienbrault.idea.symfony2plugin.templating.webpack;
2+
3+
import com.intellij.openapi.project.Project;
4+
import com.intellij.openapi.util.Pair;
5+
import com.intellij.openapi.util.io.StreamUtil;
6+
import com.intellij.openapi.vfs.VirtualFile;
7+
import com.intellij.psi.search.FilenameIndex;
8+
import com.intellij.psi.search.GlobalSearchScope;
9+
import org.jetbrains.annotations.NotNull;
10+
import org.json.simple.JSONObject;
11+
import org.json.simple.parser.JSONParser;
12+
import org.json.simple.parser.ParseException;
13+
14+
import java.io.IOException;
15+
import java.util.function.Consumer;
16+
import java.util.regex.Matcher;
17+
import java.util.regex.Pattern;
18+
19+
/**
20+
* @author Daniel Espendiller <daniel@espendiller.net>
21+
*/
22+
public class SymfonyWebpackUtil {
23+
24+
/**
25+
* - webpack.config.js
26+
* - entrypoints.json
27+
*/
28+
public static void visitAllEntryFileTypes(@NotNull Project project, @NotNull Consumer<Pair<VirtualFile, String>> consumer) {
29+
for (VirtualFile virtualFile : FilenameIndex.getVirtualFilesByName(project, "webpack.config.js", GlobalSearchScope.allScope(project))) {
30+
visitWebpackConfiguration(virtualFile, consumer);
31+
}
32+
33+
for (VirtualFile virtualFile : FilenameIndex.getVirtualFilesByName(project, "entrypoints.json", GlobalSearchScope.allScope(project))) {
34+
visitEntryPointJson(virtualFile, consumer);
35+
}
36+
}
37+
38+
/**
39+
* {
40+
* "entrypoints": {
41+
* "entry_foobar": {
42+
* "js": []
43+
* },
44+
* "entry_foobar_2": {
45+
* "js": []
46+
* }
47+
* }
48+
* }
49+
*/
50+
private static void visitEntryPointJson(@NotNull VirtualFile virtualFile, @NotNull Consumer<Pair<VirtualFile, String>> consumer) {
51+
String s;
52+
53+
try {
54+
s = StreamUtil.readText(virtualFile.getInputStream(), "UTF-8");
55+
} catch (IOException e) {
56+
return;
57+
}
58+
59+
Object parse;
60+
try {
61+
JSONParser jsonParser = new JSONParser();
62+
parse = jsonParser.parse(s);
63+
64+
} catch (ParseException e) {
65+
return;
66+
}
67+
68+
if (!(parse instanceof JSONObject)) {
69+
return;
70+
}
71+
72+
Object entrypoints = ((JSONObject) parse).get("entrypoints");
73+
74+
if (entrypoints instanceof JSONObject) {
75+
for (Object o : ((JSONObject) entrypoints).keySet()) {
76+
if (o instanceof String) {
77+
consumer.accept(Pair.create(virtualFile, (String) o));
78+
}
79+
}
80+
}
81+
}
82+
83+
/**
84+
* .addEntry('foobar', './assets/app.js')
85+
* .addEntry("foo")
86+
*/
87+
private static void visitWebpackConfiguration(@NotNull VirtualFile virtualFile, @NotNull Consumer<Pair<VirtualFile, String>> consumer) {
88+
String s;
89+
90+
try {
91+
s = StreamUtil.readText(virtualFile.getInputStream(), "UTF-8");
92+
} catch (IOException e) {
93+
return;
94+
}
95+
96+
// if adding "javascript" plugin; would resolve better but not for now
97+
Matcher matcher = Pattern.compile("addEntry\\s*\\(\\s*['\"]([^'\"]+)['\"]").matcher(s);
98+
while(matcher.find()){
99+
consumer.accept(Pair.create(virtualFile, matcher.group(1)));
100+
}
101+
}
102+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package fr.adrienbrault.idea.symfony2plugin.templating.webpack;
2+
3+
import com.intellij.codeInsight.lookup.LookupElementBuilder;
4+
import com.intellij.openapi.vfs.VfsUtil;
5+
import com.intellij.psi.PsiElement;
6+
import com.intellij.psi.PsiFile;
7+
import com.intellij.psi.PsiManager;
8+
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
9+
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
10+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.*;
11+
import fr.adrienbrault.idea.symfony2plugin.templating.TwigPattern;
12+
import fr.adrienbrault.idea.symfony2plugin.util.ProjectUtil;
13+
import org.apache.commons.lang.StringUtils;
14+
import org.jetbrains.annotations.NotNull;
15+
16+
import java.util.Collection;
17+
import java.util.Collections;
18+
import java.util.HashSet;
19+
20+
/**
21+
* - encore_entry_link_tags()
22+
* - encore_entry_script_tags()
23+
*
24+
* @author Daniel Espendiller <daniel@espendiller.net>
25+
*/
26+
public class WebpackEncoreGotoCompletionRegistrar implements GotoCompletionRegistrar {
27+
@Override
28+
public void register(@NotNull GotoCompletionRegistrarParameter registrar) {
29+
registrar.register(TwigPattern.getPrintBlockOrTagFunctionPattern("encore_entry_link_tags", "encore_entry_script_tags"), psiElement -> {
30+
if (!Symfony2ProjectComponent.isEnabled(psiElement)) {
31+
return null;
32+
}
33+
34+
return new WebpackEncoreGotoCompletionRegistrar.EncoreEntry(psiElement);
35+
});
36+
}
37+
38+
private static class EncoreEntry extends GotoCompletionProvider implements GotoCompletionProviderInterfaceEx {
39+
private EncoreEntry(@NotNull PsiElement element) {
40+
super(element);
41+
}
42+
43+
@NotNull
44+
public Collection<PsiElement> getPsiTargets(PsiElement element) {
45+
String contents = element.getText();
46+
47+
if(StringUtils.isBlank(contents)) {
48+
return Collections.emptyList();
49+
}
50+
51+
HashSet<PsiElement> targets = new HashSet<>();
52+
SymfonyWebpackUtil.visitAllEntryFileTypes(element.getProject(), pair -> {
53+
if (contents.equalsIgnoreCase(pair.second)) {
54+
PsiFile file = PsiManager.getInstance(getProject()).findFile(pair.getFirst());
55+
if (file != null) {
56+
targets.add(file);
57+
}
58+
}
59+
});
60+
61+
return targets;
62+
}
63+
64+
@Override
65+
public void getLookupElements(@NotNull GotoCompletionProviderLookupArguments arguments) {
66+
SymfonyWebpackUtil.visitAllEntryFileTypes(getProject(), pair ->
67+
{
68+
// ignore: "vendor/symonfy/.../tests/fixtures/build/entrypoints.json"
69+
String path = VfsUtil.getRelativePath(pair.first, ProjectUtil.getProjectDir(getElement()), '/');
70+
if (path != null) {
71+
String lowerCase = path.toLowerCase();
72+
73+
if (lowerCase.contains("/test/") || lowerCase.contains("/tests/")) {
74+
return;
75+
}
76+
}
77+
78+
LookupElementBuilder lookupElement = LookupElementBuilder.create(pair.getSecond())
79+
.withIcon(Symfony2Icons.SYMFONY)
80+
.withTypeText(pair.first.getName());
81+
82+
arguments.getResultSet().addElement(lookupElement);
83+
}
84+
);
85+
}
86+
}
87+
}

src/main/resources/META-INF/plugin.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@
590590
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.dic.TaggedParameterGotoCompletionRegistrar"/>
591591
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.templating.RenderParameterGotoCompletionRegistrar"/>
592592
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.security.AnnotationExpressionGotoCompletionRegistrar"/>
593+
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.templating.webpack.WebpackEncoreGotoCompletionRegistrar"/>
593594

594595
<TwigNamespaceExtension implementation="fr.adrienbrault.idea.symfony2plugin.templating.path.JsonFileIndexTwigNamespaces"/>
595596
<TwigNamespaceExtension implementation="fr.adrienbrault.idea.symfony2plugin.templating.path.ConfigAddPathTwigNamespaces"/>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.templating.webpack;
2+
3+
import fr.adrienbrault.idea.symfony2plugin.templating.webpack.SymfonyWebpackUtil;
4+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
5+
6+
import java.util.HashSet;
7+
import java.util.Set;
8+
9+
/**
10+
* @author Daniel Espendiller <daniel@espendiller.net>
11+
*/
12+
public class SymfonyWebpackUtilTest extends SymfonyLightCodeInsightFixtureTestCase {
13+
public void setUp() throws Exception {
14+
super.setUp();
15+
}
16+
17+
public String getTestDataPath() {
18+
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/webpack/fixtures";
19+
}
20+
21+
public void testVisitEntries() {
22+
myFixture.copyFileToProject("webpack.config.js");
23+
myFixture.copyFileToProject("entrypoints.json");
24+
25+
Set<String> entries = new HashSet<>();
26+
27+
SymfonyWebpackUtil.visitAllEntryFileTypes(myFixture.getProject(), pair -> entries.add(pair.second));
28+
assertContainsElements(entries, "foo", "foobar", "entry_foobar_2");
29+
}
30+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.templating.webpack;
2+
3+
import com.jetbrains.twig.TwigFileType;
4+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
5+
6+
/**
7+
* @author Daniel Espendiller <daniel@espendiller.net>
8+
* @see fr.adrienbrault.idea.symfony2plugin.templating.webpack.WebpackEncoreGotoCompletionRegistrar
9+
*/
10+
public class WebpackEncoreGotoCompletionRegistrarTest extends SymfonyLightCodeInsightFixtureTestCase {
11+
public void setUp() throws Exception {
12+
super.setUp();
13+
}
14+
15+
public String getTestDataPath() {
16+
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/webpack/fixtures";
17+
}
18+
19+
public void testVisitEntries() {
20+
myFixture.copyFileToProject("webpack.config.js");
21+
myFixture.copyFileToProject("entrypoints.json");
22+
23+
assertCompletionContains(TwigFileType.INSTANCE, "{{ encore_entry_link_tags('<caret>') }}", "foobar");
24+
assertCompletionContains(TwigFileType.INSTANCE, "{{ encore_entry_script_tags('<caret>') }}", "foobar");
25+
26+
assertCompletionContains(TwigFileType.INSTANCE, "{{ encore_entry_script_tags('<caret>') }}", "entry_foobar_2");
27+
}
28+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"entrypoints": {
3+
"entry_foobar": {
4+
"js": []
5+
},
6+
"entry_foobar_2": {
7+
"js": []
8+
},
9+
"mainEntry": {
10+
"js": [],
11+
"css": []
12+
}
13+
}
14+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const Encore = require('@symfony/webpack-encore');
2+
3+
// Manually configure the runtime environment if not already configured yet by the "encore" command.
4+
// It's useful when you use tools that rely on webpack.config.js file.
5+
if (!Encore.isRuntimeEnvironmentConfigured()) {
6+
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
7+
}
8+
9+
Encore
10+
// directory where compiled assets will be stored
11+
.setOutputPath('public/build/')
12+
// public path used by the web server to access the output path
13+
.setPublicPath('/build')
14+
// only needed for CDN's or sub-directory deploy
15+
//.setManifestKeyPrefix('build/')
16+
17+
/*
18+
* ENTRY CONFIG
19+
*
20+
* Each entry will result in one JavaScript file (e.g. app.js)
21+
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
22+
*/
23+
.addEntry('foobar', './assets/app.js')
24+
.addEntry("foo")
25+
26+
.enableVueLoader(() => {}, { runtimeCompilerBuild: false })
27+
28+
// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
29+
// .enableStimulusBridge('./assets/controllers.json')
30+
31+
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
32+
.splitEntryChunks()
33+
34+
// will require an extra script tag for runtime.js
35+
// but, you probably want this, unless you're building a single-page app
36+
.enableSingleRuntimeChunk()
37+
38+
/*
39+
* FEATURE CONFIG
40+
*
41+
* Enable & configure other features below. For a full
42+
* list of features, see:
43+
* https://symfony.com/doc/current/frontend.html#adding-more-features
44+
*/
45+
.cleanupOutputBeforeBuild()
46+
.enableBuildNotifications()
47+
.enableSourceMaps(!Encore.isProduction())
48+
// enables hashed filenames (e.g. app.abc123.css)
49+
.enableVersioning(Encore.isProduction())
50+
51+
.configureBabel((config) => {
52+
config.plugins.push('@babel/plugin-proposal-class-properties');
53+
})
54+
55+
// enables @babel/preset-env polyfills
56+
.configureBabelPresetEnv((config) => {
57+
config.useBuiltIns = 'usage';
58+
config.corejs = 3;
59+
})
60+
61+
// enables Sass/SCSS support
62+
//.enableSassLoader()
63+
64+
// uncomment if you use TypeScript
65+
//.enableTypeScriptLoader()
66+
67+
// uncomment if you use React
68+
//.enableReactPreset()
69+
70+
// uncomment to get integrity="..." attributes on your script & link tags
71+
// requires WebpackEncoreBundle 1.4 or higher
72+
//.enableIntegrityHashes(Encore.isProduction())
73+
74+
// uncomment if you're having problems with a jQuery plugin
75+
//.autoProvidejQuery()
76+
;
77+
78+
module.exports = Encore.getWebpackConfig();

0 commit comments

Comments
 (0)