Skip to content

Commit

Permalink
svelte-kit package (#1499)
Browse files Browse the repository at this point in the history
* first stab at svelte-kit package command

* include/exclude files as well

* make typescript happy

* tidy up

* improve error message

* exclude underscore-prefixed files from exports by default

* move files

* add docs

* changeset

* gitignore package directory

* update lockfile, who knows why

* update unit tests

* copy over readme
  • Loading branch information
Rich Harris authored May 24, 2021
1 parent 94c1074 commit 6372690
Show file tree
Hide file tree
Showing 17 changed files with 334 additions and 9 deletions.
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

This comment has been minimized.

Copy link
@AlexxNB

AlexxNB May 25, 2021

Looks like you should use 3rd level headers here, because Pngwin's doc-parser doesn't handle this atm.

This comment has been minimized.

Copy link
@AlexxNB

AlexxNB May 25, 2021

Ha... It might be because the doc wasn't wrote fully yet and you fail the site build advisedly =)


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
/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
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
16 changes: 16 additions & 0 deletions packages/kit/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
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
Expand Up @@ -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: '/.'
Expand Down Expand Up @@ -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: '/.'
Expand Down
38 changes: 38 additions & 0 deletions packages/kit/src/core/load_config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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}
Expand Down
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
Expand Up @@ -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,
Expand Down
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';
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
Loading

0 comments on commit 6372690

Please sign in to comment.