diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fba26943..70a7d5a81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,57 @@ ## Unreleased Changes * Added popout diff visualizer for table properties like Attributes and Tags ([#834]) - +* Updated Theme to use Studio colors ([#838]) +* Projects may now specify rules for syncing files as if they had a different file extension. ([#813]) + This is specified via a new field on project files, `syncRules`: + + ```json + { + "syncRules": [ + { + "pattern": "*.foo", + "use": "text", + "exclude": "*.exclude.foo", + }, + { + "pattern": "*.bar.baz", + "use": "json", + "suffix": ".bar.baz", + }, + ], + "name": "SyncRulesAreCool", + "tree": { + "$path": "src" + } + } + ``` + + The `pattern` field is a glob used to match the sync rule to files. If present, the `suffix` field allows you to specify parts of a file's name get cut off by Rojo to name the Instance, including the file extension. If it isn't specified, Rojo will only cut off the first part of the file extension, up to the first dot. + + Additionally, the `exclude` field allows files to be excluded from the sync rule if they match a pattern specified by it. If it's not present, all files that match `pattern` will be modified using the sync rule. + + The `use` field corresponds to one of the potential file type that Rojo will currently include in a project. Files that match the provided pattern will be treated as if they had the file extension for that file type. A full list is below: + + | `use` value | file extension | + |:---------------|:----------------| + | `serverScript` | `.server.lua` | + | `clientScript` | `.client.lua` | + | `moduleScript` | `.lua` | + | `json` | `.json` | + | `toml` | `.toml` | + | `csv` | `.csv` | + | `text` | `.txt` | + | `jsonModel` | `.model.json` | + | `rbxm` | `.rbxm` | + | `rbxmx` | `.rbxmx` | + | `project` | `.project.json` | + | `ignore` | None! | + + **All** sync rules are reset between project files, so they must be specified in each one when nesting them. This is to ensure that nothing can break other projects by changing how files are synced! + +[#813]: https://github.com/rojo-rbx/rojo/pull/813 [#834]: https://github.com/rojo-rbx/rojo/pull/834 +[#838]: https://github.com/rojo-rbx/rojo/pull/838 ## [7.4.0] - January 16, 2024 * Improved the visualization for array properties like Tags ([#829]) diff --git a/plugin/src/App/Components/Dropdown.lua b/plugin/src/App/Components/Dropdown.lua index 4b761636f..3fde88c5d 100644 --- a/plugin/src/App/Components/Dropdown.lua +++ b/plugin/src/App/Components/Dropdown.lua @@ -109,9 +109,7 @@ function Dropdown:render() }, { DropArrow = e("ImageLabel", { Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow, - ImageColor3 = self.openBinding:map(function(a) - return theme.Closed.IconColor:Lerp(theme.Open.IconColor, a) - end), + ImageColor3 = theme.IconColor, ImageTransparency = self.props.transparency, Size = UDim2.new(0, 18, 0, 18), diff --git a/plugin/src/App/Components/PatchVisualizer/ChangeList.lua b/plugin/src/App/Components/PatchVisualizer/ChangeList.lua index 188bc42ba..ce01b8010 100644 --- a/plugin/src/App/Components/PatchVisualizer/ChangeList.lua +++ b/plugin/src/App/Components/PatchVisualizer/ChangeList.lua @@ -172,7 +172,7 @@ function ChangeList:render() BackgroundTransparency = 1, Font = Enum.Font.GothamBold, TextSize = 14, - TextColor3 = theme.Settings.Setting.DescriptionColor, + TextColor3 = theme.TextColor, TextXAlignment = Enum.TextXAlignment.Left, TextTransparency = props.transparency, TextTruncate = Enum.TextTruncate.AtEnd, @@ -184,7 +184,7 @@ function ChangeList:render() BackgroundTransparency = 1, Font = Enum.Font.GothamBold, TextSize = 14, - TextColor3 = theme.Settings.Setting.DescriptionColor, + TextColor3 = theme.TextColor, TextXAlignment = Enum.TextXAlignment.Left, TextTransparency = props.transparency, TextTruncate = Enum.TextTruncate.AtEnd, @@ -196,7 +196,7 @@ function ChangeList:render() BackgroundTransparency = 1, Font = Enum.Font.GothamBold, TextSize = 14, - TextColor3 = theme.Settings.Setting.DescriptionColor, + TextColor3 = theme.TextColor, TextXAlignment = Enum.TextXAlignment.Left, TextTransparency = props.transparency, TextTruncate = Enum.TextTruncate.AtEnd, @@ -232,7 +232,7 @@ function ChangeList:render() BackgroundTransparency = 1, Font = Enum.Font.GothamMedium, TextSize = 14, - TextColor3 = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor, + TextColor3 = if isWarning then theme.Diff.Warning else theme.TextColor, TextXAlignment = Enum.TextXAlignment.Left, TextTransparency = props.transparency, TextTruncate = Enum.TextTruncate.AtEnd, diff --git a/plugin/src/App/Components/PatchVisualizer/DomLabel.lua b/plugin/src/App/Components/PatchVisualizer/DomLabel.lua index 4405d020b..b2d9afe05 100644 --- a/plugin/src/App/Components/PatchVisualizer/DomLabel.lua +++ b/plugin/src/App/Components/PatchVisualizer/DomLabel.lua @@ -207,7 +207,7 @@ function DomLabel:render() BackgroundTransparency = 1, Font = Enum.Font.GothamMedium, TextSize = 14, - TextColor3 = if props.isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor, + TextColor3 = if props.isWarning then theme.Diff.Warning else theme.TextColor, TextXAlignment = Enum.TextXAlignment.Left, TextTransparency = props.transparency, TextTruncate = Enum.TextTruncate.AtEnd, diff --git a/plugin/src/App/Components/PatchVisualizer/init.lua b/plugin/src/App/Components/PatchVisualizer/init.lua index d83667be7..67a4d7b20 100644 --- a/plugin/src/App/Components/PatchVisualizer/init.lua +++ b/plugin/src/App/Components/PatchVisualizer/init.lua @@ -99,7 +99,7 @@ function PatchVisualizer:render() Text = "No changes to sync, project is up to date.", Font = Enum.Font.GothamMedium, TextSize = 15, - TextColor3 = theme.Settings.Setting.DescriptionColor, + TextColor3 = theme.TextColor, TextWrapped = true, Size = UDim2.new(1, 0, 1, 0), BackgroundTransparency = 1, diff --git a/plugin/src/App/StatusPages/Confirming.lua b/plugin/src/App/StatusPages/Confirming.lua index 87a1cf5a6..e5b272fda 100644 --- a/plugin/src/App/StatusPages/Confirming.lua +++ b/plugin/src/App/StatusPages/Confirming.lua @@ -51,7 +51,7 @@ function ConfirmingPage:render() Font = Enum.Font.Gotham, LineHeight = 1.2, TextSize = 14, - TextColor3 = theme.Settings.Setting.DescriptionColor, + TextColor3 = theme.TextColor, TextXAlignment = Enum.TextXAlignment.Left, TextTransparency = self.props.transparency, Size = UDim2.new(1, 0, 0, 20), diff --git a/plugin/src/App/Theme.lua b/plugin/src/App/Theme.lua index d6f9622bd..2ba8fc9d8 100644 --- a/plugin/src/App/Theme.lua +++ b/plugin/src/App/Theme.lua @@ -25,249 +25,161 @@ local strict = require(script.Parent.Parent.strict) local BRAND_COLOR = Color3.fromHex("E13835") -local lightTheme = strict("LightTheme", { - BackgroundColor = Color3.fromHex("FFFFFF"), - Button = { - Solid = { - ActionFillColor = Color3.fromHex("FFFFFF"), - ActionFillTransparency = 0.8, - Enabled = { - TextColor = Color3.fromHex("FFFFFF"), - BackgroundColor = BRAND_COLOR, +local Context = Roact.createContext({}) + +local StudioProvider = Roact.Component:extend("StudioProvider") + +-- Pull the current theme from Roblox Studio and update state with it. +function StudioProvider:updateTheme() + local studioTheme = getStudio().Theme + + local theme = strict(studioTheme.Name .. "Theme", { + BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground), + TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText), + Button = { + Solid = { + -- Solid uses brand theming, not Studio theming. + ActionFillColor = Color3.fromHex("FFFFFF"), + ActionFillTransparency = 0.8, + Enabled = { + TextColor = Color3.fromHex("FFFFFF"), + BackgroundColor = BRAND_COLOR, + }, + Disabled = { + TextColor = Color3.fromHex("FFFFFF"), + BackgroundColor = BRAND_COLOR, + }, }, - Disabled = { - TextColor = Color3.fromHex("FFFFFF"), - BackgroundColor = BRAND_COLOR, + Bordered = { + ActionFillColor = studioTheme:GetColor( + Enum.StudioStyleGuideColor.ButtonText, + Enum.StudioStyleGuideModifier.Selected + ), + ActionFillTransparency = 0.9, + Enabled = { + TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.ButtonText), + BorderColor = studioTheme:GetColor( + Enum.StudioStyleGuideColor.CheckedFieldBorder, + Enum.StudioStyleGuideModifier.Disabled + ), + }, + Disabled = { + TextColor = studioTheme:GetColor( + Enum.StudioStyleGuideColor.ButtonText, + Enum.StudioStyleGuideModifier.Disabled + ), + BorderColor = studioTheme:GetColor( + Enum.StudioStyleGuideColor.CheckedFieldBorder, + Enum.StudioStyleGuideModifier.Disabled + ), + }, }, }, - Bordered = { - ActionFillColor = Color3.fromHex("000000"), - ActionFillTransparency = 0.9, - Enabled = { - TextColor = Color3.fromHex("393939"), - BorderColor = Color3.fromHex("ACACAC"), + Checkbox = { + Active = { + -- Active checkboxes use brand theming, not Studio theming. + IconColor = Color3.fromHex("FFFFFF"), + BackgroundColor = BRAND_COLOR, }, - Disabled = { - TextColor = Color3.fromHex("393939"), - BorderColor = Color3.fromHex("ACACAC"), + Inactive = { + IconColor = studioTheme:GetColor( + Enum.StudioStyleGuideColor.CheckedFieldIndicator, + Enum.StudioStyleGuideModifier.Disabled + ), + BorderColor = studioTheme:GetColor( + Enum.StudioStyleGuideColor.CheckedFieldBorder, + Enum.StudioStyleGuideModifier.Disabled + ), }, }, - }, - Checkbox = { - Active = { - IconColor = Color3.fromHex("FFFFFF"), - BackgroundColor = BRAND_COLOR, - }, - Inactive = { - IconColor = Color3.fromHex("EEEEEE"), - BorderColor = Color3.fromHex("AFAFAF"), - }, - }, - Dropdown = { - TextColor = Color3.fromHex("000000"), - BorderColor = Color3.fromHex("AFAFAF"), - BackgroundColor = Color3.fromHex("EEEEEE"), - Open = { - IconColor = BRAND_COLOR, - }, - Closed = { - IconColor = Color3.fromHex("EEEEEE"), - }, - }, - TextInput = { - Enabled = { - TextColor = Color3.fromHex("000000"), - PlaceholderColor = Color3.fromHex("8C8C8C"), - BorderColor = Color3.fromHex("ACACAC"), - }, - Disabled = { - TextColor = Color3.fromHex("393939"), - PlaceholderColor = Color3.fromHex("8C8C8C"), - BorderColor = Color3.fromHex("AFAFAF"), - }, - ActionFillColor = Color3.fromHex("000000"), - ActionFillTransparency = 0.9, - }, - AddressEntry = { - TextColor = Color3.fromHex("000000"), - PlaceholderColor = Color3.fromHex("8C8C8C"), - }, - BorderedContainer = { - BorderColor = Color3.fromHex("CBCBCB"), - BackgroundColor = Color3.fromHex("EEEEEE"), - }, - Spinner = { - ForegroundColor = BRAND_COLOR, - BackgroundColor = Color3.fromHex("EEEEEE"), - }, - Diff = { - Add = Color3.fromHex("baffbd"), - Remove = Color3.fromHex("ffbdba"), - Edit = Color3.fromHex("bacdff"), - Row = Color3.fromHex("000000"), - Warning = Color3.fromHex("FF8E3C"), - }, - ConnectionDetails = { - ProjectNameColor = Color3.fromHex("000000"), - AddressColor = Color3.fromHex("000000"), - DisconnectColor = BRAND_COLOR, - }, - Settings = { - DividerColor = Color3.fromHex("CBCBCB"), - Navbar = { - BackButtonColor = Color3.fromHex("000000"), - TextColor = Color3.fromHex("000000"), - }, - Setting = { - NameColor = Color3.fromHex("000000"), - DescriptionColor = Color3.fromHex("5F5F5F"), - }, - }, - Header = { - LogoColor = BRAND_COLOR, - VersionColor = Color3.fromHex("727272"), - }, - Notification = { - InfoColor = Color3.fromHex("000000"), - CloseColor = BRAND_COLOR, - }, - ErrorColor = Color3.fromHex("000000"), - ScrollBarColor = Color3.fromHex("000000"), -}) - -local darkTheme = strict("DarkTheme", { - BackgroundColor = Color3.fromHex("2E2E2E"), - Button = { - Solid = { - ActionFillColor = Color3.fromHex("FFFFFF"), - ActionFillTransparency = 0.8, + Dropdown = { + TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.ButtonText), + BorderColor = studioTheme:GetColor( + Enum.StudioStyleGuideColor.CheckedFieldBorder, + Enum.StudioStyleGuideModifier.Disabled + ), + BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground), + IconColor = studioTheme:GetColor( + Enum.StudioStyleGuideColor.CheckedFieldIndicator, + Enum.StudioStyleGuideModifier.Disabled + ), + }, + TextInput = { Enabled = { - TextColor = Color3.fromHex("FFFFFF"), - BackgroundColor = BRAND_COLOR, + TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), + PlaceholderColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText), + BorderColor = studioTheme:GetColor( + Enum.StudioStyleGuideColor.CheckedFieldBorder, + Enum.StudioStyleGuideModifier.Disabled + ), }, Disabled = { - TextColor = Color3.fromHex("FFFFFF"), - BackgroundColor = BRAND_COLOR, + TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText), + PlaceholderColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText), + BorderColor = studioTheme:GetColor( + Enum.StudioStyleGuideColor.CheckedFieldBorder, + Enum.StudioStyleGuideModifier.Disabled + ), }, - }, - Bordered = { - ActionFillColor = Color3.fromHex("FFFFFF"), + ActionFillColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), ActionFillTransparency = 0.9, - Enabled = { - TextColor = Color3.fromHex("DBDBDB"), - BorderColor = Color3.fromHex("535353"), + }, + AddressEntry = { + TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), + PlaceholderColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText), + }, + BorderedContainer = { + BorderColor = studioTheme:GetColor( + Enum.StudioStyleGuideColor.CheckedFieldBorder, + Enum.StudioStyleGuideModifier.Disabled + ), + BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground), + }, + Spinner = { + ForegroundColor = BRAND_COLOR, + BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground), + }, + Diff = { + Add = studioTheme:GetColor(Enum.StudioStyleGuideColor.DiffTextAdditionBackground), + Remove = studioTheme:GetColor(Enum.StudioStyleGuideColor.DiffTextDeletionBackground), + Edit = studioTheme:GetColor(Enum.StudioStyleGuideColor.DiffLineNumSeparatorBackground), + Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), + Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText), + }, + ConnectionDetails = { + ProjectNameColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), + AddressColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), + DisconnectColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), + }, + Settings = { + DividerColor = studioTheme:GetColor( + Enum.StudioStyleGuideColor.CheckedFieldBorder, + Enum.StudioStyleGuideModifier.Disabled + ), + Navbar = { + BackButtonColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), + TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), }, - Disabled = { - TextColor = Color3.fromHex("DBDBDB"), - BorderColor = Color3.fromHex("535353"), + Setting = { + NameColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), + DescriptionColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText), }, }, - }, - Checkbox = { - Active = { - IconColor = Color3.fromHex("FFFFFF"), - BackgroundColor = BRAND_COLOR, - }, - Inactive = { - IconColor = Color3.fromHex("484848"), - BorderColor = Color3.fromHex("5A5A5A"), - }, - }, - Dropdown = { - TextColor = Color3.fromHex("FFFFFF"), - BorderColor = Color3.fromHex("5A5A5A"), - BackgroundColor = Color3.fromHex("2B2B2B"), - Open = { - IconColor = BRAND_COLOR, - }, - Closed = { - IconColor = Color3.fromHex("484848"), - }, - }, - TextInput = { - Enabled = { - TextColor = Color3.fromHex("FFFFFF"), - PlaceholderColor = Color3.fromHex("8B8B8B"), - BorderColor = Color3.fromHex("535353"), - }, - Disabled = { - TextColor = Color3.fromHex("484848"), - PlaceholderColor = Color3.fromHex("8B8B8B"), - BorderColor = Color3.fromHex("5A5A5A"), - }, - ActionFillColor = Color3.fromHex("FFFFFF"), - ActionFillTransparency = 0.9, - }, - AddressEntry = { - TextColor = Color3.fromHex("FFFFFF"), - PlaceholderColor = Color3.fromHex("8B8B8B"), - }, - BorderedContainer = { - BorderColor = Color3.fromHex("535353"), - BackgroundColor = Color3.fromHex("2B2B2B"), - }, - Spinner = { - ForegroundColor = BRAND_COLOR, - BackgroundColor = Color3.fromHex("2B2B2B"), - }, - Diff = { - Add = Color3.fromHex("273732"), - Remove = Color3.fromHex("3F2D32"), - Edit = Color3.fromHex("193345"), - Row = Color3.fromHex("FFFFFF"), - Warning = Color3.fromHex("FF8E3C"), - }, - ConnectionDetails = { - ProjectNameColor = Color3.fromHex("FFFFFF"), - AddressColor = Color3.fromHex("FFFFFF"), - DisconnectColor = Color3.fromHex("FFFFFF"), - }, - Settings = { - DividerColor = Color3.fromHex("535353"), - Navbar = { - BackButtonColor = Color3.fromHex("FFFFFF"), - TextColor = Color3.fromHex("FFFFFF"), + Header = { + LogoColor = BRAND_COLOR, + VersionColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText), }, - Setting = { - NameColor = Color3.fromHex("FFFFFF"), - DescriptionColor = Color3.fromHex("D3D3D3"), + Notification = { + InfoColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), + CloseColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), }, - }, - Header = { - LogoColor = BRAND_COLOR, - VersionColor = Color3.fromHex("D3D3D3"), - }, - Notification = { - InfoColor = Color3.fromHex("FFFFFF"), - CloseColor = Color3.fromHex("FFFFFF"), - }, - ErrorColor = Color3.fromHex("FFFFFF"), - ScrollBarColor = Color3.fromHex("FFFFFF"), -}) - -local Context = Roact.createContext(lightTheme) - -local StudioProvider = Roact.Component:extend("StudioProvider") - --- Pull the current theme from Roblox Studio and update state with it. -function StudioProvider:updateTheme() - local studioTheme = getStudio().Theme - - if studioTheme.Name == "Light" then - self:setState({ - theme = lightTheme, - }) - elseif studioTheme.Name == "Dark" then - self:setState({ - theme = darkTheme, - }) - else - Log.warn("Unexpected theme '{}'' -- falling back to light theme!", studioTheme.Name) + ErrorColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), + ScrollBarColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText), + }) - self:setState({ - theme = lightTheme, - }) - end + self:setState({ + theme = theme, + }) end function StudioProvider:init() diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_alone.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_alone.snap new file mode 100644 index 000000000..53e329cc7 --- /dev/null +++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_alone.snap @@ -0,0 +1,18 @@ +--- +source: tests/tests/build.rs +assertion_line: 102 +expression: contents +--- + + + + sync_rule_alone + + + + foo + Hello, world! + + + + diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_complex.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_complex.snap new file mode 100644 index 000000000..e38050d81 --- /dev/null +++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_complex.snap @@ -0,0 +1,43 @@ +--- +source: tests/tests/build.rs +assertion_line: 104 +expression: contents +--- + + + + sync_rule_complex + + + + bar + 0 + -- Hello, from bar (a Script)! + + + + + baz + -- Hello, from baz (a LocalScript)! + + + + + cat + Hello, from cat (a StringValue)! + + + + + foo + -- Hello, from foo (a ModuleScript)! + + + + + qux + Hello, from qux (a .rojo file that's turned into a StringValue)! + + + + diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_nested_projects.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_nested_projects.snap new file mode 100644 index 000000000..6867ac375 --- /dev/null +++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_nested_projects.snap @@ -0,0 +1,12 @@ +--- +source: tests/tests/build.rs +assertion_line: 104 +expression: contents +--- + + + + sync_rule_nested_projects + + + diff --git a/rojo-test/build-tests/sync_rule_alone/default.project.json b/rojo-test/build-tests/sync_rule_alone/default.project.json new file mode 100644 index 000000000..f3a5aa342 --- /dev/null +++ b/rojo-test/build-tests/sync_rule_alone/default.project.json @@ -0,0 +1,12 @@ +{ + "name": "sync_rule_alone", + "tree": { + "$path": "src" + }, + "syncRules": [ + { + "pattern": "*.nothing", + "use": "text" + } + ] +} \ No newline at end of file diff --git a/rojo-test/build-tests/sync_rule_alone/src/foo.nothing b/rojo-test/build-tests/sync_rule_alone/src/foo.nothing new file mode 100644 index 000000000..5dd01c177 --- /dev/null +++ b/rojo-test/build-tests/sync_rule_alone/src/foo.nothing @@ -0,0 +1 @@ +Hello, world! \ No newline at end of file diff --git a/rojo-test/build-tests/sync_rule_complex/default.project.json b/rojo-test/build-tests/sync_rule_complex/default.project.json new file mode 100644 index 000000000..62764bb4c --- /dev/null +++ b/rojo-test/build-tests/sync_rule_complex/default.project.json @@ -0,0 +1,30 @@ +{ + "name": "sync_rule_complex", + "tree": { + "$path": "src" + }, + "syncRules": [ + { + "pattern": "*.module", + "use": "moduleScript" + }, + { + "pattern": "*.server", + "use": "serverScript" + }, + { + "pattern": "*.client", + "use": "clientScript" + }, + { + "pattern": "*.rojo", + "exclude": "*.ignore.rojo", + "use": "project" + }, + { + "pattern": "*.dog.rojo2", + "use": "text", + "suffix": ".dog.rojo2" + } + ] +} \ No newline at end of file diff --git a/rojo-test/build-tests/sync_rule_complex/src/bar.server b/rojo-test/build-tests/sync_rule_complex/src/bar.server new file mode 100644 index 000000000..e860bd77f --- /dev/null +++ b/rojo-test/build-tests/sync_rule_complex/src/bar.server @@ -0,0 +1 @@ +-- Hello, from bar (a Script)! \ No newline at end of file diff --git a/rojo-test/build-tests/sync_rule_complex/src/baz.client b/rojo-test/build-tests/sync_rule_complex/src/baz.client new file mode 100644 index 000000000..4326a2a43 --- /dev/null +++ b/rojo-test/build-tests/sync_rule_complex/src/baz.client @@ -0,0 +1 @@ +-- Hello, from baz (a LocalScript)! \ No newline at end of file diff --git a/rojo-test/build-tests/sync_rule_complex/src/cat.dog.rojo2 b/rojo-test/build-tests/sync_rule_complex/src/cat.dog.rojo2 new file mode 100644 index 000000000..e185da86c --- /dev/null +++ b/rojo-test/build-tests/sync_rule_complex/src/cat.dog.rojo2 @@ -0,0 +1 @@ +Hello, from cat (a StringValue)! \ No newline at end of file diff --git a/rojo-test/build-tests/sync_rule_complex/src/foo.module b/rojo-test/build-tests/sync_rule_complex/src/foo.module new file mode 100644 index 000000000..3a55f9b2b --- /dev/null +++ b/rojo-test/build-tests/sync_rule_complex/src/foo.module @@ -0,0 +1 @@ +-- Hello, from foo (a ModuleScript)! \ No newline at end of file diff --git a/rojo-test/build-tests/sync_rule_complex/src/qux.rojo b/rojo-test/build-tests/sync_rule_complex/src/qux.rojo new file mode 100644 index 000000000..7153477dc --- /dev/null +++ b/rojo-test/build-tests/sync_rule_complex/src/qux.rojo @@ -0,0 +1,9 @@ +{ + "name": "qux", + "tree": { + "$className": "StringValue", + "$properties": { + "Value": "Hello, from qux (a .rojo file that's turned into a StringValue)!" + } + } +} \ No newline at end of file diff --git a/rojo-test/build-tests/sync_rule_complex/src/rat.ignore.rojo b/rojo-test/build-tests/sync_rule_complex/src/rat.ignore.rojo new file mode 100644 index 000000000..d80a8cc88 --- /dev/null +++ b/rojo-test/build-tests/sync_rule_complex/src/rat.ignore.rojo @@ -0,0 +1 @@ +This file should be ignored! \ No newline at end of file diff --git a/rojo-test/build-tests/sync_rule_nested_projects/default.project.json b/rojo-test/build-tests/sync_rule_nested_projects/default.project.json new file mode 100644 index 000000000..1b6193bbf --- /dev/null +++ b/rojo-test/build-tests/sync_rule_nested_projects/default.project.json @@ -0,0 +1,12 @@ +{ + "name": "sync_rule_nested_projects", + "tree": { + "$path": "nested.project.json" + }, + "syncRules": [ + { + "pattern": "*.rojo", + "use": "text" + } + ] +} \ No newline at end of file diff --git a/rojo-test/build-tests/sync_rule_nested_projects/nested.project.json b/rojo-test/build-tests/sync_rule_nested_projects/nested.project.json new file mode 100644 index 000000000..3c5fea2c0 --- /dev/null +++ b/rojo-test/build-tests/sync_rule_nested_projects/nested.project.json @@ -0,0 +1,12 @@ +{ + "name": "nested", + "tree": { + "$path": "src" + }, + "syncRules": [ + { + "pattern": "*.txt", + "use": "ignore" + } + ] +} \ No newline at end of file diff --git a/rojo-test/build-tests/sync_rule_nested_projects/src/ignored.rojo b/rojo-test/build-tests/sync_rule_nested_projects/src/ignored.rojo new file mode 100644 index 000000000..d7ed8b4fd --- /dev/null +++ b/rojo-test/build-tests/sync_rule_nested_projects/src/ignored.rojo @@ -0,0 +1 @@ +This shouldn't be in the built file. If it is, something is wrong. \ No newline at end of file diff --git a/rojo-test/build-tests/sync_rule_nested_projects/src/ignored.txt b/rojo-test/build-tests/sync_rule_nested_projects/src/ignored.txt new file mode 100644 index 000000000..d7ed8b4fd --- /dev/null +++ b/rojo-test/build-tests/sync_rule_nested_projects/src/ignored.txt @@ -0,0 +1 @@ +This shouldn't be in the built file. If it is, something is wrong. \ No newline at end of file diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_all.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_all.snap new file mode 100644 index 000000000..e4fa4adfc --- /dev/null +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_all.snap @@ -0,0 +1,30 @@ +--- +source: tests/tests/serve.rs +assertion_line: 268 +expression: "read_response.intern_and_redact(&mut redactions, root_id)" +--- +instances: + id-2: + Children: + - id-3 + ClassName: Folder + Id: id-2 + Metadata: + ignoreUnknownInstances: false + Name: sync_rule_alone + Parent: "00000000000000000000000000000000" + Properties: {} + id-3: + Children: [] + ClassName: StringValue + Id: id-3 + Metadata: + ignoreUnknownInstances: false + Name: foo + Parent: id-2 + Properties: + Value: + String: "Hello, world!" +messageCursor: 0 +sessionId: id-1 + diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_info.snap new file mode 100644 index 000000000..514b88580 --- /dev/null +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_info.snap @@ -0,0 +1,14 @@ +--- +source: tests/tests/serve.rs +assertion_line: 265 +expression: redactions.redacted_yaml(info) +--- +expectedPlaceIds: ~ +gameId: ~ +placeId: ~ +projectName: sync_rule_alone +protocolVersion: 4 +rootInstanceId: id-2 +serverVersion: "[server-version]" +sessionId: id-1 + diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_all.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_all.snap new file mode 100644 index 000000000..80cd76f66 --- /dev/null +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_all.snap @@ -0,0 +1,80 @@ +--- +source: tests/tests/serve.rs +assertion_line: 284 +expression: "read_response.intern_and_redact(&mut redactions, root_id)" +--- +instances: + id-2: + Children: + - id-3 + - id-4 + - id-5 + - id-6 + - id-7 + ClassName: Folder + Id: id-2 + Metadata: + ignoreUnknownInstances: false + Name: sync_rule_complex + Parent: "00000000000000000000000000000000" + Properties: {} + id-3: + Children: [] + ClassName: Script + Id: id-3 + Metadata: + ignoreUnknownInstances: false + Name: bar + Parent: id-2 + Properties: + RunContext: + Enum: 0 + Source: + String: "-- Hello, from bar (a Script)!" + id-4: + Children: [] + ClassName: LocalScript + Id: id-4 + Metadata: + ignoreUnknownInstances: false + Name: baz + Parent: id-2 + Properties: + Source: + String: "-- Hello, from baz (a LocalScript)!" + id-5: + Children: [] + ClassName: StringValue + Id: id-5 + Metadata: + ignoreUnknownInstances: false + Name: cat + Parent: id-2 + Properties: + Value: + String: "Hello, from cat (a StringValue)!" + id-6: + Children: [] + ClassName: ModuleScript + Id: id-6 + Metadata: + ignoreUnknownInstances: false + Name: foo + Parent: id-2 + Properties: + Source: + String: "-- Hello, from foo (a ModuleScript)!" + id-7: + Children: [] + ClassName: StringValue + Id: id-7 + Metadata: + ignoreUnknownInstances: true + Name: qux + Parent: id-2 + Properties: + Value: + String: "Hello, from qux (a .rojo file that's turned into a StringValue)!" +messageCursor: 0 +sessionId: id-1 + diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_info.snap new file mode 100644 index 000000000..3ed2b805a --- /dev/null +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_info.snap @@ -0,0 +1,14 @@ +--- +source: tests/tests/serve.rs +assertion_line: 281 +expression: redactions.redacted_yaml(info) +--- +expectedPlaceIds: ~ +gameId: ~ +placeId: ~ +projectName: sync_rule_complex +protocolVersion: 4 +rootInstanceId: id-2 +serverVersion: "[server-version]" +sessionId: id-1 + diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_all.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_all.snap new file mode 100644 index 000000000..f3210d3c3 --- /dev/null +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_all.snap @@ -0,0 +1,30 @@ +--- +source: tests/tests/serve.rs +assertion_line: 303 +expression: "read_response.intern_and_redact(&mut redactions, root_id)" +--- +instances: + id-2: + Children: + - id-3 + ClassName: Folder + Id: id-2 + Metadata: + ignoreUnknownInstances: false + Name: sync_rule_no_extension + Parent: "00000000000000000000000000000000" + Properties: {} + id-3: + Children: [] + ClassName: ModuleScript + Id: id-3 + Metadata: + ignoreUnknownInstances: false + Name: no_extension + Parent: id-2 + Properties: + Source: + String: "return {\"This file has no extension but should be a ModuleScript named `no_extension`\"}" +messageCursor: 0 +sessionId: id-1 + diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_info.snap new file mode 100644 index 000000000..3503e595e --- /dev/null +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_info.snap @@ -0,0 +1,14 @@ +--- +source: tests/tests/serve.rs +assertion_line: 297 +expression: redactions.redacted_yaml(info) +--- +expectedPlaceIds: ~ +gameId: ~ +placeId: ~ +projectName: sync_rule_no_extension +protocolVersion: 4 +rootInstanceId: id-2 +serverVersion: "[server-version]" +sessionId: id-1 + diff --git a/rojo-test/serve-tests/sync_rule_alone/default.project.json b/rojo-test/serve-tests/sync_rule_alone/default.project.json new file mode 100644 index 000000000..f3a5aa342 --- /dev/null +++ b/rojo-test/serve-tests/sync_rule_alone/default.project.json @@ -0,0 +1,12 @@ +{ + "name": "sync_rule_alone", + "tree": { + "$path": "src" + }, + "syncRules": [ + { + "pattern": "*.nothing", + "use": "text" + } + ] +} \ No newline at end of file diff --git a/rojo-test/serve-tests/sync_rule_alone/src/foo.nothing b/rojo-test/serve-tests/sync_rule_alone/src/foo.nothing new file mode 100644 index 000000000..5dd01c177 --- /dev/null +++ b/rojo-test/serve-tests/sync_rule_alone/src/foo.nothing @@ -0,0 +1 @@ +Hello, world! \ No newline at end of file diff --git a/rojo-test/serve-tests/sync_rule_complex/default.project.json b/rojo-test/serve-tests/sync_rule_complex/default.project.json new file mode 100644 index 000000000..62764bb4c --- /dev/null +++ b/rojo-test/serve-tests/sync_rule_complex/default.project.json @@ -0,0 +1,30 @@ +{ + "name": "sync_rule_complex", + "tree": { + "$path": "src" + }, + "syncRules": [ + { + "pattern": "*.module", + "use": "moduleScript" + }, + { + "pattern": "*.server", + "use": "serverScript" + }, + { + "pattern": "*.client", + "use": "clientScript" + }, + { + "pattern": "*.rojo", + "exclude": "*.ignore.rojo", + "use": "project" + }, + { + "pattern": "*.dog.rojo2", + "use": "text", + "suffix": ".dog.rojo2" + } + ] +} \ No newline at end of file diff --git a/rojo-test/serve-tests/sync_rule_complex/src/bar.server b/rojo-test/serve-tests/sync_rule_complex/src/bar.server new file mode 100644 index 000000000..e860bd77f --- /dev/null +++ b/rojo-test/serve-tests/sync_rule_complex/src/bar.server @@ -0,0 +1 @@ +-- Hello, from bar (a Script)! \ No newline at end of file diff --git a/rojo-test/serve-tests/sync_rule_complex/src/baz.client b/rojo-test/serve-tests/sync_rule_complex/src/baz.client new file mode 100644 index 000000000..4326a2a43 --- /dev/null +++ b/rojo-test/serve-tests/sync_rule_complex/src/baz.client @@ -0,0 +1 @@ +-- Hello, from baz (a LocalScript)! \ No newline at end of file diff --git a/rojo-test/serve-tests/sync_rule_complex/src/cat.dog.rojo2 b/rojo-test/serve-tests/sync_rule_complex/src/cat.dog.rojo2 new file mode 100644 index 000000000..e185da86c --- /dev/null +++ b/rojo-test/serve-tests/sync_rule_complex/src/cat.dog.rojo2 @@ -0,0 +1 @@ +Hello, from cat (a StringValue)! \ No newline at end of file diff --git a/rojo-test/serve-tests/sync_rule_complex/src/foo.module b/rojo-test/serve-tests/sync_rule_complex/src/foo.module new file mode 100644 index 000000000..3a55f9b2b --- /dev/null +++ b/rojo-test/serve-tests/sync_rule_complex/src/foo.module @@ -0,0 +1 @@ +-- Hello, from foo (a ModuleScript)! \ No newline at end of file diff --git a/rojo-test/serve-tests/sync_rule_complex/src/qux.rojo b/rojo-test/serve-tests/sync_rule_complex/src/qux.rojo new file mode 100644 index 000000000..7153477dc --- /dev/null +++ b/rojo-test/serve-tests/sync_rule_complex/src/qux.rojo @@ -0,0 +1,9 @@ +{ + "name": "qux", + "tree": { + "$className": "StringValue", + "$properties": { + "Value": "Hello, from qux (a .rojo file that's turned into a StringValue)!" + } + } +} \ No newline at end of file diff --git a/rojo-test/serve-tests/sync_rule_complex/src/rat.ignore.rojo b/rojo-test/serve-tests/sync_rule_complex/src/rat.ignore.rojo new file mode 100644 index 000000000..d80a8cc88 --- /dev/null +++ b/rojo-test/serve-tests/sync_rule_complex/src/rat.ignore.rojo @@ -0,0 +1 @@ +This file should be ignored! \ No newline at end of file diff --git a/rojo-test/serve-tests/sync_rule_no_extension/default.project.json b/rojo-test/serve-tests/sync_rule_no_extension/default.project.json new file mode 100644 index 000000000..185c66334 --- /dev/null +++ b/rojo-test/serve-tests/sync_rule_no_extension/default.project.json @@ -0,0 +1,12 @@ +{ + "name": "sync_rule_no_extension", + "tree": { + "$path": "src" + }, + "syncRules": [ + { + "pattern": "src/**", + "use": "json" + } + ] +} \ No newline at end of file diff --git a/rojo-test/serve-tests/sync_rule_no_extension/src/no_extension b/rojo-test/serve-tests/sync_rule_no_extension/src/no_extension new file mode 100644 index 000000000..41c705c4a --- /dev/null +++ b/rojo-test/serve-tests/sync_rule_no_extension/src/no_extension @@ -0,0 +1 @@ +["This file has no extension but should be a ModuleScript named `no_extension`"] \ No newline at end of file diff --git a/src/project.rs b/src/project.rs index 93a3cb218..1095fba20 100644 --- a/src/project.rs +++ b/src/project.rs @@ -8,7 +8,7 @@ use std::{ use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{glob::Glob, resolution::UnresolvedValue}; +use crate::{glob::Glob, resolution::UnresolvedValue, snapshot::SyncRule}; static PROJECT_FILENAME: &str = "default.project.json"; @@ -84,6 +84,12 @@ pub struct Project { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub glob_ignore_paths: Vec, + /// A list of mappings of globs to syncing rules. If a file matches a glob, + /// it will be 'transformed' into an Instance following the rule provided. + /// Globs are relative to the folder the project file is in. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub sync_rules: Vec, + /// The path to the file that this project came from. Relative paths in the /// project should be considered relative to the parent of this field, also /// given by `Project::folder_location`. diff --git a/src/snapshot/metadata.rs b/src/snapshot/metadata.rs index 1a3273ed7..1578af230 100644 --- a/src/snapshot/metadata.rs +++ b/src/snapshot/metadata.rs @@ -4,11 +4,14 @@ use std::{ sync::Arc, }; +use anyhow::Context; use serde::{Deserialize, Serialize}; use crate::{ - glob::Glob, path_serializer, project::ProjectNode, - snapshot_middleware::emit_legacy_scripts_default, + glob::Glob, + path_serializer, + project::ProjectNode, + snapshot_middleware::{emit_legacy_scripts_default, Middleware}, }; /// Rojo-specific metadata that can be associated with an instance or a snapshot @@ -107,6 +110,8 @@ pub struct InstanceContext { #[serde(skip_serializing_if = "Vec::is_empty")] pub path_ignore_rules: Arc>, pub emit_legacy_scripts: bool, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub sync_rules: Vec, } impl InstanceContext { @@ -114,6 +119,7 @@ impl InstanceContext { Self { path_ignore_rules: Arc::new(Vec::new()), emit_legacy_scripts: emit_legacy_scripts_default().unwrap(), + sync_rules: Vec::new(), } } @@ -144,9 +150,28 @@ impl InstanceContext { rules.extend(new_rules); } + /// Extend the list of syncing rules in the context with the given new rules. + pub fn add_sync_rules(&mut self, new_rules: I) + where + I: IntoIterator, + { + self.sync_rules.extend(new_rules); + } + + /// Clears all sync rules for this InstanceContext + pub fn clear_sync_rules(&mut self) { + self.sync_rules.clear(); + } + pub fn set_emit_legacy_scripts(&mut self, emit_legacy_scripts: bool) { self.emit_legacy_scripts = emit_legacy_scripts; } + + /// Returns the middleware specified by the first sync rule that + /// matches the provided path. This does not handle default syncing rules. + pub fn get_user_sync_rule(&self, path: &Path) -> Option<&SyncRule> { + self.sync_rules.iter().find(|&rule| rule.matches(path)) + } } impl Default for InstanceContext { @@ -216,3 +241,64 @@ impl From<&Path> for InstigatingSource { InstigatingSource::Path(path.to_path_buf()) } } + +/// Represents an user-specified rule for transforming files +/// into Instances using a given middleware. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct SyncRule { + /// A pattern used to determine if a file is included in this SyncRule + #[serde(rename = "pattern")] + pub include: Glob, + /// A pattern used to determine if a file is excluded from this SyncRule. + #[serde(skip_serializing_if = "Option::is_none")] + pub exclude: Option, + /// The middleware specified by the user for this SyncRule + #[serde(rename = "use")] + pub middleware: Middleware, + /// A suffix to trim off of file names, including the file extension. + /// If not specified, the file extension is the only thing cut off. + #[serde(skip_serializing_if = "Option::is_none")] + pub suffix: Option, + /// The 'base' of the glob above, allowing it to be used + /// relative to a path instead of absolute. + #[serde(skip)] + pub base_path: PathBuf, +} + +impl SyncRule { + /// Returns whether the given path matches this rule. + pub fn matches(&self, path: &Path) -> bool { + match path.strip_prefix(&self.base_path) { + Ok(suffix) => { + if let Some(pattern) = &self.exclude { + if pattern.is_match(suffix) { + return false; + } + } + self.include.is_match(suffix) + } + Err(_) => false, + } + } + + pub fn file_name_for_path<'a>(&self, path: &'a Path) -> anyhow::Result<&'a str> { + if let Some(suffix) = &self.suffix { + let file_name = path + .file_name() + .and_then(|s| s.to_str()) + .with_context(|| format!("file name of {} is invalid", path.display()))?; + if file_name.ends_with(suffix) { + let end = file_name.len().saturating_sub(suffix.len()); + Ok(&file_name[..end]) + } else { + Ok(file_name) + } + } else { + // If the user doesn't specify a suffix, we assume they just want + // the name of the file (the file_stem) + path.file_stem() + .and_then(|s| s.to_str()) + .with_context(|| format!("file name of {} is invalid", path.display())) + } + } +} diff --git a/src/snapshot_middleware/csv.rs b/src/snapshot_middleware/csv.rs index 708f1bb8c..52e8ab0fd 100644 --- a/src/snapshot_middleware/csv.rs +++ b/src/snapshot_middleware/csv.rs @@ -10,16 +10,14 @@ use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; use super::{ dir::{dir_meta, snapshot_dir_no_meta}, meta_file::AdjacentMetadata, - util::PathExt, }; pub fn snapshot_csv( _context: &InstanceContext, vfs: &Vfs, path: &Path, + name: &str, ) -> anyhow::Result> { - let name = path.file_name_trim_end(".csv")?; - let meta_path = path.with_file_name(format!("{}.meta.json", name)); let contents = vfs.read(path)?; @@ -74,9 +72,8 @@ pub fn snapshot_csv_init( ); } - let mut init_snapshot = snapshot_csv(context, vfs, init_path)?.unwrap(); + let mut init_snapshot = snapshot_csv(context, vfs, init_path, &dir_snapshot.name)?.unwrap(); - init_snapshot.name = dir_snapshot.name; init_snapshot.children = dir_snapshot.children; init_snapshot.metadata = dir_snapshot.metadata; @@ -185,10 +182,14 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#, let mut vfs = Vfs::new(imfs); - let instance_snapshot = - snapshot_csv(&InstanceContext::default(), &mut vfs, Path::new("/foo.csv")) - .unwrap() - .unwrap(); + let instance_snapshot = snapshot_csv( + &InstanceContext::default(), + &mut vfs, + Path::new("/foo.csv"), + "foo", + ) + .unwrap() + .unwrap(); insta::assert_yaml_snapshot!(instance_snapshot); } @@ -213,10 +214,14 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#, let mut vfs = Vfs::new(imfs); - let instance_snapshot = - snapshot_csv(&InstanceContext::default(), &mut vfs, Path::new("/foo.csv")) - .unwrap() - .unwrap(); + let instance_snapshot = snapshot_csv( + &InstanceContext::default(), + &mut vfs, + Path::new("/foo.csv"), + "foo", + ) + .unwrap() + .unwrap(); insta::assert_yaml_snapshot!(instance_snapshot); } diff --git a/src/snapshot_middleware/json.rs b/src/snapshot_middleware/json.rs index 8c7f369e3..7594d96c2 100644 --- a/src/snapshot_middleware/json.rs +++ b/src/snapshot_middleware/json.rs @@ -9,14 +9,14 @@ use crate::{ snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, }; -use super::{meta_file::AdjacentMetadata, util::PathExt}; +use super::meta_file::AdjacentMetadata; pub fn snapshot_json( context: &InstanceContext, vfs: &Vfs, path: &Path, + name: &str, ) -> anyhow::Result> { - let name = path.file_name_trim_end(".json")?; let contents = vfs.read(path)?; let value: serde_json::Value = serde_json::from_slice(&contents) @@ -107,6 +107,7 @@ mod test { &InstanceContext::default(), &mut vfs, Path::new("/foo.json"), + "foo", ) .unwrap() .unwrap(); diff --git a/src/snapshot_middleware/json_model.rs b/src/snapshot_middleware/json_model.rs index b38c47a78..93d566a2e 100644 --- a/src/snapshot_middleware/json_model.rs +++ b/src/snapshot_middleware/json_model.rs @@ -10,15 +10,12 @@ use crate::{ snapshot::{InstanceContext, InstanceSnapshot}, }; -use super::util::PathExt; - pub fn snapshot_json_model( context: &InstanceContext, vfs: &Vfs, path: &Path, + name: &str, ) -> anyhow::Result> { - let name = path.file_name_trim_end(".model.json")?; - let contents = vfs.read(path)?; let contents_str = str::from_utf8(&contents) .with_context(|| format!("File was not valid UTF-8: {}", path.display()))?; @@ -158,6 +155,7 @@ mod test { &InstanceContext::default(), &vfs, Path::new("/foo.model.json"), + "foo", ) .unwrap() .unwrap(); @@ -195,6 +193,7 @@ mod test { &InstanceContext::default(), &vfs, Path::new("/foo.model.json"), + "foo", ) .unwrap() .unwrap(); diff --git a/src/snapshot_middleware/lua.rs b/src/snapshot_middleware/lua.rs index 1307b0666..ab7f15a1f 100644 --- a/src/snapshot_middleware/lua.rs +++ b/src/snapshot_middleware/lua.rs @@ -9,11 +9,10 @@ use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; use super::{ dir::{dir_meta, snapshot_dir_no_meta}, meta_file::AdjacentMetadata, - util::match_trailing, }; #[derive(Debug)] -enum ScriptType { +pub enum ScriptType { Server, Client, Module, @@ -24,32 +23,15 @@ pub fn snapshot_lua( context: &InstanceContext, vfs: &Vfs, path: &Path, + name: &str, + script_type: ScriptType, ) -> anyhow::Result> { - let file_name = path.file_name().unwrap().to_string_lossy(); - let run_context_enums = &rbx_reflection_database::get() .enums .get("RunContext") .expect("Unable to get RunContext enums!") .items; - let (script_type, instance_name) = if let Some(name) = match_trailing(&file_name, ".server.lua") - { - (ScriptType::Server, name) - } else if let Some(name) = match_trailing(&file_name, ".client.lua") { - (ScriptType::Client, name) - } else if let Some(name) = match_trailing(&file_name, ".lua") { - (ScriptType::Module, name) - } else if let Some(name) = match_trailing(&file_name, ".server.luau") { - (ScriptType::Server, name) - } else if let Some(name) = match_trailing(&file_name, ".client.luau") { - (ScriptType::Client, name) - } else if let Some(name) = match_trailing(&file_name, ".luau") { - (ScriptType::Module, name) - } else { - return Ok(None); - }; - let (class_name, run_context) = match (context.emit_legacy_scripts, script_type) { (false, ScriptType::Server) => ("Script", run_context_enums.get("Server")), (false, ScriptType::Client) => ("Script", run_context_enums.get("Client")), @@ -73,10 +55,10 @@ pub fn snapshot_lua( ); } - let meta_path = path.with_file_name(format!("{}.meta.json", instance_name)); + let meta_path = path.with_file_name(format!("{}.meta.json", name)); let mut snapshot = InstanceSnapshot::new() - .name(instance_name) + .name(name) .class_name(class_name) .properties(properties) .metadata( @@ -103,6 +85,7 @@ pub fn snapshot_lua_init( context: &InstanceContext, vfs: &Vfs, init_path: &Path, + script_type: ScriptType, ) -> anyhow::Result> { let folder_path = init_path.parent().unwrap(); let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path)?.unwrap(); @@ -119,9 +102,9 @@ pub fn snapshot_lua_init( ); } - let mut init_snapshot = snapshot_lua(context, vfs, init_path)?.unwrap(); + let mut init_snapshot = + snapshot_lua(context, vfs, init_path, &dir_snapshot.name, script_type)?.unwrap(); - init_snapshot.name = dir_snapshot.name; init_snapshot.children = dir_snapshot.children; init_snapshot.metadata = dir_snapshot.metadata; @@ -151,6 +134,8 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(true)), &mut vfs, Path::new("/foo.lua"), + "foo", + ScriptType::Module, ) .unwrap() .unwrap(); @@ -172,6 +157,8 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(false)), &mut vfs, Path::new("/foo.lua"), + "foo", + ScriptType::Module, ) .unwrap() .unwrap(); @@ -193,6 +180,8 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(true)), &mut vfs, Path::new("/foo.server.lua"), + "foo", + ScriptType::Server, ) .unwrap() .unwrap(); @@ -214,6 +203,8 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(false)), &mut vfs, Path::new("/foo.server.lua"), + "foo", + ScriptType::Server, ) .unwrap() .unwrap(); @@ -235,6 +226,8 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(true)), &mut vfs, Path::new("/foo.client.lua"), + "foo", + ScriptType::Client, ) .unwrap() .unwrap(); @@ -256,6 +249,8 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(false)), &mut vfs, Path::new("/foo.client.lua"), + "foo", + ScriptType::Client, ) .unwrap() .unwrap(); @@ -283,6 +278,8 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(true)), &mut vfs, Path::new("/root"), + "root", + ScriptType::Module, ) .unwrap() .unwrap(); @@ -315,6 +312,8 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(true)), &mut vfs, Path::new("/foo.lua"), + "foo", + ScriptType::Module, ) .unwrap() .unwrap(); @@ -347,6 +346,8 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(false)), &mut vfs, Path::new("/foo.lua"), + "foo", + ScriptType::Module, ) .unwrap() .unwrap(); @@ -379,6 +380,8 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(true)), &mut vfs, Path::new("/foo.server.lua"), + "foo", + ScriptType::Server, ) .unwrap() .unwrap(); @@ -411,6 +414,8 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(false)), &mut vfs, Path::new("/foo.server.lua"), + "foo", + ScriptType::Server, ) .unwrap() .unwrap(); @@ -445,6 +450,8 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(true)), &mut vfs, Path::new("/bar.server.lua"), + "bar", + ScriptType::Server, ) .unwrap() .unwrap(); @@ -479,6 +486,8 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(false)), &mut vfs, Path::new("/bar.server.lua"), + "bar", + ScriptType::Server, ) .unwrap() .unwrap(); diff --git a/src/snapshot_middleware/mod.rs b/src/snapshot_middleware/mod.rs index d8cd39409..30ea57061 100644 --- a/src/snapshot_middleware/mod.rs +++ b/src/snapshot_middleware/mod.rs @@ -18,30 +18,37 @@ mod toml; mod txt; mod util; -use std::path::Path; +use std::{ + path::{Path, PathBuf}, + sync::OnceLock, +}; +use anyhow::Context; use memofs::{IoResultExt, Vfs}; +use serde::{Deserialize, Serialize}; -use crate::snapshot::{InstanceContext, InstanceSnapshot}; +use crate::glob::Glob; +use crate::snapshot::{InstanceContext, InstanceSnapshot, SyncRule}; use self::{ csv::{snapshot_csv, snapshot_csv_init}, dir::snapshot_dir, json::snapshot_json, json_model::snapshot_json_model, - lua::{snapshot_lua, snapshot_lua_init}, + lua::{snapshot_lua, snapshot_lua_init, ScriptType}, project::snapshot_project, rbxm::snapshot_rbxm, rbxmx::snapshot_rbxmx, toml::snapshot_toml, txt::snapshot_txt, - util::PathExt, }; pub use self::{project::snapshot_project_node, util::emit_legacy_scripts_default}; -/// The main entrypoint to the snapshot function. This function can be pointed -/// at any path and will return something if Rojo knows how to deal with it. +/// Returns an `InstanceSnapshot` for the provided path. +/// This will inspect the path and find the appropriate middleware for it, +/// taking user-written rules into account. Then, it will attempt to convert +/// the path into an InstanceSnapshot using that middleware. #[profiling::function] pub fn snapshot_from_vfs( context: &InstanceContext, @@ -54,89 +61,234 @@ pub fn snapshot_from_vfs( }; if meta.is_dir() { - let project_path = path.join("default.project.json"); - if vfs.metadata(&project_path).with_not_found()?.is_some() { - return snapshot_project(context, vfs, &project_path); - } + if let Some(init_path) = get_init_path(vfs, path)? { + // TODO: support user-defined init paths + for rule in default_sync_rules() { + if rule.matches(&init_path) { + return match rule.middleware { + Middleware::Project => snapshot_project(context, vfs, &init_path), - let init_path = path.join("init.luau"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return snapshot_lua_init(context, vfs, &init_path); - } + Middleware::ModuleScript => { + snapshot_lua_init(context, vfs, &init_path, ScriptType::Module) + } + Middleware::ServerScript => { + snapshot_lua_init(context, vfs, &init_path, ScriptType::Server) + } + Middleware::ClientScript => { + snapshot_lua_init(context, vfs, &init_path, ScriptType::Client) + } - let init_path = path.join("init.lua"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return snapshot_lua_init(context, vfs, &init_path); - } + Middleware::Csv => snapshot_csv_init(context, vfs, &init_path), - let init_path = path.join("init.server.luau"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return snapshot_lua_init(context, vfs, &init_path); + _ => snapshot_dir(context, vfs, path), + }; + } + } + snapshot_dir(context, vfs, path) + } else { + snapshot_dir(context, vfs, path) } + } else { + let file_name = path + .file_name() + .and_then(|n| n.to_str()) + .with_context(|| format!("file name of {} is invalid", path.display()))?; - let init_path = path.join("init.server.lua"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return snapshot_lua_init(context, vfs, &init_path); + // TODO: Is this even necessary anymore? + match file_name { + "init.server.luau" | "init.server.lua" | "init.client.luau" | "init.client.lua" + | "init.luau" | "init.lua" | "init.csv" => return Ok(None), + _ => {} } - let init_path = path.join("init.client.luau"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return snapshot_lua_init(context, vfs, &init_path); - } + snapshot_from_path(context, vfs, path) + } +} - let init_path = path.join("init.client.lua"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return snapshot_lua_init(context, vfs, &init_path); - } +/// Gets an `init` path for the given directory. +/// This uses an intrinsic priority list and for compatibility, +/// it should not be changed. +fn get_init_path>(vfs: &Vfs, dir: P) -> anyhow::Result> { + let path = dir.as_ref(); - let init_path = path.join("init.csv"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return snapshot_csv_init(context, vfs, &init_path); - } + let project_path = path.join("default.project.json"); + if vfs.metadata(&project_path).with_not_found()?.is_some() { + return Ok(Some(project_path)); + } - snapshot_dir(context, vfs, path) - } else { - let script_name = path - .file_name_trim_end(".lua") - .or_else(|_| path.file_name_trim_end(".luau")); + let init_path = path.join("init.luau"); + if vfs.metadata(&init_path).with_not_found()?.is_some() { + return Ok(Some(init_path)); + } - let csv_name = path.file_name_trim_end(".csv"); + let init_path = path.join("init.lua"); + if vfs.metadata(&init_path).with_not_found()?.is_some() { + return Ok(Some(init_path)); + } - if let Ok(name) = script_name { - match name { - // init scripts are handled elsewhere and should not turn into - // their own children. - "init" | "init.client" | "init.server" => return Ok(None), + let init_path = path.join("init.server.luau"); + if vfs.metadata(&init_path).with_not_found()?.is_some() { + return Ok(Some(init_path)); + } - _ => return snapshot_lua(context, vfs, path), - } - } else if path.file_name_ends_with(".project.json") { - return snapshot_project(context, vfs, path); - } else if path.file_name_ends_with(".model.json") { - return snapshot_json_model(context, vfs, path); - } else if path.file_name_ends_with(".meta.json") { - // .meta.json files do not turn into their own instances. - return Ok(None); - } else if path.file_name_ends_with(".json") { - return snapshot_json(context, vfs, path); - } else if path.file_name_ends_with(".toml") { - return snapshot_toml(context, vfs, path); - } else if let Ok(name) = csv_name { - match name { - // init csv are handled elsewhere and should not turn into - // their own children. - "init" => return Ok(None), - - _ => return snapshot_csv(context, vfs, path), + let init_path = path.join("init.server.lua"); + if vfs.metadata(&init_path).with_not_found()?.is_some() { + return Ok(Some(init_path)); + } + + let init_path = path.join("init.client.luau"); + if vfs.metadata(&init_path).with_not_found()?.is_some() { + return Ok(Some(init_path)); + } + + let init_path = path.join("init.client.lua"); + if vfs.metadata(&init_path).with_not_found()?.is_some() { + return Ok(Some(init_path)); + } + + let init_path = path.join("init.csv"); + if vfs.metadata(&init_path).with_not_found()?.is_some() { + return Ok(Some(init_path)); + } + + Ok(None) +} + +/// Gets a snapshot for a path given an InstanceContext and Vfs, taking +/// user specified sync rules into account. +fn snapshot_from_path( + context: &InstanceContext, + vfs: &Vfs, + path: &Path, +) -> anyhow::Result> { + if let Some(rule) = context.get_user_sync_rule(path) { + return rule + .middleware + .snapshot(context, vfs, path, rule.file_name_for_path(path)?); + } else { + for rule in default_sync_rules() { + if rule.matches(path) { + return rule.middleware.snapshot( + context, + vfs, + path, + rule.file_name_for_path(path)?, + ); } - } else if path.file_name_ends_with(".txt") { - return snapshot_txt(context, vfs, path); - } else if path.file_name_ends_with(".rbxmx") { - return snapshot_rbxmx(context, vfs, path); - } else if path.file_name_ends_with(".rbxm") { - return snapshot_rbxm(context, vfs, path); } + } + Ok(None) +} + +/// Represents a possible 'transformer' used by Rojo to turn a file system +/// item into a Roblox Instance. Missing from this list are directories and +/// metadata. This is deliberate, as metadata is not a snapshot middleware +/// and directories do not make sense to turn into files. +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum Middleware { + Csv, + JsonModel, + Json, + ServerScript, + ClientScript, + ModuleScript, + Project, + Rbxm, + Rbxmx, + Toml, + Text, + Ignore, +} - Ok(None) +impl Middleware { + /// Creates a snapshot for the given path from the Middleware with + /// the provided name. + fn snapshot( + &self, + context: &InstanceContext, + vfs: &Vfs, + path: &Path, + name: &str, + ) -> anyhow::Result> { + match self { + Self::Csv => snapshot_csv(context, vfs, path, name), + Self::JsonModel => snapshot_json_model(context, vfs, path, name), + Self::Json => snapshot_json(context, vfs, path, name), + Self::ServerScript => snapshot_lua(context, vfs, path, name, ScriptType::Server), + Self::ClientScript => snapshot_lua(context, vfs, path, name, ScriptType::Client), + Self::ModuleScript => snapshot_lua(context, vfs, path, name, ScriptType::Module), + // At the moment, snapshot_project does not use `name` so we + // don't provide it. + Self::Project => snapshot_project(context, vfs, path), + Self::Rbxm => snapshot_rbxm(context, vfs, path, name), + Self::Rbxmx => snapshot_rbxmx(context, vfs, path, name), + Self::Toml => snapshot_toml(context, vfs, path, name), + Self::Text => snapshot_txt(context, vfs, path, name), + Self::Ignore => Ok(None), + } } } + +/// A helper for easily defining a SyncRule. Arguments are passed literally +/// to this macro in the order `include`, `middleware`, `suffix`, +/// and `exclude`. Both `suffix` and `exclude` are optional. +/// +/// All arguments except `middleware` are expected to be strings. +/// The `middleware` parameter is expected to be a variant of `Middleware`, +/// not including the enum name itself. +macro_rules! sync_rule { + ($pattern:expr, $middleware:ident) => { + SyncRule { + middleware: Middleware::$middleware, + include: Glob::new($pattern).unwrap(), + exclude: None, + suffix: None, + base_path: PathBuf::new(), + } + }; + ($pattern:expr, $middleware:ident, $suffix:expr) => { + SyncRule { + middleware: Middleware::$middleware, + include: Glob::new($pattern).unwrap(), + exclude: None, + suffix: Some($suffix.into()), + base_path: PathBuf::new(), + } + }; + ($pattern:expr, $middleware:ident, $suffix:expr, $exclude:expr) => { + SyncRule { + middleware: Middleware::$middleware, + include: Glob::new($pattern).unwrap(), + exclude: Some(Glob::new($exclude).unwrap()), + suffix: Some($suffix.into()), + base_path: PathBuf::new(), + } + }; +} + +/// Defines the 'default' syncing rules that Rojo uses. +/// These do not broadly overlap, but the order matters for some in the case of +/// e.g. JSON models. +fn default_sync_rules() -> &'static [SyncRule] { + static DEFAULT_SYNC_RULES: OnceLock> = OnceLock::new(); + + DEFAULT_SYNC_RULES.get_or_init(|| { + vec![ + sync_rule!("*.server.lua", ServerScript, ".server.lua"), + sync_rule!("*.server.luau", ServerScript, ".server.luau"), + sync_rule!("*.client.lua", ClientScript, ".client.lua"), + sync_rule!("*.client.luau", ClientScript, ".client.luau"), + sync_rule!("*.{lua,luau}", ModuleScript), + // Project middleware doesn't use the file name. + sync_rule!("*.project.json", Project), + sync_rule!("*.model.json", JsonModel, ".model.json"), + sync_rule!("*.json", Json, ".json", "*.meta.json"), + sync_rule!("*.toml", Toml), + sync_rule!("*.csv", Csv), + sync_rule!("*.txt", Text), + sync_rule!("*.rbxmx", Rbxmx), + sync_rule!("*.rbxm", Rbxm), + ] + }) +} diff --git a/src/snapshot_middleware/project.rs b/src/snapshot_middleware/project.rs index d1b4a2e48..c92f42d8a 100644 --- a/src/snapshot_middleware/project.rs +++ b/src/snapshot_middleware/project.rs @@ -9,6 +9,7 @@ use crate::{ project::{PathNode, Project, ProjectNode}, snapshot::{ InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource, PathIgnoreRule, + SyncRule, }, }; @@ -23,12 +24,19 @@ pub fn snapshot_project( .with_context(|| format!("File was not a valid Rojo project: {}", path.display()))?; let mut context = context.clone(); + context.clear_sync_rules(); let rules = project.glob_ignore_paths.iter().map(|glob| PathIgnoreRule { glob: glob.clone(), base_path: project.folder_location().to_path_buf(), }); + let sync_rules = project.sync_rules.iter().map(|rule| SyncRule { + base_path: project.folder_location().to_path_buf(), + ..rule.clone() + }); + + context.add_sync_rules(sync_rules); context.add_path_ignore_rules(rules); context.set_emit_legacy_scripts( project diff --git a/src/snapshot_middleware/rbxm.rs b/src/snapshot_middleware/rbxm.rs index 969f022dc..7983d0e71 100644 --- a/src/snapshot_middleware/rbxm.rs +++ b/src/snapshot_middleware/rbxm.rs @@ -5,16 +5,13 @@ use memofs::Vfs; use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; -use super::util::PathExt; - #[profiling::function] pub fn snapshot_rbxm( context: &InstanceContext, vfs: &Vfs, path: &Path, + name: &str, ) -> anyhow::Result> { - let name = path.file_name_trim_end(".rbxm")?; - let temp_tree = rbx_binary::from_reader(vfs.read(path)?.as_slice()) .with_context(|| format!("Malformed rbxm file: {}", path.display()))?; @@ -63,6 +60,7 @@ mod test { &InstanceContext::default(), &mut vfs, Path::new("/foo.rbxm"), + "foo", ) .unwrap() .unwrap(); diff --git a/src/snapshot_middleware/rbxmx.rs b/src/snapshot_middleware/rbxmx.rs index 4f128ff6e..4266dc0d7 100644 --- a/src/snapshot_middleware/rbxmx.rs +++ b/src/snapshot_middleware/rbxmx.rs @@ -5,15 +5,12 @@ use memofs::Vfs; use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; -use super::util::PathExt; - pub fn snapshot_rbxmx( context: &InstanceContext, vfs: &Vfs, path: &Path, + name: &str, ) -> anyhow::Result> { - let name = path.file_name_trim_end(".rbxmx")?; - let options = rbx_xml::DecodeOptions::new() .property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown); @@ -75,6 +72,7 @@ mod test { &InstanceContext::default(), &mut vfs, Path::new("/foo.rbxmx"), + "foo", ) .unwrap() .unwrap(); diff --git a/src/snapshot_middleware/toml.rs b/src/snapshot_middleware/toml.rs index 6ee9be47e..086c539d5 100644 --- a/src/snapshot_middleware/toml.rs +++ b/src/snapshot_middleware/toml.rs @@ -9,14 +9,14 @@ use crate::{ snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, }; -use super::{meta_file::AdjacentMetadata, util::PathExt}; +use super::meta_file::AdjacentMetadata; pub fn snapshot_toml( context: &InstanceContext, vfs: &Vfs, path: &Path, + name: &str, ) -> anyhow::Result> { - let name = path.file_name_trim_end(".toml")?; let contents = vfs.read(path)?; let value: toml::Value = toml::from_slice(&contents) @@ -114,6 +114,7 @@ mod test { &InstanceContext::default(), &mut vfs, Path::new("/foo.toml"), + "foo", ) .unwrap() .unwrap(); diff --git a/src/snapshot_middleware/txt.rs b/src/snapshot_middleware/txt.rs index 13d5b9907..207243f9f 100644 --- a/src/snapshot_middleware/txt.rs +++ b/src/snapshot_middleware/txt.rs @@ -6,15 +6,14 @@ use memofs::{IoResultExt, Vfs}; use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; -use super::{meta_file::AdjacentMetadata, util::PathExt}; +use super::meta_file::AdjacentMetadata; pub fn snapshot_txt( context: &InstanceContext, vfs: &Vfs, path: &Path, + name: &str, ) -> anyhow::Result> { - let name = path.file_name_trim_end(".txt")?; - let contents = vfs.read(path)?; let contents_str = str::from_utf8(&contents) .with_context(|| format!("File was not valid UTF-8: {}", path.display()))? @@ -59,10 +58,14 @@ mod test { let mut vfs = Vfs::new(imfs.clone()); - let instance_snapshot = - snapshot_txt(&InstanceContext::default(), &mut vfs, Path::new("/foo.txt")) - .unwrap() - .unwrap(); + let instance_snapshot = snapshot_txt( + &InstanceContext::default(), + &mut vfs, + Path::new("/foo.txt"), + "foo", + ) + .unwrap() + .unwrap(); insta::assert_yaml_snapshot!(instance_snapshot); } diff --git a/tests/tests/build.rs b/tests/tests/build.rs index 01e87da73..f23d4a4e4 100644 --- a/tests/tests/build.rs +++ b/tests/tests/build.rs @@ -59,6 +59,9 @@ gen_build_tests! { txt_in_folder, unresolved_values, weldconstraint, + sync_rule_alone, + sync_rule_complex, + sync_rule_nested_projects, } fn run_build_test(test_name: &str) { diff --git a/tests/tests/serve.rs b/tests/tests/serve.rs index d55d339bb..5d716b397 100644 --- a/tests/tests/serve.rs +++ b/tests/tests/serve.rs @@ -255,3 +255,54 @@ fn add_optional_folder() { ); }); } + +#[test] +fn sync_rule_alone() { + run_serve_test("sync_rule_alone", |session, mut redactions| { + let info = session.get_api_rojo().unwrap(); + let root_id = info.root_instance_id; + + assert_yaml_snapshot!("sync_rule_alone_info", redactions.redacted_yaml(info)); + + let read_response = session.get_api_read(root_id).unwrap(); + assert_yaml_snapshot!( + "sync_rule_alone_all", + read_response.intern_and_redact(&mut redactions, root_id) + ); + }); +} + +#[test] +fn sync_rule_complex() { + run_serve_test("sync_rule_complex", |session, mut redactions| { + let info = session.get_api_rojo().unwrap(); + let root_id = info.root_instance_id; + + assert_yaml_snapshot!("sync_rule_complex_info", redactions.redacted_yaml(info)); + + let read_response = session.get_api_read(root_id).unwrap(); + assert_yaml_snapshot!( + "sync_rule_complex_all", + read_response.intern_and_redact(&mut redactions, root_id) + ); + }); +} + +#[test] +fn sync_rule_no_extension() { + run_serve_test("sync_rule_no_extension", |session, mut redactions| { + let info = session.get_api_rojo().unwrap(); + let root_id = info.root_instance_id; + + assert_yaml_snapshot!( + "sync_rule_no_extension_info", + redactions.redacted_yaml(info) + ); + + let read_response = session.get_api_read(root_id).unwrap(); + assert_yaml_snapshot!( + "sync_rule_no_extension_all", + read_response.intern_and_redact(&mut redactions, root_id) + ); + }); +}