Plugin: CodeScript Toolkit

Hello!

New user migrating from a mix of Js-Engine, templater hotkeys, and commander, here. This is really cool and has the potential to be the main driving engine behind automation in Obsidian!

There is one concern: do require function calls require a completely immediately declared string literal, eg:

ONLY this is allowed


const myImportedFunction = require('path/to/file');

and NOT a variable that points to a string literal:

//CONSTANTS.js
const FunctionFilePath = "path/to/file";

//main.ks
const CONSTANTS = require("path/to/CONSTANTS.js");
const myImportedFunction = require(CONSTANTS.FunctionFilePath);

I ask because my system is complex, with multiple centralized dependencies (utils folder), and I’m still in the process of finishing organizing my files, so dependencies will be moved around, breaking the files (features) that depend on the dependencies (utils).

I’m not an experienced javascript developer, so I’m not really sure if there was an original intended method for solving this with node.js. I didn’t see anything on the obsidian-codescript-tookit for this (obsidian-codescript-toolkit/docs/core-functions.md at main · mnaoumov/obsidian-codescript-toolkit · GitHub ; obsidian-codescript-toolkit/docs/dynamic-import.md at main · mnaoumov/obsidian-codescript-toolkit · GitHub ) and was wondering if there was a method to doing variable imports.

Doing something naively results in the warning “Could not statically analyze require call
require(basepathjs + ‘utils/IO/insert_text_under_header_in_note.js’)” (where the filepath is a constructed string). This isn’t the exact same case (being a interpreted string instead of just an imediate constant) but the idea is still similar: can we declare const variable filepaths for require?

This is the only thing stopping me from rapid development; I would love to hear some feedback!

Thanks!

The plugin executes the code in async context whenever possible, e.g. in code-button blocks. That is done to enable the functionality on mobile devices, which can’t use synchronous require() directly.

Within async context, all require() calls are statically analyzed in advance, such that, the engine can preload then asynchronously and then sort of load them with synchronous require().

However, for static analysis to work, your script paths have to be constant strings. Otherwise you will get a warning. Therefore, your scripts won’t work on mobile. If you target desktop-only, you can safely ignore the warning.

Ideally, you don’t even use require() and you use modern ESM syntax

import { fn } from './script.js';

Which is the industry standard nowadays. And you should avoid dynamic paths, without a really good reason to. If you do have a reason to change imports dynamically, you better use

await import(dynamicPath);

1 Like

Thank you!

Your explanation of async requiring static analysis for preloading really makes it all make sense. Annoying that mobile devices can’t use synchronous require; I have quite a few workflows on mobile, and I need them to work.

I haven’t seen modern ESM syntax, this looks very promising, especially since it covers the idea that each script should only have one function / clarity.

Though– I am getting around this static path limitation by centralizing ideas (eg: features, eg: “journalling” for my daily journal) into api files (eg: features/api.js, eg: “api.js” that requires all journalling api calls in the journalling folder), and this api file is then required locally (intra-feature) and externally (invocation file requires this api file instead of requiring all functions specifically). AI is helping a bit with this.

I’m wondering if there is a better structure than this? I don’t know what the industry standards are. If not, then hopefully this folder structure (inspired by bulletproof-react) is helpful for other readers.

Time to follow ESM standards… :sweat_smile:

no, that’s a wrong statement

import { fn1, fn2, fn3 } from './script.js';

Let me be more accurate. Mobile doesn’t have require() at all, as it doesn’t have any Node.js engine running. My plugin brings some require() functionality to mobile. However, to simulate sync require(), I would need to at least have sync file reading functionality, which is also not available on mobile. So on mobile, I can rely only on async functions and therefore I need this preload trick.

However, this trick won’t work if you run in truly sync context, e.g.

```dataviewjs
// dataviewjs runs in sync, CommonJS context, ESM syntax won't work here

require('./script.js'); // this won't work on mobile, at all

await requireAsync('./script.js'); // this would work on mobile

await requireAsyncWrapper((require) => {
  require('./script.js'); // this would work on mobile, but only for constant paths
});
```

I don’t see why would you need to have dynamic paths, as modern ESM standard doesn’t encourage dynamic import().

You can have

import { fn } from './script.js'; // static import

const SCRIPT_PATH = './script.js';

const { fn } = await import(SCRIPT_PATH); // dynamic import, usually used not so often
1 Like

Thanks for the info! I’m still confused on some parts:

Barrel files architecture-style:

What I meant was that we can use modularity with barrel files: have each js file export one function (or one main function with some unique functions-specific helpers) for modularity, and then export all feature-related functions (/features/*) or utilities (/utils/*) together in a barrel js “api” file (api.js or api.ts)

Eg, in a invocable file:

import type { App } from 'obsidian';
import { Notice } from 'obsidian';

// Import the Journaling API which provides access to pushPhoneVoiceToDaily
const journalApi = require('../../features/Journaling/api.js');

This way, the specific feature-local paths (eg: /features/Journaling/utils/add-text-to-daily-todos.js) can be easily fixed in a single lin in api.js instead of having to fix all instances of its import, should the file path / file name change (migrations).

This seems like the best ‘style’ to use your plugin in? (I’m learning about it now in trying to create a react app of my own, and unsure of its optimal-ness) (let me know plss)

This also gets rid of dynamic paths for the most part, as a standard api.js path that does not change means you don’t need dynamic imports.

Mobile require:

(documentation: obsidian-codescript-toolkit/docs/core-functions.md at main · mnaoumov/obsidian-codescript-toolkit · GitHub )

Also, I’m a bit confused here– you said “require” will not work, but my above example uses require (context: in an invocable script), and it did work.

I mean, lucky for me, but this is unexpected behavior. I don’t really know why this is or why it happens?

Code for reference (this works on mobile!):

import type { App } from 'obsidian';
import { Notice } from 'obsidian';

// Import the Journaling API which provides access to pushPhoneVoiceToDaily
const journalApi = require('../../features/Journaling/api.js');

/**
 * Entry point required by Obsidian CodeScript Toolkit.
 * 
 * The toolkit automatically calls this function when the script
 * is invoked. The function must be exported and named `invoke`.
 * 
 * @param app - The Obsidian App instance provided by the toolkit
 */
export async function invoke(app: App): Promise<void> {
	try {
		console.log('--- Starting invoke_update_daily_note_todos ---');
		
		// Call the async function from Journaling API
		// This function handles reading from source, moving to daily note, and cleanup
		await journalApi.pushPhoneVoiceToDaily();
		
		// Success notification
		new Notice('✅ Daily note todos updated successfully!', 3000);
		console.log('--- invoke_update_daily_note_todos completed successfully ---');
		
	} catch (error) {
		// Error handling with user notification
		const errorMessage = error instanceof Error ? error.message : String(error);
		new Notice(`❌ Failed to update daily todos: ${errorMessage}`, 7000);
		console.error('Error in invoke_update_daily_note_todos:', error);
	}
}

This way you don’t need require() and don’t need dynamic import paths.

// Invocable.ts
import type { App } from 'obsidian';
import { Notice } from 'obsidian';
import * as journalApi from '../../features/Journaling/api.js'
// features/Journaling/api.js
export { addTextToDailyTodos } from './utils/add-text-to-daily-todos.js';

That’s because the plugin tries to bring as much of require()to mobile as possible. You are using it in invocable scripts, which is executed via invoking a command in Obsidian. In this context, the magic of the plugin makes possible to run on mobile flawlessly.

However, in some other contexts, e.g.

```dataviewjs
require('./path/to/Invocable.ts').invoke(); // won't work on mobile

// however with some tricky syntax, you can make it working equally on desktop and on mobile
requireAsyncWrapper((require) => {
  require('./path/to/Invocable.ts').invoke(); // now it works;
});
1 Like

This is the first reference to something like “ai-guidelines.md” I have found! If you still have it, could you share your ai agent guidelines with me (respond to this message / post a github link)?

AI is simply so much faster and (generally) more efficient at coding in the obsidian / typescript environment, while I just do architectural overviews.

Thank you!
To me right now, it seems like:
import (specific function namespace importing, so you know exactly what you are getting) >= requireAsyncWrapper (will always work) > require (general import).

Not really sure when to use import or when to use require, but this seems like a general typescript issue instead of a codescript toolkit issue specifically?

Feature request:

Would it be possible to create a “user manual” or “recommended usage patterns” / “recommended way of using the plugin”?

I know this is a tall request, so just an idea for now!

1 Like

Feel free to send PR to the repo https://github.com/mnaoumov/obsidian-codescript-toolkit with your ideas how to improve the docs.

1 Like

Hello I’m using the codescript toolkit plugin to implement a specialised webscraping feature on command.

I’ve been using undici + node-html-parser for performance but i get a ‘fastTimeout?.unref is not a function’ error. AI tells me I can’t use undici with the Obsidian environment.

What do you think?

Please create an issue in CodeScript Toolkit GitHub repo, attaching problematic vault

I managed to make it work with the requestUrl fonction from obsidian. The performance drop is probably not significant. Seems like undici is back-end compatible only.

Hello Everyone!

I recently discovered this plugin while trying to “export” a script I wrote in order to reuse it in different notes using an inline command.
I apologize if this is not the right place to post, but I’m still not used to forums and stuff like this.

Anyway, my usecase is basically just a fancy dataviewjs query to render a bunch of “Event” notes in a nice way, including links and portraits to Characters notes (yes, I’m using it for my dnd campaigns).

I took a look on the demo vault and the documentation but I’m struggling to understand how to use this plugin.

The general structure of an “Event” note is this:

```md


date: 2026

priority: 1.1

storyline: Session 1

characters: [“[[Someone]]”, “[[Someone Else]]”]


Event Title

Event description.

  • date obviously indicate the general time the facts happened;
  • priority is used to order events happening in the same year;
  • storyline defines in which session the event happened;
  • characters are links to character notes;

```

The script i have right now is this:

```dataviewjs
const { MarkdownRenderer } = require(“obsidian”);

const pages = dv.pages(`“Events”`) // The “database” folder containing all the events
.where(p => p.date != null)
.sort(p => p.priority, ‘asc’)
.sort(p => p.date, ‘asc’);

const timeline = dv.container.createEl(“div”, { cls: “dnd-timeline” });

for (let p of pages) {
let item = timeline.createEl(“div”, { cls: “timeline-item” });
item.createEl(“div”, { cls: “timeline-year”, text: p.data });
let divider = item.createEl(“div”, { cls: “timeline-divider” });
divider.createEl(“div”, { cls: “timeline-dot” });

let card = item.createEl("div", { cls: "timeline-card" });

// --- ADVANCED PORTRAIT LOGIC ---
let protags = p.protagonists ? (Array.isArray(p.protagonists) ? p.protagonists : \[p.protagonists\]) : \[\];

if (protags.length > 0) {
    let avatarsContainer = card.createEl("div", { cls: "timeline-protagonists" });
    
    for (let prot of protags) {
        // 1. Access the charatcter note
        let p_info = dv.page(prot.path);
        let name = prot.path.split('/').pop().replace('.md', '');
        
        // 2. Create link
        let avatar = avatarsContainer.createEl("a", { cls: "protag-avatar", title: name });
        avatar.href = name;
        avatar.addClass("internal-link");

        // 3. Check if the “portrait” attribute has an image
        if (p_info && p_info.portrait) {
            // Use it as BG if there is
            let imgUrl = app.vault.getResourcePath(app.metadataCache.getFirstLinkpathDest(p_info.portrait.path || p_info.portrait, ""));
            avatar.style.backgroundImage = \`url('${imgUrl}')\`;
            avatar.style.backgroundSize = "cover";
            avatar.style.backgroundPosition = "center";
            avatar.textContent = ""; // Remove fallback initials in case the image is used
        } else {
            // Fallback: use character initial letters
            avatar.textContent = name.substring(0, 2).toUpperCase();
        }
    }
}

let contentDiv = card.createEl("div", { cls: "timeline-content" });
let rawContent = await dv.io.load(p.file.path);
let cleanContent = rawContent.replace(/^---\[\\s\\S\]\*?---\\n/, '');
await MarkdownRenderer.render(app, cleanContent, contentDiv, p.file.path, dv.component);

}
```

Basically orders the events and for each displays the avatars of the involved characters, which double as character links.

For now, I’m just interested in copy/pasting something similar to this in an external script (a .js file such as “Vault/Resources/Scripts/timeline.js) and be able to do something like this in a note to obtain similar results:

`$= timeline(dv, “source_folder”, “filter_attribute”);`

Please note I don’t have a background in software engineering and the code i reported was obtained with the help of AI. I am however able to get the gist of what is happening, so just a small example on a base setup and a hint on how to scale should be sufficient.

Any help is appreciated, thank you in advance!

  1. Please edit your post properly using code blocks. Currently it’s barely readable.
  2. You can use something like
`$= require('/timeline.js').timeline(dv, “source_folder”, “filter_attribute”);`

Assuming that your plugin is configured to use Vault/Resources/Scripts as Scripts folder.
And that your Vault/Resources/Scripts/timeline.js looks like

// if you prefer cjs format
exports.timeline = (dv, sourceFolder, filterAttribute) => {
  // your logic adjusted to use variables sourceFolder, filterAttribute
};

// or if you prefer esm format (recommended)
export function timeline(dv, sourceFolder, filterAttribute) { // UPD: typo was here, corrected
  // your logic adjusted to use variables sourceFolder, filterAttribute
}

Many thanks for the (very quick!) response!

I apologise for the poor quality of the codeblocks. I noticed the editor needed a proper codeblock command after I posted and when I went back to edit it, it was already too late to apply the changes.

I tried out your suggestions and I think I have some progress. I adjusted the script following the esm format and tried the inline command inside the original note.

In the plugin settings the scripts folder is correctly set and the timeline.js script is in this folder as per your instructions. Naming of the functions coincides, however, using the inline command

$= require('/timeline.js').timeline(dv, "Eventi");

returns this error:

Dataview (for inline JS query 'require('/timeline.js').timeline(dv, "Eventi");'): Error: Failed to load module: D:/Data/_WORLDBUILDING/DA SISTEMARE/Risorse/Scripts/timeline.js

(I removed the second argument for simplicity, to reimplement later, also, I have some logic to manage the folder path).

I tried reloading the app both by command palette and by manually closing and reopening it.

What seems to be the problem here?

timeline.js

export function timeline(dv, sourceFolder) {
  dv.paragraph(`Timeline for ${sourceFolder}`);
}

Test.md

`$= require('/timeline.js').timeline(dv, 'MySourceFolder');`

Screenshot

Thank you for the example! That worked with no problems for me too, so I found out the issue was due to errors in the script itself. Mostly syntax, (here you used “=>” in the second option, however here you didn’t) and things like await and async.

What was confusing i think, is that the error message looked like was coming from the CodeScript Toolkit plugin failing to find the script, instead of script execution errors.

I don’t know if this is the intended behaviour but shouldn’t the codeblock wrap the error in some way to differentiate codeblock execution error and script errors? Let me know if I should open a feature-request.

Anyway, now everything works fine, so big thanks for the help and for the plugin!

Yes, there was a copy-paste typo, fixed it.

the error message looked like was coming from the CodeScript Toolkit plugin failing to find the script, instead of script execution errors.

not really

Failed to load module: D:/Data/_WORLDBUILDING/DA SISTEMARE/Risorse/Scripts/timeline.js

“Failed to load” is not the same as “Failed to find”

shouldn’t the codeblock wrap the error in some way to differentiate codeblock execution error and script errors

Here you are using Dataview codeblock, which calls CodeScript Toolkit’s require(). From that point CodeScript Toolkit cannot know from where it was called, it just executes whatever was given to it. If you require() syntactically invalid script, you get the error as above, which seems to be self-descriptive, IMO

That is clear enough, thank you!

+1!
I was about to try this, given my script is becoming long, but haven’t had the time yet.

Here I am with some news!

Like I said, I don’t have a bg in software engineering and most importantly I usually code in Python, so if there are things to tweak in what will follow please feel free to suggest.

I was interested in having a bunch of scripts containing functions and classes, which were then grouped in a single namespace. Then i wanted a single file to expose some functionality using those classes/functions.

Basically, referring to my previous post (and again i apologise for the code readability), I tried to implement some functionality to render timelines, embedding the helper functions and adding a cache for better performance in a class. The “Timeline” class would be then inserted in a “dnd” namespace to maintain coherence and i would create an “API.js” file to expose the functions to be used in obsidian.

This is the general structure I found working (I’m guessing I’m not discovering anything new, but might be useful for someone else):

Folder Structure
Vault

  • Resources
    • Scripts
      • objects.js (write your functions and classes here, can have as many files as you want)
      • namespace.js (here you include the objects to the same namespace, this is optional)
      • API.js (here you expose the thinge you want to use in obsidian)
  • obsidian_note.md (the note/s where you will be using the scripts, can be any structure you want)

object.js

const { MarkdownRenderer } = require("obsidian");

export function foo() {
    console.log("Test function executed!");
}

export class Foo {
    constructor() {}

    test() {
      console.log("Test method executed!");
  }
}

namespace.js

import { Foo } from "./object.js";

export const MyNamespace = {
    Foo,
};

API.js

import { MyNamespace} from "./namespace.js";

export function tester() {
    const foo = new MyNamespace.Foo();
    foo.test();
}

Inside a note in Obsidian

# This is a note
`$= require('/API.js').tester();`

With this you should be able to break down the scripts you have. I have not tested different folder structures in the “Scripts” folder. I guess it would be fine assuming you use the correct paths everywhere.

1 Like