Skip to content

Instantly share code, notes, and snippets.

@NathanaelA
Last active October 21, 2023 02:28
Show Gist options
  • Save NathanaelA/1d834a5967e502570279cad610f82d30 to your computer and use it in GitHub Desktop.
Save NathanaelA/1d834a5967e502570279cad610f82d30 to your computer and use it in GitHub Desktop.
Svelte Node.JS loader
// (c) 2021-2022, Nathanael Anderson @ Master Technology
// License: MIT
// Please NOTE the compiler settings on Line 49 & 89 is VERY VERY specific to my use case,
// the compiler OPTIONS probably will be different for your a normal use case.
// Flags
const DEBUGGING = false;
import { writeFileSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { dirname, extname, resolve as resolvePath } from 'node:path';
import { cwd } from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import * as svelte from 'svelte/compiler';
const baseURL = pathToFileURL(`${cwd}/`).href;
export async function resolve(specifier, context, defaultResolve) {
const { parentURL = baseURL } = context;
if (specifier.endsWith(".svelte")) {
return {
url: new URL(specifier, parentURL).href
};
}
// Let Node.js handle all other specifiers.
return defaultResolve(specifier, context, defaultResolve);
}
export function getFormat(url, context, defaultGetFormat) {
// Handle our Svelte file types
if (url.endsWith(".svelte")) {
return {
format: 'module'
};
}
// Let Node.js handle all other URLs.
return defaultGetFormat(url, context, defaultGetFormat);
}
export function transformSource(source, context, defaultTransformSource) {
const { url, format } = context;
if (url.endsWith(".svelte")) {
const transformedSource = svelte.compile(source.toString(), {
namespace: "foreign", // Needs to be this, otherwise we won't have all elements created...
dev: false,
enableSourcemap: {js: true, css: false},
filename: url
});
// We have to transform all manual imports of "svelte" to "svelte/internal" because the default import
// on NODE points to the server side script (SSR) which doesn't support some events.
let code = transformedSource.js.code.replace(/['"](svelte)['"]/g, "'svelte/internal'") + "\r\n//# sourceMappingURL="+transformedSource.js.map.toUrl();
if (DEBUGGING) {
let output = url.substr(7) + ".mjs";
writeFileSync(output, code);
}
return {
source: code
};
}
// Let Node.js handle all other sources.
return defaultTransformSource(source, context, defaultTransformSource);
}
// Later versions of Node...
export async function load(url, context, defaultLoad) {
if (url.endsWith(".svelte")) {
let format = await getPackageType(url);
if (format === false) { format = "module"; }
if (format === 'commonjs') {
return { format };
}
const { source: rawSource } = await defaultLoad(url, { format });
const transformedSource = svelte.compile(rawSource.toString(), {
namespace: "foreign", // Needs to be this, otherwise we won't have all elements created...
enableSourcemap: {js: true, css: false},
filename: url
});
// We have to transform all manual imports of "svelte" to "svelte/internal" because the default import
// on NODE points to the server side script which doesn't support some events.
let code = transformedSource.js.code.replace(/['"](svelte)['"]/g, "'svelte/internal'") + "\r\n//# sourceMappingURL="+transformedSource.js.map.toUrl();
if (DEBUGGING) {
let output = url.substr(7) + ".mjs";
writeFileSync(output, code);
}
return {
format,
source: code,
};
}
// Let Node.js handle all other URLs.
return defaultLoad(url, context, defaultLoad);
}
async function getPackageType(url) {
const isFilePath = !!extname(url);
// If it is a file path, get the directory it's in
const dir = isFilePath ?
dirname(fileURLToPath(url)) :
url;
// Compose a file path to a package.json in the same directory,
// which may or may not exist
const packagePath = resolvePath(dir, 'package.json');
// Try to read the possibly nonexistent package.json
const type = await readFile(packagePath, { encoding: 'utf8' })
.then((filestring) => JSON.parse(filestring).type)
.catch((err) => {
if (err?.code !== 'ENOENT') console.error(err);
});
// Ff package.json existed and contained a `type` field with a value, voila
if (type) return type;
// Otherwise, (if not at the root) continue checking the next directory up
// If at the root, stop and return false
return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
}
@NathanaelA
Copy link
Author

NathanaelA commented Sep 2, 2022

Some notes:

  1. you need Svelte installed as a dev dependency of your project. 😀
  2. This works with BOTH versions of the node experimental loaders, the older node versions did it one way. Newer versions do it a different way. The newer nodes will now "Warn" you about the older compatibility being unsupported. You can ignore it, or strip it out if you aren't planning on using any older versions of node.
  3. my specific use case needs the namespace: "foreign" set as a compiler option, most peoples problem will want to remove this line from both places it is set.
  4. This is named svelte-loader.mjs in my project root directory.
  5. The DEBUGGING flag will let you actually see the .mjs code the compiler built. It sometimes is helpful.

To use:

node --experimental-loader ../svelte-loader.mjs entry-point.mjs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment