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