Skip to content

Commit

Permalink
feat: improve validation when loading scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
lars-berger committed Jul 2, 2024
1 parent 2cbbf3c commit 8a79976
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 23 deletions.
18 changes: 12 additions & 6 deletions packages/client-api/src/init-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,18 @@ export async function initElement(

// Import the scripts for the element.
runWithOwner(args.owner, () => {
createEffect(() => {
if (parsedConfig.events) {
for (const event of parsedConfig.events) {
const split = event.fn_path.split('#');
scriptManager.loadScript(split[0]!);
}
createEffect(async () => {
try {
await Promise.all(
parsedConfig.events
.map(config => config.fn_path)
.map(scriptManager.loadScriptForFn),
);
} catch (err) {
await showErrorDialog({
title: `Non-fatal: Error in ${args.type}/${args.id}`,
error: err,
});
}
});
});
Expand Down
65 changes: 49 additions & 16 deletions packages/client-api/src/user-config/get-script-manager.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,47 @@
import { convertFileSrc } from '@tauri-apps/api/core';
import { join, homeDir } from '@tauri-apps/api/path';
import { createStore } from 'solid-js/store';

import { createLogger } from '~/utils';
import type { ElementContext } from '../element-context.model';

const logger = createLogger('script-manager');

const [modules, setModules] = createStore<Record<string, Promise<any>>>(
{},
);
/**
* Map of asset paths to promises that resolve to the module.
*/
const modulesByPath: Record<string, Promise<any>> = {};

/**
* Map of module paths to asset paths.
*/
const modulePathToAssetPath: Record<string, string> = {};

/**
* Abstraction over loading and invoking user-defined scripts.
*/
export function getScriptManager() {
return {
loadScript,
loadScriptForFn,
callFn,
};
}

async function loadScript(path: string): Promise<any> {
const scriptPath = await join(await homeDir(), '.glzr/zebar', path);
const scriptAssetPath = convertFileSrc(scriptPath);
async function loadScriptForFn(fnPath: string): Promise<any> {
const { modulePath } = parseFnPath(fnPath);

const assetPath =
modulePathToAssetPath[modulePath] ??
(modulePathToAssetPath[modulePath] = convertFileSrc(
await join(await homeDir(), '.glzr/zebar', modulePath),
));

logger.info(
`Loading script at path '${assetPath}' for function path '${fnPath}'.`,
);

const importPromise = import(assetPath);
modulesByPath[assetPath] = importPromise;

const importPromise = import(scriptAssetPath);
setModules({ [path]: importPromise });
return importPromise;
}

Expand All @@ -35,20 +50,38 @@ async function callFn(
event: Event,
context: ElementContext,
): Promise<any> {
const split = fnPath.split('#');
const foundModule = modules[split[0]!];
const { modulePath, functionName } = parseFnPath(fnPath);
const assetPath = modulePathToAssetPath[modulePath];
const foundModule = modulesByPath[assetPath!];

if (!foundModule) {
throw new Error('Invalid function path');
throw new Error(`No script found at function path '${fnPath}'.`);
}

return foundModule.then(m => {
const fn = m[split[1]!];
return foundModule.then(foundModule => {
const fn = foundModule[functionName!];

if (!fn) {
throw new Error('Invalid function path');
throw new Error(
`No function with the name '${functionName}' at function path '${fnPath}'.`,
);
}

return fn(event, context);
});
}

function parseFnPath(fnPath: string): {
modulePath: string;
functionName: string;
} {
const [modulePath, functionName] = fnPath.split('#');

// Should never been thrown, as the path is validated during config
// deserialization.
if (!modulePath || !functionName) {
throw new Error(`Invalid function path '${fnPath}'.`);
}

return { modulePath, functionName };
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ export const ElementEventsConfigSchema = z
.array(
z.object({
type: z.enum(HTML_EVENTS),
fn_path: z.string(),
fn_path: z
.string()
.regex(
/^(.+)#([a-zA-Z_$][a-zA-Z0-9_$]*)$/,
"Invalid function path. Needs to be in format 'path/to/my-script.js#functionName'.",
),
selector: z.string().optional(),
}),
)
Expand Down

0 comments on commit 8a79976

Please sign in to comment.