Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/lib/FileInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
import type { Snippet } from 'svelte';

type Props = {
placeholder?: boolean;
children: Snippet;
} & Omit<HTMLInputAttributes, 'type' | 'value' | 'placeholder'>;

let { placeholder = false, children, ...rest }: Props = $props();
</script>

<label class="file-input">
<input {...rest} type="file" />
<span class:placeholder>{@render children()}</span>
</label>

<style>
.file-input {
display: flex;
flex: 1;
align-items: center;
margin-bottom: 1.5rem;
border: 1px solid var(--border-strong);
border-radius: 6px;
background: var(--input-bg);
cursor: pointer;
overflow: visible;
transition: border-color 0.15s;
}

.file-input:focus-within {
border-color: var(--accent);
}

input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}

span {
color: var(--text);
font-family: inherit;
font-size: 0.9375rem;
width: 100%;
padding: 0.625rem 0.75rem;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.placeholder {
color: var(--subtle);
}
</style>
27 changes: 27 additions & 0 deletions src/lib/SingleInputSubmitButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
import type { HTMLButtonAttributes } from 'svelte/elements';

type Props = HTMLButtonAttributes;

let { ...rest }: Props = $props();
</script>

<button {...rest} type="submit">→</button>

<style>
button {
background: none;
border: none;
color: var(--accent);
font-family: inherit;
font-size: 1.125rem;
padding: 0.625rem 0.75rem;
cursor: pointer;
flex-shrink: 0;
line-height: 1;
}

button:hover {
color: var(--accent-hover);
}
</style>
78 changes: 78 additions & 0 deletions src/lib/package-json-scan.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { all } from 'module-replacements';
import { expect, test } from 'vitest';

import { eval_package_json } from './package-json-scan';

test('returns an error for invalid JSON', () => {
expect(eval_package_json('{ "dependencies": {')).toEqual({
success: false,
error: 'File was not valid JSON.'
});
});

test('returns an error when the file is not a JSON object', () => {
expect(eval_package_json('[]')).toEqual({
success: false,
error: 'File was an invalid format (not an object).'
});
});

test('returns an error when dependencies are missing', () => {
expect(eval_package_json(JSON.stringify({ name: 'empty-package' }))).toEqual({
success: false,
error: 'No dependencies or devDependencies found.'
});
});

test('returns an error when dependencies are not objects', () => {
expect(
eval_package_json(
JSON.stringify({
dependencies: 'debug'
})
)
).toEqual({
success: false,
error: 'dependencies and devDependencies must be objects.'
});
});

test('finds replacements from dependencies and devDependencies', () => {
const result = eval_package_json(
JSON.stringify({
dependencies: {
debug: '^4.4.0',
svelte: '^5.0.0'
},
devDependencies: {
qs: '^6.14.2',
vitest: '^4.1.3'
}
})
);

expect(result).toEqual({
success: true,
checked: 4,
replacements: expect.arrayContaining([
{ dep: 'debug', replacement: all.mappings.debug },
{ dep: 'qs', replacement: all.mappings.qs }
])
});
});

test('returns an empty replacement list when no replacements are found', () => {
expect(
eval_package_json(
JSON.stringify({
dependencies: {
svelte: '^5.0.0'
}
})
)
).toEqual({
success: true,
checked: 1,
replacements: []
});
});
60 changes: 60 additions & 0 deletions src/lib/package-json-scan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { all } from 'module-replacements';
import type { ModuleReplacementMapping } from 'module-replacements';

function is_record(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

export type PackageJSONScanSuccessResult = {
success: true;
checked: number;
replacements: {
dep: string;
replacement: ModuleReplacementMapping;
}[];
};

export type PackageJsonScanResult =
| { success: false; error: string }
| PackageJSONScanSuccessResult;

export function eval_package_json(package_json_string: string): PackageJsonScanResult {
let parsed_json: unknown;
try {
parsed_json = JSON.parse(package_json_string);
} catch {
return { success: false, error: 'File was not valid JSON.' };
}

if (!is_record(parsed_json)) {
return { success: false, error: 'File was an invalid format (not an object).' };
}

if (!parsed_json['devDependencies'] && !parsed_json['dependencies']) {
return { success: false, error: 'No dependencies or devDependencies found.' };
}

const dev_deps = parsed_json['devDependencies'] ?? {};
const prod_deps = parsed_json['dependencies'] ?? {};

if (!is_record(dev_deps) || !is_record(prod_deps)) {
return { success: false, error: 'dependencies and devDependencies must be objects.' };
}

const replacements = [];
for (const mapping of Object.keys(all.mappings)) {
const match = dev_deps[mapping] ?? prod_deps[mapping];
if (match) {
replacements.push({
dep: mapping,
replacement: all.mappings[mapping]
});
}
}

return {
success: true,
checked: Object.keys(dev_deps).length + Object.keys(prod_deps).length,
replacements
};
}
19 changes: 2 additions & 17 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { all } from 'module-replacements';
import Autocomplete from '$lib/Autocomplete.svelte';
import ReplacementsTitle from '$lib/ReplacementsTitle.svelte';
import SingleInputSubmitButton from '$lib/SingleInputSubmitButton.svelte';
import { scopify } from '$lib/utils';

const examples = ['is-number', 'left-pad', 'is-odd', 'object-assign'];
Expand Down Expand Up @@ -56,7 +57,7 @@
on_select_navigate_to={navigate_to}
autofocus
/>
<button type="submit" class="submit-btn" aria-label="Search">→</button>
<SingleInputSubmitButton aria-label="Search" />
</form>

<div class="examples">
Expand Down Expand Up @@ -134,22 +135,6 @@
border-color: var(--accent);
}

.submit-btn {
background: none;
border: none;
color: var(--accent);
font-family: inherit;
font-size: 1.125rem;
padding: 0.625rem 0.75rem;
cursor: pointer;
flex-shrink: 0;
line-height: 1;
}

.submit-btn:hover {
color: var(--accent-hover);
}

.examples {
margin-top: 2rem;
}
Expand Down
Loading