diff --git a/packages/sv-utils/api-surface.md b/packages/sv-utils/api-surface.md new file mode 100644 index 000000000..fda21143d --- /dev/null +++ b/packages/sv-utils/api-surface.md @@ -0,0 +1,891 @@ +# @sveltejs/sv-utils - Public API Surface + + + +```ts +/*! + * Copyright (c) Squirrel Chat et al., All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +declare class TomlDate extends Date { + #private; + constructor(date: string | Date); + isDateTime(): boolean; + isLocal(): boolean; + isDate(): boolean; + isTime(): boolean; + isValid(): boolean; + toISOString(): string; + static wrapAsOffsetDateTime(jsDate: Date, offset?: string): TomlDate; + static wrapAsLocalDateTime(jsDate: Date): TomlDate; + static wrapAsLocalDate(jsDate: Date): TomlDate; + static wrapAsLocalTime(jsDate: Date): TomlDate; +} +type TomlPrimitive = string | number | bigint | boolean | TomlDate; +type TomlTable = { + [key: string]: TomlValue; +}; +type TomlValue = TomlPrimitive | TomlValue[] | TomlTable; +declare module "estree" { + interface TSTypeAnnotation { + type: "TSTypeAnnotation"; + typeAnnotation: + | TSStringKeyword + | TSTypeReference + | TSUnionType + | TSIndexedAccessType; + } + interface TSStringKeyword { + type: "TSStringKeyword"; + } + interface TSNullKeyword { + type: "TSNullKeyword"; + } + interface TSTypeReference { + type: "TSTypeReference"; + typeName: Identifier; + } + interface TSAsExpression extends BaseNode { + type: "TSAsExpression"; + expression: Expression; + typeAnnotation: TSTypeAnnotation["typeAnnotation"]; + } + interface TSModuleDeclaration extends BaseNode { + type: "TSModuleDeclaration"; + global: boolean; + declare: boolean; + id: Identifier; + body: TSModuleBlock; + } + interface TSModuleBlock extends BaseNode { + type: "TSModuleBlock"; + body: Array; + } + interface TSInterfaceDeclaration extends BaseNode { + type: "TSInterfaceDeclaration"; + id: Identifier; + body: TSInterfaceBody; + } + interface TSInterfaceBody extends BaseNode { + type: "TSInterfaceBody"; + body: TSPropertySignature[]; + } + interface TSPropertySignature extends BaseNode { + type: "TSPropertySignature"; + computed: boolean; + key: Identifier; + optional?: boolean; + typeAnnotation: TSTypeAnnotation; + } + interface TSProgram extends Omit { + body: Array< + Directive | Statement | ModuleDeclaration | TSModuleDeclaration + >; + } + interface TSUnionType { + type: "TSUnionType"; + types: Array; + } + interface TSImportType { + type: "TSImportType"; + argument: Literal; + qualifier: Identifier; + } + interface TSIndexedAccessType { + type: "TSIndexedAccessType"; + objectType: TSImportType; + indexType: TSLiteralType; + } + interface TSLiteralType { + type: "TSLiteralType"; + literal: Literal; + } + interface TSSatisfiesExpression extends BaseNode { + type: "TSSatisfiesExpression"; + expression: Expression; + typeAnnotation: TSTypeAnnotation["typeAnnotation"]; + } + interface BaseNodeWithoutComments { + type: string; + loc?: SourceLocation | null | undefined; + range?: [number, number] | undefined; + start?: number; + end?: number; + } + interface Identifier { + typeAnnotation?: TSTypeAnnotation; + } + interface ExpressionMap { + TSAsExpression: TSAsExpression; + TSSatisfiesExpression: TSSatisfiesExpression; + } + interface NodeMap { + TSModuleDeclaration: TSModuleDeclaration; + TSInterfaceDeclaration: TSInterfaceDeclaration; + } + interface ImportDeclaration { + importKind: "type" | "value"; + } +} +type CommentType = { + type: "Line" | "Block"; + value: string; +}; +declare class Comments { + private original; + private leading; + private trailing; + constructor(); + add( + node: BaseNode$1, + comment: CommentType, + options?: { + position?: "leading" | "trailing"; + }, + ): void; + remove( + predicate: (comment: estree.Comment) => boolean | undefined | null, + ): void; +} + +type YamlDocument = { + get(key: string): unknown; + set(key: string, value: unknown): void; +}; +type ParseBase = { + source: string; + + generateCode(): string; +}; +declare function parseScript(source: string): { + ast: estree.Program; + comments: Comments; +} & ParseBase; +declare function parseCss(source: string): { + ast: Omit; +} & ParseBase; +declare function parseHtml(source: string): { + ast: SvelteAst.Fragment; +} & ParseBase; +declare function parseJson(source: string): { + data: any; +} & ParseBase; +declare function parseYaml(source: string): { + data: YamlDocument; +} & ParseBase; +declare function parseSvelte(source: string): { + ast: SvelteAst.Root; +} & ParseBase; +declare function parseToml(source: string): { + data: TomlTable; +} & ParseBase; + +type Dedent = { + (strings: TemplateStringsArray, ...values: unknown[]): string; + (source: string): string; +}; +declare const dedent: Dedent; +declare module "zimmerframe" { + export function walk< + T extends { + type: string; + }, + U extends Record | null, + >(node: T, state: U, visitors: Visitors): T; + type BaseNode = { + type: string; + }; + type NodeOf = X extends { + type: T; + } + ? X + : never; + type SpecialisedVisitors = { + [K in T["type"]]?: Visitor, U, T>; + }; + export type Visitor = (node: T, context: Context) => V | void; + export type Visitors = T["type"] extends "_" + ? never + : SpecialisedVisitors & { + _?: Visitor; + }; + export interface Context { + next: (state?: U) => T | void; + path: T[]; + state: U; + stop: () => void; + visit: (node: T, state?: U) => T; + } + export {}; +} //# sourceMappingURL=index.d.ts.map +declare function resolveCommandArray( + agent: Agent, + command: Command, + args: string[], +): string[]; +declare namespace index_d_exports$1 { + export { addAtRule, addDeclaration, addImports, addRule }; +} +declare function addRule( + node: SvelteAst.CSS.StyleSheetBase, + options: { + selector: string; + }, +): SvelteAst.CSS.Rule; +declare function addDeclaration( + node: SvelteAst.CSS.Rule, + options: { + property: string; + value: string; + }, +): void; +declare function addImports( + node: SvelteAst.CSS.StyleSheetBase, + options: { + imports: string[]; + }, +): void; +declare function addAtRule( + node: SvelteAst.CSS.StyleSheetBase, + options: { + name: string; + params: string; + append: boolean; + }, +): SvelteAst.CSS.Atrule; +declare namespace array_d_exports { + export { append, create$1 as create, prepend }; +} +declare function create$1(): estree.ArrayExpression; +declare function append( + node: estree.ArrayExpression, + element: string | estree.Expression | estree.SpreadElement, +): void; +declare function prepend( + node: estree.ArrayExpression, + element: string | estree.Expression | estree.SpreadElement, +): void; +declare namespace object_d_exports { + export { create, overrideProperties, property, propertyNode }; +} +type ObjectPrimitiveValues = string | number | boolean | undefined | null; +type ObjectValues = + | ObjectPrimitiveValues + | Record + | ObjectValues[]; +type ObjectMap = Record; +declare function property( + node: estree.ObjectExpression, + options: { + name: string; + fallback: T; + }, +): T; +declare function propertyNode( + node: estree.ObjectExpression, + options: { + name: string; + fallback: T; + }, +): estree.Property; +declare function create(properties: ObjectMap): estree.ObjectExpression; +declare function overrideProperties( + objectExpression: estree.ObjectExpression, + properties: ObjectMap, +): void; +declare namespace common_d_exports { + export { + addJsDocComment, + addJsDocTypeComment, + appendFromString, + appendStatement, + areNodesEqual, + contains, + createBlockStatement, + createExpressionStatement, + createLiteral, + createSatisfies, + createSpread, + createTypeProperty, + hasTypeProperty, + parseExpression, + parseFromString, + parseStatement, + typeAnnotate, + }; +} +declare function addJsDocTypeComment( + node: estree.Node, + comments: Comments, + options: { + type: string; + }, +): void; +declare function addJsDocComment( + node: estree.Node, + comments: Comments, + options: { + params: Record; + }, +): void; +declare function typeAnnotate( + node: estree.Expression, + options: { + type: string; + }, +): estree.TSAsExpression; +declare function createSatisfies( + node: estree.Expression, + options: { + type: string; + }, +): estree.TSSatisfiesExpression; +declare function createSpread( + argument: estree.Expression, +): estree.SpreadElement; +declare function createLiteral( + value: string | number | boolean | null, +): estree.Literal; +declare function areNodesEqual( + node: estree.Node, + otherNode: estree.Node, +): boolean; +declare function createBlockStatement(): estree.BlockStatement; +declare function createExpressionStatement(options: { + expression: estree.Expression; +}): estree.ExpressionStatement; +declare function appendFromString( + node: estree.BlockStatement | estree.Program, + options: { + code: string; + comments?: Comments; + }, +): void; +declare function parseExpression(code: string): estree.Expression; +declare function parseStatement(code: string): estree.Statement; +declare function parseFromString(code: string): T; + +declare function appendStatement( + node: estree.BlockStatement | estree.Program, + options: { + statement: estree.Statement; + }, +): void; + +declare function contains(node: estree.Node, targetNode: estree.Node): boolean; +declare function hasTypeProperty( + node: estree.TSInterfaceDeclaration["body"]["body"][number], + options: { + name: string; + }, +): boolean; +declare function createTypeProperty( + name: string, + value: string, + optional?: boolean, +): estree.TSInterfaceBody["body"][number]; +declare namespace function_d_exports { + export { createArrow, createCall, getArgument }; +} +declare function createCall(options: { + name: string; + args: string[]; + useIdentifiers?: boolean; +}): estree.CallExpression; +declare function createArrow(options: { + body: estree.Expression | estree.BlockStatement; + async: boolean; +}): estree.ArrowFunctionExpression; +declare function getArgument( + node: estree.CallExpression, + options: { + index: number; + fallback: T; + }, +): T; +declare namespace imports_d_exports { + export { + addDefault, + addEmpty, + addNamed, + addNamespace$1 as addNamespace, + find, + remove, + }; +} +declare function addEmpty( + node: estree.Program, + options: { + from: string; + }, +): void; +declare function addNamespace$1( + node: estree.Program, + options: { + from: string; + as: string; + }, +): void; +declare function addDefault( + node: estree.Program, + options: { + from: string; + as: string; + }, +): void; +declare function addNamed( + node: estree.Program, + options: { + imports: Record | string[]; + from: string; + isType?: boolean; + }, +): void; +declare function find( + ast: estree.Program, + options: { + name: string; + from: string; + }, +): + | { + statement: estree.ImportDeclaration; + alias: string; + } + | { + statement: undefined; + alias: undefined; + }; +declare function remove( + ast: estree.Program, + options: { + name: string; + from: string; + statement?: estree.ImportDeclaration; + }, +): void; +declare namespace variables_d_exports { + export { createIdentifier, declaration, typeAnnotateDeclarator }; +} +declare function declaration( + node: estree.Program | estree.Declaration, + options: { + kind: "const" | "let" | "var"; + name: string; + value: estree.Expression; + }, +): estree.VariableDeclaration; +declare function createIdentifier(name: string): estree.Identifier; +declare function typeAnnotateDeclarator( + node: estree.VariableDeclarator, + options: { + typeName: string; + }, +): estree.VariableDeclarator; +declare namespace exports_d_exports { + export { ExportDefaultResult, addNamespace, createDefault, createNamed }; +} +type ExportDefaultResult = { + astNode: estree.ExportDefaultDeclaration; + value: T; + isFallback: boolean; +}; +declare function createDefault( + node: estree.Program, + options: { + fallback: T; + }, +): ExportDefaultResult; +declare function createNamed( + node: estree.Program, + options: { + name: string; + fallback: estree.VariableDeclaration; + }, +): estree.ExportNamedDeclaration; +declare function addNamespace( + node: estree.Program, + options: { + from: string; + as?: string; + }, +): void; +declare namespace kit_d_exports { + export { addGlobalAppInterface, addHooksHandle }; +} +declare function addGlobalAppInterface( + node: estree.TSProgram, + options: { + name: "Error" | "Locals" | "PageData" | "PageState" | "Platform"; + }, +): estree.TSInterfaceDeclaration; +declare function addHooksHandle( + node: estree.Program, + options: { + language: "ts" | "js"; + newHandleName: string; + handleContent: string; + comments: Comments; + }, +): void; +declare namespace vite_d_exports { + export { addPlugin, configProperty, getConfig }; +} +declare const addPlugin: ( + ast: estree.Program, + options: { + code: string; + mode?: "append" | "prepend"; + }, +) => void; + +declare function configProperty< + T extends estree.Expression | estree.Identifier, +>( + ast: estree.Program, + config: estree.ObjectExpression, + options: { + name: string; + fallback: T; + }, +): T; +declare const getConfig: (ast: estree.Program) => estree.ObjectExpression; +declare namespace index_d_exports$3 { + export { + array_d_exports as array, + common_d_exports as common, + exports_d_exports as exports, + function_d_exports as functions, + imports_d_exports as imports, + kit_d_exports as kit, + object_d_exports as object, + variables_d_exports as variables, + vite_d_exports as vite, + }; +} +declare namespace index_d_exports$2 { + export { + addAttribute, + addFromRawHtml, + appendElement, + createElement, + insertElement, + }; +} +declare function createElement( + tagName: string, + attributes?: Record, +): SvelteAst.RegularElement; +declare function addAttribute( + element: SvelteAst.RegularElement, + name: string, + value: string, +): void; +declare function insertElement( + fragment: SvelteAst.Fragment, + elementToInsert: SvelteAst.Fragment["nodes"][0], +): void; +declare function appendElement( + fragment: SvelteAst.Fragment, + elementToAppend: SvelteAst.Fragment["nodes"][0], +): void; +declare function addFromRawHtml( + fragment: SvelteAst.Fragment, + html: string, +): void; +declare namespace text_d_exports { + export { upsert }; +} +type CommentEntry = { + text: string; + mode: "append" | "prepend"; +}; +type CommentOption = string | Array; + +declare function upsert( + content: string, + key: string, + options?: { + value?: string; + comment?: CommentOption; + separator?: boolean; + }, +): string; +declare namespace json_d_exports { + export { arrayUpsert, packageScriptsUpsert }; +} +declare function arrayUpsert( + data: any, + key: string, + value: any, + options?: { + mode?: "append" | "prepend"; + }, +): void; +declare function packageScriptsUpsert( + data: any, + key: string, + value: string, + options?: { + mode?: "append" | "prepend"; + }, +): void; +declare namespace index_d_exports$4 { + export { RootWithInstance, addFragment, addSlot, ensureScript }; +} +type RootWithInstance = SvelteAst.Root & { + instance: SvelteAst.Script; +}; +declare function ensureScript( + ast: SvelteAst.Root, + options?: { + language?: "ts" | "js"; + }, +): asserts ast is RootWithInstance; +declare function addSlot( + ast: SvelteAst.Root, + options: { + svelteVersion: string; + language?: "ts" | "js"; + }, +): void; +declare function addFragment( + ast: SvelteAst.Root, + content: string, + options?: { + mode?: "append" | "prepend"; + }, +): void; +type TransformFn = (content: string) => string; +type TransformOptions = { + onError?: (error: unknown) => void; +}; + +declare const transforms: { + script( + cb: (file: { + ast: estree.Program; + comments: Comments; + content: string; + js: typeof index_d_exports$3; + }) => void | false, + options?: TransformOptions, + ): (content: string) => string; + + svelte( + cb: (file: { + ast: SvelteAst.Root; + content: string; + svelte: typeof index_d_exports$4; + js: typeof index_d_exports$3; + }) => void | false, + options?: TransformOptions, + ): (content: string) => string; + + svelteScript( + scriptOptions: { + language: "ts" | "js"; + }, + cb: (file: { + ast: RootWithInstance; + content: string; + svelte: typeof index_d_exports$4; + js: typeof index_d_exports$3; + }) => void | false, + options?: TransformOptions, + ): TransformFn; + + css( + cb: (file: { + ast: Omit; + content: string; + css: typeof index_d_exports$1; + }) => void | false, + options?: TransformOptions, + ): TransformFn; + + json( + cb: (file: { + data: T; + content: string; + json: typeof json_d_exports; + }) => void | false, + options?: TransformOptions, + ): TransformFn; + + yaml( + cb: (file: { data: YamlDocument; content: string }) => void | false, + options?: TransformOptions, + ): TransformFn; + + toml( + cb: (file: { data: TomlTable; content: string }) => void | false, + options?: TransformOptions, + ): TransformFn; + + html( + cb: (file: { + ast: SvelteAst.Fragment; + content: string; + html: typeof index_d_exports$2; + }) => void | false, + options?: TransformOptions, + ): TransformFn; + + text( + cb: (file: { + content: string; + text: typeof text_d_exports; + }) => string | false, + ): TransformFn; +}; +type Version = { + major?: number; + minor?: number; + patch?: number; +}; +declare function splitVersion(str: string): Version; +declare function isVersionUnsupportedBelow( + versionStr: string, + belowStr: string, +): boolean | undefined; +type Printer = (content: string, alt?: string) => string; +declare function createPrinter(...conditions: boolean[]): Printer[]; + +declare function sanitizeName( + name: string, + style: "package" | "wrangler", +): string; +declare const downloadJson: (url: string) => Promise; +type Package = { + name: string; + version: string; + dependencies?: Record; + devDependencies?: Record; + bugs?: string; + repository?: { + type: string; + url: string; + }; + 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; + +declare function saveFile(cwd: string, filePath: string, content: string): void; +declare function loadPackageJson(cwd: string): { + source: 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; + command: (str: ColorInput) => string; + env: (str: ColorInput) => string; + path: (str: ColorInput) => string; + route: (str: ColorInput) => string; + website: (str: ColorInput) => string; + optional: (str: ColorInput) => string; + dim: (str: ColorInput) => string; + success: (str: ColorInput) => string; + warning: (str: ColorInput) => string; + error: (str: ColorInput) => string; + hidden: (str: ColorInput) => string; +}; + +declare const parse: { + css: typeof parseCss; + html: typeof parseHtml; + json: typeof parseJson; + script: typeof parseScript; + svelte: typeof parseSvelte; + toml: typeof parseToml; + yaml: typeof parseYaml; +}; +export { + AGENTS, + type AgentName, + type estree as AstTypes, + COMMANDS, + type Comments, + type Package, + type SvelteAst, + type TransformFn, + index_d_exports as Walker, + type YamlDocument, + color, + commonFilePaths, + constructCommand, + createPrinter, + index_d_exports$1 as css, + dedent, + detect, + downloadJson, + fileExists, + getPackageJson, + index_d_exports$2 as html, + isVersionUnsupportedBelow, + index_d_exports$3 as js, + json_d_exports as json, + loadFile, + loadPackageJson, + parse, + readFile, + resolveCommand, + resolveCommandArray, + sanitizeName, + saveFile, + splitVersion, + index_d_exports$4 as svelte, + text_d_exports as text, + transforms, + writeFile, +}; +``` diff --git a/packages/sv-utils/src/dedent.ts b/packages/sv-utils/src/dedent.ts new file mode 100644 index 000000000..1210bdac9 --- /dev/null +++ b/packages/sv-utils/src/dedent.ts @@ -0,0 +1,13 @@ +import dedentImpl from 'dedent'; + +/** + * Template-tag or single-string dedent helper (same behavior as the `dedent` package). + * Types are hand-written so the public `.d.mts` does not inline `dedent`'s full declarations. + */ +export type Dedent = { + (strings: TemplateStringsArray, ...values: unknown[]): string; + (source: string): string; +}; + +const dedent: Dedent = dedentImpl as Dedent; +export default dedent; diff --git a/packages/sv-utils/src/files.ts b/packages/sv-utils/src/files.ts index 4dd22c359..8b6dc2488 100644 --- a/packages/sv-utils/src/files.ts +++ b/packages/sv-utils/src/files.ts @@ -13,22 +13,23 @@ export type Package = { workspaces?: string[]; }; -export function getPackageJson(cwd: string): { - source: string; - data: Package; - generateCode: () => string; -} { - const packageText = readFile(cwd, commonFilePaths.packageJson); - if (!packageText) { - const pkgPath = path.join(cwd, commonFilePaths.packageJson); - throw new Error(`Invalid workspace: missing '${pkgPath}'`); - } +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; - const { data, generateCode } = parseJson(packageText); - return { source: packageText, data: data as Package, generateCode }; +export function fileExists(cwd: string, filePath: string): boolean { + const fullFilePath = path.resolve(cwd, filePath); + return fs.existsSync(fullFilePath); } -export function readFile(cwd: string, filePath: string): string { +/** Synchronous load of a workspace-relative file as UTF-8 text; missing files yield `''`. */ +export function loadFile(cwd: string, filePath: string): string { const fullFilePath = path.resolve(cwd, filePath); if (!fileExists(cwd, filePath)) { @@ -40,12 +41,8 @@ export function readFile(cwd: string, filePath: string): string { return text; } -export function fileExists(cwd: string, filePath: string): boolean { - const fullFilePath = path.resolve(cwd, filePath); - return fs.existsSync(fullFilePath); -} - -export function writeFile(cwd: string, filePath: string, content: string): void { +/** Synchronous write of a workspace-relative file (creates parent dirs). */ +export function saveFile(cwd: string, filePath: string, content: string): void { const fullFilePath = path.resolve(cwd, filePath); const fullDirectoryPath = path.dirname(fullFilePath); @@ -58,44 +55,32 @@ export function writeFile(cwd: string, filePath: string, content: string): void fs.writeFileSync(fullFilePath, content, 'utf8'); } -export function installPackages( - dependencies: Array<{ pkg: string; version: string; dev: boolean }>, - cwd: string -): string { - const { data, generateCode } = getPackageJson(cwd); - - for (const dependency of dependencies) { - if (dependency.dev) { - data.devDependencies ??= {}; - data.devDependencies[dependency.pkg] = dependency.version; - } else { - data.dependencies ??= {}; - data.dependencies[dependency.pkg] = dependency.version; - } +export function loadPackageJson(cwd: string): { + source: string; + data: Package; + generateCode: () => string; +} { + const packageText = loadFile(cwd, commonFilePaths.packageJson); + if (!packageText) { + const pkgPath = path.join(cwd, commonFilePaths.packageJson); + throw new Error(`Invalid workspace: missing '${pkgPath}'`); } - if (data.dependencies) data.dependencies = alphabetizeProperties(data.dependencies); - if (data.devDependencies) data.devDependencies = alphabetizeProperties(data.devDependencies); - - writeFile(cwd, commonFilePaths.packageJson, generateCode()); - return commonFilePaths.packageJson; + const { data, generateCode } = parseJson(packageText); + return { source: packageText, data: data as Package, generateCode }; } -function alphabetizeProperties(obj: Record) { - const orderedObj: Record = {}; - const sortedEntries = Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)); - for (const [key, value] of sortedEntries) { - orderedObj[key] = value; - } - return orderedObj; -} +/** + * @deprecated Use {@link loadFile} instead. This alias will be removed in a future version. + */ +export const readFile: typeof loadFile = loadFile; -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; +/** + * @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 29005fa4a..8e42b4c3a 100644 --- a/packages/sv-utils/src/index.ts +++ b/packages/sv-utils/src/index.ts @@ -1,8 +1,3 @@ -import { - resolveCommand as _resolveCommand, - type Agent, - type Command -} from 'package-manager-detector'; import { parseCss, parseHtml, @@ -14,22 +9,19 @@ import { } from './tooling/parsers.ts'; // External re-exports -export { default as dedent } from 'dedent'; +export { default as dedent } from './dedent.ts'; export * as Walker from 'zimmerframe'; + +// Package managers (delegates to `package-manager-detector`; see `pm.ts`) export { AGENTS, type AgentName, COMMANDS, constructCommand, detect, - resolveCommand -} from 'package-manager-detector'; - -/** Resolves a package manager command and returns it as a string array (command + args). */ -export function resolveCommandArray(agent: Agent, command: Command, args: string[]): string[] { - const cmd = _resolveCommand(agent, command, args)!; - return [cmd.command, ...cmd.args]; -} + resolveCommand, + resolveCommandArray +} from './pm.ts'; // Parsing & language namespaces export * as css from './tooling/css/index.ts'; @@ -77,20 +69,33 @@ export { createPrinter } from './utils.ts'; export { sanitizeName } from './sanitize.ts'; export { downloadJson } from './downloadJson.ts'; -// File system helpers +// File system helpers (sync, workspace-relative paths) export { commonFilePaths, fileExists, - getPackageJson, - installPackages, - readFile, - writeFile, + 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'; + // Terminal styling export { color } from './color.ts'; // Types export type { Comments, AstTypes, SvelteAst } from './tooling/index.ts'; export type { TransformFn } from './tooling/transforms.ts'; +export type { YamlDocument } from './tooling/parsers.ts'; diff --git a/packages/sv-utils/src/pm.ts b/packages/sv-utils/src/pm.ts new file mode 100644 index 000000000..fd01d791c --- /dev/null +++ b/packages/sv-utils/src/pm.ts @@ -0,0 +1,23 @@ +/** + * Thin wrapper around [`package-manager-detector`](https://github.com/antfu/package-manager-detector). + * Only the symbols re-exported from the package root are public — we keep this module small and + * avoid exposing the full upstream surface. + */ +import { + resolveCommand as _resolveCommand, + type Agent, + type Command +} from 'package-manager-detector'; +export { + AGENTS, + type AgentName, + COMMANDS, + constructCommand, + detect, + resolveCommand +} from 'package-manager-detector'; + +export function resolveCommandArray(agent: Agent, command: Command, args: string[]): string[] { + const cmd = _resolveCommand(agent, command, args)!; + return [cmd.command, ...cmd.args]; +} diff --git a/packages/sv-utils/src/tooling/parsers.ts b/packages/sv-utils/src/tooling/parsers.ts index f5661ebc2..a20a93803 100644 --- a/packages/sv-utils/src/tooling/parsers.ts +++ b/packages/sv-utils/src/tooling/parsers.ts @@ -1,6 +1,15 @@ import type { TomlTable } from 'smol-toml'; import * as utils from './index.ts'; +/** + * Minimal shape for YAML document roots from `parse.yaml` — avoids re-exporting the full `yaml` types. + * At runtime this is the library’s document type; only `get` / `set` are part of the public contract. + */ +export type YamlDocument = { + get(key: string): unknown; + set(key: string, value: unknown): void; +}; + type ParseBase = { source: string; /** @@ -52,14 +61,13 @@ export function parseJson(source: string): { data: any } & ParseBase { return { data, source, generateCode }; } -export function parseYaml( - source: string -): { data: ReturnType } & ParseBase { +export function parseYaml(source: string): { data: YamlDocument } & ParseBase { if (!source) source = ''; const data = utils.parseYaml(source); - const generateCode = () => utils.serializeYaml(data); + const generateCode = () => + utils.serializeYaml(data as Parameters[0]); - return { data, source, generateCode }; + return { data: data as YamlDocument, source, generateCode }; } export function parseSvelte(source: string): { ast: utils.SvelteAst.Root } & ParseBase { diff --git a/packages/sv-utils/src/tooling/transforms.ts b/packages/sv-utils/src/tooling/transforms.ts index 5d27c1696..361b5925b 100644 --- a/packages/sv-utils/src/tooling/transforms.ts +++ b/packages/sv-utils/src/tooling/transforms.ts @@ -12,7 +12,8 @@ import { parseScript, parseSvelte, parseToml, - parseYaml + parseYaml, + type YamlDocument } from './parsers.ts'; import { type RootWithInstance, ensureScript } from './svelte/index.ts'; import * as svelteNs from './svelte/index.ts'; @@ -190,7 +191,7 @@ export const transforms = { * Return `false` from the callback to abort - the original content is returned unchanged. */ yaml( - cb: (file: { data: ReturnType['data']; content: string }) => void | false, + cb: (file: { data: YamlDocument; content: string }) => void | false, options?: TransformOptions ): TransformFn { return (content) => { diff --git a/packages/sv/api-surface-testing.md b/packages/sv/api-surface-testing.md new file mode 100644 index 000000000..855db11e7 --- /dev/null +++ b/packages/sv/api-surface-testing.md @@ -0,0 +1,134 @@ +# sv (testing) - Public API Surface + + + +```ts +declare function addPnpmBuildDependencies( + cwd: string, + packageManager: AgentName | null | undefined, + allowedPackages: string[], +): Promise; +type ProjectVariant = "kit-js" | "kit-ts" | "vite-js" | "vite-ts"; +declare const variants: ProjectVariant[]; +type CreateProject = (options: { + testId: string; + 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; + templatesDir: string; + variants: ProjectVariant[]; + } +} +declare function setupGlobal({ + TEST_DIR, + pre, + post, +}: { + TEST_DIR: string; + pre?: () => Promise; + post?: () => Promise; +}): ({ provide }: TestProject) => Promise<() => Promise>; +type Fixtures = { + page: Page; + cwd(addonTestCase: AddonTestCase): string; +}; +type AddonTestCase = { + variant: ProjectVariant; + kind: { + type: string; + options: OptionMap; + }; +}; +type SetupTestOptions = { + kinds: Array["kind"]>; + filter?: (addonTestCase: AddonTestCase) => boolean; + browser?: boolean; + preAdd?: (o: { + addonTestCase: AddonTestCase; + cwd: string; + }) => Promise | void; +}; +type PrepareServerOptions = { + cwd: string; + page: Page; + buildCommand?: string; + previewCommand?: string; +}; +type PrepareServerReturn = { + url: string; + close: () => Promise; +}; +declare function prepareServer({ + cwd, + page, + buildCommand, + previewCommand, +}: PrepareServerOptions): Promise; +type PlaywrightContext = Pick; +type VitestContext = Pick< + typeof vitest, + "inject" | "test" | "beforeAll" | "beforeEach" +>; +declare function createSetupTest( + vitest: VitestContext, + playwright?: PlaywrightContext, +): ( + addons: Addons, + options?: SetupTestOptions, +) => { + test: vitest.TestAPI; + testCases: Array>; + prepareServer: typeof prepareServer; +}; +export { + AddonTestCase, + CreateProject, + Fixtures, + PlaywrightContext, + PrepareServerOptions, + PrepareServerReturn, + ProjectVariant, + SetupTestOptions, + VitestContext, + addPnpmBuildDependencies, + createProject, + createSetupTest, + prepareServer, + setup, + setupGlobal, + startPreview, + variants, +}; +``` diff --git a/packages/sv/api-surface.md b/packages/sv/api-surface.md new file mode 100644 index 000000000..f9ccdfbd9 --- /dev/null +++ b/packages/sv/api-surface.md @@ -0,0 +1,69 @@ +# sv - Public API Surface + + + +```ts +type TemplateType = (typeof templateTypes)[number]; +type LanguageType = (typeof languageTypes)[number]; +declare const templateTypes: readonly [ + "minimal", + "demo", + "library", + "addon", + "svelte", +]; +declare const languageTypes: readonly ["typescript", "checkjs", "none"]; +type Options = { + cwd: string; + name: string; + template: TemplateType; + types: LanguageType; +}; +declare function create(cwd: string, options: Omit): void; +declare function create(options: Options): void; +type FileEditor = Workspace & { + content: string; +}; +type FileType = { + name: (options: Workspace) => string; + condition?: ConditionDefinition; + content: (editor: FileEditor) => string; +}; +export { + type Addon, + type AddonDefinition, + type AddonInput, + type AddonMap, + type AddonReference, + type AddonResult, + type AddonSource, + type BaseQuestion, + type BooleanQuestion, + type ConfiguredAddon, + type FileEditor, + type FileType, + type InstallOptions, + type LanguageType, + type LoadedAddon, + type MultiSelectQuestion, + type NumberQuestion, + type OptionBuilder, + type OptionDefinition, + type OptionMap, + type OptionValues, + type PreparedAddon, + type Question, + type SelectQuestion, + type SetupResult, + type StringQuestion, + type SvApi, + type TemplateType, + type Workspace, + type WorkspaceOptions, + add, + create, + defineAddon, + defineAddonOptions, + officialAddons, +}; +``` diff --git a/packages/sv/src/addons/drizzle.ts b/packages/sv/src/addons/drizzle.ts index f807f11cc..c8bd70d79 100644 --- a/packages/sv/src/addons/drizzle.ts +++ b/packages/sv/src/addons/drizzle.ts @@ -206,7 +206,7 @@ export default defineAddon({ const hasPrettier = Boolean(dependencyVersion('prettier')); if (hasPrettier) { sv.file( - file.prettierignore, + '.prettierignore', transforms.text(({ content, text }) => text.upsert(content, '/drizzle/')) ); } diff --git a/packages/sv/src/addons/eslint.ts b/packages/sv/src/addons/eslint.ts index 58b1d00a4..4d6197676 100644 --- a/packages/sv/src/addons/eslint.ts +++ b/packages/sv/src/addons/eslint.ts @@ -31,7 +31,7 @@ export default defineAddon({ ); sv.file( - file.eslintConfig, + 'eslint.config.js', transforms.script(({ ast, comments, js }) => { const eslintConfigs: Array = []; js.imports.addDefault(ast, { from: './svelte.config.js', as: 'svelteConfig' }); @@ -156,14 +156,14 @@ export default defineAddon({ ); sv.file( - file.vscodeExtensions, + '.vscode/extensions.json', transforms.json(({ data, json }) => { json.arrayUpsert(data, 'recommendations', 'dbaeumer.vscode-eslint'); }) ); if (prettierInstalled) { - sv.file(file.eslintConfig, addEslintConfigPrettier); + sv.file('eslint.config.js', addEslintConfigPrettier); } } }); diff --git a/packages/sv/src/addons/prettier.ts b/packages/sv/src/addons/prettier.ts index eea8f1252..69629f2cc 100644 --- a/packages/sv/src/addons/prettier.ts +++ b/packages/sv/src/addons/prettier.ts @@ -16,7 +16,7 @@ export default defineAddon({ sv.devDependency('prettier-plugin-svelte', '^3.4.1'); sv.file( - file.prettierignore, + '.prettierignore', transforms.text(({ content }) => { if (content) return false; return dedent` @@ -34,7 +34,7 @@ export default defineAddon({ ); sv.file( - file.prettierrc, + '.prettierrc', transforms.json( ({ data, json }) => { if (Object.keys(data).length === 0) { @@ -82,7 +82,7 @@ export default defineAddon({ ); sv.file( - file.vscodeExtensions, + '.vscode/extensions.json', transforms.json(({ data, json }) => { json.arrayUpsert(data, 'recommendations', 'esbenp.prettier-vscode'); }) @@ -98,7 +98,7 @@ export default defineAddon({ if (eslintInstalled) { sv.devDependency('eslint-config-prettier', '^10.1.8'); - sv.file(file.eslintConfig, addEslintConfigPrettier); + sv.file('eslint.config.js', addEslintConfigPrettier); } } }); diff --git a/packages/sv/src/addons/sveltekit-adapter.ts b/packages/sv/src/addons/sveltekit-adapter.ts index dd3f9a3a2..4f2340da7 100644 --- a/packages/sv/src/addons/sveltekit-adapter.ts +++ b/packages/sv/src/addons/sveltekit-adapter.ts @@ -1,10 +1,10 @@ import { color, - resolveCommandArray, text, transforms, + resolveCommandArray, fileExists, - getPackageJson, + loadPackageJson, sanitizeName } from '@sveltejs/sv-utils'; import { defineAddon, defineAddonOptions } from '../core/config.ts'; @@ -140,7 +140,7 @@ export default defineAddon({ } if (!data.name) { - const pkg = getPackageJson(cwd); + const pkg = loadPackageJson(cwd); data.name = sanitizeName(pkg.data.name, 'wrangler'); } diff --git a/packages/sv/src/addons/tailwindcss.ts b/packages/sv/src/addons/tailwindcss.ts index 5dd26ce2c..97deeac55 100644 --- a/packages/sv/src/addons/tailwindcss.ts +++ b/packages/sv/src/addons/tailwindcss.ts @@ -106,7 +106,7 @@ export default defineAddon({ } sv.file( - file.vscodeSettings, + '.vscode/settings.json', transforms.json(({ data }) => { data['files.associations'] ??= {}; data['files.associations']['*.css'] = 'tailwindcss'; @@ -114,7 +114,7 @@ export default defineAddon({ ); sv.file( - file.vscodeExtensions, + '.vscode/extensions.json', transforms.json(({ data, json }) => { json.arrayUpsert(data, 'recommendations', 'bradlc.vscode-tailwindcss'); }) @@ -122,7 +122,7 @@ export default defineAddon({ if (prettierInstalled) { sv.file( - file.prettierrc, + '.prettierrc', transforms.json(({ data, json }) => { json.arrayUpsert(data, 'plugins', 'prettier-plugin-tailwindcss'); data.tailwindStylesheet ??= file.getRelative({ to: file.stylesheet }); diff --git a/packages/sv/src/cli/create.ts b/packages/sv/src/cli/create.ts index 65697c24e..8dddfe6ad 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, resolveCommandArray, commonFilePaths, getPackageJson } from '@sveltejs/sv-utils'; +import { color, commonFilePaths, loadPackageJson, resolveCommandArray } from '@sveltejs/sv-utils'; import { Command, Option } from 'commander'; import fs from 'node:fs'; import path from 'node:path'; @@ -324,7 +324,8 @@ async function createProject(cwd: ProjectPath, options: Options) { answers = result.answers; } - createKit(projectPath, { + createKit({ + cwd: projectPath, name: projectName, template, types: language @@ -464,7 +465,7 @@ export async function createVirtualWorkspace({ // Let's read the package.json of the template we will use and add the dependencies to the override const templatePackageJsonPath = dist(`templates/${template}`); - const { data: packageJson } = getPackageJson(templatePackageJsonPath); + const { data: packageJson } = loadPackageJson(templatePackageJsonPath); override.dependencies = { ...packageJson.devDependencies, ...packageJson.dependencies, diff --git a/packages/sv/src/core/common.ts b/packages/sv/src/core/common.ts index 856b8e0c5..aace74cc8 100644 --- a/packages/sv/src/core/common.ts +++ b/packages/sv/src/core/common.ts @@ -1,9 +1,9 @@ import * as p from '@clack/prompts'; import { - type AgentName, color, - resolveCommandArray, - isVersionUnsupportedBelow + isVersionUnsupportedBelow, + type AgentName, + resolveCommandArray } from '@sveltejs/sv-utils'; import type { Argument, Command, Help, HelpConfiguration, Option } from 'commander'; import fs from 'node:fs'; diff --git a/packages/sv/src/core/engine.ts b/packages/sv/src/core/engine.ts index b0bc4d053..b832dbc04 100644 --- a/packages/sv/src/core/engine.ts +++ b/packages/sv/src/core/engine.ts @@ -1,12 +1,13 @@ import * as p from '@clack/prompts'; import { color, - resolveCommand, - type AgentName, + commonFilePaths, fileExists, - installPackages, - readFile, - writeFile + loadFile, + loadPackageJson, + saveFile, + resolveCommand, + type AgentName } from '@sveltejs/sv-utils'; import { NonZeroExitError, exec } from 'tinyexec'; import { createLoadedAddon } from '../cli/add.ts'; @@ -22,6 +23,38 @@ import { import { TESTING } from './env.ts'; import { createWorkspace, type Workspace } from './workspace.ts'; +function alphabetizePackageJsonDependencies(obj: Record) { + const ordered: Record = {}; + for (const [key, value] of Object.entries(obj).sort(([a], [b]) => a.localeCompare(b))) { + ordered[key] = value; + } + return ordered; +} + +function updatePackages( + dependencies: Array<{ pkg: string; version: string; dev: boolean }>, + cwd: string +): string { + const { data, generateCode } = loadPackageJson(cwd); + + for (const dependency of dependencies) { + if (dependency.dev) { + data.devDependencies ??= {}; + data.devDependencies[dependency.pkg] = dependency.version; + } else { + data.dependencies ??= {}; + data.dependencies[dependency.pkg] = dependency.version; + } + } + + if (data.dependencies) data.dependencies = alphabetizePackageJsonDependencies(data.dependencies); + if (data.devDependencies) + data.devDependencies = alphabetizePackageJsonDependencies(data.devDependencies); + + saveFile(cwd, commonFilePaths.packageJson, generateCode()); + return commonFilePaths.packageJson; +} + export type InstallOptions = { cwd: string; addons: Addons; @@ -180,11 +213,11 @@ async function runAddon({ addon, loaded, multiple, workspace, workspaceOptions } const sv: SvApi = { file: (path, edit) => { try { - const content = fileExists(workspace.cwd, path) ? readFile(workspace.cwd, path) : ''; + const content = fileExists(workspace.cwd, path) ? loadFile(workspace.cwd, path) : ''; const editedContent = edit(content); if (editedContent === '' || editedContent === false) return content; - writeFile(workspace.cwd, path, editedContent); + saveFile(workspace.cwd, path, editedContent); files.add(path); } catch (e) { if (e instanceof Error) { @@ -250,7 +283,7 @@ async function runAddon({ addon, loaded, multiple, workspace, workspaceOptions } } if (cancels.length === 0) { - const pkgPath = installPackages(dependencies, workspace.cwd); + const pkgPath = updatePackages(dependencies, workspace.cwd); files.add(pkgPath); } diff --git a/packages/sv/src/core/package-manager.ts b/packages/sv/src/core/package-manager.ts index c0ed185c7..3edb1aaf9 100644 --- a/packages/sv/src/core/package-manager.ts +++ b/packages/sv/src/core/package-manager.ts @@ -3,9 +3,9 @@ import { AGENTS, type AgentName, COMMANDS, - color, constructCommand, detect, + color, isVersionUnsupportedBelow, parse } from '@sveltejs/sv-utils'; @@ -116,7 +116,9 @@ export async function addPnpmBuildDependencies( const content = found ? fs.readFileSync(found, 'utf-8') : ''; const { data, generateCode } = parse.yaml(content); - const onlyBuiltDependencies = data.get('onlyBuiltDependencies'); + const onlyBuiltDependencies = data.get('onlyBuiltDependencies') as + | { items?: Array<{ value: string } | string> } + | undefined; const items: Array<{ value: string } | string> = onlyBuiltDependencies?.items ?? []; for (const item of allowedPackages) { diff --git a/packages/sv/src/core/workspace.ts b/packages/sv/src/core/workspace.ts index 987787419..b04af63f4 100644 --- a/packages/sv/src/core/workspace.ts +++ b/packages/sv/src/core/workspace.ts @@ -4,8 +4,8 @@ import { js, parse, commonFilePaths, - getPackageJson, - readFile + loadFile, + loadPackageJson } from '@sveltejs/sv-utils'; import * as find from 'empathic/find'; import fs from 'node:fs'; @@ -37,13 +37,6 @@ export type Workspace = { package: 'package.json'; gitignore: '.gitignore'; - prettierignore: '.prettierignore'; - prettierrc: '.prettierrc'; - eslintConfig: 'eslint.config.js'; - - vscodeSettings: '.vscode/settings.json'; - vscodeExtensions: '.vscode/extensions.json'; - /** Get the relative path between two files */ getRelative: ({ from, to }: { from?: string; to: string }) => string; }; @@ -103,7 +96,7 @@ export async function createWorkspace({ directory.length >= workspaceRoot.length ) { if (fs.existsSync(path.join(directory, commonFilePaths.packageJson))) { - const { data: packageJson } = getPackageJson(directory); + const { data: packageJson } = loadPackageJson(directory); dependencies = { ...packageJson.devDependencies, ...packageJson.dependencies, @@ -142,11 +135,6 @@ export async function createWorkspace({ stylesheet, package: 'package.json', gitignore: '.gitignore', - prettierignore: '.prettierignore', - prettierrc: '.prettierrc', - eslintConfig: 'eslint.config.js', - vscodeSettings: '.vscode/settings.json', - vscodeExtensions: '.vscode/extensions.json', getRelative({ from, to }) { from = from ?? ''; let relativePath = path.posix.relative(path.posix.dirname(from), to); @@ -173,7 +161,7 @@ function findWorkspaceRoot(cwd: string): string { return directory; } // in other package managers it's a workspaces key in the package.json - const { data } = getPackageJson(directory); + const { data } = loadPackageJson(directory); if (data.workspaces) { return directory; } @@ -189,7 +177,7 @@ function findWorkspaceRoot(cwd: string): string { } function parseKitOptions(cwd: string, svelteConfigPath: string) { - const configSource = readFile(cwd, svelteConfigPath); + const configSource = loadFile(cwd, svelteConfigPath); const { ast } = parse.script(configSource); const defaultExport = ast.body.find((s) => s.type === 'ExportDefaultDeclaration'); diff --git a/packages/sv/src/create/index.ts b/packages/sv/src/create/index.ts index 5a2947cbe..5b78b6c45 100644 --- a/packages/sv/src/create/index.ts +++ b/packages/sv/src/create/index.ts @@ -11,6 +11,7 @@ const templateTypes = ['minimal', 'demo', 'library', 'addon', 'svelte'] as const const languageTypes = ['typescript', 'checkjs', 'none'] as const; export type Options = { + cwd: string; name: string; template: TemplateType; types: LanguageType; @@ -32,7 +33,19 @@ export type Common = { }>; }; -export function create(cwd: string, options: Options): void { +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; + } + mkdirp(cwd); write_template_files(options.template, options.types, options.name, cwd); @@ -76,7 +89,7 @@ function write_template_files(template: string, types: LanguageType, name: strin }); } -function write_common_files(cwd: string, options: Options, name: string) { +function write_common_files(cwd: string, options: Omit, name: string) { const files = getSharedFiles(); const pkg_file = path.join(cwd, commonFilePaths.packageJson); @@ -105,7 +118,7 @@ function write_common_files(cwd: string, options: Options, name: string) { fs.writeFileSync(pkg_file, JSON.stringify(pkg, null, '\t') + '\n'); } -function matches_condition(condition: Condition, options: Options) { +function matches_condition(condition: Condition, options: Omit) { if (templateTypes.includes(condition as TemplateType)) { return options.template === condition; } diff --git a/packages/sv/src/create/tests/check.ts b/packages/sv/src/create/tests/check.ts index e00b3c33f..c214a375b 100644 --- a/packages/sv/src/create/tests/check.ts +++ b/packages/sv/src/create/tests/check.ts @@ -42,7 +42,7 @@ for (const template of templates.filter((t) => t !== 'addon')) { const cwd = path.join(test_workspace_dir, `${template}-${types}`); fs.rmSync(cwd, { recursive: true, force: true }); - create(cwd, { name: `create-svelte-test-${template}-${types}`, template, types }); + create({ cwd, name: `create-svelte-test-${template}-${types}`, template, types }); await add({ cwd, addons: { eslint: officialAddons.eslint }, options: { eslint: {} } }); const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8')); diff --git a/packages/sv/src/create/tests/playground.ts b/packages/sv/src/create/tests/playground.ts index fbc79b5d1..5d7778de0 100644 --- a/packages/sv/src/create/tests/playground.ts +++ b/packages/sv/src/create/tests/playground.ts @@ -153,7 +153,8 @@ test('real world download and convert playground async', async () => { fs.rmSync(directory, { recursive: true }); } - create(directory, { + create({ + cwd: directory, name: 'real-world-playground', template: 'minimal', types: 'typescript' @@ -204,7 +205,8 @@ test('real world download and convert playground without async', async () => { fs.rmSync(directory, { recursive: true }); } - create(directory, { + create({ + cwd: directory, name: 'real-world-playground-old', template: 'minimal', types: 'typescript' diff --git a/packages/sv/src/index.ts b/packages/sv/src/index.ts index 874abb260..3c3fe50b0 100644 --- a/packages/sv/src/index.ts +++ b/packages/sv/src/index.ts @@ -4,7 +4,37 @@ export type { AddonMap, InstallOptions, OptionMap } from './core/engine.ts'; export { officialAddons } from './addons/index.ts'; // Addon authoring API export { defineAddon, defineAddonOptions } from './core/config.ts'; -export type * from './core/processors.ts'; -export type * from './core/options.ts'; -export type * from './core/config.ts'; -export type * from './core/workspace.ts'; + +// options.ts - question types for addon options +export type { + Question, + OptionDefinition, + OptionValues, + BooleanQuestion, + StringQuestion, + NumberQuestion, + SelectQuestion, + MultiSelectQuestion, + BaseQuestion +} from './core/options.ts'; + +// config.ts - addon definition and pipeline types +export type { + Addon, + SvApi, + AddonDefinition, + SetupResult, + OptionBuilder, + AddonInput, + AddonSource, + AddonReference, + LoadedAddon, + PreparedAddon, + ConfiguredAddon, + AddonResult +} from './core/config.ts'; + +// workspace.ts +export type { Workspace, WorkspaceOptions } from './core/workspace.ts'; + +export type { FileEditor, FileType } from './core/processors.ts'; diff --git a/packages/sv/src/testing.ts b/packages/sv/src/testing.ts index 285592dd5..5c892e605 100644 --- a/packages/sv/src/testing.ts +++ b/packages/sv/src/testing.ts @@ -10,6 +10,7 @@ import { add, type AddonMap, type OptionMap } from './core/engine.ts'; import { addPnpmBuildDependencies } from './core/package-manager.ts'; import { create } from './create/index.ts'; +/** @deprecated Internal helper - will be removed from public API in a future version. */ export { addPnpmBuildDependencies } from './core/package-manager.ts'; export type ProjectVariant = 'kit-js' | 'kit-ts' | 'vite-js' | 'vite-ts'; export const variants: ProjectVariant[] = ['kit-js', 'kit-ts', 'vite-js', 'vite-ts']; @@ -29,6 +30,7 @@ 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 } { const workingDir = path.resolve(cwd); if (clean && fs.existsSync(workingDir)) { @@ -43,13 +45,13 @@ export function setup({ cwd, clean = false, variants }: SetupOptions): { templat if (fs.existsSync(templatePath)) continue; if (variant === 'kit-js') { - create(templatePath, { name: variant, template: 'minimal', types: 'checkjs' }); + create({ cwd: templatePath, name: variant, template: 'minimal', types: 'checkjs' }); } else if (variant === 'kit-ts') { - create(templatePath, { name: variant, template: 'minimal', types: 'typescript' }); + create({ cwd: templatePath, name: variant, template: 'minimal', types: 'typescript' }); } else if (variant === 'vite-js') { - create(templatePath, { name: variant, template: 'svelte', types: 'none' }); + create({ cwd: templatePath, name: variant, template: 'svelte', types: 'none' }); } else if (variant === 'vite-ts') { - create(templatePath, { name: variant, template: 'svelte', types: 'typescript' }); + create({ cwd: templatePath, name: variant, template: 'svelte', types: 'typescript' }); } else { throw new Error(`Unknown project variant: ${variant}`); } @@ -59,6 +61,7 @@ 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 { // create the reference dir const testDir = path.resolve(cwd, testName); @@ -75,6 +78,7 @@ 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({ cwd, command = 'npm run preview' diff --git a/scripts/generate-api-surface.js b/scripts/generate-api-surface.js new file mode 100644 index 000000000..bc7a5deaf --- /dev/null +++ b/scripts/generate-api-surface.js @@ -0,0 +1,141 @@ +/** + * 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. + * + * Run: node scripts/generate-api-surface.js + * Or: invoked from tsdown `build:done` after all configs finish (see tsdown.config.ts). + * + * Finishes with Prettier (repo root config) so snapshots match `pnpm format`. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import * as prettier from 'prettier'; + +const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url))); + +const packages = [ + { + name: 'sv', + dts: 'packages/sv/dist/src/index.d.mts', + out: 'packages/sv/api-surface.md' + }, + { + name: 'sv (testing)', + dts: 'packages/sv/dist/src/testing.d.mts', + out: 'packages/sv/api-surface-testing.md' + }, + { + name: '@sveltejs/sv-utils', + dts: 'packages/sv-utils/dist/index.d.mts', + out: 'packages/sv-utils/api-surface.md' + } +]; + +/** Remove `//#region` / `//#endregion` lines emitted by the DTS bundler. */ +function stripRegionDirectives(source) { + return source + .split('\n') + .filter((line) => !/^\s*\/\/#(region|endregion)\b/.test(line)) + .join('\n'); +} + +/** Remove `//# sourceMappingURL=...` lines from declaration emit. */ +function stripSourceMappingUrl(source) { + return source + .split('\n') + .filter((line) => !/^\s*\/\/#\s*sourceMappingURL=/.test(line)) + .join('\n'); +} + +/** + * Remove `/** ... *\/` blocks unless they contain `@deprecated`, in which case + * the full block is preserved (including inline trailing JSDoc on a line). + */ +function stripJsDoc(source) { + return source.replace(/\/\*\*[\s\S]*?\*\//g, (match) => { + if (/@deprecated\b/.test(match)) { + return match; + } + return ''; + }); +} + +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'); +} + +function collapseBlankLines(source) { + return source.replace(/\n{3,}/g, '\n\n'); +} + +function clean(source) { + let result = stripRegionDirectives(source); + result = stripSourceMappingUrl(result); + result = stripJsDoc(result); + result = stripImportLines(result); + result = collapseBlankLines(result); + return result.trim() + '\n'; +} + +/** + * @param {string} absPath absolute path to the markdown file + */ +async function formatWithPrettier(absPath) { + const raw = fs.readFileSync(absPath, 'utf8'); + const formatted = await prettier.format(raw, { filepath: absPath }); + fs.writeFileSync(absPath, formatted, 'utf8'); +} + +/** @returns {Promise} number of api-surface files written */ +export async function generateApiSurface() { + let generated = 0; + for (const pkg of packages) { + const dtsPath = path.resolve(ROOT, pkg.dts); + if (!fs.existsSync(dtsPath)) { + console.warn(` skipped ${pkg.name} - ${pkg.dts} not found (run build first)`); + continue; + } + + const raw = fs.readFileSync(dtsPath, 'utf8'); + const cleaned = clean(raw); + + const header = + `# ${pkg.name} - Public API Surface\n\n` + + `\n\n` + + '```ts\n'; + const footer = '```\n'; + + const outPath = path.resolve(ROOT, pkg.out); + fs.writeFileSync(outPath, header + cleaned + footer, 'utf8'); + await formatWithPrettier(outPath); + generated++; + console.log(` ${pkg.name} -> ${pkg.out}`); + } + + if (generated === 0) { + console.warn('No .d.mts files found - run `pnpm build` first.'); + process.exit(1); + } + + return generated; +} + +const isMain = + process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]); + +if (isMain) { + generateApiSurface().catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/tsdown.config.ts b/tsdown.config.ts index 1a564bdcc..ce4017a73 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -3,6 +3,23 @@ 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'), @@ -57,7 +74,8 @@ export default defineConfig([ hooks: { async 'build:before'() { await buildCliTemplates(); - } + }, + 'build:done': () => hookApiSurfaceBuildDone() } }, // sv-utils: runtime build (bundles everything including svelte) @@ -88,6 +106,9 @@ export default defineConfig([ 'yaml', 'zimmerframe' ] + }, + hooks: { + 'build:done': () => hookApiSurfaceBuildDone() } }, // sv-utils: DTS-only build (svelte externalized) @@ -103,8 +124,18 @@ export default defineConfig([ }, failOnWarn: true, deps: { - neverBundle: [/^svelte/, '@types/estree', 'estree'], - onlyBundle: ['dedent', 'package-manager-detector', 'smol-toml', 'yaml', 'zimmerframe'] + neverBundle: [ + /^svelte/, + '@types/estree', + 'estree', + 'yaml', + 'dedent', + 'package-manager-detector' + ], + onlyBundle: ['smol-toml', 'zimmerframe'] + }, + hooks: { + 'build:done': () => hookApiSurfaceBuildDone() } } ]);