Repository: sveltejs/kit (js)

Issue:
Imports broken with library updated to svelte-package v2
### Describe the bug

I updated a lib to use svelte-package v2.

Previously I was using the lib like this:
```
<script lang="ts">
  import { Button } from "kit-issue-9114";
</script>

<Button>Example</Button>
```

After updating I get an error from TS that the module does not exist.
I found that if I add the output directory to the import, then it works:

Example:
```
<script lang="ts">
  import { Button } from "kit-issue-9114/package";
</script>

<Button>Example</Button>
```

I'm not sure where the issue is here, I used the migration script and have gone through [this doc](https://kit.svelte.dev/docs/packaging).

### Reproduction

https://github.com/seanlail/kit-issue-9114

### Logs

```Shell
Cannot find module 'kit-issue-9114' or its corresponding type declarations.ts
```


### System Info

```Shell
System:
    OS: macOS 13.1
    CPU: (12) x64 Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
    Memory: 3.45 GB / 16.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 18.14.0 - ~/.nvm/versions/node/v18.14.0/bin/node
    npm: 9.3.1 - ~/.nvm/versions/node/v18.14.0/bin/npm
  Browsers:
    Chrome: 110.0.5481.100
    Firefox: 99.0.1
    Safari: 16.2
  npmPackages:
    @sveltejs/adapter-static: 2.0.1 => 2.0.1 
    @sveltejs/kit: 1.7.2 => 1.7.2 
    @sveltejs/package: 2.0.1 => 2.0.1 
    svelte: 3.55.1 => 3.55.1 
    vite: 4.1.2 => 4.1.2
```


### Severity

serious, but I can work around it

### Additional Information

_No response_

Pull request:
fix: use typesVersions to wire up deep imports
and explain that stuff in the docs
closes #9114
closes #9115 

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it solves.
- [x] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. Changesets that add features should be `minor` and those that fix bugs should be `patch`. Please prefix changeset messages with `feat:`, `fix:`, or `chore:`.


Patch:
diff --git a/.changeset/fluffy-trees-rule.md b/.changeset/fluffy-trees-rule.md
new file mode 100644
index 000000000..59482c609
--- /dev/null
+++ b/.changeset/fluffy-trees-rule.md
@@ -0,0 +1,5 @@
+---
+'svelte-migrate': patch
+---
+
+fix: update existing exports with prepended outdir
diff --git a/.changeset/friendly-pandas-sort.md b/.changeset/friendly-pandas-sort.md
new file mode 100644
index 000000000..53017424f
--- /dev/null
+++ b/.changeset/friendly-pandas-sort.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/package': patch
+---
+
+fix: don't emit false positive export condition warning
diff --git a/.changeset/moody-donkeys-end.md b/.changeset/moody-donkeys-end.md
new file mode 100644
index 000000000..fd622f8a3
--- /dev/null
+++ b/.changeset/moody-donkeys-end.md
@@ -0,0 +1,5 @@
+---
+'svelte-migrate': patch
+---
+
+fix: use typesVersions to wire up deep imports
diff --git a/documentation/docs/30-advanced/70-packaging.md b/documentation/docs/30-advanced/70-packaging.md
index f2dc9bf66..d69f9b4ff 100644
--- a/documentation/docs/30-advanced/70-packaging.md
+++ b/documentation/docs/30-advanced/70-packaging.md
@@ -110,6 +110,8 @@ declare module 'your-library/Foo.svelte';
 import Foo from 'your-library/Foo.svelte';
 ```
 
+> Beware that doing this will need additional care if you provide type definitions. Read more about the caveat [here](#typescript)
+
 In general, each key of the exports map is the path the user will have to use to import something from your package, and the value is the path to the file that will be imported or a map of export conditions which in turn contains these file paths.
 
 Read more about `exports` [here](https://nodejs.org/docs/latest-v18.x/api/packages.html#package-entry-points).
@@ -124,6 +126,36 @@ This is a legacy field that enabled tooling to recognise Svelte component librar
 }
 ```
 
+## TypeScript
+
+You should ship type definitions for your library even if you don't use TypeScript yourself so that people who do get proper intellisense when using your library. `@sveltejs/package` makes the process of generating types mostly opaque to you. By default, when packaging your library, type definitions are auto-generated for JavaScript, TypeScript and Svelte files. All you need to ensure is that the `types` condition in the [exports](#anatomy-of-a-package-json-exports) map points to the correct files. When initialising a library project through `npm create svelte@latest`, this is automatically setup for the root export.
+
+If you have something else than a root export however — for example providing a `your-library/foo` import — you need to take additional care for providing type definitions. Unfortunately, TypeScript by default will _not_ resolve the `types` condition for an export like `{ "./foo": { "types": "./dist/foo.d.ts", ... }}`. Instead, it will search for a `foo.d.ts` relative to the root of your library (i.e. `your-library/foo.d.ts` instead of `your-library/dist/foo.d.ts`). To fix this, you have two options:
+
+The first option is to require people using your library to set the `moduleResolution` option in their `tsconfig/jsconfig.json` to `bundler` (available since TypeScript 5, the best and recommended option in the future), `node16` or `nodenext`. This opts TypeScript into actually looking at the exports map and resolving the types correctly.
+
+The second option is to (ab)use the `typesVersions` feature from TypeScript to wire up the types. This is a field inside `package.json` TypeScript uses to check for different type definitions depending on the TypeScript version, and also contains a path mapping feature for that. We leverage that path mapping feature to get what we want. For the mentioned `foo` export above, the corresponding `typesVersions` looks like this:
+
+```json
+{
+	"exports": {
+		"./foo": {
+			"types": "./dist/foo.d.ts",
+			"svelte": "./dist/foo.js"
+		}
+	},
+	"typesVersions": {
+		">4.0": {
+			"foo": ["./dist/foo.d.ts"]
+		}
+	}
+}
+```
+
+`>4.0` tells TypeScript to check the inner map if the used TypeScript version is greater than 4 (which should in practice always be true). The inner map tells TypeScript that the typings for `your-library/foo` are found within `./dist/foo.d.ts`, which essentially replicates the `exports` condition. You also have `*` as a wildcard at your disposal to make many type definitions at once available without repeating yourself.
+
+You can read more about that feature [here](https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#version-selection-with-typesversions).
+
 ## Best practices
 
 You should avoid using [SvelteKit-specific modules](modules) like `$app` in your packages unless you intend for them to only be consumable by other SvelteKit projects. E.g. rather than using `import { browser } from '$app/environment'` you could use `import { BROWSER } from 'esm-env'` ([see esm-env docs](https://github.com/benmccann/esm-env)). You may also wish to pass in things like the current URL or a navigation action as a prop rather than relying directly on `$app/stores`, `$app/navigation`, etc. Writing your app in this more generic fashion will also make it easier to setup tools for testing, UI demos and so on.
diff --git a/packages/migrate/migrations/package/migrate_pkg.js b/packages/migrate/migrations/package/migrate_pkg.js
index 044bb71dc..954bbb363 100644
--- a/packages/migrate/migrations/package/migrate_pkg.js
+++ b/packages/migrate/migrations/package/migrate_pkg.js
@@ -92,6 +92,8 @@ export function update_pkg_json(config, pkg, files) {
 
 	/** @type {Record<string, string>} */
 	const clashes = {};
+	/** @type {Record<string, [string]>} */
+	const types_versions = {};
 
 	for (const file of files) {
 		if (file.is_included && file.is_exported) {
@@ -106,16 +108,30 @@ export function update_pkg_json(config, pkg, files) {
 				);
 			}
 
+			const has_type = config.package.emitTypes && (file.is_svelte || file.dest.endsWith('.js'));
+			const out_dir_type_path = `./${out_dir}/${
+				file.is_svelte ? `${file.dest}.d.ts` : file.dest.slice(0, -'.js'.length) + '.d.ts'
+			}`;
+
+			if (has_type && key.slice(2) /* don't add root index type */) {
+				if (!pkg.exports[key]) {
+					types_versions[key.slice(2)] = [out_dir_type_path];
+				} else {
+					const path_without_ext = pkg.exports[key].slice(
+						0,
+						-path.extname(pkg.exports[key]).length
+					);
+					types_versions[key.slice(2)] = [
+						`./${out_dir}/${(pkg.exports[key].types ?? path_without_ext + '.d.ts').slice(2)}`
+					];
+				}
+			}
+
 			if (!pkg.exports[key]) {
-				const has_type = config.package.emitTypes && (file.is_svelte || file.dest.endsWith('.js'));
 				const needs_svelte_condition = file.is_svelte || path.basename(file.dest) === 'index.js';
 				// JSON.stringify will remove the undefined entries
 				pkg.exports[key] = {
-					types: has_type
-						? `./${out_dir}/${
-								file.is_svelte ? `${file.dest}.d.ts` : file.dest.slice(0, -'.js'.length) + '.d.ts'
-						  }`
-						: undefined,
+					types: has_type ? out_dir_type_path : undefined,
 					svelte: needs_svelte_condition ? `./${out_dir}/${file.dest}` : undefined,
 					default: `./${out_dir}/${file.dest}`
 				};
@@ -123,6 +139,17 @@ export function update_pkg_json(config, pkg, files) {
 				if (Object.values(pkg.exports[key]).filter(Boolean).length === 1) {
 					pkg.exports[key] = pkg.exports[key].default;
 				}
+			} else {
+				// Rewrite existing export to point to the new output directory
+				if (typeof pkg.exports[key] === 'string') {
+					pkg.exports[key] = prepend_out_dir(pkg.exports[key], out_dir);
+				} else {
+					for (const condition in pkg.exports[key]) {
+						if (typeof pkg.exports[key][condition] === 'string') {
+							pkg.exports[key][condition] = prepend_out_dir(pkg.exports[key][condition], out_dir);
+						}
+					}
+				}
 			}
 
 			clashes[key] = original;
@@ -154,7 +181,27 @@ export function update_pkg_json(config, pkg, files) {
 				)
 			);
 		}
+	} else if (pkg.svelte) {
+		// Rewrite existing "svelte" field to point to the new output directory
+		pkg.svelte = prepend_out_dir(pkg.svelte, out_dir);
+	}
+
+	// https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#version-selection-with-typesversions
+	// A hack to get around the limitation that TS doesn't support "exports" field  with moduleResolution: 'node'
+	if (Object.keys(types_versions).length > 0) {
+		pkg.typesVersions = { '>4.0': types_versions };
 	}
 
 	return pkg;
 }
+
+/**
+ * Rewrite existing path to point to the new output directory
+ * @param {string} path
+ * @param {string} out_dir
+ */
+function prepend_out_dir(path, out_dir) {
+	if (!path.startsWith(`./${out_dir}`) && path.startsWith('./')) {
+		return `./${out_dir}/${path.slice(2)}`;
+	}
+}
diff --git a/packages/migrate/migrations/package/migrate_pkg.spec.js b/packages/migrate/migrations/package/migrate_pkg.spec.js
index 44fce618f..933234225 100644
--- a/packages/migrate/migrations/package/migrate_pkg.spec.js
+++ b/packages/migrate/migrations/package/migrate_pkg.spec.js
@@ -13,7 +13,8 @@ test('Updates package.json', () => {
 			},
 			exports: {
 				'./ignored': './something.js'
-			}
+			},
+			svelte: './index.js'
 		},
 		[
 			{
@@ -37,6 +38,13 @@ test('Updates package.json', () => {
 				is_included: true,
 				is_svelte: false
 			},
+			{
+				name: 'bar/index.js',
+				dest: 'bar/index.js',
+				is_exported: true,
+				is_included: true,
+				is_svelte: false
+			},
 			{
 				name: 'index.js',
 				dest: 'index.js',
@@ -77,9 +85,22 @@ test('Updates package.json', () => {
 				types: './package/baz.d.ts',
 				default: './package/baz.js'
 			},
-			'./ignored': './something.js'
+			'./bar': {
+				types: './package/bar/index.d.ts',
+				svelte: './package/bar/index.js',
+				default: './package/bar/index.js'
+			},
+			'./ignored': './package/something.js'
 		},
-		svelte: './package/index.js'
+		svelte: './package/index.js',
+		typesVersions: {
+			'>4.0': {
+				'foo/Bar.svelte': ['./package/foo/Bar.svelte.d.ts'],
+				baz: ['./package/baz.d.ts'],
+				bar: ['./package/bar/index.d.ts'],
+				ignored: ['./package/something.d.ts']
+			}
+		}
 	});
 });
 
diff --git a/packages/package/src/validate.js b/packages/package/src/validate.js
index 83fe0c25f..c0276aef7 100644
--- a/packages/package/src/validate.js
+++ b/packages/package/src/validate.js
@@ -1,10 +1,46 @@
-import { existsSync, readFileSync } from 'node:fs';
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
 import colors from 'kleur';
 
 /**
  * @param {import("./types").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 = JSON.parse(readFileSync(join(options.cwd, 'package.json'), 'utf-8'));
+			const warnings = validate(pkg);
+			// 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").Options} options
+ */
+export function _create_validator(options) {
 	/** @type {Set<string>} */
 	const imports = new Set();
 	let uses_import_meta = false;
@@ -32,9 +68,10 @@ export function create_validator(options) {
 		}
 	}
 
-	function validate() {
-		/** @type {Record<string, any>} */
-		const pkg = JSON.parse(readFileSync('package.json', 'utf-8'));
+	/**
+	 * @param {Record<string, any>} pkg
+	 */
+	function validate(pkg) {
 		/** @type {string[]} */
 		const warnings = [];
 
@@ -109,19 +146,7 @@ export function create_validator(options) {
 			);
 		}
 
-		// 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`));
-			}
-		}
+		return warnings;
 	}
 
 	return {
@@ -146,7 +171,7 @@ function traverse_exports(exports_map) {
 	 */
 	function traverse(exports_map, is_first_level) {
 		for (const key of Object.keys(exports_map ?? {})) {
-			if (is_first_level) {
+			if (!is_first_level) {
 				conditions.add(key);
 			}
 


Test patch:
diff --git a/packages/package/test/fixtures/assets/package.json b/packages/package/test/fixtures/assets/package.json
index f25793367..8379863a8 100644
--- a/packages/package/test/fixtures/assets/package.json
+++ b/packages/package/test/fixtures/assets/package.json
@@ -2,5 +2,14 @@
 	"name": "assets",
 	"private": true,
 	"version": "1.0.0",
-	"description": "no tampering assets"
+	"description": "no tampering assets",
+	"peerDependencies": {
+		"svelte": "^3.55.0"
+	},
+	"exports": {
+		".": {
+			"types": "./dist/index.d.ts",
+			"svelte": "./dist/index.js"
+		}
+	}
 }
diff --git a/packages/package/test/fixtures/emitTypes-false/package.json b/packages/package/test/fixtures/emitTypes-false/package.json
index ec97559d2..382c65e0a 100644
--- a/packages/package/test/fixtures/emitTypes-false/package.json
+++ b/packages/package/test/fixtures/emitTypes-false/package.json
@@ -3,5 +3,13 @@
 	"private": true,
 	"version": "1.0.0",
 	"description": "emitTypes settings disabled",
-	"type": "module"
+	"type": "module",
+	"peerDependencies": {
+		"svelte": "^3.55.0"
+	},
+	"exports": {
+		".": {
+			"svelte": "./dist/index.js"
+		}
+	}
 }
diff --git a/packages/package/test/fixtures/javascript/package.json b/packages/package/test/fixtures/javascript/package.json
index 77440f772..37f782bec 100644
--- a/packages/package/test/fixtures/javascript/package.json
+++ b/packages/package/test/fixtures/javascript/package.json
@@ -2,5 +2,14 @@
 	"name": "javascript",
 	"private": true,
 	"version": "1.0.0",
-	"description": "standard javascript package"
+	"description": "standard javascript package",
+	"peerDependencies": {
+		"svelte": "^3.55.0"
+	},
+	"exports": {
+		".": {
+			"types": "./dist/index.d.ts",
+			"svelte": "./dist/index.js"
+		}
+	}
 }
diff --git a/packages/package/test/fixtures/resolve-alias/package.json b/packages/package/test/fixtures/resolve-alias/package.json
index 19ecfdbec..461bde2f3 100644
--- a/packages/package/test/fixtures/resolve-alias/package.json
+++ b/packages/package/test/fixtures/resolve-alias/package.json
@@ -3,5 +3,14 @@
 	"private": true,
 	"version": "1.0.0",
 	"description": "package using $lib alias",
-	"type": "module"
+	"type": "module",
+	"peerDependencies": {
+		"svelte": "^3.55.0"
+	},
+	"exports": {
+		".": {
+			"types": "./dist/index.d.ts",
+			"svelte": "./dist/index.js"
+		}
+	}
 }
diff --git a/packages/package/test/fixtures/svelte-kit/package.json b/packages/package/test/fixtures/svelte-kit/package.json
index 318400b23..202bf7e46 100644
--- a/packages/package/test/fixtures/svelte-kit/package.json
+++ b/packages/package/test/fixtures/svelte-kit/package.json
@@ -3,5 +3,14 @@
 	"private": true,
 	"version": "1.0.0",
 	"type": "module",
-	"description": "SvelteKit library project"
+	"description": "SvelteKit library project",
+	"peerDependencies": {
+		"svelte": "^3.55.0"
+	},
+	"exports": {
+		".": {
+			"types": "./dist/index.d.ts",
+			"svelte": "./dist/index.js"
+		}
+	}
 }
diff --git a/packages/package/test/fixtures/typescript/package.json b/packages/package/test/fixtures/typescript/package.json
index 7e166c721..eef968835 100644
--- a/packages/package/test/fixtures/typescript/package.json
+++ b/packages/package/test/fixtures/typescript/package.json
@@ -3,5 +3,14 @@
 	"private": true,
 	"version": "1.0.0",
 	"description": "standard typescript package",
-	"type": "module"
+	"type": "module",
+	"peerDependencies": {
+		"svelte": "^3.55.0"
+	},
+	"exports": {
+		".": {
+			"types": "./dist/index.d.ts",
+			"svelte": "./dist/index.js"
+		}
+	}
 }
diff --git a/packages/package/test/index.js b/packages/package/test/index.js
index a0ab8b4b3..caa98282b 100644
--- a/packages/package/test/index.js
+++ b/packages/package/test/index.js
@@ -9,6 +9,7 @@ import * as assert from 'uvu/assert';
 import { build, watch } from '../src/index.js';
 import { load_config } from '../src/config.js';
 import { rimraf, walk } from '../src/filesystem.js';
+import { _create_validator } from '../src/validate.js';
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = join(__filename, '..');
@@ -214,4 +215,89 @@ if (!process.env.CI) {
 	});
 }
 
+/**
+ * @param {string[]} actual
+ * @param {string[]} expected
+ */
+function has_warnings(actual, expected) {
+	assert.equal(actual.length, expected.length);
+	assert.equal(
+		actual.filter((warning) => expected.some((str) => warning.startsWith(str))).length,
+		expected.length
+	);
+}
+
+test('validates package (1)', () => {
+	const { analyse_code, validate } = _create_validator({
+		config: {},
+		cwd: '',
+		input: '',
+		output: '',
+		types: true
+	});
+	analyse_code('src/lib/index.js', 'export const a = 1;import.meta.env;');
+	analyse_code('src/lib/C.svelte', '');
+	const warnings = validate({});
+
+	has_warnings(warnings, [
+		'No `exports` field found in `package.json`, please provide one.',
+		'Avoid usage of `import.meta.env` in your code',
+		'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`. '
+	]);
+});
+
+test('validates package (2)', () => {
+	const { analyse_code, validate } = _create_validator({
+		config: {},
+		cwd: '',
+		input: '',
+		output: '',
+		types: true
+	});
+	analyse_code('src/lib/C.svelte', '');
+	const warnings = validate({
+		exports: { '.': './dist/C.svelte' },
+		peerDependencies: { svelte: '^3.55.0' }
+	});
+
+	has_warnings(warnings, [
+		'You are using Svelte files, but did not declare a `svelte` condition in one of your `exports` in your `package.json`. '
+	]);
+});
+
+test('validates package (all ok 1)', () => {
+	const { analyse_code, validate } = _create_validator({
+		config: {},
+		cwd: '',
+		input: '',
+		output: '',
+		types: true
+	});
+	analyse_code('src/lib/C.svelte', '');
+	const warnings = validate({
+		exports: { '.': { svelte: './dist/C.svelte' } },
+		peerDependencies: { svelte: '^3.55.0' }
+	});
+
+	assert.equal(warnings.length, 0);
+});
+
+test('validates package (all ok 2)', () => {
+	const { analyse_code, validate } = _create_validator({
+		config: {},
+		cwd: '',
+		input: '',
+		output: '',
+		types: true
+	});
+	analyse_code('src/lib/C.svelte', '');
+	const warnings = validate({
+		exports: { '.': { svelte: './dist/C.svelte' } },
+		peerDependencies: { svelte: '^3.55.0' },
+		svelte: './dist/C.svelte'
+	});
+
+	assert.equal(warnings.length, 0);
+});
+
 test.run();
diff --git a/packages/package/test/watch/package.json b/packages/package/test/watch/package.json
index 854873b84..390dd4f4a 100644
--- a/packages/package/test/watch/package.json
+++ b/packages/package/test/watch/package.json
@@ -1,4 +1,13 @@
 {
 	"name": "watch",
-	"type": "module"
+	"type": "module",
+	"peerDependencies": {
+		"svelte": "^3.55.0"
+	},
+	"exports": {
+		".": {
+			"types": "./dist/index.d.ts",
+			"svelte": "./dist/index.js"
+		}
+	}
 }