diff --git a/.changeset/coupling-sv-and-sv-utils.md b/.changeset/coupling-sv-and-sv-utils.md new file mode 100644 index 000000000..f7232eff5 --- /dev/null +++ b/.changeset/coupling-sv-and-sv-utils.md @@ -0,0 +1,22 @@ +--- +'sv': minor +'@sveltejs/sv-utils': minor +--- + +feat: sv / sv-utils coupling, pnpm helpers, experimental add-ons, and API snapshots + +**Highlights** + +- Replace `sv.pnpmBuildDependency` with `sv.file` plus `pnpm.onlyBuiltDependencies` from `@sveltejs/sv-utils` and `file.findUp`. + +**`@sveltejs/sv-utils`** + +- Add `pnpm.onlyBuiltDependencies` to append packages to `onlyBuiltDependencies` in pnpm YAML via `transforms.yaml`. +- Type `YamlDocument` (`parse.yaml`) with `get` / `set` using `unknown` so consumers narrow explicitly; align YAML transforms with that contract. + +**`sv`** + +- Refactor workspace / engine / package-manager flows around file IO and package JSON loading (`loadFile`, `saveFile`, `loadPackageJson`), and trim workspace addon path handling; update addons accordingly. +- Reorganize the public `testing` entry for Vitest helpers and document the surface. +- Add generated `api-surface` markdown snapshots and a `scripts/generate-api-surface.js` helper (wired through the build) to track the public API. +- Remove deprecated `pnpmBuildDependency` usage and stop exporting internal pnpm-only-built helpers from the public `sv` surface. diff --git a/.changeset/lucky-dragons-speak.md b/.changeset/lucky-dragons-speak.md new file mode 100644 index 000000000..5aa4f937d --- /dev/null +++ b/.changeset/lucky-dragons-speak.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +chore: simplify `runes` option diff --git a/.changeset/remove-pnpm-build-dependency.md b/.changeset/remove-pnpm-build-dependency.md deleted file mode 100644 index 41b340900..000000000 --- a/.changeset/remove-pnpm-build-dependency.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'sv': minor -'@sveltejs/sv-utils': minor ---- - -feat: replace `sv.pnpmBuildDependency` with `sv.file` + `pnpm.onlyBuiltDependencies` helper and `file.findUp` diff --git a/.changeset/some-rings-appear.md b/.changeset/some-rings-appear.md deleted file mode 100644 index 249478187..000000000 --- a/.changeset/some-rings-appear.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'sv': minor -'@sveltejs/sv-utils': minor ---- - -feat: community add-ons are now **experimental** diff --git a/documentation/docs/50-api/20-sv-utils.md b/documentation/docs/50-api/20-sv-utils.md index b8dcccf43..3e5a543cf 100644 --- a/documentation/docs/50-api/20-sv-utils.md +++ b/documentation/docs/50-api/20-sv-utils.md @@ -137,7 +137,7 @@ Return `false` from any transform callback to abort - the original content is re import { transforms } from '@sveltejs/sv-utils'; sv.file( - file.eslintConfig, + 'eslint.config.js', transforms.script(({ ast, js }) => { const { value: existing } = js.exports.createDefault(ast, { fallback: myConfig }); if (existing !== myConfig) { diff --git a/package.json b/package.json index caf32fd8c..b9d98b03c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "scripts": { "build": "tsdown", + "postbuild": "node scripts/generate-api-surface.js", "changeset:publish": "changeset publish", "check": "pnpm --parallel check", "dev": "tsdown -w & pnpm --parallel check -w & wait", diff --git a/packages/sv-utils/CHANGELOG.md b/packages/sv-utils/CHANGELOG.md index d0b1aa192..b9a1fdd3a 100644 --- a/packages/sv-utils/CHANGELOG.md +++ b/packages/sv-utils/CHANGELOG.md @@ -1,5 +1,11 @@ # @sveltejs/sv-utils +## 0.1.0 +### Minor Changes + + +- feat: community add-ons are now **experimental** ([#1020](https://github.com/sveltejs/cli/pull/1020)) + ## 0.0.5 ### Patch Changes diff --git a/packages/sv-utils/api-surface.md b/packages/sv-utils/api-surface.md index 1e5050d6e..d6d69bc91 100644 --- a/packages/sv-utils/api-surface.md +++ b/packages/sv-utils/api-surface.md @@ -742,15 +742,6 @@ type Package = { keywords?: string[]; workspaces?: string[]; }; -declare const commonFilePaths: { - readonly packageJson: 'package.json'; - readonly svelteConfig: 'svelte.config.js'; - readonly svelteConfigTS: 'svelte.config.ts'; - readonly jsconfig: 'jsconfig.json'; - readonly tsconfig: 'tsconfig.json'; - readonly viteConfig: 'vite.config.js'; - readonly viteConfigTS: 'vite.config.ts'; -}; declare function fileExists(cwd: string, filePath: string): boolean; declare function loadFile(cwd: string, filePath: string): string; @@ -761,18 +752,6 @@ declare function loadPackageJson(cwd: string): { data: Package; generateCode: () => string; }; -/** - * @deprecated Use {@link loadFile} instead. This alias will be removed in a future version. - */ -declare const readFile: typeof loadFile; -/** - * @deprecated Use {@link saveFile} instead. This alias will be removed in a future version. - */ -declare const writeFile: typeof saveFile; -/** - * @deprecated Use {@link loadPackageJson} instead. This alias will be removed in a future version. - */ -declare const getPackageJson: typeof loadPackageJson; type ColorInput = string | string[]; declare const color: { addon: (str: ColorInput) => string; @@ -810,7 +789,6 @@ export { index_d_exports as Walker, type YamlDocument, color, - commonFilePaths, constructCommand, createPrinter, index_d_exports$1 as css, @@ -818,7 +796,6 @@ export { detect, downloadJson, fileExists, - getPackageJson, index_d_exports$2 as html, isVersionUnsupportedBelow, index_d_exports$3 as js, @@ -827,7 +804,6 @@ export { loadPackageJson, parse, pnpm_d_exports as pnpm, - readFile, resolveCommand, resolveCommandArray, sanitizeName, @@ -835,7 +811,6 @@ export { splitVersion, index_d_exports$4 as svelte, text_d_exports as text, - transforms, - writeFile + transforms }; ``` diff --git a/packages/sv-utils/package.json b/packages/sv-utils/package.json index 8447de5c2..53d9be30d 100644 --- a/packages/sv-utils/package.json +++ b/packages/sv-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/sv-utils", - "version": "0.0.5", + "version": "0.1.0", "type": "module", "description": "Utility functions for sv", "license": "MIT", diff --git a/packages/sv-utils/src/files.ts b/packages/sv-utils/src/files.ts index 8b6dc2488..233a11323 100644 --- a/packages/sv-utils/src/files.ts +++ b/packages/sv-utils/src/files.ts @@ -13,16 +13,6 @@ export type Package = { workspaces?: string[]; }; -export const commonFilePaths = { - packageJson: 'package.json', - svelteConfig: 'svelte.config.js', - svelteConfigTS: 'svelte.config.ts', - jsconfig: 'jsconfig.json', - tsconfig: 'tsconfig.json', - viteConfig: 'vite.config.js', - viteConfigTS: 'vite.config.ts' -} as const; - export function fileExists(cwd: string, filePath: string): boolean { const fullFilePath = path.resolve(cwd, filePath); return fs.existsSync(fullFilePath); @@ -60,27 +50,12 @@ export function loadPackageJson(cwd: string): { data: Package; generateCode: () => string; } { - const packageText = loadFile(cwd, commonFilePaths.packageJson); + const packageText = loadFile(cwd, 'package.json'); if (!packageText) { - const pkgPath = path.join(cwd, commonFilePaths.packageJson); + const pkgPath = path.join(cwd, 'package.json'); throw new Error(`Invalid workspace: missing '${pkgPath}'`); } const { data, generateCode } = parseJson(packageText); return { source: packageText, data: data as Package, generateCode }; } - -/** - * @deprecated Use {@link loadFile} instead. This alias will be removed in a future version. - */ -export const readFile: typeof loadFile = loadFile; - -/** - * @deprecated Use {@link saveFile} instead. This alias will be removed in a future version. - */ -export const writeFile: typeof saveFile = saveFile; - -/** - * @deprecated Use {@link loadPackageJson} instead. This alias will be removed in a future version. - */ -export const getPackageJson: typeof loadPackageJson = loadPackageJson; diff --git a/packages/sv-utils/src/index.ts b/packages/sv-utils/src/index.ts index d8615802c..74f334c47 100644 --- a/packages/sv-utils/src/index.ts +++ b/packages/sv-utils/src/index.ts @@ -73,27 +73,7 @@ export { sanitizeName } from './sanitize.ts'; export { downloadJson } from './downloadJson.ts'; // File system helpers (sync, workspace-relative paths) -export { - commonFilePaths, - fileExists, - loadFile, - loadPackageJson, - saveFile, - type Package -} from './files.ts'; - -/** - * @deprecated Use {@link loadFile} instead. This alias will be removed in a future version. - */ -export { readFile } from './files.ts'; -/** - * @deprecated Use {@link saveFile} instead. This alias will be removed in a future version. - */ -export { writeFile } from './files.ts'; -/** - * @deprecated Use {@link loadPackageJson} instead. This alias will be removed in a future version. - */ -export { getPackageJson } from './files.ts'; +export { fileExists, loadFile, loadPackageJson, saveFile, type Package } from './files.ts'; // Terminal styling export { color } from './color.ts'; diff --git a/packages/sv-utils/src/pnpm.ts b/packages/sv-utils/src/pnpm.ts index aa86f928e..c7979e8f3 100644 --- a/packages/sv-utils/src/pnpm.ts +++ b/packages/sv-utils/src/pnpm.ts @@ -12,7 +12,9 @@ import { transforms, type TransformFn } from './tooling/transforms.ts'; */ export function onlyBuiltDependencies(...packages: string[]): TransformFn { return transforms.yaml(({ data }) => { - const existing = data.get('onlyBuiltDependencies'); + const existing = data.get('onlyBuiltDependencies') as + | { items?: Array<{ value: string } | string> } + | undefined; const items: Array<{ value: string } | string> = existing?.items ?? []; for (const pkg of packages) { if (items.includes(pkg)) continue; diff --git a/packages/sv/CHANGELOG.md b/packages/sv/CHANGELOG.md index 59091894b..582eb76a9 100644 --- a/packages/sv/CHANGELOG.md +++ b/packages/sv/CHANGELOG.md @@ -1,5 +1,17 @@ # sv +## 0.14.0 +### Minor Changes + + +- feat: community add-ons are now **experimental** ([#1020](https://github.com/sveltejs/cli/pull/1020)) + + +### Patch Changes + +- Updated dependencies [[`c0e5831`](https://github.com/sveltejs/cli/commit/c0e583126279afe7aff8cebb03c5b3928d73b521)]: + - @sveltejs/sv-utils@0.1.0 + ## 0.13.2 ### Patch Changes diff --git a/packages/sv/api-surface-testing.md b/packages/sv/api-surface-testing.md index 6295a1d41..5f0ded335 100644 --- a/packages/sv/api-surface-testing.md +++ b/packages/sv/api-surface-testing.md @@ -10,31 +10,6 @@ type CreateProject = (options: { variant: ProjectVariant; clean?: boolean; }) => string; -type SetupOptions = { - cwd: string; - variants: readonly ProjectVariant[]; - clean?: boolean; -}; -/** @deprecated Internal helper used by `createSetupTest` - will be removed from public API in a future version. */ -declare function setup({ cwd, clean, variants }: SetupOptions): { - templatesDir: string; -}; -type CreateOptions = { - cwd: string; - testName: string; - templatesDir: string; -}; -/** @deprecated Internal helper used by `createSetupTest` - will be removed from public API in a future version. */ -declare function createProject({ cwd, testName, templatesDir }: CreateOptions): CreateProject; -type PreviewOptions = { - cwd: string; - command?: string; -}; -/** @deprecated Internal helper used by `prepareServer` - will be removed from public API in a future version. */ -declare function startPreview({ cwd, command }: PreviewOptions): Promise<{ - url: string; - close: () => Promise; -}>; declare module 'vitest' { interface ProvidedContext { testDir: string; @@ -102,12 +77,9 @@ export { ProjectVariant, SetupTestOptions, VitestContext, - createProject, createSetupTest, prepareServer, - setup, setupGlobal, - startPreview, variants }; ``` diff --git a/packages/sv/api-surface.md b/packages/sv/api-surface.md index 7a98dcb58..77af976cf 100644 --- a/packages/sv/api-surface.md +++ b/packages/sv/api-surface.md @@ -13,8 +13,7 @@ type Options = { template: TemplateType; types: LanguageType; }; -declare function create(cwd: string, options: Omit): void; -declare function create(options: Options): void; +declare function create({ cwd, ...options }: Options): void; type FileEditor = Workspace & { content: string; }; diff --git a/packages/sv/package.json b/packages/sv/package.json index 7d2234658..0b9f13f68 100644 --- a/packages/sv/package.json +++ b/packages/sv/package.json @@ -1,6 +1,6 @@ { "name": "sv", - "version": "0.13.2", + "version": "0.14.0", "type": "module", "description": "A command line interface (CLI) for creating and maintaining Svelte applications", "license": "MIT", diff --git a/packages/sv/src/cli/create.ts b/packages/sv/src/cli/create.ts index f8793efc2..75888d8ee 100644 --- a/packages/sv/src/cli/create.ts +++ b/packages/sv/src/cli/create.ts @@ -1,5 +1,5 @@ import * as p from '@clack/prompts'; -import { color, commonFilePaths, loadPackageJson, resolveCommandArray } from '@sveltejs/sv-utils'; +import { color, loadPackageJson, resolveCommandArray } from '@sveltejs/sv-utils'; import { Command, Option } from 'commander'; import fs from 'node:fs'; import path from 'node:path'; @@ -258,14 +258,25 @@ async function createProject(cwd: ProjectPath, options: Options) { const projectPath = path.resolve(directory); const basename = path.basename(projectPath); const parentDirName = path.basename(path.dirname(projectPath)); - const projectName = parentDirName.startsWith('@') ? `${parentDirName}/${basename}` : basename; + let projectName = parentDirName.startsWith('@') ? `${parentDirName}/${basename}` : basename; if (template === 'addon' && !projectName.startsWith('@')) { // At this stage, we don't support un-scoped add-ons // FYI: a demo exists for `npx sv add my-cool-addon` - common.errorAndExit( - `Community add-ons must be published under an npm org (e.g. ${color.command('@my-org/sv')}). Unscoped package names are not supported at this stage.` - ); + const org = await p.text({ + message: `Community add-ons must be published under an npm org. Enter the name of your npm org:`, + placeholder: ' @my-org', + validate: (value) => { + if (!value) return 'Organization name is required'; + if (!value.startsWith('@')) return 'Must start with @'; + if (value.includes('/')) return 'Just the org, not the full package name'; + } + }); + if (p.isCancel(org)) { + p.cancel('Operation cancelled.'); + process.exit(0); + } + projectName = `${org}/${basename}`; } if (template === 'addon' && options.add.length > 0) { @@ -468,8 +479,11 @@ export async function createVirtualWorkspace({ language: type === 'typescript' ? 'ts' : 'js', file: { ...tentativeWorkspace.file, - viteConfig: type === 'typescript' ? commonFilePaths.viteConfigTS : commonFilePaths.viteConfig, - svelteConfig: commonFilePaths.svelteConfig // currently we always use js files, never typescript files + viteConfig: + type === 'typescript' + ? common.commonFilePaths.viteConfigTS + : common.commonFilePaths.viteConfig, + svelteConfig: common.commonFilePaths.svelteConfig // currently we always use js files, never typescript files } }; diff --git a/packages/sv/src/cli/tests/snapshots/@my-org/sv/tests/addon.test.js b/packages/sv/src/cli/tests/snapshots/@my-org/sv/tests/addon.test.js index dc2b8c3cb..9d8704974 100644 --- a/packages/sv/src/cli/tests/snapshots/@my-org/sv/tests/addon.test.js +++ b/packages/sv/src/cli/tests/snapshots/@my-org/sv/tests/addon.test.js @@ -10,7 +10,7 @@ const browser = false; const { test, prepareServer, testCases } = setupTest( { addon }, { - kinds: [{ type: 'default', options: { addon: { who: 'you' } } }], + kinds: [{ type: 'default', options: { [addon.id]: { who: 'you' } } }], filter: (testCase) => testCase.variant.includes('kit'), browser } @@ -21,15 +21,18 @@ test.concurrent.for(testCases)( async (testCase, { page, ...ctx }) => { const cwd = ctx.cwd(testCase); - const msg = - "This is a text file made by the Community Addon Template demo for the add-on: '@my-org/sv'!"; + const msg = "Community Addon Template demo for the add-on: '@my-org/sv'!"; const contentPath = path.resolve(cwd, `src/lib/@my-org/sv/content.txt`); const contentContent = fs.readFileSync(contentPath, 'utf8'); - // Check if we have the imports expect(contentContent).toContain(msg); + const helloPath = path.resolve(cwd, `src/lib/@my-org/sv/HelloComponent.svelte`); + const helloContent = fs.readFileSync(helloPath, 'utf8'); + // Check if we have the imports + expect(helloContent).toContain('you'); + // For browser testing if (browser) { const { close } = await prepareServer({ cwd, page }); diff --git a/packages/sv/src/cli/tests/snapshots/create-only/svelte.config.js b/packages/sv/src/cli/tests/snapshots/create-only/svelte.config.js index 58d330bd3..0c3412e9f 100644 --- a/packages/sv/src/cli/tests/snapshots/create-only/svelte.config.js +++ b/packages/sv/src/cli/tests/snapshots/create-only/svelte.config.js @@ -1,17 +1,10 @@ import adapter from '@sveltejs/adapter-auto'; -import { relative, sep } from 'node:path'; /** @type {import('@sveltejs/kit').Config} */ const config = { compilerOptions: { - // defaults to rune mode for the project, except for `node_modules`. Can be removed in svelte 6. - runes: ({ filename }) => { - const relativePath = relative(import.meta.dirname, filename); - const pathSegments = relativePath.toLowerCase().split(sep); - const isExternalLibrary = pathSegments.includes('node_modules'); - - return isExternalLibrary ? undefined : true; - } + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) }, kit: { // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/svelte.config.js b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/svelte.config.js index 2aeeecd4e..6479320f8 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/svelte.config.js +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/svelte.config.js @@ -1,18 +1,11 @@ import { mdsvex } from 'mdsvex'; import adapter from '@sveltejs/adapter-node'; -import { relative, sep } from 'node:path'; /** @type {import('@sveltejs/kit').Config} */ const config = { compilerOptions: { - // defaults to rune mode for the project, except for `node_modules`. Can be removed in svelte 6. - runes: ({ filename }) => { - const relativePath = relative(import.meta.dirname, filename); - const pathSegments = relativePath.toLowerCase().split(sep); - const isExternalLibrary = pathSegments.includes('node_modules'); - - return isExternalLibrary ? undefined : true; - } + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => filename.split(/[/\\]/).includes('node_modules') ? undefined : true }, kit: { adapter: adapter() }, preprocess: [mdsvex({ extensions: ['.svx', '.md'] })], diff --git a/packages/sv/src/core/common.ts b/packages/sv/src/core/common.ts index aace74cc8..c834a08a7 100644 --- a/packages/sv/src/core/common.ts +++ b/packages/sv/src/core/common.ts @@ -324,3 +324,13 @@ export function updateAgent( fs.writeFileSync(agentPath, content); } } + +export const commonFilePaths = { + packageJson: 'package.json', + svelteConfig: 'svelte.config.js', + svelteConfigTS: 'svelte.config.ts', + jsconfig: 'jsconfig.json', + tsconfig: 'tsconfig.json', + viteConfig: 'vite.config.js', + viteConfigTS: 'vite.config.ts' +} as const; diff --git a/packages/sv/src/core/engine.ts b/packages/sv/src/core/engine.ts index c9bdeb1ff..7cb87c48d 100644 --- a/packages/sv/src/core/engine.ts +++ b/packages/sv/src/core/engine.ts @@ -1,7 +1,6 @@ import * as p from '@clack/prompts'; import { color, - commonFilePaths, fileExists, loadFile, loadPackageJson, @@ -11,6 +10,7 @@ import { } from '@sveltejs/sv-utils'; import { NonZeroExitError, exec } from 'tinyexec'; import { createLoadedAddon } from '../cli/add.ts'; +import { commonFilePaths } from './common.ts'; import { getErrorHint, type Addon, diff --git a/packages/sv/src/core/workspace.ts b/packages/sv/src/core/workspace.ts index cb38bcb9b..35ceac23c 100644 --- a/packages/sv/src/core/workspace.ts +++ b/packages/sv/src/core/workspace.ts @@ -3,13 +3,13 @@ import { type AstTypes, js, parse, - commonFilePaths, loadFile, loadPackageJson } from '@sveltejs/sv-utils'; import * as find from 'empathic/find'; import fs from 'node:fs'; import path from 'node:path'; +import { commonFilePaths } from './common.ts'; import type { OptionDefinition, OptionValues } from './options.ts'; import { detectPackageManager } from './package-manager.ts'; diff --git a/packages/sv/src/create/index.ts b/packages/sv/src/create/index.ts index 5b78b6c45..ffe49c5ee 100644 --- a/packages/sv/src/create/index.ts +++ b/packages/sv/src/create/index.ts @@ -1,6 +1,7 @@ -import { sanitizeName, commonFilePaths } from '@sveltejs/sv-utils'; +import { sanitizeName } from '@sveltejs/sv-utils'; import fs from 'node:fs'; import path from 'node:path'; +import { commonFilePaths } from '../core/common.ts'; import { mkdirp, copy, dist, getSharedFiles, replace, kv } from './utils.ts'; export type TemplateType = (typeof templateTypes)[number]; @@ -33,19 +34,7 @@ export type Common = { }>; }; -export function create(cwd: string, options: Omit): void; -export function create(options: Options): void; -export function create(cwdOrOptions: string | Options, legacyOptions?: Omit): void { - let cwd: string; - let options: Omit; - if (typeof cwdOrOptions === 'string') { - cwd = cwdOrOptions; - options = legacyOptions!; - } else { - cwd = cwdOrOptions.cwd; - options = cwdOrOptions; - } - +export function create({ cwd, ...options }: Options): void { mkdirp(cwd); write_template_files(options.template, options.types, options.name, cwd); diff --git a/packages/sv/src/create/playground.ts b/packages/sv/src/create/playground.ts index ae2fae244..f93788126 100644 --- a/packages/sv/src/create/playground.ts +++ b/packages/sv/src/create/playground.ts @@ -5,11 +5,11 @@ import { parse, svelte, downloadJson, - Walker, - commonFilePaths + Walker } from '@sveltejs/sv-utils'; import fs from 'node:fs'; import path from 'node:path'; +import { commonFilePaths } from '../core/common.ts'; import { getSharedFiles } from './utils.ts'; export function validatePlaygroundUrl(link: string): boolean { diff --git a/packages/sv/src/create/shared/+typescript/svelte.config.js b/packages/sv/src/create/shared/+typescript/svelte.config.js index 58d330bd3..0c3412e9f 100644 --- a/packages/sv/src/create/shared/+typescript/svelte.config.js +++ b/packages/sv/src/create/shared/+typescript/svelte.config.js @@ -1,17 +1,10 @@ import adapter from '@sveltejs/adapter-auto'; -import { relative, sep } from 'node:path'; /** @type {import('@sveltejs/kit').Config} */ const config = { compilerOptions: { - // defaults to rune mode for the project, except for `node_modules`. Can be removed in svelte 6. - runes: ({ filename }) => { - const relativePath = relative(import.meta.dirname, filename); - const pathSegments = relativePath.toLowerCase().split(sep); - const isExternalLibrary = pathSegments.includes('node_modules'); - - return isExternalLibrary ? undefined : true; - } + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) }, kit: { // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. diff --git a/packages/sv/src/create/shared/-typescript/svelte.config.js b/packages/sv/src/create/shared/-typescript/svelte.config.js index 58d330bd3..0c3412e9f 100644 --- a/packages/sv/src/create/shared/-typescript/svelte.config.js +++ b/packages/sv/src/create/shared/-typescript/svelte.config.js @@ -1,17 +1,10 @@ import adapter from '@sveltejs/adapter-auto'; -import { relative, sep } from 'node:path'; /** @type {import('@sveltejs/kit').Config} */ const config = { compilerOptions: { - // defaults to rune mode for the project, except for `node_modules`. Can be removed in svelte 6. - runes: ({ filename }) => { - const relativePath = relative(import.meta.dirname, filename); - const pathSegments = relativePath.toLowerCase().split(sep); - const isExternalLibrary = pathSegments.includes('node_modules'); - - return isExternalLibrary ? undefined : true; - } + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) }, kit: { // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. diff --git a/packages/sv/src/create/templates/addon/tests/addon.test.js b/packages/sv/src/create/templates/addon/tests/addon.test.js index e525062c5..6ff085500 100644 --- a/packages/sv/src/create/templates/addon/tests/addon.test.js +++ b/packages/sv/src/create/templates/addon/tests/addon.test.js @@ -10,7 +10,7 @@ const browser = false; const { test, prepareServer, testCases } = setupTest( { addon }, { - kinds: [{ type: 'default', options: { addon: { who: 'you' } } }], + kinds: [{ type: 'default', options: { [addon.id]: { who: 'you' } } }], filter: (testCase) => testCase.variant.includes('kit'), browser } @@ -21,15 +21,18 @@ test.concurrent.for(testCases)( async (testCase, { page, ...ctx }) => { const cwd = ctx.cwd(testCase); - const msg = - "This is a text file made by the Community Addon Template demo for the add-on: '~SV-NAME-TODO~'!"; + const msg = "Community Addon Template demo for the add-on: '~SV-NAME-TODO~'!"; const contentPath = path.resolve(cwd, `src/lib/~SV-NAME-TODO~/content.txt`); const contentContent = fs.readFileSync(contentPath, 'utf8'); - // Check if we have the imports expect(contentContent).toContain(msg); + const helloPath = path.resolve(cwd, `src/lib/~SV-NAME-TODO~/HelloComponent.svelte`); + const helloContent = fs.readFileSync(helloPath, 'utf8'); + // Check if we have the imports + expect(helloContent).toContain('you'); + // For browser testing if (browser) { const { close } = await prepareServer({ cwd, page }); diff --git a/packages/sv/src/testing.ts b/packages/sv/src/testing.ts index 02d6c14b2..1075f4496 100644 --- a/packages/sv/src/testing.ts +++ b/packages/sv/src/testing.ts @@ -28,8 +28,8 @@ type SetupOptions = { /** @default false */ clean?: boolean; }; -/** @deprecated Internal helper used by `createSetupTest` - will be removed from public API in a future version. */ -export function setup({ cwd, clean = false, variants }: SetupOptions): { templatesDir: string } { + +function setup({ cwd, clean = false, variants }: SetupOptions): { templatesDir: string } { const workingDir = path.resolve(cwd); if (clean && fs.existsSync(workingDir)) { fs.rmSync(workingDir, { force: true, recursive: true }); @@ -59,8 +59,8 @@ export function setup({ cwd, clean = false, variants }: SetupOptions): { templat } type CreateOptions = { cwd: string; testName: string; templatesDir: string }; -/** @deprecated Internal helper used by `createSetupTest` - will be removed from public API in a future version. */ -export function createProject({ cwd, testName, templatesDir }: CreateOptions): CreateProject { + +function createProject({ cwd, testName, templatesDir }: CreateOptions): CreateProject { // create the reference dir const testDir = path.resolve(cwd, testName); fs.mkdirSync(testDir, { recursive: true }); @@ -76,8 +76,8 @@ export function createProject({ cwd, testName, templatesDir }: CreateOptions): C } type PreviewOptions = { cwd: string; command?: string }; -/** @deprecated Internal helper used by `prepareServer` - will be removed from public API in a future version. */ -export async function startPreview({ + +async function startPreview({ cwd, command = 'npm run preview' }: PreviewOptions): Promise<{ url: string; close: () => Promise }> { @@ -241,12 +241,17 @@ export async function prepareServer({ return { url, close }; } +export type PlaywrightContext = Pick; + export type VitestContext = Pick< typeof import('vitest'), 'inject' | 'test' | 'beforeAll' | 'beforeEach' >; -export function createSetupTest(vitest: VitestContext): ( +export function createSetupTest( + vitest: VitestContext, + playwright?: PlaywrightContext +): ( addons: Addons, options?: SetupTestOptions ) => { @@ -274,12 +279,16 @@ export function createSetupTest(vitest: VitestContext): { let chromium: Awaited['chromium']; - try { - ({ chromium } = await import('@playwright/test')); - } catch { - throw new Error( - 'Browser testing requires @playwright/test. Install it with: pnpm add -D @playwright/test' - ); + if (playwright) { + chromium = playwright.chromium; + } else { + try { + ({ chromium } = await import('@playwright/test')); + } catch { + throw new Error( + 'Browser testing requires @playwright/test. Install it with: pnpm add -D @playwright/test' + ); + } } browser = await chromium.launch(); return async () => { diff --git a/scripts/generate-api-surface.js b/scripts/generate-api-surface.js index 811c44d7e..e1f3c03d4 100644 --- a/scripts/generate-api-surface.js +++ b/scripts/generate-api-surface.js @@ -1,24 +1,23 @@ /** - * Reads the generated .d.mts files and produces a cleaned-up - * api-surface.md for each package. Strips `//#region` / `//#endregion` - * directives, `//# sourceMappingURL=...` lines, non-deprecated JSDoc, import-only lines, - * and blank runs so the result is a compact, diff-friendly snapshot - * of the public API. JSDoc blocks that contain `@deprecated` are kept - * in full. + * Reads generated `.d.mts` files and writes compact `api-surface.md` snapshots per package. * - * Run: node scripts/generate-api-surface.js - * Or: invoked from tsdown `build:done` after all configs finish (see tsdown.config.ts). + * Strips region/source-map directives, non-deprecated block comments, import-only lines, + * and excess blank lines. Blocks mentioning `@deprecated` are kept verbatim. * - * Finishes with Prettier (repo root config) so snapshots match `pnpm format`. + * @remarks + * Run: `node scripts/generate-api-surface.js` — or via root `postbuild` after `pnpm build`. + * Output is formatted with Prettier using the repo root `prettier.config.js` so it matches `pnpm format`. */ import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; -import * as prettier from 'prettier'; +import prettier from 'prettier'; const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +/** Absolute path to the repo Prettier config (explicit so formatting does not depend on cwd). */ +const PRETTIER_CONFIG = path.join(ROOT, 'prettier.config.js'); const packages = [ { @@ -38,7 +37,11 @@ const packages = [ } ]; -/** Remove `//#region` / `//#endregion` lines emitted by the DTS bundler. */ +/** + * Remove `//#region` / `//#endregion` lines emitted by the DTS bundler. + * @param {string} source + * @returns {string} + */ function stripRegionDirectives(source) { return source .split('\n') @@ -46,7 +49,11 @@ function stripRegionDirectives(source) { .join('\n'); } -/** Remove `//# sourceMappingURL=...` lines from declaration emit. */ +/** + * Remove `//# sourceMappingURL=...` lines from declaration emit. + * @param {string} source + * @returns {string} + */ function stripSourceMappingUrl(source) { return source .split('\n') @@ -55,8 +62,9 @@ function stripSourceMappingUrl(source) { } /** - * Remove `/** ... *\/` blocks unless they contain `@deprecated`, in which case - * the full block is preserved (including inline trailing JSDoc on a line). + * Remove slash-star-star block comments unless they contain `@deprecated` (full block kept). + * @param {string} source + * @returns {string} */ function stripJsDoc(source) { return source.replace(/\/\*\*[\s\S]*?\*\//g, (match) => { @@ -67,18 +75,32 @@ function stripJsDoc(source) { }); } +/** + * Drop top-level `import …` lines (types-only noise in the snapshot). + * @param {string} source + * @returns {string} + */ function stripImportLines(source) { - // Remove `import ...` lines that are only used for type resolution return source .split('\n') .filter((line) => !line.match(/^import\s/)) .join('\n'); } +/** + * Collapse three or more consecutive newlines to two. + * @param {string} source + * @returns {string} + */ function collapseBlankLines(source) { return source.replace(/\n{3,}/g, '\n\n'); } +/** + * Apply all cleaning steps to declaration text. + * @param {string} source + * @returns {string} + */ function clean(source) { let result = stripRegionDirectives(source); result = stripSourceMappingUrl(result); @@ -89,11 +111,17 @@ function clean(source) { } /** + * Format a file with repo Prettier options (plugins + overrides). * @param {string} absPath absolute path to the markdown file + * @returns {Promise} */ async function formatWithPrettier(absPath) { const raw = fs.readFileSync(absPath, 'utf8'); - const formatted = await prettier.format(raw, { filepath: absPath }); + const options = + (await prettier.resolveConfig(absPath, { + config: PRETTIER_CONFIG + })) ?? {}; + const formatted = await prettier.format(raw, { ...options, filepath: absPath }); fs.writeFileSync(absPath, formatted, 'utf8'); } diff --git a/tsdown.config.ts b/tsdown.config.ts index ce4017a73..da4d41568 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -3,23 +3,6 @@ import process from 'node:process'; import { defineConfig } from 'tsdown'; import { buildTemplates } from './packages/sv/src/create/scripts/build-templates.js'; -/** - * tsdown runs each `defineConfig` entry in parallel. There is no single - * "all builds finished" hook, so we count `build:done` (must match the number - * of config objects below) and run api-surface generation once all `.d.mts` - * outputs exist. - */ -const API_SURFACE_CONFIG_COUNT = 3; -let apiSurfaceBuildsDone = 0; - -function hookApiSurfaceBuildDone(): void | Promise { - apiSurfaceBuildsDone++; - if (apiSurfaceBuildsDone === API_SURFACE_CONFIG_COUNT) { - apiSurfaceBuildsDone = 0; - return import('./scripts/generate-api-surface.js').then((m) => m.generateApiSurface()); - } -} - export default defineConfig([ { cwd: path.resolve('packages/sv'), @@ -74,8 +57,7 @@ export default defineConfig([ hooks: { async 'build:before'() { await buildCliTemplates(); - }, - 'build:done': () => hookApiSurfaceBuildDone() + } } }, // sv-utils: runtime build (bundles everything including svelte) @@ -106,9 +88,6 @@ export default defineConfig([ 'yaml', 'zimmerframe' ] - }, - hooks: { - 'build:done': () => hookApiSurfaceBuildDone() } }, // sv-utils: DTS-only build (svelte externalized) @@ -133,9 +112,6 @@ export default defineConfig([ 'package-manager-detector' ], onlyBundle: ['smol-toml', 'zimmerframe'] - }, - hooks: { - 'build:done': () => hookApiSurfaceBuildDone() } } ]);