-
Notifications
You must be signed in to change notification settings - Fork 867
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
[docfx] adding --watch option to build command #10010
Open
rmannibucau
wants to merge
4
commits into
dotnet:main
Choose a base branch
from
rmannibucau:rmannibucau/watch-option
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.ComponentModel; | ||
using Spectre.Console.Cli; | ||
|
||
namespace Docfx; | ||
|
||
internal class DefaultBuildCommandOptions : LogOptions | ||
{ | ||
[Description("Specify the output base directory")] | ||
[CommandOption("-o|--output")] | ||
public string OutputFolder { get; set; } | ||
|
||
[Description("Path to docfx.json")] | ||
[CommandArgument(0, "[config]")] | ||
public string ConfigFile { get; set; } | ||
|
||
[Description("Specify a list of global metadata in key value pairs (e.g. --metadata _appTitle=\"My App\" --metadata _disableContribution)")] | ||
[CommandOption("-m|--metadata")] | ||
public string[] Metadata { get; set; } | ||
|
||
[Description("Specify the urls of xrefmap used by content files.")] | ||
[CommandOption("-x|--xref")] | ||
[TypeConverter(typeof(ArrayOptionConverter))] | ||
public IEnumerable<string> XRefMaps { get; set; } | ||
|
||
[Description("Specify the template name to apply to. If not specified, output YAML file will not be transformed.")] | ||
[CommandOption("-t|--template")] | ||
[TypeConverter(typeof(ArrayOptionConverter))] | ||
public IEnumerable<string> Templates { get; set; } | ||
|
||
[Description("Specify which theme to use. By default 'default' theme is offered.")] | ||
[CommandOption("--theme")] | ||
[TypeConverter(typeof(ArrayOptionConverter))] | ||
public IEnumerable<string> Themes { get; set; } | ||
|
||
[Description("Specify the hostname of the hosted website (e.g., 'localhost' or '*')")] | ||
[CommandOption("-n|--hostname")] | ||
public string Host { get; set; } | ||
|
||
[Description("Specify the port of the hosted website")] | ||
[CommandOption("-p|--port")] | ||
public int? Port { get; set; } | ||
|
||
[Description("Open a file in a web browser when the hosted website starts.")] | ||
[CommandOption("--open-file <RELATIVE_PATH>")] | ||
public string OpenFile { get; set; } | ||
|
||
[Description("Run in debug mode. With debug mode, raw model and view model will be exported automatically when it encounters error when applying templates. If not specified, it is false.")] | ||
[CommandOption("--debug")] | ||
public bool EnableDebugMode { get; set; } | ||
|
||
[Description("The output folder for files generated for debugging purpose when in debug mode. If not specified, it is ${TempPath}/docfx")] | ||
[CommandOption("--debugOutput")] | ||
public string OutputFolderForDebugFiles { get; set; } | ||
|
||
[Description("If set to true, data model to run template script will be extracted in .raw.model.json extension")] | ||
[CommandOption("--exportRawModel")] | ||
public bool ExportRawModel { get; set; } | ||
|
||
[Description("Specify the output folder for the raw model. If not set, the raw model will be generated to the same folder as the output documentation")] | ||
[CommandOption("--rawModelOutputFolder")] | ||
public string RawModelOutputFolder { get; set; } | ||
|
||
[Description("Specify the output folder for the view model. If not set, the view model will be generated to the same folder as the output documentation")] | ||
[CommandOption("--viewModelOutputFolder")] | ||
public string ViewModelOutputFolder { get; set; } | ||
|
||
[Description("If set to true, data model to apply template will be extracted in .view.model.json extension")] | ||
[CommandOption("--exportViewModel")] | ||
public bool ExportViewModel { get; set; } | ||
|
||
[Description("If set to true, template will not be actually applied to the documents. This option is always used with --exportRawModel or --exportViewModel is set so that only raw model files or view model files are generated.")] | ||
[CommandOption("--dryRun")] | ||
public bool DryRun { get; set; } | ||
|
||
[Description("Set the max parallelism, 0 is auto.")] | ||
[CommandOption("--maxParallelism")] | ||
public int? MaxParallelism { get; set; } | ||
|
||
[Description("Set the parameters for markdown engine, value should be a JSON string.")] | ||
[CommandOption("--markdownEngineProperties")] | ||
public string MarkdownEngineProperties { get; set; } | ||
|
||
[Description("Set the order of post processors in plugins")] | ||
[CommandOption("--postProcessors")] | ||
[TypeConverter(typeof(ArrayOptionConverter))] | ||
public IEnumerable<string> PostProcessors { get; set; } | ||
|
||
[Description("Disable fetching Git related information for articles. By default it is enabled and may have side effect on performance when the repo is large.")] | ||
[CommandOption("--disableGitFeatures")] | ||
public bool DisableGitFeatures { get; set; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Docfx.Common; | ||
using Spectre.Console.Cli; | ||
|
||
namespace Docfx; | ||
|
||
internal class WatchCommand : Command<WatchCommandOptions> | ||
{ | ||
public override int Execute(CommandContext context, WatchCommandOptions settings) | ||
{ | ||
return CommandHelper.Run(settings, () => | ||
{ | ||
var (config, baseDirectory) = Docset.GetConfig(settings.ConfigFile); | ||
BuildCommand.MergeOptionsToConfig(settings, config.build, baseDirectory); | ||
var conf = new BuildOptions(); | ||
var serveDirectory = RunBuild.Exec(config.build, conf, baseDirectory, settings.OutputFolder); | ||
|
||
void onChange() | ||
{ | ||
RunBuild.Exec(config.build, conf, baseDirectory, settings.OutputFolder); | ||
} | ||
|
||
// always do an initial rendering to start from something | ||
onChange(); | ||
|
||
if (!settings.NoServe) | ||
{ | ||
using var watcher = Watch(baseDirectory, config.build, onChange); | ||
Serve(serveDirectory, settings.Host, settings.Port, settings.OpenBrowser, settings.OpenFile); | ||
} | ||
else | ||
{ | ||
using var watcher = Watch(baseDirectory, config.build, onChange); | ||
|
||
// just block but here we can't use the host mecanism | ||
// since we didn't start the server so use console one | ||
using var canceller = new CancellationTokenSource(); | ||
Console.CancelKeyPress += (sender, args) => canceller.Cancel(); | ||
Task.Delay(Timeout.Infinite, canceller.Token).Wait(); | ||
} | ||
}); | ||
} | ||
|
||
internal void Serve(string serveDirectory, string host, int? port, bool openBrowser, string openFile) { | ||
if (CommandHelper.IsTcpPortAlreadyUsed(host, port)) | ||
{ | ||
Logger.LogError($"Serve option specified. But TCP port {port ?? 8080} is already being in use."); | ||
return; | ||
} | ||
RunServe.Exec(serveDirectory, host, port, openBrowser, openFile); | ||
} | ||
|
||
// For now it is a simplistic implementation, in particular on the glob to filter mappping | ||
// but it should be sufficient for most cases. | ||
internal static IDisposable Watch(string baseDir, BuildJsonConfig config, Action onChange) | ||
{ | ||
FileSystemWatcher watcher = new(baseDir) | ||
{ | ||
IncludeSubdirectories = true, | ||
NotifyFilter = NotifyFilters.Attributes | NotifyFilters.Size | NotifyFilters.FileName | | ||
NotifyFilters.DirectoryName | NotifyFilters.LastWrite | ||
}; | ||
|
||
if (WatchAll(config)) | ||
{ | ||
watcher.Filters.Add("*.*"); | ||
} | ||
else | ||
{ | ||
RegisterFiles(watcher, config.Content); | ||
RegisterFiles(watcher, config.Resource); | ||
|
||
IEnumerable<string> forcedFiles = ["docfx.json", "*.md", "toc.yml"]; | ||
foreach (var forcedFile in forcedFiles) | ||
{ | ||
if (!watcher.Filters.Any(f => f == forcedFile)) | ||
{ | ||
watcher.Filters.Add(forcedFile); | ||
} | ||
} | ||
} | ||
|
||
// avoid to call onChange() in chain so await "last" event before re-rendering | ||
var cancellation = new CancellationTokenSource[] { null }; | ||
async void debounce() | ||
{ | ||
var token = new CancellationTokenSource(); | ||
lock (cancellation) | ||
{ | ||
ResetToken(cancellation); | ||
cancellation[0] = token; | ||
} | ||
|
||
await Task.Delay(100, token.Token); | ||
if (!token.IsCancellationRequested) | ||
{ | ||
onChange(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if the build invoked by |
||
} | ||
} | ||
|
||
watcher.Changed += (_, _) => debounce(); | ||
watcher.Created += (_, _) => debounce(); | ||
watcher.Deleted += (_, _) => debounce(); | ||
watcher.Renamed += (_, _) => debounce(); | ||
watcher.EnableRaisingEvents = true; | ||
|
||
return new DisposableAction(() => | ||
{ | ||
watcher.Dispose(); | ||
lock (cancellation) | ||
{ | ||
ResetToken(cancellation); | ||
} | ||
}); | ||
} | ||
|
||
private static void ResetToken(CancellationTokenSource[] cancellation) | ||
{ | ||
var token = cancellation[0]; | ||
if (token is not null && !token.IsCancellationRequested) | ||
{ | ||
token.Cancel(); | ||
token.Dispose(); | ||
} | ||
} | ||
|
||
internal static bool WatchAll(BuildJsonConfig config) | ||
{ | ||
return ((IEnumerable<FileMapping>)[config.Resource, config.Content]) | ||
.Where(it => it is not null) | ||
.SelectMany(it => it.Items) | ||
.SelectMany(it => it.Files) | ||
.Any(it => it.EndsWith("**")); | ||
} | ||
|
||
internal static void RegisterFiles(FileSystemWatcher watcher, FileMapping content) | ||
{ | ||
foreach (var pattern in content? | ||
.Items? | ||
.SelectMany(it => it.Files) | ||
.SelectMany(SanitizePatternForWatcher) | ||
.Distinct() | ||
.ToList()) | ||
{ | ||
watcher.Filters.Add(pattern); | ||
} | ||
} | ||
|
||
// as of now it can list too much files but will less hurt to render more often with deboucning | ||
// than not rendering when needed. | ||
internal static IEnumerable<string> SanitizePatternForWatcher(string file) | ||
{ | ||
var name = file[(file.LastIndexOf('.') + 1)..]; // "**/images/**/*.png" => "*.png" | ||
if (name.EndsWith('}')) // "**/*.{md,yml}" => "*.md" and "*.yml" | ||
{ | ||
var start = name.IndexOf('{'); | ||
if (start > 0) | ||
{ | ||
var prefix = file[0..start]; | ||
return file[(start + 1)..^1] | ||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) | ||
.Select(extension => $"{prefix}{extension}"); | ||
} | ||
} | ||
return [name]; | ||
} | ||
|
||
internal class DisposableAction(Action action) : IDisposable | ||
{ | ||
public void Dispose() => action(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.ComponentModel; | ||
using Spectre.Console.Cli; | ||
|
||
namespace Docfx; | ||
|
||
[Description("Generate client-only website combining API in YAML files and conceptual files and watch them for changes")] | ||
internal class WatchCommandOptions : DefaultBuildCommandOptions | ||
{ | ||
[Description("Host the generated documentation to a website")] | ||
[CommandOption("--no-serve")] | ||
[DefaultValue("true")] | ||
public bool NoServe { get; set; } | ||
|
||
[Description("Open a web browser when the hosted website starts.")] | ||
[CommandOption("--open-browser")] | ||
[DefaultValue("false")] | ||
public bool OpenBrowser { get; set; } | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can start with a simple deterministic watch implementation that watches all files changes in the
docfx.json
directory, except for the output directory (_site
)