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

breaking: update svelte-package #8922

Merged
merged 44 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
55524fc
remove config.package, dont generate package.json
Rich-Harris Feb 7, 2023
0915da2
remove automatic package generation
Rich-Harris Feb 7, 2023
c52b2c2
oops
Rich-Harris Feb 7, 2023
47bd6b0
migration
dummdidumm Feb 7, 2023
ac82d25
docs
dummdidumm Feb 7, 2023
a6c16dc
create-svelte
dummdidumm Feb 7, 2023
3628c4f
add files migration, more robust outdir migration
dummdidumm Feb 7, 2023
5455622
changelog
dummdidumm Feb 7, 2023
5f6a616
get some tests passing
dummdidumm Feb 7, 2023
66cf5c2
fix $lib path creation
dummdidumm Feb 7, 2023
7193530
special case
Rich-Harris Feb 8, 2023
05aec7b
Update documentation/docs/30-advanced/70-packaging.md
Rich-Harris Feb 8, 2023
fe4f9f7
Update documentation/docs/30-advanced/70-packaging.md
Rich-Harris Feb 8, 2023
05b549c
Update documentation/docs/30-advanced/70-packaging.md
Rich-Harris Feb 8, 2023
5cec3e1
Update documentation/docs/30-advanced/70-packaging.md
Rich-Harris Feb 8, 2023
da8be8f
doc tweaks
dummdidumm Feb 9, 2023
b1afaf7
migrate to export conditions
dummdidumm Feb 9, 2023
03c6906
add warning log before prompt
dummdidumm Feb 9, 2023
86ab0c9
remove test
dummdidumm Feb 9, 2023
3f45335
fix types
dummdidumm Feb 9, 2023
9c443f2
Update documentation/docs/30-advanced/70-packaging.md
dummdidumm Feb 9, 2023
833c546
update link
dummdidumm Feb 9, 2023
4b2aa81
bump package version during migration
dummdidumm Feb 9, 2023
609b49a
fix package.json template
dummdidumm Feb 9, 2023
d12f7e1
fix link
dummdidumm Feb 9, 2023
698bf2e
add validation
dummdidumm Feb 9, 2023
a6fe310
tabs
Rich-Harris Feb 9, 2023
0abd600
fix
Rich-Harris Feb 9, 2023
b33c555
Update packages/package/src/validate.js
Rich-Harris Feb 9, 2023
b579f15
Update documentation/docs/30-advanced/70-packaging.md
Rich-Harris Feb 9, 2023
f108a2b
combine
Rich-Harris Feb 9, 2023
b0a605e
Update documentation/docs/30-advanced/70-packaging.md
dummdidumm Feb 10, 2023
45d89a6
Update packages/create-svelte/templates/skeletonlib/package.template.…
dummdidumm Feb 10, 2023
2a8c832
nested conditions
dummdidumm Feb 10, 2023
00502ec
use publint, add svelte field match check
dummdidumm Feb 10, 2023
2c668c6
Update documentation/docs/30-advanced/70-packaging.md
Rich-Harris Feb 10, 2023
d13f1d9
Update documentation/docs/30-advanced/70-packaging.md
Rich-Harris Feb 10, 2023
6fa4730
Update documentation/docs/30-advanced/70-packaging.md
Rich-Harris Feb 10, 2023
d04620c
Update documentation/docs/30-advanced/70-packaging.md
Rich-Harris Feb 10, 2023
d7ddb4c
Update packages/create-svelte/templates/skeletonlib/package.template.…
dummdidumm Feb 13, 2023
9263309
package.json
dummdidumm Feb 13, 2023
92488f4
docs on exports
dummdidumm Feb 15, 2023
2b09af6
update docs
Rich-Harris Feb 16, 2023
add1d89
remove default export condition
Rich-Harris Feb 16, 2023
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/clever-pianos-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte-migrate': minor
---

feat: add `@sveltejs/package` migration (v1->v2)
5 changes: 5 additions & 0 deletions .changeset/few-donuts-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-svelte': major
---

breaking: update library scaffolding for `@sveltejs/package` version 2
5 changes: 5 additions & 0 deletions .changeset/plenty-points-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/package': major
---

breaking: remove `package.json` generation and package options from `svelte.config.js`. New default output directory is `dist`. Read the migration guide at https://github.com/sveltejs/kit/pull/8922 to learn how to update
124 changes: 106 additions & 18 deletions documentation/docs/30-advanced/70-packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,104 @@
title: Packaging
---

> `svelte-package` is currently experimental. Non-backward compatible changes may occur in any future release.

You can use SvelteKit to build apps as well as component libraries, using the `@sveltejs/package` package (`npm create svelte` has an option to set this up for you).

When you're creating an app, the contents of `src/routes` is the public-facing stuff; [`src/lib`](modules#$lib) contains your app's internal library.

A component library has the exact same structure as a SvelteKit app, except that `src/lib` is the public-facing bit. `src/routes` might be a documentation or demo site that accompanies the library, or it might just be a sandbox you use during development.
A component library has the exact same structure as a SvelteKit app, except that `src/lib` is the public-facing bit, and your root `package.json` is used to publish the package. `src/routes` might be a documentation or demo site that accompanies the library, or it might just be a sandbox you use during development.

Running the `svelte-package` command from `@sveltejs/package` will take the contents of `src/lib` and generate a `dist` directory (which can be [configured](#options)) containing the following:

- All the files in `src/lib`. Svelte components will be preprocessed, TypeScript files will be transpiled to JavaScript.
- Type definitions (`d.ts` files) which are generated for Svelte, JavaScript and TypeScript files. You need to install `typescript >= 4.0.0` for this. Type definitions are placed next to their implementation, hand-written `d.ts` files are copied over as is. You can [disable generation](#options), but we strongly recommend against it — people using your library might use TypeScript, for which they require these type definition files.

> `@sveltejs/package` version 1 also copied over a generated `package.json`. This is no longer the case. If you're still on version 1, see [this PR](https://github.com/sveltejs/kit/pull/8922) for migration instructions.
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved

## Anatomy of a package.json

Since you're now building a library for public use, the contents of your `package.json` will become more important. Through it, you configure the entry points of your package, which files are published to npm, and which dependencies your library has. Let's go through the most important fields one by one.

### name

This is the name of your package. It will be available for others to install using that name, and visible on `https://npmjs.com/package/<name>`.

```json
{
"name": "your-library"
}
```

Read more about it [here](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#name).

### license

Every package should have a license field so people know how they are allowed to use it. A very popular license which is also very permissive in terms of distribution and reuse without warranty is `MIT`.

```json
{
"license": "MIT"
}
```

Read more about it [here](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#license). Note that you should also include a `LICENSE` file in your package.
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved

### files

This tells npm which files it will pack up and upload to npm. It should contain your output folder (`dist` by default). Your `package.json` and `README` and `LICENSE` will always be included, so you don't need to specify them.

Running the `svelte-package` command from `@sveltejs/package` will take the contents of `src/lib` and generate a `package` directory (which can be [configured](configuration)) containing the following:
```json
{
"files": ["dist"]
}
```

To exclude unnecessary files (such as unit tests, or modules that are only imported from `src/routes` etc) you can add them to an `.npmignore` file. This will result in smaller packages that are faster to install.

Read more about it [here](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#files).
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved

- All the files in `src/lib`, unless you [configure](configuration) custom `include`/`exclude` options. Svelte components will be preprocessed, TypeScript files will be transpiled to JavaScript.
- Type definitions (`d.ts` files) which are generated for Svelte, JavaScript and TypeScript files. You need to install `typescript >= 4.0.0` for this. Type definitions are placed next to their implementation, hand-written `d.ts` files are copied over as is. You can [disable generation](configuration), but we strongly recommend against it — people using your library might use TypeScript, for which they require these type definition files.
- A `package.json` copied from the project root with all fields except `"scripts"`, `"publishConfig.directory"` and `"publishConfig.linkDirectory"`. The `"dependencies"` field is included, which means you should add packages that you only need for your documentation or demo site to `"devDependencies"`. A `"type": "module"` and an `"exports"` field will be added if it's not defined in the original file.
### exports

The `"exports"` field contains the package's entry points. By default, all files in `src/lib` will be treated as an entry point unless they start with (or live in a directory that starts with) an underscore, but you can [configure](configuration) this behaviour. If you have a `src/lib/index.js` or `src/lib/index.svelte` file, it will be treated as the package root.
The `"exports"` field contains the package's entry points. If you set up a new library project through `npm create svelte@latest`, it's set to a single export, the package root:

```json
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
```

For example, if you had a `src/lib/Foo.svelte` component and a `src/lib/index.js` module that re-exported it, a consumer of your library could do either of the following:
This tells bundlers and tooling that your package only has one entry point, the root, and everything should be imported through that, like this:
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved

```js
// @filename: ambient.d.ts
declare module 'your-library';
import { Something } from 'your-library';
```

// @filename: index.js
// ---cut---
import { Foo } from 'your-library';
The `types`/`svelte`/`default` keys are so-called export conditions. They tell tooling what file to import when they look up the `your-library` import:
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
- TypeScript sees the `types` condition and looks up the type definition file
- Svelte-aware tooling knows this is a Svelte component library. `vite-plugin-svelte` then knows it needs to ensure that Vite does not attempt to prebundle the library (which causes errors)
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
- other tooling falls back to the `default` export.

You can adjust `exports` to your liking and provide more entry points. For example, if instead of a `src/lib/index.js` file that re-exported components you wanted to expose a `src/lib/Foo.svelte` component directly, you could create the following export map...

```json
{
"exports": {
"./Foo.svelte": {
"types": "./dist/Foo.svelte.d.ts",
"svelte": "./dist/Foo.svelte",
"default": "./dist/Foo.svelte"
}
}
}
```

...and a consumer of your library could import the component like so:

```js
// @filename: ambient.d.ts
declare module 'your-library/Foo.svelte';
Expand All @@ -38,24 +109,41 @@ declare module 'your-library/Foo.svelte';
import Foo from 'your-library/Foo.svelte';
```

> 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.meta.env.SSR`](https://vitejs.dev/guide/env-and-mode.html#env-variables) to make the library available to all Vite-based projects or better yet use [Node conditional exports](https://nodejs.org/api/packages.html#conditional-exports) to make it work for all bundlers. 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.
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 an export conditions map which in turn contain these file paths.
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved

Read more about `exports` [here](https://nodejs.org/docs/latest-v18.x/api/packages.html#package-entry-points).

### svelte

This is a legacy field that was used to signal tooling that it is dealing with a Svelte component library. It's no longer necessary when using the [Svelte export condition](#exports), but for backwards compatibility with outdated tooling that doesn't know about export conditions yet it's good to keep it around. It should point towards your root entry point.

```json
{
"svelte": "./dist/index.js"
}
```

## Best practices
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved

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.
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved

## Options

`svelte-package` accepts the following options:

- `-w`/`--watch` — watch files in `src/lib` for changes and rebuild the package
- `-i`/`--input` — the input directory which contains all the files of the package. Defaults to `src/lib`
- `-o`/`--o` — the output directory where the processed files are written to. You `package.json`'s `exports` should point to files inside there, and the `files` array should include that folder. Defaults to `dist`
- `-t`/`--types` — whether or not to create type definitions (`d.ts` files). We strongly recommend doing this as it fosters ecosystem library quality. Defaults to `true`

## Publishing

To publish the generated package:

```sh
npm publish ./package
npm publish
```

The `./package` above is referring to the directory name generated, change accordingly if you configure a custom [`package.dir`](configuration).

## Caveats

All relative file imports need to be fully specified, adhering to Node's ESM algorithm. This means you cannot import the file `src/lib/something/index.js` like `import { something } from './something`, instead you need to import it like this: `import { something } from './something/index.js`. If you are using TypeScript, you need to import `.ts` files the same way, but using a `.js` file ending, _not_ a `.ts` file ending (this isn't under our control, the TypeScript team has made that decision). Setting `"moduleResolution": "NodeNext"` in your `tsconfig.json` or `jsconfig.json` will help you with this.
Expand Down
2 changes: 1 addition & 1 deletion packages/create-svelte/templates/skeletonlib/.meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "Library skeleton project",
"title": "Library project",
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
"description": "Barebones scaffolding for your new Svelte library"
}
12 changes: 10 additions & 2 deletions packages/create-svelte/templates/skeletonlib/package.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"build": "svelte-kit sync && svelte-package",
"prepublishOnly": "echo 'Did you mean to publish `./package/`, instead of `./`?' && exit 1"
"build": "vite build && npm run package",
"package": "svelte-kit sync && svelte-package"
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
},
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
},
"peerDependencies": {
"svelte": "^3.54.0"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
Expand All @@ -15,5 +22,6 @@
"typescript": "^4.9.3",
"vite": "^4.0.0"
},
"svelte": "./dist/index.js",
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
"type": "module"
}
80 changes: 80 additions & 0 deletions packages/migrate/migrations/package/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import fs from 'fs';
import colors from 'kleur';
import path from 'path';
import prompts from 'prompts';
import { pathToFileURL } from 'url';
import { bail, check_git } from '../../utils.js';
import { migrate_config } from './migrate_config.js';
import { migrate_pkg } from './migrate_pkg.js';

export async function migrate() {
if (!fs.existsSync('svelte.config.js')) {
bail('Please re-run this script in a directory with a svelte.config.js');
}
if (!fs.existsSync('package.json')) {
bail('Please re-run this script in a directory with a package.json');
}

console.log(
colors
.bold()
.yellow(
'\nThis will update your svelte.config.js and package.json in the current directory\n'
)
);

const use_git = check_git();

const response = await prompts({
type: 'confirm',
name: 'value',
message: 'Continue?',
initial: false
});
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved

if (!response.value) {
process.exit(1);
}

const { default: config } = await import(pathToFileURL(path.resolve('svelte.config.js')).href);
const has_package_config = !!config.package;

config.package = {
source: path.resolve(config.kit?.files?.lib ?? config.package?.source ?? 'src/lib'),
dir: config.package?.dir ?? 'package',
exports:
config.package?.exports ??
((/** @type {string} */ filepath) => !/^_|\/_|\.d\.ts$/.test(filepath)),
files: config.package?.files ?? (() => true),
emitTypes: config.package?.emitTypes ?? true
};
config.extensions = config.extensions ?? ['.svelte'];

migrate_pkg(config);

if (has_package_config) {
migrate_config();
}

console.log(colors.bold().green('✔ Your project has been migrated'));

console.log('\nRecommended next steps:\n');

const cyan = colors.bold().cyan;

const tasks = [
use_git && cyan('git commit -m "migration to @sveltejs/package v2"'),
`Review the migration guide at https://github.com/sveltejs/kit/pull/8922`,
`Read the updated docs at https://kit.svelte.dev/docs/packaging`
].filter(Boolean);

tasks.forEach((task, i) => {
console.log(` ${i + 1}: ${task}`);
});

console.log('');

if (use_git) {
console.log(`Run ${cyan('git diff')} to review changes.\n`);
}
}
81 changes: 81 additions & 0 deletions packages/migrate/migrations/package/migrate_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import fs from 'fs';
import colors from 'kleur';
import MagicString from 'magic-string';
import ts from 'typescript';

export function migrate_config() {
try {
const content = fs.readFileSync('svelte.config.js', 'utf8');
fs.writeFileSync('svelte.config.js', remove_package_from_config(content));
} catch {
console.log(
colors
.bold()
.yellow('Could not remove package config from svelte.config.js, please remove it manually')
);
}
}

/**
* @param {string} content
*/
export function remove_package_from_config(content) {
const ast = ts.createSourceFile(
'filename.ts',
content,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS
);

const code = new MagicString(content);

for (const statement of ast.statements) {
if (ts.isExportAssignment(statement)) {
if (ts.isObjectLiteralExpression(statement.expression)) {
remove(statement.expression);
} else if (ts.isIdentifier(statement.expression)) {
for (const statement2 of ast.statements) {
if (ts.isVariableStatement(statement2)) {
for (const declaration of statement2.declarationList.declarations) {
if (
ts.isIdentifier(declaration.name) &&
declaration.name.text === statement.expression.text &&
declaration.initializer &&
ts.isObjectLiteralExpression(declaration.initializer)
) {
remove(declaration.initializer);
}
}
}
}
}
}
}

return code.toString();

/** @param {ts.ObjectLiteralExpression} expression */
function remove(expression) {
for (let i = 0; i < expression.properties.length; i++) {
const property = expression.properties[i];
if (
ts.isPropertyAssignment(property) &&
ts.isIdentifier(property.name) &&
property.name.text === 'package' &&
ts.isObjectLiteralExpression(property.initializer)
) {
if (expression.properties.length === 1) {
code.overwrite(expression.getStart(), expression.getEnd(), '{}');
} else {
const next_property = expression.properties[i + 1];
if (next_property) {
code.remove(property.getStart(), next_property.getStart());
} else {
code.remove(property.getStart(), content.lastIndexOf('}', expression.getEnd()));
}
}
}
}
}
}
Loading