Skip to content

Commit

Permalink
feat: esbuild admin watcher supports now multiple plugins or an compl…
Browse files Browse the repository at this point in the history
…ete project
  • Loading branch information
shyim committed Nov 10, 2023
1 parent fe90256 commit 3723044
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 60 deletions.
136 changes: 92 additions & 44 deletions cmd/extension/extension_admin_watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import (
_ "embed"
"encoding/json"
"fmt"
"github.com/FriendsOfShopware/shopware-cli/internal/asset"
"io"
"net/http"
"net/url"
"path/filepath"
"path"
"regexp"
"strings"
"time"
Expand All @@ -32,6 +33,9 @@ var (
uriRegExp = regexp.MustCompile(`(?m)uri:\s.*,`)
assetPathRegExp = regexp.MustCompile(`(?m)assetPath:\s.*`)
assetRegExp = regexp.MustCompile(`(?m)(src|href|content)="(https?.*\/bundles.*)"`)

extensionAssetRegExp = regexp.MustCompile(`(?m)/bundles/([a-z-]+)/static/(.*)$`)
extensionEsbuildRegExp = regexp.MustCompile(`(?m)/.shopware-cli/([a-z-]+)/(.*)$`)
)

//go:embed static/live-reload.js
Expand All @@ -45,51 +49,70 @@ var (
var extensionAdminWatchCmd = &cobra.Command{
Use: "admin-watch [path] [host]",
Short: "Builds assets for extensions",
Args: cobra.ExactArgs(2),
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
ext, err := extension.GetExtensionByFolder(args[0])
if err != nil {
return err
}
var sources []asset.Source

listenSplit := strings.Split(adminWatchListen, ":")
for _, extensionPath := range args[:len(args)-1] {
ext, err := extension.GetExtensionByFolder(extensionPath)
if err != nil {
sources = append(sources, extension.FindAssetSourcesOfProject(cmd.Context(), extensionPath)...)
continue
}

if len(listenSplit) != 2 {
return fmt.Errorf("listen should contain a colon")
sources = append(sources, extension.ConvertExtensionsToSources(cmd.Context(), []extension.Extension{ext})...)
}

if len(adminWatchURL) == 0 {
adminWatchURL = "http://localhost:" + listenSplit[1]
}
esbuildInstances := make(map[string]adminWatchExtension)

browserUrl, err := url.Parse(adminWatchURL)
if err != nil {
return err
}
for _, source := range sources {
options := esbuild.NewAssetCompileOptionsAdmin(source.Name, source.Path)
options.ProductionMode = false

name, _ := ext.GetName()
esbuildContext, err := esbuild.Context(cmd.Context(), options)

options := esbuild.NewAssetCompileOptionsAdmin(name, ext.GetPath())
options.ProductionMode = false
if err != nil {
return err
}

esbuildContext, esBuildError := esbuild.Context(cmd.Context(), options)
if err := esbuildContext.Watch(api.WatchOptions{}); err != nil {
return err
}

if esBuildError != nil && len(esBuildError.Errors) > 0 {
return err
watchServer, contextError := esbuildContext.Serve(api.ServeOptions{
Host: "127.0.0.1",
})

if contextError != nil {
return err
}

technicalName := esbuild.ToKebabCase(source.Name)
esbuildInstances[technicalName] = adminWatchExtension{
name: source.Name,
assetName: technicalName,
context: esbuildContext,
watchServer: watchServer,
staticDir: path.Join(source.Path, "Resources", "app", "static"),
}
}

if err := esbuildContext.Watch(api.WatchOptions{}); err != nil {
return err
listenSplit := strings.Split(adminWatchListen, ":")

if len(listenSplit) != 2 {
return fmt.Errorf("listen should contain a colon")
}

esbuildServer, err := esbuildContext.Serve(api.ServeOptions{
Host: "127.0.0.1",
})
if len(adminWatchURL) == 0 {
adminWatchURL = "http://localhost:" + listenSplit[1]
}

browserUrl, err := url.Parse(adminWatchURL)
if err != nil {
return err
}

targetShopUrl, err := url.Parse(strings.TrimSuffix(args[1], "/"))
targetShopUrl, err := url.Parse(strings.TrimSuffix(args[len(args)-1], "/"))
if err != nil {
return err
}
Expand Down Expand Up @@ -117,20 +140,20 @@ var extensionAdminWatchCmd = &cobra.Command{
return
}

// Serve the local static folder to the cdn url
assetPrefix := fmt.Sprintf(targetShopUrl.Path+"/bundles/%s/static/", strings.ToLower(name))
if strings.HasPrefix(req.URL.Path, assetPrefix) {
newFilePath := strings.TrimPrefix(req.URL.Path, assetPrefix)
assetMatching := extensionAssetRegExp.FindAllString(req.URL.Path, -1)

expectedLocation := filepath.Join(filepath.Dir(filepath.Dir(filepath.Join(ext.GetPath(), "Resources", "app", "administration", "src"))), "static", newFilePath)
if len(assetMatching) > 0 {
if ext, ok := esbuildInstances[assetMatching[0]]; ok {
assetPrefix := fmt.Sprintf(targetShopUrl.Path+"/bundles/%s/static/", ext.name)

http.ServeFile(w, req, expectedLocation)
return
http.ServeFile(w, req, path.Join(ext.staticDir, assetPrefix))
return
}
}

// Modify admin url index page to load anything from our watcher
if req.URL.Path == targetShopUrl.Path+"/admin" {
resp, err := http.Get(fmt.Sprintf("%s/admin", args[1]))
resp, err := http.Get(fmt.Sprintf("%s/admin", targetShopUrl.Scheme+schemeHostSeparator+targetShopUrl.Host))
if err != nil {
logging.FromContext(cmd.Context()).Errorf("proxy failed %v", err)
w.WriteHeader(http.StatusInternalServerError)
Expand Down Expand Up @@ -256,8 +279,16 @@ var extensionAdminWatchCmd = &cobra.Command{
bundleInfo.Bundles[name] = adminBundlesInfoAsset{Css: newCss, Js: newJS}
}

bundleInfo.Bundles[name] = adminBundlesInfoAsset{Css: []string{browserUrl.String() + "/extension.css"}, Js: []string{browserUrl.String() + "/extension.js"}}
bundleInfo.Bundles["live-reload"] = adminBundlesInfoAsset{Css: []string{}, Js: []string{browserUrl.String() + "/__internal-admin-proxy/live-reload.js"}}
for _, ext := range esbuildInstances {
bundleInfo.Bundles[ext.name] = adminBundlesInfoAsset{
Css: []string{fmt.Sprintf("%s/.shopware-cli/%s/extension.css", browserUrl.String(), ext.assetName)},
Js: []string{fmt.Sprintf("%s/.shopware-cli/%s/extension.js", browserUrl.String(), ext.assetName)},
LiveReload: true,
Name: ext.assetName,
}
}

bundleInfo.Bundles["ShopwareCLI"] = adminBundlesInfoAsset{Css: []string{}, Js: []string{browserUrl.String() + "/__internal-admin-proxy/live-reload.js"}}

newJson, err := json.Marshal(bundleInfo)
if err != nil {
Expand All @@ -274,10 +305,17 @@ var extensionAdminWatchCmd = &cobra.Command{
return
}

if req.URL.Path == "/extension.css" || req.URL.Path == "/extension.js" || req.URL.Path == "/esbuild" {
req.URL = &url.URL{Scheme: "http", Host: fmt.Sprintf("%s:%d", esbuildServer.Host, esbuildServer.Port), Path: req.URL.Path}
fwd.ServeHTTP(w, req)
return
esbuildMatch := extensionEsbuildRegExp.FindStringSubmatch(req.URL.Path)

if len(esbuildMatch) > 0 {
if ext, ok := esbuildInstances[esbuildMatch[1]]; ok {
req.URL = &url.URL{Scheme: "http", Host: fmt.Sprintf("%s:%d", ext.watchServer.Host, ext.watchServer.Port), Path: "/" + esbuildMatch[2]}
req.Host = req.URL.Host
req.RequestURI = req.URL.Path

fwd.ServeHTTP(w, req)
return
}
}

// let us forward this request to another server
Expand Down Expand Up @@ -323,6 +361,16 @@ type adminBundlesInfo struct {
}

type adminBundlesInfoAsset struct {
Css []string `json:"css"`
Js []string `json:"js"`
Css []string `json:"css"`
Js []string `json:"js"`
LiveReload bool `json:"liveReload"`
Name string `json:"name"`
}

type adminWatchExtension struct {
name string
assetName string
context api.BuildContext
watchServer api.ServeResult
staticDir string
}
41 changes: 27 additions & 14 deletions cmd/extension/static/live-reload.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
new EventSource('/esbuild').addEventListener('change', e => {
const { added, removed, updated } = JSON.parse(e.data)
const bundles = Shopware.State.get('context').app.config.bundles;

if (!added.length && !removed.length && updated.length === 1) {
for (const link of document.getElementsByTagName("link")) {
const url = new URL(link.href)
for (const bundleName of Object.keys(bundles)) {
const bundle = bundles[bundleName];

if (url.host === location.host && url.pathname === updated[0]) {
const next = link.cloneNode()
next.href = updated[0] + '?' + Math.random().toString(36).slice(2)
next.onload = () => link.remove()
link.parentNode.insertBefore(next, link.nextSibling)
return
if (bundle.liveReload !== true) {
continue;
}

new EventSource(`/.shopware-cli/${bundle.name}/esbuild`).addEventListener('change', e => {
const { added, removed, updated } = JSON.parse(e.data)

// patch the path of esbuild
updated[0] = `/.shopware-cli/${bundle.name}${updated[0]}`

if (!added.length && !removed.length && updated.length === 1) {
for (const link of document.getElementsByTagName("link")) {
const url = new URL(link.href)

if (url.host === location.host && url.pathname === updated[0]) {
const next = link.cloneNode()
next.href = updated[0] + '?' + Math.random().toString(36).slice(2)
next.onload = () => link.remove()
link.parentNode.insertBefore(next, link.nextSibling)
return
}
}
}
}

location.reload()
})
location.reload()
})
}
1 change: 1 addition & 0 deletions internal/esbuild/esbuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func getEsbuildOptions(ctx context.Context, options AssetCompileOptions) (*api.B
LogLevel: api.LogLevelWarning,
Plugins: []api.Plugin{newScssPlugin(ctx)},
Loader: map[string]api.Loader{
".html": api.LoaderText,
".twig": api.LoaderText,
".scss": api.LoaderCSS,
".css": api.LoaderCSS,
Expand Down
2 changes: 0 additions & 2 deletions internal/esbuild/sass_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ func newScssPlugin(ctx context.Context) api.Plugin {
logging.FromContext(ctx).Fatalln(err)
}

logging.FromContext(ctx).Infof("Using dart-sass binary %s", dartSassBinary)

start, err := godartsass.Start(godartsass.Options{
DartSassEmbeddedFilename: dartSassBinary,
Timeout: 0,
Expand Down

0 comments on commit 3723044

Please sign in to comment.