Skip to content

Commit

Permalink
Add proper Media Type handling in js.Build
Browse files Browse the repository at this point in the history
See #732
  • Loading branch information
bep committed Jul 12, 2020
1 parent c53c609 commit e5c934b
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 142 deletions.
73 changes: 60 additions & 13 deletions hugolib/js_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package hugolib

import (
"os"
"os/exec"
"path/filepath"
"testing"

Expand All @@ -29,7 +30,7 @@ import (
"github.com/gohugoio/hugo/common/loggers"
)

func TestJS_Build(t *testing.T) {
func TestJSBuild(t *testing.T) {
if !isCI() {
t.Skip("skip (relative) long running modules test when running locally")
}
Expand All @@ -43,13 +44,44 @@ func TestJS_Build(t *testing.T) {

mainJS := `
import "./included";
import { toCamelCase } from "to-camel-case";
console.log("main");
`
console.log("To camel:", toCamelCase("space case"));
`
includedJS := `
console.log("included");
`

workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-test-babel")
jsxContent := `
import * as React from 'react'
import * as ReactDOM from 'react-dom'
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
`

tsContent := `function greeter(person: string) {
return "Hello, " + person;
}
let user = [0, 1, 2];
document.body.textContent = greeter(user);`

packageJSON := `{
"scripts": {},
"dependencies": {
"to-camel-case": "1.0.0"
}
}
`

workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-test-js")
c.Assert(err, qt.IsNil)
defer clean()

Expand All @@ -65,23 +97,38 @@ func TestJS_Build(t *testing.T) {
b.WithContent("p1.md", "")

b.WithTemplates("index.html", `
{{ $options := dict "minify" true }}
{{ $transpiled := resources.Get "js/main.js" | js.Build $options }}
Built: {{ $transpiled.Content | safeJS }}
`)
{{ $options := dict "minify" false "externals" (slice "react" "react-dom") }}
{{ $js := resources.Get "js/main.js" | js.Build $options }}
JS: {{ template "print" $js }}
{{ $jsx := resources.Get "js/myjsx.jsx" | js.Build $options }}
JSX: {{ template "print" $jsx }}
{{ $ts := resources.Get "js/myts.ts" | js.Build }}
TS: {{ template "print" $ts }}
{{ define "print" }}RelPermalink: {{.RelPermalink}}|MIME: {{ .MediaType }}|Content: {{ .Content | safeJS }}{{ end }}
`)

jsDir := filepath.Join(workDir, "assets", "js")
b.Assert(os.MkdirAll(jsDir, 0777), qt.IsNil)
b.Assert(os.Chdir(workDir), qt.IsNil)
b.WithSourceFile("package.json", packageJSON)
b.WithSourceFile("assets/js/main.js", mainJS)
b.WithSourceFile("assets/js/myjsx.jsx", jsxContent)
b.WithSourceFile("assets/js/myts.ts", tsContent)

b.WithSourceFile("assets/js/included.js", includedJS)

_, err = captureStdout(func() error {
return b.BuildE(BuildCfg{})
})
b.Assert(err, qt.IsNil)
out, err := exec.Command("npm", "install").CombinedOutput()
b.Assert(err, qt.IsNil, qt.Commentf(string(out)))

b.Build(BuildCfg{})

b.AssertFileContent("public/index.html", `
Built: (()=&gt;{console.log(&#34;included&#34;);console.log(&#34;main&#34;);})();
`)
console.log(&#34;included&#34;);
if (hasSpace.test(string))
const React = __toModule(require(&#34;react&#34;));
function greeter(person) {
`)

}
22 changes: 14 additions & 8 deletions media/mediaType.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ type Type struct {

Delimiter string `json:"delimiter"` // e.g. "."

// TODO(bep) make this a string to make it hashable + method
Suffixes []string `json:"suffixes"`

// Set when doing lookup by suffix.
Expand Down Expand Up @@ -130,13 +129,17 @@ var (
CSVType = Type{MainType: "text", SubType: "csv", Suffixes: []string{"csv"}, Delimiter: defaultDelimiter}
HTMLType = Type{MainType: "text", SubType: "html", Suffixes: []string{"html"}, Delimiter: defaultDelimiter}
JavascriptType = Type{MainType: "application", SubType: "javascript", Suffixes: []string{"js"}, Delimiter: defaultDelimiter}
JSONType = Type{MainType: "application", SubType: "json", Suffixes: []string{"json"}, Delimiter: defaultDelimiter}
RSSType = Type{MainType: "application", SubType: "rss", mimeSuffix: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter}
XMLType = Type{MainType: "application", SubType: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter}
SVGType = Type{MainType: "image", SubType: "svg", mimeSuffix: "xml", Suffixes: []string{"svg"}, Delimiter: defaultDelimiter}
TextType = Type{MainType: "text", SubType: "plain", Suffixes: []string{"txt"}, Delimiter: defaultDelimiter}
TOMLType = Type{MainType: "application", SubType: "toml", Suffixes: []string{"toml"}, Delimiter: defaultDelimiter}
YAMLType = Type{MainType: "application", SubType: "yaml", Suffixes: []string{"yaml", "yml"}, Delimiter: defaultDelimiter}
TypeScriptType = Type{MainType: "application", SubType: "typescript", Suffixes: []string{"ts"}, Delimiter: defaultDelimiter}
TSXType = Type{MainType: "text", SubType: "tsx", Suffixes: []string{"tsx"}, Delimiter: defaultDelimiter}
JSXType = Type{MainType: "text", SubType: "jsx", Suffixes: []string{"jsx"}, Delimiter: defaultDelimiter}

JSONType = Type{MainType: "application", SubType: "json", Suffixes: []string{"json"}, Delimiter: defaultDelimiter}
RSSType = Type{MainType: "application", SubType: "rss", mimeSuffix: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter}
XMLType = Type{MainType: "application", SubType: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter}
SVGType = Type{MainType: "image", SubType: "svg", mimeSuffix: "xml", Suffixes: []string{"svg"}, Delimiter: defaultDelimiter}
TextType = Type{MainType: "text", SubType: "plain", Suffixes: []string{"txt"}, Delimiter: defaultDelimiter}
TOMLType = Type{MainType: "application", SubType: "toml", Suffixes: []string{"toml"}, Delimiter: defaultDelimiter}
YAMLType = Type{MainType: "application", SubType: "yaml", Suffixes: []string{"yaml", "yml"}, Delimiter: defaultDelimiter}

// Common image types
PNGType = Type{MainType: "image", SubType: "png", Suffixes: []string{"png"}, Delimiter: defaultDelimiter}
Expand Down Expand Up @@ -165,6 +168,9 @@ var DefaultTypes = Types{
SASSType,
HTMLType,
JavascriptType,
TypeScriptType,
TSXType,
JSXType,
JSONType,
RSSType,
XMLType,
Expand Down
5 changes: 4 additions & 1 deletion media/mediaType_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ func TestDefaultTypes(t *testing.T) {
{CSVType, "text", "csv", "csv", "text/csv", "text/csv"},
{HTMLType, "text", "html", "html", "text/html", "text/html"},
{JavascriptType, "application", "javascript", "js", "application/javascript", "application/javascript"},
{TypeScriptType, "application", "typescript", "ts", "application/typescript", "application/typescript"},
{TSXType, "text", "tsx", "tsx", "text/tsx", "text/tsx"},
{JSXType, "text", "jsx", "jsx", "text/jsx", "text/jsx"},
{JSONType, "application", "json", "json", "application/json", "application/json"},
{RSSType, "application", "rss", "xml", "application/rss+xml", "application/rss+xml"},
{SVGType, "image", "svg", "svg", "image/svg+xml", "image/svg+xml"},
Expand All @@ -58,7 +61,7 @@ func TestDefaultTypes(t *testing.T) {

}

c.Assert(len(DefaultTypes), qt.Equals, 23)
c.Assert(len(DefaultTypes), qt.Equals, 26)

}

Expand Down
108 changes: 81 additions & 27 deletions resources/resource_transformers/js/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ import (
"fmt"
"io/ioutil"
"path"
"strings"

"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/internal"

"github.com/mitchellh/mapstructure"
Expand All @@ -28,22 +31,60 @@ import (
"github.com/gohugoio/hugo/resources/resource"
)

const defaultTarget = "esnext"

type Options struct {
// If not set, the source path will be used as the base target path.
// Note that the target path's extension may change if the target MIME type
// is different, e.g. when the source is TypeScript.
TargetPath string

// Whether to minify to output.
Minify bool

// The language target.
// One of: es2015, es2016, es2017, es2018, es2019, es2020 or esnext.
// Default is esnext.
Target string

// External dependencies, e.g. "react".
Externals []string `hash:"set"`

// What to use instead of React.createElement.
JSXFactory string

// What to use instead of React.Fragment.
JSXFragment string
}

type internalOptions struct {
TargetPath string
Minify bool
Externals []string
Target string
Loader string
Defines map[string]string
JSXFactory string
JSXFragment string
TSConfig string

Externals []string `hash:"set"`

// These are currently not exposed in the public Options struct,
// but added here to make the options hash as stable as possible for
// whenever we do.
Defines map[string]string
TSConfig string
}

func DecodeOptions(m map[string]interface{}) (opts Options, err error) {
if m == nil {
return
}
err = mapstructure.WeakDecode(m, &opts)

if opts.TargetPath != "" {
opts.TargetPath = helpers.ToSlashTrimLeading(opts.TargetPath)
}

opts.Target = strings.ToLower(opts.Target)

return
}

Expand All @@ -57,7 +98,7 @@ func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) *Client {
}

type buildTransformation struct {
options Options
options internalOptions
rs *resources.Spec
sfs *filesystems.SourceFilesystem
}
Expand All @@ -67,9 +108,17 @@ func (t *buildTransformation) Key() internal.ResourceTransformationKey {
}

func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
ctx.OutMediaType = media.JavascriptType

if t.options.TargetPath != "" {
ctx.OutPath = t.options.TargetPath
} else {
ctx.ReplaceOutPathExtension(".js")
}

var target api.Target
switch t.options.Target {
case "", "esnext":
case defaultTarget:
target = api.ESNext
case "es6", "es2015":
target = api.ES2015
Expand All @@ -88,29 +137,20 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
}

var loader api.Loader
switch t.options.Loader {
case "", "js":
switch ctx.InMediaType.SubType {
// TODO(bep) ESBuild support a set of other loaders, but I currently fail
// to see the relevance. That may change as we start using this.
case media.JavascriptType.SubType:
loader = api.LoaderJS
case "jsx":
loader = api.LoaderJSX
case "ts":
case media.TypeScriptType.SubType:
loader = api.LoaderTS
case "tsx":
case media.TSXType.SubType:
loader = api.LoaderTSX
case "json":
loader = api.LoaderJSON
case "text":
loader = api.LoaderText
case "base64":
loader = api.LoaderBase64
case "dataURL":
loader = api.LoaderDataURL
case "file":
loader = api.LoaderFile
case "binary":
loader = api.LoaderBinary
case media.JSXType.SubType:
loader = api.LoaderJSX
default:
return fmt.Errorf("invalid loader: %q", t.options.Loader)
return fmt.Errorf("unsupported Media Type: %q", ctx.InMediaType)

}

src, err := ioutil.ReadAll(ctx.From)
Expand Down Expand Up @@ -159,8 +199,22 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
return nil
}

func (c *Client) Process(res resources.ResourceTransformer, options Options) (resource.Resource, error) {
func (c *Client) Process(res resources.ResourceTransformer, opts Options) (resource.Resource, error) {
return res.Transform(
&buildTransformation{rs: c.rs, sfs: c.sfs, options: options},
&buildTransformation{rs: c.rs, sfs: c.sfs, options: toInternalOptions(opts)},
)
}

func toInternalOptions(opts Options) internalOptions {
target := opts.Target
if target == "" {
target = defaultTarget
}
return internalOptions{
TargetPath: opts.TargetPath,
Target: target,
Externals: opts.Externals,
JSXFactory: opts.JSXFactory,
JSXFragment: opts.JSXFragment,
}
}
Loading

0 comments on commit e5c934b

Please sign in to comment.