about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--kittybox-rs/build.rs32
-rw-r--r--kittybox-rs/companion-lite/main.js70
-rw-r--r--kittybox-rs/companion-lite/micropub_api.js43
-rw-r--r--kittybox-rs/companion-lite/src/main.ts85
-rw-r--r--kittybox-rs/companion-lite/src/micropub_api.ts58
-rw-r--r--kittybox-rs/companion-lite/tsconfig.json104
-rw-r--r--kittybox-rs/javascript/src/indieauth.ts15
-rw-r--r--kittybox-rs/javascript/src/lib.ts3
-rw-r--r--kittybox-rs/src/frontend/mod.rs13
-rw-r--r--kittybox-rs/src/lib.rs6
10 files changed, 301 insertions, 128 deletions
diff --git a/kittybox-rs/build.rs b/kittybox-rs/build.rs
index c9d6bfe..3d4c62b 100644
--- a/kittybox-rs/build.rs
+++ b/kittybox-rs/build.rs
@@ -2,18 +2,40 @@ fn main() {
     use std::env;
     let out_dir = env::var("OUT_DIR").unwrap();
     println!("cargo:rerun-if-changed=javascript/");
-    eprintln!("Out dir: {out_dir}");
 
-    let mut child = std::process::Command::new("tsc")
+    if let Ok(exit) = std::process::Command::new("tsc")
         .arg("--outDir")
-        .arg(out_dir)
+        .arg(std::path::Path::new(&out_dir).join("kittybox_js"))
         .current_dir("javascript")
         .spawn()
-        .unwrap();
+        .unwrap()
+        .wait()
+    {
+        if !exit.success() {
+            std::process::exit(exit.code().unwrap_or(1))
+        }
+    }
 
-    if let Ok(exit) = child.wait() {
+    println!("cargo:rerun-if-changed=companion-lite/");
+    let companion_out = std::path::Path::new(&out_dir).join("companion");
+
+    if let Ok(exit) = std::process::Command::new("tsc")
+        .arg("--outDir")
+        .arg(companion_out.as_os_str())
+        .current_dir("companion-lite")
+        .spawn()
+        .unwrap()
+        .wait()
+    {
         if !exit.success() {
             std::process::exit(exit.code().unwrap_or(1))
         }
     }
+    let companion_in = std::path::Path::new("companion-lite");
+    for file in ["index.html", "style.css"] {
+        std::fs::copy(
+            companion_in.join(file),
+            &companion_out.join(file)
+        ).unwrap();
+    }
 }
diff --git a/kittybox-rs/companion-lite/main.js b/kittybox-rs/companion-lite/main.js
deleted file mode 100644
index da7e6e1..0000000
--- a/kittybox-rs/companion-lite/main.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import { query_channels, submit } from "./micropub_api.js";
-
-function get_token() {
-    return form.elements.access_token.value
-}
-
-const form = document.getElementById("micropub");
-const channel_select_radio = document.getElementById("select_channels");
-
-channel_select_radio.onclick = async () => {
-    const channels = await query_channels(form.action, get_token())
-    if (channels !== undefined) {
-        populate_channel_list(channels)
-    }
-}
-
-const no_channel_radio = document.getElementById("no_channel");
-no_channel_radio.onclick = () => {
-    document.getElementById("channels").style.display = "none";
-    const channel_list = document.getElementById("channels_target")
-    channel_list.innerHTML = "";
-}
-
-function construct_body(form) {
-    return {
-        type: ["h-entry"],
-        properties: {
-            content: [form.elements.content.value],
-            name: form.elements.name.value ? [form.elements.name.value] : undefined,
-            category: form.elements.category.value ? form.elements.category.value.split(",").map(val => val.trim()) : undefined,
-            channel: form.elements.channel_select.value ?  Array.from(form.elements.channel).map(i => i.checked ? i.value : false).filter(i => i) : undefined
-        }
-    }
-}
-
-function populate_channel_list(channels) {
-    document.getElementById("channels").style.display = "block";
-    const channel_list = document.getElementById("channels_target")
-    channel_list.innerHTML = "";
-    channels.forEach((channel) => {
-        const template = document.getElementById("channel_selector").content.cloneNode(true)
-        const input = template.querySelector("input")
-        const label = template.querySelector("label")
-        input.id = `channel_selector_option_${channel.uid}`
-        input.value = channel.uid
-        label.for = input.id
-        label.innerHTML = `<a href="${channel.uid}">${channel.name}</a>`
-
-        channel_list.appendChild(template)
-    })
-}
-
-form.onsubmit = async (event) => {
-    event.preventDefault()
-    const mf2 = construct_body(form);
-    console.log(JSON.stringify(mf2));
-    try {
-        submit(form.action, get_token(), mf2)
-    } catch (e) {
-        // TODO show errors to user
-
-        return
-    }
-    form.clear()
-}
-
-document.getElementById("authorized").style.display = "block";
-// Local Variables:
-// js-indent-level: 4
-// End:
diff --git a/kittybox-rs/companion-lite/micropub_api.js b/kittybox-rs/companion-lite/micropub_api.js
deleted file mode 100644
index 402c075..0000000
--- a/kittybox-rs/companion-lite/micropub_api.js
+++ /dev/null
@@ -1,43 +0,0 @@
-export async function query_channels(endpoint, token) {
-    const response = await fetch(endpoint + "?q=config", {
-        headers: {
-            "Authorization": `Bearer ${get_token()}`
-        }
-    })
-
-    const config = await response.json();
-
-    return config["channels"]
-}
-
-export async function submit(endpoint, token, mf2) {
-    try {
-        const response = await fetch(endpoint, {
-            method: "POST",
-            headers: {
-                "Authorization": `Bearer ${token}`,
-                "Content-Type": "application/json"
-            },
-            body: JSON.stringify(mf2)
-        })
-
-
-        if (response.status != 201 || response.status != 202) {
-            let err = await response.json();
-            console.error("Micropub error!", err);
-            
-            return err;
-        } else {
-            return {
-                "location": response.headers.get("Location")
-            }
-        }
-    } catch (e) {
-        console.error("Network error!", e)
-        throw e
-    }
-}
-
-// Local Variables:
-// js-indent-level: 4
-// End:
diff --git a/kittybox-rs/companion-lite/src/main.ts b/kittybox-rs/companion-lite/src/main.ts
new file mode 100644
index 0000000..d593188
--- /dev/null
+++ b/kittybox-rs/companion-lite/src/main.ts
@@ -0,0 +1,85 @@
+import { query_channels, submit, MicropubChannel, MF2 } from "./micropub_api.js";
+
+function get_token() {
+  return (form.elements.namedItem("access_token") as HTMLInputElement).value
+}
+
+const form = document.getElementById("micropub") as HTMLFormElement;
+const channel_select_radio = document.getElementById("select_channels") as HTMLInputElement;
+
+channel_select_radio.onclick = async () => {
+  const channels = await query_channels(form.action, get_token())
+  if (channels !== undefined) {
+    populate_channel_list(channels)
+  }
+}
+
+const no_channel_radio = document.getElementById("no_channel") as HTMLInputElement;
+no_channel_radio.onclick = () => {
+  (document.getElementById("channels") as HTMLElement).style.display = "none";
+  const channel_list = document.getElementById("channels_target") as HTMLElement
+  channel_list.innerHTML = "";
+}
+
+function construct_body(form: HTMLFormElement): MF2 {
+  let content = (form.elements.namedItem("content") as HTMLInputElement).value;
+  let name: string | undefined = (form.elements.namedItem("name") as HTMLInputElement).value || undefined;
+  let category: string[] = (form.elements.namedItem("category") as HTMLInputElement).value
+    .split(",")
+    .map(val => val.trim());
+
+  let channel: string[] | undefined = undefined;
+  let channel_select = (form.elements.namedItem("channel_select") as HTMLInputElement).value;
+  if (channel_select) {
+    let channel_selector = form.elements.namedItem("channel");
+    if (channel_selector instanceof RadioNodeList) {
+      channel = (Array.from(channel_selector) as HTMLInputElement[])
+        .map(i => i.checked ? i.value : false)
+        .filter(i => i) as string[];
+    } else if (channel_selector instanceof HTMLInputElement) {
+      channel = [channel_selector.value]
+    }
+  }
+  return {
+    type: ["h-entry"],
+    properties: {
+      content: [content],
+      name: name ? [name] : undefined,
+      category: category.length ? category : undefined,
+      channel: channel ? channel : undefined
+    }
+  }
+}
+
+function populate_channel_list(channels: MicropubChannel[]) {
+  (document.getElementById("channels") as HTMLElement).style.display = "block";
+  const channel_list = document.getElementById("channels_target") as HTMLElement;
+  channel_list.innerHTML = "";
+  channels.forEach((channel) => {
+    const template = (document.getElementById("channel_selector") as HTMLTemplateElement).content.cloneNode(true) as HTMLElement;
+    const input = template.querySelector("input") as HTMLInputElement;
+    const label = template.querySelector("label") as HTMLLabelElement;
+    input.id = `channel_selector_option_${channel.uid}`
+    input.value = channel.uid
+    label.htmlFor = input.id
+    label.innerHTML = `<a href="${channel.uid}">${channel.name}</a>`
+
+    channel_list.appendChild(template)
+  })
+}
+
+form.onsubmit = async (event) => {
+  event.preventDefault()
+  const mf2 = construct_body(form);
+  console.log(JSON.stringify(mf2));
+  try {
+    submit(form.action, get_token(), mf2)
+  } catch (e) {
+    // TODO show errors to user
+
+    return
+  }
+  form.clear()
+}
+
+(document.getElementById("authorized") as HTMLElement).style.display = "block";
diff --git a/kittybox-rs/companion-lite/src/micropub_api.ts b/kittybox-rs/companion-lite/src/micropub_api.ts
new file mode 100644
index 0000000..9eb65a2
--- /dev/null
+++ b/kittybox-rs/companion-lite/src/micropub_api.ts
@@ -0,0 +1,58 @@
+export interface MicropubChannel {
+  uid: string,
+  name: string
+}
+
+export interface MF2 {
+  type: string[],
+  properties: { [key:string]: (string | MF2 | {[key:string]: string})[] | undefined }
+}
+
+export interface MicropubConfig {
+  channels: MicropubChannel[],
+  "media-endpoint": string
+}
+
+export async function query_channels(endpoint: string, token: string): Promise<MicropubChannel[]> {
+  const response = await fetch(endpoint + "?q=config", {
+    headers: {
+      "Authorization": `Bearer ${token}`
+    }
+  })
+
+  if (response.ok) {
+    const config = await response.json() as MicropubConfig;
+
+    return config["channels"]
+  } else {
+    throw new Error(`Micropub endpoint returned ${response.status}: ${await response.json()}`)
+  }
+  
+}
+
+export async function submit(endpoint: string, token: string, mf2: MF2) {
+  try {
+    const response = await fetch(endpoint, {
+      method: "POST",
+      headers: {
+        "Authorization": `Bearer ${token}`,
+        "Content-Type": "application/json"
+      },
+      body: JSON.stringify(mf2)
+    })
+
+    if (response.status != 201 && response.status != 202) {
+      let err = await response.json();
+      console.error("Micropub error!", err);
+
+      return err;
+    } else {
+      return {
+        "location": response.headers.get("Location")
+      }
+    }
+  } catch (e) {
+    console.error("Network error!", e)
+    throw e
+  }
+}
diff --git a/kittybox-rs/companion-lite/tsconfig.json b/kittybox-rs/companion-lite/tsconfig.json
new file mode 100644
index 0000000..18b94c7
--- /dev/null
+++ b/kittybox-rs/companion-lite/tsconfig.json
@@ -0,0 +1,104 @@
+{
+  "compilerOptions": {
+    /* Visit https://aka.ms/tsconfig to read more about this file */
+
+    /* Projects */
+    // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
+    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
+    // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
+    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
+    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
+    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
+
+    /* Language and Environment */
+    "target": "es2022",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
+    // "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
+    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
+    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
+    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+    // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
+    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
+    // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
+
+    /* Modules */
+    "module": "es2022",                                     /* Specify what module code is generated. */
+    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
+    // "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
+    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
+    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
+    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
+    // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
+    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
+    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
+    // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
+    // "resolveJsonModule": true,                        /* Enable importing .json files. */
+    // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
+
+    /* JavaScript Support */
+    // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
+    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+
+    /* Emit */
+    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
+    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
+    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
+    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+    "outDir": "./dist",                                  /* Specify an output folder for all emitted files. */
+    // "removeComments": true,                           /* Disable emitting comments. */
+    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
+    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */
+    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
+    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
+    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
+    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
+    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
+    // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
+    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
+    // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
+    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
+    // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
+
+    /* Interop Constraints */
+    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
+    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
+    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
+    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
+
+    /* Type Checking */
+    "strict": true,                                      /* Enable all strict type-checking options. */
+    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+    // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
+    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+    // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
+    // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
+    // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
+    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
+    // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
+    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
+    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
+    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
+    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
+    // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
+    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
+    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
+    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
+    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
+
+    /* Completeness */
+    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
+    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
+  },
+  "include": ["src/**/*"]
+}
diff --git a/kittybox-rs/javascript/src/indieauth.ts b/kittybox-rs/javascript/src/indieauth.ts
index ef314e9..01732b7 100644
--- a/kittybox-rs/javascript/src/indieauth.ts
+++ b/kittybox-rs/javascript/src/indieauth.ts
@@ -1,3 +1,5 @@
+import { unreachable } from "./lib.js";
+
 const WEBAUTHN_TIMEOUT = 60 * 1000;
 
 interface KittyboxWebauthnPreRegistrationData {
@@ -69,16 +71,19 @@ export async function submit_handler(e: SubmitEvent) {
     const form = e.target as HTMLFormElement;
 
     let scopes: Array<string>;
-    if (form.elements.namedItem("scope") === undefined) {
+    let scope_elem = form.elements.namedItem("scope");
+    if (scope_elem == null) {
       scopes = []
-    } else if (form.elements.namedItem("scope") instanceof Node) {
-      scopes = ([form.elements.namedItem("scope")] as Array<HTMLInputElement>)
+    } else if (scope_elem instanceof Element) {
+      scopes = ([scope_elem] as Array<HTMLInputElement>)
         .filter((e: HTMLInputElement) => e.checked)
         .map((e: HTMLInputElement) => e.value);
-    } else {
-      scopes = (Array.from(form.elements.namedItem("scope") as RadioNodeList) as Array<HTMLInputElement>)
+    } else if (scope_elem instanceof RadioNodeList) {
+      scopes = (Array.from(scope_elem) as Array<HTMLInputElement>)
         .filter((e: HTMLInputElement) => e.checked)
         .map((e: HTMLInputElement) => e.value);
+    } else {
+      unreachable("HTMLFormControlsCollection returned something that's not null, Element or RadioNodeList")
     }
 
     const authorization_request = {
diff --git a/kittybox-rs/javascript/src/lib.ts b/kittybox-rs/javascript/src/lib.ts
new file mode 100644
index 0000000..38ba65b
--- /dev/null
+++ b/kittybox-rs/javascript/src/lib.ts
@@ -0,0 +1,3 @@
+export function unreachable(msg: string): never {
+  throw new Error(msg);
+}
diff --git a/kittybox-rs/src/frontend/mod.rs b/kittybox-rs/src/frontend/mod.rs
index 43a2824..6b4bdae 100644
--- a/kittybox-rs/src/frontend/mod.rs
+++ b/kittybox-rs/src/frontend/mod.rs
@@ -268,9 +268,18 @@ pub async fn catchall<D: Storage>(
 }
 
 const STYLE_CSS: &[u8] = include_bytes!("./style.css");
-const ONBOARDING_JS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/onboarding.js"));
+// XXX const path handling is ugly, and concat!() doesn't take
+// constants, only literals... how annoying!
+//
+// This might break compiling on inferior operating systems that use
+// backslashes as their path separator
+const ONBOARDING_JS: &[u8] = include_bytes!(concat!(
+    env!("OUT_DIR"), "/", "kittybox_js", "/", "onboarding.js"
+));
 const ONBOARDING_CSS: &[u8] = include_bytes!("./onboarding.css");
-const INDIEAUTH_JS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/indieauth.js"));
+const INDIEAUTH_JS: &[u8] = include_bytes!(concat!(
+    env!("OUT_DIR"), "/", "kittybox_js", "/", "indieauth.js"
+));
 
 const MIME_JS: &str = "application/javascript";
 const MIME_CSS: &str = "text/css";
diff --git a/kittybox-rs/src/lib.rs b/kittybox-rs/src/lib.rs
index 7084601..97bb608 100644
--- a/kittybox-rs/src/lib.rs
+++ b/kittybox-rs/src/lib.rs
@@ -55,13 +55,13 @@ pub mod companion {
             let mut map = HashMap::new();
 
             macro_rules! register_resource {
-                ($map:ident, $prefix:literal, ($filename:literal, $mime:literal)) => {{
+                ($map:ident, $prefix:expr, ($filename:literal, $mime:literal)) => {{
                     $map.insert($filename, Resource {
                         data: include_bytes!(concat!($prefix, $filename)),
                         mime: $mime
                     })
                 }};
-                ($map:ident, $prefix:literal, ($filename:literal, $mime:literal), $( ($f:literal, $m:literal) ),+) => {{
+                ($map:ident, $prefix:expr, ($filename:literal, $mime:literal), $( ($f:literal, $m:literal) ),+) => {{
                     register_resource!($map, $prefix, ($filename, $mime));
                     register_resource!($map, $prefix, $(($f, $m)),+);
                 }};
@@ -69,7 +69,7 @@ pub mod companion {
 
             register_resource! {
                 map,
-                "../companion-lite/",
+                concat!(env!("OUT_DIR"), "/", "companion", "/"),
                 ("index.html", "text/html; charset=\"utf-8\""),
                 ("main.js", "text/javascript"),
                 ("micropub_api.js", "text/javascript"),