Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-snails-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sv': patch
---

fix(addon): relax dependency fields restriction on community add-ons
4 changes: 2 additions & 2 deletions documentation/docs/30-add-ons/99-community.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ Community add-ons are bundled with [tsdown](https://tsdown.dev/) into a single f

### `package.json`

Your add-on must have `sv` as a peer dependency and **no** `dependencies` in `package.json`:
Your add-on must have `sv` as a peer dependency. Any `dependencies` declared will **not** be available at runtime, everything must be bundled:

```jsonc
{
Expand All @@ -164,7 +164,7 @@ Your add-on must have `sv` as a peer dependency and **no** `dependencies` in `pa
"publishConfig": {
"access": "public"
},
// cannot have dependencies
// packages declared here will not be available during runtime, it must be bundled
"dependencies": {},
"peerDependencies": {
// minimum version required to run by this add-on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ npm publish

## Things to be aware of

Community add-ons must have `sv` as a `peerDependency` and should **not** have any `dependencies`. Everything else (including `@sveltejs/sv-utils`) is bundled at build time by tsdown.
Community add-ons must have `sv` as a `peerDependency`. Any `dependencies` declared in `package.json` will not be available at runtime. Everything else (including `@sveltejs/sv-utils`) is bundled at build time by tsdown.
9 changes: 8 additions & 1 deletion packages/sv/src/core/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,14 @@ export function updateReadme(projectPath: string, command: string) {
}

export function errorAndExit(message: string) {
p.log.error(message);
const [firstLine, ...restLines] = message.split('\n');

p.log.error(firstLine);
// Fixes issue where the first line of the error message is not the same color as the rest of the lines
for (const line of restLines) {
p.log.message(color.optional(line), { spacing: 0 });
}

p.log.message();
p.cancel('Operation failed.');
process.exit(1);
Expand Down
75 changes: 48 additions & 27 deletions packages/sv/src/core/fetch-packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const NODE_MODULES = fileURLToPath(new URL('../../node_modules', import.meta.url

function verifyPackage(addonPkg: Record<string, any>, specifier: string): string | undefined {
const peerDeps = { ...addonPkg.peerDependencies };
const deps = { ...addonPkg.dependencies };

// valid addons should always have `sv` as a peerDependency
const addonSvVersion = peerDeps['sv'];
Expand All @@ -25,13 +24,6 @@ function verifyPackage(addonPkg: Record<string, any>, specifier: string): string
);
}

// addons should not have any dependencies (everything should be bundled)
if (Object.keys(deps).length > 0) {
throw new Error(
`Invalid add-on package detected: '${specifier}'\nCommunity add-ons should not have any 'dependencies'. Use 'peerDependencies' for 'sv' and bundle everything else`
);
}

// Check version compatibility and warn if there's a major version mismatch
const addon = coerceVersion(addonSvVersion);
const sv_major = coerceVersion(pkg.version).major;
Expand Down Expand Up @@ -102,7 +94,8 @@ export async function downloadPackage(options: DownloadOptions): Promise<AddonDe
// Try to create a symlink, but fall back to copying on Windows if it fails with EPERM
try {
fs.symlinkSync(options.path, dest, 'dir');
} catch (error: any) {
} catch (error) {
if (!isNodeError(error)) throw error;
// On Windows, symlinks may fail with EPERM if admin privileges aren't available
// In that case, fall back to copying the directory
if (platform() === 'win32' && (error.code === 'EPERM' || error.code === 'EACCES')) {
Expand All @@ -112,7 +105,7 @@ export async function downloadPackage(options: DownloadOptions): Promise<AddonDe
}
}

return await importAddonCode(pkg.name, pkg.version);
return await importAddonCode(pkg.name, pkg.version, pkg.exports);
}

const tarballUrl: string = pkg.dist.tarball;
Expand All @@ -130,35 +123,63 @@ export async function downloadPackage(options: DownloadOptions): Promise<AddonDe
unpackTar(path.join(NODE_MODULES, pkg.name), { strip: 1 })
);

return await importAddonCode(pkg.name, pkg.version);
return await importAddonCode(pkg.name, pkg.version, pkg.exports);
}

async function importAddonCode(pkgName: string, pkgVersion: string): Promise<AddonDefinition> {
async function importAddonCode(
pkgName: string,
pkgVersion: string,
exports?: Record<string, string | undefined>
): Promise<AddonDefinition> {
const issues: string[] = [];

let details: AddonDefinition | undefined;
try {
({ default: details } = await import(`${pkgName}/sv`));
} catch {
issues.push(`'/sv' export not found`);
const error = () => {
return new Error(
`Failed to load add-on '${pkgName}@${pkgVersion}':\n- ${issues.join('\n- ')}\n\n` +
`Please report this to the add-on author.`
);
};

if (!exports) {
issues.push(`'exports' field not found in package.json`);
throw error();
}

if (!details) {
const svImport = exports['./sv'] ? `${pkgName}/sv` : undefined;
const defaultImport = exports['.'] ? pkgName : undefined;
if (!svImport && !defaultImport) {
issues.push(`export conditions './sv' or '.' are not present in package.json`);
throw error();
}

let details: AddonDefinition | undefined;

for (const importPath of [svImport, defaultImport]) {
if (!importPath) continue;
try {
({ default: details } = await import(pkgName));
} catch {
issues.push(`default export not found`);
details ??= await import(importPath).then((m) => m.default);
} catch (e) {
if (isNodeError(e)) {
if (e.code === 'ERR_MODULE_NOT_FOUND') {
issues.push('the add-on contains dependencies that are not bundled');
throw error();
}
issues.push(`Failed to import add-on '${importPath}': ${e.message}`);
} else {
issues.push(`An unknown error has occurred: ${e}`);
}
}
}

if (!details && issues.length > 0) {
throw new Error(
`Failed to load add-on '${pkgName}@${pkgVersion}':\n- ${issues.join('\n- ')}\n\n` +
`Please report this to the add-on author.`
);
if (!details) {
throw error();
}

return details!;
return details;
}

function isNodeError(err: unknown): err is Error & NodeJS.ErrnoException {
return err instanceof Error;
}

type PackageJSON = {
Expand Down
2 changes: 1 addition & 1 deletion packages/sv/src/create/shared/+addon/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ npm publish

## Things to be aware of

Community add-ons must have `sv` as a `peerDependency` and should **not** have any `dependencies`. Everything else (including `@sveltejs/sv-utils`) is bundled at build time by tsdown.
Community add-ons must have `sv` as a `peerDependency`. Any `dependencies` declared in `package.json` will not be available at runtime. Everything else (including `@sveltejs/sv-utils`) is bundled at build time by tsdown.