Skip to content

Instantly share code, notes, and snippets.

@ewmb7701
Created February 2, 2026 08:58
Show Gist options
  • Select an option

  • Save ewmb7701/d14a8bc2cea5d50bd6ccd3df121f1452 to your computer and use it in GitHub Desktop.

Select an option

Save ewmb7701/d14a8bc2cea5d50bd6ccd3df121f1452 to your computer and use it in GitHub Desktop.
import { glob } from "glob";
import { mkdir, copyFile } from "node:fs/promises";
import { dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { Module, ModuleEmitter, ModuleResolver, type API } from "swooce";
/**
* Module without any content.
*
* Useful for eg,
*/
export class VoidModule extends Module {}
/**
* Module with content.
*/
export abstract class ContentModule<TContent> extends Module {
/**
* Fetch the content of the src file of this module.
*/
abstract fetch(api: API): Promise<TContent>;
}
/**
* Creates a module resolver which resolves the "sidecar file" {@link VoidModule} as a {@link VoidModule}.
*
* Example usage:
* ```ts
* export default SidecarFileVoidModuleResolver(import.meta.url);
* ```
*
* @param resolverImportMetaURL `import.meta.url` of the resolver. ie, the `import.meta.url` from the esmodule that called this.
*/
export function SidecarFileVoidModuleResolver<TModule extends Module>(
resolverImportMetaURL: string,
moduleFactory: (srcModuleFileURL: URL) => TModule,
) {
if (resolverImportMetaURL.match(/\.[^/.]+$/)) {
throw new Error(
`Sidecar resolver file has no extension: ${resolverImportMetaURL}`,
);
}
return class extends ModuleResolver<TModule> {
override async resolve(_api: API) {
const url = new URL(resolverImportMetaURL);
// remove the last extension
url.pathname = url.pathname.replace(/\.[^/.]+$/, "");
return moduleFactory(url);
}
};
}
/**
* Creates a module resolver which resolves some files in the sidecar directory as {@link VoidModule}.
*
* # Example usage:
*
* ## Import all .png and .svg images in the sidecar dir
* ```ts
* // src/site/page/post.ts
* export default SidecarDirModuleResolver(
* import.meta.url, // resolver file URL
* "*.md", // glob pattern to match all post markdowns
* (url) => new PostPageModule(url), // factory for each matched file
* );
* ```
*
* @param resolverImportMetaURL `import.meta.url` of the resolver. ie, the `import.meta.url` from the esmodule that called this.
*/
export function SidecarDirModuleResolver<T extends Module>(
resolverImportMetaURL: string,
pattern: string,
moduleFactory: (srcModuleFileURL: URL) => T,
) {
return class extends ModuleResolver<Module> {
override async resolve(_api: API): Promise<Module[]> {
// strip last extension ==> directory name
const sidecarDirURL = new URL(
resolverImportMetaURL.replace(/\.[^/.]+$/, "") + "/",
resolverImportMetaURL,
);
const sidecarDirPath = fileURLToPath(sidecarDirURL);
const sidecarDirFileMatches = await glob(pattern, {
cwd: sidecarDirPath,
nodir: true,
posix: true,
});
return sidecarDirFileMatches.map((sidecarDirFileRelativePath) =>
moduleFactory(new URL(sidecarDirFileRelativePath, sidecarDirURL)),
);
}
};
}
/**
* Creates a module resolver which resolves some files in the sidecar directory.
*
* ie, resolves the resolvers in a sidecar dir.
*
* # Example usage:
*
* ## Import all .png and .svg images in sidecar dir
* ```ts
* // src/assets/images.ts
* export default sidecarDirBarrelModuleResolver(import.meta.url, "*.{png,svg});
* ```
*
* @param resolverImportMetaURL `import.meta.url` of the resolver. ie, the `import.meta.url` from the esmodule that called this.
*/
export function SidecarDirBarrelModuleResolver(
resolverImportMetaURL: string,
pattern: string,
) {
return class extends ModuleResolver<Module> {
override async resolve(api: API): Promise<Module[]> {
// strip last extension ==> directory name
const sidecarDirURL = new URL(
resolverImportMetaURL.replace(/\.[^/.]+$/, "") + "/",
resolverImportMetaURL,
);
const sidecarDirPath = fileURLToPath(sidecarDirURL);
const sidecarDirFileMatches = await glob(pattern, {
cwd: sidecarDirPath,
nodir: true,
posix: true,
});
const modules: Module[] = [];
for (const iSidecarDirFileRelativePath of sidecarDirFileMatches) {
const resolverFileURL = pathToFileURL(
fileURLToPath(sidecarDirURL + iSidecarDirFileRelativePath),
);
// dynamic import of resolver
const imported = await import(resolverFileURL.href);
const ResolverClass =
imported.default as new () => ModuleResolver<Module>;
const resolverInstance = new ResolverClass();
const resolvedModules = await resolverInstance.resolve(api);
if (Array.isArray(resolvedModules)) {
modules.push(...resolvedModules);
} else {
modules.push(resolvedModules);
}
}
return modules;
}
};
}
/**
* Module emitter which copies the module src file to its target file path using {@link API}.
*/
export class CopyModuleEmitter extends ModuleEmitter<Module> {
#mkdir: boolean;
async emit(api: API, module: Module): Promise<void> {
const targetFileURL = api.paths.resolveModuleTargetFileURL(api, module);
const srcPath = fileURLToPath(module.srcFileURL);
const targetPath = fileURLToPath(targetFileURL);
if (this.#mkdir) {
await mkdir(dirname(targetPath), { recursive: true });
}
await copyFile(srcPath, targetPath);
}
constructor(mkdir: boolean) {
super();
this.#mkdir = mkdir;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment