diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2020-09-09 22:31:43 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2020-09-13 20:55:29 +0200 |
commit | 85ba9bfffba9bfd0b095cb766f72700d4c211e31 (patch) | |
tree | 43b66efaafe4cb804234ca7273873ab949305799 /modules/npm | |
parent | 9df60b62f9c4e36a269f0c6e9a69bee9dc691031 (diff) |
Add "hugo mod npm pack"
This commit also introduces a convention where these common JS config files, including `package.hugo.json`, gets mounted into:
```
assets/_jsconfig
´``
These files mapped to their real filename will be added to the environment when running PostCSS, Babel etc., so you can do `process.env.HUGO_FILE_TAILWIND_CONFIG_JS` to resolve the real filename.
But do note that `assets` is a composite/union filesystem, so if your config file is not meant to be overridden, name them something specific.
This commit also adds adds `workDir/node_modules` to `NODE_PATH` and `HUGO_WORKDIR` to the env when running the JS tools above.
Fixes #7644
Fixes #7656
Fixes #7675
Diffstat (limited to 'modules/npm')
-rw-r--r-- | modules/npm/package_builder.go | 230 | ||||
-rw-r--r-- | modules/npm/package_builder_test.go | 95 |
2 files changed, 325 insertions, 0 deletions
diff --git a/modules/npm/package_builder.go b/modules/npm/package_builder.go new file mode 100644 index 000000000..23aac7246 --- /dev/null +++ b/modules/npm/package_builder.go @@ -0,0 +1,230 @@ +// Copyright 2020 The Hugo Authors. All rights reserved. +// +// 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 npm + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/gohugoio/hugo/hugofs/files" + + "github.com/pkg/errors" + + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/afero" + + "github.com/spf13/cast" + + "github.com/gohugoio/hugo/helpers" +) + +const ( + dependenciesKey = "dependencies" + devDependenciesKey = "devDependencies" + + packageJSONName = "package.json" + + packageJSONTemplate = `{ + "name": "%s", + "version": "%s" +}` +) + +func Pack(fs afero.Fs, fis []hugofs.FileMetaInfo) error { + + var b *packageBuilder + + // Have a package.hugo.json? + fi, err := fs.Stat(files.FilenamePackageHugoJSON) + if err != nil { + // Have a package.json? + fi, err = fs.Stat(packageJSONName) + if err != nil { + // Create one. + name := "project" + // Use the Hugo site's folder name as the default name. + // The owner can change it later. + rfi, err := fs.Stat("") + if err == nil { + name = rfi.Name() + } + packageJSONContent := fmt.Sprintf(packageJSONTemplate, name, "0.1.0") + if err = afero.WriteFile(fs, files.FilenamePackageHugoJSON, []byte(packageJSONContent), 0666); err != nil { + return err + } + fi, err = fs.Stat(files.FilenamePackageHugoJSON) + if err != nil { + return err + } + } + } + + meta := fi.(hugofs.FileMetaInfo).Meta() + masterFilename := meta.Filename() + f, err := meta.Open() + if err != nil { + return errors.Wrap(err, "npm pack: failed to open package file") + } + b = newPackageBuilder(meta.Module(), f) + f.Close() + + for _, fi := range fis { + if fi.IsDir() { + // We only care about the files in the root. + continue + } + + if fi.Name() != files.FilenamePackageHugoJSON { + continue + } + + meta := fi.(hugofs.FileMetaInfo).Meta() + + if meta.Filename() == masterFilename { + continue + } + + f, err := meta.Open() + if err != nil { + return errors.Wrap(err, "npm pack: failed to open package file") + } + b.Add(meta.Module(), f) + f.Close() + } + + if b.Err() != nil { + return errors.Wrap(b.Err(), "npm pack: failed to build") + } + + // Replace the dependencies in the original template with the merged set. + b.originalPackageJSON[dependenciesKey] = b.dependencies + b.originalPackageJSON[devDependenciesKey] = b.devDependencies + var commentsm map[string]interface{} + comments, found := b.originalPackageJSON["comments"] + if found { + commentsm = cast.ToStringMap(comments) + } else { + commentsm = make(map[string]interface{}) + } + commentsm[dependenciesKey] = b.dependenciesComments + commentsm[devDependenciesKey] = b.devDependenciesComments + b.originalPackageJSON["comments"] = commentsm + + // Write it out to the project package.json + packageJSONData, err := json.MarshalIndent(b.originalPackageJSON, "", " ") + if err != nil { + return errors.Wrap(err, "npm pack: failed to marshal JSON") + } + + if err := afero.WriteFile(fs, packageJSONName, packageJSONData, 0666); err != nil { + return errors.Wrap(err, "npm pack: failed to write package.json") + } + + return nil + +} + +func newPackageBuilder(source string, first io.Reader) *packageBuilder { + b := &packageBuilder{ + devDependencies: make(map[string]interface{}), + devDependenciesComments: make(map[string]interface{}), + dependencies: make(map[string]interface{}), + dependenciesComments: make(map[string]interface{}), + } + + m := b.unmarshal(first) + if b.err != nil { + return b + } + + b.addm(source, m) + b.originalPackageJSON = m + + return b +} + +type packageBuilder struct { + err error + + // The original package.hugo.json. + originalPackageJSON map[string]interface{} + + devDependencies map[string]interface{} + devDependenciesComments map[string]interface{} + dependencies map[string]interface{} + dependenciesComments map[string]interface{} +} + +func (b *packageBuilder) Add(source string, r io.Reader) *packageBuilder { + if b.err != nil { + return b + } + + m := b.unmarshal(r) + if b.err != nil { + return b + } + + b.addm(source, m) + + return b +} + +func (b *packageBuilder) addm(source string, m map[string]interface{}) { + if source == "" { + source = "project" + } + + // The version selection is currently very simple. + // We may consider minimal version selection or something + // after testing this out. + // + // But for now, the first version string for a given dependency wins. + // These packages will be added by order of import (project, module1, module2...), + // so that should at least give the project control over the situation. + if devDeps, found := m[devDependenciesKey]; found { + mm := cast.ToStringMapString(devDeps) + for k, v := range mm { + if _, added := b.devDependencies[k]; !added { + b.devDependencies[k] = v + b.devDependenciesComments[k] = source + } + } + } + + if deps, found := m[dependenciesKey]; found { + mm := cast.ToStringMapString(deps) + for k, v := range mm { + if _, added := b.dependencies[k]; !added { + b.dependencies[k] = v + b.dependenciesComments[k] = source + } + } + } + +} + +func (b *packageBuilder) unmarshal(r io.Reader) map[string]interface{} { + m := make(map[string]interface{}) + err := json.Unmarshal(helpers.ReaderToBytes(r), &m) + if err != nil { + b.err = err + } + return m +} + +func (b *packageBuilder) Err() error { + return b.err +} diff --git a/modules/npm/package_builder_test.go b/modules/npm/package_builder_test.go new file mode 100644 index 000000000..510a04776 --- /dev/null +++ b/modules/npm/package_builder_test.go @@ -0,0 +1,95 @@ +// Copyright 2020 The Hugo Authors. All rights reserved. +// +// 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 npm + +import ( + "strings" + "testing" + + qt "github.com/frankban/quicktest" +) + +const templ = `{ + "name": "foo", + "version": "0.1.1", + "scripts": {}, + "dependencies": { + "react-dom": "1.1.1", + "tailwindcss": "1.2.0", + "@babel/cli": "7.8.4", + "@babel/core": "7.9.0", + "@babel/preset-env": "7.9.5" + }, + "devDependencies": { + "postcss-cli": "7.1.0", + "tailwindcss": "1.2.0", + "@babel/cli": "7.8.4", + "@babel/core": "7.9.0", + "@babel/preset-env": "7.9.5" + } +}` + +func TestPackageBuilder(t *testing.T) { + c := qt.New(t) + + b := newPackageBuilder("", strings.NewReader(templ)) + c.Assert(b.Err(), qt.IsNil) + + b.Add("mymod", strings.NewReader(`{ +"dependencies": { + "react-dom": "9.1.1", + "add1": "1.1.1" +}, +"devDependencies": { + "tailwindcss": "error", + "add2": "2.1.1" +} +}`)) + + b.Add("mymod", strings.NewReader(`{ +"dependencies": { + "react-dom": "error", + "add1": "error", + "add3": "3.1.1" +}, +"devDependencies": { + "tailwindcss": "error", + "add2": "error", + "add4": "4.1.1" + +} +}`)) + + c.Assert(b.Err(), qt.IsNil) + + c.Assert(b.dependencies, qt.DeepEquals, map[string]interface{}{ + "@babel/cli": "7.8.4", + "add1": "1.1.1", + "add3": "3.1.1", + "@babel/core": "7.9.0", + "@babel/preset-env": "7.9.5", + "react-dom": "1.1.1", + "tailwindcss": "1.2.0", + }) + + c.Assert(b.devDependencies, qt.DeepEquals, map[string]interface{}{ + "tailwindcss": "1.2.0", + "@babel/cli": "7.8.4", + "@babel/core": "7.9.0", + "add2": "2.1.1", + "add4": "4.1.1", + "@babel/preset-env": "7.9.5", + "postcss-cli": "7.1.0", + }) +} |