@sveltejs/package
Version:
The fastest way to build Svelte packages
204 lines (182 loc) • 6.33 kB
JavaScript
import colors from 'kleur';
import { load_pkg_json } from './config.js';
/**
* @param {import("./types.js").Options} options
*/
export function create_validator(options) {
const { analyse_code, validate } = _create_validator(options);
return {
/**
* Checks a file content for problematic imports and things like `import.meta`
* @param {string} name
* @param {string} content
*/
analyse_code(name, content) {
analyse_code(name, content);
},
validate() {
/** @type {Record<string, any>} */
const pkg = load_pkg_json(options.cwd);
const warnings = validate(pkg);
if (Object.keys(pkg).length === 0) {
warnings.push(
'No package.json found in the current directory. Please create one or run this command in a directory containing one.'
);
}
// Just warnings, not errors, because
// - would be annoying in watch mode (would have to restart the server)
// - maybe there's a custom post-build script that fixes some of these
if (warnings.length) {
console.log(
colors
.bold()
.yellow('@sveltejs/package found the following issues while packaging your library:')
);
for (const warning of warnings) {
console.log(colors.yellow(`${warning}\n`));
}
}
}
};
}
/**
* @param {import("./types.js").Options} options
*/
export function _create_validator(options) {
/** @type {Set<string>} */
const imports = new Set();
let uses_import_meta = false;
let has_svelte_files = false;
/**
* Checks a file content for problematic imports and things like `import.meta`
* @param {string} name
* @param {string} content
*/
function analyse_code(name, content) {
has_svelte_files =
has_svelte_files ||
(options.config.extensions ?? ['.svelte']).some((ext) => name.endsWith(ext));
uses_import_meta = uses_import_meta || content.includes('import.meta.env');
const file_imports = [
...content.matchAll(/from\s+('|")([^"';,]+?)\1/g),
...content.matchAll(/import\s*\(\s*('|")([^"';,]+?)\1\s*\)/g)
];
for (const [, , import_path] of file_imports) {
if (import_path.startsWith('$app/')) {
imports.add(import_path);
}
}
}
/**
* @param {Record<string, any>} pkg
*/
function validate(pkg) {
/** @type {string[]} */
const warnings = [];
if (
imports.has('$app/environment') &&
[...imports].filter((i) => i.startsWith('$app/')).length === 1
) {
warnings.push(
'Avoid usage of `$app/environment` in your code, if you want the library to work for people not using SvelteKit (only regular Svelte, for example). ' +
'Consider using packages like `esm-env` instead which provide cross-bundler-compatible environment variables.'
);
}
if (uses_import_meta) {
warnings.push(
'Avoid usage of `import.meta.env` in your code. It only works in apps bundled with Vite. ' +
'Consider using packages like `esm-env` instead which works with all bundlers or without bundling.'
);
}
if (
!(pkg.dependencies?.['@sveltejs/kit'] || pkg.peerDependencies?.['@sveltejs/kit']) &&
([...imports].some((i) => i.startsWith('$app/')) || imports.has('@sveltejs/kit'))
) {
warnings.push(
'You are using SvelteKit-specific imports in your code, but you have not declared a dependency on `@sveltejs/kit` in your `package.json`. ' +
'Add it to your `dependencies` or `peerDependencies`.'
);
}
if (
!(pkg.dependencies?.svelte || pkg.peerDependencies?.svelte) &&
(has_svelte_files ||
[...imports].some((i) => i.startsWith('svelte/') || imports.has('svelte')))
) {
warnings.push(
'You are using Svelte components or Svelte-specific imports in your code, but you have not declared a dependency on `svelte` in your `package.json`. ' +
'Add it to your `dependencies` or `peerDependencies`.'
);
}
if (pkg.exports) {
const { conditions } = traverse_exports(pkg.exports);
if (has_svelte_files && !pkg.svelte && !conditions.has('svelte')) {
warnings.push(
'You are using Svelte files, but did not declare a `svelte` condition in one of your `exports` in your `package.json`. ' +
'Add a `svelte` condition to your `exports` to ensure that your package is recognized as Svelte package by tooling. ' +
'See https://svelte.dev/docs/kit/packaging#anatomy-of-a-package-json-exports for more info'
);
}
if (pkg.svelte) {
const root_export = pkg.exports['.'];
if (!root_export) {
warnings.push(
'You have a `svelte` field in your `package.json`, but no root export in your `exports`. Please align them so that bundlers will resolve consistently to the same file.'
);
} else {
const { exports } = traverse_exports({ '.': root_export });
if (![...exports].map(export_to_regex).some((_export) => _export.test(pkg.svelte))) {
warnings.push(
'The `svelte` field in your `package.json` does not match any export in your root `exports`. Please align them so that bundlers will resolve consistently to the same file.'
);
Object.keys(pkg.exports).map(export_to_regex);
}
}
}
} else {
warnings.push(
'No `exports` field found in `package.json`, please provide one. ' +
'See https://svelte.dev/docs/kit/packaging#anatomy-of-a-package-json-exports for more info'
);
}
return warnings;
}
return {
analyse_code,
validate
};
}
/**
* @param {Record<string, any>} exports_map
* @returns {{ exports: Set<string>; conditions: Set<string> }}
*/
function traverse_exports(exports_map) {
/** @type {Set<string>} */
const exports = new Set();
/** @type {Set<string>} */
const conditions = new Set();
/**
* @param {Record<string, any>} exports_map
* @param {boolean} is_first_level
*/
function traverse(exports_map, is_first_level) {
for (const key of Object.keys(exports_map ?? {})) {
if (!is_first_level) {
conditions.add(key);
}
const _export = exports_map[key];
if (typeof _export === 'string') {
exports.add(_export);
} else {
traverse(_export, false);
}
}
}
traverse(exports_map, true);
return { exports, conditions };
}
/** @param {string} _export */
function export_to_regex(_export) {
// $& means the whole matched string
const regex_str = _export.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
return new RegExp(`^${regex_str}$`);
}