diff --git a/.changeset/four-pillows-give.md b/.changeset/four-pillows-give.md new file mode 100644 index 000000000000..6043eaaa70cb --- /dev/null +++ b/.changeset/four-pillows-give.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Add svelte-kit package command diff --git a/.changeset/twelve-feet-deny.md b/.changeset/twelve-feet-deny.md new file mode 100644 index 000000000000..25e40588a3c6 --- /dev/null +++ b/.changeset/twelve-feet-deny.md @@ -0,0 +1,5 @@ +--- +'create-svelte': patch +--- + +gitignore package directory diff --git a/documentation/docs/12-packaging.md b/documentation/docs/12-packaging.md new file mode 100644 index 000000000000..d9e33ecd5528 --- /dev/null +++ b/documentation/docs/12-packaging.md @@ -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 diff --git a/documentation/docs/12-cli.md b/documentation/docs/13-cli.md similarity index 100% rename from documentation/docs/12-cli.md rename to documentation/docs/13-cli.md diff --git a/documentation/docs/13-configuration.md b/documentation/docs/14-configuration.md similarity index 100% rename from documentation/docs/13-configuration.md rename to documentation/docs/14-configuration.md diff --git a/packages/create-svelte/templates/default/.gitignore b/packages/create-svelte/templates/default/.gitignore index 0a4623184983..2d66ec056330 100644 --- a/packages/create-svelte/templates/default/.gitignore +++ b/packages/create-svelte/templates/default/.gitignore @@ -1,5 +1,4 @@ .DS_Store node_modules /.svelte-kit -/build -/functions +/package diff --git a/packages/create-svelte/templates/skeleton/.gitignore b/packages/create-svelte/templates/skeleton/.gitignore index 0a4623184983..2d66ec056330 100644 --- a/packages/create-svelte/templates/skeleton/.gitignore +++ b/packages/create-svelte/templates/skeleton/.gitignore @@ -1,5 +1,4 @@ .DS_Store node_modules /.svelte-kit -/build -/functions +/package diff --git a/packages/kit/package.json b/packages/kit/package.json index a2d132977f90..41bbe339ea7a 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -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", diff --git a/packages/kit/src/cli.js b/packages/kit/src/cli.js index 97b44bac0836..f80e01a461b2 100644 --- a/packages/kit/src/cli.js +++ b/packages/kit/src/cli.js @@ -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 */ diff --git a/packages/kit/src/core/load_config/index.spec.js b/packages/kit/src/core/load_config/index.spec.js index 4678a7300126..d1138752f849 100644 --- a/packages/kit/src/core/load_config/index.spec.js +++ b/packages/kit/src/core/load_config/index.spec.js @@ -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: '/.' diff --git a/packages/kit/src/core/load_config/options.js b/packages/kit/src/core/load_config/options.js index 8c63e9748885..c763465039e9 100644 --- a/packages/kit/src/core/load_config/options.js +++ b/packages/kit/src/core/load_config/options.js @@ -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} diff --git a/packages/kit/src/core/load_config/test/index.js b/packages/kit/src/core/load_config/test/index.js index c86f90e55307..eb41d36ad697 100644 --- a/packages/kit/src/core/load_config/test/index.js +++ b/packages/kit/src/core/load_config/test/index.js @@ -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, diff --git a/packages/kit/src/core/make_package/index.js b/packages/kit/src/core/make_package/index.js new file mode 100644 index 000000000000..c081bb5dd1f9 --- /dev/null +++ b/packages/kit/src/core/make_package/index.js @@ -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} */ + 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); +} diff --git a/packages/kit/test/types.d.ts b/packages/kit/test/types.d.ts index 5214190d8dac..f612487e9d2e 100644 --- a/packages/kit/test/types.d.ts +++ b/packages/kit/test/types.d.ts @@ -1,4 +1,4 @@ -/// +/// import { Page, Response as PlaywrightResponse } from 'playwright-chromium'; import { RequestInfo, RequestInit, Response as NodeFetchResponse } from 'node-fetch'; diff --git a/packages/kit/tsconfig.json b/packages/kit/tsconfig.json index 0d8a30793399..e8904ff2aaea 100644 --- a/packages/kit/tsconfig.json +++ b/packages/kit/tsconfig.json @@ -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"] diff --git a/packages/kit/types/config.d.ts b/packages/kit/types/config.d.ts index cd3d7e54ade4..375b8258aa39 100644 --- a/packages/kit/types/config.d.ts +++ b/packages/kit/types/config.d.ts @@ -44,6 +44,17 @@ export type Config = { host?: string; hostHeader?: string; hydrate?: boolean; + package?: { + dir?: string; + exports?: { + include?: string[]; + exclude?: string[]; + }; + files?: { + include?: string[]; + exclude?: string[]; + }; + }; paths?: { base?: string; assets?: string; @@ -83,6 +94,17 @@ export type ValidatedConfig = { host: string; hostHeader: string; hydrate: boolean; + package: { + dir: string; + exports: { + include: string[]; + exclude: string[]; + }; + files: { + include: string[]; + exclude: string[]; + }; + }; paths: { base: string; assets: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7026b60b70fb..175b5a0c93db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,10 +199,10 @@ importers: packages/kit: specifiers: '@rollup/plugin-replace': ^2.4.2 - '@sveltejs/kit': workspace:* '@sveltejs/vite-plugin-svelte': ^1.0.0-next.10 '@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 @@ -213,7 +213,9 @@ importers: 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 @@ -235,9 +237,9 @@ importers: vite: 2.3.1 devDependencies: '@rollup/plugin-replace': 2.4.2_rollup@2.47.0 - '@sveltejs/kit': 'link:' '@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 @@ -247,7 +249,9 @@ importers: 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 @@ -686,6 +690,10 @@ packages: '@types/node': 14.14.43 dev: true + /@types/globrex/0.1.0: + resolution: {integrity: sha512-aBkxDgp/UbnluE+CIT3V3PoNewwOlLCzXSF3ipD86Slv8xVjwxrDAfSGbsfGgMzPo/fEMPXc+gNUJbtiugwfoA==} + dev: true + /@types/istanbul-lib-coverage/2.0.3: resolution: {integrity: sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==} dev: true @@ -2343,6 +2351,10 @@ packages: strip-bom: 3.0.0 dev: true + /locate-character/2.0.5: + resolution: {integrity: sha512-n2GmejDXtOPBAZdIiEFy5dJ5N38xBCXLNOtw2WpB9kGh6pnrEuKlwYI+Tkpofc4wDtVXHtoAOJaMRlYG/oYaxg==} + dev: true + /locate-path/2.0.0: resolution: {integrity: sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=} engines: {node: '>=4'}