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

svelte-kit package #1499

Merged
merged 14 commits into from
May 24, 2021
5 changes: 5 additions & 0 deletions .changeset/four-pillows-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Add svelte-kit package command
5 changes: 5 additions & 0 deletions .changeset/twelve-feet-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-svelte': patch
---

gitignore package directory
44 changes: 44 additions & 0 deletions documentation/docs/12-packaging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: Packaging
---

You can use SvelteKit to build component libraries as well as apps.

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 SvelteKit 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.

Running `svelte-kit package` will take the contents of `src/lib` and generate a `package` directory (which can be [configured](#configuration-package)) containing the following:

- All the files in `src/lib`, unless you [configure](#configuration-package) custom `include`/`exclude` options. Svelte components will be preprocessed (but note the [caveats](#packaging-caveats) below)
- A `package.json` that copies the `name`, `version`, `description`, `keywords`, `homepage`, `bugs`, `license`, `author`, `contributors`, `funding`, `repository`, `dependencies`, `private` and `publishConfig` fields from the root of the project, and adds a `"type": "module"` and an `"exports"` field

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-package) this behaviour. 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:

```js
import { Foo } from 'your-library';
```

```js
import Foo from 'your-library/Foo.svelte';
```

## Publishing

To publish the generated package:

```
npm publish package
```

If you configure a custom [`package.dir`](#configuration-package), change `package` accordingly.

## Caveats

This is a relatively experimental feature and is not yet fully implemented:

- if a preprocessor is specified, `.svelte` files are transformed (meaning they can contain TypeScript, for example), but `.d.ts` files are not generated
- `.ts` files are not currently transformed, and will cause the process to fail
- all other files are copied across as-is
File renamed without changes.
File renamed without changes.
3 changes: 1 addition & 2 deletions packages/create-svelte/templates/default/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.DS_Store
node_modules
/.svelte-kit
/build
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Rich-Harris did you mean to remove both /build and /functions here?

/functions
/package
3 changes: 1 addition & 2 deletions packages/create-svelte/templates/skeleton/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.DS_Store
node_modules
/.svelte-kit
/build
/functions
/package
4 changes: 3 additions & 1 deletion packages/kit/package.json
Original file line number Diff line number Diff line change
@@ -10,9 +10,9 @@
},
"devDependencies": {
"@rollup/plugin-replace": "^2.4.2",
"@sveltejs/kit": "workspace:*",
"@types/amphtml-validator": "^1.0.1",
"@types/cookie": "^0.4.0",
"@types/globrex": "^0.1.0",
"@types/marked": "^2.0.2",
"@types/mime": "^2.0.3",
"@types/node": "^14.14.43",
@@ -22,7 +22,9 @@
"cookie": "^0.4.1",
"devalue": "^2.0.1",
"eslint": "^7.25.0",
"globrex": "^0.1.2",
"kleur": "^4.1.4",
"locate-character": "^2.0.5",
"marked": "^2.0.3",
"node-fetch": "^3.0.0-beta.9",
"port-authority": "^1.1.2",
16 changes: 16 additions & 0 deletions packages/kit/src/cli.js
Original file line number Diff line number Diff line change
@@ -178,6 +178,22 @@ prog
);
});

prog
.command('package')
.describe('Create a package')
.option('-d, --dir', 'Destination directory', 'package')
.action(async () => {
const config = await get_config();

const { make_package } = await import('./core/make_package/index.js');

try {
await make_package(config);
} catch (error) {
handle_error(error);
}
});

prog.parse(process.argv, { unknown: (arg) => `Unknown option: ${arg}` });

/** @param {number} port */
22 changes: 22 additions & 0 deletions packages/kit/src/core/load_config/index.spec.js
Original file line number Diff line number Diff line change
@@ -27,6 +27,17 @@ test('fills in defaults', () => {
host: null,
hostHeader: null,
hydrate: true,
package: {
dir: 'package',
exports: {
include: ['**'],
exclude: ['_*', '**/_*']
},
files: {
include: ['**'],
exclude: []
}
},
paths: {
base: '',
assets: '/.'
@@ -111,6 +122,17 @@ test('fills in partial blanks', () => {
host: null,
hostHeader: null,
hydrate: true,
package: {
dir: 'package',
exports: {
include: ['**'],
exclude: ['_*', '**/_*']
},
files: {
include: ['**'],
exclude: []
}
},
paths: {
base: '',
assets: '/.'
38 changes: 38 additions & 0 deletions packages/kit/src/core/load_config/options.js
Original file line number Diff line number Diff line change
@@ -80,6 +80,27 @@ const options = {

hydrate: expect_boolean(true),

package: {
type: 'branch',
children: {
dir: expect_string('package'),
exports: {
type: 'branch',
children: {
include: expect_array_of_strings(['**']),
exclude: expect_array_of_strings(['_*', '**/_*'])
}
},
files: {
type: 'branch',
children: {
include: expect_array_of_strings(['**']),
exclude: expect_array_of_strings([])
}
}
}
},

paths: {
type: 'branch',
children: {
@@ -171,6 +192,23 @@ function expect_string(string, allow_empty = true) {
};
}

/**
* @param {string[]} array
* @returns {ConfigDefinition}
*/
function expect_array_of_strings(array) {
return {
type: 'leaf',
default: array,
validate: (option, keypath) => {
if (!Array.isArray(option) || !option.every((glob) => typeof glob === 'string')) {
throw new Error(`${keypath} must be an array of strings`);
}
return option;
}
};
}

/**
* @param {boolean} boolean
* @returns {ConfigDefinition}
11 changes: 11 additions & 0 deletions packages/kit/src/core/load_config/test/index.js
Original file line number Diff line number Diff line change
@@ -37,6 +37,17 @@ async function testLoadDefaultConfig(path) {
host: null,
hostHeader: null,
hydrate: true,
package: {
dir: 'package',
exports: {
include: ['**'],
exclude: ['_*', '**/_*']
},
files: {
include: ['**'],
exclude: []
}
},
paths: { base: '', assets: '/.' },
prerender: { crawl: true, enabled: true, force: false, pages: ['*'] },
router: true,
150 changes: 150 additions & 0 deletions packages/kit/src/core/make_package/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import * as fs from 'fs';
import * as path from 'path';
import { preprocess } from 'svelte/compiler';
import globrex from 'globrex';
import { mkdirp, rimraf } from '../filesystem';

/**
* @param {import('types/config').ValidatedConfig} config
* @param {string} cwd
*/
export async function make_package(config, cwd = process.cwd()) {
rimraf(path.join(cwd, config.kit.package.dir));

const files_filter = create_filter(config.kit.package.files);
const exports_filter = create_filter(config.kit.package.exports);

const files = walk(config.kit.files.lib);

const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));

const package_pkg = {
name: pkg.name,
version: pkg.version,
description: pkg.description,
keywords: pkg.keywords,
homepage: pkg.homepage,
bugs: pkg.bugs,
license: pkg.license,
author: pkg.author,
contributors: pkg.contributors,
funding: pkg.funding,
repository: pkg.repository,
dependencies: pkg.dependencies,
private: pkg.private,
publishConfig: pkg.publishConfig,
type: 'module',
/** @type {Record<string, string>} */
exports: {
'./package.json': './package.json'
}
};

for (const file of files) {
if (!files_filter(file)) continue;

const filename = path.join(config.kit.files.lib, file);
const source = fs.readFileSync(filename, 'utf8');

const ext = path.extname(file);
const svelte_ext = config.extensions.find((ext) => file.endsWith(ext)); // unlike `ext`, could be e.g. `.svelte.md`

/** @type {string} */
let out_file;

/** @type {string} */
let out_contents;

if (svelte_ext) {
// it's a Svelte component
// TODO how to emit types?
out_file = file.slice(0, -svelte_ext.length) + '.svelte';
out_contents = config.preprocess
? (await preprocess(source, config.preprocess, { filename })).code
: source;
} else if (ext === '.ts' && !file.endsWith('.d.ts')) {
// TODO transpile TS file and emit types
// also, we want to emit types from JSDoc annotations in .js files
throw new Error('svelte-kit package does not yet support TypeScript');
} else {
out_file = file;
out_contents = source;
}

write(path.join(cwd, config.kit.package.dir, out_file), out_contents);

if (exports_filter(file)) {
const entry = `./${out_file}`;
package_pkg.exports[entry] = entry;
}
}

const main = package_pkg.exports['./index.js'] || package_pkg.exports['./index.svelte'];

if (main) {
package_pkg.exports['.'] = main;
}

write(
path.join(cwd, config.kit.package.dir, 'package.json'),
JSON.stringify(package_pkg, null, ' ')
);

const project_readme = path.join(cwd, 'README.md');
const package_readme = path.join(cwd, config.kit.package.dir, 'README.md');

if (fs.existsSync(project_readme) && !fs.existsSync(package_readme)) {
fs.copyFileSync(project_readme, package_readme);
}
}

/**
* @param {{
* include: string[];
* exclude: string[];
* }} options
*/
function create_filter(options) {
const include = options.include.map((str) => str && globrex(str));
const exclude = options.exclude.map((str) => str && globrex(str));

/** @param {string} str */
const filter = (str) =>
include.some((glob) => glob.regex.test(str)) && !exclude.some((glob) => glob.regex.test(str));

return filter;
}

/** @param {string} cwd */
function walk(cwd) {
/** @type {string[]} */
const all_files = [];

/** @param {string} dir */
function walk_dir(dir) {
const files = fs.readdirSync(path.join(cwd, dir));

for (const file of files) {
const joined = path.join(dir, file);
const stats = fs.statSync(path.join(cwd, joined));

if (stats.isDirectory()) {
walk_dir(joined);
} else {
all_files.push(joined);
}
}
}

walk_dir('');
return all_files;
}

/**
* @param {string} file
* @param {string} contents
*/
function write(file, contents) {
mkdirp(path.dirname(file));
fs.writeFileSync(file, contents);
}
2 changes: 1 addition & 1 deletion packages/kit/test/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// <reference types="@sveltejs/kit" />
/// <reference types="../types/index.js" />

import { Page, Response as PlaywrightResponse } from 'playwright-chromium';
import { RequestInfo, RequestInit, Response as NodeFetchResponse } from 'node-fetch';
2 changes: 1 addition & 1 deletion packages/kit/tsconfig.json
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@
"paths": {
"test": ["./test/types"],
"types/*": ["./types/*"],
"@sveltejs/kit": ["../types"]
"@sveltejs/kit": ["./types/index"]
}
},
"include": ["src/**/*", "test/**/*", "types/**/*", "test/types.d.ts", "test/ambient.d.ts"]
Loading