diff --git a/app/components/editor/codemirror/CodeMirrorEditor.tsx b/app/components/editor/codemirror/CodeMirrorEditor.tsx index 8e9f3a3f..e222eabb 100644 --- a/app/components/editor/codemirror/CodeMirrorEditor.tsx +++ b/app/components/editor/codemirror/CodeMirrorEditor.tsx @@ -25,6 +25,7 @@ import { BinaryContent } from './BinaryContent'; import { getTheme, reconfigureTheme } from './cm-theme'; import { indentKeyBinding } from './indent'; import { getLanguage } from './languages'; +import { createEnvMaskingExtension } from './EnvMasking'; const logger = createScopedLogger('CodeMirrorEditor'); @@ -134,6 +135,9 @@ export const CodeMirrorEditor = memo( const [languageCompartment] = useState(new Compartment()); + // Add a compartment for the env masking extension + const [envMaskingCompartment] = useState(new Compartment()); + const containerRef = useRef(null); const viewRef = useRef(); const themeRef = useRef(); @@ -214,6 +218,7 @@ export const CodeMirrorEditor = memo( if (!doc) { const state = newEditorState('', theme, settings, onScrollRef, debounceScroll, onSaveRef, [ languageCompartment.of([]), + envMaskingCompartment.of([]), ]); view.setState(state); @@ -236,6 +241,7 @@ export const CodeMirrorEditor = memo( if (!state) { state = newEditorState(doc.value, theme, settings, onScrollRef, debounceScroll, onSaveRef, [ languageCompartment.of([]), + envMaskingCompartment.of([createEnvMaskingExtension(() => docRef.current?.filePath)]), ]); editorStates.set(doc.filePath, state); diff --git a/app/components/editor/codemirror/EnvMasking.ts b/app/components/editor/codemirror/EnvMasking.ts new file mode 100644 index 00000000..5c9077d4 --- /dev/null +++ b/app/components/editor/codemirror/EnvMasking.ts @@ -0,0 +1,80 @@ +import { EditorView, Decoration, type DecorationSet, ViewPlugin, WidgetType } from '@codemirror/view'; + +// Create a proper WidgetType class for the masked text +class MaskedTextWidget extends WidgetType { + constructor(private readonly _value: string) { + super(); + } + + eq(other: MaskedTextWidget) { + return other._value === this._value; + } + + toDOM() { + const span = document.createElement('span'); + span.textContent = '*'.repeat(this._value.length); + span.className = 'cm-masked-text'; + + return span; + } + + ignoreEvent() { + return false; + } +} + +export function createEnvMaskingExtension(getFilePath: () => string | undefined) { + return ViewPlugin.fromClass( + class { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = this.buildDecorations(view); + } + + update(update: { docChanged: boolean; view: EditorView; viewportChanged: boolean }) { + if (update.docChanged || update.viewportChanged) { + this.decorations = this.buildDecorations(update.view); + } + } + + buildDecorations(view: EditorView) { + const filePath = getFilePath(); + const isEnvFile = filePath?.endsWith('.env') || filePath?.includes('.env.') || filePath?.includes('/.env'); + + if (!isEnvFile) { + return Decoration.none; + } + + const decorations: any[] = []; + const doc = view.state.doc; + + for (let i = 1; i <= doc.lines; i++) { + const line = doc.line(i); + const text = line.text; + + // Match lines with KEY=VALUE format + const match = text.match(/^([^=]+)=(.+)$/); + + if (match && !text.trim().startsWith('#')) { + const [, key, value] = match; + const valueStart = line.from + key.length + 1; + + // Create a decoration that replaces the value with asterisks + decorations.push( + Decoration.replace({ + inclusive: true, + widget: new MaskedTextWidget(value), + }).range(valueStart, line.to), + ); + } + } + + return Decoration.set(decorations); + } + }, + { + decorations: (v) => v.decorations, + }, + ); +} diff --git a/app/routes/api.supabase.variables.ts b/app/routes/api.supabase.variables.ts index 55e796cf..45eccb23 100644 --- a/app/routes/api.supabase.variables.ts +++ b/app/routes/api.supabase.variables.ts @@ -1,4 +1,4 @@ -import { json, type ActionFunctionArgs } from '@remix-run/node'; +import { json, type ActionFunctionArgs } from '@remix-run/cloudflare'; export async function action({ request }: ActionFunctionArgs) { try {