diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5fc22b258..735e38cd5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -71,12 +71,18 @@ jobs:
path: test/e2e/path
edition: ${{ matrix.edition.name }}
- - name: E2E (enterprise)
+ - name: E2E (enterprise/html)
uses: ./.github/actions/e2e
with:
path: enterprise/e2e/html
edition: ${{ matrix.edition.name }}
if: matrix.edition.name == 'enterprise'
+ - name: E2E (enterprise/headless)
+ uses: ./.github/actions/e2e
+ with:
+ path: enterprise/e2e/headless
+ edition: ${{ matrix.edition.name }}
+ if: matrix.edition.name == 'enterprise'
# Public instance
- run: docker build . --file public/Dockerfile --progress plain
diff --git a/Makefile b/Makefile
index 490a1ad52..2fabc5fa1 100644
--- a/Makefile
+++ b/Makefile
@@ -78,6 +78,7 @@ docker: docker-build
$(MAKE) -C test/e2e/chaos EDITION=$(EDITION)
ifeq ($(ENTERPRISE),ON)
$(MAKE) -C enterprise/e2e/html EDITION=$(EDITION)
+ $(MAKE) -C enterprise/e2e/headless EDITION=$(EDITION)
endif
.PHONY: docker-benchmark
diff --git a/collections/self/v1/schemas/api/list/response.json b/collections/self/v1/schemas/api/list/response.json
index de43983ae..88f0f7a50 100644
--- a/collections/self/v1/schemas/api/list/response.json
+++ b/collections/self/v1/schemas/api/list/response.json
@@ -125,6 +125,9 @@
},
"website": {
"$ref": "https://schemas.sourcemeta.com/sourcemeta/std/v0/ietf/uri/url"
+ },
+ "documentation": {
+ "$ref": "https://schemas.sourcemeta.com/sourcemeta/std/v0/ietf/uri/uri-relative"
}
},
"additionalProperties": false
diff --git a/collections/self/v1/schemas/configuration/collection.json b/collections/self/v1/schemas/configuration/collection.json
index 28d01e8d4..47c2ddda1 100644
--- a/collections/self/v1/schemas/configuration/collection.json
+++ b/collections/self/v1/schemas/configuration/collection.json
@@ -30,6 +30,9 @@
"x-sourcemeta-one:provenance": {
"$ref": "./rpath.json"
},
+ "x-sourcemeta-one:documentation": {
+ "$ref": "https://schemas.sourcemeta.com/sourcemeta/std/v0/ieee/posix/2017/path"
+ },
"baseUri": {
"$ref": "https://schemas.sourcemeta.com/sourcemeta/std/v0/ietf/uri/uri-reference"
},
diff --git a/docs/api.md b/docs/api.md
index 9c04a6257..ece32d981 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -82,6 +82,7 @@ navigation and discovery purposes.
| `/email` | String | No | The e-mail address associated with the directory |
| `/github` | String | No | The GitHub organisation or repository associated with the directory |
| `/website` | String | No | The external URL associated with the directory |
+ | `/documentation` | String | No | For directories that correspond to a collection with documentation configured, a relative URL to the static file containing the raw Markdown content. Only available in the [Enterprise](commercial.md) edition |
| `/schemas` | Integer | Yes | The recursive count of schemas in this directory |
| `/entries` | Array | Yes | The entries inside the directory |
| `/entries/*/type` | String | Yes | The type of the entry (`schema` or `directory`) |
@@ -106,6 +107,27 @@ navigation and discovery purposes.
The directory does not exist.
+## Static Files
+
+### Fetch
+
+*This endpoint serves a static file by its hash identifier.*
+
+```
+GET {base_path}/self/v1/static/{hash}
+```
+
+Static files are external assets (such as documentation Markdown files)
+that have been indexed into the registry.
+
+=== "200"
+
+ The raw file content.
+
+=== "404"
+
+ The static file does not exist.
+
## Schemas
### Fetch
diff --git a/docs/commercial.md b/docs/commercial.md
index 5bd75bc16..e5e841377 100644
--- a/docs/commercial.md
+++ b/docs/commercial.md
@@ -25,9 +25,10 @@ Sourcemeta One is available in two editions:
release, the code transitions to AGPL-3.0.
- **Enterprise**: Includes the [Standard
- Library](https://github.com/sourcemeta/std), additional features, and supply
- chain security capabilities not available in the Community edition. Requires
- a [commercial
+ Library](https://github.com/sourcemeta/std), additional features such as
+ [custom linter rules](configuration.md#linter), [collection
+ documentation](configuration.md#documentation), and supply chain security
+ capabilities not available in the Community edition. Requires a [commercial
license](https://github.com/sourcemeta/one/blob/main/LICENSE-COMMERCIAL)
from Sourcemeta.
diff --git a/docs/configuration.md b/docs/configuration.md
index 5e65f3464..71cd3f658 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -147,6 +147,7 @@ contain the actual schema definitions that power your instance.
| `/ignore` | Array | No | None | An array of file paths (relative to the configuration file location) to exclude from the schema collection. See the [JSON Schema CLI configuration](https://github.com/sourcemeta/jsonschema/blob/main/docs/configuration.markdown) for more information |
| `/x-sourcemeta-one:evaluate` | Boolean | No | `true` | When set to `false`, disable the evaluation API for this schema collection. This is useful if you will never make use of the [evaluation API](api.md) and want to speed up the generation of the instance |
| `/x-sourcemeta-one:alert` | String | No | N/A | When set, provide a human-readable alert on both the API and the HTML explorer for every schema in the collection. This is useful to provide any important message to consumers. The web explorer renders this as Markdown |
+| `/x-sourcemeta-one:documentation` (**Enterprise**) | String | No | N/A | Path (relative to the configuration file) to a Markdown documentation file for this collection. Rendered in the web explorer below the file listing. See the [Documentation](#documentation) section for more information |
!!! warning
@@ -295,6 +296,44 @@ Rule file paths are relative to the configuration file location. You can list
multiple rules in the array to enforce several constraints at once. Rule names
must be unique across all rules in a collection.
+### Documentation
+
+!!! success "Enterprise"
+
+ Collection documentation is only available in the
+ [Enterprise](commercial.md) edition. Learn more about [commercial
+ licensing](commercial.md).
+
+The `x-sourcemeta-one:documentation` property lets you attach a Markdown file
+to a schema collection. The content is rendered in the web explorer below the
+file listing, similar to how GitHub renders a README.
+
+```json hl_lines="6" title="one.json"
+{
+ "url": "https://schemas.example.com",
+ "contents": {
+ "my-collection": {
+ "path": "./schemas",
+ "x-sourcemeta-one:documentation": "./docs/my-collection.md"
+ }
+ }
+}
+```
+
+The path is relative to the configuration file location. The documentation
+file must exist at indexing time.
+
+The Markdown content is rendered using GitHub Flavored Markdown (GFM),
+supporting tables, autolinks, strikethrough, task lists, and footnotes.
+Unsafe HTML (such as `
+
+And this too:
diff --git a/enterprise/e2e/html/hurl/documentation.hurl b/enterprise/e2e/html/hurl/documentation.hurl
new file mode 100644
index 000000000..d96b305af
--- /dev/null
+++ b/enterprise/e2e/html/hurl/documentation.hurl
@@ -0,0 +1,145 @@
+# Collection with documentation has the documentation URL in the full list response
+GET {{base}}/self/v1/api/list/test
+HTTP 200
+Content-Type: application/json
+Access-Control-Allow-Origin: *
+Link: ; rel="describedby"
+[Captures]
+last_response: body
+schema_path: header "Link" regex "<([^>]+)>"
+documentation_url: jsonpath "$.documentation"
+[Asserts]
+header "ETag" exists
+header "Last-Modified" exists
+jsonpath "$.path" == "/test"
+jsonpath "$.url" == "{{base}}/test"
+jsonpath "$.schemas" == 1
+jsonpath "$.health" == 67
+jsonpath "$.breadcrumb" count == 1
+jsonpath "$.breadcrumb[0].name" == "test"
+jsonpath "$.breadcrumb[0].path" == "/test/"
+jsonpath "$.documentation" matches "^/self/v1/static/[a-f0-9]{64}$"
+jsonpath "$.entries" count == 1
+jsonpath "$.entries[0].name" == "object"
+jsonpath "$.entries[0].type" == "schema"
+jsonpath "$.entries[0].path" == "/test/object"
+jsonpath "$.entries[0].identifier" == "{{base}}/test/object"
+jsonpath "$.entries[0].title" == "My Object"
+jsonpath "$.entries[0].description" == "An Object Example"
+jsonpath "$.entries[0].health" == 67
+jsonpath "$.entries[0].documentation" not exists
+
+POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
+```
+{{last_response}}
+```
+HTTP 200
+Link: ; rel="describedby"
+[Asserts]
+jsonpath "$.valid" == true
+
+# The static file endpoint serves the raw markdown content
+GET {{base}}{{documentation_url}}
+HTTP 200
+[Asserts]
+header "ETag" exists
+header "Last-Modified" exists
+body contains "# Test Collection"
+body contains "**test schemas**"
+body contains "[example guide](https://example.com/guide)"
+
+# HTML page for collection with documentation renders the markdown
+GET {{base}}/test
+Accept: text/html
+HTTP 200
+Content-Type: text/html
+[Asserts]
+xpath "boolean(//div[contains(@class, 'card')]//div[contains(@class, 'documentation')])" == true
+xpath "boolean(//div[contains(@class, 'documentation')]//h1[text()='Test Collection'])" == true
+xpath "boolean(//strong[text()='test schemas'])" == true
+xpath "boolean(//a[@href='https://example.com/guide'][text()='example guide'])" == true
+xpath "boolean(//em[text()='emphasis'])" == true
+xpath "boolean(//code[text()='inline code'])" == true
+
+# XSS in documentation is filtered (check within the documentation container)
+GET {{base}}/test
+Accept: text/html
+HTTP 200
+Content-Type: text/html
+[Asserts]
+xpath "count(//div[contains(@class, 'documentation')]//script)" == 0
+xpath "count(//div[contains(@class, 'documentation')]//iframe)" == 0
+
+# The self collection does not have the documentation field
+GET {{base}}/self/v1/api/list/self
+HTTP 200
+Content-Type: application/json
+Access-Control-Allow-Origin: *
+Link: ; rel="describedby"
+[Captures]
+last_response: body
+schema_path: header "Link" regex "<([^>]+)>"
+[Asserts]
+header "ETag" exists
+header "Last-Modified" exists
+jsonpath "$.path" == "/self"
+jsonpath "$.url" == "{{base}}/self"
+jsonpath "$.schemas" == 31
+jsonpath "$.health" == 100
+jsonpath "$.documentation" not exists
+
+POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
+```
+{{last_response}}
+```
+HTTP 200
+Link: ; rel="describedby"
+[Asserts]
+jsonpath "$.valid" == true
+
+# The self collection HTML page does not render documentation
+GET {{base}}/self
+Accept: text/html
+HTTP 200
+Content-Type: text/html
+[Asserts]
+xpath "boolean(//h1[text()='Test Collection'])" == false
+
+# Parent listing does not include documentation on child entries
+GET {{base}}/self/v1/api/list
+HTTP 200
+Content-Type: application/json
+Access-Control-Allow-Origin: *
+Link: ; rel="describedby"
+[Captures]
+last_response: body
+schema_path: header "Link" regex "<([^>]+)>"
+[Asserts]
+header "ETag" exists
+header "Last-Modified" exists
+jsonpath "$.path" == "/"
+jsonpath "$.url" == "{{base}}"
+jsonpath "$.breadcrumb" count == 0
+jsonpath "$.schemas" == 32
+jsonpath "$.entries" count == 2
+jsonpath "$.entries[0].name" == "self"
+jsonpath "$.entries[0].type" == "directory"
+jsonpath "$.entries[0].health" == 100
+jsonpath "$.entries[0].schemas" == 31
+jsonpath "$.entries[0].path" == "/self/"
+jsonpath "$.entries[0].documentation" not exists
+jsonpath "$.entries[1].name" == "test"
+jsonpath "$.entries[1].type" == "directory"
+jsonpath "$.entries[1].health" == 67
+jsonpath "$.entries[1].schemas" == 1
+jsonpath "$.entries[1].path" == "/test/"
+jsonpath "$.entries[1].documentation" not exists
+
+POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
+```
+{{last_response}}
+```
+HTTP 200
+Link: ; rel="describedby"
+[Asserts]
+jsonpath "$.valid" == true
diff --git a/enterprise/e2e/html/one.json b/enterprise/e2e/html/one.json
index 0be9a1a47..5db95ec26 100644
--- a/enterprise/e2e/html/one.json
+++ b/enterprise/e2e/html/one.json
@@ -6,7 +6,8 @@
"path": "./schemas/test",
"lint": {
"rules": [ "./rules/camelcase.json" ]
- }
+ },
+ "x-sourcemeta-one:documentation": "./docs/test-readme.md"
}
}
}
diff --git a/enterprise/index/CMakeLists.txt b/enterprise/index/CMakeLists.txt
index 5c6b40366..6f8b6ec20 100644
--- a/enterprise/index/CMakeLists.txt
+++ b/enterprise/index/CMakeLists.txt
@@ -3,11 +3,16 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT one NAME enterprise_index
target_compile_definitions(sourcemeta_one_enterprise_index PUBLIC SOURCEMETA_ONE_ENTERPRISE)
+target_link_libraries(sourcemeta_one_enterprise_index PRIVATE sourcemeta::one::metapack)
target_link_libraries(sourcemeta_one_enterprise_index PUBLIC sourcemeta::one::resolver)
-target_link_libraries(sourcemeta_one_enterprise_index PUBLIC sourcemeta::blaze::configuration)
+target_link_libraries(sourcemeta_one_enterprise_index PUBLIC sourcemeta::one::build)
+
target_link_libraries(sourcemeta_one_enterprise_index PRIVATE sourcemeta::core::error)
-target_link_libraries(sourcemeta_one_enterprise_index PRIVATE sourcemeta::blaze::compiler)
-target_link_libraries(sourcemeta_one_enterprise_index PRIVATE sourcemeta::blaze::alterschema)
target_link_libraries(sourcemeta_one_enterprise_index PRIVATE sourcemeta::core::yaml)
-target_link_libraries(sourcemeta_one_enterprise_index PUBLIC sourcemeta::one::build)
+target_link_libraries(sourcemeta_one_enterprise_index PUBLIC sourcemeta::core::html)
+target_link_libraries(sourcemeta_one_enterprise_index PRIVATE sourcemeta::core::markdown)
target_link_libraries(sourcemeta_one_enterprise_index PUBLIC sourcemeta::core::jsonschema)
+
+target_link_libraries(sourcemeta_one_enterprise_index PRIVATE sourcemeta::blaze::compiler)
+target_link_libraries(sourcemeta_one_enterprise_index PUBLIC sourcemeta::blaze::configuration)
+target_link_libraries(sourcemeta_one_enterprise_index PUBLIC sourcemeta::blaze::alterschema)
diff --git a/enterprise/index/enterprise_index.cc b/enterprise/index/enterprise_index.cc
index 69523c1d4..199a4daf4 100644
--- a/enterprise/index/enterprise_index.cc
+++ b/enterprise/index/enterprise_index.cc
@@ -5,7 +5,9 @@
#include
#include
+#include
#include
+#include
#include // std::string
@@ -47,4 +49,18 @@ auto load_custom_lint_rules(
}
}
+auto render_documentation(sourcemeta::core::HTMLWriter &writer,
+ const std::filesystem::path &metapack_path) -> void {
+ const auto content{metapack_read_text(metapack_path)};
+ if (!content.has_value()) {
+ return;
+ }
+
+ writer.div().attribute("class", "card mt-4");
+ writer.div().attribute("class", "card-body documentation");
+ writer.raw(sourcemeta::core::markdown_to_html(content.value()));
+ writer.close();
+ writer.close();
+}
+
} // namespace sourcemeta::one
diff --git a/enterprise/index/include/sourcemeta/one/enterprise_index.h b/enterprise/index/include/sourcemeta/one/enterprise_index.h
index 5a78e903a..9097e1750 100644
--- a/enterprise/index/include/sourcemeta/one/enterprise_index.h
+++ b/enterprise/index/include/sourcemeta/one/enterprise_index.h
@@ -7,6 +7,9 @@
#include
#include
+#include
+
+#include // std::filesystem::path
#include // std::string_view
#include // std::unordered_set
@@ -19,6 +22,9 @@ auto load_custom_lint_rules(
const sourcemeta::one::Resolver &resolver,
const sourcemeta::one::BuildDynamicCallback &callback) -> void;
+auto render_documentation(sourcemeta::core::HTMLWriter &writer,
+ const std::filesystem::path &metapack_path) -> void;
+
} // namespace sourcemeta::one
#endif
diff --git a/src/actions/action_serve_static_file_v1.h b/src/actions/action_serve_static_file_v1.h
new file mode 100644
index 000000000..465d5886f
--- /dev/null
+++ b/src/actions/action_serve_static_file_v1.h
@@ -0,0 +1,51 @@
+#ifndef SOURCEMETA_ONE_ACTIONS_SERVE_STATIC_FILE_V1_H
+#define SOURCEMETA_ONE_ACTIONS_SERVE_STATIC_FILE_V1_H
+
+#include
+
+#include
+#include
+
+#include "action_serve_metapack_file_v1.h"
+
+#include // std::filesystem
+#include // std::span
+#include // std::string
+#include // std::string_view
+
+class ActionServeStaticFile_v1 : public sourcemeta::one::Action {
+public:
+ ActionServeStaticFile_v1(
+ const std::filesystem::path &base,
+ const sourcemeta::core::URITemplateRouterView &router,
+ const sourcemeta::core::URITemplateRouter::Identifier identifier)
+ : sourcemeta::one::Action{base, router.base_path()} {
+ router.arguments(identifier, [this](const auto &key, const auto &value) {
+ if (key == "errorSchema") {
+ this->error_schema_ = std::get(value);
+ }
+ });
+ }
+
+ auto run(const std::span matches,
+ sourcemeta::one::HTTPRequest &request,
+ sourcemeta::one::HTTPResponse &response) -> void override {
+ if (matches.empty()) {
+ sourcemeta::one::json_error(
+ request, response, sourcemeta::one::STATUS_NOT_FOUND, "missing-hash",
+ "No hash was provided in the URL", this->error_schema_);
+ return;
+ }
+
+ const auto absolute_path{this->base() / "static" /
+ (std::string{matches.front()} + ".metapack")};
+ ActionServeMetapackFile_v1::serve(absolute_path, sourcemeta::one::STATUS_OK,
+ true, {}, {}, request, response,
+ this->error_schema_);
+ }
+
+private:
+ std::string_view error_schema_;
+};
+
+#endif
diff --git a/src/actions/dispatch.cc b/src/actions/dispatch.cc
index 17a0dc50b..3082d2efe 100644
--- a/src/actions/dispatch.cc
+++ b/src/actions/dispatch.cc
@@ -7,6 +7,7 @@
#include "action_schema_search_v1.h"
#include "action_serve_explorer_artifact_v1.h"
#include "action_serve_schema_artifact_v1.h"
+#include "action_serve_static_file_v1.h"
#include "action_serve_static_v1.h"
#include // std::array
diff --git a/src/actions/include/sourcemeta/one/actions.h b/src/actions/include/sourcemeta/one/actions.h
index 7235ed50d..0817b1d17 100644
--- a/src/actions/include/sourcemeta/one/actions.h
+++ b/src/actions/include/sourcemeta/one/actions.h
@@ -24,7 +24,8 @@ namespace sourcemeta::one {
X(EXPLORER_ARTIFACT_V1, ActionServeExplorerArtifact_v1) \
X(JSONSCHEMA_EVALUATE_V1, ActionJSONSchemaEvaluate_v1) \
X(SCHEMA_SEARCH_V1, ActionSchemaSearch_v1) \
- X(SERVE_STATIC_V1, ActionServeStatic_v1)
+ X(SERVE_STATIC_V1, ActionServeStatic_v1) \
+ X(SERVE_STATIC_FILE_V1, ActionServeStaticFile_v1)
#define SOURCEMETA_ONE_DEFINE_ACTION_TYPE(Name, Class) ACTION_TYPE_##Name,
diff --git a/src/build/CMakeLists.txt b/src/build/CMakeLists.txt
index e5888be6b..2741ed098 100644
--- a/src/build/CMakeLists.txt
+++ b/src/build/CMakeLists.txt
@@ -4,4 +4,6 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT one NAME build
target_link_libraries(sourcemeta_one_build PUBLIC
sourcemeta::core::json
sourcemeta::core::io
- sourcemeta::one::resolver)
+ sourcemeta::core::crypto
+ sourcemeta::one::resolver
+ sourcemeta::one::configuration)
diff --git a/src/build/delta.cc b/src/build/delta.cc
index 32a157272..aed2729a1 100644
--- a/src/build/delta.cc
+++ b/src/build/delta.cc
@@ -1,14 +1,19 @@
#include
+#include
+
+#include
#include "rules.h"
#include // std::ranges::sort, std::ranges::all_of
#include // assert
#include // std::size_t
+#include // std::ostringstream
#include // std::string
#include // std::string_view
#include // std::unordered_map
#include // std::unordered_set
+#include // std::get_if
#include // std::vector
namespace sourcemeta::one {
@@ -194,7 +199,8 @@ auto delta(const BuildPhase phase, const BuildPlan::Type build_type,
const BuildState &entries, const std::filesystem::path &output,
const Resolver::Views &schemas, const std::string_view version,
const bool incremental, const std::string_view comment,
- const BuildLimits &limits) -> BuildPlan {
+ const BuildLimits &limits, const Configuration &configuration)
+ -> BuildPlan {
assert(output.is_absolute());
assert(std::ranges::all_of(schemas, [](const auto &entry) {
return entry.second.path.is_absolute() &&
@@ -493,6 +499,34 @@ auto delta(const BuildPhase phase, const BuildPlan::Type build_type,
}
}
+ if (!fast_path_dirty) {
+ for (const auto &[entry_path, entry_value] : configuration.entries) {
+ const auto *collection{
+ std::get_if(&entry_value)};
+ if (!collection ||
+ !collection->extra.defines("x-sourcemeta-one:documentation")) {
+ continue;
+ }
+
+ const auto &documentation_path{
+ collection->extra.at("x-sourcemeta-one:documentation").to_string()};
+ std::ostringstream hash_stream;
+ sourcemeta::core::sha256(documentation_path, hash_stream);
+ const auto static_destination{
+ (output / "static" / (hash_stream.str() + ".metapack"))
+ .lexically_normal()
+ .string()};
+ const std::filesystem::path source_path{documentation_path};
+ if (!std::filesystem::exists(static_destination) ||
+ (std::filesystem::exists(source_path) &&
+ std::filesystem::last_write_time(source_path) >
+ std::filesystem::last_write_time(static_destination))) {
+ fast_path_dirty = true;
+ break;
+ }
+ }
+ }
+
if (!fast_path_dirty) {
if (!comment.empty()) {
BuildPlan plan;
@@ -854,8 +888,38 @@ auto delta(const BuildPhase phase, const BuildPlan::Type build_type,
return {.output = output, .type = build_type, .waves = {}, .size = 0};
}
+ bool has_dirty_static_files{false};
+ std::vector dirty_static_collection_paths;
+ for (const auto &[entry_path, entry_value] : configuration.entries) {
+ const auto *collection{
+ std::get_if(&entry_value)};
+ if (!collection ||
+ !collection->extra.defines("x-sourcemeta-one:documentation")) {
+ continue;
+ }
+
+ const auto &documentation_path{
+ collection->extra.at("x-sourcemeta-one:documentation").to_string()};
+ std::ostringstream hash_stream;
+ sourcemeta::core::sha256(documentation_path, hash_stream);
+ const auto static_destination{
+ (output / "static" / (hash_stream.str() + ".metapack"))
+ .lexically_normal()
+ .string()};
+ const std::filesystem::path source_path{documentation_path};
+ const auto is_dirty{
+ !std::filesystem::exists(static_destination) ||
+ (std::filesystem::exists(source_path) &&
+ std::filesystem::last_write_time(source_path) >
+ std::filesystem::last_write_time(static_destination))};
+ if (is_dirty || is_full) {
+ has_dirty_static_files = true;
+ dirty_static_collection_paths.emplace_back(entry_path);
+ }
+ }
+
const auto has_schema_work{is_full || !dirty_set.empty() || has_missing_web ||
- has_potential_stale};
+ has_potential_stale || has_dirty_static_files};
std::vector affected_relative_paths;
if (has_schema_work) {
@@ -870,6 +934,10 @@ auto delta(const BuildPhase phase, const BuildPlan::Type build_type,
}
}
+ for (const auto &collection_path : dirty_static_collection_paths) {
+ affected_relative_paths.emplace_back(collection_path / ".");
+ }
+
auto has_graph_change{false};
if (!has_graph_change) {
for (const auto &schema : active_schemas) {
@@ -918,6 +986,51 @@ auto delta(const BuildPhase phase, const BuildPlan::Type build_type,
const auto all_directories{
collect_affected_directories(schemas_path, all_relative_paths)};
+ {
+ static constexpr std::string_view STATIC_DIRECTORY{"static"};
+ static constexpr std::string_view DOCUMENTATION_MIME{"text/markdown"};
+ std::unordered_set static_file_destinations;
+ for (const auto &[entry_path, entry_value] : configuration.entries) {
+ const auto *collection{
+ std::get_if(&entry_value)};
+ if (!collection ||
+ !collection->extra.defines("x-sourcemeta-one:documentation")) {
+ continue;
+ }
+
+ const auto &documentation_path{
+ collection->extra.at("x-sourcemeta-one:documentation").to_string()};
+ std::ostringstream hash_stream;
+ sourcemeta::core::sha256(documentation_path, hash_stream);
+ auto destination{
+ (output / STATIC_DIRECTORY / (hash_stream.str() + ".metapack"))
+ .lexically_normal()
+ .string()};
+
+ if (static_file_destinations.contains(destination)) {
+ continue;
+ }
+
+ static_file_destinations.insert(destination);
+
+ const std::filesystem::path source_path{documentation_path};
+ bool is_dirty{!std::filesystem::exists(destination)};
+ if (!is_dirty && std::filesystem::exists(source_path)) {
+ const auto source_mtime{
+ std::filesystem::last_write_time(source_path)};
+ const auto destination_mtime{
+ std::filesystem::last_write_time(destination)};
+ is_dirty = source_mtime > destination_mtime;
+ }
+
+ if (is_dirty || is_full) {
+ declare_target(targets, BuildPlan::Action::Type::StaticFile,
+ destination, {documentation_path}, DOCUMENTATION_MIME);
+ dirty_set.insert(std::move(destination));
+ }
+ }
+ }
+
for (const auto &rule : DIRECTORY_RULES) {
if (rule.gate == TargetGate::FullOnly &&
build_type != BuildPlan::Type::Full) {
@@ -1006,6 +1119,26 @@ auto delta(const BuildPhase phase, const BuildPlan::Type build_type,
case DirectoryDependencyKind::ExternalConfig:
rule_dependencies.push_back(configuration_string);
break;
+ case DirectoryDependencyKind::StaticFile: {
+ const auto entry_match{configuration.entries.find(relative)};
+ if (entry_match != configuration.entries.cend()) {
+ const auto *collection{std::get_if(
+ &entry_match->second)};
+ if (collection && collection->extra.defines(
+ "x-sourcemeta-one:documentation")) {
+ const auto &documentation_path{
+ collection->extra.at("x-sourcemeta-one:documentation")
+ .to_string()};
+ std::ostringstream hash_stream;
+ sourcemeta::core::sha256(documentation_path, hash_stream);
+ rule_dependencies.push_back(
+ (output / "static" / (hash_stream.str() + ".metapack"))
+ .lexically_normal()
+ .string());
+ }
+ }
+ break;
+ }
}
}
@@ -1106,6 +1239,23 @@ auto delta(const BuildPhase phase, const BuildPlan::Type build_type,
}
known_bases.insert(explorer_string + '/' + SENTINEL);
+ for (const auto &[entry_path, entry_value] : configuration.entries) {
+ const auto *collection{
+ std::get_if(&entry_value)};
+ if (!collection ||
+ !collection->extra.defines("x-sourcemeta-one:documentation")) {
+ continue;
+ }
+
+ const auto &documentation_path{
+ collection->extra.at("x-sourcemeta-one:documentation").to_string()};
+ std::ostringstream hash_stream;
+ sourcemeta::core::sha256(documentation_path, hash_stream);
+ known_bases.insert((output / "static" / (hash_stream.str() + ".metapack"))
+ .lexically_normal()
+ .string());
+ }
+
std::unordered_set known_ancestors;
known_ancestors.reserve(known_bases.size() * 3);
for (const auto &base : known_bases) {
diff --git a/src/build/include/sourcemeta/one/build.h b/src/build/include/sourcemeta/one/build.h
index 6cf513fe2..12bc24481 100644
--- a/src/build/include/sourcemeta/one/build.h
+++ b/src/build/include/sourcemeta/one/build.h
@@ -6,6 +6,7 @@
#endif
#include
+#include
#include
#include
@@ -46,6 +47,7 @@ struct BuildPlan {
Configuration,
Version,
Routes,
+ StaticFile,
Remove
};
@@ -106,7 +108,8 @@ auto delta(const BuildPhase phase, const BuildPlan::Type build_type,
const BuildState &entries, const std::filesystem::path &output,
const Resolver::Views &schemas, const std::string_view version,
bool incremental, const std::string_view comment,
- const BuildLimits &limits) -> BuildPlan;
+ const BuildLimits &limits, const Configuration &configuration)
+ -> BuildPlan;
} // namespace sourcemeta::one
diff --git a/src/build/rules.h b/src/build/rules.h
index 8089f4b0a..0fb077762 100644
--- a/src/build/rules.h
+++ b/src/build/rules.h
@@ -197,7 +197,8 @@ enum class DirectoryDependencyKind : std::uint8_t {
ChildDirectories,
AllDirectoryListings,
SameDirectoryTarget,
- ExternalConfig
+ ExternalConfig,
+ StaticFile
};
struct DirectoryDependency {
@@ -205,7 +206,7 @@ struct DirectoryDependency {
const char *filename;
};
-static constexpr std::size_t MAX_DIRECTORY_DEPENDENCIES = 2;
+static constexpr std::size_t MAX_DIRECTORY_DEPENDENCIES = 3;
struct DirectoryRule {
BuildPlan::Action::Type action;
@@ -226,8 +227,10 @@ static constexpr std::array DIRECTORY_RULES{{
.dependencies = {{{.kind = DirectoryDependencyKind::SchemaMetadata,
.filename = nullptr},
{.kind = DirectoryDependencyKind::ChildDirectories,
+ .filename = nullptr},
+ {.kind = DirectoryDependencyKind::StaticFile,
.filename = nullptr}}},
- .dependency_count = 2},
+ .dependency_count = 3},
{.action = BuildPlan::Action::Type::SearchIndex,
.filename = "search.metapack",
@@ -244,8 +247,10 @@ static constexpr std::array DIRECTORY_RULES{{
.scope = DirectoryScope::RootOnly,
.only_full_rebuild = false,
.dependencies = {{{.kind = DirectoryDependencyKind::SameDirectoryTarget,
- .filename = "directory.metapack"}}},
- .dependency_count = 1},
+ .filename = "directory.metapack"},
+ {.kind = DirectoryDependencyKind::StaticFile,
+ .filename = nullptr}}},
+ .dependency_count = 2},
{.action = BuildPlan::Action::Type::WebDirectory,
.filename = "directory-html.metapack",
@@ -253,8 +258,10 @@ static constexpr std::array DIRECTORY_RULES{{
.scope = DirectoryScope::NonRoot,
.only_full_rebuild = false,
.dependencies = {{{.kind = DirectoryDependencyKind::SameDirectoryTarget,
- .filename = "directory.metapack"}}},
- .dependency_count = 1},
+ .filename = "directory.metapack"},
+ {.kind = DirectoryDependencyKind::StaticFile,
+ .filename = nullptr}}},
+ .dependency_count = 2},
{.action = BuildPlan::Action::Type::WebNotFound,
.filename = "404.metapack",
diff --git a/src/configuration/include/sourcemeta/one/configuration_error.h b/src/configuration/include/sourcemeta/one/configuration_error.h
index bbd792a40..9cb0e8a3a 100644
--- a/src/configuration/include/sourcemeta/one/configuration_error.h
+++ b/src/configuration/include/sourcemeta/one/configuration_error.h
@@ -142,6 +142,61 @@ class ConfigurationCyclicReferenceError : public std::exception {
std::filesystem::path target_;
};
+class ConfigurationDocumentationNotFoundError : public std::exception {
+public:
+ ConfigurationDocumentationNotFoundError(std::filesystem::path from,
+ sourcemeta::core::Pointer location,
+ std::filesystem::path target)
+ : from_{std::move(from)}, location_{std::move(location)},
+ target_{std::move(target)} {}
+
+ [[nodiscard]] auto what() const noexcept -> const char * override {
+ return "Could not read the documentation file";
+ }
+
+ [[nodiscard]] auto from() const noexcept -> const std::filesystem::path & {
+ return this->from_;
+ }
+
+ [[nodiscard]] auto target() const noexcept -> const std::filesystem::path & {
+ return this->target_;
+ }
+
+ [[nodiscard]] auto location() const noexcept
+ -> const sourcemeta::core::Pointer & {
+ return this->location_;
+ }
+
+private:
+ std::filesystem::path from_;
+ sourcemeta::core::Pointer location_;
+ std::filesystem::path target_;
+};
+
+class ConfigurationDocumentationAtPrefixError : public std::exception {
+public:
+ ConfigurationDocumentationAtPrefixError(std::filesystem::path from,
+ sourcemeta::core::Pointer location)
+ : from_{std::move(from)}, location_{std::move(location)} {}
+
+ [[nodiscard]] auto what() const noexcept -> const char * override {
+ return "The @ prefix is not supported for documentation paths";
+ }
+
+ [[nodiscard]] auto from() const noexcept -> const std::filesystem::path & {
+ return this->from_;
+ }
+
+ [[nodiscard]] auto location() const noexcept
+ -> const sourcemeta::core::Pointer & {
+ return this->location_;
+ }
+
+private:
+ std::filesystem::path from_;
+ sourcemeta::core::Pointer location_;
+};
+
} // namespace sourcemeta::one
#endif
diff --git a/src/configuration/read.cc b/src/configuration/read.cc
index a7c680448..f70533c04 100644
--- a/src/configuration/read.cc
+++ b/src/configuration/read.cc
@@ -124,6 +124,30 @@ auto dereference(const std::filesystem::path &collections_path,
sourcemeta::core::JSON{base.string()});
}
+ if (input.defines("x-sourcemeta-one:documentation") &&
+ input.at("x-sourcemeta-one:documentation").is_string()) {
+ const auto documentation_value{
+ input.at("x-sourcemeta-one:documentation").to_string()};
+ if (documentation_value.starts_with("@")) {
+ throw sourcemeta::one::ConfigurationDocumentationAtPrefixError(
+ base, location.concat({"x-sourcemeta-one:documentation"}));
+ }
+
+ const std::filesystem::path documentation_path{documentation_value};
+ const auto resolved_documentation{std::filesystem::weakly_canonical(
+ base.parent_path() / documentation_path)};
+ if (!std::filesystem::is_regular_file(resolved_documentation)) {
+ throw sourcemeta::one::ConfigurationDocumentationNotFoundError(
+ base, location.concat({"x-sourcemeta-one:documentation"}),
+ resolved_documentation);
+ }
+
+ input.at("x-sourcemeta-one:documentation")
+ .into(sourcemeta::core::JSON{resolved_documentation});
+ all_files.emplace(
+ std::filesystem::weakly_canonical(resolved_documentation).native());
+ }
+
// Recurse on children, if any
} else if (input.defines("contents") && input.at("contents").is_object()) {
// TODO: All of this dance because we can't get mutable iterators out of
diff --git a/src/index/error.h b/src/index/error.h
index 0feb84acf..97aca2706 100644
--- a/src/index/error.h
+++ b/src/index/error.h
@@ -103,6 +103,46 @@ class CustomRuleError : public std::exception {
std::filesystem::path path_;
};
+class DocumentationNotSupportedError : public std::exception {
+public:
+ DocumentationNotSupportedError(std::filesystem::path path)
+ : path_{std::move(path)} {}
+
+ [[nodiscard]] auto what() const noexcept -> const char * override {
+ return "Collection documentation is only available on the enterprise "
+ "edition";
+ }
+
+ [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & {
+ return this->path_;
+ }
+
+private:
+ std::filesystem::path path_;
+};
+
+class StaticFileTooLargeError : public std::exception {
+public:
+ StaticFileTooLargeError(std::filesystem::path path, std::size_t size)
+ : path_{std::move(path)}, size_{size} {}
+
+ [[nodiscard]] auto what() const noexcept -> const char * override {
+ return "Static file exceeds the maximum allowed size";
+ }
+
+ [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & {
+ return this->path_;
+ }
+
+ [[nodiscard]] auto size() const noexcept -> std::size_t {
+ return this->size_;
+ }
+
+private:
+ std::filesystem::path path_;
+ std::size_t size_;
+};
+
} // namespace sourcemeta::one
#endif
diff --git a/src/index/explorer.h b/src/index/explorer.h
index 0330c8573..66ca62f19 100644
--- a/src/index/explorer.h
+++ b/src/index/explorer.h
@@ -1,12 +1,15 @@
#ifndef SOURCEMETA_ONE_INDEX_EXPLORER_H_
#define SOURCEMETA_ONE_INDEX_EXPLORER_H_
+#include "error.h"
+
#include
#include
#include
#include
#include
+#include
#include
#include
#include
@@ -22,10 +25,12 @@
#include // std::numeric_limits
#include // std::accumulate
#include // std::optional
+#include // std::ostringstream
#include // std::string
#include // std::string_view
#include // std::tuple
#include // std::move
+#include // std::get_if
#include // std::vector
static auto make_breadcrumb(const std::string &base_path,
@@ -872,6 +877,32 @@ struct GENERATE_EXPLORER_DIRECTORY_LIST {
inflate_metadata(configuration, relative_path, meta);
+ {
+ const auto entry_match{configuration.entries.find(relative_path)};
+ if (entry_match != configuration.entries.cend()) {
+ const auto *collection{
+ std::get_if(
+ &entry_match->second)};
+ if (collection &&
+ collection->extra.defines("x-sourcemeta-one:documentation")) {
+#if !defined(SOURCEMETA_ONE_ENTERPRISE)
+ throw DocumentationNotSupportedError(std::filesystem::path{
+ collection->extra.at("x-sourcemeta-one:path").to_string()});
+#else
+ const auto &documentation_path{
+ collection->extra.at("x-sourcemeta-one:documentation")
+ .to_string()};
+ std::ostringstream hash_stream;
+ sourcemeta::core::sha256(documentation_path, hash_stream);
+ meta.assign("documentation",
+ sourcemeta::core::JSON{configuration.base_path +
+ "/self/v1/static/" +
+ hash_stream.str()});
+#endif
+ }
+ }
+ }
+
if (!scores.empty()) {
const auto accumulated_health = static_cast(
std::lround(static_cast(std::accumulate(scores.cbegin(),
diff --git a/src/index/generators.h b/src/index/generators.h
index 07194d630..455c949be 100644
--- a/src/index/generators.h
+++ b/src/index/generators.h
@@ -642,6 +642,37 @@ struct GENERATE_STATS {
}
};
+struct GENERATE_STATIC_FILE {
+ static constexpr std::size_t MAX_STATIC_FILE_SIZE{1048576};
+
+ static auto handler(const sourcemeta::one::BuildState &,
+ const sourcemeta::one::BuildPlan::Action &action,
+ const sourcemeta::one::BuildDynamicCallback &,
+ sourcemeta::one::Resolver &,
+ const sourcemeta::one::Configuration &,
+ const sourcemeta::core::JSON &) -> void {
+ const auto timestamp_start{std::chrono::steady_clock::now()};
+ const auto &source_path{action.dependencies.at(0)};
+ const auto file_size{std::filesystem::file_size(source_path)};
+ if (file_size > MAX_STATIC_FILE_SIZE) {
+ throw StaticFileTooLargeError(source_path, file_size);
+ }
+
+ std::ifstream stream{source_path, std::ios::binary};
+ assert(stream.is_open());
+ const std::string content{std::istreambuf_iterator{stream},
+ std::istreambuf_iterator{}};
+
+ std::filesystem::create_directories(action.destination.parent_path());
+ const auto timestamp_end{std::chrono::steady_clock::now()};
+ sourcemeta::one::metapack_write_text(
+ action.destination, content, std::string{action.data},
+ sourcemeta::one::MetapackEncoding::GZIP, {},
+ std::chrono::duration_cast(timestamp_end -
+ timestamp_start));
+ }
+};
+
struct GENERATE_URITEMPLATE_ROUTES {
static auto handler(const sourcemeta::one::BuildState &,
const sourcemeta::one::BuildPlan::Action &action,
@@ -793,6 +824,15 @@ struct GENERATE_URITEMPLATE_ROUTES {
router.add("/self/v1/api/{+any}", next_id++,
sourcemeta::one::ACTION_TYPE_NOT_FOUND_V1, not_found_arguments);
+ {
+ const sourcemeta::core::URITemplateRouter::Argument
+ static_file_arguments[] = {
+ {"errorSchema", std::string_view{error_schema}}};
+ router.add("/self/v1/static/{hash}", next_id++,
+ sourcemeta::one::ACTION_TYPE_SERVE_STATIC_FILE_V1,
+ static_file_arguments);
+ }
+
if (action.data == "Full") {
const sourcemeta::core::URITemplateRouter::Argument static_arguments[] = {
{"path", std::string_view{SOURCEMETA_ONE_STATIC}},
diff --git a/src/index/index.cc b/src/index/index.cc
index a53e48aa8..8131a1a92 100644
--- a/src/index/index.cc
+++ b/src/index/index.cc
@@ -59,7 +59,7 @@ using BuildHandlerFunction = auto (*)(
const sourcemeta::one::Configuration &, const sourcemeta::core::JSON &)
-> void;
-static constexpr std::array HANDLERS{{
+static constexpr std::array HANDLERS{{
&sourcemeta::one::GENERATE_MATERIALISED_SCHEMA::handler,
&sourcemeta::one::GENERATE_POINTER_POSITIONS::handler,
&sourcemeta::one::GENERATE_FRAME_LOCATIONS::handler,
@@ -82,6 +82,7 @@ static constexpr std::array HANDLERS{{
&sourcemeta::one::GENERATE_CONFIGURATION::handler,
&sourcemeta::one::GENERATE_VERSION::handler,
&sourcemeta::one::GENERATE_URITEMPLATE_ROUTES::handler,
+ &sourcemeta::one::GENERATE_STATIC_FILE::handler,
nullptr,
}};
@@ -504,19 +505,19 @@ static auto index_main(const std::string_view &program,
? parse_numeric_option(app, "maximum-direct-directory-entries")
: 1000};
- auto produce_plan{
- sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce, build_type,
- entries, canonical_output, resolver.data(),
- this_version, incremental, comment, limits)};
+ auto produce_plan{sourcemeta::one::delta(
+ sourcemeta::one::BuildPhase::Produce, build_type, entries,
+ canonical_output, resolver.data(), this_version, incremental, comment,
+ limits, configuration)};
PROFILE_END(profiling, "Producing (Delta)");
execute_plan(mutex, entries, canonical_output, resolver, configuration,
raw_configuration, concurrency, produce_plan, "Producing");
PROFILE_END(profiling, "Producing (Build)");
- auto combine_plan{
- sourcemeta::one::delta(sourcemeta::one::BuildPhase::Combine, build_type,
- entries, canonical_output, resolver.data(),
- this_version, incremental, comment, limits)};
+ auto combine_plan{sourcemeta::one::delta(
+ sourcemeta::one::BuildPhase::Combine, build_type, entries,
+ canonical_output, resolver.data(), this_version, incremental, comment,
+ limits, configuration)};
PROFILE_END(profiling, "Combining (Delta)");
execute_plan(mutex, entries, canonical_output, resolver, configuration,
raw_configuration, concurrency, combine_plan, "Combining");
@@ -617,6 +618,21 @@ auto main(int argc, char *argv[]) noexcept -> int {
sourcemeta::core::to_string(error.location()),
error.target().string());
return EXIT_FAILURE;
+ } catch (
+ const sourcemeta::one::ConfigurationDocumentationNotFoundError &error) {
+ std::print(stderr,
+ "error: {}\n from path {}\n at location \"{}\"\n"
+ " to path {}\n",
+ error.what(), error.from().string(),
+ sourcemeta::core::to_string(error.location()),
+ error.target().string());
+ return EXIT_FAILURE;
+ } catch (
+ const sourcemeta::one::ConfigurationDocumentationAtPrefixError &error) {
+ std::print(stderr, "error: {}\n from path {}\n at location \"{}\"\n",
+ error.what(), error.from().string(),
+ sourcemeta::core::to_string(error.location()));
+ return EXIT_FAILURE;
} catch (const sourcemeta::one::ConfigurationUnknownBuiltInCollectionError
&error) {
std::print(stderr,
@@ -657,6 +673,14 @@ auto main(int argc, char *argv[]) noexcept -> int {
std::print(stderr, "error: {}\n at path {}\n", error.what(),
error.path().string());
return EXIT_FAILURE;
+ } catch (const sourcemeta::one::DocumentationNotSupportedError &error) {
+ std::print(stderr, "error: {}\n at path {}\n", error.what(),
+ error.path().string());
+ return EXIT_FAILURE;
+ } catch (const sourcemeta::one::StaticFileTooLargeError &error) {
+ std::print(stderr, "error: {}\n at path {}\n with size {}\n",
+ error.what(), error.path().string(), error.size());
+ return EXIT_FAILURE;
} catch (const sourcemeta::core::FileError<
sourcemeta::blaze::SchemaRuleInvalidNamePatternError> &error) {
std::print(stderr,
diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt
index 2808eca84..e0ba99688 100644
--- a/src/web/CMakeLists.txt
+++ b/src/web/CMakeLists.txt
@@ -5,6 +5,12 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT one NAME web
pages/schema.cc
pages/not_found.cc)
+if(ONE_ENTERPRISE)
+ target_compile_definitions(sourcemeta_one_web
+ PRIVATE SOURCEMETA_ONE_ENTERPRISE)
+ target_link_libraries(sourcemeta_one_web PRIVATE sourcemeta::one::enterprise_index)
+endif()
+
target_link_libraries(sourcemeta_one_web PUBLIC sourcemeta::one::build)
target_link_libraries(sourcemeta_one_web PRIVATE sourcemeta::core::json)
target_link_libraries(sourcemeta_one_web PRIVATE sourcemeta::core::html)
diff --git a/src/web/helpers.h b/src/web/helpers.h
index 97d896dfa..b02aecff3 100644
--- a/src/web/helpers.h
+++ b/src/web/helpers.h
@@ -317,11 +317,9 @@ inline auto make_file_manager(sourcemeta::core::HTMLWriter &writer,
const sourcemeta::core::JSON &directory,
const std::string &base_path) -> void {
if (directory.at("entries").empty()) {
- writer.div().attribute("class", "container-fluid p-4 flex-grow-1");
writer.p(
"Things look a bit empty over here. Try ingesting some schemas using "
"the configuration file!");
- writer.close();
return;
}
@@ -340,8 +338,6 @@ inline auto make_file_manager(sourcemeta::core::HTMLWriter &writer,
}
}
- writer.div().attribute("class", "container-fluid p-4 flex-grow-1");
-
if (has_regular_entries) {
writer.table().attribute(
"class", "table table-bordered border-light-subtle table-light");
@@ -374,8 +370,6 @@ inline auto make_file_manager(sourcemeta::core::HTMLWriter &writer,
writer.close();
writer.close();
}
-
- writer.close();
}
} // namespace sourcemeta::one::html
diff --git a/src/web/pages/directory.cc b/src/web/pages/directory.cc
index 576c1e724..2b5b4a5bd 100644
--- a/src/web/pages/directory.cc
+++ b/src/web/pages/directory.cc
@@ -7,6 +7,10 @@
#include
#include
+#if defined(SOURCEMETA_ONE_ENTERPRISE)
+#include
+#endif
+
#include // assert
#include // std::chrono
@@ -31,15 +35,24 @@ auto GENERATE_WEB_DIRECTORY::handler(
directory.defines("description")
? directory.at("description").to_string()
: ("Schemas located at " + directory.at("path").to_string())};
+
sourcemeta::core::HTMLWriter writer;
- html::make_page(writer, configuration, canonical, title, description,
- [&](sourcemeta::core::HTMLWriter &w) {
- html::make_breadcrumb(w, directory.at("breadcrumb"),
- configuration.base_path);
- html::make_directory_header(w, directory);
- html::make_file_manager(w, directory,
- configuration.base_path);
- });
+ html::make_page(
+ writer, configuration, canonical, title, description,
+ [&](sourcemeta::core::HTMLWriter &w) {
+ html::make_breadcrumb(w, directory.at("breadcrumb"),
+ configuration.base_path);
+ html::make_directory_header(w, directory);
+ w.div().attribute("class", "container-fluid p-4 flex-grow-1");
+ html::make_file_manager(w, directory, configuration.base_path);
+#if defined(SOURCEMETA_ONE_ENTERPRISE)
+ if (directory.defines("documentation") &&
+ action.dependencies.size() > 1) {
+ render_documentation(w, action.dependencies.at(1));
+ }
+#endif
+ w.close();
+ });
const auto timestamp_end{std::chrono::steady_clock::now()};
metapack_write_text(action.destination, writer.str(), "text/html",
diff --git a/src/web/pages/index.cc b/src/web/pages/index.cc
index f151e2cf3..07c71dcab 100644
--- a/src/web/pages/index.cc
+++ b/src/web/pages/index.cc
@@ -45,12 +45,14 @@ auto GENERATE_WEB_INDEX::handler(
const auto title{configuration.html->name + " Schemas"};
const auto &description{configuration.html->description};
sourcemeta::core::HTMLWriter writer;
- html::make_page(writer, configuration, canonical, title, description,
- [&](sourcemeta::core::HTMLWriter &w) {
- make_hero(w, configuration);
- html::make_file_manager(w, directory,
- configuration.base_path);
- });
+ html::make_page(
+ writer, configuration, canonical, title, description,
+ [&](sourcemeta::core::HTMLWriter &w) {
+ make_hero(w, configuration);
+ w.div().attribute("class", "container-fluid p-4 flex-grow-1");
+ html::make_file_manager(w, directory, configuration.base_path);
+ w.close();
+ });
const auto timestamp_end{std::chrono::steady_clock::now()};
metapack_write_text(action.destination, writer.str(), "text/html",
diff --git a/src/web/style.scss b/src/web/style.scss
index 33e32bc2d..83f3fe4e9 100644
--- a/src/web/style.scss
+++ b/src/web/style.scss
@@ -31,7 +31,7 @@ $breadcrumb-font-size: 15px;
@import "../../vendor/bootstrap/scss/button-group";
@import "../../vendor/bootstrap/scss/nav";
@import "../../vendor/bootstrap/scss/navbar";
-// @import "../../vendor/bootstrap/scss/card";
+@import "../../vendor/bootstrap/scss/card";
// @import "../../vendor/bootstrap/scss/accordion";
@import "../../vendor/bootstrap/scss/breadcrumb";
// @import "../../vendor/bootstrap/scss/pagination";
@@ -73,3 +73,20 @@ table tr > th[scope="row"] {
.alert > p:last-child {
@extend .mb-0;
}
+
+// Render inline code like GitHub instead of Bootstrap's pink
+code {
+ color: inherit;
+ background-color: var(--bs-tertiary-bg);
+ padding: 0.2em 0.4em;
+ font-size: $code-font-size;
+}
+
+// Scale down rendered documentation to fit within the page
+.documentation {
+ font-size: $font-size-sm;
+ h1 { @extend .h5; }
+ h2 { @extend .h6; }
+ h3, h4, h5, h6 { font-size: $font-size-base; }
+ > *:last-child { @extend .mb-0; }
+}
diff --git a/test/cli/CMakeLists.txt b/test/cli/CMakeLists.txt
index 9e9f8371f..f1bc74c49 100644
--- a/test/cli/CMakeLists.txt
+++ b/test/cli/CMakeLists.txt
@@ -141,10 +141,21 @@ if(ONE_INDEX)
sourcemeta_one_test_cli(enterprise index fail-lint-rule-no-title)
sourcemeta_one_test_cli(enterprise index fail-lint-rule-invalid-title)
sourcemeta_one_test_cli(enterprise index fail-lint-rule-no-dialect)
+ sourcemeta_one_test_cli(enterprise index fail-documentation-file-not-found)
+ sourcemeta_one_test_cli(enterprise index fail-documentation-at-prefix)
+ sourcemeta_one_test_cli(enterprise index documentation-basic)
+ sourcemeta_one_test_cli(enterprise index documentation-two-collections-same-file)
+ sourcemeta_one_test_cli(enterprise index fail-documentation-too-large)
+ sourcemeta_one_test_cli(enterprise index rebuild-documentation-modify)
+ sourcemeta_one_test_cli(enterprise index rebuild-documentation-add)
+ sourcemeta_one_test_cli(enterprise index rebuild-documentation-remove)
+ sourcemeta_one_test_cli(enterprise index rebuild-documentation-shared-modify)
+ sourcemeta_one_test_cli(enterprise index rebuild-documentation-shared-remove-one)
else()
sourcemeta_one_test_cli(community index no-options)
sourcemeta_one_test_cli(community index no-output)
sourcemeta_one_test_cli(community index fail-sourcemeta-std)
sourcemeta_one_test_cli(community index fail-lint-rule-enterprise-only)
+ sourcemeta_one_test_cli(community index fail-documentation-enterprise-only)
endif()
endif()
diff --git a/test/cli/index/community/fail-documentation-enterprise-only.sh b/test/cli/index/community/fail-documentation-enterprise-only.sh
new file mode 100755
index 000000000..7366e1748
--- /dev/null
+++ b/test/cli/index/community/fail-documentation-enterprise-only.sh
@@ -0,0 +1,74 @@
+#!/bin/sh
+
+set -o errexit
+set -o nounset
+
+TMP="$(mktemp -d)"
+clean() { rm -rf "$TMP"; }
+trap clean EXIT
+
+mkdir -p "$TMP/schemas" "$TMP/docs"
+
+cat << 'EOF' > "$TMP/schemas/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+cat << 'EOF' > "$TMP/docs/readme.md"
+# Hello
+EOF
+
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "test": {
+ "path": "./schemas",
+ "x-sourcemeta-one:documentation": "./docs/readme.md"
+ }
+ }
+}
+EOF
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?"
+test "$CODE" = "1" || exit 1
+
+remove_threads_information() {
+ expr='s/ \[[^]]*[^a-z-][^]]*\]//g'
+ if [ "$(uname -s)" = "Darwin" ]; then
+ sed -i '' "$expr" "$1"
+ else
+ sed -i "$expr" "$1"
+ fi
+}
+
+remove_threads_information "$TMP/output.txt"
+
+RESOLVED_DOCS_PATH="$(realpath "$TMP/docs/readme.md")"
+HASH="$(printf '%s' "$RESOLVED_DOCS_PATH" | shasum -a 256 | cut -d' ' -f1)"
+
+cat << EOF > "$TMP/expected.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+Detecting: $(realpath "$TMP")/schemas/test.json (#1)
+(100%) Resolving: test.json
+( 4%) Producing: configuration.json
+( 9%) Producing: version.json
+( 13%) Producing: explorer/%/404.metapack
+( 18%) Producing: schemas/test/test/%/schema.metapack
+( 22%) Producing: static/${HASH}.metapack
+( 27%) Producing: schemas/test/test/%/dependencies.metapack
+( 31%) Producing: schemas/test/test/%/locations.metapack
+( 36%) Producing: schemas/test/test/%/positions.metapack
+( 40%) Producing: schemas/test/test/%/stats.metapack
+( 45%) Producing: schemas/test/test/%/bundle.metapack
+( 50%) Producing: schemas/test/test/%/health.metapack
+( 54%) Producing: explorer/test/test/%/schema.metapack
+( 59%) Producing: schemas/test/test/%/blaze-exhaustive.metapack
+( 63%) Producing: schemas/test/test/%/blaze-fast.metapack
+( 68%) Producing: schemas/test/test/%/editor.metapack
+( 72%) Producing: explorer/test/%/directory.metapack
+error: Collection documentation is only available on the enterprise edition
+ at path $(realpath "$TMP")/one.json
+EOF
+
+diff "$TMP/output.txt" "$TMP/expected.txt"
diff --git a/test/cli/index/enterprise/documentation-basic.sh b/test/cli/index/enterprise/documentation-basic.sh
new file mode 100755
index 000000000..ca93aae7b
--- /dev/null
+++ b/test/cli/index/enterprise/documentation-basic.sh
@@ -0,0 +1,124 @@
+#!/bin/sh
+
+set -o errexit
+set -o nounset
+
+TMP="$(mktemp -d)"
+clean() { rm -rf "$TMP"; }
+trap clean EXIT
+
+mkdir -p "$TMP/schemas" "$TMP/docs"
+
+cat << 'EOF' > "$TMP/schemas/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+cat << 'EOF' > "$TMP/docs/readme.md"
+# Hello World
+EOF
+
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "test": {
+ "path": "./schemas",
+ "x-sourcemeta-one:documentation": "./docs/readme.md"
+ }
+ }
+}
+EOF
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?"
+test "$CODE" = "0" || exit 1
+
+remove_threads_information() {
+ expr='s/ \[[^]]*[^a-z-][^]]*\]//g'
+ if [ "$(uname -s)" = "Darwin" ]; then
+ sed -i '' "$expr" "$1"
+ else
+ sed -i "$expr" "$1"
+ fi
+}
+
+remove_threads_information "$TMP/output.txt"
+
+RESOLVED_DOCS_PATH="$(realpath "$TMP/docs/readme.md")"
+HASH="$(printf '%s' "$RESOLVED_DOCS_PATH" | shasum -a 256 | cut -d' ' -f1)"
+
+cat << EOF > "$TMP/expected.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+Detecting: $(realpath "$TMP")/schemas/test.json (#1)
+(100%) Resolving: test.json
+( 4%) Producing: configuration.json
+( 9%) Producing: version.json
+( 13%) Producing: explorer/%/404.metapack
+( 18%) Producing: schemas/test/test/%/schema.metapack
+( 22%) Producing: static/${HASH}.metapack
+( 27%) Producing: schemas/test/test/%/dependencies.metapack
+( 31%) Producing: schemas/test/test/%/locations.metapack
+( 36%) Producing: schemas/test/test/%/positions.metapack
+( 40%) Producing: schemas/test/test/%/stats.metapack
+( 45%) Producing: schemas/test/test/%/bundle.metapack
+( 50%) Producing: schemas/test/test/%/health.metapack
+( 54%) Producing: explorer/test/test/%/schema.metapack
+( 59%) Producing: schemas/test/test/%/blaze-exhaustive.metapack
+( 63%) Producing: schemas/test/test/%/blaze-fast.metapack
+( 68%) Producing: schemas/test/test/%/editor.metapack
+( 72%) Producing: explorer/test/%/directory.metapack
+( 77%) Producing: explorer/test/test/%/schema-html.metapack
+( 81%) Producing: explorer/%/directory.metapack
+( 86%) Producing: explorer/test/%/directory-html.metapack
+( 90%) Producing: explorer/%/directory-html.metapack
+( 95%) Producing: explorer/%/search.metapack
+(100%) Producing: routes.bin
+(100%) Combining: schemas/test/test/%/dependents.metapack
+EOF
+
+diff "$TMP/output.txt" "$TMP/expected.txt"
+
+# Verify the full output file listing
+cd "$TMP/output"
+find . -mindepth 1 | LC_ALL=C sort > "$TMP/manifest.txt"
+cd - > /dev/null
+
+cat << EOF > "$TMP/expected_manifest.txt"
+./configuration.json
+./explorer
+./explorer/%
+./explorer/%/404.metapack
+./explorer/%/directory-html.metapack
+./explorer/%/directory.metapack
+./explorer/%/search.metapack
+./explorer/test
+./explorer/test/%
+./explorer/test/%/directory-html.metapack
+./explorer/test/%/directory.metapack
+./explorer/test/test
+./explorer/test/test/%
+./explorer/test/test/%/schema-html.metapack
+./explorer/test/test/%/schema.metapack
+./routes.bin
+./schemas
+./schemas/test
+./schemas/test/test
+./schemas/test/test/%
+./schemas/test/test/%/blaze-exhaustive.metapack
+./schemas/test/test/%/blaze-fast.metapack
+./schemas/test/test/%/bundle.metapack
+./schemas/test/test/%/dependencies.metapack
+./schemas/test/test/%/dependents.metapack
+./schemas/test/test/%/editor.metapack
+./schemas/test/test/%/health.metapack
+./schemas/test/test/%/locations.metapack
+./schemas/test/test/%/positions.metapack
+./schemas/test/test/%/schema.metapack
+./schemas/test/test/%/stats.metapack
+./state.bin
+./static
+./static/${HASH}.metapack
+./version.json
+EOF
+
+diff "$TMP/manifest.txt" "$TMP/expected_manifest.txt"
diff --git a/test/cli/index/enterprise/documentation-two-collections-same-file.sh b/test/cli/index/enterprise/documentation-two-collections-same-file.sh
new file mode 100755
index 000000000..ab3957fba
--- /dev/null
+++ b/test/cli/index/enterprise/documentation-two-collections-same-file.sh
@@ -0,0 +1,171 @@
+#!/bin/sh
+
+set -o errexit
+set -o nounset
+
+TMP="$(mktemp -d)"
+clean() { rm -rf "$TMP"; }
+trap clean EXIT
+
+mkdir -p "$TMP/schemas-a" "$TMP/schemas-b" "$TMP/docs"
+
+cat << 'EOF' > "$TMP/schemas-a/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+cat << 'EOF' > "$TMP/schemas-b/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+cat << 'EOF' > "$TMP/docs/readme.md"
+# Shared Docs
+EOF
+
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "alpha": {
+ "path": "./schemas-a",
+ "x-sourcemeta-one:documentation": "./docs/readme.md"
+ },
+ "beta": {
+ "path": "./schemas-b",
+ "x-sourcemeta-one:documentation": "./docs/readme.md"
+ }
+ }
+}
+EOF
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?"
+test "$CODE" = "0" || exit 1
+
+remove_threads_information() {
+ expr='s/ \[[^]]*[^a-z-][^]]*\]//g'
+ if [ "$(uname -s)" = "Darwin" ]; then
+ sed -i '' "$expr" "$1"
+ else
+ sed -i "$expr" "$1"
+ fi
+}
+
+remove_threads_information "$TMP/output.txt"
+
+RESOLVED_DOCS_PATH="$(realpath "$TMP/docs/readme.md")"
+HASH="$(printf '%s' "$RESOLVED_DOCS_PATH" | shasum -a 256 | cut -d' ' -f1)"
+
+cat << EOF > "$TMP/expected.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+Detecting: $(realpath "$TMP")/schemas-b/test.json (#1)
+Detecting: $(realpath "$TMP")/schemas-a/test.json (#2)
+( 50%) Resolving: test.json
+(100%) Resolving: test.json
+( 2%) Producing: configuration.json
+( 5%) Producing: version.json
+( 8%) Producing: explorer/%/404.metapack
+( 11%) Producing: schemas/alpha/test/%/schema.metapack
+( 13%) Producing: schemas/beta/test/%/schema.metapack
+( 16%) Producing: static/${HASH}.metapack
+( 19%) Producing: schemas/alpha/test/%/dependencies.metapack
+( 22%) Producing: schemas/alpha/test/%/locations.metapack
+( 25%) Producing: schemas/alpha/test/%/positions.metapack
+( 27%) Producing: schemas/alpha/test/%/stats.metapack
+( 30%) Producing: schemas/beta/test/%/dependencies.metapack
+( 33%) Producing: schemas/beta/test/%/locations.metapack
+( 36%) Producing: schemas/beta/test/%/positions.metapack
+( 38%) Producing: schemas/beta/test/%/stats.metapack
+( 41%) Producing: schemas/alpha/test/%/bundle.metapack
+( 44%) Producing: schemas/alpha/test/%/health.metapack
+( 47%) Producing: schemas/beta/test/%/bundle.metapack
+( 50%) Producing: schemas/beta/test/%/health.metapack
+( 52%) Producing: explorer/alpha/test/%/schema.metapack
+( 55%) Producing: explorer/beta/test/%/schema.metapack
+( 58%) Producing: schemas/alpha/test/%/blaze-exhaustive.metapack
+( 61%) Producing: schemas/alpha/test/%/blaze-fast.metapack
+( 63%) Producing: schemas/alpha/test/%/editor.metapack
+( 66%) Producing: schemas/beta/test/%/blaze-exhaustive.metapack
+( 69%) Producing: schemas/beta/test/%/blaze-fast.metapack
+( 72%) Producing: schemas/beta/test/%/editor.metapack
+( 75%) Producing: explorer/alpha/%/directory.metapack
+( 77%) Producing: explorer/alpha/test/%/schema-html.metapack
+( 80%) Producing: explorer/beta/%/directory.metapack
+( 83%) Producing: explorer/beta/test/%/schema-html.metapack
+( 86%) Producing: explorer/%/directory.metapack
+( 88%) Producing: explorer/alpha/%/directory-html.metapack
+( 91%) Producing: explorer/beta/%/directory-html.metapack
+( 94%) Producing: explorer/%/directory-html.metapack
+( 97%) Producing: explorer/%/search.metapack
+(100%) Producing: routes.bin
+( 50%) Combining: schemas/alpha/test/%/dependents.metapack
+(100%) Combining: schemas/beta/test/%/dependents.metapack
+EOF
+
+diff "$TMP/output.txt" "$TMP/expected.txt"
+
+# Verify the full output file listing
+cd "$TMP/output"
+find . -mindepth 1 | LC_ALL=C sort > "$TMP/manifest.txt"
+cd - > /dev/null
+
+cat << EOF > "$TMP/expected_manifest.txt"
+./configuration.json
+./explorer
+./explorer/%
+./explorer/%/404.metapack
+./explorer/%/directory-html.metapack
+./explorer/%/directory.metapack
+./explorer/%/search.metapack
+./explorer/alpha
+./explorer/alpha/%
+./explorer/alpha/%/directory-html.metapack
+./explorer/alpha/%/directory.metapack
+./explorer/alpha/test
+./explorer/alpha/test/%
+./explorer/alpha/test/%/schema-html.metapack
+./explorer/alpha/test/%/schema.metapack
+./explorer/beta
+./explorer/beta/%
+./explorer/beta/%/directory-html.metapack
+./explorer/beta/%/directory.metapack
+./explorer/beta/test
+./explorer/beta/test/%
+./explorer/beta/test/%/schema-html.metapack
+./explorer/beta/test/%/schema.metapack
+./routes.bin
+./schemas
+./schemas/alpha
+./schemas/alpha/test
+./schemas/alpha/test/%
+./schemas/alpha/test/%/blaze-exhaustive.metapack
+./schemas/alpha/test/%/blaze-fast.metapack
+./schemas/alpha/test/%/bundle.metapack
+./schemas/alpha/test/%/dependencies.metapack
+./schemas/alpha/test/%/dependents.metapack
+./schemas/alpha/test/%/editor.metapack
+./schemas/alpha/test/%/health.metapack
+./schemas/alpha/test/%/locations.metapack
+./schemas/alpha/test/%/positions.metapack
+./schemas/alpha/test/%/schema.metapack
+./schemas/alpha/test/%/stats.metapack
+./schemas/beta
+./schemas/beta/test
+./schemas/beta/test/%
+./schemas/beta/test/%/blaze-exhaustive.metapack
+./schemas/beta/test/%/blaze-fast.metapack
+./schemas/beta/test/%/bundle.metapack
+./schemas/beta/test/%/dependencies.metapack
+./schemas/beta/test/%/dependents.metapack
+./schemas/beta/test/%/editor.metapack
+./schemas/beta/test/%/health.metapack
+./schemas/beta/test/%/locations.metapack
+./schemas/beta/test/%/positions.metapack
+./schemas/beta/test/%/schema.metapack
+./schemas/beta/test/%/stats.metapack
+./state.bin
+./static
+./static/${HASH}.metapack
+./version.json
+EOF
+
+diff "$TMP/manifest.txt" "$TMP/expected_manifest.txt"
diff --git a/test/cli/index/enterprise/fail-documentation-at-prefix.sh b/test/cli/index/enterprise/fail-documentation-at-prefix.sh
new file mode 100755
index 000000000..5940d7e57
--- /dev/null
+++ b/test/cli/index/enterprise/fail-documentation-at-prefix.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+
+set -o errexit
+set -o nounset
+
+TMP="$(mktemp -d)"
+clean() { rm -rf "$TMP"; }
+trap clean EXIT
+
+mkdir "$TMP/schemas"
+
+cat << 'EOF' > "$TMP/schemas/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "test": {
+ "path": "./schemas",
+ "x-sourcemeta-one:documentation": "@some/docs/readme.md"
+ }
+ }
+}
+EOF
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?"
+test "$CODE" = "1" || exit 1
+
+cat << EOF > "$TMP/expected.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+error: The @ prefix is not supported for documentation paths
+ from path $(realpath "$TMP")/one.json
+ at location "/contents/test/x-sourcemeta-one:documentation"
+EOF
+
+diff "$TMP/output.txt" "$TMP/expected.txt"
diff --git a/test/cli/index/enterprise/fail-documentation-file-not-found.sh b/test/cli/index/enterprise/fail-documentation-file-not-found.sh
new file mode 100755
index 000000000..fbededa31
--- /dev/null
+++ b/test/cli/index/enterprise/fail-documentation-file-not-found.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+set -o errexit
+set -o nounset
+
+TMP="$(mktemp -d)"
+clean() { rm -rf "$TMP"; }
+trap clean EXIT
+
+mkdir "$TMP/schemas"
+
+cat << 'EOF' > "$TMP/schemas/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "test": {
+ "path": "./schemas",
+ "x-sourcemeta-one:documentation": "./does-not-exist.md"
+ }
+ }
+}
+EOF
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?"
+test "$CODE" = "1" || exit 1
+
+cat << EOF > "$TMP/expected.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+error: Could not read the documentation file
+ from path $(realpath "$TMP")/one.json
+ at location "/contents/test/x-sourcemeta-one:documentation"
+ to path $(realpath "$TMP")/does-not-exist.md
+EOF
+
+diff "$TMP/output.txt" "$TMP/expected.txt"
diff --git a/test/cli/index/enterprise/fail-documentation-too-large.sh b/test/cli/index/enterprise/fail-documentation-too-large.sh
new file mode 100755
index 000000000..e086e3288
--- /dev/null
+++ b/test/cli/index/enterprise/fail-documentation-too-large.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+set -o errexit
+set -o nounset
+
+TMP="$(mktemp -d)"
+clean() { rm -rf "$TMP"; }
+trap clean EXIT
+
+mkdir -p "$TMP/schemas" "$TMP/docs"
+
+cat << 'EOF' > "$TMP/schemas/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+dd if=/dev/zero bs=1048577 count=1 2>/dev/null | tr '\0' 'a' > "$TMP/docs/big.md"
+
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "test": {
+ "path": "./schemas",
+ "x-sourcemeta-one:documentation": "./docs/big.md"
+ }
+ }
+}
+EOF
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output.txt" && CODE="$?" || CODE="$?"
+test "$CODE" = "1" || exit 1
+
+remove_threads_information() {
+ expr='s/ \[[^]]*[^a-z-][^]]*\]//g'
+ if [ "$(uname -s)" = "Darwin" ]; then
+ sed -i '' "$expr" "$1"
+ else
+ sed -i "$expr" "$1"
+ fi
+}
+
+remove_threads_information "$TMP/output.txt"
+
+RESOLVED_DOCS_PATH="$(realpath "$TMP/docs/big.md")"
+HASH="$(printf '%s' "$RESOLVED_DOCS_PATH" | shasum -a 256 | cut -d' ' -f1)"
+
+cat << EOF > "$TMP/expected.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+Detecting: $(realpath "$TMP")/schemas/test.json (#1)
+(100%) Resolving: test.json
+( 4%) Producing: configuration.json
+( 9%) Producing: version.json
+( 13%) Producing: explorer/%/404.metapack
+( 18%) Producing: schemas/test/test/%/schema.metapack
+( 22%) Producing: static/${HASH}.metapack
+error: Static file exceeds the maximum allowed size
+ at path $(realpath "$TMP")/docs/big.md
+ with size 1048577
+EOF
+
+diff "$TMP/output.txt" "$TMP/expected.txt"
diff --git a/test/cli/index/enterprise/rebuild-documentation-add.sh b/test/cli/index/enterprise/rebuild-documentation-add.sh
new file mode 100755
index 000000000..7e4a3c0db
--- /dev/null
+++ b/test/cli/index/enterprise/rebuild-documentation-add.sh
@@ -0,0 +1,176 @@
+#!/bin/sh
+
+set -o errexit
+set -o nounset
+
+TMP="$(mktemp -d)"
+clean() { rm -rf "$TMP"; }
+trap clean EXIT
+
+mkdir -p "$TMP/schemas" "$TMP/docs"
+
+cat << 'EOF' > "$TMP/schemas/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+remove_threads_information() {
+ expr='s/ \[[^]]*[^a-z-][^]]*\]//g'
+ if [ "$(uname -s)" = "Darwin" ]; then
+ sed -i '' "$expr" "$1"
+ else
+ sed -i "$expr" "$1"
+ fi
+}
+
+# Run 1: Build without documentation
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "test": {
+ "path": "./schemas"
+ }
+ }
+}
+EOF
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output1.txt"
+
+# Verify output after run 1 (no static directory)
+cd "$TMP/output"
+find . -mindepth 1 | LC_ALL=C sort > "$TMP/manifest1.txt"
+cd - > /dev/null
+
+cat << 'EOF' > "$TMP/expected_manifest1.txt"
+./configuration.json
+./explorer
+./explorer/%
+./explorer/%/404.metapack
+./explorer/%/directory-html.metapack
+./explorer/%/directory.metapack
+./explorer/%/search.metapack
+./explorer/test
+./explorer/test/%
+./explorer/test/%/directory-html.metapack
+./explorer/test/%/directory.metapack
+./explorer/test/test
+./explorer/test/test/%
+./explorer/test/test/%/schema-html.metapack
+./explorer/test/test/%/schema.metapack
+./routes.bin
+./schemas
+./schemas/test
+./schemas/test/test
+./schemas/test/test/%
+./schemas/test/test/%/blaze-exhaustive.metapack
+./schemas/test/test/%/blaze-fast.metapack
+./schemas/test/test/%/bundle.metapack
+./schemas/test/test/%/dependencies.metapack
+./schemas/test/test/%/dependents.metapack
+./schemas/test/test/%/editor.metapack
+./schemas/test/test/%/health.metapack
+./schemas/test/test/%/locations.metapack
+./schemas/test/test/%/positions.metapack
+./schemas/test/test/%/schema.metapack
+./schemas/test/test/%/stats.metapack
+./state.bin
+./version.json
+EOF
+
+diff "$TMP/manifest1.txt" "$TMP/expected_manifest1.txt"
+
+# Run 2: Add documentation
+echo '# New Docs' > "$TMP/docs/readme.md"
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "test": {
+ "path": "./schemas",
+ "x-sourcemeta-one:documentation": "./docs/readme.md"
+ }
+ }
+}
+EOF
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output2.txt"
+remove_threads_information "$TMP/output2.txt"
+
+RESOLVED_DOCS_PATH="$(realpath "$TMP/docs/readme.md")"
+HASH="$(printf '%s' "$RESOLVED_DOCS_PATH" | shasum -a 256 | cut -d' ' -f1)"
+
+cat << EOF > "$TMP/expected2.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+Detecting: $(realpath "$TMP")/schemas/test.json (#1)
+(100%) Resolving: test.json
+( 4%) Producing: configuration.json
+( 9%) Producing: version.json
+( 13%) Producing: explorer/%/404.metapack
+( 18%) Producing: schemas/test/test/%/schema.metapack
+( 22%) Producing: static/${HASH}.metapack
+( 27%) Producing: schemas/test/test/%/dependencies.metapack
+( 31%) Producing: schemas/test/test/%/locations.metapack
+( 36%) Producing: schemas/test/test/%/positions.metapack
+( 40%) Producing: schemas/test/test/%/stats.metapack
+( 45%) Producing: schemas/test/test/%/bundle.metapack
+( 50%) Producing: schemas/test/test/%/health.metapack
+( 54%) Producing: explorer/test/test/%/schema.metapack
+( 59%) Producing: schemas/test/test/%/blaze-exhaustive.metapack
+( 63%) Producing: schemas/test/test/%/blaze-fast.metapack
+( 68%) Producing: schemas/test/test/%/editor.metapack
+( 72%) Producing: explorer/test/%/directory.metapack
+( 77%) Producing: explorer/test/test/%/schema-html.metapack
+( 81%) Producing: explorer/%/directory.metapack
+( 86%) Producing: explorer/test/%/directory-html.metapack
+( 90%) Producing: explorer/%/directory-html.metapack
+( 95%) Producing: explorer/%/search.metapack
+(100%) Producing: routes.bin
+EOF
+
+diff "$TMP/output2.txt" "$TMP/expected2.txt"
+
+# Verify output after run 2 (static directory added)
+cd "$TMP/output"
+find . -mindepth 1 | LC_ALL=C sort > "$TMP/manifest2.txt"
+cd - > /dev/null
+
+cat << EOF > "$TMP/expected_manifest2.txt"
+./configuration.json
+./explorer
+./explorer/%
+./explorer/%/404.metapack
+./explorer/%/directory-html.metapack
+./explorer/%/directory.metapack
+./explorer/%/search.metapack
+./explorer/test
+./explorer/test/%
+./explorer/test/%/directory-html.metapack
+./explorer/test/%/directory.metapack
+./explorer/test/test
+./explorer/test/test/%
+./explorer/test/test/%/schema-html.metapack
+./explorer/test/test/%/schema.metapack
+./routes.bin
+./schemas
+./schemas/test
+./schemas/test/test
+./schemas/test/test/%
+./schemas/test/test/%/blaze-exhaustive.metapack
+./schemas/test/test/%/blaze-fast.metapack
+./schemas/test/test/%/bundle.metapack
+./schemas/test/test/%/dependencies.metapack
+./schemas/test/test/%/dependents.metapack
+./schemas/test/test/%/editor.metapack
+./schemas/test/test/%/health.metapack
+./schemas/test/test/%/locations.metapack
+./schemas/test/test/%/positions.metapack
+./schemas/test/test/%/schema.metapack
+./schemas/test/test/%/stats.metapack
+./state.bin
+./static
+./static/${HASH}.metapack
+./version.json
+EOF
+
+diff "$TMP/manifest2.txt" "$TMP/expected_manifest2.txt"
diff --git a/test/cli/index/enterprise/rebuild-documentation-modify.sh b/test/cli/index/enterprise/rebuild-documentation-modify.sh
new file mode 100755
index 000000000..f28378652
--- /dev/null
+++ b/test/cli/index/enterprise/rebuild-documentation-modify.sh
@@ -0,0 +1,155 @@
+#!/bin/sh
+
+set -o errexit
+set -o nounset
+
+TMP="$(mktemp -d)"
+clean() { rm -rf "$TMP"; }
+trap clean EXIT
+
+mkdir -p "$TMP/schemas" "$TMP/docs"
+
+cat << 'EOF' > "$TMP/schemas/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+cat << 'EOF' > "$TMP/docs/readme.md"
+# Original
+EOF
+
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "test": {
+ "path": "./schemas",
+ "x-sourcemeta-one:documentation": "./docs/readme.md"
+ }
+ }
+}
+EOF
+
+remove_threads_information() {
+ expr='s/ \[[^]]*[^a-z-][^]]*\]//g'
+ if [ "$(uname -s)" = "Darwin" ]; then
+ sed -i '' "$expr" "$1"
+ else
+ sed -i "$expr" "$1"
+ fi
+}
+
+RESOLVED_DOCS_PATH="$(realpath "$TMP/docs/readme.md")"
+HASH="$(printf '%s' "$RESOLVED_DOCS_PATH" | shasum -a 256 | cut -d' ' -f1)"
+
+# Run 1: Fresh build
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output1.txt"
+remove_threads_information "$TMP/output1.txt"
+
+cat << EOF > "$TMP/expected1.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+Detecting: $(realpath "$TMP")/schemas/test.json (#1)
+(100%) Resolving: test.json
+( 4%) Producing: configuration.json
+( 9%) Producing: version.json
+( 13%) Producing: explorer/%/404.metapack
+( 18%) Producing: schemas/test/test/%/schema.metapack
+( 22%) Producing: static/${HASH}.metapack
+( 27%) Producing: schemas/test/test/%/dependencies.metapack
+( 31%) Producing: schemas/test/test/%/locations.metapack
+( 36%) Producing: schemas/test/test/%/positions.metapack
+( 40%) Producing: schemas/test/test/%/stats.metapack
+( 45%) Producing: schemas/test/test/%/bundle.metapack
+( 50%) Producing: schemas/test/test/%/health.metapack
+( 54%) Producing: explorer/test/test/%/schema.metapack
+( 59%) Producing: schemas/test/test/%/blaze-exhaustive.metapack
+( 63%) Producing: schemas/test/test/%/blaze-fast.metapack
+( 68%) Producing: schemas/test/test/%/editor.metapack
+( 72%) Producing: explorer/test/%/directory.metapack
+( 77%) Producing: explorer/test/test/%/schema-html.metapack
+( 81%) Producing: explorer/%/directory.metapack
+( 86%) Producing: explorer/test/%/directory-html.metapack
+( 90%) Producing: explorer/%/directory-html.metapack
+( 95%) Producing: explorer/%/search.metapack
+(100%) Producing: routes.bin
+(100%) Combining: schemas/test/test/%/dependents.metapack
+EOF
+
+diff "$TMP/output1.txt" "$TMP/expected1.txt"
+
+# Run 2: No changes - cache hit
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output2.txt"
+remove_threads_information "$TMP/output2.txt"
+
+cat << EOF > "$TMP/expected2.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+Detecting: $(realpath "$TMP")/schemas/test.json (#1)
+EOF
+
+diff "$TMP/output2.txt" "$TMP/expected2.txt"
+
+# Run 3: Modify documentation file
+echo '# Updated Content' > "$TMP/docs/readme.md"
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output3.txt"
+remove_threads_information "$TMP/output3.txt"
+
+cat << EOF > "$TMP/expected3.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+Detecting: $(realpath "$TMP")/schemas/test.json (#1)
+( 16%) Producing: static/${HASH}.metapack
+( 33%) Producing: explorer/test/%/directory.metapack
+( 50%) Producing: explorer/%/directory.metapack
+( 66%) Producing: explorer/test/%/directory-html.metapack
+( 83%) Producing: explorer/%/directory-html.metapack
+(100%) Producing: explorer/%/search.metapack
+EOF
+
+diff "$TMP/output3.txt" "$TMP/expected3.txt"
+
+# Verify the full output file listing
+cd "$TMP/output"
+find . -mindepth 1 | LC_ALL=C sort > "$TMP/manifest.txt"
+cd - > /dev/null
+
+cat << EOF > "$TMP/expected_manifest.txt"
+./configuration.json
+./explorer
+./explorer/%
+./explorer/%/404.metapack
+./explorer/%/directory-html.metapack
+./explorer/%/directory.metapack
+./explorer/%/search.metapack
+./explorer/test
+./explorer/test/%
+./explorer/test/%/directory-html.metapack
+./explorer/test/%/directory.metapack
+./explorer/test/test
+./explorer/test/test/%
+./explorer/test/test/%/schema-html.metapack
+./explorer/test/test/%/schema.metapack
+./routes.bin
+./schemas
+./schemas/test
+./schemas/test/test
+./schemas/test/test/%
+./schemas/test/test/%/blaze-exhaustive.metapack
+./schemas/test/test/%/blaze-fast.metapack
+./schemas/test/test/%/bundle.metapack
+./schemas/test/test/%/dependencies.metapack
+./schemas/test/test/%/dependents.metapack
+./schemas/test/test/%/editor.metapack
+./schemas/test/test/%/health.metapack
+./schemas/test/test/%/locations.metapack
+./schemas/test/test/%/positions.metapack
+./schemas/test/test/%/schema.metapack
+./schemas/test/test/%/stats.metapack
+./state.bin
+./static
+./static/${HASH}.metapack
+./version.json
+EOF
+
+diff "$TMP/manifest.txt" "$TMP/expected_manifest.txt"
diff --git a/test/cli/index/enterprise/rebuild-documentation-remove.sh b/test/cli/index/enterprise/rebuild-documentation-remove.sh
new file mode 100755
index 000000000..011331e92
--- /dev/null
+++ b/test/cli/index/enterprise/rebuild-documentation-remove.sh
@@ -0,0 +1,179 @@
+#!/bin/sh
+
+set -o errexit
+set -o nounset
+
+TMP="$(mktemp -d)"
+clean() { rm -rf "$TMP"; }
+trap clean EXIT
+
+mkdir -p "$TMP/schemas" "$TMP/docs"
+
+cat << 'EOF' > "$TMP/schemas/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+cat << 'EOF' > "$TMP/docs/readme.md"
+# Docs
+EOF
+
+remove_threads_information() {
+ expr='s/ \[[^]]*[^a-z-][^]]*\]//g'
+ if [ "$(uname -s)" = "Darwin" ]; then
+ sed -i '' "$expr" "$1"
+ else
+ sed -i "$expr" "$1"
+ fi
+}
+
+# Run 1: Build with documentation
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "test": {
+ "path": "./schemas",
+ "x-sourcemeta-one:documentation": "./docs/readme.md"
+ }
+ }
+}
+EOF
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output1.txt"
+
+# Verify output after run 1
+cd "$TMP/output"
+find . -mindepth 1 | LC_ALL=C sort > "$TMP/manifest1.txt"
+cd - > /dev/null
+
+RESOLVED_DOCS_PATH="$(realpath "$TMP/docs/readme.md")"
+HASH="$(printf '%s' "$RESOLVED_DOCS_PATH" | shasum -a 256 | cut -d' ' -f1)"
+
+cat << EOF > "$TMP/expected_manifest1.txt"
+./configuration.json
+./explorer
+./explorer/%
+./explorer/%/404.metapack
+./explorer/%/directory-html.metapack
+./explorer/%/directory.metapack
+./explorer/%/search.metapack
+./explorer/test
+./explorer/test/%
+./explorer/test/%/directory-html.metapack
+./explorer/test/%/directory.metapack
+./explorer/test/test
+./explorer/test/test/%
+./explorer/test/test/%/schema-html.metapack
+./explorer/test/test/%/schema.metapack
+./routes.bin
+./schemas
+./schemas/test
+./schemas/test/test
+./schemas/test/test/%
+./schemas/test/test/%/blaze-exhaustive.metapack
+./schemas/test/test/%/blaze-fast.metapack
+./schemas/test/test/%/bundle.metapack
+./schemas/test/test/%/dependencies.metapack
+./schemas/test/test/%/dependents.metapack
+./schemas/test/test/%/editor.metapack
+./schemas/test/test/%/health.metapack
+./schemas/test/test/%/locations.metapack
+./schemas/test/test/%/positions.metapack
+./schemas/test/test/%/schema.metapack
+./schemas/test/test/%/stats.metapack
+./state.bin
+./static
+./static/${HASH}.metapack
+./version.json
+EOF
+
+diff "$TMP/manifest1.txt" "$TMP/expected_manifest1.txt"
+
+# Run 2: Remove documentation from config
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "test": {
+ "path": "./schemas"
+ }
+ }
+}
+EOF
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output2.txt"
+remove_threads_information "$TMP/output2.txt"
+
+cat << EOF > "$TMP/expected2.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+Detecting: $(realpath "$TMP")/schemas/test.json (#1)
+(100%) Resolving: test.json
+( 4%) Producing: configuration.json
+( 9%) Producing: version.json
+( 13%) Producing: explorer/%/404.metapack
+( 18%) Producing: schemas/test/test/%/schema.metapack
+( 22%) Producing: schemas/test/test/%/dependencies.metapack
+( 27%) Producing: schemas/test/test/%/locations.metapack
+( 31%) Producing: schemas/test/test/%/positions.metapack
+( 36%) Producing: schemas/test/test/%/stats.metapack
+( 40%) Producing: schemas/test/test/%/bundle.metapack
+( 45%) Producing: schemas/test/test/%/health.metapack
+( 50%) Producing: explorer/test/test/%/schema.metapack
+( 54%) Producing: schemas/test/test/%/blaze-exhaustive.metapack
+( 59%) Producing: schemas/test/test/%/blaze-fast.metapack
+( 63%) Producing: schemas/test/test/%/editor.metapack
+( 68%) Producing: explorer/test/%/directory.metapack
+( 72%) Producing: explorer/test/test/%/schema-html.metapack
+( 77%) Producing: explorer/%/directory.metapack
+( 81%) Producing: explorer/test/%/directory-html.metapack
+( 86%) Producing: explorer/%/directory-html.metapack
+( 90%) Producing: explorer/%/search.metapack
+( 95%) Producing: routes.bin
+(100%) Disposing: static
+EOF
+
+diff "$TMP/output2.txt" "$TMP/expected2.txt"
+
+# Verify output after run 2 (static directory removed)
+cd "$TMP/output"
+find . -mindepth 1 | LC_ALL=C sort > "$TMP/manifest2.txt"
+cd - > /dev/null
+
+cat << 'EOF' > "$TMP/expected_manifest2.txt"
+./configuration.json
+./explorer
+./explorer/%
+./explorer/%/404.metapack
+./explorer/%/directory-html.metapack
+./explorer/%/directory.metapack
+./explorer/%/search.metapack
+./explorer/test
+./explorer/test/%
+./explorer/test/%/directory-html.metapack
+./explorer/test/%/directory.metapack
+./explorer/test/test
+./explorer/test/test/%
+./explorer/test/test/%/schema-html.metapack
+./explorer/test/test/%/schema.metapack
+./routes.bin
+./schemas
+./schemas/test
+./schemas/test/test
+./schemas/test/test/%
+./schemas/test/test/%/blaze-exhaustive.metapack
+./schemas/test/test/%/blaze-fast.metapack
+./schemas/test/test/%/bundle.metapack
+./schemas/test/test/%/dependencies.metapack
+./schemas/test/test/%/dependents.metapack
+./schemas/test/test/%/editor.metapack
+./schemas/test/test/%/health.metapack
+./schemas/test/test/%/locations.metapack
+./schemas/test/test/%/positions.metapack
+./schemas/test/test/%/schema.metapack
+./schemas/test/test/%/stats.metapack
+./state.bin
+./version.json
+EOF
+
+diff "$TMP/manifest2.txt" "$TMP/expected_manifest2.txt"
diff --git a/test/cli/index/enterprise/rebuild-documentation-shared-modify.sh b/test/cli/index/enterprise/rebuild-documentation-shared-modify.sh
new file mode 100755
index 000000000..96d706ad7
--- /dev/null
+++ b/test/cli/index/enterprise/rebuild-documentation-shared-modify.sh
@@ -0,0 +1,89 @@
+#!/bin/sh
+
+set -o errexit
+set -o nounset
+
+TMP="$(mktemp -d)"
+clean() { rm -rf "$TMP"; }
+trap clean EXIT
+
+mkdir -p "$TMP/schemas-a" "$TMP/schemas-b" "$TMP/docs"
+
+cat << 'EOF' > "$TMP/schemas-a/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+cat << 'EOF' > "$TMP/schemas-b/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+cat << 'EOF' > "$TMP/docs/readme.md"
+# Shared
+EOF
+
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "alpha": {
+ "path": "./schemas-a",
+ "x-sourcemeta-one:documentation": "./docs/readme.md"
+ },
+ "beta": {
+ "path": "./schemas-b",
+ "x-sourcemeta-one:documentation": "./docs/readme.md"
+ }
+ }
+}
+EOF
+
+remove_threads_information() {
+ expr='s/ \[[^]]*[^a-z-][^]]*\]//g'
+ if [ "$(uname -s)" = "Darwin" ]; then
+ sed -i '' "$expr" "$1"
+ else
+ sed -i "$expr" "$1"
+ fi
+}
+
+RESOLVED_DOCS_PATH="$(realpath "$TMP/docs/readme.md")"
+HASH="$(printf '%s' "$RESOLVED_DOCS_PATH" | shasum -a 256 | cut -d' ' -f1)"
+
+# Run 1: Full build
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output1.txt"
+
+# Run 2: Cached
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output2.txt"
+remove_threads_information "$TMP/output2.txt"
+
+cat << EOF > "$TMP/expected2.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+Detecting: $(realpath "$TMP")/schemas-b/test.json (#1)
+Detecting: $(realpath "$TMP")/schemas-a/test.json (#2)
+EOF
+
+diff "$TMP/output2.txt" "$TMP/expected2.txt"
+
+# Run 3: Modify shared documentation file
+echo '# Updated Shared' > "$TMP/docs/readme.md"
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output3.txt"
+remove_threads_information "$TMP/output3.txt"
+
+cat << EOF > "$TMP/expected3.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+Detecting: $(realpath "$TMP")/schemas-b/test.json (#1)
+Detecting: $(realpath "$TMP")/schemas-a/test.json (#2)
+( 12%) Producing: static/${HASH}.metapack
+( 25%) Producing: explorer/alpha/%/directory.metapack
+( 37%) Producing: explorer/beta/%/directory.metapack
+( 50%) Producing: explorer/%/directory.metapack
+( 62%) Producing: explorer/alpha/%/directory-html.metapack
+( 75%) Producing: explorer/beta/%/directory-html.metapack
+( 87%) Producing: explorer/%/directory-html.metapack
+(100%) Producing: explorer/%/search.metapack
+EOF
+
+diff "$TMP/output3.txt" "$TMP/expected3.txt"
diff --git a/test/cli/index/enterprise/rebuild-documentation-shared-remove-one.sh b/test/cli/index/enterprise/rebuild-documentation-shared-remove-one.sh
new file mode 100755
index 000000000..87e2efdc5
--- /dev/null
+++ b/test/cli/index/enterprise/rebuild-documentation-shared-remove-one.sh
@@ -0,0 +1,253 @@
+#!/bin/sh
+
+set -o errexit
+set -o nounset
+
+TMP="$(mktemp -d)"
+clean() { rm -rf "$TMP"; }
+trap clean EXIT
+
+mkdir -p "$TMP/schemas-a" "$TMP/schemas-b" "$TMP/docs"
+
+cat << 'EOF' > "$TMP/schemas-a/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+cat << 'EOF' > "$TMP/schemas-b/test.json"
+{ "$schema": "https://json-schema.org/draft/2020-12/schema" }
+EOF
+
+cat << 'EOF' > "$TMP/docs/readme.md"
+# Shared
+EOF
+
+remove_threads_information() {
+ expr='s/ \[[^]]*[^a-z-][^]]*\]//g'
+ if [ "$(uname -s)" = "Darwin" ]; then
+ sed -i '' "$expr" "$1"
+ else
+ sed -i "$expr" "$1"
+ fi
+}
+
+# Run 1: Build with both collections having documentation
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "alpha": {
+ "path": "./schemas-a",
+ "x-sourcemeta-one:documentation": "./docs/readme.md"
+ },
+ "beta": {
+ "path": "./schemas-b",
+ "x-sourcemeta-one:documentation": "./docs/readme.md"
+ }
+ }
+}
+EOF
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output1.txt"
+
+RESOLVED_DOCS_PATH="$(realpath "$TMP/docs/readme.md")"
+HASH="$(printf '%s' "$RESOLVED_DOCS_PATH" | shasum -a 256 | cut -d' ' -f1)"
+
+# Verify output after run 1
+cd "$TMP/output"
+find . -mindepth 1 | LC_ALL=C sort > "$TMP/manifest1.txt"
+cd - > /dev/null
+
+cat << EOF > "$TMP/expected_manifest1.txt"
+./configuration.json
+./explorer
+./explorer/%
+./explorer/%/404.metapack
+./explorer/%/directory-html.metapack
+./explorer/%/directory.metapack
+./explorer/%/search.metapack
+./explorer/alpha
+./explorer/alpha/%
+./explorer/alpha/%/directory-html.metapack
+./explorer/alpha/%/directory.metapack
+./explorer/alpha/test
+./explorer/alpha/test/%
+./explorer/alpha/test/%/schema-html.metapack
+./explorer/alpha/test/%/schema.metapack
+./explorer/beta
+./explorer/beta/%
+./explorer/beta/%/directory-html.metapack
+./explorer/beta/%/directory.metapack
+./explorer/beta/test
+./explorer/beta/test/%
+./explorer/beta/test/%/schema-html.metapack
+./explorer/beta/test/%/schema.metapack
+./routes.bin
+./schemas
+./schemas/alpha
+./schemas/alpha/test
+./schemas/alpha/test/%
+./schemas/alpha/test/%/blaze-exhaustive.metapack
+./schemas/alpha/test/%/blaze-fast.metapack
+./schemas/alpha/test/%/bundle.metapack
+./schemas/alpha/test/%/dependencies.metapack
+./schemas/alpha/test/%/dependents.metapack
+./schemas/alpha/test/%/editor.metapack
+./schemas/alpha/test/%/health.metapack
+./schemas/alpha/test/%/locations.metapack
+./schemas/alpha/test/%/positions.metapack
+./schemas/alpha/test/%/schema.metapack
+./schemas/alpha/test/%/stats.metapack
+./schemas/beta
+./schemas/beta/test
+./schemas/beta/test/%
+./schemas/beta/test/%/blaze-exhaustive.metapack
+./schemas/beta/test/%/blaze-fast.metapack
+./schemas/beta/test/%/bundle.metapack
+./schemas/beta/test/%/dependencies.metapack
+./schemas/beta/test/%/dependents.metapack
+./schemas/beta/test/%/editor.metapack
+./schemas/beta/test/%/health.metapack
+./schemas/beta/test/%/locations.metapack
+./schemas/beta/test/%/positions.metapack
+./schemas/beta/test/%/schema.metapack
+./schemas/beta/test/%/stats.metapack
+./state.bin
+./static
+./static/${HASH}.metapack
+./version.json
+EOF
+
+diff "$TMP/manifest1.txt" "$TMP/expected_manifest1.txt"
+
+# Run 2: Remove documentation from beta only
+cat << EOF > "$TMP/one.json"
+{
+ "url": "https://example.com",
+ "contents": {
+ "alpha": {
+ "path": "./schemas-a",
+ "x-sourcemeta-one:documentation": "./docs/readme.md"
+ },
+ "beta": {
+ "path": "./schemas-b"
+ }
+ }
+}
+EOF
+
+"$1" --skip-banner "$TMP/one.json" "$TMP/output" --concurrency 1 2> "$TMP/output2.txt"
+remove_threads_information "$TMP/output2.txt"
+
+cat << EOF > "$TMP/expected2.txt"
+Writing output to: $(realpath "$TMP")/output
+Using configuration: $(realpath "$TMP")/one.json
+Detecting: $(realpath "$TMP")/schemas-b/test.json (#1)
+Detecting: $(realpath "$TMP")/schemas-a/test.json (#2)
+( 50%) Resolving: test.json
+(100%) Resolving: test.json
+( 2%) Producing: configuration.json
+( 5%) Producing: version.json
+( 8%) Producing: explorer/%/404.metapack
+( 11%) Producing: schemas/alpha/test/%/schema.metapack
+( 13%) Producing: schemas/beta/test/%/schema.metapack
+( 16%) Producing: static/${HASH}.metapack
+( 19%) Producing: schemas/alpha/test/%/dependencies.metapack
+( 22%) Producing: schemas/alpha/test/%/locations.metapack
+( 25%) Producing: schemas/alpha/test/%/positions.metapack
+( 27%) Producing: schemas/alpha/test/%/stats.metapack
+( 30%) Producing: schemas/beta/test/%/dependencies.metapack
+( 33%) Producing: schemas/beta/test/%/locations.metapack
+( 36%) Producing: schemas/beta/test/%/positions.metapack
+( 38%) Producing: schemas/beta/test/%/stats.metapack
+( 41%) Producing: schemas/alpha/test/%/bundle.metapack
+( 44%) Producing: schemas/alpha/test/%/health.metapack
+( 47%) Producing: schemas/beta/test/%/bundle.metapack
+( 50%) Producing: schemas/beta/test/%/health.metapack
+( 52%) Producing: explorer/alpha/test/%/schema.metapack
+( 55%) Producing: explorer/beta/test/%/schema.metapack
+( 58%) Producing: schemas/alpha/test/%/blaze-exhaustive.metapack
+( 61%) Producing: schemas/alpha/test/%/blaze-fast.metapack
+( 63%) Producing: schemas/alpha/test/%/editor.metapack
+( 66%) Producing: schemas/beta/test/%/blaze-exhaustive.metapack
+( 69%) Producing: schemas/beta/test/%/blaze-fast.metapack
+( 72%) Producing: schemas/beta/test/%/editor.metapack
+( 75%) Producing: explorer/alpha/%/directory.metapack
+( 77%) Producing: explorer/alpha/test/%/schema-html.metapack
+( 80%) Producing: explorer/beta/%/directory.metapack
+( 83%) Producing: explorer/beta/test/%/schema-html.metapack
+( 86%) Producing: explorer/%/directory.metapack
+( 88%) Producing: explorer/alpha/%/directory-html.metapack
+( 91%) Producing: explorer/beta/%/directory-html.metapack
+( 94%) Producing: explorer/%/directory-html.metapack
+( 97%) Producing: explorer/%/search.metapack
+(100%) Producing: routes.bin
+EOF
+
+diff "$TMP/output2.txt" "$TMP/expected2.txt"
+
+# Verify output after run 2 (static file still exists, alpha still uses it)
+cd "$TMP/output"
+find . -mindepth 1 | LC_ALL=C sort > "$TMP/manifest2.txt"
+cd - > /dev/null
+
+cat << EOF > "$TMP/expected_manifest2.txt"
+./configuration.json
+./explorer
+./explorer/%
+./explorer/%/404.metapack
+./explorer/%/directory-html.metapack
+./explorer/%/directory.metapack
+./explorer/%/search.metapack
+./explorer/alpha
+./explorer/alpha/%
+./explorer/alpha/%/directory-html.metapack
+./explorer/alpha/%/directory.metapack
+./explorer/alpha/test
+./explorer/alpha/test/%
+./explorer/alpha/test/%/schema-html.metapack
+./explorer/alpha/test/%/schema.metapack
+./explorer/beta
+./explorer/beta/%
+./explorer/beta/%/directory-html.metapack
+./explorer/beta/%/directory.metapack
+./explorer/beta/test
+./explorer/beta/test/%
+./explorer/beta/test/%/schema-html.metapack
+./explorer/beta/test/%/schema.metapack
+./routes.bin
+./schemas
+./schemas/alpha
+./schemas/alpha/test
+./schemas/alpha/test/%
+./schemas/alpha/test/%/blaze-exhaustive.metapack
+./schemas/alpha/test/%/blaze-fast.metapack
+./schemas/alpha/test/%/bundle.metapack
+./schemas/alpha/test/%/dependencies.metapack
+./schemas/alpha/test/%/dependents.metapack
+./schemas/alpha/test/%/editor.metapack
+./schemas/alpha/test/%/health.metapack
+./schemas/alpha/test/%/locations.metapack
+./schemas/alpha/test/%/positions.metapack
+./schemas/alpha/test/%/schema.metapack
+./schemas/alpha/test/%/stats.metapack
+./schemas/beta
+./schemas/beta/test
+./schemas/beta/test/%
+./schemas/beta/test/%/blaze-exhaustive.metapack
+./schemas/beta/test/%/blaze-fast.metapack
+./schemas/beta/test/%/bundle.metapack
+./schemas/beta/test/%/dependencies.metapack
+./schemas/beta/test/%/dependents.metapack
+./schemas/beta/test/%/editor.metapack
+./schemas/beta/test/%/health.metapack
+./schemas/beta/test/%/locations.metapack
+./schemas/beta/test/%/positions.metapack
+./schemas/beta/test/%/schema.metapack
+./schemas/beta/test/%/stats.metapack
+./state.bin
+./static
+./static/${HASH}.metapack
+./version.json
+EOF
+
+diff "$TMP/manifest2.txt" "$TMP/expected_manifest2.txt"
diff --git a/test/e2e/html/hurl/list.hurl b/test/e2e/html/hurl/list.hurl
index 36ae93145..06c8831a1 100644
--- a/test/e2e/html/hurl/list.hurl
+++ b/test/e2e/html/hurl/list.hurl
@@ -13,6 +13,7 @@ jsonpath "$.path" == "/"
jsonpath "$.url" == "{{base}}"
jsonpath "$.breadcrumb" count == 0
jsonpath "$.schemas" == 75
+jsonpath "$.documentation" not exists
jsonpath "$.entries" count >= 2
jsonpath "$.entries[0].name" == "self"
jsonpath "$.entries[0].title" == "Self"
@@ -21,6 +22,7 @@ jsonpath "$.entries[0].type" == "directory"
jsonpath "$.entries[0].health" == 100
jsonpath "$.entries[0].schemas" == 31
jsonpath "$.entries[0].path" == "/self/"
+jsonpath "$.entries[0].documentation" not exists
jsonpath "$.entries[1].name" == "test"
jsonpath "$.entries[1].title" == "Test"
jsonpath "$.entries[1].description" == "A directory full of testing schemas"
@@ -28,6 +30,7 @@ jsonpath "$.entries[1].type" == "directory"
jsonpath "$.entries[1].health" == 12
jsonpath "$.entries[1].schemas" == 44
jsonpath "$.entries[1].path" == "/test/"
+jsonpath "$.entries[1].documentation" not exists
POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
```
@@ -57,22 +60,26 @@ jsonpath "$.breadcrumb[0].path" == "/test/"
jsonpath "$.description" == "A directory full of testing schemas"
jsonpath "$.title" == "Test"
jsonpath "$.schemas" == 44
+jsonpath "$.documentation" not exists
jsonpath "$.entries" count == 12
jsonpath "$.entries[0].name" == "v2.0"
jsonpath "$.entries[0].type" == "directory"
jsonpath "$.entries[0].health" == 0
jsonpath "$.entries[0].schemas" == 1
jsonpath "$.entries[0].path" == "/test/v2.0/"
+jsonpath "$.entries[0].documentation" not exists
jsonpath "$.entries[1].name" == "bundling"
jsonpath "$.entries[1].type" == "directory"
jsonpath "$.entries[1].health" == 59
jsonpath "$.entries[1].schemas" == 2
jsonpath "$.entries[1].path" == "/test/bundling/"
+jsonpath "$.entries[1].documentation" not exists
jsonpath "$.entries[2].name" == "camelcase"
jsonpath "$.entries[2].type" == "directory"
jsonpath "$.entries[2].health" == 25
jsonpath "$.entries[2].schemas" == 2
jsonpath "$.entries[2].path" == "/test/camelcase/"
+jsonpath "$.entries[2].documentation" not exists
jsonpath "$.entries[3].name" == "doc"
jsonpath "$.entries[3].type" == "directory"
jsonpath "$.entries[3].health" == 0
@@ -81,44 +88,53 @@ jsonpath "$.entries[3].path" == "/test/doc/"
jsonpath "$.entries[3].title" == "A sample schema folder"
jsonpath "$.entries[3].description" == "For **testing** purposes with a [link](https://example.com)"
jsonpath "$.entries[3].github" == "sourcemeta/one"
+jsonpath "$.entries[3].documentation" not exists
jsonpath "$.entries[4].name" == "extension"
jsonpath "$.entries[4].type" == "directory"
jsonpath "$.entries[4].health" == 17
jsonpath "$.entries[4].schemas" == 3
jsonpath "$.entries[4].path" == "/test/extension/"
+jsonpath "$.entries[4].documentation" not exists
jsonpath "$.entries[5].name" == "hyper"
jsonpath "$.entries[5].type" == "directory"
jsonpath "$.entries[5].health" == 34
jsonpath "$.entries[5].schemas" == 1
jsonpath "$.entries[5].path" == "/test/hyper/"
+jsonpath "$.entries[5].documentation" not exists
jsonpath "$.entries[6].name" == "markdown-alert"
jsonpath "$.entries[6].type" == "directory"
jsonpath "$.entries[6].schemas" == 1
jsonpath "$.entries[6].path" == "/test/markdown-alert/"
+jsonpath "$.entries[6].documentation" not exists
jsonpath "$.entries[7].name" == "no-base"
jsonpath "$.entries[7].type" == "directory"
jsonpath "$.entries[7].health" == 0
jsonpath "$.entries[7].schemas" == 2
jsonpath "$.entries[7].path" == "/test/no-base/"
+jsonpath "$.entries[7].documentation" not exists
jsonpath "$.entries[8].name" == "no-blaze"
jsonpath "$.entries[8].type" == "directory"
jsonpath "$.entries[8].health" == 0
jsonpath "$.entries[8].schemas" == 1
jsonpath "$.entries[8].path" == "/test/no-blaze/"
+jsonpath "$.entries[8].documentation" not exists
jsonpath "$.entries[9].name" == "same"
jsonpath "$.entries[9].type" == "directory"
jsonpath "$.entries[9].health" == 0
jsonpath "$.entries[9].schemas" == 1
jsonpath "$.entries[9].path" == "/test/same/"
+jsonpath "$.entries[9].documentation" not exists
jsonpath "$.entries[10].name" == "schemas"
jsonpath "$.entries[10].type" == "directory"
jsonpath "$.entries[10].health" == 7
jsonpath "$.entries[10].schemas" == 25
jsonpath "$.entries[10].path" == "/test/schemas/"
+jsonpath "$.entries[10].documentation" not exists
jsonpath "$.entries[11].name" == "unsafe-alert"
jsonpath "$.entries[11].type" == "directory"
jsonpath "$.entries[11].schemas" == 1
jsonpath "$.entries[11].path" == "/test/unsafe-alert/"
+jsonpath "$.entries[11].documentation" not exists
POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
```
@@ -143,6 +159,7 @@ header "Last-Modified" exists
jsonpath "$.path" == "/test/v2.0"
jsonpath "$.url" == "{{base}}/test/v2.0"
jsonpath "$.schemas" == 1
+jsonpath "$.documentation" not exists
jsonpath "$.breadcrumb" count == 2
jsonpath "$.breadcrumb[0].name" == "test"
jsonpath "$.breadcrumb[0].path" == "/test/"
@@ -160,6 +177,7 @@ jsonpath "$.entries[0].provenance" == null
jsonpath "$.entries[0].path" == "/test/v2.0/schema"
jsonpath "$.entries[0].health" == 0
jsonpath "$.entries[0].dependencies" == 0
+jsonpath "$.entries[0].documentation" not exists
POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
```
@@ -207,6 +225,7 @@ jsonpath "$.path" == "/test/schemas/versions"
jsonpath "$.url" == "{{base}}/test/schemas/versions"
jsonpath "$.health" == 0
jsonpath "$.schemas" == 1
+jsonpath "$.documentation" not exists
jsonpath "$.breadcrumb" count == 3
jsonpath "$.breadcrumb[0].name" == "test"
jsonpath "$.breadcrumb[0].path" == "/test/"
@@ -226,6 +245,7 @@ jsonpath "$.entries[0].provenance" == null
jsonpath "$.entries[0].path" == "/test/schemas/versions/v1.2.3"
jsonpath "$.entries[0].health" == 0
jsonpath "$.entries[0].dependencies" == 0
+jsonpath "$.entries[0].documentation" not exists
POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
```
@@ -251,6 +271,7 @@ jsonpath "$.path" == "/test/schemas/clash/foo"
jsonpath "$.url" == "{{base}}/test/schemas/clash/foo"
jsonpath "$.health" == 0
jsonpath "$.schemas" == 1
+jsonpath "$.documentation" not exists
jsonpath "$.breadcrumb" count == 4
jsonpath "$.breadcrumb[0].name" == "test"
jsonpath "$.breadcrumb[0].path" == "/test/"
@@ -272,6 +293,7 @@ jsonpath "$.entries[0].provenance" == null
jsonpath "$.entries[0].path" == "/test/schemas/clash/foo/bar"
jsonpath "$.entries[0].health" == 0
jsonpath "$.entries[0].dependencies" == 0
+jsonpath "$.entries[0].documentation" not exists
POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
```
@@ -297,6 +319,7 @@ jsonpath "$.path" == "/test/schemas/clash"
jsonpath "$.url" == "{{base}}/test/schemas/clash"
jsonpath "$.health" == 0
jsonpath "$.schemas" == 2
+jsonpath "$.documentation" not exists
jsonpath "$.breadcrumb" count == 3
jsonpath "$.breadcrumb[0].name" == "test"
jsonpath "$.breadcrumb[0].path" == "/test/"
@@ -310,6 +333,7 @@ jsonpath "$.entries[0].type" == "directory"
jsonpath "$.entries[0].health" == 0
jsonpath "$.entries[0].schemas" == 1
jsonpath "$.entries[0].path" == "/test/schemas/clash/foo/"
+jsonpath "$.entries[0].documentation" not exists
jsonpath "$.entries[1].name" == "foo"
jsonpath "$.entries[1].type" == "schema"
jsonpath "$.entries[1].bytes" == 120
@@ -321,6 +345,7 @@ jsonpath "$.entries[1].health" == 0
jsonpath "$.entries[1].dependencies" == 0
jsonpath "$.entries[1].alert" == null
jsonpath "$.entries[1].provenance" == null
+jsonpath "$.entries[1].documentation" not exists
POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
```
@@ -346,6 +371,7 @@ jsonpath "$.path" == "/test/extension"
jsonpath "$.url" == "{{base}}/test/extension"
jsonpath "$.health" == 17
jsonpath "$.schemas" == 3
+jsonpath "$.documentation" not exists
jsonpath "$.breadcrumb" count == 2
jsonpath "$.breadcrumb[0].name" == "test"
jsonpath "$.breadcrumb[0].path" == "/test/"
@@ -363,6 +389,7 @@ jsonpath "$.entries[0].provenance" == null
jsonpath "$.entries[0].path" == "/test/extension/to-without"
jsonpath "$.entries[0].health" == 50
jsonpath "$.entries[0].dependencies" == 1
+jsonpath "$.entries[0].documentation" not exists
jsonpath "$.entries[1].bytes" == 132
jsonpath "$.entries[1].baseDialect" == "http://json-schema.org/draft-07/schema#"
jsonpath "$.entries[1].dialect" == "http://json-schema.org/draft-07/schema#"
@@ -374,6 +401,7 @@ jsonpath "$.entries[1].provenance" == null
jsonpath "$.entries[1].path" == "/test/extension/with"
jsonpath "$.entries[1].health" == 0
jsonpath "$.entries[1].dependencies" == 0
+jsonpath "$.entries[1].documentation" not exists
jsonpath "$.entries[2].bytes" == 135
jsonpath "$.entries[2].baseDialect" == "http://json-schema.org/draft-07/schema#"
jsonpath "$.entries[2].dialect" == "http://json-schema.org/draft-07/schema#"
@@ -385,6 +413,7 @@ jsonpath "$.entries[2].provenance" == null
jsonpath "$.entries[2].path" == "/test/extension/without"
jsonpath "$.entries[2].health" == 0
jsonpath "$.entries[2].dependencies" == 0
+jsonpath "$.entries[2].documentation" not exists
POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}}
```
diff --git a/test/unit/build/build_delta_test.cc b/test/unit/build/build_delta_test.cc
index a36079bf3..e1863a822 100644
--- a/test/unit/build/build_delta_test.cc
+++ b/test/unit/build/build_delta_test.cc
@@ -1,14 +1,16 @@
#include
+#include
#include
#include
+#include
#include "build_test_utils.h"
#include // std::filesystem::path
+#include // std::ostringstream
#include // std::string
#include // std::unordered_map
-#include // std::vector
TEST(Build_delta, full_empty_registry) {
const std::filesystem::path output{"/output"};
@@ -18,7 +20,7 @@ TEST(Build_delta, full_empty_registry) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- false, "", {})};
+ false, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 4, 7);
@@ -60,7 +62,7 @@ TEST(Build_delta, full_single_schema) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- false, "", {})};
+ false, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 19);
@@ -189,7 +191,7 @@ TEST(Build_delta, incremental_changed_same_mtime) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 0, 0);
@@ -232,7 +234,7 @@ TEST(Build_delta, incremental_missing_schema_metapack) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 16);
@@ -381,7 +383,7 @@ TEST(Build_delta, incremental_one_schema_added) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 6, 15);
@@ -501,7 +503,7 @@ TEST(Build_delta, full_stale_file_in_entries) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- false, "", {})};
+ false, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 9, 20);
@@ -617,7 +619,7 @@ TEST(Build_delta, full_stale_directory_in_entries) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- false, "", {})};
+ false, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 9, 20);
@@ -729,7 +731,7 @@ TEST(Build_delta, full_with_comment) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- false, "Hello world", {})};
+ false, "Hello world", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 4, 8);
@@ -773,7 +775,7 @@ TEST(Build_delta, full_without_comment_removes_existing) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- false, "", {})};
+ false, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 5, 8);
@@ -820,7 +822,7 @@ TEST(Build_delta, incremental_with_comment) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "Hello world", {})};
+ true, "Hello world", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 17);
@@ -934,7 +936,7 @@ TEST(Build_delta, incremental_empty_comment_removes_existing) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 17);
@@ -1054,7 +1056,7 @@ TEST(Build_delta, incremental_no_changes_adds_comment) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "hello", {})};
+ true, "hello", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 1, 1);
@@ -1093,7 +1095,7 @@ TEST(Build_delta, incremental_no_changes_removes_comment) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 1, 1);
@@ -1130,7 +1132,7 @@ TEST(Build_delta, incremental_schema_removed_cleans_stale_entries) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 3, 5);
@@ -1178,7 +1180,7 @@ TEST(Build_delta, remove_wave_deduplicates_children_of_removed_directories) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 3, 5);
@@ -1226,7 +1228,7 @@ TEST(Build_delta, full_config_change_to_empty_schemas) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- false, "", {})};
+ false, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 5, 9);
@@ -1270,7 +1272,7 @@ TEST(Build_delta, full_single_schema_evaluate_false) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- false, "", {})};
+ false, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 17);
@@ -1377,7 +1379,7 @@ TEST(Build_delta, full_evaluate_false_removes_existing_blaze) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- false, "", {})};
+ false, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 9, 19);
@@ -1489,7 +1491,7 @@ TEST(Build_delta, incremental_evaluate_false) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 14);
@@ -1604,7 +1606,7 @@ TEST(Build_delta, incremental_missing_blaze_exhaustive) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 2, 4);
@@ -1673,7 +1675,7 @@ TEST(Build_delta, incremental_missing_bundle) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 2, 7);
@@ -1755,7 +1757,7 @@ TEST(Build_delta, incremental_missing_web_schema) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 2, 4);
@@ -1815,10 +1817,10 @@ TEST(Build_delta, incremental_missing_web_not_checked_headless) {
{.file_mark = MTIME(100), .dependencies = {}});
const sourcemeta::one::Resolver::Views schemas{
{"https://example.com/foo", {"/src/foo.json", "foo", MTIME(40)}}};
- const auto plan{
- sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
- sourcemeta::one::BuildPlan::Type::Headless,
- entries, output, schemas, "1.0.0", true, "", {})};
+ const auto plan{sourcemeta::one::delta(
+ sourcemeta::one::BuildPhase::Produce,
+ sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas,
+ "1.0.0", true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 0, 0);
@@ -1866,7 +1868,7 @@ TEST(Build_delta, mtime_nothing_changed) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 0, 0);
@@ -1918,7 +1920,7 @@ TEST(Build_delta, mtime_source_newer) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 6, 15);
@@ -2052,7 +2054,7 @@ TEST(Build_delta, mtime_no_entry) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 6, 15);
@@ -2187,7 +2189,7 @@ TEST(Build_delta, mtime_no_file_mark) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 6, 15);
@@ -2303,7 +2305,7 @@ TEST(Build_delta, incremental_reverse_dep_direct) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 28);
@@ -2495,7 +2497,7 @@ TEST(Build_delta, incremental_reverse_dep_transitive) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 40);
@@ -2751,7 +2753,7 @@ TEST(Build_delta, mtime_reverse_dep) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 27);
@@ -2927,7 +2929,7 @@ TEST(Build_delta, incremental_evaluate_false_removes_existing_blaze) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 16);
@@ -3023,10 +3025,10 @@ TEST(Build_delta, headless_full_empty_registry) {
const std::filesystem::path output{"/output"};
const sourcemeta::one::BuildState entries;
const sourcemeta::one::Resolver::Views schemas;
- const auto plan{
- sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
- sourcemeta::one::BuildPlan::Type::Headless,
- entries, output, schemas, "1.0.0", false, "", {})};
+ const auto plan{sourcemeta::one::delta(
+ sourcemeta::one::BuildPhase::Produce,
+ sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas,
+ "1.0.0", false, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 4, 5);
@@ -3055,10 +3057,10 @@ TEST(Build_delta, headless_full_single_schema) {
const sourcemeta::one::BuildState entries;
const sourcemeta::one::Resolver::Views schemas{
{"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}};
- const auto plan{
- sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
- sourcemeta::one::BuildPlan::Type::Headless,
- entries, output, schemas, "1.0.0", false, "", {})};
+ const auto plan{sourcemeta::one::delta(
+ sourcemeta::one::BuildPhase::Produce,
+ sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas,
+ "1.0.0", false, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 8, 16);
@@ -3156,10 +3158,10 @@ TEST(Build_delta, headless_incremental) {
{.file_mark = MTIME(100), .dependencies = {}});
const sourcemeta::one::Resolver::Views schemas{
{"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}};
- const auto plan{
- sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
- sourcemeta::one::BuildPlan::Type::Headless,
- entries, output, schemas, "1.0.0", true, "", {})};
+ const auto plan{sourcemeta::one::delta(
+ sourcemeta::one::BuildPhase::Produce,
+ sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas,
+ "1.0.0", true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 6, 13);
@@ -3262,10 +3264,10 @@ TEST(Build_delta, full_to_headless_removes_web) {
{.file_mark = MTIME(100), .dependencies = {}});
const sourcemeta::one::Resolver::Views schemas{
{"https://example.com/foo", {"/src/foo.json", "foo", MTIME(200)}}};
- const auto plan{
- sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
- sourcemeta::one::BuildPlan::Type::Headless,
- entries, output, schemas, "1.0.0", true, "", {})};
+ const auto plan{sourcemeta::one::delta(
+ sourcemeta::one::BuildPhase::Produce,
+ sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas,
+ "1.0.0", true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 8, 18);
@@ -3381,10 +3383,10 @@ TEST(Build_delta, full_to_headless_no_change_removes_web) {
const sourcemeta::one::Resolver::Views schemas{
{"https://example.com/foo",
{.path = "/src/foo.json", .relative_path = "foo", .mtime = MTIME(100)}}};
- const auto plan{
- sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
- sourcemeta::one::BuildPlan::Type::Headless,
- entries, output, schemas, "1.0.0", true, "", {})};
+ const auto plan{sourcemeta::one::delta(
+ sourcemeta::one::BuildPhase::Produce,
+ sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas,
+ "1.0.0", true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 2, 5);
@@ -3433,7 +3435,7 @@ TEST(Build_delta, headless_to_full_incremental) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 16);
@@ -3573,7 +3575,7 @@ TEST(Build_delta, headless_to_full_full_rebuild) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 7, 16);
@@ -3685,10 +3687,10 @@ TEST(Build_delta, full_to_headless_full_rebuild) {
{.file_mark = MTIME(100), .dependencies = {}});
const sourcemeta::one::Resolver::Views schemas{
{"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}};
- const auto plan{
- sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
- sourcemeta::one::BuildPlan::Type::Headless,
- entries, output, schemas, "2.0.0", false, "", {})};
+ const auto plan{sourcemeta::one::delta(
+ sourcemeta::one::BuildPhase::Produce,
+ sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas,
+ "2.0.0", false, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 9, 19);
@@ -3790,10 +3792,10 @@ TEST(Build_delta, full_single_schema_nested_path_headless) {
const sourcemeta::one::Resolver::Views schemas{
{"https://example.com/test",
{"/src/test.json", "example/test", MTIME(100)}}};
- const auto plan{
- sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
- sourcemeta::one::BuildPlan::Type::Headless,
- entries, output, schemas, "1.0.0", false, "", {})};
+ const auto plan{sourcemeta::one::delta(
+ sourcemeta::one::BuildPhase::Produce,
+ sourcemeta::one::BuildPlan::Type::Headless, entries, output, schemas,
+ "1.0.0", false, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Headless, 9, 17);
@@ -3948,7 +3950,7 @@ TEST(Build_delta, incremental_add_schema_preserves_intermediate_dirs) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 19);
@@ -4193,7 +4195,7 @@ TEST(Build_delta, incremental_directory_listing_includes_unchanged_siblings) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 6, 15);
@@ -4332,7 +4334,7 @@ TEST(Build_delta, incremental_add_schema_rebuilds_all_dependents) {
const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- true, "", {})};
+ true, "", {}, {})};
EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 6, 15);
@@ -4465,7 +4467,7 @@ TEST(Build_delta, limits_zero_does_not_enforce) {
EXPECT_NO_THROW(sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- false, "", limits));
+ false, "", limits, {}));
}
TEST(Build_delta, limits_directory_entries_within_limit) {
@@ -4479,7 +4481,7 @@ TEST(Build_delta, limits_directory_entries_within_limit) {
EXPECT_NO_THROW(sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- false, "", limits));
+ false, "", limits, {}));
}
TEST(Build_delta, limits_directory_entries_exceeded_flat) {
@@ -4494,7 +4496,7 @@ TEST(Build_delta, limits_directory_entries_exceeded_flat) {
try {
sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full, entries,
- output, schemas, "1.0.0", false, "", limits);
+ output, schemas, "1.0.0", false, "", limits, {});
FAIL();
} catch (const sourcemeta::one::BuildTooManyDirectoryEntriesError &error) {
EXPECT_EQ(error.path(), output / "schemas");
@@ -4515,7 +4517,7 @@ TEST(Build_delta, limits_directory_entries_nested_ok) {
EXPECT_NO_THROW(sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full,
entries, output, schemas, "1.0.0",
- false, "", limits));
+ false, "", limits, {}));
}
TEST(Build_delta, limits_directory_entries_subdirectories_count) {
@@ -4530,7 +4532,7 @@ TEST(Build_delta, limits_directory_entries_subdirectories_count) {
try {
sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
sourcemeta::one::BuildPlan::Type::Full, entries,
- output, schemas, "1.0.0", false, "", limits);
+ output, schemas, "1.0.0", false, "", limits, {});
FAIL();
} catch (const sourcemeta::one::BuildTooManyDirectoryEntriesError &error) {
EXPECT_EQ(error.path(), output / "schemas");
@@ -4539,3 +4541,501 @@ TEST(Build_delta, limits_directory_entries_subdirectories_count) {
FAIL();
}
}
+
+TEST(Build_delta, full_single_schema_with_documentation) {
+ const std::filesystem::path output{"/output"};
+ const sourcemeta::one::BuildState entries;
+ const sourcemeta::one::Resolver::Views schemas{
+ {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}};
+
+ sourcemeta::one::Configuration configuration;
+ auto extra{sourcemeta::core::JSON::make_object()};
+ extra.assign("x-sourcemeta-one:documentation",
+ sourcemeta::core::JSON{"/docs/readme.md"});
+ sourcemeta::one::Configuration::Collection collection;
+ collection.extra = std::move(extra);
+ configuration.entries.emplace("foo", std::move(collection));
+
+ // TODO: Improve the use of sha256 for strings in Core
+ std::ostringstream hash_stream;
+ sourcemeta::core::sha256("/docs/readme.md", hash_stream);
+ const auto static_path{output / "static" / (hash_stream.str() + ".metapack")};
+
+ const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
+ sourcemeta::one::BuildPlan::Type::Full,
+ entries, output, schemas, "1.0.0",
+ false, "", {}, configuration)};
+
+ EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 22);
+
+ EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json",
+ "");
+ EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0");
+
+ EXPECT_ACTION(plan, 1, 0, 3, WebNotFound,
+ output / "explorer" / "%" / "404.metapack", "",
+ output / "configuration.json");
+ EXPECT_ACTION(plan, 1, 1, 3, Materialise,
+ output / "schemas" / "foo" / "%" / "schema.metapack",
+ "https://example.com/foo",
+ std::filesystem::path{"/"} / "src" / "foo.json",
+ output / "configuration.json");
+ EXPECT_ACTION(plan, 1, 2, 3, StaticFile, static_path, "text/markdown",
+ std::filesystem::path{"/docs/readme.md"});
+
+ EXPECT_ACTION(plan, 2, 0, 5, DirectoryList,
+ output / "explorer" / "foo" / "%" / "directory.metapack", "",
+ static_path);
+ EXPECT_ACTION(plan, 2, 1, 5, Dependencies,
+ output / "schemas" / "foo" / "%" / "dependencies.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 2, 2, 5, Locations,
+ output / "schemas" / "foo" / "%" / "locations.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 2, 3, 5, Positions,
+ output / "schemas" / "foo" / "%" / "positions.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 2, 4, 5, Stats,
+ output / "schemas" / "foo" / "%" / "stats.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack");
+
+ EXPECT_ACTION_UNORDERED(
+ plan, 3, 0, 3, WebDirectory,
+ output / "explorer" / "foo" / "%" / "directory-html.metapack", "",
+ output / "explorer" / "foo" / "%" / "directory.metapack", static_path);
+ EXPECT_ACTION(plan, 3, 1, 3, Bundle,
+ output / "schemas" / "foo" / "%" / "bundle.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack",
+ output / "schemas" / "foo" / "%" / "dependencies.metapack");
+ EXPECT_ACTION(plan, 3, 2, 3, Health,
+ output / "schemas" / "foo" / "%" / "health.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack",
+ output / "schemas" / "foo" / "%" / "dependencies.metapack");
+
+ EXPECT_ACTION(plan, 4, 0, 4, SchemaMetadata,
+ output / "explorer" / "foo" / "%" / "schema.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack",
+ output / "schemas" / "foo" / "%" / "health.metapack",
+ output / "schemas" / "foo" / "%" / "dependencies.metapack");
+ EXPECT_ACTION(plan, 4, 1, 4, BlazeExhaustive,
+ output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "bundle.metapack");
+ EXPECT_ACTION(plan, 4, 2, 4, BlazeFast,
+ output / "schemas" / "foo" / "%" / "blaze-fast.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "bundle.metapack");
+ EXPECT_ACTION(plan, 4, 3, 4, Editor,
+ output / "schemas" / "foo" / "%" / "editor.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "bundle.metapack");
+
+ EXPECT_ACTION_UNORDERED(plan, 5, 0, 2, DirectoryList,
+ output / "explorer" / "%" / "directory.metapack", "",
+ output / "explorer" / "foo" / "%" / "schema.metapack",
+ output / "explorer" / "foo" / "%" /
+ "directory.metapack");
+ EXPECT_ACTION(plan, 5, 1, 2, WebSchema,
+ output / "explorer" / "foo" / "%" / "schema-html.metapack",
+ "https://example.com/foo",
+ output / "explorer" / "foo" / "%" / "schema.metapack",
+ output / "schemas" / "foo" / "%" / "health.metapack");
+
+ EXPECT_ACTION(plan, 6, 0, 2, WebIndex,
+ output / "explorer" / "%" / "directory-html.metapack", "",
+ output / "explorer" / "%" / "directory.metapack");
+ EXPECT_ACTION(plan, 6, 1, 2, SearchIndex,
+ output / "explorer" / "%" / "search.metapack", "",
+ output / "explorer" / "%" / "directory.metapack");
+
+ EXPECT_ACTION(plan, 7, 0, 1, Routes, output / "routes.bin", "Full",
+ output / "configuration.json");
+
+ EXPECT_TOTAL_FILES(
+ plan, entries, output / "configuration.json", output / "version.json",
+ static_path, output / "schemas" / "foo" / "%" / "schema.metapack",
+ output / "schemas" / "foo" / "%" / "dependencies.metapack",
+ output / "schemas" / "foo" / "%" / "locations.metapack",
+ output / "schemas" / "foo" / "%" / "positions.metapack",
+ output / "schemas" / "foo" / "%" / "stats.metapack",
+ output / "schemas" / "foo" / "%" / "bundle.metapack",
+ output / "schemas" / "foo" / "%" / "health.metapack",
+ output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack",
+ output / "schemas" / "foo" / "%" / "blaze-fast.metapack",
+ output / "schemas" / "foo" / "%" / "editor.metapack",
+ output / "explorer" / "foo" / "%" / "schema.metapack",
+ output / "explorer" / "foo" / "%" / "schema-html.metapack",
+ output / "explorer" / "foo" / "%" / "directory.metapack",
+ output / "explorer" / "foo" / "%" / "directory-html.metapack",
+ output / "explorer" / "%" / "search.metapack",
+ output / "explorer" / "%" / "directory.metapack",
+ output / "explorer" / "%" / "directory-html.metapack",
+ output / "explorer" / "%" / "404.metapack", output / "routes.bin");
+}
+
+TEST(Build_delta, full_two_collections_same_documentation) {
+ const std::filesystem::path output{"/output"};
+ const sourcemeta::one::BuildState entries;
+ const sourcemeta::one::Resolver::Views schemas{
+ {"https://example.com/alpha/a",
+ {"/src/alpha/a.json", "alpha/a", MTIME(100)}},
+ {"https://example.com/beta/b",
+ {"/src/beta/b.json", "beta/b", MTIME(100)}}};
+
+ sourcemeta::one::Configuration configuration;
+
+ auto extra_alpha{sourcemeta::core::JSON::make_object()};
+ extra_alpha.assign("x-sourcemeta-one:documentation",
+ sourcemeta::core::JSON{"/docs/shared.md"});
+ sourcemeta::one::Configuration::Collection collection_alpha;
+ collection_alpha.extra = std::move(extra_alpha);
+ configuration.entries.emplace("alpha", std::move(collection_alpha));
+
+ auto extra_beta{sourcemeta::core::JSON::make_object()};
+ extra_beta.assign("x-sourcemeta-one:documentation",
+ sourcemeta::core::JSON{"/docs/shared.md"});
+ sourcemeta::one::Configuration::Collection collection_beta;
+ collection_beta.extra = std::move(extra_beta);
+ configuration.entries.emplace("beta", std::move(collection_beta));
+
+ // TODO: Improve the use of sha256 for strings in Core
+ std::ostringstream hash_stream;
+ sourcemeta::core::sha256("/docs/shared.md", hash_stream);
+ const auto static_path{output / "static" / (hash_stream.str() + ".metapack")};
+
+ const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
+ sourcemeta::one::BuildPlan::Type::Full,
+ entries, output, schemas, "1.0.0",
+ false, "", {}, configuration)};
+
+ EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 9, 36);
+
+ EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json",
+ "");
+ EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0");
+
+ EXPECT_ACTION(plan, 1, 0, 4, WebNotFound,
+ output / "explorer" / "%" / "404.metapack", "",
+ output / "configuration.json");
+ EXPECT_ACTION(plan, 1, 1, 4, Materialise,
+ output / "schemas" / "alpha" / "a" / "%" / "schema.metapack",
+ "https://example.com/alpha/a",
+ std::filesystem::path{"/"} / "src" / "alpha" / "a.json",
+ output / "configuration.json");
+ EXPECT_ACTION(plan, 1, 2, 4, Materialise,
+ output / "schemas" / "beta" / "b" / "%" / "schema.metapack",
+ "https://example.com/beta/b",
+ std::filesystem::path{"/"} / "src" / "beta" / "b.json",
+ output / "configuration.json");
+ EXPECT_ACTION(plan, 1, 3, 4, StaticFile, static_path, "text/markdown",
+ std::filesystem::path{"/docs/shared.md"});
+
+ EXPECT_ACTION(plan, 2, 0, 8, Dependencies,
+ output / "schemas" / "alpha" / "a" / "%" /
+ "dependencies.metapack",
+ "https://example.com/alpha/a",
+ output / "schemas" / "alpha" / "a" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 2, 1, 8, Locations,
+ output / "schemas" / "alpha" / "a" / "%" / "locations.metapack",
+ "https://example.com/alpha/a",
+ output / "schemas" / "alpha" / "a" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 2, 2, 8, Positions,
+ output / "schemas" / "alpha" / "a" / "%" / "positions.metapack",
+ "https://example.com/alpha/a",
+ output / "schemas" / "alpha" / "a" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 2, 3, 8, Stats,
+ output / "schemas" / "alpha" / "a" / "%" / "stats.metapack",
+ "https://example.com/alpha/a",
+ output / "schemas" / "alpha" / "a" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 2, 4, 8, Dependencies,
+ output / "schemas" / "beta" / "b" / "%" /
+ "dependencies.metapack",
+ "https://example.com/beta/b",
+ output / "schemas" / "beta" / "b" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 2, 5, 8, Locations,
+ output / "schemas" / "beta" / "b" / "%" / "locations.metapack",
+ "https://example.com/beta/b",
+ output / "schemas" / "beta" / "b" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 2, 6, 8, Positions,
+ output / "schemas" / "beta" / "b" / "%" / "positions.metapack",
+ "https://example.com/beta/b",
+ output / "schemas" / "beta" / "b" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 2, 7, 8, Stats,
+ output / "schemas" / "beta" / "b" / "%" / "stats.metapack",
+ "https://example.com/beta/b",
+ output / "schemas" / "beta" / "b" / "%" / "schema.metapack");
+
+ EXPECT_ACTION(plan, 3, 0, 4, Bundle,
+ output / "schemas" / "alpha" / "a" / "%" / "bundle.metapack",
+ "https://example.com/alpha/a",
+ output / "schemas" / "alpha" / "a" / "%" / "schema.metapack",
+ output / "schemas" / "alpha" / "a" / "%" /
+ "dependencies.metapack");
+ EXPECT_ACTION(plan, 3, 1, 4, Health,
+ output / "schemas" / "alpha" / "a" / "%" / "health.metapack",
+ "https://example.com/alpha/a",
+ output / "schemas" / "alpha" / "a" / "%" / "schema.metapack",
+ output / "schemas" / "alpha" / "a" / "%" /
+ "dependencies.metapack");
+ EXPECT_ACTION(plan, 3, 2, 4, Bundle,
+ output / "schemas" / "beta" / "b" / "%" / "bundle.metapack",
+ "https://example.com/beta/b",
+ output / "schemas" / "beta" / "b" / "%" / "schema.metapack",
+ output / "schemas" / "beta" / "b" / "%" /
+ "dependencies.metapack");
+ EXPECT_ACTION(plan, 3, 3, 4, Health,
+ output / "schemas" / "beta" / "b" / "%" / "health.metapack",
+ "https://example.com/beta/b",
+ output / "schemas" / "beta" / "b" / "%" / "schema.metapack",
+ output / "schemas" / "beta" / "b" / "%" /
+ "dependencies.metapack");
+
+ EXPECT_ACTION(plan, 4, 0, 8, SchemaMetadata,
+ output / "explorer" / "alpha" / "a" / "%" / "schema.metapack",
+ "https://example.com/alpha/a",
+ output / "schemas" / "alpha" / "a" / "%" / "schema.metapack",
+ output / "schemas" / "alpha" / "a" / "%" / "health.metapack",
+ output / "schemas" / "alpha" / "a" / "%" /
+ "dependencies.metapack");
+ EXPECT_ACTION(plan, 4, 1, 8, SchemaMetadata,
+ output / "explorer" / "beta" / "b" / "%" / "schema.metapack",
+ "https://example.com/beta/b",
+ output / "schemas" / "beta" / "b" / "%" / "schema.metapack",
+ output / "schemas" / "beta" / "b" / "%" / "health.metapack",
+ output / "schemas" / "beta" / "b" / "%" /
+ "dependencies.metapack");
+ EXPECT_ACTION(plan, 4, 2, 8, BlazeExhaustive,
+ output / "schemas" / "alpha" / "a" / "%" /
+ "blaze-exhaustive.metapack",
+ "https://example.com/alpha/a",
+ output / "schemas" / "alpha" / "a" / "%" / "bundle.metapack");
+ EXPECT_ACTION(plan, 4, 3, 8, BlazeFast,
+ output / "schemas" / "alpha" / "a" / "%" /
+ "blaze-fast.metapack",
+ "https://example.com/alpha/a",
+ output / "schemas" / "alpha" / "a" / "%" / "bundle.metapack");
+ EXPECT_ACTION(plan, 4, 4, 8, Editor,
+ output / "schemas" / "alpha" / "a" / "%" / "editor.metapack",
+ "https://example.com/alpha/a",
+ output / "schemas" / "alpha" / "a" / "%" / "bundle.metapack");
+ EXPECT_ACTION(plan, 4, 5, 8, BlazeExhaustive,
+ output / "schemas" / "beta" / "b" / "%" /
+ "blaze-exhaustive.metapack",
+ "https://example.com/beta/b",
+ output / "schemas" / "beta" / "b" / "%" / "bundle.metapack");
+ EXPECT_ACTION(plan, 4, 6, 8, BlazeFast,
+ output / "schemas" / "beta" / "b" / "%" / "blaze-fast.metapack",
+ "https://example.com/beta/b",
+ output / "schemas" / "beta" / "b" / "%" / "bundle.metapack");
+ EXPECT_ACTION(plan, 4, 7, 8, Editor,
+ output / "schemas" / "beta" / "b" / "%" / "editor.metapack",
+ "https://example.com/beta/b",
+ output / "schemas" / "beta" / "b" / "%" / "bundle.metapack");
+
+ EXPECT_ACTION_UNORDERED(
+ plan, 5, 0, 4, DirectoryList,
+ output / "explorer" / "alpha" / "%" / "directory.metapack", "",
+ output / "explorer" / "alpha" / "a" / "%" / "schema.metapack",
+ static_path);
+ EXPECT_ACTION(plan, 5, 1, 4, WebSchema,
+ output / "explorer" / "alpha" / "a" / "%" /
+ "schema-html.metapack",
+ "https://example.com/alpha/a",
+ output / "explorer" / "alpha" / "a" / "%" / "schema.metapack",
+ output / "schemas" / "alpha" / "a" / "%" / "health.metapack");
+ EXPECT_ACTION_UNORDERED(
+ plan, 5, 2, 4, DirectoryList,
+ output / "explorer" / "beta" / "%" / "directory.metapack", "",
+ output / "explorer" / "beta" / "b" / "%" / "schema.metapack",
+ static_path);
+ EXPECT_ACTION(plan, 5, 3, 4, WebSchema,
+ output / "explorer" / "beta" / "b" / "%" /
+ "schema-html.metapack",
+ "https://example.com/beta/b",
+ output / "explorer" / "beta" / "b" / "%" / "schema.metapack",
+ output / "schemas" / "beta" / "b" / "%" / "health.metapack");
+
+ EXPECT_ACTION_UNORDERED(
+ plan, 6, 0, 3, DirectoryList,
+ output / "explorer" / "%" / "directory.metapack", "",
+ output / "explorer" / "alpha" / "%" / "directory.metapack",
+ output / "explorer" / "beta" / "%" / "directory.metapack");
+ EXPECT_ACTION_UNORDERED(
+ plan, 6, 1, 3, WebDirectory,
+ output / "explorer" / "alpha" / "%" / "directory-html.metapack", "",
+ output / "explorer" / "alpha" / "%" / "directory.metapack", static_path);
+ EXPECT_ACTION_UNORDERED(
+ plan, 6, 2, 3, WebDirectory,
+ output / "explorer" / "beta" / "%" / "directory-html.metapack", "",
+ output / "explorer" / "beta" / "%" / "directory.metapack", static_path);
+
+ EXPECT_ACTION(plan, 7, 0, 2, WebIndex,
+ output / "explorer" / "%" / "directory-html.metapack", "",
+ output / "explorer" / "%" / "directory.metapack");
+ EXPECT_ACTION_UNORDERED(
+ plan, 7, 1, 2, SearchIndex, output / "explorer" / "%" / "search.metapack",
+ "", output / "explorer" / "alpha" / "%" / "directory.metapack",
+ output / "explorer" / "beta" / "%" / "directory.metapack",
+ output / "explorer" / "%" / "directory.metapack");
+
+ EXPECT_ACTION(plan, 8, 0, 1, Routes, output / "routes.bin", "Full",
+ output / "configuration.json");
+
+ EXPECT_TOTAL_FILES(
+ plan, entries, output / "configuration.json", output / "version.json",
+ static_path, output / "schemas" / "alpha" / "a" / "%" / "schema.metapack",
+ output / "schemas" / "alpha" / "a" / "%" / "dependencies.metapack",
+ output / "schemas" / "alpha" / "a" / "%" / "locations.metapack",
+ output / "schemas" / "alpha" / "a" / "%" / "positions.metapack",
+ output / "schemas" / "alpha" / "a" / "%" / "stats.metapack",
+ output / "schemas" / "alpha" / "a" / "%" / "bundle.metapack",
+ output / "schemas" / "alpha" / "a" / "%" / "health.metapack",
+ output / "schemas" / "alpha" / "a" / "%" / "blaze-exhaustive.metapack",
+ output / "schemas" / "alpha" / "a" / "%" / "blaze-fast.metapack",
+ output / "schemas" / "alpha" / "a" / "%" / "editor.metapack",
+ output / "schemas" / "beta" / "b" / "%" / "schema.metapack",
+ output / "schemas" / "beta" / "b" / "%" / "dependencies.metapack",
+ output / "schemas" / "beta" / "b" / "%" / "locations.metapack",
+ output / "schemas" / "beta" / "b" / "%" / "positions.metapack",
+ output / "schemas" / "beta" / "b" / "%" / "stats.metapack",
+ output / "schemas" / "beta" / "b" / "%" / "bundle.metapack",
+ output / "schemas" / "beta" / "b" / "%" / "health.metapack",
+ output / "schemas" / "beta" / "b" / "%" / "blaze-exhaustive.metapack",
+ output / "schemas" / "beta" / "b" / "%" / "blaze-fast.metapack",
+ output / "schemas" / "beta" / "b" / "%" / "editor.metapack",
+ output / "explorer" / "alpha" / "a" / "%" / "schema.metapack",
+ output / "explorer" / "alpha" / "a" / "%" / "schema-html.metapack",
+ output / "explorer" / "alpha" / "%" / "directory.metapack",
+ output / "explorer" / "alpha" / "%" / "directory-html.metapack",
+ output / "explorer" / "beta" / "b" / "%" / "schema.metapack",
+ output / "explorer" / "beta" / "b" / "%" / "schema-html.metapack",
+ output / "explorer" / "beta" / "%" / "directory.metapack",
+ output / "explorer" / "beta" / "%" / "directory-html.metapack",
+ output / "explorer" / "%" / "search.metapack",
+ output / "explorer" / "%" / "directory.metapack",
+ output / "explorer" / "%" / "directory-html.metapack",
+ output / "explorer" / "%" / "404.metapack", output / "routes.bin");
+}
+
+TEST(Build_delta, full_no_documentation_no_static_file) {
+ const std::filesystem::path output{"/output"};
+ const sourcemeta::one::BuildState entries;
+ const sourcemeta::one::Resolver::Views schemas{
+ {"https://example.com/foo", {"/src/foo.json", "foo", MTIME(100)}}};
+
+ sourcemeta::one::Configuration configuration;
+ sourcemeta::one::Configuration::Collection collection;
+ configuration.entries.emplace("foo", std::move(collection));
+
+ const auto plan{sourcemeta::one::delta(sourcemeta::one::BuildPhase::Produce,
+ sourcemeta::one::BuildPlan::Type::Full,
+ entries, output, schemas, "1.0.0",
+ false, "", {}, configuration)};
+
+ EXPECT_CONSISTENT_PLAN(plan, entries, output, Full, 8, 19);
+
+ EXPECT_ACTION(plan, 0, 0, 2, Configuration, output / "configuration.json",
+ "");
+ EXPECT_ACTION(plan, 0, 1, 2, Version, output / "version.json", "1.0.0");
+
+ EXPECT_ACTION(plan, 1, 0, 2, WebNotFound,
+ output / "explorer" / "%" / "404.metapack", "",
+ output / "configuration.json");
+ EXPECT_ACTION(plan, 1, 1, 2, Materialise,
+ output / "schemas" / "foo" / "%" / "schema.metapack",
+ "https://example.com/foo",
+ std::filesystem::path{"/"} / "src" / "foo.json",
+ output / "configuration.json");
+
+ EXPECT_ACTION(plan, 2, 0, 4, Dependencies,
+ output / "schemas" / "foo" / "%" / "dependencies.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 2, 1, 4, Locations,
+ output / "schemas" / "foo" / "%" / "locations.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 2, 2, 4, Positions,
+ output / "schemas" / "foo" / "%" / "positions.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 2, 3, 4, Stats,
+ output / "schemas" / "foo" / "%" / "stats.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack");
+
+ EXPECT_ACTION(plan, 3, 0, 2, Bundle,
+ output / "schemas" / "foo" / "%" / "bundle.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack",
+ output / "schemas" / "foo" / "%" / "dependencies.metapack");
+ EXPECT_ACTION(plan, 3, 1, 2, Health,
+ output / "schemas" / "foo" / "%" / "health.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack",
+ output / "schemas" / "foo" / "%" / "dependencies.metapack");
+
+ EXPECT_ACTION(plan, 4, 0, 4, SchemaMetadata,
+ output / "explorer" / "foo" / "%" / "schema.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "schema.metapack",
+ output / "schemas" / "foo" / "%" / "health.metapack",
+ output / "schemas" / "foo" / "%" / "dependencies.metapack");
+ EXPECT_ACTION(plan, 4, 1, 4, BlazeExhaustive,
+ output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "bundle.metapack");
+ EXPECT_ACTION(plan, 4, 2, 4, BlazeFast,
+ output / "schemas" / "foo" / "%" / "blaze-fast.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "bundle.metapack");
+ EXPECT_ACTION(plan, 4, 3, 4, Editor,
+ output / "schemas" / "foo" / "%" / "editor.metapack",
+ "https://example.com/foo",
+ output / "schemas" / "foo" / "%" / "bundle.metapack");
+
+ EXPECT_ACTION(plan, 5, 0, 2, DirectoryList,
+ output / "explorer" / "%" / "directory.metapack", "",
+ output / "explorer" / "foo" / "%" / "schema.metapack");
+ EXPECT_ACTION(plan, 5, 1, 2, WebSchema,
+ output / "explorer" / "foo" / "%" / "schema-html.metapack",
+ "https://example.com/foo",
+ output / "explorer" / "foo" / "%" / "schema.metapack",
+ output / "schemas" / "foo" / "%" / "health.metapack");
+
+ EXPECT_ACTION(plan, 6, 0, 2, WebIndex,
+ output / "explorer" / "%" / "directory-html.metapack", "",
+ output / "explorer" / "%" / "directory.metapack");
+ EXPECT_ACTION(plan, 6, 1, 2, SearchIndex,
+ output / "explorer" / "%" / "search.metapack", "",
+ output / "explorer" / "%" / "directory.metapack");
+
+ EXPECT_ACTION(plan, 7, 0, 1, Routes, output / "routes.bin", "Full",
+ output / "configuration.json");
+
+ EXPECT_TOTAL_FILES(
+ plan, entries, output / "configuration.json", output / "version.json",
+ output / "schemas" / "foo" / "%" / "schema.metapack",
+ output / "schemas" / "foo" / "%" / "dependencies.metapack",
+ output / "schemas" / "foo" / "%" / "locations.metapack",
+ output / "schemas" / "foo" / "%" / "positions.metapack",
+ output / "schemas" / "foo" / "%" / "stats.metapack",
+ output / "schemas" / "foo" / "%" / "bundle.metapack",
+ output / "schemas" / "foo" / "%" / "health.metapack",
+ output / "schemas" / "foo" / "%" / "blaze-exhaustive.metapack",
+ output / "schemas" / "foo" / "%" / "blaze-fast.metapack",
+ output / "schemas" / "foo" / "%" / "editor.metapack",
+ output / "explorer" / "foo" / "%" / "schema.metapack",
+ output / "explorer" / "foo" / "%" / "schema-html.metapack",
+ output / "explorer" / "%" / "search.metapack",
+ output / "explorer" / "%" / "directory.metapack",
+ output / "explorer" / "%" / "directory-html.metapack",
+ output / "explorer" / "%" / "404.metapack", output / "routes.bin");
+}