Skip to content

Commit

Permalink
Merge pull request #2466 from posit-dev/mnv-rmd-sites
Browse files Browse the repository at this point in the history
Improved support for RMarkdown sites
  • Loading branch information
marcosnav authored Dec 12, 2024
2 parents 956e4be + 24283b2 commit 1a6d99b
Show file tree
Hide file tree
Showing 64 changed files with 7,700 additions and 35 deletions.
9 changes: 9 additions & 0 deletions internal/bundles/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"io"
"sort"
"strings"

"github.com/posit-dev/publisher/internal/clients/connect"
"github.com/posit-dev/publisher/internal/config"
Expand Down Expand Up @@ -202,6 +203,14 @@ func (manifest *Manifest) AddFile(path string, fileMD5 []byte) {
manifest.Files[path] = ManifestFile{
Checksum: hex.EncodeToString(fileMD5),
}

// Update manifest content category if a file being added is a site configuration
for _, ymlFile := range util.KnownSiteYmlConfigFiles {
if strings.ToLower(path) == ymlFile {
manifest.Metadata.ContentCategory = "site"
break
}
}
}

func (manifest *Manifest) ToJSON() ([]byte, error) {
Expand Down
19 changes: 19 additions & 0 deletions internal/bundles/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ func (s *ManifestSuite) TestAddFile() {
"subdir/test.Rmd": ManifestFile{Checksum: "030405"},
})
}

func (s *ManifestSuite) TestAddFile_UpdatesSiteCategory() {
for _, siteConfigYml := range util.KnownSiteYmlConfigFiles {
manifest := NewManifest()
manifest.AddFile("test.Rmd", []byte{0x00, 0x01, 0x02})
s.Equal(manifest.Files, ManifestFileMap{
"test.Rmd": ManifestFile{Checksum: "000102"},
})
s.Equal(manifest.Metadata.ContentCategory, "")

manifest.AddFile(siteConfigYml, []byte{0x03, 0x04, 0x05})
s.Equal(manifest.Files, ManifestFileMap{
"test.Rmd": ManifestFile{Checksum: "000102"},
siteConfigYml: ManifestFile{Checksum: "030405"},
})
s.Equal(manifest.Metadata.ContentCategory, "site")
}
}

func (s *ManifestSuite) TestReadManifest() {
manifestJson := `{"version": 1, "platform": "4.1.0"}`
reader := strings.NewReader(manifestJson)
Expand Down
3 changes: 3 additions & 0 deletions internal/bundles/matcher/walker.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ var StandardExclusions = []string{
"!.DS_Store",
"!.Rhistory",
"!.quarto/",
"!packrat/",
"!*.Rproj",
"!.rscignore",
// Less precise than rsconnect, which checks for a
// matching Rmd filename in the same directory.
"!*_cache/",
Expand Down
155 changes: 120 additions & 35 deletions internal/inspect/detectors/rmarkdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package detectors
// Copyright (C) 2023 by Posit Software, PBC.

import (
"fmt"
"regexp"
"slices"
"strings"

"github.com/posit-dev/publisher/internal/config"
Expand Down Expand Up @@ -80,14 +82,131 @@ func isShinyRmd(metadata *RMarkdownMetadata) bool {
return false
}

func (d *RMarkdownDetector) isSite(base util.AbsolutePath) (bool, string) {
for _, ymlFile := range util.KnownSiteYmlConfigFiles {
exists, err := base.Join(ymlFile).Exists()
if err == nil && exists {
d.log.Debug("Site configuration file is present, project being considered as static site", "file", ymlFile)
return true, ymlFile
}
}
return false, ""
}

func (d *RMarkdownDetector) lookForSiteMetadata(base util.AbsolutePath) (*RMarkdownMetadata, string) {
// Attempt to get site metadata looking up in common files
possibleIndexFiles := []string{"index.Rmd", "index.rmd", "app.Rmd", "app.rmd"}
for _, file := range possibleIndexFiles {
fileAbsPath := base.Join(file)
exists, err := fileAbsPath.Exists()
if err != nil || !exists {
continue
}
metadata, err := d.getRmdFileMetadata(fileAbsPath)
if err != nil {
continue
}
if metadata != nil {
return metadata, file
}
}
return nil, ""
}

func (d *RMarkdownDetector) configFromFileInspect(base util.AbsolutePath, entrypointPath util.AbsolutePath) (*config.Config, error) {
relEntrypoint, err := entrypointPath.Rel(base)
if err != nil {
return nil, err
}

metadata, err := d.getRmdFileMetadata(entrypointPath)
if err != nil {
d.log.Warn("Failed to read RMarkdown metadata", "path", entrypointPath, "error", err)
return nil, err
}

cfg := config.New()
cfg.Entrypoint = relEntrypoint.String()

if isShinyRmd(metadata) {
cfg.Type = config.ContentTypeRMarkdownShiny
} else {
cfg.Type = config.ContentTypeRMarkdown
}

if isSite, siteConfigFile := d.isSite(base); isSite {
var siteMetadata *RMarkdownMetadata
var indexFile string
cfg.Files = []string{fmt.Sprint("/", siteConfigFile)}

if metadata == nil {
siteMetadata, indexFile = d.lookForSiteMetadata(base)
metadata = siteMetadata
}

if indexFile != "" {
cfg.Files = append(cfg.Files, fmt.Sprint("/", indexFile))
}

entrypointBase := fmt.Sprint("/", entrypointPath.Base())
if !slices.Contains(cfg.Files, entrypointBase) {
cfg.Files = append(cfg.Files, entrypointBase)
}
}

if metadata != nil {
title := metadata.Title
if title != "" {
cfg.Title = title
}

if metadata.Params != nil {
cfg.HasParameters = true
}
}
needsR, needsPython, err := pydeps.DetectMarkdownLanguages(base)
if err != nil {
return nil, err
}
if needsR {
// Indicate that R inspection is needed.
d.log.Info("RMarkdown: detected R code; configuration will include R")
cfg.R = &config.R{}
}
if needsPython {
// Indicate that Python inspection is needed.
d.log.Info("RMarkdown: detected Python code; configuration will include Python")
cfg.Python = &config.Python{}
}
return cfg, nil
}

func (d *RMarkdownDetector) InferType(base util.AbsolutePath, entrypoint util.RelativePath) ([]*config.Config, error) {
entrypointIsSiteConfig := slices.Contains(util.KnownSiteYmlConfigFiles, strings.ToLower(entrypoint.String()))

// When the choosen entrypoint is a site configuration yml
// generate a single configuration as a site project.
if entrypointIsSiteConfig {
d.log.Debug("A site configuration file was picked as entrypoint", "entrypoint", entrypoint.String())

cfg, err := d.configFromFileInspect(base, base.Join(entrypoint.String()))
if err != nil {
return nil, err
}

if cfg != nil {
return []*config.Config{cfg}, nil
}
}

if entrypoint.String() != "" {
// Optimization: skip inspection if there's a specified entrypoint
// and it's not one of ours.
if entrypoint.Ext() != ".Rmd" {
return nil, nil
}
}

var configs []*config.Config
entrypointPaths, err := base.Glob("*.Rmd")
if err != nil {
Expand All @@ -102,44 +221,10 @@ func (d *RMarkdownDetector) InferType(base util.AbsolutePath, entrypoint util.Re
// Only inspect the specified file
continue
}
metadata, err := d.getRmdFileMetadata(entrypointPath)
if err != nil {
d.log.Warn("Failed to read RMarkdown metadata", "path", entrypointPath, "error", err)
continue
}
cfg := config.New()
cfg.Entrypoint = relEntrypoint.String()

if isShinyRmd(metadata) {
cfg.Type = config.ContentTypeRMarkdownShiny
} else {
cfg.Type = config.ContentTypeRMarkdown
}

if metadata != nil {
title := metadata.Title
if title != "" {
cfg.Title = title
}

if metadata.Params != nil {
cfg.HasParameters = true
}
}
needsR, needsPython, err := pydeps.DetectMarkdownLanguages(base)
cfg, err := d.configFromFileInspect(base, entrypointPath)
if err != nil {
return nil, err
}
if needsR {
// Indicate that R inspection is needed.
d.log.Info("RMarkdown: detected R code; configuration will include R")
cfg.R = &config.R{}
}
if needsPython {
// Indicate that Python inspection is needed.
d.log.Info("RMarkdown: detected Python code; configuration will include Python")
cfg.Python = &config.Python{}
}
configs = append(configs, cfg)
}
return configs, nil
Expand Down
Loading

0 comments on commit 1a6d99b

Please sign in to comment.