Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: log warning if svelte field causes difference in resolve #510

Merged
merged 17 commits into from
Apr 21, 2023
Merged
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/cold-horses-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/vite-plugin-svelte': minor
---

log warnings for packages that use the `svelte` field to resolve Svelte files differently than standard Vite resolve
7 changes: 7 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,3 +403,10 @@ export default {
rawWarnings: Warning[]; // raw compiler output
};
```

### disableSvelteResolveWarnings

- **Type** `boolean`
- **Default:** `false`

disable svelte resolve warnings. Note: this is highly discouraged and you should instead fix these packages which will break in the future.
31 changes: 31 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,34 @@ module.exports = {
+ }
}
```

<!-- the following header generates an anchor that is used in logging, do not modify!-->

### conflicts in svelte resolve

| If you see a warning logged for this when using a Svelte library, please tell the library maintainers.

In the past, Svelte recommended using the custom `svelte` field in `package.json` to allow libraries to point at `.svelte` source files.
This field requires a custom implementation to resolve, so you have to use a bundler plugin and this plugin needs to implement resolving.
Since then, Node has added support for [conditional exports](https://nodejs.org/api/packages.html#conditional-exports), which have more generic support in bundlers and Node itself. So to increase the compatibility with the wider ecosystem and reduce the implementation needs for current and future bundler plugins, it is recommended that packages use the `svelte` exports condition.

Example:

```diff
// package.json
"files": ["dist"],
"svelte": "dist/index.js",
+ "exports": {
+ ".": {
+ "svelte": "./dist/index.js"
+ }
}
```

You can also add individual exports of .svelte files in the exports map which gives users a choice to also use deep imports.
See the faq about [vite and prebundling](#what-is-going-on-with-vite-and-pre-bundling-dependencies) why they can be useful at times.

> Library authors are highly encouraged to update their packages to add the new exports condition as outlined above. Check out
> [svelte-package](https://kit.svelte.dev/docs/packaging) which already supports it.
>
> For backwards compatibility, you can keep the `svelte` field in addition to the `exports` condition. But make sure that both always resolve to the same files.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Dependency } from './src/components/Dependency.svelte';
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
"type": "module",
"files": [
"src",
"index.js"
"index.js",
"index.svelte.js"
],
"svelte": "./index.svelte.js",
"exports": {
"./*": {
"svelte": "./src/components/*"
Expand Down
6 changes: 5 additions & 1 deletion packages/e2e-tests/resolve-exports-svelte/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ import { svelte } from '@sveltejs/vite-plugin-svelte';

// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte({ compilerOptions: { css: 'none' } })]
plugins: [
svelte({
compilerOptions: { css: 'none' }
})
]
});
46 changes: 41 additions & 5 deletions packages/vite-plugin-svelte/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { saveSvelteMetadata } from './utils/optimizer';
import { svelteInspector } from './ui/inspector/plugin';
import { VitePluginSvelteCache } from './utils/vite-plugin-svelte-cache';
import { loadRaw } from './utils/load-raw';
import { FAQ_LINK_CONFLICTS_IN_SVELTE_RESOLVE } from './utils/constants';

interface PluginAPI {
/**
Expand All @@ -50,6 +51,7 @@ export function svelte(inlineOptions?: Partial<Options>): Plugin[] {
/* eslint-enable no-unused-vars */

let resolvedSvelteSSR: Promise<PartialResolvedId | null>;
let packagesWithResolveWarnings: Set<string>;
const api: PluginAPI = {};
const plugins: Plugin[] = [
{
Expand All @@ -73,7 +75,7 @@ export function svelte(inlineOptions?: Partial<Options>): Plugin[] {
},

async configResolved(config) {
options = resolveOptions(options, config);
options = resolveOptions(options, config, cache);
patchResolvedViteConfig(config, options);
requestParser = buildIdParser(options);
compileSvelte = createCompileSvelte(options);
Expand All @@ -84,6 +86,7 @@ export function svelte(inlineOptions?: Partial<Options>): Plugin[] {
},

async buildStart() {
packagesWithResolveWarnings = new Set<string>();
if (!options.prebundleSvelteLibraries) return;
const isSvelteMetadataChanged = await saveSvelteMetadata(viteConfig.cacheDir, options);
if (isSvelteMetadataChanged) {
Expand Down Expand Up @@ -164,13 +167,36 @@ export function svelte(inlineOptions?: Partial<Options>): Plugin[] {
// for ssr, during scanning and non-prebundled, we do it
if (ssr || scan || !isPrebundled) {
try {
const isFirstResolve = !cache.hasResolvedSvelteField(importee, importer);
const resolved = await resolveViaPackageJsonSvelte(importee, importer, cache);
if (resolved) {
log.debug(
`resolveId resolved ${resolved} via package.json svelte field of ${importee}`
if (isFirstResolve && resolved) {
const packageInfo = await cache.getPackageInfo(resolved);
const packageVersion = `${packageInfo.name}@${packageInfo.version}`;
log.debug.once(
`resolveId resolved ${importee} to ${resolved} via package.json svelte field of ${packageVersion}`
);
return resolved;

try {
const viteResolved = (
await this.resolve(importee, importer, { ...opts, skipSelf: true })
)?.id;
if (resolved !== viteResolved) {
packagesWithResolveWarnings.add(packageVersion);
log.debug.enabled &&
log.debug.once(
`resolve difference for ${packageVersion} ${importee} - svelte: "${resolved}", vite: "${viteResolved}"`
);
}
} catch (e) {
packagesWithResolveWarnings.add(packageVersion);
log.debug.enabled &&
log.debug.once(
`resolve error for ${packageVersion} ${importee} - svelte: "${resolved}", vite: ERROR`,
e
);
}
}
return resolved;
} catch (e) {
log.debug.once(
`error trying to resolve ${importee} from ${importer} via package.json svelte field `,
Expand Down Expand Up @@ -224,6 +250,16 @@ export function svelte(inlineOptions?: Partial<Options>): Plugin[] {
},
async buildEnd() {
await options.stats?.finishAll();
if (
!options.experimental?.disableSvelteResolveWarnings &&
packagesWithResolveWarnings?.size > 0
) {
log.warn(
`WARNING: The following packages use a svelte resolve configuration in package.json that has conflicting results and is going to cause problems future.\n\n${[
...packagesWithResolveWarnings
].join('\n')}\n\nPlease see ${FAQ_LINK_CONFLICTS_IN_SVELTE_RESOLVE} for details.`
);
}
}
}
];
Expand Down
3 changes: 3 additions & 0 deletions packages/vite-plugin-svelte/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ export const SVELTE_HMR_IMPORTS = [
];

export const SVELTE_EXPORT_CONDITIONS = ['svelte'];

export const FAQ_LINK_CONFLICTS_IN_SVELTE_RESOLVE =
'https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#conflicts-in-svelte-resolve';
2 changes: 1 addition & 1 deletion packages/vite-plugin-svelte/src/utils/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function createLogger(level: string): LogFn {
const logFn: LogFn = _log.bind(null, logger) as LogFn;
const logged = new Set<String>();
const once = function (message: string, payload?: any, namespace?: string) {
if (logged.has(message)) {
if (!logger.enabled || logged.has(message)) {
return;
}
logged.add(message);
Expand Down
13 changes: 11 additions & 2 deletions packages/vite-plugin-svelte/src/utils/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {

import { isCommonDepWithoutSvelteField } from './dependencies';
import { VitePluginSvelteStats } from './vite-plugin-svelte-stats';
import { VitePluginSvelteCache } from './vite-plugin-svelte-cache';

const allowedPluginOptions = new Set([
'include',
Expand Down Expand Up @@ -178,7 +179,8 @@ function mergeConfigs<T>(...configs: (Partial<T> | undefined)[]): T {
// also validates the final config.
export function resolveOptions(
preResolveOptions: PreResolvedOptions,
viteConfig: ResolvedConfig
viteConfig: ResolvedConfig,
cache: VitePluginSvelteCache
): ResolvedOptions {
const css = preResolveOptions.emitCss ? 'external' : 'injected';
const defaultOptions: Partial<Options> = {
Expand Down Expand Up @@ -207,7 +209,7 @@ export function resolveOptions(
enforceOptionsForProduction(merged);
// mergeConfigs would mangle functions on the stats class, so do this afterwards
if (log.debug.enabled && isDebugNamespaceEnabled('stats')) {
merged.stats = new VitePluginSvelteStats();
merged.stats = new VitePluginSvelteStats(cache);
}
return merged;
}
Expand Down Expand Up @@ -721,6 +723,13 @@ export interface ExperimentalOptions {
*
*/
sendWarningsToBrowser?: boolean;

/**
* disable svelte field resolve warnings
*
* @default false
*/
disableSvelteResolveWarnings?: boolean;
}

export interface InspectorOptions {
Expand Down
55 changes: 55 additions & 0 deletions packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-cache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { SvelteRequest } from './id';
import { Code, CompileData } from './compile';
import { readFileSync } from 'fs';
import { dirname } from 'path';
//eslint-disable-next-line node/no-missing-import
import { findClosestPkgJsonPath } from 'vitefu';
import { normalizePath } from 'vite';

interface PackageInfo {
name: string;
version: string;
svelte?: string;
path: string;
}

export class VitePluginSvelteCache {
private _css = new Map<string, Code>();
Expand All @@ -8,6 +20,7 @@ export class VitePluginSvelteCache {
private _dependants = new Map<string, Set<string>>();
private _resolvedSvelteFields = new Map<string, string>();
private _errors = new Map<string, any>();
private _packageInfos: PackageInfo[] = [];

public update(compileData: CompileData) {
this._errors.delete(compileData.normalizedFilename);
Expand Down Expand Up @@ -110,6 +123,9 @@ export class VitePluginSvelteCache {
return this._resolvedSvelteFields.get(this._getResolvedSvelteFieldKey(name, importer));
}

public hasResolvedSvelteField(name: string, importer?: string) {
return this._resolvedSvelteFields.has(this._getResolvedSvelteFieldKey(name, importer));
}
public setResolvedSvelteField(
importee: string,
importer: string | undefined = undefined,
Expand All @@ -124,4 +140,43 @@ export class VitePluginSvelteCache {
private _getResolvedSvelteFieldKey(importee: string, importer?: string): string {
return importer ? `${importer} > ${importee}` : importee;
}

public async getPackageInfo(file: string): Promise<PackageInfo> {
let info = this._packageInfos.find((pi) => file.startsWith(pi.path));
if (!info) {
info = await findPackageInfo(file);
this._packageInfos.push(info);
}
return info;
}
}

/**
* utility to get some info from the closest package.json with a "name" set
*
* @param {string} file to find info for
* @returns {PackageInfo}
*/
async function findPackageInfo(file: string): Promise<PackageInfo> {
const info: PackageInfo = {
name: '$unknown',
version: '0.0.0-unknown',
path: '$unknown'
};
let path = await findClosestPkgJsonPath(file, (pkgPath) => {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
if (pkg.name != null) {
info.name = pkg.name;
if (pkg.version != null) {
info.version = pkg.version;
}
info.svelte = pkg.svelte;
return true;
}
return false;
});
// return normalized path with appended '/' so .startsWith works for future file checks
path = normalizePath(dirname(path ?? file)) + '/';
info.path = path;
return info;
}
38 changes: 6 additions & 32 deletions packages/vite-plugin-svelte/src/utils/vite-plugin-svelte-stats.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { log } from './log';
//eslint-disable-next-line node/no-missing-import
import { findClosestPkgJsonPath } from 'vitefu';
import { readFileSync } from 'fs';
import { dirname } from 'path';
import { performance } from 'perf_hooks';
import { normalizePath } from 'vite';
import { VitePluginSvelteCache } from './vite-plugin-svelte-cache';

interface Stat {
file: string;
Expand Down Expand Up @@ -87,31 +84,13 @@ function formatPackageStats(pkgStats: PackageStats[]) {
return table;
}

/**
* utility to get the package name a file belongs to
*
* @param {string} file to find package for
* @returns {path:string,name:string} tuple of path,name where name is the parsed package name and path is the normalized path to it
*/
async function getClosestNamedPackage(file: string): Promise<{ name: string; path: string }> {
let name = '$unknown';
let path = await findClosestPkgJsonPath(file, (pkgPath) => {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
if (pkg.name != null) {
name = pkg.name;
return true;
}
return false;
});
// return normalized path with appended '/' so .startsWith works for future file checks
path = normalizePath(dirname(path ?? file)) + '/';
return { name, path };
}

export class VitePluginSvelteStats {
// package directory -> package name
private _packages: { path: string; name: string }[] = [];
private _cache: VitePluginSvelteCache;
private _collections: StatCollection[] = [];
constructor(cache: VitePluginSvelteCache) {
this._cache = cache;
}
startCollection(name: string, opts?: Partial<CollectionOptions>) {
const options = {
...defaultCollectionOptions,
Expand Down Expand Up @@ -190,12 +169,7 @@ export class VitePluginSvelteStats {
private async _aggregateStatsResult(collection: StatCollection) {
const stats = collection.stats;
for (const stat of stats) {
let pkg = this._packages.find((p) => stat.file.startsWith(p.path));
if (!pkg) {
pkg = await getClosestNamedPackage(stat.file);
this._packages.push(pkg);
}
stat.pkg = pkg.name;
stat.pkg = (await this._cache.getPackageInfo(stat.file)).name;
}

// group stats
Expand Down