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: add packageJson setting #8808

Closed
wants to merge 1 commit into from
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/nine-spoons-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/package': minor
---

feat: add `packageJson` setting to adjust final `package.json`
10 changes: 4 additions & 6 deletions documentation/docs/30-advanced/70-packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
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.
Expand All @@ -12,11 +10,11 @@ A component library has the exact same structure as a SvelteKit app, except that

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:

- 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.
- All the files in `src/lib`, unless you [configure](configuration) the custom `files` option. 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) by setting `emitTypes: false`, 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. You can customize the final `package.json` contents through the `packageJson` option, which is passed the original and generated `package.json`. If you return `undefined`, the `package.json` will not be written to the output directory.

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. 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 through the `packageJson` option. If you have a `src/lib/index.js` or `src/lib/index.svelte` file, it will be treated as the package root.

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:

Expand Down
15 changes: 14 additions & 1 deletion packages/package/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,26 @@ export async function load_config({ cwd = process.cwd() } = {}) {
* @returns {import('./types').ValidatedConfig}
*/
function process_config(config, { cwd = process.cwd() } = {}) {
let warned = false;

return {
extensions: config.extensions ?? ['.svelte'],
kit: config.kit,
package: {
source: path.resolve(cwd, config.kit?.files?.lib ?? config.package?.source ?? 'src/lib'),
dir: config.package?.dir ?? 'package',
exports: config.package?.exports ?? ((filepath) => !/^_|\/_|\.d\.ts$/.test(filepath)),
exports: config.package?.exports
? (filepath) => {
if (!warned) {
console.warn(
'The `package.exports` option is deprecated. Use `package.packageJson` instead.'
);
warned = true;
}
return /** @type {any} */ (config.package).exports(filepath);
}
: (filepath) => !/^_|\/_|\.d\.ts$/.test(filepath),
packageJson: config.package?.packageJson ?? ((_, pkg) => pkg),
files: config.package?.files ?? (() => true),
emitTypes: config.package?.emitTypes ?? true
},
Expand Down
62 changes: 23 additions & 39 deletions packages/package/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import colors from 'kleur';
import chokidar from 'chokidar';
import { preprocess } from 'svelte/compiler';
import { copy, mkdirp, rimraf } from './filesystem.js';
import { analyze, generate_pkg, resolve_lib_alias, scan, strip_lang_tags, write } from './utils.js';
import {
analyze,
generate_pkg,
resolve_lib_alias,
scan,
strip_lang_tags,
write,
write_if_changed
} from './utils.js';
import { emit_dts, transpile_ts } from './typescript.js';

const essential_files = ['README', 'LICENSE', 'CHANGELOG', '.gitignore', '.npmignore'];
Expand All @@ -30,39 +38,12 @@ export async function build(config, cwd = process.cwd()) {
await emit_dts(config, cwd, files);
}

const pkg = generate_pkg(cwd, files);
const { pkg, pkg_name } = generate_pkg(cwd, config.package.packageJson, files);

if (!pkg.dependencies?.svelte && !pkg.peerDependencies?.svelte) {
console.warn(
'Svelte libraries should include "svelte" in either "dependencies" or "peerDependencies".'
);
if (pkg) {
write_if_changed(join(dir, 'package.json'), JSON.stringify(pkg, null, 2));
}

if (!pkg.svelte && files.some((file) => file.is_svelte)) {
// Several heuristics in Kit/vite-plugin-svelte to tell Vite to mark Svelte packages
// rely on the "svelte" property. Vite/Rollup/Webpack plugin can all deal with it.
// See https://github.com/sveltejs/kit/issues/1959 for more info and related threads.
if (pkg.exports['.']) {
const svelte_export =
typeof pkg.exports['.'] === 'string'
? pkg.exports['.']
: pkg.exports['.'].import || pkg.exports['.'].default;
if (svelte_export) {
pkg.svelte = svelte_export;
} else {
console.warn(
'Cannot generate a "svelte" entry point because the "." entry in "exports" is not a string. If you set it by hand, please also set one of the options as a "svelte" entry point\n'
);
}
} else {
console.warn(
'Cannot generate a "svelte" entry point because the "." entry in "exports" is missing. Please specify one or set a "svelte" entry point yourself\n'
);
}
}

write(join(dir, 'package.json'), JSON.stringify(pkg, null, 2));

for (const file of files) {
await process_file(config, file);
}
Expand All @@ -83,8 +64,10 @@ export async function build(config, cwd = process.cwd()) {
const from = relative(cwd, lib);
const to = relative(cwd, dir);
console.log(colors.bold().green(`${from} -> ${to}`));
console.log(`Successfully built '${pkg.name}' package. To publish it to npm:`);
console.log(colors.bold().cyan(` cd ${to}`));
console.log(`Successfully built '${pkg_name}' package. To publish it to npm:`);
if (pkg) {
console.log(colors.bold().cyan(` cd ${to}`));
}
console.log(colors.bold().cyan(' npm publish\n'));
}

Expand All @@ -98,8 +81,7 @@ export async function watch(config, cwd = process.cwd()) {

console.log(message);

const { source: lib } = config.package;
const { dir } = config.package;
const { dir, source: lib } = config.package;

/** @type {Array<{ file: import('./types').File, type: string }>} */
const pending = [];
Expand Down Expand Up @@ -129,7 +111,7 @@ export async function watch(config, cwd = process.cwd()) {
pending.length = 0;

for (const { file, type } of events) {
if ((type === 'unlink' || type === 'add') && file.is_exported) {
if (type === 'unlink' || type === 'add') {
should_update_pkg = true;
}

Expand Down Expand Up @@ -161,9 +143,11 @@ export async function watch(config, cwd = process.cwd()) {
}

if (should_update_pkg) {
const pkg = generate_pkg(cwd, files);
write(join(dir, 'package.json'), JSON.stringify(pkg, null, 2));
console.log('Updated package.json');
const pkg = generate_pkg(cwd, config.package.packageJson, files);
const changed = write_if_changed(join(dir, 'package.json'), JSON.stringify(pkg, null, 2));
if (changed) {
console.log('Updated package.json');
}
}

if (config.package.emitTypes) {
Expand Down
53 changes: 51 additions & 2 deletions packages/package/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ export function write(file, contents) {
fs.writeFileSync(file, contents);
}

/** @type {Map<string, string>} */
let current = new Map();
/**
* @param {string} file
* @param {string} contents
*/
export function write_if_changed(file, contents) {
if (current.get(file) !== contents) {
write(file, contents);
current.set(file, contents);
return true;
}
return false;
}

/**
* @param {import('./types').ValidatedConfig} config
* @returns {import('./types').File[]}
Expand Down Expand Up @@ -106,10 +121,12 @@ export function analyze(config, file) {

/**
* @param {string} cwd
* @param {NonNullable<import('types').PackageConfig['packageJson']>} packageJson
* @param {import('./types').File[]} files
*/
export function generate_pkg(cwd, files) {
export function generate_pkg(cwd, packageJson, files) {
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
const original = JSON.parse(JSON.stringify(pkg));

// Remove fields that are specific to the original package.json
// See: https://pnpm.io/package_json#publishconfigdirectory
Expand Down Expand Up @@ -147,5 +164,37 @@ export function generate_pkg(cwd, files) {
}
}

return pkg;
if (!pkg.dependencies?.svelte && !pkg.peerDependencies?.svelte) {
console.warn(
'Svelte libraries should include "svelte" in either "dependencies" or "peerDependencies".'
);
}

if (!pkg.svelte && files.some((file) => file.is_svelte)) {
// Several heuristics in Kit/vite-plugin-svelte to tell Vite to mark Svelte packages
// rely on the "svelte" property. Vite/Rollup/Webpack plugin can all deal with it.
// See https://github.com/sveltejs/kit/issues/1959 for more info and related threads.
if (pkg.exports['.']) {
const svelte_export =
typeof pkg.exports['.'] === 'string'
? pkg.exports['.']
: pkg.exports['.'].svelte || pkg.exports['.'].import || pkg.exports['.'].default;
if (svelte_export) {
pkg.svelte = svelte_export;
} else {
console.warn(
'Cannot generate a "svelte" entry point because the "." entry in "exports" is not a string. If you set it by hand, please also set one of the options as a "svelte" entry point in your package.json\n' +
'Example: { ..., "svelte": "./index.svelte" } }\n'
);
}
} else {
console.warn(
'Cannot generate a "svelte" entry point because the "." entry in "exports" is missing. Please specify one or set a "svelte" entry point yourself in your package.json\n' +
'Example: { ..., "svelte": "./index.svelte" } }\n'
);
}
}

const final = packageJson(original, pkg);
return { pkg: packageJson(original, pkg), pkg_name: final?.name ?? original.name };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const foo: true;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const foo = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
7 changes: 7 additions & 0 deletions packages/package/test/fixtures/package-json-omit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "package-json-omit",
"private": true,
"version": "1.0.0",
"description": "omits package json",
"type": "module"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const foo = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
package: {
packageJson: () => undefined
}
};

export default config;
12 changes: 12 additions & 0 deletions packages/package/test/fixtures/package-json/expected/Test.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script>
import { createEventDispatcher } from 'svelte';
/**
* @type {string}
*/
export const astring = 'potato';

const dispatch = createEventDispatcher();
dispatch('event', true);
</script>

<slot {astring} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/** @typedef {typeof __propDef.props} TestProps */
/** @typedef {typeof __propDef.events} TestEvents */
/** @typedef {typeof __propDef.slots} TestSlots */
export default class Test extends SvelteComponentTyped<
{
astring?: string;
},
{
event: CustomEvent<any>;
} & {
[evt: string]: CustomEvent<any>;
},
{
default: {
astring: string;
};
}
> {
get astring(): string;
}
export type TestProps = typeof __propDef.props;
export type TestEvents = typeof __propDef.events;
export type TestSlots = typeof __propDef.slots;
import { SvelteComponentTyped } from 'svelte';
declare const __propDef: {
props: {
astring?: string;
};
events: {
event: CustomEvent<any>;
} & {
[evt: string]: CustomEvent<any>;
};
slots: {
default: {
astring: string;
};
};
};
export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Test } from './Test.svelte';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Test } from './Test.svelte';
13 changes: 13 additions & 0 deletions packages/package/test/fixtures/package-json/expected/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "package-json",
"private": true,
"version": "1.0.0",
"description": "uses package.json as is",
"type": "module",
"exports": {
".": {
"import": "./index.js"
}
},
"svelte": "./Test.svelte"
}
1 change: 1 addition & 0 deletions packages/package/test/fixtures/package-json/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
13 changes: 13 additions & 0 deletions packages/package/test/fixtures/package-json/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "package-json",
"private": true,
"version": "1.0.0",
"description": "uses package.json as is",
"type": "module",
"exports": {
".": {
"import": "./index.js"
}
},
"svelte": "./Test.svelte"
}
12 changes: 12 additions & 0 deletions packages/package/test/fixtures/package-json/src/lib/Test.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script>
import { createEventDispatcher } from 'svelte';
/**
* @type {string}
*/
export const astring = 'potato';

const dispatch = createEventDispatcher();
dispatch('event', true);
</script>

<slot {astring} />
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Test } from './Test.svelte';
7 changes: 7 additions & 0 deletions packages/package/test/fixtures/package-json/svelte.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
package: {
packageJson: (original) => original
}
};

export default config;
8 changes: 8 additions & 0 deletions packages/package/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ test('SvelteKit interop', async () => {
await test_make_package('svelte-kit');
});

test('packageJson option (use original package.json)', async () => {
await test_make_package('package-json');
});

test('packageJson option (omit package.json)', async () => {
await test_make_package('package-json-omit');
});

// chokidar doesn't fire events in github actions :shrug:
if (!process.env.CI) {
test('watches for changes', async () => {
Expand Down
Loading