Skip to content

Commit

Permalink
feat: add packageJson setting
Browse files Browse the repository at this point in the history
- closes #7258
- closes #6824
- closes #4828
- part of #2242
- better message for #2884
  • Loading branch information
dummdidumm committed Jan 31, 2023
1 parent 5571e4a commit 45bef9e
Show file tree
Hide file tree
Showing 23 changed files with 233 additions and 48 deletions.
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

0 comments on commit 45bef9e

Please sign in to comment.