Improved the static assets definitions in the Website service (#18)
This PR contains the work done to overhaul the static asset definitions for the Website service.
To provide further details about the work:
* Website library
* Overhauled the `StateFile` enumeration to reduce the number of cases to one case per logical file name, each exposing a `fileExtensions` list.
* Wired everything into the pages with a consistent ordering convention: stylesheets load shared-first so the page sheet wins the CSS cascade; scripts load page-first with shared.js last. The error page also gained the shared stylesheet and its scripts; the index page gained its page CSS/JS and the new touch icon link.
* Used the `StaticFile` enumeration as a single source of truth for every _href_/_src_ in the `IndexPage` and the `ErrorPage` pages, eliminating hardcoded asset paths.
* Website service
* Added new assets to the Resources folder:
* `apple-touch-icon.png`
* `css/index.css`
* `js/index.js`
* `js/error.js`
* `sitemap.xml`
* Renamed existing assets within the Resources folder:
* `css/style.css` → `css/shared.css`
* `js/app.js` → `js/shared.js`
* Fixed the working-directory location for the scheme in the Xcode project.
Reviewed-on: rock-n-code/loud-amsterdam#18
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
@@ -50,7 +50,8 @@
|
|||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "YES"
|
||||||
|
customWorkingDirectory = "/Users/logan/Documents/Development/Platforms/Röck+Cöde/Loud/Services/Website"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
@@ -3,3 +3,5 @@
|
|||||||
# Allow crawling of all content
|
# Allow crawling of all content
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow:
|
Disallow:
|
||||||
|
|
||||||
|
Sitemap: https://loud.amsterdam/sitemap.xml
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://loud.amsterdam/</loc>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
@@ -1,24 +1,27 @@
|
|||||||
/// A static file shipped with the website service.
|
/// A static file shipped with the website service.
|
||||||
///
|
///
|
||||||
/// Each case identifies a file stored under the static files root (the `Resources/Static`
|
/// Each case identifies a file name stored under the static files root (the `Resources/Static`
|
||||||
/// directory) and served by Hummingbird's `FileMiddleware` middleware.
|
/// directory) and served by Hummingbird's `FileMiddleware` middleware. A name can be available
|
||||||
|
/// with more than one extension (see ``fileExtensions``), each resolving to its own file.
|
||||||
enum StaticFile: CaseIterable, Sendable {
|
enum StaticFile: CaseIterable, Sendable {
|
||||||
/// The `js/app.js` script.
|
/// The `apple-touch-icon.png` icon.
|
||||||
case appJS
|
case appleTouchIcon
|
||||||
/// The `css/error.css` stylesheet for the not-found page.
|
/// The `css/error.css` stylesheet and `js/error.js` script for the not-found page.
|
||||||
case errorCSS
|
case error
|
||||||
/// The `favicon.ico` icon.
|
/// The `favicon.ico` icon.
|
||||||
case faviconICO
|
case favicon
|
||||||
/// The `icon.png` icon.
|
/// The `icon.png` and `icon.svg` icons.
|
||||||
case iconPNG
|
case icon
|
||||||
/// The `icon.svg` icon.
|
/// The `css/index.css` stylesheet and `js/index.js` script for the landing page.
|
||||||
case iconSVG
|
case index
|
||||||
/// The `robots.txt` crawler directives.
|
/// The `robots.txt` crawler directives.
|
||||||
case robotsTXT
|
case robots
|
||||||
|
/// The `css/shared.css` stylesheet and `js/shared.js` script shared across pages.
|
||||||
|
case shared
|
||||||
/// The `site.webmanifest` web application manifest.
|
/// The `site.webmanifest` web application manifest.
|
||||||
case siteWebmanifest
|
case site
|
||||||
/// The `css/style.css` stylesheet.
|
/// The `sitemap.xml` crawler sitemap.
|
||||||
case styleCSS
|
case sitemap
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Enumerations
|
// MARK: - Enumerations
|
||||||
@@ -40,6 +43,8 @@ extension StaticFile {
|
|||||||
case txt
|
case txt
|
||||||
/// A web application manifest file.
|
/// A web application manifest file.
|
||||||
case webmanifest
|
case webmanifest
|
||||||
|
/// An Extensible Markup Language file.
|
||||||
|
case xml
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,9 +54,91 @@ extension StaticFile {
|
|||||||
|
|
||||||
// MARK: Computed
|
// MARK: Computed
|
||||||
|
|
||||||
|
/// The file extensions the file is available with.
|
||||||
|
var fileExtensions: [Extension] {
|
||||||
|
switch self {
|
||||||
|
case .appleTouchIcon: [.png]
|
||||||
|
case .error,
|
||||||
|
.index,
|
||||||
|
.shared: [.css, .js]
|
||||||
|
case .favicon: [.ico]
|
||||||
|
case .icon: [.png, .svg]
|
||||||
|
case .robots: [.txt]
|
||||||
|
case .site: [.webmanifest]
|
||||||
|
case .sitemap: [.xml]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The file's name, without extension.
|
||||||
|
var fileName: String {
|
||||||
|
switch self {
|
||||||
|
case .appleTouchIcon: "apple-touch-icon"
|
||||||
|
case .error: "error"
|
||||||
|
case .favicon: "favicon"
|
||||||
|
case .icon: "icon"
|
||||||
|
case .index: "index"
|
||||||
|
case .robots: "robots"
|
||||||
|
case .shared: "shared"
|
||||||
|
case .site: "site"
|
||||||
|
case .sitemap: "sitemap"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Methods
|
||||||
|
|
||||||
|
/// Resolves the file's path against the given base directory.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - basePath: the directory the static files are served from.
|
||||||
|
/// - fileExtension: the extension of the file to resolve.
|
||||||
|
/// - Returns: the path to the file, relative to the `basePath` path.
|
||||||
|
func path(
|
||||||
|
relativeTo basePath: String,
|
||||||
|
for fileExtension: Extension
|
||||||
|
) -> String {
|
||||||
|
let relativePath = relativePath(for: fileExtension)
|
||||||
|
|
||||||
|
guard !basePath.isEmpty else {
|
||||||
|
return relativePath
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(basePath)/\(relativePath)"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the file's path relative to the static files root (e.g. `"css/shared.css"`).
|
||||||
|
///
|
||||||
|
/// This also matches the URL path the file is served at by `FileMiddleware`.
|
||||||
|
///
|
||||||
|
/// - Parameter fileExtension: the extension of the file to resolve.
|
||||||
|
/// - Returns: the path to the file, relative to the static files root.
|
||||||
|
func relativePath(
|
||||||
|
for fileExtension: Extension
|
||||||
|
) -> String {
|
||||||
|
let file = "\(fileName).\(fileExtension.rawValue)"
|
||||||
|
|
||||||
|
return fileExtension.subdirectory
|
||||||
|
.map { "\($0)/\(file)" } ?? file
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the absolute URL path the file is served at (e.g. `"/css/shared.css"`).
|
||||||
|
///
|
||||||
|
/// - Parameter fileExtension: the extension of the file to resolve.
|
||||||
|
/// - Returns: the path to use in `href` and `src` attributes.
|
||||||
|
func urlPath(
|
||||||
|
for fileExtension: Extension
|
||||||
|
) -> String {
|
||||||
|
"/\(relativePath(for: fileExtension))"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StaticFile.Extension {
|
||||||
|
|
||||||
|
// MARK: Computed
|
||||||
|
|
||||||
/// The file's content type.
|
/// The file's content type.
|
||||||
var contentType: String {
|
var contentType: String {
|
||||||
switch fileExtension {
|
switch self {
|
||||||
case .css: "text/css"
|
case .css: "text/css"
|
||||||
case .js: "text/javascript"
|
case .js: "text/javascript"
|
||||||
case .png: "image/png"
|
case .png: "image/png"
|
||||||
@@ -59,77 +146,15 @@ extension StaticFile {
|
|||||||
case .svg: "image/svg+xml"
|
case .svg: "image/svg+xml"
|
||||||
case .txt: "text/plain"
|
case .txt: "text/plain"
|
||||||
case .webmanifest: "application/manifest+json"
|
case .webmanifest: "application/manifest+json"
|
||||||
|
case .xml: "application/xml"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The file's extension.
|
/// The sub-directory within the static root that holds files with this extension, if any.
|
||||||
var fileExtension: Extension {
|
|
||||||
switch self {
|
|
||||||
case .errorCSS,
|
|
||||||
.styleCSS: .css
|
|
||||||
case .appJS: .js
|
|
||||||
case .faviconICO: .ico
|
|
||||||
case .iconPNG: .png
|
|
||||||
case .iconSVG: .svg
|
|
||||||
case .robotsTXT: .txt
|
|
||||||
case .siteWebmanifest: .webmanifest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The file's name, without extension.
|
|
||||||
var fileName: String {
|
|
||||||
switch self {
|
|
||||||
case .appJS: "app"
|
|
||||||
case .errorCSS: "error"
|
|
||||||
case .faviconICO: "favicon"
|
|
||||||
case .iconPNG,
|
|
||||||
.iconSVG: "icon"
|
|
||||||
case .robotsTXT: "robots"
|
|
||||||
case .siteWebmanifest: "site"
|
|
||||||
case .styleCSS: "style"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The path relative to the static files root (e.g. `"css/style.css"`).
|
|
||||||
///
|
|
||||||
/// This also matches the URL path the file is served at by `FileMiddleware`.
|
|
||||||
var relativePath: String {
|
|
||||||
let file = "\(fileName).\(fileExtension.rawValue)"
|
|
||||||
|
|
||||||
return subdirectory
|
|
||||||
.map { "\($0)/\(file)" } ?? file
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Methods
|
|
||||||
|
|
||||||
/// Resolves the file's path against the given base directory.
|
|
||||||
///
|
|
||||||
/// - Parameter basePath: the directory the static files are served from.
|
|
||||||
/// - Returns: the path to the file, relative to the `basePath` path.
|
|
||||||
func path(
|
|
||||||
relativeTo basePath: String
|
|
||||||
) -> String {
|
|
||||||
guard !basePath.isEmpty else {
|
|
||||||
return relativePath
|
|
||||||
}
|
|
||||||
|
|
||||||
return "\(basePath)/\(relativePath)"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helpers
|
|
||||||
|
|
||||||
private extension StaticFile {
|
|
||||||
|
|
||||||
// MARK: Computed
|
|
||||||
|
|
||||||
/// The sub-directory within the static root that holds the file, if any.
|
|
||||||
var subdirectory: String? {
|
var subdirectory: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .appJS: "js"
|
case .css: "css"
|
||||||
case .errorCSS,
|
case .js: "js"
|
||||||
.styleCSS: "css"
|
|
||||||
default: nil
|
default: nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ struct ErrorPage: HTMLDocument, Sendable {
|
|||||||
|
|
||||||
// MARK: Document
|
// MARK: Document
|
||||||
|
|
||||||
/// The page's content: a localized heading and explanatory message.
|
/// The page's content: a localized heading and explanatory message, followed by the error and shared scripts.
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
h1 {
|
h1 {
|
||||||
localize("error.heading", locale: locale)
|
localize("error.heading", locale: locale)
|
||||||
@@ -34,9 +34,11 @@ struct ErrorPage: HTMLDocument, Sendable {
|
|||||||
p {
|
p {
|
||||||
localize("error.message", locale: locale)
|
localize("error.message", locale: locale)
|
||||||
}
|
}
|
||||||
|
script(.src(StaticFile.error.urlPath(for: .js))) {}
|
||||||
|
script(.src(StaticFile.shared.urlPath(for: .js))) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The metadata and stylesheet link placed in the document head.
|
/// The metadata and stylesheet links placed in the document head.
|
||||||
var head: some HTML {
|
var head: some HTML {
|
||||||
meta(.charset(.utf8))
|
meta(.charset(.utf8))
|
||||||
meta(
|
meta(
|
||||||
@@ -45,7 +47,11 @@ struct ErrorPage: HTMLDocument, Sendable {
|
|||||||
)
|
)
|
||||||
link(
|
link(
|
||||||
.rel(.stylesheet),
|
.rel(.stylesheet),
|
||||||
.href("/css/error.css")
|
.href(StaticFile.shared.urlPath(for: .css))
|
||||||
|
)
|
||||||
|
link(
|
||||||
|
.rel(.stylesheet),
|
||||||
|
.href(StaticFile.error.urlPath(for: .css))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,13 @@ struct IndexPage: HTMLDocument, Sendable {
|
|||||||
|
|
||||||
// MARK: Document
|
// MARK: Document
|
||||||
|
|
||||||
/// The page's content: a localized greeting followed by the app script.
|
/// The page's content: a localized greeting followed by the shared and index scripts.
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
p {
|
p {
|
||||||
localize("index.greeting", locale: locale)
|
localize("index.greeting", locale: locale)
|
||||||
}
|
}
|
||||||
script(.src("/js/app.js")) {}
|
script(.src(StaticFile.index.urlPath(for: .js))) {}
|
||||||
|
script(.src(StaticFile.shared.urlPath(for: .js))) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The metadata, stylesheet, icon, and manifest links placed in the document head.
|
/// The metadata, stylesheet, icon, and manifest links placed in the document head.
|
||||||
@@ -43,11 +44,15 @@ struct IndexPage: HTMLDocument, Sendable {
|
|||||||
)
|
)
|
||||||
link(
|
link(
|
||||||
.rel(.stylesheet),
|
.rel(.stylesheet),
|
||||||
.href("/css/style.css")
|
.href(StaticFile.shared.urlPath(for: .css))
|
||||||
|
)
|
||||||
|
link(
|
||||||
|
.rel(.stylesheet),
|
||||||
|
.href(StaticFile.index.urlPath(for: .css))
|
||||||
)
|
)
|
||||||
link(
|
link(
|
||||||
.rel(.icon),
|
.rel(.icon),
|
||||||
.href("/favicon.ico"),
|
.href(StaticFile.favicon.urlPath(for: .ico)),
|
||||||
.custom(
|
.custom(
|
||||||
name: "sizes",
|
name: "sizes",
|
||||||
value: "any"
|
value: "any"
|
||||||
@@ -55,7 +60,7 @@ struct IndexPage: HTMLDocument, Sendable {
|
|||||||
)
|
)
|
||||||
link(
|
link(
|
||||||
.rel(.icon),
|
.rel(.icon),
|
||||||
.href("/icon.svg"),
|
.href(StaticFile.icon.urlPath(for: .svg)),
|
||||||
.custom(
|
.custom(
|
||||||
name: "type",
|
name: "type",
|
||||||
value: "image/svg+xml"
|
value: "image/svg+xml"
|
||||||
@@ -63,11 +68,11 @@ struct IndexPage: HTMLDocument, Sendable {
|
|||||||
)
|
)
|
||||||
link(
|
link(
|
||||||
.rel("apple-touch-icon"),
|
.rel("apple-touch-icon"),
|
||||||
.href("/icon.png")
|
.href(StaticFile.appleTouchIcon.urlPath(for: .png))
|
||||||
)
|
)
|
||||||
link(
|
link(
|
||||||
.rel("manifest"),
|
.rel("manifest"),
|
||||||
.href("/site.webmanifest")
|
.href(StaticFile.site.urlPath(for: .webmanifest))
|
||||||
)
|
)
|
||||||
meta(
|
meta(
|
||||||
.name("theme-color"),
|
.name("theme-color"),
|
||||||
|
|||||||
@@ -94,20 +94,22 @@ struct AppTests {
|
|||||||
try await app(
|
try await app(
|
||||||
staticFilesPath: staticFilesPath
|
staticFilesPath: staticFilesPath
|
||||||
).test(.router) { client in
|
).test(.router) { client in
|
||||||
try await client.execute(
|
for fileExtension in file.fileExtensions {
|
||||||
uri: "/\(file.relativePath)",
|
try await client.execute(
|
||||||
method: .get
|
uri: "/\(file.relativePath(for: fileExtension))",
|
||||||
) { response in
|
method: .get
|
||||||
#expect(response.status == .ok)
|
) { response in
|
||||||
#expect(response.headers[.contentType] == file.contentType)
|
#expect(response.status == .ok)
|
||||||
|
#expect(response.headers[.contentType] == fileExtension.contentType)
|
||||||
let cacheControl = try #require(response.headers[.cacheControl])
|
|
||||||
|
|
||||||
#expect(cacheControl.contains("public") == true)
|
let cacheControl = try #require(response.headers[.cacheControl])
|
||||||
#expect(cacheControl.contains("max-age=") == true)
|
|
||||||
|
#expect(cacheControl.contains("public") == true)
|
||||||
if textExtensions.contains(file.fileExtension) {
|
#expect(cacheControl.contains("max-age=") == true)
|
||||||
#expect(cacheControl.contains("must-revalidate") == true)
|
|
||||||
|
if textExtensions.contains(fileExtension) {
|
||||||
|
#expect(cacheControl.contains("must-revalidate") == true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,36 +5,25 @@ import Testing
|
|||||||
|
|
||||||
@Suite("StaticFile enumeration")
|
@Suite("StaticFile enumeration")
|
||||||
struct StaticFileTests {
|
struct StaticFileTests {
|
||||||
|
|
||||||
// MARK: Type aliases
|
// MARK: Type aliases
|
||||||
|
|
||||||
typealias File = StaticFile
|
typealias File = StaticFile
|
||||||
typealias FileExtension = StaticFile.Extension
|
typealias FileExtension = StaticFile.Extension
|
||||||
|
|
||||||
// MARK: Computed tests
|
// MARK: Computed tests
|
||||||
|
|
||||||
@Test(arguments: zip(
|
|
||||||
File.allCases,
|
|
||||||
Self.contentTypes
|
|
||||||
))
|
|
||||||
func `content type`(
|
|
||||||
for file: File,
|
|
||||||
expects contentType: String
|
|
||||||
) {
|
|
||||||
#expect(file.contentType == contentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(arguments: zip(
|
@Test(arguments: zip(
|
||||||
File.allCases,
|
File.allCases,
|
||||||
Self.fileExtensions
|
Self.fileExtensions
|
||||||
))
|
))
|
||||||
func `file extension`(
|
func `file extensions`(
|
||||||
for file: File,
|
for file: File,
|
||||||
expects `extension`: FileExtension
|
expects extensions: [FileExtension]
|
||||||
) {
|
) {
|
||||||
#expect(file.fileExtension == `extension`)
|
#expect(file.fileExtensions == extensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(arguments: zip(
|
@Test(arguments: zip(
|
||||||
File.allCases,
|
File.allCases,
|
||||||
Self.fileNames
|
Self.fileNames
|
||||||
@@ -45,20 +34,57 @@ struct StaticFileTests {
|
|||||||
) {
|
) {
|
||||||
#expect(file.fileName == fileName)
|
#expect(file.fileName == fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(arguments: zip(
|
||||||
|
Self.extensions,
|
||||||
|
Self.contentTypes
|
||||||
|
))
|
||||||
|
func `content type`(
|
||||||
|
for fileExtension: FileExtension,
|
||||||
|
expects contentType: String
|
||||||
|
) {
|
||||||
|
#expect(fileExtension.contentType == contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: zip(
|
||||||
|
Self.extensions,
|
||||||
|
Self.subdirectories
|
||||||
|
))
|
||||||
|
func `subdirectory`(
|
||||||
|
for fileExtension: FileExtension,
|
||||||
|
expects subdirectory: String?
|
||||||
|
) {
|
||||||
|
#expect(fileExtension.subdirectory == subdirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Method tests
|
||||||
|
|
||||||
@Test(arguments: zip(
|
@Test(arguments: zip(
|
||||||
File.allCases,
|
File.allCases,
|
||||||
Self.relativePaths
|
Self.relativePaths
|
||||||
))
|
))
|
||||||
func `relative path`(
|
func `relative path for`(
|
||||||
for file: File,
|
for file: File,
|
||||||
expects relativePath: String
|
expects relativePaths: [String]
|
||||||
) {
|
) {
|
||||||
#expect(file.relativePath == relativePath)
|
for (fileExtension, relativePath) in zip(file.fileExtensions, relativePaths) {
|
||||||
|
#expect(file.relativePath(for: fileExtension) == relativePath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Method tests
|
@Test(arguments: zip(
|
||||||
|
File.allCases,
|
||||||
|
Self.relativePaths
|
||||||
|
))
|
||||||
|
func `url path for`(
|
||||||
|
for file: File,
|
||||||
|
expects relativePaths: [String]
|
||||||
|
) {
|
||||||
|
for (fileExtension, relativePath) in zip(file.fileExtensions, relativePaths) {
|
||||||
|
#expect(file.urlPath(for: fileExtension) == "/\(relativePath)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test(arguments: [
|
@Test(arguments: [
|
||||||
"",
|
"",
|
||||||
".",
|
".",
|
||||||
@@ -68,70 +94,99 @@ struct StaticFileTests {
|
|||||||
_ basePath: String
|
_ basePath: String
|
||||||
) {
|
) {
|
||||||
for file in File.allCases {
|
for file in File.allCases {
|
||||||
let pathRelativeToBasePath = file.path(relativeTo: basePath)
|
for fileExtension in file.fileExtensions {
|
||||||
|
let pathRelativeToBasePath = file.path(
|
||||||
|
relativeTo: basePath,
|
||||||
|
for: fileExtension
|
||||||
|
)
|
||||||
|
let relativePath = file.relativePath(for: fileExtension)
|
||||||
|
|
||||||
if basePath.isEmpty {
|
if basePath.isEmpty {
|
||||||
#expect(pathRelativeToBasePath == file.relativePath)
|
#expect(pathRelativeToBasePath == relativePath)
|
||||||
} else {
|
} else {
|
||||||
#expect(pathRelativeToBasePath == "\(basePath)/\(file.relativePath)")
|
#expect(pathRelativeToBasePath == "\(basePath)/\(relativePath)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: CaseIterable tests
|
// MARK: CaseIterable tests
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func `all cases`() {
|
func `all cases`() {
|
||||||
#expect(File.allCases.count == 8)
|
#expect(File.allCases.count == 9)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
private extension StaticFileTests {
|
private extension StaticFileTests {
|
||||||
|
|
||||||
// MARK: Constants
|
// MARK: Constants
|
||||||
|
|
||||||
static let contentTypes: [String] = [
|
static let extensions: [FileExtension] = [
|
||||||
"text/javascript",
|
|
||||||
"text/css",
|
|
||||||
"image/vnd.microsoft.icon",
|
|
||||||
"image/png",
|
|
||||||
"image/svg+xml",
|
|
||||||
"text/plain",
|
|
||||||
"application/manifest+json",
|
|
||||||
"text/css"
|
|
||||||
]
|
|
||||||
static let fileExtensions: [FileExtension] = [
|
|
||||||
.js,
|
|
||||||
.css,
|
.css,
|
||||||
.ico,
|
.js,
|
||||||
.png,
|
.png,
|
||||||
|
.ico,
|
||||||
.svg,
|
.svg,
|
||||||
.txt,
|
.txt,
|
||||||
.webmanifest,
|
.webmanifest,
|
||||||
.css
|
.xml
|
||||||
|
]
|
||||||
|
static let contentTypes: [String] = [
|
||||||
|
"text/css",
|
||||||
|
"text/javascript",
|
||||||
|
"image/png",
|
||||||
|
"image/vnd.microsoft.icon",
|
||||||
|
"image/svg+xml",
|
||||||
|
"text/plain",
|
||||||
|
"application/manifest+json",
|
||||||
|
"application/xml"
|
||||||
|
]
|
||||||
|
static let subdirectories: [String?] = [
|
||||||
|
"css",
|
||||||
|
"js",
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil
|
||||||
|
]
|
||||||
|
static let fileExtensions: [[FileExtension]] = [
|
||||||
|
[.png],
|
||||||
|
[.css, .js],
|
||||||
|
[.ico],
|
||||||
|
[.png, .svg],
|
||||||
|
[.css, .js],
|
||||||
|
[.txt],
|
||||||
|
[.css, .js],
|
||||||
|
[.webmanifest],
|
||||||
|
[.xml]
|
||||||
]
|
]
|
||||||
static let fileNames: [String] = [
|
static let fileNames: [String] = [
|
||||||
"app",
|
"apple-touch-icon",
|
||||||
"error",
|
"error",
|
||||||
"favicon",
|
"favicon",
|
||||||
"icon",
|
"icon",
|
||||||
"icon",
|
"index",
|
||||||
"robots",
|
"robots",
|
||||||
|
"shared",
|
||||||
"site",
|
"site",
|
||||||
"style"
|
"sitemap"
|
||||||
]
|
]
|
||||||
static let relativePaths: [String] = [
|
static let relativePaths: [[String]] = [
|
||||||
"js/app.js",
|
["apple-touch-icon.png"],
|
||||||
"css/error.css",
|
["css/error.css", "js/error.js"],
|
||||||
"favicon.ico",
|
["favicon.ico"],
|
||||||
"icon.png",
|
["icon.png", "icon.svg"],
|
||||||
"icon.svg",
|
["css/index.css", "js/index.js"],
|
||||||
"robots.txt",
|
["robots.txt"],
|
||||||
"site.webmanifest",
|
["css/shared.css", "js/shared.js"],
|
||||||
"css/style.css"
|
["site.webmanifest"],
|
||||||
|
["sitemap.xml"]
|
||||||
]
|
]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ struct ErrorPageTests {
|
|||||||
#expect(html.contains(#"lang="en""#))
|
#expect(html.contains(#"lang="en""#))
|
||||||
#expect(html.contains("Page Not Found"))
|
#expect(html.contains("Page Not Found"))
|
||||||
#expect(html.contains("Sorry, but the page you were trying to view does not exist."))
|
#expect(html.contains("Sorry, but the page you were trying to view does not exist."))
|
||||||
|
#expect(html.contains("/css/shared.css"))
|
||||||
#expect(html.contains("/css/error.css"))
|
#expect(html.contains("/css/error.css"))
|
||||||
|
#expect(html.contains("/js/error.js"))
|
||||||
|
#expect(html.contains("/js/shared.js"))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,15 @@ struct IndexPageTests {
|
|||||||
|
|
||||||
#expect(html.contains("<!DOCTYPE html>"))
|
#expect(html.contains("<!DOCTYPE html>"))
|
||||||
#expect(html.contains(#"lang="en""#))
|
#expect(html.contains(#"lang="en""#))
|
||||||
#expect(html.contains("/css/style.css"))
|
#expect(html.contains("/css/shared.css"))
|
||||||
|
#expect(html.contains("/css/index.css"))
|
||||||
#expect(html.contains("/favicon.ico"))
|
#expect(html.contains("/favicon.ico"))
|
||||||
#expect(html.contains("/icon.svg"))
|
#expect(html.contains("/icon.svg"))
|
||||||
#expect(html.contains("/icon.png"))
|
#expect(html.contains("/apple-touch-icon.png"))
|
||||||
#expect(html.contains("/site.webmanifest"))
|
#expect(html.contains("/site.webmanifest"))
|
||||||
#expect(html.contains("Hello world!"))
|
#expect(html.contains("Hello world!"))
|
||||||
#expect(html.contains("/js/app.js"))
|
#expect(html.contains("/js/shared.js"))
|
||||||
|
#expect(html.contains("/js/index.js"))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user