Shared resources for NotePlan plugins. (There are no commands for users to run directly.)
This plugin simply ensures that there are some shared resources for NotePlan plugins to use. It has no commands for users to run (apart from some test functions).
In your plugin's plugin.json file include a list of files you want from the shared resouce. E.g. some font resources:
"plugin.requiredSharedFiles": [
"fontawesome.css",
"regular.min.flat4NP.css",
"solid.min.flat4NP.css",
"fa-regular-400.woff2",
"fa-solid-900.woff2"
],
Important notes:
"plugin.requiredFiles": [ ... ] list, which are provided by your plugin itself.To reference them in your own plugin, you need to traverse up and down the folder structure, e.g. the first file above is available at "../np.Shared/fontawesome.css".
There are some functions provided to help you test:
logProvidedResources() functionThis function logs the list of resource files that should currently be available by this plugin (i.e. at run-time, not compile-time).
logAvailableSharedResources() functionThis function logs the set of resource files actually available from np.Shared (by checking its list when this was compiled into your client plugin).
checkForWantedResources(fileList?) functionThis function is provided for your plugin to be able to check resources are available before trying to use them. It can be called two ways:
checkForWantedResources(): returns true or false depending whether np.Shared is loadedcheckForWantedResources(Array<filenames>): returns the number of the filenames that are available from np.Shared.Note: You must set const pluginID = '<your plugin ID>' in the file(s) where you call this function.
If your plugin's _logLevel is set to "DEBUG" then useful details are logged.
NotePlan's licensed Font Awesome Pro 7 'Regular', 'Solid', 'Duotone' and 'Light' webfonts (*.woff2) are made available through this Shared Resource plugin, along with their CSS. To use them your HTML will need to include the relevant items from the following in the <head> section:
<head>
...
<link href="../np.Shared/fontawesome.css" rel="stylesheet">
<link href="../np.Shared/light.min.flat4NP.css" rel="stylesheet">
<link href="../np.Shared/regular.min.flat4NP.css" rel="stylesheet">
<link href="../np.Shared/solid.min.flat4NP.css" rel="stylesheet">
<link href="../np.Shared/duotone.min.flat4NP.css" rel="stylesheet">
...
</head>
(Note: the *.flat4NP.css stylesheets are FA style sheets tweaked so font url(...) paths are flat filenames, which NotePlan's shared-file layout requires.)
And then to use the icons use the italic-element syntax like:
<p><i class="fa-solid fa-arrow-rotate-right"></i> Refresh</p>
Please use the Font Awesome website to view/search for icons.
There is also a pluginToHTMLCommsBridge file that can be used to enable bi-directional communications between the plugin and the HTML window. To use this file, import it like so, making sure to set the variable receivingPluginID to your plugin where you want to receive the messages:
<script type="text/javascript" src="../np.Shared/pluginToHTMLErrorBridge.js"></script>
<script>const receivingPluginID = "jgclark.Dashboard"</script>
<script type="text/javascript" src="./html-plugin-comms.js"></script>
<script type="text/javascript" src="../np.Shared/pluginToHTMLCommsBridge.js"></script>
<script>
/* you must set these variables before you import the bridge */
const receivingPluginID = "author.PluginName"; // the plugin ID of the plugin which will receive the comms from HTML
// That plugin should have a function NAMED `onMessageFromHTMLView` (in the plugin.json and exported in the plugin's index.js)
// this onMessageFromHTMLView will receive any arguments you send using the sendToPlugin() command in the HTML window
/* the switchboard function is called when data is received from your plugin and needs to be processed. this function
should not do the work itself, it should just send the data payload to a function for processing. The switchboard function
below and your processing functions can be in your html document or could be imported in an external file. The only
requirement is that switchboard (and receivingPluginID) must be defined or imported before the `pluginToHTMLCommsBridge`
be in your html document or could be imported in an external file */
function switchboard(type, data) {
switch (type) {
case 'yourType1':
// call some function to process the data for yourType1 messages and pass the `data` parameter
break
case 'yourType2':
// call some function to process the data for yourType2 messages
break
}
}
</script>
<script type="text/javascript" src="../npShared/pluginToHTMLCommsBridge.js"></script>
/Open Template Form will open a dialog/form with the items specified in the template.DynamicDialog component from np.Shared and another Component FormView which basically just takes the form items from the template and sends them to the DynamicDialog componentUse the DynamicDialog component from np.Shared
NOTE: The html-plugin-comms.js is where you will do the sending/receiving in the HTML window (browser side). That file is auto-created for you when you run a
np-cli plugin:createcommand.
The live-server npm package can be very useful to locally open saved HTML output file but running the react script files updated in the background by npc ... -w. For example:
live-server --open="jgclark.Dashboard/dashboard-react.html" --ignore="*.json"
(The ignore in this case stops it re-loading when that plugin's todaysChangedNoteList.json file changes, which it can do frequently.)
Plugins can share a vault-wide index of which notes contain particular #hashtags and @mentions, without each plugin scanning every note itself. Implementation: np.Shared/src/tagMentionCache.js. Originally written by @jgclark for Dashboard; moved here in Sep 2026 so other plugins (e.g. Projects + Reviews) can use it.
The cache does not return paragraphs or task text. It answers: "which note filenames currently have any of these wanted tags/mentions?"
Each hit is stored as { filename, items: ['#project', '@alice'] } in two lists: regularNotes and calendarNotes. Lookups are case-insensitive (#Area matches #area).
A note is indexed only if a wanted item appears in:
Hashtags that appear only on done tasks, or only in body prose, are not indexed (that would make the file much larger). Notes in @ special folders (@Archive, @Templates, @Trash, …) are skipped on a full rebuild.
Two files under data/np.Shared/ (paths are fully specified so any plugin context can read them):
| File | Role |
|---|---|
wantedTagMentionsList.json | Per-plugin registrations. Each plugin writes only its own slot. |
tagMentionCache.json | The index: generatedAt, lastUpdated, wantedItems, regularNotes, calendarNotes. |
The cache body's wantedItems (and every rebuild / incremental update) is the union of all slots. An item stays in the union until no registered plugin still wants it.
When any client runs generateTagMentionCache or updateTagMentionCache, the scan covers every plugin's registrations, not just the caller's. E.g. for the following registration, a single update from Reviews for #project will also cover @bob.
{
"registrations": {
"jgclark.Dashboard": ["@bob"],
"jgclark.Reviews": ["#project"]
}
}
Unfortunately, Shared cannot self-update the cache, as it has no timer and no long-lived context. It only exposes functions (generateTagMentionCache, updateTagMentionCache, age checks, scheduleTagMentionCacheGeneration). Therefore clients must manage updates and rebuilds to ensure it is ready when needed. If no client calls generate/update, the cache is not refreshed and will go stale.
scheduleTagMentionCacheGeneration); they do not start a scan. Shared never starts generateTagMentionCache by itself.isTagMentionCacheGenerationScheduled() after first paint and after section refresh, then runs generateTagMentionCache (with a progress banner). TAG lookups via getFilenamesOfNotesWithTagOrMentions can incrementally update if the client passes firstUpdateCache: true (default) and the cache is more than about 1 hour old, and they schedule a full rebuild if it is more than about 5 days old. Dashboard still has to run that scheduled rebuild.registerTagMentionCacheItems, and do not await one on a UI refresh path if you can schedule it instead. JSContext is single-threaded, so a fire-and-forget generate still blocks the caller.Import from Shared (Rollup will bundle the module). Use your plugin.id so other plugins' lists are left alone.
import {
registerTagMentionCacheItems,
unregisterTagMentionCacheItems,
addTagMentionCacheItemsForPlugin,
getTagMentionCacheDefinitions,
isTagMentionCacheAvailable,
isTagMentionCacheAvailableForItem,
} from '../../np.Shared/src/tagMentionCache'
// Replace this plugin's list (does not wipe other plugins)
registerTagMentionCacheItems('jgclark.Reviews', ['#project', '#area', '#goal'])
// Add without removing existing items for this plugin
addTagMentionCacheItemsForPlugin('jgclark.Reviews', ['#area'])
// Drop this plugin's list. Items remain if another plugin still registered them.
unregisterTagMentionCacheItems('jgclark.Reviews')
getTagMentionCacheDefinitions() returns the current union (all plugins). isTagMentionCacheAvailable() is true when tagMentionCache.json exists. isTagMentionCacheAvailableForItem('#project') is true when that item is already in the cache body's wantedItems (so a lookup will not miss it for being unregistered).
If you register items that are not yet in the union, Shared only schedules a full rebuild. Your plugin (or Dashboard) must run generateTagMentionCache when it is ready. A 5-day-old cache is also only flagged; a client has to run generate.
addTagMentionCacheDefinitions / setTagMentionCacheDefinitions are Dashboard-compat helpers that write only the jgclark.Dashboard slot.
Cheap read (preferred on a refresh path). Regular notes only. Does not update or rebuild. Returns [] if the cache file is missing.
import { getRegularNoteFilenamesFromTagMentionCache } from '../../np.Shared/src/tagMentionCache'
const filenames = getRegularNoteFilenamesFromTagMentionCache(['#project', '#area'])
// e.g. ['Projects/Home.md', 'Areas/Health.md']
Resolve a note with DataStore.projectNoteByFilename(filename) (or your usual helper) if you need the TNote.
Full lookup (calendar + regular). Can incrementally update the cache first.
import { getFilenamesOfNotesWithTagOrMentions } from '../../np.Shared/src/tagMentionCache'
// firstUpdateCache=false: do not rebuild or incrementally update on this call
const [filenames, cacheAgeInfo] = await getFilenamesOfNotesWithTagOrMentions(
['#project', '@alice'],
false,
)
Pass firstUpdateCache: true (the default) only when you can afford updateTagMentionCache() (it walks notes changed since last run). The second return value is a short cache-age string for diagnostics.
One note. getCacheItemsFromNote(note, wantedItems) returns the wanted tags/mentions found on that note using the same rules as a cache build (open items + any frontmatter field).
registerTagMentionCacheItems(yourPluginId, yourTags).isTagMentionCacheAvailableForItem is true for the tags you need, call getRegularNoteFilenamesFromTagMentionCache.generateTagMentionCache / updateTagMentionCache (Dashboard does this after paint and after refresh). Shared will not do it for you.Plugins can share a rolling 7 calendar day list of which notes changed recently, without each caller re-running getNotesChangedInInterval on hot paths. Implementation: np.Shared/src/notesChangedRecentlyCache.js. Design: PLAN-notes-changed-recently-cache.md.
Answers only: "which notes changed recently?" -- not done counts, tags, or project metadata.
Each entry is { filename, noteType: 'Notes'|'Calendar', changedAt } (ISO UTC). The on-disk window is fixed at 7 calendar days (today + 6 prior). Readers who need "today only" or "since timestamp T" filter with the sync getters below.
This does not replace getNotesChangedInInterval in helpers/NPnote.js. Full generate / incremental update call that scan (via getNotesChangedInLastCalendarDays(7), which maps to interval arg 6). Sync getters only read the JSON.
| File | Role |
|---|---|
../../data/np.Shared/notesChangedRecently.json | Cache body: generatedAt, lastUpdated, windowDays, notes[] |
Prefs: np.Shared.notesChangedRecently.lastUpdated, np.Shared.notesChangedRecently.regenerate.
Shared cannot self-update (no timer). Clients must call:
updateNotesChangedRecentlyCache() -- incremental upsert + prune; if the file is missing or corrupt, generates immediately (entry build on async thread when NotePlan supports it)generateNotesChangedRecentlyCache(reason?) -- full 7-day rebuild (vault scan on main thread; build/prune via runSyncWorkOnAsyncThread when available)updateNotesChangedRecentlyCacheIfTooOld(maxAgeHours?) -- incremental if last update older than ~1 hour (default); generates immediately if cache is missingscheduleNotesChangedRecentlyCacheGeneration() / isNotesChangedRecentlyCacheGenerationScheduled() -- for age-based full rebuilds (e.g. generatedAt older than ~7 days); same schedule-then-run-after-paint pattern as the tag cacheCommands (optional): /generateNotesChangedRecentlyCache (gncrc), /updateNotesChangedRecentlyCache (uncrc).
import {
isNotesChangedRecentlyCacheAvailable,
getFilenamesChangedToday,
getFilenamesChangedRecently,
getFilenamesChangedSince,
updateNotesChangedRecentlyCacheIfTooOld,
generateNotesChangedRecentlyCache,
isNotesChangedRecentlyCacheGenerationScheduled,
} from '../../np.Shared/src/notesChangedRecentlyCache'
// After UI paint / before heavy work:
await updateNotesChangedRecentlyCacheIfTooOld()
if (isNotesChangedRecentlyCacheGenerationScheduled()) {
await generateNotesChangedRecentlyCache('after paint')
}
if (isNotesChangedRecentlyCacheAvailable()) {
const today = getFilenamesChangedToday({ noteTypes: ['Notes', 'Calendar'] })
const recentNotesOnly = getFilenamesChangedRecently({ noteTypes: ['Notes'] })
const since = getFilenamesChangedSince(lastRunDate, { noteTypes: ['Notes'] })
}
If the cache is missing or corrupt, getters return [] and do not start a scan.
updateNotesChangedRecentlyCacheIfTooOld(); if generation is scheduled, run generateNotesChangedRecentlyCache.getFilenamesChangedToday / getFilenamesChangedSince / getFilenamesChangedRecently.If you find an issue with this plugin, or would like to suggest new features for it, please raise a Bug or Feature 'Issue' in GitHub.
See CHANGELOG for latest updates/changes to this plugin.