From 826e6be0ed18d0ad61a6d98dbf75024a6fee7ab4 Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Mon, 2 Sep 2024 17:10:12 +1000 Subject: [PATCH 01/26] add baas category --- packages/config/adders/official.ts | 1 + packages/config/categories.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/config/adders/official.ts b/packages/config/adders/official.ts index 740c83e2..fddc0d94 100644 --- a/packages/config/adders/official.ts +++ b/packages/config/adders/official.ts @@ -6,6 +6,7 @@ export const adderCategories: AdderCategories = { css: ['tailwindcss'], db: ['drizzle'], additional: ['storybook', 'mdsvex', 'routify'], + baas: ['supabase'], }; export const adderIds = Object.values(adderCategories).flatMap((x) => x); diff --git a/packages/config/categories.ts b/packages/config/categories.ts index c704ab16..d4ae2d4f 100644 --- a/packages/config/categories.ts +++ b/packages/config/categories.ts @@ -4,7 +4,7 @@ export type CategoryInfo = { description: string; }; -export type CategoryKeys = 'codeQuality' | 'css' | 'db' | 'testing' | 'additional'; +export type CategoryKeys = 'codeQuality' | 'css' | 'db' | 'testing' | 'additional' | 'baas'; export type CategoryDetails = Record; export type AdderCategories = Record; @@ -35,4 +35,9 @@ export const categories: CategoryDetails = { name: 'Database', description: '', }, + baas: { + id: 'baas', + name: 'Backend as a Service', + description: 'Services that provide database, Auth, storage, etc.', + }, }; From fffe6a6425a3b7a0c49e51b083d1b2b94851fb7e Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Mon, 2 Sep 2024 17:10:32 +1000 Subject: [PATCH 02/26] init attempt --- adders/supabase/config/adder.ts | 362 ++++++++++++++++++++++++++++++ adders/supabase/config/checks.ts | 6 + adders/supabase/config/options.ts | 9 + adders/supabase/config/tests.ts | 9 + adders/supabase/index.ts | 8 + adders/supabase/supabase.svg | 15 ++ 6 files changed, 409 insertions(+) create mode 100644 adders/supabase/config/adder.ts create mode 100644 adders/supabase/config/checks.ts create mode 100644 adders/supabase/config/options.ts create mode 100644 adders/supabase/config/tests.ts create mode 100644 adders/supabase/index.ts create mode 100644 adders/supabase/supabase.svg diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts new file mode 100644 index 00000000..c23cdfa8 --- /dev/null +++ b/adders/supabase/config/adder.ts @@ -0,0 +1,362 @@ +import { defineAdderConfig, dedent, type TextFileEditorArgs } from '@svelte-add/core'; +import { options as availableOptions } from './options'; + +export const adder = defineAdderConfig({ + metadata: { + id: 'supabase', + name: 'Supabase', + description: `Supabase is an open source Firebase alternative. +Start your project with a Postgres database, Authentication, instant APIs, Edge Functions, Realtime subscriptions, Storage, and Vector embeddings.`, + environments: { svelte: false, kit: true }, + website: { + logo: './supabase.svg', + keywords: ['supabase', 'database', 'postgres', 'auth'], + documentation: 'https://supabase.com/docs', + }, + }, + options: availableOptions, + integrationType: 'inline', + packages: [ + { name: '@supabase/supabase-js', version: '^2.45.3', dev: false }, + { name: '@supabase/ssr', version: '^0.5.1', dev: false }, + // Local development CLI + { + name: 'supabase', + version: '^1.191.3', + dev: true, + condition: ({ options }) => options.cli, + }, + ], + files: [ + { + name: () => `.env`, + contentType: 'text', + content: generateEnvFileContent, + }, + { + name: () => `.env.example`, + contentType: 'text', + content: generateEnvFileContent, + }, + { + name: ({ typescript }) => `./src/hooks.server.${typescript.installed ? 'ts' : 'js'}`, + contentType: 'text', + content: ({ typescript }) => { + return dedent` + import { createServerClient } from '@supabase/ssr' + import {${typescript.installed ? ' type Handle,' : ''} redirect } from '@sveltejs/kit' + import { sequence } from '@sveltejs/kit/hooks' + + import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' + + const supabase${typescript.installed ? ': Handle' : ''} = async ({ event, resolve }) => { + event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { + cookies: { + getAll: () => event.cookies.getAll(), + setAll: (cookiesToSet) => { + cookiesToSet.forEach(({ name, value, options }) => { + event.cookies.set(name, value, { ...options, path: '/' }) + }) + }, + }, + }) + + event.locals.safeGetSession = async () => { + const { + data: { session }, + } = await event.locals.supabase.auth.getSession() + if (!session) { + return { session: null, user: null } + } + + const { + data: { user }, + error, + } = await event.locals.supabase.auth.getUser() + if (error) { + return { session: null, user: null } + } + + return { session, user } + } + + return resolve(event, { + filterSerializedResponseHeaders(name) { + return name === 'content-range' || name === 'x-supabase-api-version' + }, + }) + } + + const authGuard${typescript.installed ? ': Handle' : ''} = async ({ event, resolve }) => { + const { session, user } = await event.locals.safeGetSession() + event.locals.session = session + event.locals.user = user + + if (!event.locals.session && event.url.pathname.startsWith('/private')) { + redirect(303, '/auth') + } + + if (event.locals.session && event.url.pathname === '/auth') { + redirect(303, '/private') + } + + return resolve(event) + } + + export const handle${typescript.installed ? ': Handle' : ''} = sequence(supabase, authGuard) + `; + }, + }, + { + name: () => './src/app.d.ts', + contentType: 'text', + condition: ({ typescript }) => typescript.installed, + content: () => { + return dedent` + import type { Session, SupabaseClient, User } from '@supabase/supabase-js' + + declare global { + namespace App { + // interface Error {} + interface Locals { + supabase: SupabaseClient + safeGetSession: () => Promise<{ session: Session | null; user: User | null }> + session: Session | null + user: User | null + } + interface PageData { + session: Session | null + } + // interface PageState {} + // interface Platform {} + } + } + + export {} + `; + }, + }, + { + name: ({ kit, typescript }) => + `${kit.routesDirectory}/+layout.${typescript.installed ? 'ts' : 'js'}`, + contentType: 'text', + content: ({ typescript }) => { + return dedent` + import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr' + import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' + ${typescript.installed ? `import type { LayoutLoad } from './$types'\n` : ''} + export const load${typescript.installed ? ': LayoutLoad' : ''} = async ({ data, depends, fetch }) => { + depends('supabase:auth') + + const supabase = isBrowser() + ? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { + global: { fetch }, + }) + : createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { + global: { fetch }, + cookies: { + getAll() { + return data.cookies + }, + }, + }) + + const { + data: { session }, + } = await supabase.auth.getSession() + + const { + data: { user }, + } = await supabase.auth.getUser() + + return { session, supabase, user } + } + `; + }, + }, + { + name: ({ kit, typescript }) => + `${kit.routesDirectory}/+layout.server.${typescript.installed ? 'ts' : 'js'}`, + contentType: 'text', + content: ({ typescript }) => { + return dedent` + ${typescript.installed ? `import type { LayoutServerLoad } from './$types'\n` : ''} + export const load${typescript.installed ? ': LayoutServerLoad' : ''} = async ({ locals: { session }, cookies }) => { + return { + session, + cookies: cookies.getAll(), + } + } + `; + }, + }, + { + name: ({ kit }) => `${kit.routesDirectory}/+layout.svelte`, + contentType: 'text', + content: ({ typescript }) => { + return dedent` + + import { invalidate } from '$app/navigation'; + import { onMount } from 'svelte'; + + export let data; + $: ({ session, supabase } = data); + + onMount(() => { + const { data } = supabase.auth.onAuthStateChange((_, newSession) => { + if (newSession?.expires_at !== session?.expires_at) { + invalidate('supabase:auth'); + } + }); + + return () => data.subscription.unsubscribe(); + }); + + + + `; + }, + }, + { + name: ({ kit, typescript }) => + `${kit.routesDirectory}/auth/+page.server.${typescript.installed ? 'ts' : 'js'}`, + contentType: 'text', + content: ({ typescript }) => { + return dedent` + import { redirect } from '@sveltejs/kit' + ${typescript.installed ? `import type { Actions } from './$types'` : ''} + export const actions${typescript.installed ? `: Actions` : ''} = { + signup: async ({ request, locals: { supabase } }) => { + const formData = await request.formData() + const email = formData.get('email')${typescript.installed ? ' as string' : ''} + const password = formData.get('password')${typescript.installed ? ' as string' : ''} + + const { error } = await supabase.auth.signUp({ email, password }) + if (error) { + console.error(error) + redirect(303, '/auth/error') + } else { + redirect(303, '/') + } + }, + login: async ({ request, locals: { supabase } }) => { + const formData = await request.formData() + const email = formData.get('email')${typescript.installed ? ' as string' : ''} + const password = formData.get('password')${typescript.installed ? ' as string' : ''} + + const { error } = await supabase.auth.signInWithPassword({ email, password }) + if (error) { + console.error(error) + redirect(303, '/auth/error') + } else { + redirect(303, '/private') + } + }, + } + `; + }, + }, + { + name: ({ kit }) => `${kit.routesDirectory}/auth/+page.svelte`, + contentType: 'text', + content: () => { + return dedent` +
+ + + + +
+ `; + }, + }, + { + name: ({ kit }) => `${kit.routesDirectory}/auth/error/+page.svelte`, + contentType: 'text', + content: () => { + return '

Login error

'; + }, + }, + { + name: ({ kit, typescript }) => + `${kit.routesDirectory}/auth/confirm/+server.${typescript.installed ? 'ts' : 'js'}`, + contentType: 'text', + content: ({ typescript }) => { + return dedent` + import { redirect } from '@sveltejs/kit' + ${ + typescript.installed + ? dedent`import type { EmailOtpType } from '@supabase/supabase-js' + import type { RequestHandler } from './$types' + ` + : '' + } + export const GET${typescript.installed ? ': RequestHandler' : ''} = async ({ url, locals: { supabase } }) => { + const token_hash = url.searchParams.get('token_hash') + const type = url.searchParams.get('type')${typescript.installed ? ' as EmailOtpType | null' : ''} + const next = url.searchParams.get('next') ?? '/' + + const redirectTo = new URL(url) + redirectTo.pathname = next + redirectTo.searchParams.delete('token_hash') + redirectTo.searchParams.delete('type') + + if (token_hash && type) { + const { error } = await supabase.auth.verifyOtp({ type, token_hash }) + if (!error) { + redirectTo.searchParams.delete('next') + redirect(303, redirectTo) + } + } + + redirectTo.pathname = '/auth/error' + redirect(303, redirectTo) + } + `; + }, + }, + ], + nextSteps: ({ options }) => { + const steps = ['Visit the Supabase docs: https://supabase.com/docs']; + + if (options.cli) { + steps.push('Read the Supabase CLI Docs: https://supabase.com/docs/reference/cli'); + } + + return steps; + }, +}); + +function generateEnvFileContent({ content, options }: TextFileEditorArgs) { + if (options.cli) { + // Local development CLI always has the same credentials + content = addEnvVar(content, 'PUBLIC_SUPABASE_URL', '"http://127.0.0.1:54321"'); + content = addEnvVar( + content, + 'PUBLIC_SUPABASE_ANON_KEY', + '"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"', + ); + } else { + content = addEnvVar(content, 'PUBLIC_SUPABASE_URL', '""'); + content = addEnvVar(content, 'PUBLIC_SUPABASE_ANON_KEY', '""'); + } + return content; +} + +function addEnvVar(content: string, key: string, value: string) { + if (!content.includes(key + '=')) { + content = appendEnvContent(content, `${key}=${value}`); + } + return content; +} + +function appendEnvContent(existing: string, content: string) { + const withNewLine = !existing.length || existing.endsWith('\n') ? existing : existing + '\n'; + return withNewLine + content + '\n'; +} diff --git a/adders/supabase/config/checks.ts b/adders/supabase/config/checks.ts new file mode 100644 index 00000000..fc08d6f3 --- /dev/null +++ b/adders/supabase/config/checks.ts @@ -0,0 +1,6 @@ +import { defineAdderChecks } from '@svelte-add/core'; +import { options } from './options'; + +export const checks = defineAdderChecks({ + options, +}); diff --git a/adders/supabase/config/options.ts b/adders/supabase/config/options.ts new file mode 100644 index 00000000..ac86ef14 --- /dev/null +++ b/adders/supabase/config/options.ts @@ -0,0 +1,9 @@ +import { defineAdderOptions } from '@svelte-add/core'; + +export const options = defineAdderOptions({ + cli: { + question: 'Do you want to install the Supabase CLI for local development?', + type: 'boolean', + default: true, + }, +}); diff --git a/adders/supabase/config/tests.ts b/adders/supabase/config/tests.ts new file mode 100644 index 00000000..524e74cc --- /dev/null +++ b/adders/supabase/config/tests.ts @@ -0,0 +1,9 @@ +import { defineAdderTests } from '@svelte-add/core'; +import { options } from './options.js'; + +export const tests = defineAdderTests({ + files: [], + options, + optionValues: [], + tests: [], +}); diff --git a/adders/supabase/index.ts b/adders/supabase/index.ts new file mode 100644 index 00000000..08f7ba75 --- /dev/null +++ b/adders/supabase/index.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +import { defineAdder } from '@svelte-add/core'; +import { adder } from './config/adder.js'; +import { tests } from './config/tests.js'; +import { checks } from './config/checks.js'; + +export default defineAdder(adder, checks, tests); diff --git a/adders/supabase/supabase.svg b/adders/supabase/supabase.svg new file mode 100644 index 00000000..ad802ac1 --- /dev/null +++ b/adders/supabase/supabase.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + From 23f2aa856b40172433c4ae410a083422781b00b1 Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Mon, 2 Sep 2024 17:31:33 +1000 Subject: [PATCH 03/26] helper script option --- adders/supabase/config/adder.ts | 22 +++++++++++++++++++--- adders/supabase/config/options.ts | 8 +++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index c23cdfa8..672a192e 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -111,15 +111,15 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge name: () => './src/app.d.ts', contentType: 'text', condition: ({ typescript }) => typescript.installed, - content: () => { + content: ({ options }) => { return dedent` import type { Session, SupabaseClient, User } from '@supabase/supabase-js' - + ${options.cli && options.helpers ? `import type { Database } from '$lib/supabase-types'\n` : ''} declare global { namespace App { // interface Error {} interface Locals { - supabase: SupabaseClient + supabase: SupabaseClient${options.cli && options.helpers ? `` : ''} safeGetSession: () => Promise<{ session: Session | null; user: User | null }> session: Session | null user: User | null @@ -321,6 +321,22 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge `; }, }, + { + name: () => `package.json`, + contentType: 'json', + content: ({ data, typescript }) => { + data.scripts ??= {}; + const scripts: Record = data.scripts; + scripts['db:migration'] ??= 'supabase migration new'; + scripts['db:migration:up'] ??= 'supabase migration up --local'; + scripts['db:reset'] ??= 'supabase db reset'; + if (typescript.installed) { + scripts['db:types'] ??= + 'supabase gen types typescript --local > src/lib/supabase-types.ts'; + } + }, + condition: ({ options }) => options.helpers, + }, ], nextSteps: ({ options }) => { const steps = ['Visit the Supabase docs: https://supabase.com/docs']; diff --git a/adders/supabase/config/options.ts b/adders/supabase/config/options.ts index ac86ef14..7c499b11 100644 --- a/adders/supabase/config/options.ts +++ b/adders/supabase/config/options.ts @@ -1,4 +1,4 @@ -import { defineAdderOptions } from '@svelte-add/core'; +import { colors, defineAdderOptions } from '@svelte-add/core'; export const options = defineAdderOptions({ cli: { @@ -6,4 +6,10 @@ export const options = defineAdderOptions({ type: 'boolean', default: true, }, + helpers: { + question: `Do you want to add Supabase helper scripts to your package.json? E.g., ${colors.yellow('db:reset')} and ${colors.yellow('db:migration "description"')}`, + type: 'boolean', + default: false, + condition: ({ cli }) => cli === true, + }, }); From 9abcf13275ef45bb68db4d6bd0213a6cd5114e10 Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Mon, 2 Sep 2024 18:11:48 +1000 Subject: [PATCH 04/26] admin option. refactor setting env vars --- adders/supabase/config/adder.ts | 63 +++++++++++++++++++++++++------ adders/supabase/config/options.ts | 5 +++ 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index 672a192e..460b035a 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -321,6 +321,31 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge `; }, }, + { + name: ({ kit, typescript }) => + `${kit.libDirectory}/server/supabase-admin.${typescript.installed ? 'ts' : 'js'}`, + contentType: 'text', + content: ({ options, typescript }) => { + return dedent` + import { PUBLIC_SUPABASE_URL } from '$env/static/public' + import { SUPABASE_SERVICE_ROLE_KEY } from '$env/static/private' + ${typescript && options.cli && options.helpers ? `import type { Database } from '$lib/supabase-types'\n` : ''} + import { createClient } from '@supabase/supabase-js' + + export const supabaseAdmin = createClient${typescript && options.cli && options.helpers ? '' : ''}( + PUBLIC_SUPABASE_URL, + SUPABASE_SERVICE_ROLE_KEY, + { + auth: { + autoRefreshToken: false, + persistSession: false, + }, + }, + ); + `; + }, + condition: ({ options }) => options.admin, + }, { name: () => `package.json`, contentType: 'json', @@ -350,18 +375,32 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge }); function generateEnvFileContent({ content, options }: TextFileEditorArgs) { - if (options.cli) { - // Local development CLI always has the same credentials - content = addEnvVar(content, 'PUBLIC_SUPABASE_URL', '"http://127.0.0.1:54321"'); - content = addEnvVar( - content, - 'PUBLIC_SUPABASE_ANON_KEY', - '"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"', - ); - } else { - content = addEnvVar(content, 'PUBLIC_SUPABASE_URL', '""'); - content = addEnvVar(content, 'PUBLIC_SUPABASE_ANON_KEY', '""'); - } + content = addEnvVar( + content, + 'PUBLIC_SUPABASE_URL', + // Local development env always has the same credentials, prepopulate the local dev env file + options.cli ? '"http://127.0.0.1:54321"' : '""', + ); + content = addEnvVar( + content, + 'PUBLIC_SUPABASE_ANON_KEY', + // Local development env always has the same credentials, prepopulate the local dev env file + options.cli + ? '"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"' + : '""', + ); + + content = options.admin + ? addEnvVar( + content, + 'SUPABASE_SERVICE_ROLE_KEY', + // Local development env always has the same credentials, prepopulate the local dev env file + options.cli + ? '"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU"' + : '""', + ) + : content; + return content; } diff --git a/adders/supabase/config/options.ts b/adders/supabase/config/options.ts index 7c499b11..03921429 100644 --- a/adders/supabase/config/options.ts +++ b/adders/supabase/config/options.ts @@ -1,6 +1,11 @@ import { colors, defineAdderOptions } from '@svelte-add/core'; export const options = defineAdderOptions({ + admin: { + question: `Do you want to add an Supabase admin client? ${colors.red('Warning: This client bypasses row level security, only use server side.')}`, + type: 'boolean', + default: false, + }, cli: { question: 'Do you want to install the Supabase CLI for local development?', type: 'boolean', From 6f8b748bd75b640c94f6ae79c642e94731bb9027 Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Mon, 2 Sep 2024 21:10:50 +1000 Subject: [PATCH 05/26] update next steps --- adders/supabase/config/adder.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index 460b035a..a74ccf21 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -1,4 +1,4 @@ -import { defineAdderConfig, dedent, type TextFileEditorArgs } from '@svelte-add/core'; +import { defineAdderConfig, dedent, type TextFileEditorArgs, colors } from '@svelte-add/core'; import { options as availableOptions } from './options'; export const adder = defineAdderConfig({ @@ -364,10 +364,20 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge }, ], nextSteps: ({ options }) => { - const steps = ['Visit the Supabase docs: https://supabase.com/docs']; + const steps = [ + 'Visit the Supabase docs: https://supabase.com/docs', + 'Update the authGuard server hook function with your protected routes', + ]; if (options.cli) { - steps.push('Read the Supabase CLI Docs: https://supabase.com/docs/reference/cli'); + steps.push( + dedent`Local development environment: + + 1. Initialize the local development environment: ${colors.yellow('pnpm supabase init')} + 2. Start the local development services: ${colors.yellow('pnpm supabase start')}. This may take a while the first time you run it + 3. Depending on your Auth selections, you may need to configure local email templates and modify ${colors.green('./supabase/config.toml')} + `, + ); } return steps; From 7c2fcf2c52426ad2ef08137ad69b4b520fddb7df Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Mon, 2 Sep 2024 22:42:39 +1000 Subject: [PATCH 06/26] demo option --- adders/supabase/config/adder.ts | 166 +++++++++++++++++++++++++++++- adders/supabase/config/options.ts | 5 + 2 files changed, 167 insertions(+), 4 deletions(-) diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index a74ccf21..0358df59 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -41,7 +41,7 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge { name: ({ typescript }) => `./src/hooks.server.${typescript.installed ? 'ts' : 'js'}`, contentType: 'text', - content: ({ typescript }) => { + content: ({ options, typescript }) => { return dedent` import { createServerClient } from '@supabase/ssr' import {${typescript.installed ? ' type Handle,' : ''} redirect } from '@sveltejs/kit' @@ -91,7 +91,9 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge const { session, user } = await event.locals.safeGetSession() event.locals.session = session event.locals.user = user - + ${ + options.demo + ? ` if (!event.locals.session && event.url.pathname.startsWith('/private')) { redirect(303, '/auth') } @@ -99,7 +101,11 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge if (event.locals.session && event.url.pathname === '/auth') { redirect(303, '/private') } - + ` + : ` + // Add authentication guards here + ` + } return resolve(event) } @@ -362,6 +368,157 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge }, condition: ({ options }) => options.helpers, }, + // Demo routes + { + name: ({ kit, typescript }) => + `${kit.routesDirectory}/private/+layout.server.${typescript.installed ? 'ts' : 'js'}`, + contentType: 'text', + content: () => { + return dedent` + /** + * This file is necessary to ensure protection of all routes in the \`private\` + * directory. It makes the routes in this directory _dynamic_ routes, which + * send a server request, and thus trigger \`hooks.server.ts\`. + **/ + `; + }, + condition: ({ options }) => options.demo, + }, + { + name: ({ kit }) => `${kit.routesDirectory}/private/+layout.svelte`, + contentType: 'text', + content: () => { + return dedent` + + +
+ + +
+
+ +
+ `; + }, + condition: ({ options }) => options.demo, + }, + { + name: ({ kit }) => `${kit.routesDirectory}/private/+page.svelte`, + contentType: 'text', + content: ({ options, typescript }) => { + return dedent` + + import { invalidate } from '$app/navigation' + ${typescript.installed ? `import type { EventHandler } from 'svelte/elements'\n` : ''} + ${typescript.installed ? `import type { PageData } from './$types'\n` : ''} + export let data${typescript.installed ? ': PageData' : ''} + + $: ({ ${options.cli ? 'notes, supabase, user' : 'user'} } = data) + ${ + options.cli + ? ` + let handleSubmit${typescript.installed ? ': EventHandler' : ''} + $: handleSubmit = async (evt) => { + evt.preventDefault(); + if (!evt.target) return; + + const form = evt.target${typescript.installed ? ' as HTMLFormElement' : ''} + + const note = (new FormData(form).get('note') ?? '')${typescript.installed ? ' as string' : ''} + if (!note) return; + + const { error } = await supabase.from('notes').insert({ note }); + if (error) console.error(error); + + invalidate('supabase:db:notes'); + form.reset(); + } + ` + : '' + } + + +

Private page for user: {user?.email}

+ ${ + options.cli + ? ` +

Notes

+
    + {#each notes as note} +
  • {note.note}
  • + {/each} +
+
+ +
+ ` + : '' + } + `; + }, + condition: ({ options }) => options.demo, + }, + { + name: ({ kit, typescript }) => + `${kit.routesDirectory}/private/+page.server.${typescript.installed ? 'ts' : 'js'}`, + contentType: 'text', + content: ({ typescript }) => { + return dedent` + ${typescript.installed ? `import type { PageServerLoad } from './$types'\n` : ''} + export const load${typescript.installed ? ': PageServerLoad' : ''} = async ({ depends, locals: { supabase } }) => { + depends('supabase:db:notes') + const { data: notes } = await supabase.from('notes').select('id,note').order('id') + return { notes: notes ?? [] } + } + `; + }, + condition: ({ options }) => options.demo && options.cli, + }, + { + name: () => './supabase/migrations/00000000000000_demo.sql', + contentType: 'text', + content: () => { + return dedent` + create table notes ( + id bigint primary key generated always as identity, + created_at timestamp with time zone not null default now(), + user_id uuid references auth.users on delete cascade not null default auth.uid(), + note text not null + ); + + alter table notes enable row level security; + + revoke all on table notes from authenticated; + revoke all on table notes from anon; + + grant all (note) on table notes to authenticated; + grant select (id) on table notes to authenticated; + grant delete on table notes to authenticated; + + create policy "Users can access and modify their own notes" + on notes + for all + to authenticated + using ((select auth.uid()) = user_id); + `; + }, + condition: ({ options }) => options.demo && options.cli, + }, ], nextSteps: ({ options }) => { const steps = [ @@ -375,7 +532,8 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge 1. Initialize the local development environment: ${colors.yellow('pnpm supabase init')} 2. Start the local development services: ${colors.yellow('pnpm supabase start')}. This may take a while the first time you run it - 3. Depending on your Auth selections, you may need to configure local email templates and modify ${colors.green('./supabase/config.toml')} + 3. Update ${colors.green('./supabase/config.toml')} [auth] section \`site_url\` and \`additional_redirect_urls\` to use port 5173 + 4. Depending on your Auth selections, you may need to create local email templates and update ${colors.green('./supabase/config.toml')} `, ); } diff --git a/adders/supabase/config/options.ts b/adders/supabase/config/options.ts index 03921429..441d6e9a 100644 --- a/adders/supabase/config/options.ts +++ b/adders/supabase/config/options.ts @@ -1,6 +1,11 @@ import { colors, defineAdderOptions } from '@svelte-add/core'; export const options = defineAdderOptions({ + demo: { + question: 'Do you want to include demo routes to show protected routes?', + type: 'boolean', + default: false, + }, admin: { question: `Do you want to add an Supabase admin client? ${colors.red('Warning: This client bypasses row level security, only use server side.')}`, type: 'boolean', From 45ef58aa873b25f3554cf6d0f053436afa0671e1 Mon Sep 17 00:00:00 2001 From: Stibbs Date: Tue, 3 Sep 2024 10:27:13 +1000 Subject: [PATCH 07/26] Apply suggestions from benmccann and manuel3108 Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: Manuel <30698007+manuel3108@users.noreply.github.com> --- adders/supabase/config/options.ts | 2 +- packages/config/adders/official.ts | 3 +-- packages/config/categories.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/adders/supabase/config/options.ts b/adders/supabase/config/options.ts index 441d6e9a..71e4e7df 100644 --- a/adders/supabase/config/options.ts +++ b/adders/supabase/config/options.ts @@ -7,7 +7,7 @@ export const options = defineAdderOptions({ default: false, }, admin: { - question: `Do you want to add an Supabase admin client? ${colors.red('Warning: This client bypasses row level security, only use server side.')}`, + question: `Do you want to add a Supabase admin client? ${colors.red('Warning: This client bypasses row level security, only use server side.')}`, type: 'boolean', default: false, }, diff --git a/packages/config/adders/official.ts b/packages/config/adders/official.ts index fddc0d94..10ad88a6 100644 --- a/packages/config/adders/official.ts +++ b/packages/config/adders/official.ts @@ -4,9 +4,8 @@ export const adderCategories: AdderCategories = { codeQuality: ['prettier', 'eslint'], testing: ['vitest', 'playwright'], css: ['tailwindcss'], - db: ['drizzle'], + db: ['drizzle', 'supabase'], additional: ['storybook', 'mdsvex', 'routify'], - baas: ['supabase'], }; export const adderIds = Object.values(adderCategories).flatMap((x) => x); diff --git a/packages/config/categories.ts b/packages/config/categories.ts index d4ae2d4f..09b4a371 100644 --- a/packages/config/categories.ts +++ b/packages/config/categories.ts @@ -4,7 +4,7 @@ export type CategoryInfo = { description: string; }; -export type CategoryKeys = 'codeQuality' | 'css' | 'db' | 'testing' | 'additional' | 'baas'; +export type CategoryKeys = 'codeQuality' | 'css' | 'db' | 'testing' | 'additional'; export type CategoryDetails = Record; export type AdderCategories = Record; From cb6c8bba6f4d13e188bb915cf5045ef1f24f3e1f Mon Sep 17 00:00:00 2001 From: Stibbs Date: Tue, 3 Sep 2024 10:28:20 +1000 Subject: [PATCH 08/26] actioning a missed suggestion from benmccann Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- adders/supabase/config/adder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index 0358df59..1305c46b 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -6,7 +6,7 @@ export const adder = defineAdderConfig({ id: 'supabase', name: 'Supabase', description: `Supabase is an open source Firebase alternative. -Start your project with a Postgres database, Authentication, instant APIs, Edge Functions, Realtime subscriptions, Storage, and Vector embeddings.`, +Start your project with a Postgres database, authentication, instant APIs, edge functions, realtime subscriptions, storage, and vector embeddings.`, environments: { svelte: false, kit: true }, website: { logo: './supabase.svg', From cf3d46788c51b497acce1ec62eea49b4ce15faa1 Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Tue, 3 Sep 2024 11:11:23 +1000 Subject: [PATCH 09/26] remove baas category --- packages/config/categories.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/config/categories.ts b/packages/config/categories.ts index 09b4a371..c704ab16 100644 --- a/packages/config/categories.ts +++ b/packages/config/categories.ts @@ -35,9 +35,4 @@ export const categories: CategoryDetails = { name: 'Database', description: '', }, - baas: { - id: 'baas', - name: 'Backend as a Service', - description: 'Services that provide database, Auth, storage, etc.', - }, }; From 9e818fae4b97e9dda2cb68b51150eb8da0f54f9c Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Tue, 3 Sep 2024 12:04:54 +1000 Subject: [PATCH 10/26] add packageManager to nextSteps data object --- adders/supabase/config/adder.ts | 6 ++-- packages/core/adder/config.ts | 2 ++ packages/core/adder/execute.ts | 2 +- packages/core/adder/nextSteps.ts | 7 +++- packages/core/utils/dependencies.ts | 53 ++++++++++++++++++++--------- 5 files changed, 48 insertions(+), 22 deletions(-) diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index 1305c46b..be9fa7e8 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -520,7 +520,7 @@ Start your project with a Postgres database, authentication, instant APIs, edge condition: ({ options }) => options.demo && options.cli, }, ], - nextSteps: ({ options }) => { + nextSteps: ({ options, packageManager }) => { const steps = [ 'Visit the Supabase docs: https://supabase.com/docs', 'Update the authGuard server hook function with your protected routes', @@ -530,8 +530,8 @@ Start your project with a Postgres database, authentication, instant APIs, edge steps.push( dedent`Local development environment: - 1. Initialize the local development environment: ${colors.yellow('pnpm supabase init')} - 2. Start the local development services: ${colors.yellow('pnpm supabase start')}. This may take a while the first time you run it + 1. Initialize the local development environment: ${colors.yellow(`${packageManager} supabase init`)} + 2. Start the local development services: ${colors.yellow(`${packageManager} supabase start`)}. This may take a while the first time you run it 3. Update ${colors.green('./supabase/config.toml')} [auth] section \`site_url\` and \`additional_redirect_urls\` to use port 5173 4. Depending on your Auth selections, you may need to create local email templates and update ${colors.green('./supabase/config.toml')} `, diff --git a/packages/core/adder/config.ts b/packages/core/adder/config.ts index 3b66b342..95529b7e 100644 --- a/packages/core/adder/config.ts +++ b/packages/core/adder/config.ts @@ -9,6 +9,7 @@ import type { FileTypes } from '../files/processors.js'; import type { Workspace } from '../utils/workspace.js'; import type { Postcondition } from './postconditions.js'; import type { Colors } from 'picocolors/types.js'; +import type { PackageManager } from '../utils/dependencies.js'; export type { CssAstEditor, HtmlAstEditor, JsAstEditor, SvelteAstEditor }; @@ -60,6 +61,7 @@ export type InlineAdderConfig = BaseAdderConfig string[]; installHook?: (workspace: Workspace) => Promise; uninstallHook?: (workspace: Workspace) => Promise; diff --git a/packages/core/adder/execute.ts b/packages/core/adder/execute.ts index 36935329..bccb34d4 100644 --- a/packages/core/adder/execute.ts +++ b/packages/core/adder/execute.ts @@ -270,7 +270,7 @@ async function executePlan( } if (!isTesting) { - displayNextSteps(adderDetails, isApplyingMultipleAdders, executionPlan); + await displayNextSteps(adderDetails, isApplyingMultipleAdders, executionPlan); endPrompts("You're all set!"); } } diff --git a/packages/core/adder/nextSteps.ts b/packages/core/adder/nextSteps.ts index e6225f96..f297bb9b 100644 --- a/packages/core/adder/nextSteps.ts +++ b/packages/core/adder/nextSteps.ts @@ -1,14 +1,18 @@ +import { detectPackageManager } from '../utils/dependencies'; import { messagePrompt } from '../utils/prompts'; import type { InlineAdderConfig } from './config'; import type { AdderDetails, AddersExecutionPlan } from './execute'; import type { OptionDefinition, OptionValues } from './options'; import pc from 'picocolors'; -export function displayNextSteps( +export async function displayNextSteps( adderDetails: AdderDetails[], multipleAdders: boolean, executionPlan: AddersExecutionPlan, ) { + const packageManager = await detectPackageManager(executionPlan.workingDirectory); + if (!packageManager) throw new Error('Unable to detect package manager'); + const allAddersMessage = adderDetails .filter((x) => x.config.integrationType == 'inline' && x.config.nextSteps) .map((x) => x.config as InlineAdderConfig) @@ -28,6 +32,7 @@ export function displayNextSteps( cwd: executionPlan.workingDirectory, colors: pc, docs: x.metadata.website?.documentation, + packageManager, }); adderMessage += `- ${adderNextSteps.join('\n- ')}`; return adderMessage; diff --git a/packages/core/utils/dependencies.ts b/packages/core/utils/dependencies.ts index 8ef6a57f..1973eb42 100644 --- a/packages/core/utils/dependencies.ts +++ b/packages/core/utils/dependencies.ts @@ -4,9 +4,21 @@ import { COMMANDS } from 'package-manager-detector/agents'; import { spinner } from '@svelte-add/clack-prompts'; import { executeCli } from './cli.js'; -type PackageManager = (typeof packageManagers)[number] | undefined; +export type PackageManager = (typeof packageManagers)[number]; const packageManagers = ['npm', 'pnpm', 'yarn', 'bun'] as const; +/** + * @param workingDirectory + * @returns the package manager + */ +export async function detectPackageManager( + workingDirectory: string, +): Promise { + const detectedPm = await detect({ cwd: workingDirectory }); + const pm = normalizePackageManager(detectedPm.agent); + return pm; +} + /** * @param workingDirectory * @returns the install status of dependencies @@ -14,22 +26,23 @@ const packageManagers = ['npm', 'pnpm', 'yarn', 'bun'] as const; export async function suggestInstallingDependencies( workingDirectory: string, ): Promise<'installed' | 'skipped'> { - const detectedPm = await detect({ cwd: workingDirectory }); - let selectedPm = detectedPm.agent; - - selectedPm ??= await selectPrompt( - 'Which package manager do you want to install dependencies with?', - undefined, - [ - { - label: 'None', - value: undefined, - }, - ...packageManagers.map((x) => { - return { label: x, value: x as PackageManager }; - }), - ], - ); + let selectedPm = await detectPackageManager(workingDirectory); + + if (!selectedPm) { + selectedPm = await selectPrompt( + 'Which package manager do you want to install dependencies with?', + undefined, + [ + { + label: 'None', + value: undefined, + }, + ...packageManagers.map((x) => { + return { label: x, value: x }; + }), + ], + ); + } if (!selectedPm || !COMMANDS[selectedPm]) { return 'skipped'; @@ -54,3 +67,9 @@ async function installDependencies(command: string, args: string[], workingDirec throw new Error('unable to install dependencies: ' + typedError.message); } } + +function normalizePackageManager(pm: string | undefined): PackageManager | undefined { + if (pm === 'yarn@berry' || pm === 'yarn') return 'yarn'; + if (pm === 'pnpm@6' || pm === 'pnpm') return 'pnpm'; + return pm as PackageManager; +} From ab819791f67cc9c49a82738f26dd302ac69adba0 Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Tue, 3 Sep 2024 15:06:46 +1000 Subject: [PATCH 11/26] caps reverted somehow --- adders/supabase/config/adder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index be9fa7e8..fe494bf7 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -6,7 +6,7 @@ export const adder = defineAdderConfig({ id: 'supabase', name: 'Supabase', description: `Supabase is an open source Firebase alternative. -Start your project with a Postgres database, authentication, instant APIs, edge functions, realtime subscriptions, storage, and vector embeddings.`, +Start your project with a Postgres database, Authentication, instant APIs, Edge Functions, Realtime subscriptions, Storage, and Vector embeddings.`, environments: { svelte: false, kit: true }, website: { logo: './supabase.svg', From 712f07dfaa88f4a69abdf388f41f318dbd8937b9 Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Wed, 4 Sep 2024 11:44:11 +1000 Subject: [PATCH 12/26] add script execution step between packages and files steps --- adders/drizzle/config/adder.ts | 1 + adders/eslint/config/adder.ts | 1 + adders/mdsvex/config/adder.ts | 1 + adders/playwright/config/adder.ts | 1 + adders/prettier/config/adder.ts | 1 + adders/routify/config/adder.ts | 1 + adders/supabase/config/adder.ts | 7 +++++++ adders/tailwindcss/config/adder.ts | 1 + adders/vitest/config/adder.ts | 1 + packages/core/adder/config.ts | 7 +++++++ packages/core/adder/execute.ts | 5 +++-- packages/core/files/processors.ts | 25 ++++++++++++++++++++++++- packages/core/utils/workspace.ts | 7 +++++++ 13 files changed, 56 insertions(+), 3 deletions(-) diff --git a/adders/drizzle/config/adder.ts b/adders/drizzle/config/adder.ts index 490b5f9a..e9ad3fea 100644 --- a/adders/drizzle/config/adder.ts +++ b/adders/drizzle/config/adder.ts @@ -70,6 +70,7 @@ export const adder = defineAdderConfig({ condition: ({ options }) => options.sqlite === 'libsql' || options.sqlite === 'turso', }, ], + scripts: [], files: [ { name: () => `.env`, diff --git a/adders/eslint/config/adder.ts b/adders/eslint/config/adder.ts index ca02bb91..79e8989e 100644 --- a/adders/eslint/config/adder.ts +++ b/adders/eslint/config/adder.ts @@ -36,6 +36,7 @@ export const adder = defineAdderConfig({ condition: ({ prettier }) => prettier.installed, }, ], + scripts: [], files: [ { name: () => 'package.json', diff --git a/adders/mdsvex/config/adder.ts b/adders/mdsvex/config/adder.ts index 47f6e233..fa377709 100644 --- a/adders/mdsvex/config/adder.ts +++ b/adders/mdsvex/config/adder.ts @@ -16,6 +16,7 @@ export const adder = defineAdderConfig({ options, integrationType: 'inline', packages: [{ name: 'mdsvex', version: '^0.11.2', dev: true }], + scripts: [], files: [ { name: () => `svelte.config.js`, diff --git a/adders/playwright/config/adder.ts b/adders/playwright/config/adder.ts index ff0ad4f9..93de98fb 100644 --- a/adders/playwright/config/adder.ts +++ b/adders/playwright/config/adder.ts @@ -18,6 +18,7 @@ export const adder = defineAdderConfig({ options, integrationType: 'inline', packages: [{ name: '@playwright/test', version: '^1.45.3', dev: true }], + scripts: [], files: [ { name: () => 'package.json', diff --git a/adders/prettier/config/adder.ts b/adders/prettier/config/adder.ts index 9e888b53..2d451710 100644 --- a/adders/prettier/config/adder.ts +++ b/adders/prettier/config/adder.ts @@ -26,6 +26,7 @@ export const adder = defineAdderConfig({ condition: ({ dependencies }) => hasEslint(dependencies), }, ], + scripts: [], files: [ { name: () => `.prettierignore`, diff --git a/adders/routify/config/adder.ts b/adders/routify/config/adder.ts index 36900971..ca187eb9 100644 --- a/adders/routify/config/adder.ts +++ b/adders/routify/config/adder.ts @@ -16,6 +16,7 @@ export const adder = defineAdderConfig({ options, integrationType: 'inline', packages: [{ name: '@roxi/routify', version: 'next', dev: true }], + scripts: [], files: [ { name: ({ typescript }) => `vite.config.${typescript.installed ? 'ts' : 'js'}`, diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index fe494bf7..194ca08d 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -27,6 +27,13 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge condition: ({ options }) => options.cli, }, ], + scripts: [ + { + description: 'Supabase CLI initialization', + args: ['supabase', 'init', '--with-intellij-settings false', '--with-vscode-settings false'], + condition: ({ options }) => options.cli, + }, + ], files: [ { name: () => `.env`, diff --git a/adders/tailwindcss/config/adder.ts b/adders/tailwindcss/config/adder.ts index 8700d9f9..e6a7140c 100644 --- a/adders/tailwindcss/config/adder.ts +++ b/adders/tailwindcss/config/adder.ts @@ -32,6 +32,7 @@ export const adder = defineAdderConfig({ condition: ({ prettier }) => prettier.installed, }, ], + scripts: [], files: [ { name: ({ typescript }) => `tailwind.config.${typescript.installed ? 'ts' : 'js'}`, diff --git a/adders/vitest/config/adder.ts b/adders/vitest/config/adder.ts index 2c8a99e8..4c7094dc 100644 --- a/adders/vitest/config/adder.ts +++ b/adders/vitest/config/adder.ts @@ -16,6 +16,7 @@ export const adder = defineAdderConfig({ options, integrationType: 'inline', packages: [{ name: 'vitest', version: '^2.0.4', dev: true }], + scripts: [], files: [ { name: () => 'package.json', diff --git a/packages/core/adder/config.ts b/packages/core/adder/config.ts index 95529b7e..1b78a9d2 100644 --- a/packages/core/adder/config.ts +++ b/packages/core/adder/config.ts @@ -45,6 +45,12 @@ export type PackageDefinition = { condition?: ConditionDefinition; }; +export type Scripts = { + description: string; + args: string[]; + condition?: ConditionDefinition; +}; + export type BaseAdderConfig = { metadata: AdderConfigMetadata; options: Args; @@ -55,6 +61,7 @@ export type BaseAdderConfig = { export type InlineAdderConfig = BaseAdderConfig & { integrationType: 'inline'; packages: PackageDefinition[]; + scripts: Scripts[]; files: FileTypes[]; nextSteps?: (data: { options: OptionValues; diff --git a/packages/core/adder/execute.ts b/packages/core/adder/execute.ts index bccb34d4..fb1c682f 100644 --- a/packages/core/adder/execute.ts +++ b/packages/core/adder/execute.ts @@ -3,7 +3,7 @@ import * as pc from 'picocolors'; import { serializeJson } from '@svelte-add/ast-tooling'; import { commonFilePaths, format, writeFile } from '../files/utils.js'; import { type ProjectType, createProject, detectSvelteDirectory } from '../utils/create-project.js'; -import { createOrUpdateFiles } from '../files/processors.js'; +import { createOrUpdateFiles, executeScripts } from '../files/processors.js'; import { getPackageJson } from '../utils/common.js'; import { type Workspace, @@ -281,10 +281,11 @@ async function processInlineAdder( isInstall: boolean, ) { const pkgPath = await installPackages(config, workspace); + const scriptsExecuted = await executeScripts(config.scripts, workspace); const updatedOrCreatedFiles = await createOrUpdateFiles(config.files, workspace); await runHooks(config, workspace, isInstall); - const changedFiles = [pkgPath, ...updatedOrCreatedFiles]; + const changedFiles = [pkgPath, ...scriptsExecuted, ...updatedOrCreatedFiles]; return changedFiles; } diff --git a/packages/core/files/processors.ts b/packages/core/files/processors.ts index 5478ab5c..4836c34c 100644 --- a/packages/core/files/processors.ts +++ b/packages/core/files/processors.ts @@ -20,9 +20,10 @@ import { serializeSvelteFile, } from '@svelte-add/ast-tooling'; import { fileExistsWorkspace, readFile, writeFile } from './utils.js'; -import type { ConditionDefinition } from '../adder/config.js'; +import type { ConditionDefinition, Scripts } from '../adder/config.js'; import type { OptionDefinition } from '../adder/options.js'; import type { Workspace } from '../utils/workspace.js'; +import { executeCli } from '../utils/cli.js'; export type BaseFile = { name: (options: Workspace) => string; @@ -82,6 +83,28 @@ export type FileTypes = | HtmlFile | CssFile; +export async function executeScripts( + scripts: Scripts[], + workspace: Workspace, +): Promise { + const scriptsExecuted = []; + + for (const script of scripts) { + if (script.condition && !script.condition(workspace)) { + continue; + } + try { + await executeCli(workspace.packageManager, script.args, workspace.cwd); + } catch (error) { + const typedError = error as Error; + throw new Error('Failed to execute package scripts: ' + typedError.message); + } + scriptsExecuted.push(script.description); + } + + return scriptsExecuted; +} + /** * @param files * @param workspace diff --git a/packages/core/utils/workspace.ts b/packages/core/utils/workspace.ts index b6b4c690..fa2f773a 100644 --- a/packages/core/utils/workspace.ts +++ b/packages/core/utils/workspace.ts @@ -5,6 +5,7 @@ import { getJsAstEditor } from '@svelte-add/ast-manipulation'; import type { OptionDefinition, OptionValues, Question } from '../adder/options.js'; import { remoteControl } from '../internal.js'; import path from 'path'; +import { detectPackageManager } from './dependencies.js'; export type PrettierData = { installed: boolean; @@ -27,6 +28,7 @@ export type Workspace = { typescript: TypescriptData; kit: SvelteKitData; dependencies: Record; + packageManager: string; }; export type WorkspaceWithoutExplicitArgs = Workspace>; @@ -46,6 +48,7 @@ export function createEmptyWorkspace(): Workspace routesDirectory: 'src/routes', libDirectory: 'src/lib', }, + packageManager: '', } as Workspace; } @@ -76,6 +79,10 @@ export async function populateWorkspaceDetails( ) { workspace.cwd = workingDirectory; + const packageManager = await detectPackageManager(workingDirectory); + if (!packageManager) throw new Error('Unable to detect package manager'); + workspace.packageManager = packageManager; + const tsConfigFileName = 'tsconfig.json'; const viteConfigFileName = 'vite.config.ts'; let usesTypescript = await fileExists(path.join(workingDirectory, viteConfigFileName)); From 2a8d39fd3410b419fec7a90517523da99d5e958e Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Wed, 4 Sep 2024 14:52:33 +1000 Subject: [PATCH 13/26] update config.toml and email template. improve demo --- adders/supabase/config/adder.ts | 148 ++++++++++++++++++++++++-------- 1 file changed, 114 insertions(+), 34 deletions(-) diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index 194ca08d..5644ca3b 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -234,22 +234,39 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge name: ({ kit, typescript }) => `${kit.routesDirectory}/auth/+page.server.${typescript.installed ? 'ts' : 'js'}`, contentType: 'text', - content: ({ typescript }) => { + content: ({ options, typescript }) => { return dedent` import { redirect } from '@sveltejs/kit' - ${typescript.installed ? `import type { Actions } from './$types'` : ''} + import { PUBLIC_BASE_URL } from '$env/static/public' + ${typescript.installed ? `import type { Actions } from './$types'\n` : ''} export const actions${typescript.installed ? `: Actions` : ''} = { signup: async ({ request, locals: { supabase } }) => { const formData = await request.formData() const email = formData.get('email')${typescript.installed ? ' as string' : ''} const password = formData.get('password')${typescript.installed ? ' as string' : ''} - const { error } = await supabase.auth.signUp({ email, password }) + const { error } = await supabase.auth.signUp(${ + options.demo + ? ` + { + email, + password, + options: { + emailRedirectTo: \`\${PUBLIC_BASE_URL}/private\`, + } + }` + : '{ email, password }' + }) if (error) { console.error(error) - redirect(303, '/auth/error') + return { message: 'Something went wrong, please try again.' } } else { - redirect(303, '/') + ${ + options.demo + ? `// Redirect to local Inbucket for demo purposes + redirect(303, \`http://localhost:54324/m/\${email}\`)` + : `return { message: 'Sign up succeeded! Please check your email inbox.' }` + } } }, login: async ({ request, locals: { supabase } }) => { @@ -260,7 +277,7 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge const { error } = await supabase.auth.signInWithPassword({ email, password }) if (error) { console.error(error) - redirect(303, '/auth/error') + return { message: 'Something went wrong, please try again.' } } else { redirect(303, '/private') } @@ -272,9 +289,15 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge { name: ({ kit }) => `${kit.routesDirectory}/auth/+page.svelte`, contentType: 'text', - content: () => { + content: ({ typescript }) => { return dedent` -
+ + +
+
+						User: {JSON.stringify($page.data.user, null, 2)}
+					
+ `; + }, + condition: ({ options }) => options.demo, + }, { name: ({ kit, typescript }) => `${kit.routesDirectory}/private/+layout.server.${typescript.installed ? 'ts' : 'js'}`, @@ -526,6 +564,47 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge }, condition: ({ options }) => options.demo && options.cli, }, + { + name: () => './supabase/config.toml', + contentType: 'text', + content: ({ content }) => { + content = content.replace('"http://127.0.0.1:3000"', '"http://localhost:5173"'); + content = content.replace('"https://127.0.0.1:3000"', '"https://localhost:5173/*"'); + content = content.replace('enable_confirmations = false', 'enable_confirmations = true'); + + content = appendContent( + content, + dedent` + \n# Custom email confirmation template + [auth.email.template.confirmation] + subject = "Confirm Your Signup" + content_path = "./supabase/templates/confirmation.html" + `, + ); + + return content; + }, + condition: ({ options }) => options.cli, + }, + { + name: () => './supabase/templates/confirmation.html', + contentType: 'text', + content: () => { + return dedent` + + +

Confirm your signup

+

Follow this link to confirm your user:

+

Confirm your email

+ + + `; + }, + condition: ({ options }) => options.cli, + }, ], nextSteps: ({ options, packageManager }) => { const steps = [ @@ -550,6 +629,7 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge }); function generateEnvFileContent({ content, options }: TextFileEditorArgs) { + content = addEnvVar(content, 'PUBLIC_BASE_URL', '"http://localhost:5173"'); content = addEnvVar( content, 'PUBLIC_SUPABASE_URL', @@ -581,12 +661,12 @@ function generateEnvFileContent({ content, options }: TextFileEditorArgs Date: Wed, 4 Sep 2024 17:21:30 +1000 Subject: [PATCH 14/26] adder.scripts optional --- adders/drizzle/config/adder.ts | 1 - adders/eslint/config/adder.ts | 1 - adders/mdsvex/config/adder.ts | 1 - adders/playwright/config/adder.ts | 1 - adders/prettier/config/adder.ts | 1 - adders/routify/config/adder.ts | 1 - adders/tailwindcss/config/adder.ts | 1 - adders/vitest/config/adder.ts | 1 - packages/core/adder/config.ts | 2 +- packages/core/adder/execute.ts | 8 ++++++-- 10 files changed, 7 insertions(+), 11 deletions(-) diff --git a/adders/drizzle/config/adder.ts b/adders/drizzle/config/adder.ts index e9ad3fea..490b5f9a 100644 --- a/adders/drizzle/config/adder.ts +++ b/adders/drizzle/config/adder.ts @@ -70,7 +70,6 @@ export const adder = defineAdderConfig({ condition: ({ options }) => options.sqlite === 'libsql' || options.sqlite === 'turso', }, ], - scripts: [], files: [ { name: () => `.env`, diff --git a/adders/eslint/config/adder.ts b/adders/eslint/config/adder.ts index 79e8989e..ca02bb91 100644 --- a/adders/eslint/config/adder.ts +++ b/adders/eslint/config/adder.ts @@ -36,7 +36,6 @@ export const adder = defineAdderConfig({ condition: ({ prettier }) => prettier.installed, }, ], - scripts: [], files: [ { name: () => 'package.json', diff --git a/adders/mdsvex/config/adder.ts b/adders/mdsvex/config/adder.ts index fa377709..47f6e233 100644 --- a/adders/mdsvex/config/adder.ts +++ b/adders/mdsvex/config/adder.ts @@ -16,7 +16,6 @@ export const adder = defineAdderConfig({ options, integrationType: 'inline', packages: [{ name: 'mdsvex', version: '^0.11.2', dev: true }], - scripts: [], files: [ { name: () => `svelte.config.js`, diff --git a/adders/playwright/config/adder.ts b/adders/playwright/config/adder.ts index 93de98fb..ff0ad4f9 100644 --- a/adders/playwright/config/adder.ts +++ b/adders/playwright/config/adder.ts @@ -18,7 +18,6 @@ export const adder = defineAdderConfig({ options, integrationType: 'inline', packages: [{ name: '@playwright/test', version: '^1.45.3', dev: true }], - scripts: [], files: [ { name: () => 'package.json', diff --git a/adders/prettier/config/adder.ts b/adders/prettier/config/adder.ts index 2d451710..9e888b53 100644 --- a/adders/prettier/config/adder.ts +++ b/adders/prettier/config/adder.ts @@ -26,7 +26,6 @@ export const adder = defineAdderConfig({ condition: ({ dependencies }) => hasEslint(dependencies), }, ], - scripts: [], files: [ { name: () => `.prettierignore`, diff --git a/adders/routify/config/adder.ts b/adders/routify/config/adder.ts index ca187eb9..36900971 100644 --- a/adders/routify/config/adder.ts +++ b/adders/routify/config/adder.ts @@ -16,7 +16,6 @@ export const adder = defineAdderConfig({ options, integrationType: 'inline', packages: [{ name: '@roxi/routify', version: 'next', dev: true }], - scripts: [], files: [ { name: ({ typescript }) => `vite.config.${typescript.installed ? 'ts' : 'js'}`, diff --git a/adders/tailwindcss/config/adder.ts b/adders/tailwindcss/config/adder.ts index e6a7140c..8700d9f9 100644 --- a/adders/tailwindcss/config/adder.ts +++ b/adders/tailwindcss/config/adder.ts @@ -32,7 +32,6 @@ export const adder = defineAdderConfig({ condition: ({ prettier }) => prettier.installed, }, ], - scripts: [], files: [ { name: ({ typescript }) => `tailwind.config.${typescript.installed ? 'ts' : 'js'}`, diff --git a/adders/vitest/config/adder.ts b/adders/vitest/config/adder.ts index 4c7094dc..2c8a99e8 100644 --- a/adders/vitest/config/adder.ts +++ b/adders/vitest/config/adder.ts @@ -16,7 +16,6 @@ export const adder = defineAdderConfig({ options, integrationType: 'inline', packages: [{ name: 'vitest', version: '^2.0.4', dev: true }], - scripts: [], files: [ { name: () => 'package.json', diff --git a/packages/core/adder/config.ts b/packages/core/adder/config.ts index 1b78a9d2..9ca5074f 100644 --- a/packages/core/adder/config.ts +++ b/packages/core/adder/config.ts @@ -61,7 +61,7 @@ export type BaseAdderConfig = { export type InlineAdderConfig = BaseAdderConfig & { integrationType: 'inline'; packages: PackageDefinition[]; - scripts: Scripts[]; + scripts?: Scripts[]; files: FileTypes[]; nextSteps?: (data: { options: OptionValues; diff --git a/packages/core/adder/execute.ts b/packages/core/adder/execute.ts index fb1c682f..6039e973 100644 --- a/packages/core/adder/execute.ts +++ b/packages/core/adder/execute.ts @@ -281,11 +281,15 @@ async function processInlineAdder( isInstall: boolean, ) { const pkgPath = await installPackages(config, workspace); - const scriptsExecuted = await executeScripts(config.scripts, workspace); + + if (config.scripts && config.scripts.length > 0) { + await executeScripts(config.scripts, workspace); + } + const updatedOrCreatedFiles = await createOrUpdateFiles(config.files, workspace); await runHooks(config, workspace, isInstall); - const changedFiles = [pkgPath, ...scriptsExecuted, ...updatedOrCreatedFiles]; + const changedFiles = [pkgPath, ...updatedOrCreatedFiles]; return changedFiles; } From 3da363e2c8d7102f07a4c7a70bb7505205b231d6 Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Wed, 4 Sep 2024 20:41:17 +1000 Subject: [PATCH 15/26] more experimenting --- packages/core/adder/execute.ts | 18 +++++++++--- packages/core/adder/nextSteps.ts | 8 ++---- packages/core/files/processors.ts | 8 ++++++ packages/core/utils/create-project.ts | 9 ++++++ packages/core/utils/dependencies.ts | 41 +++++++++++++++------------ packages/core/utils/workspace.ts | 8 +++--- 6 files changed, 60 insertions(+), 32 deletions(-) diff --git a/packages/core/adder/execute.ts b/packages/core/adder/execute.ts index 6039e973..76e4feb7 100644 --- a/packages/core/adder/execute.ts +++ b/packages/core/adder/execute.ts @@ -28,7 +28,7 @@ import type { InlineAdderConfig, } from './config.js'; import type { RemoteControlOptions } from './remoteControl.js'; -import { suggestInstallingDependencies } from '../utils/dependencies.js'; +import { suggestInstallingDependencies, type PackageManager } from '../utils/dependencies.js'; import { validatePreconditions } from './preconditions.js'; import { endPrompts, startPrompts } from '../utils/prompts.js'; import { checkPostconditions, printUnmetPostconditions } from './postconditions.js'; @@ -57,6 +57,7 @@ export type AddersExecutionPlan = { commonCliOptions: AvailableCliOptionValues; cliOptionsByAdderId: Record>; workingDirectory: string; + packageManager: PackageManager | undefined; selectAddersToApply?: AddersToApplySelector; }; @@ -102,6 +103,7 @@ export async function executeAdders( commonCliOptions, cliOptionsByAdderId, selectAddersToApply, + packageManager: undefined, }; await executePlan(executionPlan, executingAdder, adderDetails, remoteControlOptions); @@ -133,9 +135,14 @@ async function executePlan( const cwd = executionPlan.commonCliOptions.path ?? executionPlan.workingDirectory; const supportKit = adderDetails.some((x) => x.config.metadata.environments.kit); const supportSvelte = adderDetails.some((x) => x.config.metadata.environments.svelte); - const { projectCreated, directory } = await createProject(cwd, supportKit, supportSvelte); + const { projectCreated, directory, packageManager } = await createProject( + cwd, + supportKit, + supportSvelte, + ); if (!projectCreated) return; executionPlan.workingDirectory = directory; + executionPlan.packageManager = packageManager; } const workspace = createEmptyWorkspace(); @@ -255,7 +262,10 @@ async function executePlan( let installStatus; if (!remoteControlled && !executionPlan.commonCliOptions.skipInstall) - installStatus = await suggestInstallingDependencies(executionPlan.workingDirectory); + installStatus = await suggestInstallingDependencies( + executionPlan.packageManager, + executionPlan.workingDirectory, + ); if (installStatus === 'installed' && workspace.prettier.installed) { const formatSpinner = spinner(); @@ -270,7 +280,7 @@ async function executePlan( } if (!isTesting) { - await displayNextSteps(adderDetails, isApplyingMultipleAdders, executionPlan); + displayNextSteps(adderDetails, isApplyingMultipleAdders, executionPlan); endPrompts("You're all set!"); } } diff --git a/packages/core/adder/nextSteps.ts b/packages/core/adder/nextSteps.ts index f297bb9b..a065d0f4 100644 --- a/packages/core/adder/nextSteps.ts +++ b/packages/core/adder/nextSteps.ts @@ -1,18 +1,14 @@ -import { detectPackageManager } from '../utils/dependencies'; import { messagePrompt } from '../utils/prompts'; import type { InlineAdderConfig } from './config'; import type { AdderDetails, AddersExecutionPlan } from './execute'; import type { OptionDefinition, OptionValues } from './options'; import pc from 'picocolors'; -export async function displayNextSteps( +export function displayNextSteps( adderDetails: AdderDetails[], multipleAdders: boolean, executionPlan: AddersExecutionPlan, ) { - const packageManager = await detectPackageManager(executionPlan.workingDirectory); - if (!packageManager) throw new Error('Unable to detect package manager'); - const allAddersMessage = adderDetails .filter((x) => x.config.integrationType == 'inline' && x.config.nextSteps) .map((x) => x.config as InlineAdderConfig) @@ -32,7 +28,7 @@ export async function displayNextSteps( cwd: executionPlan.workingDirectory, colors: pc, docs: x.metadata.website?.documentation, - packageManager, + packageManager: executionPlan.packageManager ?? 'pnpm', }); adderMessage += `- ${adderNextSteps.join('\n- ')}`; return adderMessage; diff --git a/packages/core/files/processors.ts b/packages/core/files/processors.ts index 4836c34c..ddb190d9 100644 --- a/packages/core/files/processors.ts +++ b/packages/core/files/processors.ts @@ -24,6 +24,8 @@ import type { ConditionDefinition, Scripts } from '../adder/config.js'; import type { OptionDefinition } from '../adder/options.js'; import type { Workspace } from '../utils/workspace.js'; import { executeCli } from '../utils/cli.js'; +import { COMMANDS } from 'package-manager-detector/agents'; +import { installDependencies } from '../utils/dependencies.js'; export type BaseFile = { name: (options: Workspace) => string; @@ -89,6 +91,12 @@ export async function executeScripts( ): Promise { const scriptsExecuted = []; + if (scripts.length === 0) return []; + if (!workspace.packageManager) return []; + + const installCommand = COMMANDS[workspace.packageManager].install; + await installDependencies(workspace.packageManager, [installCommand], workspace.cwd); + for (const script of scripts) { if (script.condition && !script.condition(workspace)) { continue; diff --git a/packages/core/utils/create-project.ts b/packages/core/utils/create-project.ts index 8fc807ea..bc52108a 100644 --- a/packages/core/utils/create-project.ts +++ b/packages/core/utils/create-project.ts @@ -98,6 +98,14 @@ export async function createProject(cwd: string, supportKit: boolean, supportSve args = ['init', 'vite@latest', directory, '--', '--template', template]; } + let packageManager = undefined; + packageManager = await selectPrompt('Choose package manager', packageManager, [ + { label: 'npm', value: 'npm' }, + { label: 'pnpm', value: 'pnpm' }, + { label: 'yarn', value: 'yarn' }, + { label: 'bun', value: 'bun' }, + ]); + const loadingSpinner = spinner(); loadingSpinner.start('Initializing template...'); @@ -119,5 +127,6 @@ export async function createProject(cwd: string, supportKit: boolean, supportSve return { projectCreated: true, directory: path.join(process.cwd(), directory), + packageManager, }; } diff --git a/packages/core/utils/dependencies.ts b/packages/core/utils/dependencies.ts index 1973eb42..fe61c59a 100644 --- a/packages/core/utils/dependencies.ts +++ b/packages/core/utils/dependencies.ts @@ -11,25 +11,14 @@ const packageManagers = ['npm', 'pnpm', 'yarn', 'bun'] as const; * @param workingDirectory * @returns the package manager */ -export async function detectPackageManager( +export async function selectPackageManager( workingDirectory: string, ): Promise { const detectedPm = await detect({ cwd: workingDirectory }); - const pm = normalizePackageManager(detectedPm.agent); - return pm; -} + let pm = normalizePackageManager(detectedPm.agent); -/** - * @param workingDirectory - * @returns the install status of dependencies - */ -export async function suggestInstallingDependencies( - workingDirectory: string, -): Promise<'installed' | 'skipped'> { - let selectedPm = await detectPackageManager(workingDirectory); - - if (!selectedPm) { - selectedPm = await selectPrompt( + if (!pm) { + pm = await selectPrompt( 'Which package manager do you want to install dependencies with?', undefined, [ @@ -44,14 +33,26 @@ export async function suggestInstallingDependencies( ); } - if (!selectedPm || !COMMANDS[selectedPm]) { + return pm; +} + +/** + * @param packageManager + * @param workingDirectory + * @returns the install status of dependencies + */ +export async function suggestInstallingDependencies( + packageManager: PackageManager | undefined, + workingDirectory: string, +): Promise<'installed' | 'skipped'> { + if (!packageManager || !COMMANDS[packageManager]) { return 'skipped'; } const loadingSpinner = spinner(); loadingSpinner.start('Installing dependencies...'); - const installCommand = COMMANDS[selectedPm].install; + const installCommand = COMMANDS[packageManager].install; const [pm, install] = installCommand.split(' '); await installDependencies(pm, [install], workingDirectory); @@ -59,7 +60,11 @@ export async function suggestInstallingDependencies( return 'installed'; } -async function installDependencies(command: string, args: string[], workingDirectory: string) { +export async function installDependencies( + command: string, + args: string[], + workingDirectory: string, +) { try { await executeCli(command, args, workingDirectory); } catch (error) { diff --git a/packages/core/utils/workspace.ts b/packages/core/utils/workspace.ts index fa2f773a..01d45241 100644 --- a/packages/core/utils/workspace.ts +++ b/packages/core/utils/workspace.ts @@ -5,7 +5,7 @@ import { getJsAstEditor } from '@svelte-add/ast-manipulation'; import type { OptionDefinition, OptionValues, Question } from '../adder/options.js'; import { remoteControl } from '../internal.js'; import path from 'path'; -import { detectPackageManager } from './dependencies.js'; +import { selectPackageManager, type PackageManager } from './dependencies.js'; export type PrettierData = { installed: boolean; @@ -28,7 +28,7 @@ export type Workspace = { typescript: TypescriptData; kit: SvelteKitData; dependencies: Record; - packageManager: string; + packageManager: PackageManager | undefined; }; export type WorkspaceWithoutExplicitArgs = Workspace>; @@ -48,7 +48,7 @@ export function createEmptyWorkspace(): Workspace routesDirectory: 'src/routes', libDirectory: 'src/lib', }, - packageManager: '', + packageManager: undefined, } as Workspace; } @@ -79,7 +79,7 @@ export async function populateWorkspaceDetails( ) { workspace.cwd = workingDirectory; - const packageManager = await detectPackageManager(workingDirectory); + const packageManager = await selectPackageManager(workingDirectory); if (!packageManager) throw new Error('Unable to detect package manager'); workspace.packageManager = packageManager; From 9e47a80123f1e98adca4c7adc69da0f2bcdde226 Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Wed, 4 Sep 2024 20:58:47 +1000 Subject: [PATCH 16/26] revert script execution experiment --- adders/supabase/config/adder.ts | 60 +++------------------------ packages/core/adder/config.ts | 9 ---- packages/core/adder/execute.ts | 23 ++-------- packages/core/adder/nextSteps.ts | 1 - packages/core/files/processors.ts | 33 +-------------- packages/core/utils/create-project.ts | 9 ---- packages/core/utils/dependencies.ts | 60 +++++++++------------------ packages/core/utils/workspace.ts | 7 ---- 8 files changed, 31 insertions(+), 171 deletions(-) diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index 5644ca3b..1f0572bb 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -27,13 +27,6 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge condition: ({ options }) => options.cli, }, ], - scripts: [ - { - description: 'Supabase CLI initialization', - args: ['supabase', 'init', '--with-intellij-settings false', '--with-vscode-settings false'], - condition: ({ options }) => options.cli, - }, - ], files: [ { name: () => `.env`, @@ -564,49 +557,8 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge }, condition: ({ options }) => options.demo && options.cli, }, - { - name: () => './supabase/config.toml', - contentType: 'text', - content: ({ content }) => { - content = content.replace('"http://127.0.0.1:3000"', '"http://localhost:5173"'); - content = content.replace('"https://127.0.0.1:3000"', '"https://localhost:5173/*"'); - content = content.replace('enable_confirmations = false', 'enable_confirmations = true'); - - content = appendContent( - content, - dedent` - \n# Custom email confirmation template - [auth.email.template.confirmation] - subject = "Confirm Your Signup" - content_path = "./supabase/templates/confirmation.html" - `, - ); - - return content; - }, - condition: ({ options }) => options.cli, - }, - { - name: () => './supabase/templates/confirmation.html', - contentType: 'text', - content: () => { - return dedent` - - -

Confirm your signup

-

Follow this link to confirm your user:

-

Confirm your email

- - - `; - }, - condition: ({ options }) => options.cli, - }, ], - nextSteps: ({ options, packageManager }) => { + nextSteps: ({ options }) => { const steps = [ 'Visit the Supabase docs: https://supabase.com/docs', 'Update the authGuard server hook function with your protected routes', @@ -616,9 +568,9 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge steps.push( dedent`Local development environment: - 1. Initialize the local development environment: ${colors.yellow(`${packageManager} supabase init`)} - 2. Start the local development services: ${colors.yellow(`${packageManager} supabase start`)}. This may take a while the first time you run it - 3. Update ${colors.green('./supabase/config.toml')} [auth] section \`site_url\` and \`additional_redirect_urls\` to use port 5173 + 1. Initialize the local development environment (E.g., ${colors.yellow(`npx supabase init`)} or ${colors.yellow(`pnpm supabase init`)}) + 2. Start the local development services (E.g., ${colors.yellow(`npx supabase start`)} or ${colors.yellow(`pnpm supabase start`)}) + 3. Update ${colors.green('./supabase/config.toml')} [auth] section \`site_url\` and \`additional_redirect_urls\` to use port http://localhost:5173 4. Depending on your Auth selections, you may need to create local email templates and update ${colors.green('./supabase/config.toml')} `, ); @@ -661,12 +613,12 @@ function generateEnvFileContent({ content, options }: TextFileEditorArgs = { condition?: ConditionDefinition; }; -export type Scripts = { - description: string; - args: string[]; - condition?: ConditionDefinition; -}; - export type BaseAdderConfig = { metadata: AdderConfigMetadata; options: Args; @@ -61,14 +54,12 @@ export type BaseAdderConfig = { export type InlineAdderConfig = BaseAdderConfig & { integrationType: 'inline'; packages: PackageDefinition[]; - scripts?: Scripts[]; files: FileTypes[]; nextSteps?: (data: { options: OptionValues; cwd: string; colors: Colors; docs: string | undefined; - packageManager: PackageManager; }) => string[]; installHook?: (workspace: Workspace) => Promise; uninstallHook?: (workspace: Workspace) => Promise; diff --git a/packages/core/adder/execute.ts b/packages/core/adder/execute.ts index 76e4feb7..36935329 100644 --- a/packages/core/adder/execute.ts +++ b/packages/core/adder/execute.ts @@ -3,7 +3,7 @@ import * as pc from 'picocolors'; import { serializeJson } from '@svelte-add/ast-tooling'; import { commonFilePaths, format, writeFile } from '../files/utils.js'; import { type ProjectType, createProject, detectSvelteDirectory } from '../utils/create-project.js'; -import { createOrUpdateFiles, executeScripts } from '../files/processors.js'; +import { createOrUpdateFiles } from '../files/processors.js'; import { getPackageJson } from '../utils/common.js'; import { type Workspace, @@ -28,7 +28,7 @@ import type { InlineAdderConfig, } from './config.js'; import type { RemoteControlOptions } from './remoteControl.js'; -import { suggestInstallingDependencies, type PackageManager } from '../utils/dependencies.js'; +import { suggestInstallingDependencies } from '../utils/dependencies.js'; import { validatePreconditions } from './preconditions.js'; import { endPrompts, startPrompts } from '../utils/prompts.js'; import { checkPostconditions, printUnmetPostconditions } from './postconditions.js'; @@ -57,7 +57,6 @@ export type AddersExecutionPlan = { commonCliOptions: AvailableCliOptionValues; cliOptionsByAdderId: Record>; workingDirectory: string; - packageManager: PackageManager | undefined; selectAddersToApply?: AddersToApplySelector; }; @@ -103,7 +102,6 @@ export async function executeAdders( commonCliOptions, cliOptionsByAdderId, selectAddersToApply, - packageManager: undefined, }; await executePlan(executionPlan, executingAdder, adderDetails, remoteControlOptions); @@ -135,14 +133,9 @@ async function executePlan( const cwd = executionPlan.commonCliOptions.path ?? executionPlan.workingDirectory; const supportKit = adderDetails.some((x) => x.config.metadata.environments.kit); const supportSvelte = adderDetails.some((x) => x.config.metadata.environments.svelte); - const { projectCreated, directory, packageManager } = await createProject( - cwd, - supportKit, - supportSvelte, - ); + const { projectCreated, directory } = await createProject(cwd, supportKit, supportSvelte); if (!projectCreated) return; executionPlan.workingDirectory = directory; - executionPlan.packageManager = packageManager; } const workspace = createEmptyWorkspace(); @@ -262,10 +255,7 @@ async function executePlan( let installStatus; if (!remoteControlled && !executionPlan.commonCliOptions.skipInstall) - installStatus = await suggestInstallingDependencies( - executionPlan.packageManager, - executionPlan.workingDirectory, - ); + installStatus = await suggestInstallingDependencies(executionPlan.workingDirectory); if (installStatus === 'installed' && workspace.prettier.installed) { const formatSpinner = spinner(); @@ -291,11 +281,6 @@ async function processInlineAdder( isInstall: boolean, ) { const pkgPath = await installPackages(config, workspace); - - if (config.scripts && config.scripts.length > 0) { - await executeScripts(config.scripts, workspace); - } - const updatedOrCreatedFiles = await createOrUpdateFiles(config.files, workspace); await runHooks(config, workspace, isInstall); diff --git a/packages/core/adder/nextSteps.ts b/packages/core/adder/nextSteps.ts index a065d0f4..e6225f96 100644 --- a/packages/core/adder/nextSteps.ts +++ b/packages/core/adder/nextSteps.ts @@ -28,7 +28,6 @@ export function displayNextSteps( cwd: executionPlan.workingDirectory, colors: pc, docs: x.metadata.website?.documentation, - packageManager: executionPlan.packageManager ?? 'pnpm', }); adderMessage += `- ${adderNextSteps.join('\n- ')}`; return adderMessage; diff --git a/packages/core/files/processors.ts b/packages/core/files/processors.ts index ddb190d9..5478ab5c 100644 --- a/packages/core/files/processors.ts +++ b/packages/core/files/processors.ts @@ -20,12 +20,9 @@ import { serializeSvelteFile, } from '@svelte-add/ast-tooling'; import { fileExistsWorkspace, readFile, writeFile } from './utils.js'; -import type { ConditionDefinition, Scripts } from '../adder/config.js'; +import type { ConditionDefinition } from '../adder/config.js'; import type { OptionDefinition } from '../adder/options.js'; import type { Workspace } from '../utils/workspace.js'; -import { executeCli } from '../utils/cli.js'; -import { COMMANDS } from 'package-manager-detector/agents'; -import { installDependencies } from '../utils/dependencies.js'; export type BaseFile = { name: (options: Workspace) => string; @@ -85,34 +82,6 @@ export type FileTypes = | HtmlFile | CssFile; -export async function executeScripts( - scripts: Scripts[], - workspace: Workspace, -): Promise { - const scriptsExecuted = []; - - if (scripts.length === 0) return []; - if (!workspace.packageManager) return []; - - const installCommand = COMMANDS[workspace.packageManager].install; - await installDependencies(workspace.packageManager, [installCommand], workspace.cwd); - - for (const script of scripts) { - if (script.condition && !script.condition(workspace)) { - continue; - } - try { - await executeCli(workspace.packageManager, script.args, workspace.cwd); - } catch (error) { - const typedError = error as Error; - throw new Error('Failed to execute package scripts: ' + typedError.message); - } - scriptsExecuted.push(script.description); - } - - return scriptsExecuted; -} - /** * @param files * @param workspace diff --git a/packages/core/utils/create-project.ts b/packages/core/utils/create-project.ts index bc52108a..8fc807ea 100644 --- a/packages/core/utils/create-project.ts +++ b/packages/core/utils/create-project.ts @@ -98,14 +98,6 @@ export async function createProject(cwd: string, supportKit: boolean, supportSve args = ['init', 'vite@latest', directory, '--', '--template', template]; } - let packageManager = undefined; - packageManager = await selectPrompt('Choose package manager', packageManager, [ - { label: 'npm', value: 'npm' }, - { label: 'pnpm', value: 'pnpm' }, - { label: 'yarn', value: 'yarn' }, - { label: 'bun', value: 'bun' }, - ]); - const loadingSpinner = spinner(); loadingSpinner.start('Initializing template...'); @@ -127,6 +119,5 @@ export async function createProject(cwd: string, supportKit: boolean, supportSve return { projectCreated: true, directory: path.join(process.cwd(), directory), - packageManager, }; } diff --git a/packages/core/utils/dependencies.ts b/packages/core/utils/dependencies.ts index fe61c59a..ed41128c 100644 --- a/packages/core/utils/dependencies.ts +++ b/packages/core/utils/dependencies.ts @@ -4,55 +4,41 @@ import { COMMANDS } from 'package-manager-detector/agents'; import { spinner } from '@svelte-add/clack-prompts'; import { executeCli } from './cli.js'; -export type PackageManager = (typeof packageManagers)[number]; +type PackageManager = (typeof packageManagers)[number] | undefined; const packageManagers = ['npm', 'pnpm', 'yarn', 'bun'] as const; /** - * @param workingDirectory - * @returns the package manager - */ -export async function selectPackageManager( - workingDirectory: string, -): Promise { - const detectedPm = await detect({ cwd: workingDirectory }); - let pm = normalizePackageManager(detectedPm.agent); - - if (!pm) { - pm = await selectPrompt( - 'Which package manager do you want to install dependencies with?', - undefined, - [ - { - label: 'None', - value: undefined, - }, - ...packageManagers.map((x) => { - return { label: x, value: x }; - }), - ], - ); - } - - return pm; -} - -/** - * @param packageManager * @param workingDirectory * @returns the install status of dependencies */ export async function suggestInstallingDependencies( - packageManager: PackageManager | undefined, workingDirectory: string, ): Promise<'installed' | 'skipped'> { - if (!packageManager || !COMMANDS[packageManager]) { + const detectedPm = await detect({ cwd: workingDirectory }); + let selectedPm = detectedPm.agent; + + selectedPm ??= await selectPrompt( + 'Which package manager do you want to install dependencies with?', + undefined, + [ + { + label: 'None', + value: undefined, + }, + ...packageManagers.map((x) => { + return { label: x, value: x as PackageManager }; + }), + ], + ); + + if (!selectedPm || !COMMANDS[selectedPm]) { return 'skipped'; } const loadingSpinner = spinner(); loadingSpinner.start('Installing dependencies...'); - const installCommand = COMMANDS[packageManager].install; + const installCommand = COMMANDS[selectedPm].install; const [pm, install] = installCommand.split(' '); await installDependencies(pm, [install], workingDirectory); @@ -72,9 +58,3 @@ export async function installDependencies( throw new Error('unable to install dependencies: ' + typedError.message); } } - -function normalizePackageManager(pm: string | undefined): PackageManager | undefined { - if (pm === 'yarn@berry' || pm === 'yarn') return 'yarn'; - if (pm === 'pnpm@6' || pm === 'pnpm') return 'pnpm'; - return pm as PackageManager; -} diff --git a/packages/core/utils/workspace.ts b/packages/core/utils/workspace.ts index 01d45241..b6b4c690 100644 --- a/packages/core/utils/workspace.ts +++ b/packages/core/utils/workspace.ts @@ -5,7 +5,6 @@ import { getJsAstEditor } from '@svelte-add/ast-manipulation'; import type { OptionDefinition, OptionValues, Question } from '../adder/options.js'; import { remoteControl } from '../internal.js'; import path from 'path'; -import { selectPackageManager, type PackageManager } from './dependencies.js'; export type PrettierData = { installed: boolean; @@ -28,7 +27,6 @@ export type Workspace = { typescript: TypescriptData; kit: SvelteKitData; dependencies: Record; - packageManager: PackageManager | undefined; }; export type WorkspaceWithoutExplicitArgs = Workspace>; @@ -48,7 +46,6 @@ export function createEmptyWorkspace(): Workspace routesDirectory: 'src/routes', libDirectory: 'src/lib', }, - packageManager: undefined, } as Workspace; } @@ -79,10 +76,6 @@ export async function populateWorkspaceDetails( ) { workspace.cwd = workingDirectory; - const packageManager = await selectPackageManager(workingDirectory); - if (!packageManager) throw new Error('Unable to detect package manager'); - workspace.packageManager = packageManager; - const tsConfigFileName = 'tsconfig.json'; const viteConfigFileName = 'vite.config.ts'; let usesTypescript = await fileExists(path.join(workingDirectory, viteConfigFileName)); From fa1d289a4017c5fd1144e1c541f7759db2e28c29 Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Wed, 4 Sep 2024 21:00:48 +1000 Subject: [PATCH 17/26] revert formatting change --- packages/core/utils/dependencies.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/core/utils/dependencies.ts b/packages/core/utils/dependencies.ts index ed41128c..8ef6a57f 100644 --- a/packages/core/utils/dependencies.ts +++ b/packages/core/utils/dependencies.ts @@ -46,11 +46,7 @@ export async function suggestInstallingDependencies( return 'installed'; } -export async function installDependencies( - command: string, - args: string[], - workingDirectory: string, -) { +async function installDependencies(command: string, args: string[], workingDirectory: string) { try { await executeCli(command, args, workingDirectory); } catch (error) { From 9ed4603779e4420313a41a98c903c3df1e85f6a0 Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Thu, 5 Sep 2024 14:45:26 +1000 Subject: [PATCH 18/26] better script experiment --- adders/supabase/config/adder.ts | 72 ++++++++++++++++++++----- packages/core/adder/config.ts | 10 ++++ packages/core/adder/execute.ts | 67 +++++++++++++++++++++-- packages/core/adder/nextSteps.ts | 1 + packages/core/utils/dependencies.ts | 48 ++++++++++++----- packages/core/utils/workspace.ts | 5 ++ packages/testing-library/utils/adder.ts | 2 +- 7 files changed, 174 insertions(+), 31 deletions(-) diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index 1f0572bb..aeee2fea 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -27,6 +27,14 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge condition: ({ options }) => options.cli, }, ], + scripts: [ + { + description: 'Supabase CLI initialization', + args: ['supabase', 'init', '--with-intellij-settings false', '--with-vscode-settings false'], + type: 'dependency', + condition: ({ options }) => options.cli, + }, + ], files: [ { name: () => `.env`, @@ -385,6 +393,47 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge }, condition: ({ options }) => options.helpers, }, + { + name: () => './supabase/config.toml', + contentType: 'text', + content: ({ content }) => { + content = content.replace('"http://127.0.0.1:3000"', '"http://localhost:5173"'); + content = content.replace('"https://127.0.0.1:3000"', '"https://localhost:5173/*"'); + content = content.replace('enable_confirmations = false', 'enable_confirmations = true'); + + content = appendContent( + content, + dedent` + \n# Custom email confirmation template + [auth.email.template.confirmation] + subject = "Confirm Your Signup" + content_path = "./supabase/templates/confirmation.html" + `, + ); + + return content; + }, + condition: ({ options }) => options.cli, + }, + { + name: () => './supabase/templates/confirmation.html', + contentType: 'text', + content: () => { + return dedent` + + +

Confirm your signup

+

Follow this link to confirm your user:

+

Confirm your email

+ + + `; + }, + condition: ({ options }) => options.cli, + }, // Demo routes { name: ({ kit }) => `${kit.routesDirectory}/+page.svelte`, @@ -558,22 +607,21 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge condition: ({ options }) => options.demo && options.cli, }, ], - nextSteps: ({ options }) => { + nextSteps: ({ options, packageManager }) => { + let command: string; + if (!packageManager || packageManager === 'npm') { + command = 'npx'; + } else { + command = packageManager; + } + const steps = [ 'Visit the Supabase docs: https://supabase.com/docs', 'Update the authGuard server hook function with your protected routes', ]; if (options.cli) { - steps.push( - dedent`Local development environment: - - 1. Initialize the local development environment (E.g., ${colors.yellow(`npx supabase init`)} or ${colors.yellow(`pnpm supabase init`)}) - 2. Start the local development services (E.g., ${colors.yellow(`npx supabase start`)} or ${colors.yellow(`pnpm supabase start`)}) - 3. Update ${colors.green('./supabase/config.toml')} [auth] section \`site_url\` and \`additional_redirect_urls\` to use port http://localhost:5173 - 4. Depending on your Auth selections, you may need to create local email templates and update ${colors.green('./supabase/config.toml')} - `, - ); + steps.push(`Start local Supabase services: ${colors.yellow(`${command} supabase start`)}`); } return steps; @@ -613,12 +661,12 @@ function generateEnvFileContent({ content, options }: TextFileEditorArgs = { condition?: ConditionDefinition; }; +export type Scripts = { + description: string; + args: string[]; + type: 'dependency' | 'external'; + condition?: ConditionDefinition; +}; + export type BaseAdderConfig = { metadata: AdderConfigMetadata; options: Args; @@ -54,12 +62,14 @@ export type BaseAdderConfig = { export type InlineAdderConfig = BaseAdderConfig & { integrationType: 'inline'; packages: PackageDefinition[]; + scripts?: Scripts[]; files: FileTypes[]; nextSteps?: (data: { options: OptionValues; cwd: string; colors: Colors; docs: string | undefined; + packageManager: PackageManager; }) => string[]; installHook?: (workspace: Workspace) => Promise; uninstallHook?: (workspace: Workspace) => Promise; diff --git a/packages/core/adder/execute.ts b/packages/core/adder/execute.ts index 36935329..acdefc47 100644 --- a/packages/core/adder/execute.ts +++ b/packages/core/adder/execute.ts @@ -26,9 +26,15 @@ import type { AdderConfigMetadata, ExternalAdderConfig, InlineAdderConfig, + Scripts, } from './config.js'; import type { RemoteControlOptions } from './remoteControl.js'; -import { suggestInstallingDependencies } from '../utils/dependencies.js'; +import { + getPackageManager, + installDependencies, + suggestInstallingDependencies, + type PackageManager, +} from '../utils/dependencies.js'; import { validatePreconditions } from './preconditions.js'; import { endPrompts, startPrompts } from '../utils/prompts.js'; import { checkPostconditions, printUnmetPostconditions } from './postconditions.js'; @@ -57,6 +63,7 @@ export type AddersExecutionPlan = { commonCliOptions: AvailableCliOptionValues; cliOptionsByAdderId: Record>; workingDirectory: string; + packageManager: PackageManager; selectAddersToApply?: AddersToApplySelector; }; @@ -95,12 +102,14 @@ export async function executeAdders( workingDirectory = await detectSvelteDirectory(workingDirectory); const createProject = workingDirectory == null; if (!workingDirectory) workingDirectory = process.cwd(); + const packageManager = getPackageManager(); const executionPlan: AddersExecutionPlan = { workingDirectory, createProject, commonCliOptions, cliOptionsByAdderId, + packageManager, selectAddersToApply, }; @@ -139,7 +148,11 @@ async function executePlan( } const workspace = createEmptyWorkspace(); - await populateWorkspaceDetails(workspace, executionPlan.workingDirectory); + await populateWorkspaceDetails( + workspace, + executionPlan.workingDirectory, + executionPlan.packageManager, + ); const projectType: ProjectType = workspace.kit.installed ? 'kit' : 'svelte'; // select appropriate adders @@ -217,7 +230,11 @@ async function executePlan( const adderId = config.metadata.id; const adderWorkspace = createEmptyWorkspace(); - await populateWorkspaceDetails(adderWorkspace, executionPlan.workingDirectory); + await populateWorkspaceDetails( + adderWorkspace, + executionPlan.workingDirectory, + executionPlan.packageManager, + ); if (executionPlan.cliOptionsByAdderId) { for (const [key, value] of Object.entries(executionPlan.cliOptionsByAdderId[adderId])) { addPropertyToWorkspaceOption(adderWorkspace, key, value); @@ -251,11 +268,18 @@ async function executePlan( } // reload workspace as adders might have changed i.e. dependencies - await populateWorkspaceDetails(workspace, executionPlan.workingDirectory); + await populateWorkspaceDetails( + workspace, + executionPlan.workingDirectory, + executionPlan.packageManager, + ); let installStatus; if (!remoteControlled && !executionPlan.commonCliOptions.skipInstall) - installStatus = await suggestInstallingDependencies(executionPlan.workingDirectory); + installStatus = await suggestInstallingDependencies( + executionPlan.packageManager, + executionPlan.workingDirectory, + ); if (installStatus === 'installed' && workspace.prettier.installed) { const formatSpinner = spinner(); @@ -281,6 +305,7 @@ async function processInlineAdder( isInstall: boolean, ) { const pkgPath = await installPackages(config, workspace); + if (config.scripts) await runScripts(config.scripts, workspace); const updatedOrCreatedFiles = await createOrUpdateFiles(config.files, workspace); await runHooks(config, workspace, isInstall); @@ -371,3 +396,35 @@ async function runHooks( if (isInstall && config.installHook) await config.installHook(workspace); else if (!isInstall && config.uninstallHook) await config.uninstallHook(workspace); } + +async function runScripts( + scripts: Scripts[], + workspace: Workspace, +) { + if (scripts.length < 1) return; + if (!workspace.packageManager) return; + + const requiresPackageInstall = scripts.some((script) => script.type === 'dependency'); + if (requiresPackageInstall) { + await installDependencies(workspace.packageManager, workspace.cwd); + } + + const loadingSpinner = spinner(); + loadingSpinner.start('Running scripts...'); + + for (const script of scripts) { + if (script.condition && !script.condition(workspace)) { + continue; + } + try { + let command: string = workspace.packageManager; + if (command === 'npm') command = 'npx'; + await executeCli(command, script.args, workspace.cwd); + } catch (error) { + const typedError = error as Error; + throw new Error('Failed to execute scripts: ' + typedError.message); + } + } + + loadingSpinner.stop('Successfully executed scripts'); +} diff --git a/packages/core/adder/nextSteps.ts b/packages/core/adder/nextSteps.ts index e6225f96..a48fb65d 100644 --- a/packages/core/adder/nextSteps.ts +++ b/packages/core/adder/nextSteps.ts @@ -28,6 +28,7 @@ export function displayNextSteps( cwd: executionPlan.workingDirectory, colors: pc, docs: x.metadata.website?.documentation, + packageManager: executionPlan.packageManager, }); adderMessage += `- ${adderNextSteps.join('\n- ')}`; return adderMessage; diff --git a/packages/core/utils/dependencies.ts b/packages/core/utils/dependencies.ts index 8ef6a57f..3e37c0bf 100644 --- a/packages/core/utils/dependencies.ts +++ b/packages/core/utils/dependencies.ts @@ -1,21 +1,21 @@ import { selectPrompt } from './prompts'; -import { detect } from 'package-manager-detector'; import { COMMANDS } from 'package-manager-detector/agents'; import { spinner } from '@svelte-add/clack-prompts'; import { executeCli } from './cli.js'; -type PackageManager = (typeof packageManagers)[number] | undefined; +export type PackageManager = (typeof packageManagers)[number] | undefined; const packageManagers = ['npm', 'pnpm', 'yarn', 'bun'] as const; /** + * @param packageManager * @param workingDirectory * @returns the install status of dependencies */ export async function suggestInstallingDependencies( + packageManager: PackageManager, workingDirectory: string, ): Promise<'installed' | 'skipped'> { - const detectedPm = await detect({ cwd: workingDirectory }); - let selectedPm = detectedPm.agent; + let selectedPm = packageManager; selectedPm ??= await selectPrompt( 'Which package manager do you want to install dependencies with?', @@ -35,22 +35,44 @@ export async function suggestInstallingDependencies( return 'skipped'; } - const loadingSpinner = spinner(); - loadingSpinner.start('Installing dependencies...'); + await installDependencies(selectedPm, workingDirectory); - const installCommand = COMMANDS[selectedPm].install; - const [pm, install] = installCommand.split(' '); - await installDependencies(pm, [install], workingDirectory); - - loadingSpinner.stop('Successfully installed dependencies'); return 'installed'; } -async function installDependencies(command: string, args: string[], workingDirectory: string) { +export async function installDependencies( + packageManager: PackageManager, + workingDirectory: string, +) { + if (!packageManager) throw new Error('unable to install dependencies: No package manager found'); try { - await executeCli(command, args, workingDirectory); + const installCommand = COMMANDS[packageManager].install; + const [pm, install] = installCommand.split(' '); + + const loadingSpinner = spinner(); + loadingSpinner.start('Installing dependencies...'); + + await executeCli(pm, [install], workingDirectory); + + loadingSpinner.stop('Successfully installed dependencies'); } catch (error) { const typedError = error as Error; throw new Error('unable to install dependencies: ' + typedError.message); } } + +/** + * Supports npm, pnpm, Yarn, cnpm, bun and any other package manager that sets the + * npm_config_user_agent env variable. + * Thanks to https://github.com/zkochan/packages/tree/main/which-pm-runs for this code! + */ +export function getPackageManager() { + if (!process.env.npm_config_user_agent) { + return undefined; + } + const user_agent = process.env.npm_config_user_agent; + const pm_spec = user_agent.split(' ')[0]; + const separator_pos = pm_spec.lastIndexOf('/'); + const name = pm_spec.substring(0, separator_pos); + return (name === 'npminstall' ? 'cnpm' : name) as PackageManager; +} diff --git a/packages/core/utils/workspace.ts b/packages/core/utils/workspace.ts index b6b4c690..4aa3eb31 100644 --- a/packages/core/utils/workspace.ts +++ b/packages/core/utils/workspace.ts @@ -5,6 +5,7 @@ import { getJsAstEditor } from '@svelte-add/ast-manipulation'; import type { OptionDefinition, OptionValues, Question } from '../adder/options.js'; import { remoteControl } from '../internal.js'; import path from 'path'; +import type { PackageManager } from './dependencies.js'; export type PrettierData = { installed: boolean; @@ -27,6 +28,7 @@ export type Workspace = { typescript: TypescriptData; kit: SvelteKitData; dependencies: Record; + packageManager: PackageManager; }; export type WorkspaceWithoutExplicitArgs = Workspace>; @@ -46,6 +48,7 @@ export function createEmptyWorkspace(): Workspace routesDirectory: 'src/routes', libDirectory: 'src/lib', }, + packageManager: undefined, } as Workspace; } @@ -73,8 +76,10 @@ export function addPropertyToWorkspaceOption( export async function populateWorkspaceDetails( workspace: WorkspaceWithoutExplicitArgs, workingDirectory: string, + packageManager: PackageManager, ) { workspace.cwd = workingDirectory; + workspace.packageManager = packageManager; const tsConfigFileName = 'tsconfig.json'; const viteConfigFileName = 'vite.config.ts'; diff --git a/packages/testing-library/utils/adder.ts b/packages/testing-library/utils/adder.ts index b38b448a..498ba10e 100644 --- a/packages/testing-library/utils/adder.ts +++ b/packages/testing-library/utils/adder.ts @@ -31,6 +31,6 @@ export async function runAdder( workspace.cwd = workingDirectory; workspace.options = optionValues; - await populateWorkspaceDetails(workspace, workingDirectory); + await populateWorkspaceDetails(workspace, workingDirectory, 'pnpm'); await createOrUpdateFiles(adder.tests?.files ?? [], workspace); } From d117d1bb579ca6a78b87266b2d958a634eb3830c Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Thu, 5 Sep 2024 23:39:31 +1000 Subject: [PATCH 19/26] multiselect auth option: basic, magic link, oauth. better nextSteps --- adders/supabase/config/adder.ts | 378 +++++++++++++++++++++++------- adders/supabase/config/options.ts | 40 +++- packages/core/adder/options.ts | 26 +- 3 files changed, 343 insertions(+), 101 deletions(-) diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index aeee2fea..2443c243 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -18,7 +18,12 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge integrationType: 'inline', packages: [ { name: '@supabase/supabase-js', version: '^2.45.3', dev: false }, - { name: '@supabase/ssr', version: '^0.5.1', dev: false }, + { + name: '@supabase/ssr', + version: '^0.5.1', + dev: false, + condition: ({ options }) => options.auth.length > 0, + }, // Local development CLI { name: 'supabase', @@ -49,15 +54,18 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge { name: ({ typescript }) => `./src/hooks.server.${typescript.installed ? 'ts' : 'js'}`, contentType: 'text', + condition: ({ options }) => options.auth.length > 0, content: ({ options, typescript }) => { + const isTs = typescript.installed; + return dedent` import { createServerClient } from '@supabase/ssr' - import {${typescript.installed ? ' type Handle,' : ''} redirect } from '@sveltejs/kit' + import {${isTs ? ' type Handle,' : ''} redirect } from '@sveltejs/kit' import { sequence } from '@sveltejs/kit/hooks' import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' - const supabase${typescript.installed ? ': Handle' : ''} = async ({ event, resolve }) => { + const supabase${isTs ? ': Handle' : ''} = async ({ event, resolve }) => { event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { cookies: { getAll: () => event.cookies.getAll(), @@ -95,7 +103,7 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge }) } - const authGuard${typescript.installed ? ': Handle' : ''} = async ({ event, resolve }) => { + const authGuard${isTs ? ': Handle' : ''} = async ({ event, resolve }) => { const { session, user } = await event.locals.safeGetSession() event.locals.session = session event.locals.user = user @@ -117,23 +125,25 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge return resolve(event) } - export const handle${typescript.installed ? ': Handle' : ''} = sequence(supabase, authGuard) + export const handle${isTs ? ': Handle' : ''} = sequence(supabase, authGuard) `; }, }, { name: () => './src/app.d.ts', contentType: 'text', - condition: ({ typescript }) => typescript.installed, + condition: ({ options, typescript }) => typescript.installed && options.auth.length > 0, content: ({ options }) => { + const { cli, helpers } = options; + return dedent` import type { Session, SupabaseClient, User } from '@supabase/supabase-js' - ${options.cli && options.helpers ? `import type { Database } from '$lib/supabase-types'\n` : ''} + ${cli && helpers ? `import type { Database } from '$lib/supabase-types'\n` : ''} declare global { namespace App { // interface Error {} interface Locals { - supabase: SupabaseClient${options.cli && options.helpers ? `` : ''} + supabase: SupabaseClient${cli && helpers ? `` : ''} safeGetSession: () => Promise<{ session: Session | null; user: User | null }> session: Session | null user: User | null @@ -154,12 +164,15 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge name: ({ kit, typescript }) => `${kit.routesDirectory}/+layout.${typescript.installed ? 'ts' : 'js'}`, contentType: 'text', + condition: ({ options }) => options.auth.length > 0, content: ({ typescript }) => { + const isTs = typescript.installed; + return dedent` import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr' import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' - ${typescript.installed ? `import type { LayoutLoad } from './$types'\n` : ''} - export const load${typescript.installed ? ': LayoutLoad' : ''} = async ({ data, depends, fetch }) => { + ${isTs ? `import type { LayoutLoad } from './$types'\n` : ''} + export const load${isTs ? ': LayoutLoad' : ''} = async ({ data, depends, fetch }) => { depends('supabase:auth') const supabase = isBrowser() @@ -192,10 +205,13 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge name: ({ kit, typescript }) => `${kit.routesDirectory}/+layout.server.${typescript.installed ? 'ts' : 'js'}`, contentType: 'text', + condition: ({ options }) => options.auth.length > 0, content: ({ typescript }) => { + const isTs = typescript.installed; + return dedent` - ${typescript.installed ? `import type { LayoutServerLoad } from './$types'\n` : ''} - export const load${typescript.installed ? ': LayoutServerLoad' : ''} = async ({ locals: { session }, cookies }) => { + ${isTs ? `import type { LayoutServerLoad } from './$types'\n` : ''} + export const load${isTs ? ': LayoutServerLoad' : ''} = async ({ locals: { session }, cookies }) => { return { session, cookies: cookies.getAll(), @@ -207,6 +223,7 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge { name: ({ kit }) => `${kit.routesDirectory}/+layout.svelte`, contentType: 'text', + condition: ({ options }) => options.auth.length > 0, content: ({ typescript }) => { return dedent` @@ -235,35 +252,49 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge name: ({ kit, typescript }) => `${kit.routesDirectory}/auth/+page.server.${typescript.installed ? 'ts' : 'js'}`, contentType: 'text', + condition: ({ options }) => + options.auth.includes('basic') || options.auth.includes('magicLink'), content: ({ options, typescript }) => { + const isTs = typescript.installed; + const { auth, demo: isDemo } = options; + + const isBasic = auth.includes('basic'); + const isMagicLink = auth.includes('magicLink'); + return dedent` - import { redirect } from '@sveltejs/kit' - import { PUBLIC_BASE_URL } from '$env/static/public' - ${typescript.installed ? `import type { Actions } from './$types'\n` : ''} - export const actions${typescript.installed ? `: Actions` : ''} = { + ${ + isBasic + ? ` + import { redirect } from '@sveltejs/kit' + import { PUBLIC_BASE_URL } from '$env/static/public'` + : '' + } + ${isTs ? `import type { Actions } from './$types'\n` : ''} + export const actions${isTs ? `: Actions` : ''} = {${ + isBasic + ? ` signup: async ({ request, locals: { supabase } }) => { const formData = await request.formData() - const email = formData.get('email')${typescript.installed ? ' as string' : ''} - const password = formData.get('password')${typescript.installed ? ' as string' : ''} + const email = formData.get('email')${isTs ? ' as string' : ''} + const password = formData.get('password')${isTs ? ' as string' : ''} - const { error } = await supabase.auth.signUp(${ - options.demo + const { error } = await supabase.auth.signUp({${ + isDemo ? ` - { - email, - password, - options: { - emailRedirectTo: \`\${PUBLIC_BASE_URL}/private\`, - } - }` - : '{ email, password }' - }) + email, + password, + options: { + emailRedirectTo: \`\${PUBLIC_BASE_URL}/private\`, + } + })` + : ' email, password })' + } if (error) { console.error(error) return { message: 'Something went wrong, please try again.' } } else { ${ - options.demo + isDemo ? `// Redirect to local Inbucket for demo purposes redirect(303, \`http://localhost:54324/m/\${email}\`)` : `return { message: 'Sign up succeeded! Please check your email inbox.' }` @@ -272,8 +303,8 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge }, login: async ({ request, locals: { supabase } }) => { const formData = await request.formData() - const email = formData.get('email')${typescript.installed ? ' as string' : ''} - const password = formData.get('password')${typescript.installed ? ' as string' : ''} + const email = formData.get('email')${isTs ? ' as string' : ''} + const password = formData.get('password')${isTs ? ' as string' : ''} const { error } = await supabase.auth.signInWithPassword({ email, password }) if (error) { @@ -282,7 +313,25 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge } else { redirect(303, '/private') } - }, + },` + : '' + }${ + isMagicLink + ? ` + magic: async ({ request, locals: { supabase } }) => { + const formData = await request.formData() + const email = formData.get('email')${isTs ? ' as string' : ''} + + const { error } = await supabase.auth.signInWithOtp({ email }) + if (error) { + console.error(error) + return { message: 'Something went wrong, please try again.' } + } else { + return { message: 'Check your email inbox.' } + } + },` + : '' + } } `; }, @@ -290,30 +339,73 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge { name: ({ kit }) => `${kit.routesDirectory}/auth/+page.svelte`, contentType: 'text', - content: ({ typescript }) => { + condition: ({ options }) => options.auth.length > 0, + content: ({ options, typescript }) => { + const isTs = typescript.installed; + const { auth } = options; + + const isBasic = auth.includes('basic'); + const isMagicLink = auth.includes('magicLink'); + const isOAuth = auth.includes('oauth'); + return dedent` - -
+ ${ + isBasic || isMagicLink + ? ` + + ` + : '' + } + ${ + isBasic + ? ` - - -
- + + ` + : '' + } + ${isMagicLink ? '' : ''} + ${isBasic || isMagicLink ? `` : ''} + ${isOAuth ? `` : ''} + ${ + isBasic || isMagicLink + ? ` {#if form?.message}

{form.message}

- {/if} + {/if}` + : '' + } `; }, }, @@ -321,19 +413,23 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge name: ({ kit, typescript }) => `${kit.routesDirectory}/auth/confirm/+server.${typescript.installed ? 'ts' : 'js'}`, contentType: 'text', + condition: ({ options }) => + options.auth.includes('basic') || options.auth.includes('magicLink'), content: ({ typescript }) => { + const isTs = typescript.installed; + return dedent` import { error, redirect } from '@sveltejs/kit' import { PUBLIC_BASE_URL } from '$env/static/public' ${ - typescript.installed + isTs ? dedent`import type { EmailOtpType } from '@supabase/supabase-js' import type { RequestHandler } from './$types'\n` : '' } - export const GET${typescript.installed ? ': RequestHandler' : ''} = async ({ url, locals: { supabase } }) => { + export const GET${isTs ? ': RequestHandler' : ''} = async ({ url, locals: { supabase } }) => { const token_hash = url.searchParams.get('token_hash') - const type = url.searchParams.get('type')${typescript.installed ? ' as EmailOtpType | null' : ''} + const type = url.searchParams.get('type')${isTs ? ' as EmailOtpType | null' : ''} const next = url.searchParams.get('next') ?? \`\${PUBLIC_BASE_URL}/\` const redirectTo = new URL(next) @@ -352,18 +448,51 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge `; }, }, + { + name: ({ kit, typescript }) => + `${kit.routesDirectory}/auth/callback/+server.${typescript.installed ? 'ts' : 'js'}`, + contentType: 'text', + condition: ({ options }) => options.auth.includes('oauth'), + content: ({ typescript }) => { + const isTs = typescript.installed; + + return dedent` + import { error, redirect } from '@sveltejs/kit' + ${isTs ? `import type { RequestHandler } from './$types'` : ''} + + export const GET${isTs ? ': RequestHandler' : ''} = async ({ url, locals: { supabase } }) => { + const code = url.searchParams.get('code')${isTs ? ' as string' : ''} + const next = url.searchParams.get('next') ?? '/' + + if (code) { + const { error: authError } = await supabase.auth.exchangeCodeForSession(code) + if (authError) { + console.error(authError) + error(500, 'Something went wrong, please try again.') + } + } + + throw redirect(303, \`/\${next.slice(1)}\`) + } + `; + }, + }, { name: ({ kit, typescript }) => `${kit.libDirectory}/server/supabase-admin.${typescript.installed ? 'ts' : 'js'}`, contentType: 'text', + condition: ({ options }) => options.admin, content: ({ options, typescript }) => { + const isTs = typescript.installed; + const { cli: isCli, helpers: isHelpers } = options; + return dedent` import { PUBLIC_SUPABASE_URL } from '$env/static/public' import { SUPABASE_SERVICE_ROLE_KEY } from '$env/static/private' - ${typescript && options.cli && options.helpers ? `import type { Database } from '$lib/supabase-types'\n` : ''} + ${isTs && isCli && isHelpers ? `import type { Database } from '$lib/supabase-types'` : ''} import { createClient } from '@supabase/supabase-js' - export const supabaseAdmin = createClient${typescript && options.cli && options.helpers ? '' : ''}( + export const supabaseAdmin = createClient${isTs && isCli && isHelpers ? '' : ''}( PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { @@ -375,11 +504,11 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge ); `; }, - condition: ({ options }) => options.admin, }, { name: () => `package.json`, contentType: 'json', + condition: ({ options }) => options.helpers, content: ({ data, typescript }) => { data.scripts ??= {}; const scripts: Record = data.scripts; @@ -391,33 +520,46 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge 'supabase gen types typescript --local > src/lib/supabase-types.ts'; } }, - condition: ({ options }) => options.helpers, }, { name: () => './supabase/config.toml', contentType: 'text', - content: ({ content }) => { + condition: ({ options }) => options.cli, + content: ({ content, options }) => { content = content.replace('"http://127.0.0.1:3000"', '"http://localhost:5173"'); content = content.replace('"https://127.0.0.1:3000"', '"https://localhost:5173/*"'); - content = content.replace('enable_confirmations = false', 'enable_confirmations = true'); - - content = appendContent( - content, - dedent` - \n# Custom email confirmation template - [auth.email.template.confirmation] - subject = "Confirm Your Signup" - content_path = "./supabase/templates/confirmation.html" - `, - ); + + if (options.auth.includes('basic')) { + content = content.replace('enable_confirmations = false', 'enable_confirmations = true'); + content = appendContent( + content, + dedent` + \n# Custom email confirmation template + [auth.email.template.confirmation] + subject = "Confirm Your Signup" + content_path = "./supabase/templates/confirmation.html" + `, + ); + } + if (options.auth.includes('magicLink')) { + content = appendContent( + content, + dedent` + \n# Custom magic link template + [auth.email.template.magic_link] + subject = "Your Magic Link" + content_path = "./supabase/templates/magic_link.html" + `, + ); + } return content; }, - condition: ({ options }) => options.cli, }, { name: () => './supabase/templates/confirmation.html', contentType: 'text', + condition: ({ options }) => options.cli && options.auth.includes('basic'), content: () => { return dedent` @@ -432,12 +574,30 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge `; }, - condition: ({ options }) => options.cli, }, - // Demo routes + { + name: () => './supabase/templates/magic_link.html', + contentType: 'text', + condition: ({ options }) => options.cli && options.auth.includes('magicLink'), + content: () => { + return dedent` + + +

Follow this link to login:

+

Log in

+ + + `; + }, + }, + // Demo routes when user has selected Basic Auth { name: ({ kit }) => `${kit.routesDirectory}/+page.svelte`, contentType: 'text', + condition: ({ options }) => options.demo, content: ({ typescript }) => { return dedent` @@ -454,12 +614,12 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge `; }, - condition: ({ options }) => options.demo, }, { name: ({ kit, typescript }) => `${kit.routesDirectory}/private/+layout.server.${typescript.installed ? 'ts' : 'js'}`, contentType: 'text', + condition: ({ options }) => options.demo, content: () => { return dedent` /** @@ -469,11 +629,11 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge **/ `; }, - condition: ({ options }) => options.demo, }, { name: ({ kit }) => `${kit.routesDirectory}/private/+layout.svelte`, contentType: 'text', + condition: ({ options }) => options.demo, content: () => { return dedent` + +
+ + +
+ + {#if form?.message} +

{form.message}

+ {/if} + `; + }, + }, + { + name: ({ kit, typescript }) => + `${kit.routesDirectory}/auth/forgot-password/+page.server.${typescript.installed ? 'ts' : 'js'}`, + contentType: 'text', + condition: ({ options }) => options.auth.includes('basic'), + content: ({ typescript }) => { + const isTs = typescript.installed; + + return dedent` + import { PUBLIC_BASE_URL } from '$env/static/public' + ${isTs ? `import type { Actions } from './$types'` : ''} + + export const actions${isTs ? `: Actions` : ''} = { + default: async ({ request, locals: { supabase } }) => { + const formData = await request.formData() + const email = formData.get('email')${isTs ? ' as string' : ''} + + const { error } = await supabase.auth.resetPasswordForEmail( + email, + { redirectTo: \`\${PUBLIC_BASE_URL}/auth/reset-password\` } + ) + if (error) { + console.error(error) + return { message: 'Something went wrong, please try again.' } + } else { + return { message: 'Please check your email inbox.' } + } + }, + } + `; + }, + }, + { + name: ({ kit, typescript }) => + `${kit.routesDirectory}/auth/reset-password/+page.server.${typescript.installed ? 'ts' : 'js'}`, + contentType: 'text', + condition: ({ options }) => options.auth.includes('basic'), + content: ({ typescript }) => { + const isTs = typescript.installed; + + return dedent` + ${isTs ? `import type { Actions } from './$types'` : ''} + + export const actions${isTs ? `: Actions` : ''} = { + default: async ({ request, locals: { supabase } }) => { + const formData = await request.formData() + const password = formData.get('password')${isTs ? ' as string' : ''} + + const { error } = await supabase.auth.updateUser({ password }) + if (error) { + console.error(error) + return { message: 'Something went wrong, please try again.' } + } else { + return { message: 'Password has been reset' } + } + }, + } + `; + }, + }, + { + name: ({ kit }) => `${kit.routesDirectory}/auth/reset-password/+page.svelte`, + contentType: 'text', + condition: ({ options }) => options.auth.includes('basic'), + content: ({ typescript }) => { + const isTs = typescript.installed; + + return dedent` + + import { enhance } from '$app/forms' + + ${isTs ? `import type { ActionData } from './$types'` : ''} + + export let form${isTs ? ': ActionData' : ''} + + +
+ + +
+ + {#if form?.message} +

{form.message}

+ {/if} + `; + }, + }, + // Basic auth and/or magic link { name: ({ kit, typescript }) => `${kit.routesDirectory}/auth/confirm/+server.${typescript.installed ? 'ts' : 'js'}`, @@ -449,6 +573,7 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge `; }, }, + // OAuth only { name: ({ kit, typescript }) => `${kit.routesDirectory}/auth/callback/+server.${typescript.installed ? 'ts' : 'js'}`, @@ -478,6 +603,7 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge `; }, }, + // Admin client helper { name: ({ kit, typescript }) => `${kit.libDirectory}/server/supabase-admin.${typescript.installed ? 'ts' : 'js'}`, @@ -506,6 +632,7 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge `; }, }, + // Helper scripts { name: () => `package.json`, contentType: 'json', @@ -522,6 +649,7 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge } }, }, + // CLI local development configuration { name: () => './supabase/config.toml', contentType: 'text', @@ -539,6 +667,11 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge [auth.email.template.confirmation] subject = "Confirm Your Signup" content_path = "./supabase/templates/confirmation.html" + + # Custom password reset request template + [auth.email.template.recovery] + subject = "Reset Your Password" + content_path = "./supabase/templates/recovery.html" `, ); } @@ -594,7 +727,26 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge `; }, }, - // Demo routes when user has selected Basic Auth + { + name: () => './supabase/templates/recovery.html', + contentType: 'text', + condition: ({ options }) => options.cli && options.auth.includes('basic'), + content: () => { + return dedent` + + +

Reset Password

+

Follow this link to reset your password:

+

Reset Password

+ + + `; + }, + }, + // Demo routes when user has selected Basic Auth and/or Magic Link { name: ({ kit }) => `${kit.routesDirectory}/+page.svelte`, contentType: 'text', @@ -610,6 +762,9 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge
  • Login
  • Protected page
  • + {#if $page.data.user} + Logout + {/if}
     						User: {JSON.stringify($page.data.user, null, 2)}
     					
    @@ -653,7 +808,7 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge - + Logout
    From a3bf9e78238126587eed36d68418acc2f5534148 Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Sat, 7 Sep 2024 14:18:14 +1000 Subject: [PATCH 24/26] add local google oauth. move oauth server-side --- adders/supabase/config/adder.ts | 133 +++++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 35 deletions(-) diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index b0124c1b..9b06722e 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -262,6 +262,7 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge const isBasic = auth.includes('basic'); const isMagicLink = auth.includes('magicLink'); + const isOAuth = auth.includes('oauth'); return dedent` ${ @@ -271,7 +272,9 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge import { PUBLIC_BASE_URL } from '$env/static/public'` : '' } - ${isTs ? `import type { Actions } from './$types'\n` : ''} + ${isTs ? `import type { Actions } from './$types'` : ''} + ${isTs && isOAuth ? `import type { Provider } from '@supabase/supabase-js'` : ''} + export const actions${isTs ? `: Actions` : ''} = {${ isBasic ? ` @@ -312,9 +315,9 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge if (error) { console.error(error) return { message: 'Something went wrong, please try again.' } - } else { - redirect(303, '/private') } + + redirect(303, '${isDemo ? '/private' : '/'}') },` : '' }${ @@ -328,9 +331,32 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge if (error) { console.error(error) return { message: 'Something went wrong, please try again.' } - } else { - return { message: 'Check your email inbox.' } } + + return { message: 'Check your email inbox.' } + + },` + : '' + } + ${ + isOAuth + ? ` + oauth: async ({ request, locals: { supabase } }) => { + const formData = await request.formData() + const provider = formData.get('provider')${isTs ? ' as Provider' : ''} + + const { data, error } = await supabase.auth.signInWithOAuth({ + provider, + options: { + redirectTo: \`\${PUBLIC_BASE_URL}/auth/callback\`, + } + }) + if (error) { + console.error(error) + return { message: 'Something went wrong, please try again.' } + } + + redirect(303, data.url) },` : '' } @@ -360,26 +386,15 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge : '' } ${(isBasic || isMagicLink) && isTs ? `import type { ActionData } from './$types'` : ''} + ${isBasic || isMagicLink ? `export let form${isTs ? ': ActionData' : ''}` : ''} - ${ - isOAuth - ? ` - async function googleOAuth() { - await $page.data.supabase.auth.signInWithOAuth({ - provider: 'google', - options: { - redirectTo: \`\${PUBLIC_BASE_URL}/auth/callback\`, - }, - }) - }` - : '' - } + ${isOAuth ? `let provider = ''` : ''} +
    ${ isBasic || isMagicLink ? ` - + Forgot password? + ` : '' } ${isMagicLink ? '' : ''} - ${isBasic || isMagicLink ? `
    ` : ''} - ${isBasic ? `Forgot password?` : ''} - ${isOAuth ? `` : ''} ${ - isBasic || isMagicLink + isOAuth ? ` - {#if form?.message} -

    {form.message}

    - {/if}` + + + ` : '' } + + + + {#if form?.message} +

    {form.message}

    + {/if} `; }, }, @@ -655,10 +675,14 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge contentType: 'text', condition: ({ options }) => options.cli, content: ({ content, options }) => { + const isBasic = options.auth.includes('basic'); + const isMagicLink = options.auth.includes('magicLink'); + const isOAuth = options.auth.includes('oauth'); + content = content.replace('"http://127.0.0.1:3000"', '"http://localhost:5173"'); content = content.replace('"https://127.0.0.1:3000"', '"https://localhost:5173/*"'); - if (options.auth.includes('basic')) { + if (isBasic) { content = content.replace('enable_confirmations = false', 'enable_confirmations = true'); content = appendContent( content, @@ -675,7 +699,7 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge `, ); } - if (options.auth.includes('magicLink')) { + if (isMagicLink) { content = appendContent( content, dedent` @@ -686,6 +710,20 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge `, ); } + if (isOAuth) { + content = appendContent( + content, + dedent` + \n# Local Google auth configuration + [auth.external.google] + enabled = true + client_id = "env(SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID)" + secret = "env(SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET)" + redirect_uri = "http://127.0.0.1:54321/auth/v1/callback" + skip_nonce_check = true + `, + ); + } return content; }, @@ -755,6 +793,13 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge return dedent` import { page } from "$app/stores"; + + $: logout = async () => { + const { error } = await $page.data.supabase.auth.signOut(); + if (error) { + console.error(error); + } + };

    Welcome to SvelteKit with Supabase

    @@ -959,7 +1004,7 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge } if (isBasic || isMagicLink) { - steps.push(`Remember to update your hosted project's email templates`); + steps.push(`Update your hosted project's email templates`); if (isCli) { steps.push(`Local email templates are located in ${colors.green(`./supabase/templates`)}`); @@ -968,13 +1013,15 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge if (isOAuth) { steps.push(dedent` - OAuth added 'Login with Google' by default. Update ${colors.green(`src/routes/auth/+page.server.js/ts`)} with chosen provider(s)`); + ${colors.bold(`OAuth:`)} Refer to the docs for other OAuth providers: https://supabase.com/docs/guides/auth/social-login`); steps.push(dedent` - There are additional steps required for each OAuth provider: https://supabase.com/docs/guides/auth/social-login`); + ${colors.bold(`OAuth:`)} Enable Google in your hosted project dashboard and populate with your application's Google OAuth credentials. Create them via: https://console.cloud.google.com/apis/credentials/consent`); if (isCli) { steps.push(dedent` - Update ${colors.green(`./supabase/config.toml`)} with your chosen OAuth provider(s) details. Look for ${colors.blue(`[auth.external.apple]`)} as an example`); + ${colors.bold(`OAuth (Local Dev):`)} Add your application's Google OAuth credentials to ${colors.green(`.env`)}. Create them via: https://console.cloud.google.com/apis/credentials/consent`); + steps.push(dedent` + ${colors.bold(`OAuth (Local Dev):`)} To enable other local providers (or disable Google) update ${colors.green(`./supabase/config.toml`)} and restart the local Supabase services`); } } @@ -983,18 +1030,21 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge }); function generateEnvFileContent({ content, options }: TextFileEditorArgs) { + const isCli = options.cli; + const isOAuth = options.auth.includes('oauth'); + content = addEnvVar(content, 'PUBLIC_BASE_URL', '"http://localhost:5173"'); content = addEnvVar( content, 'PUBLIC_SUPABASE_URL', // Local development env always has the same credentials, prepopulate the local dev env file - options.cli ? '"http://127.0.0.1:54321"' : '""', + isCli ? '"http://127.0.0.1:54321"' : '""', ); content = addEnvVar( content, 'PUBLIC_SUPABASE_ANON_KEY', // Local development env always has the same credentials, prepopulate the local dev env file - options.cli + isCli ? '"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"' : '""', ); @@ -1004,12 +1054,25 @@ function generateEnvFileContent({ content, options }: TextFileEditorArgs"', ) : content; + if (isOAuth) { + content = addEnvVar( + content, + 'SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID', + '" Date: Thu, 12 Sep 2024 10:11:48 +1000 Subject: [PATCH 25/26] add changeset --- .changeset/four-cheetahs-argue.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/four-cheetahs-argue.md diff --git a/.changeset/four-cheetahs-argue.md b/.changeset/four-cheetahs-argue.md new file mode 100644 index 00000000..05d00735 --- /dev/null +++ b/.changeset/four-cheetahs-argue.md @@ -0,0 +1,10 @@ +--- +'@svelte-add/testing-library': minor +'@svelte-add/config': minor +'@svelte-add/core': minor +'@svelte-add/adders': minor +--- + +feat: auto detect package manager +feat: add support for executing scripts (e.g., npx, pnpm dlx, etc) within an adder +feat: add support for multiselect options within an adder From bcf7b1be4f539d44150f64d0f28fa1cb8d9ebd44 Mon Sep 17 00:00:00 2001 From: Matthew Stibbard Date: Thu, 12 Sep 2024 13:09:06 +1000 Subject: [PATCH 26/26] fix unused imports --- adders/supabase/config/adder.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/adders/supabase/config/adder.ts b/adders/supabase/config/adder.ts index 9b06722e..553a25f9 100644 --- a/adders/supabase/config/adder.ts +++ b/adders/supabase/config/adder.ts @@ -62,7 +62,9 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge return dedent` import { createServerClient } from '@supabase/ssr' - import {${isTs ? ' type Handle,' : ''} redirect } from '@sveltejs/kit' + ${!isTs && isDemo ? `import { redirect } from '@sveltejs/kit'` : ''} + ${isTs && isDemo ? `import { type Handle, redirect } from '@sveltejs/kit'` : ''} + ${isTs && !isDemo ? `import type { Handle } from '@sveltejs/kit'` : ''} import { sequence } from '@sveltejs/kit/hooks' import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' @@ -265,13 +267,8 @@ Start your project with a Postgres database, Authentication, instant APIs, Edge const isOAuth = auth.includes('oauth'); return dedent` - ${ - isBasic - ? ` - import { redirect } from '@sveltejs/kit' - import { PUBLIC_BASE_URL } from '$env/static/public'` - : '' - } + ${isBasic || isOAuth ? `import { redirect } from '@sveltejs/kit'` : ''} + ${isDemo || isOAuth ? `import { PUBLIC_BASE_URL } from '$env/static/public'` : ''} ${isTs ? `import type { Actions } from './$types'` : ''} ${isTs && isOAuth ? `import type { Provider } from '@supabase/supabase-js'` : ''}