Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved support for RMarkdown sites #2466

Merged
merged 9 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading