diff --git a/.changeset/wide-waves-read.md b/.changeset/wide-waves-read.md new file mode 100644 index 00000000..81f8c524 --- /dev/null +++ b/.changeset/wide-waves-read.md @@ -0,0 +1,22 @@ +--- +'vite-plugin-stripper': minor +--- + +You should STOP using `decorators` and `hard` in favor of using the new `strip` config! +```ts +strip: [ + { decorator: 'BackendMethod' }, + { + decorator: 'Entity', + args_1: [ + { fn: 'backendPrefilter' }, + { fn: 'backendPreprocessFilter' }, + { fn: 'sqlExpression' }, + // { + // fn: 'saved', + // excludeEntityKeys: ['users'] + // } + ] + } +] +``` diff --git a/docs/src/content/docs/docs/tools/07_vite-plugin-stripper.mdx b/docs/src/content/docs/docs/tools/07_vite-plugin-stripper.mdx index 8e9f800e..41158791 100644 --- a/docs/src/content/docs/docs/tools/07_vite-plugin-stripper.mdx +++ b/docs/src/content/docs/docs/tools/07_vite-plugin-stripper.mdx @@ -23,7 +23,7 @@ import { stripper } from 'vite-plugin-stripper' export default defineConfig({ plugins: [ // To strip `@BackendMethod` from your browser bundle - stripper({ decorators: ['BackendMethod'] }), + stripper({ strip: ['BackendMethod'] }), sveltekit(), ], }) @@ -35,6 +35,24 @@ export default defineConfig({ npm i -D vite-plugin-stripper ``` -## Configuration +## Advanced configuration -🚧🚧🚧 +```ts +stripper({ + strip: [ + { decorator: 'BackendMethod' }, + { + decorator: 'Entity', + args_1: [ + { fn: 'backendPrefilter' }, + { fn: 'backendPreprocessFilter' }, + { fn: 'sqlExpression' }, + // { + // fn: 'saved', + // excludeEntityKeys: ['users'] + // } + ], + }, + ], +}) +``` diff --git a/package.json b/package.json index 5e68b31f..6bf0d4d6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@changesets/cli": "catalog:lib-author-helper", "@vitest/coverage-v8": "catalog:testing", "esbuild": "catalog:tooling", + "prettier": "catalog:linting", "rimraf": "catalog:tooling" }, "pnpm": { diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 35affe8c..febd1cce 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -34,6 +34,9 @@ "lint:example": "kitql-lint", "inspector": "npx @eslint/config-inspector" }, + "peerDependencies": { + "prettier": "catalog:linting" + }, "dependencies": { "@eslint/compat": "catalog:linting", "@eslint/js": "catalog:linting", diff --git a/packages/helpers/eslint.config.js b/packages/helpers/eslint.config.js index 3202d429..b84f7b48 100644 --- a/packages/helpers/eslint.config.js +++ b/packages/helpers/eslint.config.js @@ -1,3 +1,3 @@ -import kitql from '@kitql/eslint-config' +import { kitql } from '@kitql/eslint-config' -export default [...kitql] +export default [...kitql()] diff --git a/packages/vite-plugin-stripper/.gitignore b/packages/vite-plugin-stripper/.gitignore new file mode 100644 index 00000000..19e01807 --- /dev/null +++ b/packages/vite-plugin-stripper/.gitignore @@ -0,0 +1 @@ +db/ diff --git a/packages/vite-plugin-stripper/package.json b/packages/vite-plugin-stripper/package.json index 45920e84..29f12cc2 100644 --- a/packages/vite-plugin-stripper/package.json +++ b/packages/vite-plugin-stripper/package.json @@ -34,6 +34,7 @@ "@sveltejs/kit": "catalog:sveltekit", "@sveltejs/package": "catalog:sveltekit", "@sveltejs/vite-plugin-svelte": "catalog:sveltekit", + "@vitest/ui": "catalog:", "publint": "catalog:lib-author-helper", "remult": "catalog:remult", "svelte": "catalog:svelte", diff --git a/packages/vite-plugin-stripper/src/hooks.server.ts b/packages/vite-plugin-stripper/src/hooks.server.ts index cee9d5a9..e8caef32 100644 --- a/packages/vite-plugin-stripper/src/hooks.server.ts +++ b/packages/vite-plugin-stripper/src/hooks.server.ts @@ -1,5 +1,5 @@ import { sequence } from '@sveltejs/kit/hooks' -import { handleRemult } from './hooks/handleRemult.js' +import { api as handleRemult } from './server/api.js' export const handle = sequence(handleRemult) diff --git a/packages/vite-plugin-stripper/src/lib/ast.spec.ts b/packages/vite-plugin-stripper/src/lib/ast.spec.ts new file mode 100644 index 00000000..30a47145 --- /dev/null +++ b/packages/vite-plugin-stripper/src/lib/ast.spec.ts @@ -0,0 +1,170 @@ +import { describe, expect, it } from 'vitest' + +import { imports, usage } from './ast.js' + +describe('imports', () => { + it('should return an import a', () => { + const data = imports('import { a } from "lib"') + expect(data.importsList).toMatchInlineSnapshot(` + [ + { + "localName": undefined, + "name": "a", + "source": "lib", + "type": "named", + }, + ] + `) + }) + + it('should return an import a and b', () => { + const data = imports('import { a, b } from "lib"') + expect(data.importsList).toMatchInlineSnapshot(` + [ + { + "localName": undefined, + "name": "a", + "source": "lib", + "type": "named", + }, + { + "localName": undefined, + "name": "b", + "source": "lib", + "type": "named", + }, + ] + `) + }) + + it('should return an import a and b and c as d', () => { + const data = imports('import { a, b, c as d } from "lib"') + expect(data.importsList).toMatchInlineSnapshot(` + [ + { + "localName": undefined, + "name": "a", + "source": "lib", + "type": "named", + }, + { + "localName": undefined, + "name": "b", + "source": "lib", + "type": "named", + }, + { + "localName": "d", + "name": "c", + "source": "lib", + "type": "named", + }, + ] + `) + }) + + it('should return an import a and type b', () => { + const data = imports('import { a, type b } from "lib"') + expect(data.importsList).toMatchInlineSnapshot(` + [ + { + "localName": undefined, + "name": "a", + "source": "lib", + "type": "named", + }, + { + "localName": undefined, + "name": "b", + "source": "lib", + "type": "type", + }, + ] + `) + }) + + it('should return an import types a and b', () => { + const data = imports('import type { a, b } from "lib"') + expect(data.importsList).toMatchInlineSnapshot(` + [ + { + "localName": undefined, + "name": "a", + "source": "lib", + "type": "type", + }, + { + "localName": undefined, + "name": "b", + "source": "lib", + "type": "type", + }, + ] + `) + }) + + it('should return * stuff as toto', () => { + const data = imports('import * as toto from "lib"') + expect(data.importsList).toMatchInlineSnapshot(` + [ + { + "name": "toto", + "source": "lib", + "type": "namespace", + }, + ] + `) + }) + + it('should handle bare imports', () => { + const data = imports('import "lib"') + expect(data.importsList).toMatchInlineSnapshot(` + [ + { + "name": "default", + "source": "lib", + "type": "default", + }, + ] + `) + }) + + it('should handle named imports', () => { + const data = imports(`import { default as Icon } from './ui/Icon.svelte'`) + expect(data.importsList).toMatchInlineSnapshot(` + [ + { + "localName": "Icon", + "name": "default", + "source": "./ui/Icon.svelte", + "type": "named", + }, + ] + `) + }) + + it('should handle default imports', () => { + const data = imports('import DefaultExport from "lib"') + expect(data.importsList).toMatchInlineSnapshot(` + [ + { + "name": "DefaultExport", + "source": "lib", + "type": "default", + }, + ] + `) + }) +}) + +describe('usage', () => { + it('should return an usage', () => { + const code = `import { a } from "lib" + const b = a` + + const importsData = imports(code) + const usageData = usage(code, importsData.importsList) + + expect(usageData.importsList).toMatchInlineSnapshot(`[]`) + }) +}) diff --git a/packages/vite-plugin-stripper/src/lib/ast.ts b/packages/vite-plugin-stripper/src/lib/ast.ts new file mode 100644 index 00000000..b50fc3f4 --- /dev/null +++ b/packages/vite-plugin-stripper/src/lib/ast.ts @@ -0,0 +1,86 @@ +import { parseTs, visit } from '@kitql/internals' + +export type ImportInfo = { + name: string + type: 'default' | 'namespace' | 'named' | 'type' + localName?: string + source: string +} + +export type UserInfo = { + used: boolean +} + +export const imports = ( + code_or_program: string | ReturnType, +): { program: ReturnType; importsList: ImportInfo[] } => { + const program = typeof code_or_program === 'string' ? parseTs(code_or_program) : code_or_program + const importsList: ImportInfo[] = [] + + visit(program, { + visitImportDeclaration(path: any) { + const source = path.node.source.value as string + const isTypeOnly = path.node.importKind === 'type' + + // Handle bare imports (import "source") + if (!path.node.specifiers || path.node.specifiers.length === 0) { + importsList.push({ + name: 'default', + type: 'default', + source, + }) + return false + } + + // Process all specifiers in this import declaration + path.node.specifiers?.forEach((specifier: any) => { + if (specifier.type === 'ImportDefaultSpecifier') { + // Default import: import Name from 'source' + importsList.push({ + name: specifier.local.name, + type: 'default', + source, + }) + } else if (specifier.type === 'ImportNamespaceSpecifier') { + // Namespace import: import * as Name from 'source' + importsList.push({ + name: specifier.local.name, + type: 'namespace', + source, + }) + } else if (specifier.type === 'ImportSpecifier') { + // Named import: import { name } from 'source' + // or type import: import type { name } from 'source' + const importedName = specifier.imported?.name || specifier.local.name + const localName = specifier.local.name + + // Handle type imports + const importType = isTypeOnly || specifier.importKind === 'type' ? 'type' : 'named' + + importsList.push({ + name: importedName, + type: importType, + localName: importedName !== localName ? localName : undefined, + source, + }) + } + }) + + return false + }, + }) + + return { program, importsList } +} + +export const usage = ( + code_or_program: string | ReturnType, + importsList: ImportInfo[], +): { program: ReturnType; importsList: ImportInfo[] } => { + const program = typeof code_or_program === 'string' ? parseTs(code_or_program) : code_or_program + if (importsList.length === 0) return { program, importsList: [] } + + const usageList: (ImportInfo & UserInfo)[] = [] + + return { program, importsList: [] } +} diff --git a/packages/vite-plugin-stripper/src/lib/transformPackage.spec.ts b/packages/vite-plugin-stripper/src/lib/nullifyImports.spec.ts similarity index 88% rename from packages/vite-plugin-stripper/src/lib/transformPackage.spec.ts rename to packages/vite-plugin-stripper/src/lib/nullifyImports.spec.ts index e0e84976..6a2a3e44 100644 --- a/packages/vite-plugin-stripper/src/lib/transformPackage.spec.ts +++ b/packages/vite-plugin-stripper/src/lib/nullifyImports.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { removePackages } from './transformPackage.js' +import { nullifyImports } from './nullifyImports.js' describe('package', () => { it('1 replace', async () => { @@ -23,7 +23,7 @@ describe('package', () => { } ` - const transformed = await removePackages(code, ['mongodb']) + const transformed = await nullifyImports(code, ['mongodb']) expect(transformed).toMatchInlineSnapshot(` { @@ -47,7 +47,7 @@ describe('package', () => { aMongoDbIdField = ""; }", "info": [ - "Replaced import from 'mongodb'", + "Nullify import from 'mongodb'", ], } `) @@ -72,7 +72,7 @@ describe('package', () => { } ` - const transformed = await removePackages(code, ['mongodb']) + const transformed = await nullifyImports(code, ['mongodb']) expect(transformed).toMatchInlineSnapshot(` { @@ -96,7 +96,7 @@ describe('package', () => { aMongoDbIdField = ""; }", "info": [ - "Replaced import from 'mongodb'", + "Nullify import from 'mongodb'", ], } `) diff --git a/packages/vite-plugin-stripper/src/lib/transformPackage.ts b/packages/vite-plugin-stripper/src/lib/nullifyImports.ts similarity index 88% rename from packages/vite-plugin-stripper/src/lib/transformPackage.ts rename to packages/vite-plugin-stripper/src/lib/nullifyImports.ts index 4dd401a7..109a9e60 100644 --- a/packages/vite-plugin-stripper/src/lib/transformPackage.ts +++ b/packages/vite-plugin-stripper/src/lib/nullifyImports.ts @@ -1,6 +1,6 @@ import { builders, parseTs, prettyPrint, visit } from '@kitql/internals' -export const removePackages = async (code: string, packages_to_strip: string[]) => { +export const nullifyImports = async (code: string, packages_to_strip: string[]) => { try { const ast = parseTs(code) @@ -37,7 +37,7 @@ export const removePackages = async (code: string, packages_to_strip: string[]) return { code: prettyPrint(ast).code, - info: packages_striped.map((pkg) => `Replaced import from '${pkg}'`), + info: packages_striped.map((pkg) => `Nullify import from '${pkg}'`), } } catch (error) { return { code, info: [] } diff --git a/packages/vite-plugin-stripper/src/lib/plugin.spec.ts b/packages/vite-plugin-stripper/src/lib/plugin.spec.ts new file mode 100644 index 00000000..ee402707 --- /dev/null +++ b/packages/vite-plugin-stripper/src/lib/plugin.spec.ts @@ -0,0 +1,268 @@ +import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs' +import { join } from 'path' +import { build } from 'vite' +import { afterEach, describe, expect, it } from 'vitest' + +import { read } from '@kitql/internals' + +import type { ViteStripperOptions } from './plugin.js' + +describe('Plugin build output', () => { + // Helper function to set up test environment + async function setupTestEnvironment(code: string, stripperConfig: ViteStripperOptions) { + // Create a temporary directory for the test + const tempDir = join(process.cwd(), 'temp-test-build') + + // Clean up any previous test runs + rmSync(tempDir, { recursive: true, force: true }) + mkdirSync(tempDir, { recursive: true }) + mkdirSync(join(tempDir, 'src'), { recursive: true }) + + // Transform the code with our decorator transformer + // await nullifyImports(code, nullify) + // const transformed = await transformStrip(code, decoratorsConfig) + // console.info('Transformed code:', transformed.code) + + // Write the transformed code to a temporary file + const inputFile = join(tempDir, 'src', 'input.ts') + writeFileSync(inputFile, code) + + // Create a simple package.json + const packageJsonFile = join(tempDir, 'package.json') + writeFileSync( + packageJsonFile, + JSON.stringify({ + name: 'test-build', + type: 'module', + }), + ) + + // Create a tsconfig.json + const tsconfigFile = join(tempDir, 'tsconfig.json') + writeFileSync( + tsconfigFile, + JSON.stringify({ + compilerOptions: { + target: 'ESNext', + useDefineForClassFields: true, + module: 'ESNext', + lib: ['ESNext', 'DOM'], + moduleResolution: 'Node', + strict: true, + resolveJsonModule: true, + isolatedModules: true, + esModuleInterop: true, + noEmit: true, + noUnusedLocals: true, + noUnusedParameters: true, + noImplicitReturns: true, + experimentalDecorators: true, + }, + include: ['src/**/*.ts'], + }), + ) + + // Create a simple index.html + const indexHtmlFile = join(tempDir, 'index.html') + writeFileSync( + indexHtmlFile, + ` + + + + + Test + + + + + + `, + ) + + // Create a simple Vite config + const viteConfigFile = join(tempDir, 'vite.config.js') + writeFileSync( + viteConfigFile, + ` + import { defineConfig } from 'vite'; + import { stripper } from '../src/lib/plugin.js'; + + export default defineConfig({ + build: { + outDir: 'dist', + minify: false, + rollupOptions: { + output: { + entryFileNames: 'bundle.js' + }, + } + }, + plugins: [ + stripper(${JSON.stringify(stripperConfig)}), + ], + resolve: { + alias: { + '$env/static/private': '${join(tempDir, 'src', 'env-mock.js')}' + } + } + }); + `, + ) + + // Create a mock for $env/static/private + const envMockFile = join(tempDir, 'src', 'env-mock.js') + writeFileSync(envMockFile, `export const AUTH_SECRET = 'FAKE_SECRET_FOR_TEST';`) + + // Run Vite build + await build({ + configFile: viteConfigFile, + root: tempDir, + logLevel: 'info', + }) + + // Read the output file + const outputFile = join(tempDir, 'dist', 'bundle.js') + const outputContent = readFileSync(outputFile, 'utf-8') + + return { outputContent, tempDir } + } + + afterEach(() => { + rmSync(join(process.cwd(), 'temp-test-build'), { + recursive: true, + force: true, + }) + }) + + describe('BackendMethod', async () => { + ;[ + { + name: 'no strip', + strip: [], + expects: (outputContent: string) => { + expect(outputContent).not.toContain('import.meta.env.SSR') + expect(outputContent).toContain('SECRET_123') + expect(outputContent).toContain('FAKE_SECRET_FOR_TEST') + expect(outputContent).toContain('This should only run on the server') + }, + }, + { + name: 'strip', + strip: [{ decorator: 'BackendMethod' }], + expects: (outputContent: string) => { + // Verify the output that should NOT be present + expect(outputContent).not.toContain('import.meta.env.SSR') + expect(outputContent).not.toContain('SECRET_123') + expect(outputContent).not.toContain('FAKE_SECRET_FOR_TEST') + expect(outputContent).not.toContain('This should only run on the server') + }, + }, + ].forEach(async (input) => { + it(input.name, async () => { + // Sample code with BackendMethod decorator + const code = ` + import { Allow, BackendMethod, remult } from "remult"; + import { AUTH_SECRET } from '$env/static/private'; + + export class TasksController { + static async regularMethod(completed: boolean) { + const result = "This is a regular method"; + console.log(result); + return result; + } + + @BackendMethod({ allowed: Allow.authenticated }) + static async backendMethod(completed: boolean) { + console.log("This should only run on the server"); + + // This is the code that should only be included in SSR builds + const secretValue = "SECRET_123"; + console.log("Secret value:", secretValue); + console.log("Secret AUTH_SECRET:", AUTH_SECRET); + + return "Backend operation completed"; + } + }` + + const { outputContent } = await setupTestEnvironment(code, { + strip: input.strip, + }) + + input.expects(outputContent) + + // The regular method should be there + expect(outputContent).toContain('This is a regular method') + + // The BackendMethod decorator should always be present + expect(outputContent).toContain('BackendMethod') + expect(outputContent).toContain('backendMethod') + }) + }) + }) + + describe('Entity + BackendMethod, methods', async () => { + ;[ + { + name: 'no strip', + strip: [], + expects: (outputContent: string) => { + expect(outputContent).not.toContain('import.meta.env.SSR') + expect(outputContent).toContain('AUTH_SECRET') + expect(outputContent).toContain('backendPrefilter_top_secret') + expect(outputContent).toContain('AUTH_SECRET_backendPrefilter') + expect(outputContent).toContain('backendPreprocessFilter_top_secret') + expect(outputContent).toContain('AUTH_SECRET_backendPreprocessFilter') + expect(outputContent).toContain('sqlExpression_top_secret') + expect(outputContent).toContain('AUTH_SECRET_sqlExpression') + }, + }, + { + name: 'strip', + strip: [ + { decorator: 'BackendMethod' }, + { + decorator: 'Entity', + args_1: [ + { fn: 'backendPrefilter' }, + { fn: 'backendPreprocessFilter' }, + { fn: 'sqlExpression' }, + ], + }, + ], + expects: (outputContent: string) => { + expect(outputContent).not.toContain('import.meta.env.SSR') + expect(outputContent).not.toContain('AUTH_SECRET') + expect(outputContent).not.toContain('backendPrefilter_top_secret') + expect(outputContent).not.toContain('AUTH_SECRET_backendPrefilter') + expect(outputContent).not.toContain('backendPreprocessFilter_top_secret') + expect(outputContent).not.toContain('AUTH_SECRET_backendPreprocessFilter') + expect(outputContent).not.toContain('sqlExpression_top_secret') + expect(outputContent).not.toContain('AUTH_SECRET_sqlExpression') + }, + }, + ].forEach(async (input) => { + it(input.name, async () => { + const code = read(join(process.cwd(), 'src', 'shared', 'User.ts')) ?? '' + + const { outputContent } = await setupTestEnvironment(code, { + strip: input.strip, + // debug: true, + nullify: ['$env/static/private'], + }) + + input.expects(outputContent) + + // The Entity decorator and class structure should be there + expect(outputContent).toContain('Entity') + expect(outputContent).toContain('User') + expect(outputContent).toContain('Fields.uuid') + expect(outputContent).toContain('Fields.string') + + // The BackendMethod decorator should be there + expect(outputContent).toContain('BackendMethod') + expect(outputContent).toContain('hi') + }) + }) + }) +}) diff --git a/packages/vite-plugin-stripper/src/lib/plugin.ts b/packages/vite-plugin-stripper/src/lib/plugin.ts index a20d1821..91d6e884 100644 --- a/packages/vite-plugin-stripper/src/lib/plugin.ts +++ b/packages/vite-plugin-stripper/src/lib/plugin.ts @@ -5,24 +5,59 @@ import { watchAndRun } from 'vite-plugin-watch-and-run' import { gray, green, Log, yellow } from '@kitql/helpers' import { getFilesUnder } from '@kitql/internals' +import { nullifyImports } from './nullifyImports.js' import { transformDecorator } from './transformDecorator.js' -import { removePackages } from './transformPackage.js' +import { transformStrip, type StripConfig } from './transformStrip.js' import { transformWarningThrow, type WarningThrow } from './transformWarningThrow.js' -export type ViteStriperOptions = { +export type ViteStripperOptions = { /** * for example: `['BackendMethod']` + * @deprecated, you should use `strip` instead */ decorators?: string[] /** * If true, will empty almost all the file if a decorator is found. (experimental!) + * @deprecated, you should use `strip` instead */ hard?: boolean + /** + * Wrap the code in an if(import.meta.env.SSR) condition if it's belongs a match of the config. + * + * @example Advanced format + * ```ts + * strip: [ + * { decorator: 'BackendMethod' }, + * { + * decorator: 'Entity', + * args_1: [ + * { fn: 'backendPrefilter' }, + * { fn: 'backendPreprocessFilter' }, + * { fn: 'sqlExpression' }, + * { fn: 'saved', excludeEntityKeys: ['users'] } + * ] + * } + * ] + * ``` + */ + strip?: StripConfig[] + /** * For example if you set `nullify: ['mongodb']` * + * @example 1 + * ```ts + * // This line + * import { AUTH_SECRET, AUTH_SECRET_NOT_USED } from '$env/static/private' + * + * // We become + * let AUTH_SECRET = null; + * let AUTH_SECRET_NOT_USED = null; + * ``` + * + * @example 2 * ```ts * // This line * import { ObjectId } from 'mongodb' @@ -51,20 +86,20 @@ export type ViteStriperOptions = { * * It should look like this: * ```ts - import { sveltekit } from "@sveltejs/kit/vite"; - import { defineConfig } from "vite"; - import { stripper } from "vite-plugin-stripper"; // 👈 + import { sveltekit } from "@sveltejs/kit/vite"; + import { defineConfig } from "vite"; + import { stripper } from "vite-plugin-stripper"; // 👈 - export default defineConfig({ - plugins: [ - stripper({ decorators: ['BackendMethod'] }), // 👈 - sveltekit() - ], - }); + export default defineConfig({ + plugins: [ + stripper({ decorators: ['BackendMethod'] }), // 👈 + sveltekit() + ], + }); * ``` * */ -export function stripper(options?: ViteStriperOptions): PluginOption { +export function stripper(options?: ViteStripperOptions): PluginOption { const log = new Log('stripper') let listOrThrow: WarningThrow[] = [] @@ -115,7 +150,7 @@ export function stripper(options?: ViteStriperOptions): PluginOption { return } - let infosNumber = 0 + const allInfos: string[] = [] if (options && options?.decorators && options.decorators.length > 0) { const { info, ...rest } = await transformDecorator( @@ -126,46 +161,39 @@ export function stripper(options?: ViteStriperOptions): PluginOption { // Update the code for later transforms & return it code = rest.code - - infosNumber += info.length - - if (options?.debug && info.length > 0) { - log.info( - `` + - `${gray('File:')} ${yellow(filepath)}\n` + - `${green('-----')}\n` + - `${rest.code}` + - `\n${green(':::::')}\n` + - `${info.join('\n')}` + - `\n${green('-----')}` + - ``, - ) - } + allInfos.push(...info) } if (options && options?.nullify && options.nullify.length > 0) { - const { info, ...rest } = await removePackages(code, options.nullify) + const { info, ...rest } = await nullifyImports(code, options.nullify) // Update the code for later transforms & return it code = rest.code + allInfos.push(...info) + } - infosNumber += info.length - - if (options?.debug && info.length > 0) { - log.info( - `` + - `${gray('File:')} ${yellow(filepath)}\n` + - `${green('-----')}\n` + - `${rest.code}` + - `\n${green(':::::')}\n` + - `${info.join('\n')}` + - `\n${green('-----')}` + - ``, - ) - } + if (options && options?.strip && options.strip.length > 0) { + const { info, ...rest } = await transformStrip(code, options.strip) + + // Update the code for later transforms & return it + code = rest.code + allInfos.push(...info) + } + + if (options?.debug && allInfos.length > 0) { + log.info( + `` + + `${gray('File:')} ${yellow(filepath)}\n` + + `${green('-----')}\n` + + `${code}` + + `\n${green(':::::')}\n` + + `${allInfos.join('\n')}` + + `\n${green('-----')}` + + ``, + ) } - if (infosNumber > 0) { + if (allInfos.length > 0) { return { code, map: null } } diff --git a/packages/vite-plugin-stripper/src/lib/transformStrip.spec.ts b/packages/vite-plugin-stripper/src/lib/transformStrip.spec.ts new file mode 100644 index 00000000..84aa7a1e --- /dev/null +++ b/packages/vite-plugin-stripper/src/lib/transformStrip.spec.ts @@ -0,0 +1,728 @@ +import { describe, expect, it } from 'vitest' + +import { nullifyImports } from './nullifyImports.js' +import { transformStrip } from './transformStrip.js' + +describe('transformStrip (init)', () => { + it('should add import.meta.env.SSR and log the performance bad import', async () => { + const code = ` +import { BackendMethod, remult, type Allowed } from 'remult' +import { performance } from 'perf_hooks' + +const helpers = async () => { + const { AUTH_SECRET } = await import('$env/static/private') + return { AUTH_SECRET } +} + +export class ActionsController { + @BackendMethod({ allowed: () => remult.user === undefined }) + static async read(info: Allowed) { + // if (import.meta.env.SSR) { + const { plop } = await import('./toto.js') + const { AUTH_SECRET } = await import('$env/static/private') + helpers() + const start = performance.now() + console.info('AUTH_SECRET', AUTH_SECRET) + const end = performance.now() + console.info(end - start) + plop() + return AUTH_SECRET + ' ' + info + // } + } +} +` + + const data = await transformStrip(code, [{ decorator: 'BackendMethod' }]) + expect(data).toMatchInlineSnapshot(` + { + "code": "import { BackendMethod, remult, type Allowed } from "remult"; + import { performance } from "perf_hooks"; + + const helpers = async () => { + const { + AUTH_SECRET + } = await import("$env/static/private"); + + return { + AUTH_SECRET + }; + }; + + export class ActionsController { + @BackendMethod({ + allowed: () => remult.user === undefined + }) + static async read(info: Allowed) { + if (import.meta.env.SSR) { + const { + plop + } = await import("./toto.js"); + + const { + AUTH_SECRET + } = await import("$env/static/private"); + + helpers(); + const start = performance.now(); + console.info("AUTH_SECRET", AUTH_SECRET); + const end = performance.now(); + console.info(end - start); + plop(); + return AUTH_SECRET + " " + info; + } + } + }", + "info": [ + "Wrapped with if(import.meta.env.SSR): ["ActionsController","BackendMethod","read"]", + ], + } + `) + }) +}) + +describe('transformStrip', () => { + it('should empty @BackendMethod and clean imports', async () => { + const code = `import { Allow, BackendMethod, remult } from "remult"; +import { Task } from "./task"; +import { AUTH_SECRET } from "$env/static/private"; + +export class TasksController { + static async yop1(completed: boolean) { + const taskRepo = remult.repo(Task); + } + + @BackendMethod({ allowed: Allow.authenticated }) + static async setAllCompleted(completed: boolean) { + console.log("AUTH_SECRET", AUTH_SECRET); + + const taskRepo = remult.repo(Task); + for (const task of await taskRepo.find()) { + await taskRepo.save({ ...task, completed }); + } + } + + @BackendMethod({ allowed: Allow.authenticated }) + static async Yop(completed: boolean) { + // console.log("AUTH_SECRET", AUTH_SECRET); + + const taskRepo = remult.repo(Task); + for (const task of await taskRepo.find()) { + await taskRepo.save({ ...task, completed }); + } + } +} + ` + + const transformed = await transformStrip(code, [{ decorator: 'BackendMethod' }]) + + expect(transformed).toMatchInlineSnapshot(` + { + "code": "import { Allow, BackendMethod, remult } from "remult"; + import { Task } from "./task"; + import { AUTH_SECRET } from "$env/static/private"; + + export class TasksController { + static async yop1(completed: boolean) { + const taskRepo = remult.repo(Task); + } + + @BackendMethod({ + allowed: Allow.authenticated + }) + static async setAllCompleted(completed: boolean) { + if (import.meta.env.SSR) { + console.log("AUTH_SECRET", AUTH_SECRET); + const taskRepo = remult.repo(Task); + + for (const task of await taskRepo.find()) { + await taskRepo.save({ + ...task, + completed + }); + } + } + } + + @BackendMethod({ + allowed: Allow.authenticated + }) + static async Yop(completed: boolean) { + if (import.meta.env.SSR) { + const taskRepo = remult.repo(Task); + + for (const task of await taskRepo.find()) { + await taskRepo.save({ + ...task, + completed + }); + } + } + } + }", + "info": [ + "Wrapped with if(import.meta.env.SSR): ["TasksController","BackendMethod","setAllCompleted"]", + "Wrapped with if(import.meta.env.SSR): ["TasksController","BackendMethod","Yop"]", + ], + } + `) + }) + + it('should not crash if there is an error in the original file', async () => { + const code = `import { Allow, BackendMethod, remult } from "remult"; +import { Task } from "./task"; +import { AUTH_SECRET } from "$env/static/private"; + +export class TasksController { + @BackendMethod({ allowed: Allow.authenticated }) + static async setAllCompleted(completed: boolean) { + console.log("AUTH_SECRET", AUTH_SECRET); + //} LEAVE THIS ERROR TO SIMULATE A WRONG PARSED FILE +} + ` + + const transformed = await transformStrip(code, [{ decorator: 'BackendMethod' }]) + + expect(transformed).toMatchInlineSnapshot(` + { + "code": "import { Allow, BackendMethod, remult } from "remult"; + import { Task } from "./task"; + import { AUTH_SECRET } from "$env/static/private"; + + export class TasksController { + @BackendMethod({ allowed: Allow.authenticated }) + static async setAllCompleted(completed: boolean) { + console.log("AUTH_SECRET", AUTH_SECRET); + //} LEAVE THIS ERROR TO SIMULATE A WRONG PARSED FILE + } + ", + "info": [], + } + `) + }) + + it('should not do anything as there is no @BackendMethod', async () => { + const code = `import { Allow, BackendMethod, remult } from "remult"; +import { Task } from "./task"; +import { AUTH_SECRET } from "$env/static/private"; + +export class TasksController { + static async yop1(completed: boolean) { + const taskRepo = remult.repo(Task); + } + + static async setAllCompleted(completed: boolean) { + console.log("AUTH_SECRET", AUTH_SECRET); + + const taskRepo = remult.repo(Task); + for (const task of await taskRepo.find()) { + await taskRepo.save({ ...task, completed }); + } + } +} + ` + + const transformed = await transformStrip(code, [{ decorator: 'BackendMethod' }]) + + expect(transformed).toMatchInlineSnapshot(` + { + "code": "import { Allow, BackendMethod, remult } from "remult"; + import { Task } from "./task"; + import { AUTH_SECRET } from "$env/static/private"; + + export class TasksController { + static async yop1(completed: boolean) { + const taskRepo = remult.repo(Task); + } + + static async setAllCompleted(completed: boolean) { + console.log("AUTH_SECRET", AUTH_SECRET); + const taskRepo = remult.repo(Task); + + for (const task of await taskRepo.find()) { + await taskRepo.save({ + ...task, + completed + }); + } + } + }", + "info": [], + } + `) + }) + + it('should strip also unused methods', async () => { + const code = `import { TOP_SECRET, TOP_SECRET_NOT_USED } from '$env/static/private'; + import { stry0 } from '@kitql/helper'; + import { BackendMethod, Entity, Fields, remult } from 'remult'; + + @Entity() + export class Ent { + @Fields.uuid() + id!: string; + } + + const getInfo = () => { + const client = {} + + console.log(TOP_SECRET) + + return client; + }; + + export class EntController { + @BackendMethod({ allowed: false }) + static async init(hello: string) { + const client = getInfo(); + + // Do a lot of things here + } + } + ` + + const transformed = await transformStrip(code, [{ decorator: 'BackendMethod' }]) + + expect(transformed).toMatchInlineSnapshot(` + { + "code": "import { TOP_SECRET, TOP_SECRET_NOT_USED } from "$env/static/private"; + import { stry0 } from "@kitql/helper"; + import { BackendMethod, Entity, Fields, remult } from "remult"; + + @Entity() + export class Ent { + @Fields.uuid() + id!: string; + } + + const getInfo = () => { + const client = {}; + console.log(TOP_SECRET); + return client; + }; + + export class EntController { + @BackendMethod({ + allowed: false + }) + static async init(hello: string) { + if (import.meta.env.SSR) { + const client = getInfo(); + } + } + }", + "info": [ + "Wrapped with if(import.meta.env.SSR): ["EntController","BackendMethod","init"]", + ], + } + `) + }) + + it('should strip just the right things', async () => { + const code = `import { Allow, BackendMethod, Entity, Fields, Validators } from 'remult' + + @Entity('userstest', { + allowApiCrud: Allow.authenticated, + }) + export class User2 { + @Fields.uuid() + id = '' + + @Fields.string({ + validate: [Validators.required, Validators.uniqueOnBackend], + }) + email = '' + + @BackendMethod({ allowed: Allow.everyone }) + async testMethod() { + console.log('hello') + } + } + ` + + const transformed = await transformStrip(code, [{ decorator: 'BackendMethod' }]) + + expect(transformed).toMatchInlineSnapshot(` + { + "code": "import { Allow, BackendMethod, Entity, Fields, Validators } from "remult"; + + @Entity("userstest", { + allowApiCrud: Allow.authenticated + }) + export class User2 { + @Fields.uuid() + id = ""; + + @Fields.string({ + validate: [Validators.required, Validators.uniqueOnBackend] + }) + email = ""; + + @BackendMethod({ + allowed: Allow.everyone + }) + async testMethod() { + if (import.meta.env.SSR) { + console.log("hello"); + } + } + }", + "info": [ + "Wrapped with if(import.meta.env.SSR): ["User2","BackendMethod","testMethod"]", + ], + } + `) + }) + + it('should strip unused stuff when decorator', async () => { + const code = `import { Allow, BackendMethod, Entity, Fields, Validators } from 'remult' + + @Entity('userstest', { + allowApiCrud: Allow.authenticated, + }) + export class User2 { + @Fields.uuid() + id = '' + + @Fields.string({}) + email = '' + + @BackendMethod({ allowed: Allow.everyone }) + async testMethod() { + console.log('hello') + } + } + ` + + const transformed = await transformStrip(code, [{ decorator: 'BackendMethod' }]) + + expect(transformed).toMatchInlineSnapshot(` + { + "code": "import { Allow, BackendMethod, Entity, Fields, Validators } from "remult"; + + @Entity("userstest", { + allowApiCrud: Allow.authenticated + }) + export class User2 { + @Fields.uuid() + id = ""; + + @Fields.string({}) + email = ""; + + @BackendMethod({ + allowed: Allow.everyone + }) + async testMethod() { + if (import.meta.env.SSR) { + console.log("hello"); + } + } + }", + "info": [ + "Wrapped with if(import.meta.env.SSR): ["User2","BackendMethod","testMethod"]", + ], + } + `) + }) + + it('should strip imports that are in the BackendMethod', async () => { + const code = `import { Allow, BackendMethod, Entity, Fields, Validators } from 'remult' + + @Entity('userstest', { + allowApiCrud: Allow.authenticated, + }) + export class User2 { + @Fields.uuid() + id = '' + + @Fields.string({}) + email = '' + + @BackendMethod({ allowed: Allow.everyone }) + async testMethod() { + console.log('hello', Validators.required) + } + } + ` + + const transformed = await transformStrip(code, [{ decorator: 'BackendMethod' }]) + + expect(transformed).toMatchInlineSnapshot(` + { + "code": "import { Allow, BackendMethod, Entity, Fields, Validators } from "remult"; + + @Entity("userstest", { + allowApiCrud: Allow.authenticated + }) + export class User2 { + @Fields.uuid() + id = ""; + + @Fields.string({}) + email = ""; + + @BackendMethod({ + allowed: Allow.everyone + }) + async testMethod() { + if (import.meta.env.SSR) { + console.log("hello", Validators.required); + } + } + }", + "info": [ + "Wrapped with if(import.meta.env.SSR): ["User2","BackendMethod","testMethod"]", + ], + } + `) + }) + + it('should NOT strip imports that are in both in BackendMethod and not in', async () => { + const code = `import { Allow, BackendMethod, Entity, Fields, Validators } from 'remult' + + @Entity('userstest', { + allowApiCrud: Allow.authenticated, + }) + export class User2 { + @Fields.uuid() + id = '' + + @Fields.string({ + validate: [Validators.required, Validators.uniqueOnBackend], + }) + email = '' + + @BackendMethod({ allowed: Allow.everyone }) + async testMethod() { + console.log('hello', Validators.required) + } + } + ` + + const transformed = await transformStrip(code, [{ decorator: 'BackendMethod' }]) + + expect(transformed).toMatchInlineSnapshot(` + { + "code": "import { Allow, BackendMethod, Entity, Fields, Validators } from "remult"; + + @Entity("userstest", { + allowApiCrud: Allow.authenticated + }) + export class User2 { + @Fields.uuid() + id = ""; + + @Fields.string({ + validate: [Validators.required, Validators.uniqueOnBackend] + }) + email = ""; + + @BackendMethod({ + allowed: Allow.everyone + }) + async testMethod() { + if (import.meta.env.SSR) { + console.log("hello", Validators.required); + } + } + }", + "info": [ + "Wrapped with if(import.meta.env.SSR): ["User2","BackendMethod","testMethod"]", + ], + } + `) + }) + + it('should strip import types', async () => { + const code = `import { AUTH_SECRET } from '$env/static/private' + import { BackendMethod, type Allowed, remult } from 'remult' + + export class ActionsController { + @BackendMethod({ + // Only unauthenticated users can call this method + allowed: () => remult.user === undefined, + }) + static async read(info: Allowed) { + console.log('AUTH_SECRET', AUTH_SECRET) + return AUTH_SECRET + ' ' + info + } + } + + ` + + const transformed = await transformStrip(code, [{ decorator: 'BackendMethod' }]) + + expect(transformed).toMatchInlineSnapshot(` + { + "code": "import { AUTH_SECRET } from "$env/static/private"; + import { BackendMethod, type Allowed, remult } from "remult"; + + export class ActionsController { + @BackendMethod({ + allowed: () => remult.user === undefined + }) + static async read(info: Allowed) { + if (import.meta.env.SSR) { + console.log("AUTH_SECRET", AUTH_SECRET); + return AUTH_SECRET + " " + info; + } + } + }", + "info": [ + "Wrapped with if(import.meta.env.SSR): ["ActionsController","BackendMethod","read"]", + ], + } + `) + }) +}) + +describe('decoratorEntity', () => { + it('should strip @BackendMethod in @Entity', async () => { + const code = `import { AUTH_SECRET, AUTH_SECRET_NOT_USED } from "$env/static/private"; +import { BackendMethod, Entity, Fields, remult, type Allowed } from "remult"; + +@Entity('users', { + backendPrefilter: async () => { + console.log('backendPrefilter') + return {} + } +}) +export class User { + @Fields.uuid() + id = '' + + @Fields.string() + name = '' + + @BackendMethod({ + // Only unauthenticated users can call this method + allowed: () => remult.user === undefined, + }) + static async hi(info: Allowed) { + console.info('AUTH_SECRET', AUTH_SECRET) + return AUTH_SECRET + ' ' + info + } +} +` + + const code1 = await nullifyImports(code, ['$env/static/private']) + const transformed = await transformStrip(code1.code, [ + { decorator: 'BackendMethod' }, + { decorator: 'Entity', args_1: [{ fn: 'backendPrefilter' }] }, + ]) + expect(transformed).toMatchInlineSnapshot(` + { + "code": "let AUTH_SECRET = null; + let AUTH_SECRET_NOT_USED = null; + import { BackendMethod, Entity, Fields, remult, type Allowed } from "remult"; + + @Entity("users", { + backendPrefilter: async () => { + if (import.meta.env.SSR) { + console.log("backendPrefilter"); + return {}; + } + } + }) + export class User { + @Fields.uuid() + id = ""; + + @Fields.string() + name = ""; + + @BackendMethod({ + allowed: () => remult.user === undefined + }) + static async hi(info: Allowed) { + if (import.meta.env.SSR) { + console.info("AUTH_SECRET", AUTH_SECRET); + return AUTH_SECRET + " " + info; + } + } + }", + "info": [ + "Wrapped with if(import.meta.env.SSR): ["User","Entity","backendPrefilter"]", + "Wrapped with if(import.meta.env.SSR): ["User","BackendMethod","hi"]", + ], + } + `) + }) + + it('should strip @BackendMethod in @Entity with excludeEntityKeys', async () => { + const code = `import { AUTH_SECRET, AUTH_SECRET_NOT_USED } from "$env/static/private"; +import { BackendMethod, Entity, Fields, remult, type Allowed } from "remult"; + +@Entity('users', { + backendPrefilter: () => { + console.log('backendPrefilter') + return {} + } +}) +export class User { + @Fields.uuid() + id = '' + + @Fields.string() + name = '' + + @BackendMethod({ + // Only unauthenticated users can call this method + allowed: () => remult.user === undefined, + }) + static async hi(info: Allowed) { + console.info('AUTH_SECRET', AUTH_SECRET) + return AUTH_SECRET + ' ' + info + } +} +` + + const code1 = await nullifyImports(code, ['$env/static/private']) + const transformed = await transformStrip(code1.code, [ + { decorator: 'BackendMethod' }, + { + decorator: 'Entity', + args_1: [ + { + fn: 'backendPrefilter', + excludeEntityKeys: ['users'], + }, + ], + }, + ]) + expect(transformed).toMatchInlineSnapshot(` + { + "code": "let AUTH_SECRET = null; + let AUTH_SECRET_NOT_USED = null; + import { BackendMethod, Entity, Fields, remult, type Allowed } from "remult"; + + @Entity("users", { + backendPrefilter: () => { + console.log("backendPrefilter"); + return {}; + } + }) + export class User { + @Fields.uuid() + id = ""; + + @Fields.string() + name = ""; + + @BackendMethod({ + allowed: () => remult.user === undefined + }) + static async hi(info: Allowed) { + if (import.meta.env.SSR) { + console.info("AUTH_SECRET", AUTH_SECRET); + return AUTH_SECRET + " " + info; + } + } + }", + "info": [ + "Wrapped with if(import.meta.env.SSR): ["User","BackendMethod","hi"]", + ], + } + `) + }) +}) diff --git a/packages/vite-plugin-stripper/src/lib/transformStrip.ts b/packages/vite-plugin-stripper/src/lib/transformStrip.ts new file mode 100644 index 00000000..0965eb11 --- /dev/null +++ b/packages/vite-plugin-stripper/src/lib/transformStrip.ts @@ -0,0 +1,250 @@ +import { parseTs, prettyPrint, visit } from '@kitql/internals' + +// Define the type for the decorator config +export type StripConfig = { + decorator: string + args_1?: { fn: string; excludeEntityKeys?: string[] }[] // Array of objects with function name and optional entity keys to exclude +} + +export const transformStrip = async (code: string, decorators_config: StripConfig[]) => { + try { + const program = parseTs(code) + + let currentClassName = '' // Variable to hold the current class name + const decorators_wrapped: { decorator: string; functionName: string; className: string }[] = [] + const entityClassesWithSpecialFilters: string[] = [] // Track classes with Entity decorator and special filters + const entityNameMap = new Map() // Map class names to their entity names + + // First pass: identify classes with special decorators and filters + visit(program, { + visitClassDeclaration(path: any) { + // @ts-ignore + const className = path.node.id.name + // @ts-ignore + const decorators: any[] = path.node.decorators || [] + + decorators.forEach((decorator) => { + if (!decorator.expression.callee) return + + const decoratorName = decorator.expression.callee.name + // Find matching config for this decorator + const config = decorators_config.find((c) => c.decorator === decoratorName) + + // If this is an Entity-like decorator, store the entity name + if (config && decorator.expression.arguments && decorator.expression.arguments.length >= 1) { + const entityNameArg = decorator.expression.arguments[0] + if (entityNameArg && entityNameArg.value) { + entityNameMap.set(className, entityNameArg.value) + } + } + + if ( + config && + config.args_1 && + decorator.expression.arguments && + decorator.expression.arguments.length >= 2 + ) { + // Check if the second argument (options) has any of the specified function names + const options = decorator.expression.arguments[1] + if (options && options.properties) { + const hasSpecialFilter = options.properties.some((prop: any) => + config.args_1?.some((c) => c.fn === prop.key.name), + ) + + if (hasSpecialFilter) { + entityClassesWithSpecialFilters.push(className) + + // Wrap these special functions in if(import.meta.env.SSR) + options.properties.forEach((prop: any) => { + if (config.args_1?.some((c) => c.fn === prop.key.name)) { + if (prop.value && prop.value.body && prop.value.body.body) { + const originalBody = prop.value.body.body + + // Find the matching config entry + const matchingConfig = config.args_1?.find((c) => c.fn === prop.key.name) + + // Get the entity name for this class + const entityName = entityNameMap.get(className) + + // Check if we need to exclude this entity based on entity name + const shouldExclude = + (entityName && matchingConfig?.excludeEntityKeys?.includes(entityName)) || false + + // Only wrap if not excluded + if (!shouldExclude) { + // Create the if statement wrapping the original body + prop.value.body.body = [ + { + type: 'IfStatement', + test: { + type: 'MemberExpression', + object: { + type: 'MemberExpression', + object: { + type: 'MetaProperty', + meta: { type: 'Identifier', name: 'import' }, + property: { type: 'Identifier', name: 'meta' }, + }, + property: { type: 'Identifier', name: 'env' }, + }, + property: { type: 'Identifier', name: 'SSR' }, + }, + consequent: { + type: 'BlockStatement', + body: originalBody, + }, + alternate: null, + }, + ] + + // Record that we wrapped this function + decorators_wrapped.push({ + className, + decorator: decoratorName, + functionName: prop.key.name, + }) + } + } + } + }) + } + } + } + }) + + this.traverse(path) + }, + }) + + // Second pass: wrap functions with decorators in if(import.meta.env.SSR) condition + visit(program, { + visitClassDeclaration(path: any) { + // @ts-ignore + currentClassName = path.node.id.name + this.traverse(path) + }, + visitFunction(path: any) { + // @ts-ignore + const decorators: any[] = path.node.decorators || [] + let foundDecorator = false + let decoratorName = '' + + // Initialize functionName with a default value + let functionName = '???' + + // Check if the function is a standalone function or a method in a class + if (path.node.id && path.node.id.name) { + // Standalone function + functionName = typeof path.node.id.name === 'string' ? path.node.id.name : 'IdentifierKind' + // @ts-ignore + } else if (path.node.key && path.node.key.name) { + // @ts-ignore + functionName = path.node.key.name + } + + // Check if any of the decorators match our list + decorators.forEach((decorator) => { + if (decorator.expression.callee) { + const name = decorator.expression.callee.name + const matchingConfig = decorators_config.find((c) => c.decorator === name) + + if (matchingConfig) { + foundDecorator = true + decoratorName = name + + // Push both the decorator name and the associated function name + decorators_wrapped.push({ + className: currentClassName, + decorator: name, + functionName, + }) + } + } + }) + + // Check if this function is in a class with special filters and has a special name + const isInEntityClass = entityClassesWithSpecialFilters.includes(currentClassName) + const isSpecialFunction = + functionName && + decorators_config.some((config) => config.args_1?.some((c) => functionName.startsWith(c.fn))) + + // Get the entity name for this class + const entityName = entityNameMap.get(currentClassName) + + // Check if this entity should be excluded + const shouldExclude = + entityName && + decorators_config.some((config) => + config.args_1?.some( + (c) => functionName.startsWith(c.fn) && c.excludeEntityKeys?.includes(entityName), + ), + ) + + // If one of the decorators was found OR it's a special function in a tracked class, wrap the function body in if(import.meta.env.SSR) + if ( + (foundDecorator || (isInEntityClass && isSpecialFunction && !shouldExclude)) && + path.node.body && + path.node.body.body + ) { + const originalBody = path.node.body.body + + // Create the if statement wrapping the original body + path.node.body.body = [ + { + type: 'IfStatement', + test: { + type: 'MemberExpression', + object: { + type: 'MemberExpression', + object: { + type: 'MetaProperty', + meta: { type: 'Identifier', name: 'import' }, + property: { type: 'Identifier', name: 'meta' }, + }, + property: { type: 'Identifier', name: 'env' }, + }, + property: { type: 'Identifier', name: 'SSR' }, + }, + consequent: { + type: 'BlockStatement', + body: originalBody, + }, + alternate: null, + }, + ] + + // If it's a special function but not already recorded, add it to the list + if (isInEntityClass && isSpecialFunction && !foundDecorator) { + // Find the decorator config that has this special function + const matchingConfig = decorators_config.find((config) => + config.args_1?.some((c) => functionName.startsWith(c.fn)), + ) + + // Use the decorator name from the config instead of hardcoding "Entity" + const decoratorNameFromConfig = matchingConfig ? matchingConfig.decorator : 'Unknown' + + decorators_wrapped.push({ + className: currentClassName, + decorator: decoratorNameFromConfig, + functionName, + }) + } + } + + this.traverse(path) + }, + }) + + const res = prettyPrint(program, {}) + const info = decorators_wrapped.map( + (decorator) => + `Wrapped with if(import.meta.env.SSR): ${JSON.stringify(Object.values(decorator))}`, + ) + + return { ...res, info } + } catch (error) { + // if anything happens, just return the original code + console.error('Error in transformDecorator:', error) + return { code, info: [] } + } +} diff --git a/packages/vite-plugin-stripper/src/routes/+layout.svelte b/packages/vite-plugin-stripper/src/routes/+layout.svelte index b8d586a9..3f028712 100644 --- a/packages/vite-plugin-stripper/src/routes/+layout.svelte +++ b/packages/vite-plugin-stripper/src/routes/+layout.svelte @@ -7,4 +7,12 @@ +Home | +ThrowClass | +ThrowRandom | +decorators | +decoratorsEntity + +
+ {@render children?.()} diff --git a/packages/vite-plugin-stripper/src/routes/+page.svelte b/packages/vite-plugin-stripper/src/routes/+page.svelte index 1523cead..e965047a 100644 --- a/packages/vite-plugin-stripper/src/routes/+page.svelte +++ b/packages/vite-plugin-stripper/src/routes/+page.svelte @@ -1,5 +1 @@ Hello - -ThrowClass -ThrowRandom -decorators diff --git a/packages/vite-plugin-stripper/src/routes/api/[...remult]/+server.ts b/packages/vite-plugin-stripper/src/routes/api/[...remult]/+server.ts new file mode 100644 index 00000000..ac613ab1 --- /dev/null +++ b/packages/vite-plugin-stripper/src/routes/api/[...remult]/+server.ts @@ -0,0 +1,3 @@ +import { api } from '../../../server/api.js' + +export const { GET, POST, PUT, DELETE } = api diff --git a/packages/vite-plugin-stripper/src/routes/decoratorsEntity/+page.svelte b/packages/vite-plugin-stripper/src/routes/decoratorsEntity/+page.svelte new file mode 100644 index 00000000..4431e254 --- /dev/null +++ b/packages/vite-plugin-stripper/src/routes/decoratorsEntity/+page.svelte @@ -0,0 +1,44 @@ + + +

Bye bye decorators entity

+secretContent: {secretContent} + +
+ + +{#each users as user (user.id)} +
+ {user.name} + +
+{/each} diff --git a/packages/vite-plugin-stripper/src/hooks/handleRemult.ts b/packages/vite-plugin-stripper/src/server/api.ts similarity index 62% rename from packages/vite-plugin-stripper/src/hooks/handleRemult.ts rename to packages/vite-plugin-stripper/src/server/api.ts index 448fac41..dfdf19d0 100644 --- a/packages/vite-plugin-stripper/src/hooks/handleRemult.ts +++ b/packages/vite-plugin-stripper/src/server/api.ts @@ -1,7 +1,9 @@ import { remultSveltekit } from 'remult/remult-sveltekit' import { ActionsController } from '../shared/actionsController.js' +import { User } from '../shared/User.js' -export const handleRemult = remultSveltekit({ +export const api = remultSveltekit({ + entities: [User], controllers: [ActionsController], }) diff --git a/packages/vite-plugin-stripper/src/shared/User.ts b/packages/vite-plugin-stripper/src/shared/User.ts new file mode 100644 index 00000000..fc9c092f --- /dev/null +++ b/packages/vite-plugin-stripper/src/shared/User.ts @@ -0,0 +1,38 @@ +import { BackendMethod, Entity, Fields, remult, type Allowed } from 'remult' + +import { AUTH_SECRET } from '$env/static/private' + +@Entity('users', { + allowApiCrud: true, + backendPrefilter: () => { + console.info('AUTH_SECRET_backendPrefilter', AUTH_SECRET) + console.info('backendPrefilter_top_secret') + return {} + }, + backendPreprocessFilter: (f) => { + console.info('AUTH_SECRET_backendPreprocessFilter', AUTH_SECRET) + console.info('backendPreprocessFilter_top_secret') + return f + }, + sqlExpression: () => { + console.info('AUTH_SECRET_sqlExpression', AUTH_SECRET) + console.info('sqlExpression_top_secret') + return 'users' + }, +}) +export class User { + @Fields.uuid() + id = '' + + @Fields.string() + name = '' + + @BackendMethod({ + // Only unauthenticated users can call this method + allowed: () => remult.user === undefined, + }) + static async hi(info: Allowed) { + console.info('AUTH_SECRET', AUTH_SECRET) + return AUTH_SECRET + ' ' + info + } +} diff --git a/packages/vite-plugin-stripper/src/shared/actionsController.ts b/packages/vite-plugin-stripper/src/shared/actionsController.ts index 5c5b8515..c6d7640e 100644 --- a/packages/vite-plugin-stripper/src/shared/actionsController.ts +++ b/packages/vite-plugin-stripper/src/shared/actionsController.ts @@ -1,14 +1,26 @@ import { BackendMethod, remult, type Allowed } from 'remult' -import { AUTH_SECRET } from '$env/static/private' +// import { performance } from 'perf_hooks' +// import { AUTH_SECRET } from "$env/static/private" + +// const helpers = async () => { +// const { AUTH_SECRET } = await import('$env/static/private') +// return { AUTH_SECRET } +// } export class ActionsController { - @BackendMethod({ - // Only unauthenticated users can call this method - allowed: () => remult.user === undefined, - }) + @BackendMethod({ allowed: () => remult.user === undefined }) static async read(info: Allowed) { + // if (import.meta.env.SSR) { + const { plop } = await import('./toto.js') + const { AUTH_SECRET } = await import('$env/static/private') + const { performance: p } = await import('perf_hooks') + const start = p.now() console.info('AUTH_SECRET', AUTH_SECRET) + const end = p.now() + console.info(end - start) + plop() return AUTH_SECRET + ' ' + info + // } } } diff --git a/packages/vite-plugin-stripper/src/shared/toto.ts b/packages/vite-plugin-stripper/src/shared/toto.ts new file mode 100644 index 00000000..4a9743a9 --- /dev/null +++ b/packages/vite-plugin-stripper/src/shared/toto.ts @@ -0,0 +1,5 @@ +console.info('plop') +export const plop = () => { + console.info('plop fn') + return 'plop' +} diff --git a/packages/vite-plugin-stripper/vite.config.ts b/packages/vite-plugin-stripper/vite.config.ts index 446012a7..dd5e7406 100644 --- a/packages/vite-plugin-stripper/vite.config.ts +++ b/packages/vite-plugin-stripper/vite.config.ts @@ -20,15 +20,29 @@ export default defineConfig(() => ({ }, plugins: [ stripper({ - // decorators: ['BackendMethod'], debug: true, log_on_throw_is_not_a_new_class: true, - hard: true, + // decorators: ['BackendMethod'], + // hard: true, nullify: ['$env/static/private', 'oslo/password'], + strip: [ + { decorator: 'BackendMethod' }, + { + decorator: 'Entity', + args_1: [ + { fn: 'backendPrefilter' }, + { fn: 'backendPreprocessFilter' }, + { fn: 'sqlExpression' }, + ], + }, + ], }), sveltekit(), ], test: { include: ['src/**/*.{test,spec}.{js,ts}'], + coverage: { + reporter: ['html'], + }, }, })) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75ba7a65..a81f6aad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,10 @@ settings: excludeLinksFromLockfile: false catalogs: + default: + '@vitest/ui': + specifier: 3.0.4 + version: 3.0.4 docs: '@astrojs/starlight': specifier: 0.32.2 @@ -91,7 +95,7 @@ catalogs: specifier: 2.4.0 version: 2.4.0 prettier: - specifier: 3.5.3 + specifier: ^3.5.3 version: 3.5.3 prettier-plugin-svelte: specifier: 3.3.2 @@ -196,10 +200,13 @@ importers: version: 2.28.1 '@vitest/coverage-v8': specifier: catalog:testing - version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + version: 3.0.4(vitest@3.0.4) esbuild: specifier: catalog:tooling version: 0.25.0 + prettier: + specifier: catalog:linting + version: 3.5.3 rimraf: specifier: catalog:tooling version: 6.0.1 @@ -341,7 +348,7 @@ importers: version: 6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) vitest: specifier: catalog:testing - version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(@vitest/ui@3.0.4)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) publishDirectory: dist packages/helpers: @@ -388,7 +395,7 @@ importers: version: 6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) vitest: specifier: catalog:testing - version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(@vitest/ui@3.0.4)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) publishDirectory: dist packages/internals: @@ -441,7 +448,7 @@ importers: version: 6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) vitest: specifier: catalog:testing - version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(@vitest/ui@3.0.4)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) publishDirectory: dist packages/sveltekit: @@ -463,7 +470,7 @@ importers: version: 6.6.3 '@testing-library/svelte': specifier: catalog:testing - version: 5.2.4(svelte@5.23.0)(vite@6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + version: 5.2.4(svelte@5.23.0)(vite@6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))(vitest@3.0.4) jsdom: specifier: catalog:testing version: 26.0.0 @@ -484,7 +491,7 @@ importers: version: 6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) vitest: specifier: catalog:testing - version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(@vitest/ui@3.0.4)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) publishDirectory: dist packages/vite-plugin-kit-routes: @@ -534,7 +541,7 @@ importers: version: 6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) vitest: specifier: catalog:testing - version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(@vitest/ui@3.0.4)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) publishDirectory: dist packages/vite-plugin-stripper: @@ -564,6 +571,9 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: catalog:sveltekit version: 5.0.1(svelte@5.23.0)(vite@6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + '@vitest/ui': + specifier: 'catalog:' + version: 3.0.4(vitest@3.0.4) publint: specifier: catalog:lib-author-helper version: 0.3.1 @@ -587,7 +597,7 @@ importers: version: 6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) vitest: specifier: catalog:testing - version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(@vitest/ui@3.0.4)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) publishDirectory: dist packages/vite-plugin-watch-and-run: @@ -637,7 +647,7 @@ importers: version: 6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) vitest: specifier: catalog:testing - version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(@vitest/ui@3.0.4)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) publishDirectory: dist packages: @@ -1779,6 +1789,11 @@ packages: '@vitest/spy@3.0.4': resolution: {integrity: sha512-sXIMF0oauYyUy2hN49VFTYodzEAu744MmGcPR3ZBsPM20G+1/cSW/n1U+3Yu/zHxX2bIDe1oJASOkml+osTU6Q==} + '@vitest/ui@3.0.4': + resolution: {integrity: sha512-e+s2F9e9FUURkZ5aFIe1Fi3Y8M7UF6gEuShcaV/ur7y/Ldri+1tzWQ1TJq9Vas42NXnXvCAIrU39Z4U2RyET6g==} + peerDependencies: + vitest: 3.0.4 + '@vitest/utils@3.0.4': resolution: {integrity: sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==} @@ -2554,6 +2569,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2584,6 +2602,9 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + flattie@1.1.1: resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} engines: {node: '>=8'} @@ -5934,13 +5955,13 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/svelte@5.2.4(svelte@5.23.0)(vite@6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))': + '@testing-library/svelte@5.2.4(svelte@5.23.0)(vite@6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))(vitest@3.0.4)': dependencies: '@testing-library/dom': 10.4.0 svelte: 5.23.0 optionalDependencies: vite: 6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) - vitest: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + vitest: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(@vitest/ui@3.0.4)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) '@theguild/prettier-config@3.0.0(@vue/compiler-sfc@3.5.13)(prettier@3.5.3)': dependencies: @@ -6118,7 +6139,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))': + '@vitest/coverage-v8@3.0.4(vitest@3.0.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -6132,7 +6153,7 @@ snapshots: std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + vitest: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(@vitest/ui@3.0.4)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - supports-color @@ -6170,6 +6191,17 @@ snapshots: dependencies: tinyspy: 3.0.2 + '@vitest/ui@3.0.4(vitest@3.0.4)': + dependencies: + '@vitest/utils': 3.0.4 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.0 + tinyglobby: 0.2.12 + tinyrainbow: 2.0.0 + vitest: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(@vitest/ui@3.0.4)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + '@vitest/utils@3.0.4': dependencies: '@vitest/pretty-format': 3.0.4 @@ -7084,6 +7116,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -7116,6 +7150,8 @@ snapshots: flatted@3.3.1: {} + flatted@3.3.3: {} + flattie@1.1.1: {} follow-redirects@1.15.9: {} @@ -9609,7 +9645,7 @@ snapshots: optionalDependencies: vite: 6.2.0(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) - vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0): + vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.13.9)(@vitest/ui@3.0.4)(jiti@1.21.7)(jsdom@26.0.0)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0): dependencies: '@vitest/expect': 3.0.4 '@vitest/mocker': 3.0.4(vite@6.2.1(@types/node@22.13.9)(jiti@1.21.7)(lightningcss@1.28.2)(sass@1.85.1)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) @@ -9634,6 +9670,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 22.13.9 + '@vitest/ui': 3.0.4(vitest@3.0.4) jsdom: 26.0.0 transitivePeerDependencies: - jiti diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 49ee19df..82614b57 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -37,7 +37,7 @@ catalogs: 'eslint-plugin-svelte': '3.1.0' 'eslint-plugin-unused-imports': '4.1.4' 'globals': '16.0.0' - 'prettier': '3.5.3' + 'prettier': '^3.5.3' 'prettier-plugin-svelte': '3.3.2' 'prettier-plugin-tailwindcss': '0.6.6' 'typescript-eslint': '8.26.0' @@ -83,3 +83,5 @@ catalogs: astro-icon: 1.1.5 sharp: 0.33.5 prettier-plugin-astro: 0.14.1 +catalog: + '@vitest/ui': 3.0.4