Skip to content

Commit 324024d

Browse files
committed
Added auto importmap creation for actions
1 parent 84649ec commit 324024d

File tree

8 files changed

+136
-70
lines changed

8 files changed

+136
-70
lines changed

internal/app/action/action.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,20 @@ type Action struct {
6666
showValidate bool
6767
auditInsert func(*types.AuditEvent) error
6868
containerManager any // Container manager, if available, used to run commands in the container
69+
jsLibs []types.JSLibrary
6970
}
7071

7172
// NewAction creates a new action
7273
func NewAction(logger *types.Logger, sourceFS *appfs.SourceFs, isDev bool, name, description, apath string, run, suggest starlark.Callable,
7374
params []apptype.AppParam, paramValuesStr map[string]string, paramDict starlark.StringDict,
7475
appPath string, styleType types.StyleType, containerProxyUrl string, hidden []string, showValidate bool,
75-
auditInsert func(*types.AuditEvent) error, containerManager any) (*Action, error) {
76+
auditInsert func(*types.AuditEvent) error, containerManager any, jsLibs []types.JSLibrary) (*Action, error) {
7677

7778
funcMap := system.GetFuncMap()
7879

7980
funcMap["static"] = func(name string) string {
80-
fullPath := path.Join(appPath, sourceFS.HashName(name))
81+
staticPath := path.Join("static", name)
82+
fullPath := path.Join(appPath, sourceFS.HashName(staticPath))
8183
return fullPath
8284
}
8385

@@ -87,7 +89,8 @@ func NewAction(logger *types.Logger, sourceFS *appfs.SourceFs, isDev bool, name,
8789
}
8890

8991
funcMap["fileNonEmpty"] = func(name string) bool {
90-
fi, err := sourceFS.Stat(name)
92+
staticPath := path.Join("static", name)
93+
fi, err := sourceFS.Stat(staticPath)
9194
if err != nil {
9295
return false
9396
}
@@ -116,6 +119,10 @@ func NewAction(logger *types.Logger, sourceFS *appfs.SourceFs, isDev bool, name,
116119
hiddenParams[h] = true
117120
}
118121

122+
if len(jsLibs) == 0 {
123+
jsLibs = []types.JSLibrary{*&types.JSLibrary{}}
124+
}
125+
119126
return &Action{
120127
Logger: &appLogger,
121128
isDev: isDev,
@@ -135,6 +142,7 @@ func NewAction(logger *types.Logger, sourceFS *appfs.SourceFs, isDev bool, name,
135142
showValidate: showValidate,
136143
auditInsert: auditInsert,
137144
containerManager: containerManager,
145+
jsLibs: jsLibs,
138146
// Links, AppTemplate and Theme names are initialized later
139147
}, nil
140148
}
@@ -449,6 +457,7 @@ func (a *Action) execAction(w http.ResponseWriter, r *http.Request, isSuggest, i
449457
"path": a.pagePath,
450458
"lightTheme": a.LightTheme,
451459
"darkTheme": a.DarkTheme,
460+
"jsLibs": a.jsLibs,
452461
}
453462

454463
if !isHtmxRequest {
@@ -806,6 +815,7 @@ func (a *Action) getForm(w http.ResponseWriter, r *http.Request) {
806815
"hasFileUpload": hasFileUpload,
807816
"showSuggest": a.suggest != nil,
808817
"showValidate": a.showValidate,
818+
"jsLibs": a.jsLibs,
809819
}
810820
err := a.actionTemplate.ExecuteTemplate(w, "form.go.html", input)
811821
if err != nil {

internal/app/action/layout.go.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,20 @@
4747
<script src="{{ astatic "astatic/json.js" }}"></script>
4848
<script src="{{ astatic "astatic/toggle.js" }}"></script>
4949
<script src="{{ astatic "astatic/htmx.min.js" }}"></script>
50+
51+
{{ if gt (len .jsLibs) 0 }}
52+
<script type="importmap">
53+
{
54+
"imports": {
55+
{{ range $i, $lib := .jsLibs }}
56+
{{ $libPath := printf "%s%s" "gen/esm/" $lib.SanitizedFileName }}
57+
"{{ $lib.PackageName }}": "{{ static $libPath}}"
58+
{{ if not (eq $i (sub (len $.jsLibs) 1)) }},{{end}}
59+
{{ end }}
60+
}
61+
}
62+
</script>
63+
64+
{{end}}
65+
5066
{{ end -}}

internal/app/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ type App struct {
6969
template *template.Template // unstructured templates, no base_templates defined
7070
templateMap map[string]*template.Template // structured templates, base_templates defined
7171
staticOnly bool // app has only static files, no HTML routes
72+
jsLibs []types.JSLibrary // JS libraries used by the app
7273

7374
watcher *fsnotify.Watcher
7475
sseListeners []chan SSEMessage

internal/app/dev/appdev.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ type AppDev struct {
4545
AppStyle *AppStyle
4646

4747
filesDownloaded map[string][]string
48-
JsLibs []JSLibrary
49-
jsCache map[JSLibrary]string
48+
JsLibs []types.JSLibrary
49+
jsCache map[types.JSLibrary]string
5050
}
5151

5252
func NewAppDev(logger *types.Logger, sourceFS *appfs.WritableSourceFs, workFS *appfs.WorkFs, appStyle *AppStyle, systemConfig *types.SystemConfig) *AppDev {
@@ -57,8 +57,8 @@ func NewAppDev(logger *types.Logger, sourceFS *appfs.WritableSourceFs, workFS *a
5757
AppStyle: appStyle,
5858
systemConfig: systemConfig,
5959
filesDownloaded: make(map[string][]string),
60-
jsCache: make(map[JSLibrary]string),
61-
JsLibs: []JSLibrary{},
60+
jsCache: make(map[types.JSLibrary]string),
61+
JsLibs: []types.JSLibrary{},
6262
}
6363
return dev
6464
}
@@ -102,11 +102,11 @@ func (a *AppDev) SetupJsLibs() error {
102102
hasHtmx := false
103103
hasHtmxSSE := false
104104
for _, jsLib := range a.JsLibs {
105-
if jsLib.libType == Library {
106-
if strings.Contains(jsLib.directUrl, "htmx.org") && !strings.Contains(jsLib.directUrl, "ext") {
105+
if jsLib.LibType == types.Library {
106+
if strings.Contains(jsLib.DirectUrl, "htmx.org") && !strings.Contains(jsLib.DirectUrl, "ext") {
107107
hasHtmx = true
108108
}
109-
if strings.Contains(jsLib.directUrl, "htmx.org") && strings.Contains(jsLib.directUrl, "ext/sse.js") {
109+
if strings.Contains(jsLib.DirectUrl, "htmx.org") && strings.Contains(jsLib.DirectUrl, "ext/sse.js") {
110110
hasHtmxSSE = true
111111
}
112112
}
@@ -126,7 +126,8 @@ func (a *AppDev) SetupJsLibs() error {
126126
continue
127127
}
128128

129-
targetFile, err := jsLib.Setup(a, a.sourceFS, a.workFS)
129+
jsLibManager := JsLibManager{jsLib}
130+
targetFile, err := jsLibManager.Setup(a, a.sourceFS, a.workFS)
130131
if err != nil {
131132
if targetFile == "" {
132133
// Setup failed and cannot check if file exists, error out

internal/app/dev/jslibs.go

Lines changed: 35 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,78 +11,61 @@ import (
1111
"strings"
1212

1313
"github.com/claceio/clace/internal/app/appfs"
14+
"github.com/claceio/clace/internal/types"
1415
"github.com/evanw/esbuild/pkg/api"
1516
"github.com/evanw/esbuild/pkg/cli"
1617
)
1718

18-
type LibraryType string
19-
20-
const (
21-
ESModule LibraryType = "ecmascript_module"
22-
Library LibraryType = "library"
23-
)
24-
25-
const (
26-
LIB_PATH = "static/gen/lib"
27-
ESM_PATH = "static/gen/esm"
28-
)
29-
30-
// JSLibrary handles the downloading for JS libraries and esbuild based bundling for ESM libraries
31-
type JSLibrary struct {
32-
libType LibraryType
33-
directUrl string
34-
packageName string
35-
version string
36-
esbuildArgs [10]string // use an array so that the struct can be used as key in the jsCache map
37-
sanitizedFileName string
38-
}
39-
40-
func NewLibrary(url string) *JSLibrary {
41-
j := JSLibrary{
42-
libType: Library,
43-
directUrl: url,
44-
sanitizedFileName: sanitizeFileName(url),
19+
func NewLibrary(url string) *types.JSLibrary {
20+
j := types.JSLibrary{
21+
LibType: types.Library,
22+
DirectUrl: url,
23+
SanitizedFileName: sanitizeFileName(url),
4524
}
4625
return &j
4726
}
4827

49-
func NewLibraryESM(packageName string, version string, esbuildArgs []string) *JSLibrary {
28+
func NewLibraryESM(packageName string, version string, esbuildArgs []string) *types.JSLibrary {
5029
args := [10]string{}
5130
if esbuildArgs != nil {
5231
copy(args[:], esbuildArgs)
5332
}
5433

55-
j := JSLibrary{
56-
libType: ESModule,
57-
packageName: packageName,
58-
version: version,
59-
esbuildArgs: args,
60-
sanitizedFileName: sanitizeFileName(packageName) + "-" + version + ".js",
34+
j := types.JSLibrary{
35+
LibType: types.ESModule,
36+
PackageName: packageName,
37+
Version: version,
38+
EsbuildArgs: args,
39+
SanitizedFileName: sanitizeFileName(packageName) + "-" + version + ".js",
6140
}
6241
return &j
6342
}
6443

65-
func (j *JSLibrary) Setup(dev *AppDev, sourceFS *appfs.WritableSourceFs, workFS *appfs.WorkFs) (string, error) {
66-
if j.libType == Library {
67-
targetFile := path.Join(LIB_PATH, j.sanitizedFileName)
44+
type JsLibManager struct {
45+
types.JSLibrary
46+
}
47+
48+
func (j *JsLibManager) Setup(dev *AppDev, sourceFS *appfs.WritableSourceFs, workFS *appfs.WorkFs) (string, error) {
49+
if j.LibType == types.Library {
50+
targetFile := path.Join(types.LIB_PATH, j.SanitizedFileName)
6851
targetDir := path.Dir(targetFile)
6952
if err := os.MkdirAll(targetDir, 0755); err != nil {
70-
return "", fmt.Errorf("error creating directory %s : %s", LIB_PATH, err)
53+
return "", fmt.Errorf("error creating directory %s : %s", types.LIB_PATH, err)
7154
}
72-
if err := dev.downloadFile(j.directUrl, sourceFS, targetFile); err != nil {
73-
return "", fmt.Errorf("error downloading %s : %s", j.directUrl, err)
55+
if err := dev.downloadFile(j.DirectUrl, sourceFS, targetFile); err != nil {
56+
return "", fmt.Errorf("error downloading %s : %s", j.DirectUrl, err)
7457
}
7558
return targetFile, nil
76-
} else if j.libType == ESModule {
59+
} else if j.LibType == types.ESModule {
7760
return j.setupEsbuild(dev, sourceFS, workFS)
7861
} else {
79-
return "", fmt.Errorf("invalid library type : %s", j.libType)
62+
return "", fmt.Errorf("invalid library type : %s", j.LibType)
8063
}
8164
}
8265

83-
func (j *JSLibrary) setupEsbuild(dev *AppDev, sourceFS *appfs.WritableSourceFs, workFS *appfs.WorkFs) (string, error) {
84-
targetDir := path.Join(sourceFS.Root, ESM_PATH)
85-
targetFile := path.Join(targetDir, j.sanitizedFileName)
66+
func (j *JsLibManager) setupEsbuild(dev *AppDev, sourceFS *appfs.WritableSourceFs, workFS *appfs.WorkFs) (string, error) {
67+
targetDir := path.Join(sourceFS.Root, types.ESM_PATH)
68+
targetFile := path.Join(targetDir, j.SanitizedFileName)
8669

8770
sourceFile, err := j.generateSourceFile(workFS)
8871
if err != nil {
@@ -92,7 +75,7 @@ func (j *JSLibrary) setupEsbuild(dev *AppDev, sourceFS *appfs.WritableSourceFs,
9275
esbuildArgs := []string{sourceFile, "--bundle", "--format=esm"}
9376

9477
args := []string{}
95-
for _, arg := range j.esbuildArgs {
78+
for _, arg := range j.EsbuildArgs {
9679
if arg == "" {
9780
break
9881
}
@@ -135,11 +118,11 @@ func (j *JSLibrary) setupEsbuild(dev *AppDev, sourceFS *appfs.WritableSourceFs,
135118
if len(result.Errors) > 0 {
136119
// Return the target file name. The caller can check if the file exists to determine if the
137120
// setup was successful even though this step failed
138-
dev.Error().Msgf("error building %s : %v", j.packageName, result.Warnings)
139-
return targetFile, fmt.Errorf("error building %s : %v", j.packageName, result.Errors)
121+
dev.Error().Msgf("error building %s : %v", j.PackageName, result.Warnings)
122+
return targetFile, fmt.Errorf("error building %s : %v", j.PackageName, result.Errors)
140123
}
141124
if len(result.Warnings) > 0 {
142-
dev.Warn().Msgf("warning building %s : %v", j.packageName, result.Warnings)
125+
dev.Warn().Msgf("warning building %s : %v", j.PackageName, result.Warnings)
143126
}
144127

145128
for _, file := range result.OutputFiles {
@@ -153,9 +136,9 @@ func (j *JSLibrary) setupEsbuild(dev *AppDev, sourceFS *appfs.WritableSourceFs,
153136
return options.Outfile, nil
154137
}
155138

156-
func (j *JSLibrary) generateSourceFile(workFS *appfs.WorkFs) (string, error) {
157-
sourceFileName := j.sanitizedFileName
158-
sourceContent := fmt.Sprintf(`export * from "%s"`, j.packageName)
139+
func (j *JsLibManager) generateSourceFile(workFS *appfs.WorkFs) (string, error) {
140+
sourceFileName := j.SanitizedFileName
141+
sourceContent := fmt.Sprintf(`export * from "%s"`, j.PackageName)
159142
if err := workFS.Write(sourceFileName, []byte(sourceContent)); err != nil {
160143
return "", fmt.Errorf("error writing source file %s : %s", sourceFileName, err)
161144
}

internal/app/setup.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,14 @@ func (a *App) loadStarlarkConfig(dryRun types.DryRun) error {
8383
return err
8484
}
8585

86+
a.jsLibs, err = a.loadLibraryInfo()
87+
if err != nil {
88+
return err
89+
}
90+
8691
if a.IsDev {
8792
a.appDev.CustomLayout = a.CustomLayout
88-
a.appDev.JsLibs, err = a.loadLibraryInfo()
89-
if err != nil {
90-
return err
91-
}
93+
a.appDev.JsLibs = a.jsLibs
9294
}
9395

9496
var settingsMap map[string]interface{}
@@ -554,7 +556,7 @@ func (a *App) addAction(count int, val starlark.Value, router *chi.Mux) (err err
554556
}
555557
action, err := action.NewAction(a.Logger, a.sourceFS, a.IsDev, name, description, path, run, suggest,
556558
slices.Collect(maps.Values(a.paramInfo)), a.paramValuesStr, a.paramDict, a.Path, a.appStyle.GetStyleType(),
557-
containerProxyUrl, hidden, showValidate, a.auditInsert, a.containerManager)
559+
containerProxyUrl, hidden, showValidate, a.auditInsert, a.containerManager, a.jsLibs)
558560
if err != nil {
559561
return fmt.Errorf("error creating action %s: %w", name, err)
560562
}
@@ -1056,7 +1058,7 @@ func (a *App) userFileHandler(w http.ResponseWriter, r *http.Request) {
10561058
}
10571059
}
10581060

1059-
func (a *App) loadLibraryInfo() ([]dev.JSLibrary, error) {
1061+
func (a *App) loadLibraryInfo() ([]types.JSLibrary, error) {
10601062
lib, err := a.appDef.Attr("libraries")
10611063
if err != nil {
10621064
return nil, err
@@ -1073,7 +1075,7 @@ func (a *App) loadLibraryInfo() ([]dev.JSLibrary, error) {
10731075
return nil, fmt.Errorf("libraries is not a list")
10741076
}
10751077

1076-
libraries := []dev.JSLibrary{}
1078+
libraries := []types.JSLibrary{}
10771079
iter := libList.Iterate()
10781080
var libValue starlark.Value
10791081
count := 0

internal/app/tests/appaction_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,37 @@ app = ace.app("testApp",
613613
<div id="action_result" hx-swap-oob="innerHTML"> <output role="alert"> </output> </div>html/template: "custom" is undefined`, response.Body.String())
614614
}
615615

616+
func TestCustomESMImport(t *testing.T) {
617+
logger := testutil.TestLogger()
618+
fileData := map[string]string{
619+
"app.star": `
620+
def handler(dry_run, args):
621+
return ace.result(status="done", values=[{"a": 1, "b": "abc"}], report="custom")
622+
623+
app = ace.app("testApp",
624+
actions=[ace.action("testAction", "/", handler)],
625+
libraries=[ace.library("d3", "2.3"), ace.library("e4", "3.4")])
626+
`,
627+
"params.star": `param("param1", description="param1 description", type=STRING, default="myvalue")`,
628+
"myfile.go.html": `{{block "custom" .}} customdata {{end}}`,
629+
"static/gen/esm/d3-2.3.js": "abc",
630+
"static/gen/esm/e4-3.4.js": "def",
631+
}
632+
a, _, err := CreateTestApp(logger, fileData)
633+
if err != nil {
634+
t.Fatalf("Error %s", err)
635+
}
636+
637+
request := httptest.NewRequest("GET", "/test/", nil)
638+
response := httptest.NewRecorder()
639+
a.ServeHTTP(response, request)
640+
641+
testutil.AssertEqualsInt(t, "code", 200, response.Code)
642+
testutil.AssertStringContains(t, response.Body.String(), "script type=\"importmap\"")
643+
testutil.AssertStringContains(t, response.Body.String(), "\"d3\": \"/test/static/gen/esm/d3-2-ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.3.js\"")
644+
testutil.AssertStringContains(t, response.Body.String(), "\"e4\": \"/test/static/gen/esm/e4-3-cb8379ac2098aa165029e3938a51da0bcecfc008fd6795f401178647f96c5b34.4.js\"")
645+
}
646+
616647
func TestParamOptions(t *testing.T) {
617648
logger := testutil.TestLogger()
618649
fileData := map[string]string{

internal/types/types.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,3 +638,25 @@ type AppUpdateMessage struct {
638638
MessageType string `json:"message_type"`
639639
Payload AppUpdatePayload `json:"payload"`
640640
}
641+
642+
type LibraryType string
643+
644+
const (
645+
ESModule LibraryType = "ecmascript_module"
646+
Library LibraryType = "library"
647+
)
648+
649+
const (
650+
LIB_PATH = "static/gen/lib"
651+
ESM_PATH = "static/gen/esm"
652+
)
653+
654+
// JSLibrary handles the downloading for JS libraries and esbuild based bundling for ESM libraries
655+
type JSLibrary struct {
656+
LibType LibraryType
657+
DirectUrl string
658+
PackageName string
659+
Version string
660+
EsbuildArgs [10]string // use an array so that the struct can be used as key in the jsCache map
661+
SanitizedFileName string
662+
}

0 commit comments

Comments
 (0)