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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ tsconfig.tsbuildinfo

apps/tests/**/test-results
.solid-start

.image
8 changes: 7 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,11 @@
},
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": true,
"editor.defaultFormatter": "oxc.oxc-vscode"
"editor.defaultFormatter": "oxc.oxc-vscode",
"[typescriptreact]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
}
}
Binary file added apps/tests/src/images/example.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions apps/tests/src/routes/image-local.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { StartImage as Image } from "@solidjs/start/image";
import { type JSX, onMount, Show } from "solid-js";
import exampleImage from "../images/example.jpg?image";

interface PlaceholderProps {
show: () => void;
}

function Placeholder(props: PlaceholderProps): JSX.Element {
onMount(() => {
props.show();
});

return <div>Loading...</div>;
}

export default function App(): JSX.Element {
return (
<div style={{ width: "50vw" }}>
<Image
{...exampleImage}
alt="example"
fallback={(visible, show) => (
<Show when={visible()}>
<Placeholder show={show} />
</Show>
)}
/>
</div>
);
}
35 changes: 35 additions & 0 deletions apps/tests/src/routes/image-remote.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { StartImage as Image } from "@solidjs/start/image";
import { type JSX, onMount, Show } from "solid-js";
// local
// import exampleImage from './example.jpg?image';

// remote
import exampleImage from "image:foobar";

interface PlaceholderProps {
show: () => void;
}

function Placeholder(props: PlaceholderProps): JSX.Element {
onMount(() => {
props.show();
});

return <div>Loading...</div>;
}

export default function App(): JSX.Element {
return (
<div style={{ width: "50vw" }}>
<Image
{...exampleImage}
alt="example"
fallback={(visible, show) => (
<Show when={visible()}>
<Placeholder show={show} />
</Show>
)}
/>
</div>
);
}
48 changes: 46 additions & 2 deletions apps/tests/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,54 @@
import { defineConfig } from "vite";
import { solidStart } from "../../packages/start/src/config";
import { nitroV2Plugin } from "../../packages/start-nitro-v2-vite-plugin/src";
import { solidStart } from "../../packages/start/src/config";

export default defineConfig({
server: {
port: 3000,
},
plugins: [solidStart(), nitroV2Plugin()],
plugins: [
solidStart({
image: {
local: {
sizes: [480, 600],
quality: 80,
publicPath: "public",
},
remote: {
transformURL(url) {
return {
src: {
source: `https://picsum.photos/seed/${url}/1200/900.webp`,
width: 1080,
height: 760,
},
variants: [
{
path: `https://picsum.photos/seed/${url}/800/600.jpg`,
width: 800,
type: "image/jpeg",
},
{
path: `https://picsum.photos/seed/${url}/400/300.jpg`,
width: 400,
type: "image/jpeg",
},
{
path: `https://picsum.photos/seed/${url}/800/600.png`,
width: 800,
type: "image/png",
},
{
path: `https://picsum.photos/seed/${url}/400/300.png`,
width: 400,
type: "image/png",
},
],
};
},
},
},
}),
nitroV2Plugin(),
],
});
16 changes: 16 additions & 0 deletions packages/start/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,19 @@ declare namespace App {
[key: string | symbol]: any;
}
}

declare module 'image:*' {
import type { StartImageProps } from "./src/image.ts";

const props: Pick<StartImageProps<unknown>, 'src' | 'transformer'>;

export default props;
}

declare module '*?image' {
import type { StartImageProps } from "./src/image.ts";

const props: Pick<StartImageProps<unknown>, 'src' | 'transformer'>;

export default props;
}
7 changes: 5 additions & 2 deletions packages/start/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"./client/spa": "./src/client/spa/index.tsx",
"./middleware": "./src/middleware/index.ts",
"./http": "./src/http/index.ts",
"./env": "./env.d.ts"
"./env": "./env.d.ts",
"./image": "./src/image/index.tsx"
},
"publishConfig": {
"access": "public",
Expand All @@ -33,7 +34,8 @@
"./client/spa": "./dist/client/spa/index.jsx",
"./middleware": "./dist/middleware/index.js",
"./http": "./dist/http/index.js",
"./env": "./env.d.ts"
"./env": "./env.d.ts",
"./image": "./dist/image/index.jsx"
}
},
"dependencies": {
Expand All @@ -58,6 +60,7 @@
"radix3": "^1.1.2",
"seroval": "^1.4.1",
"seroval-plugins": "^1.4.0",
"sharp": "^0.34.5",
"shiki": "^1.26.1",
"solid-js": "^1.9.9",
"source-map-js": "^1.2.1",
Expand Down
11 changes: 7 additions & 4 deletions packages/start/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { extname, isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";
import { normalizePath, type PluginOption } from "vite";
import solid, { type Options as SolidOptions } from "vite-plugin-solid";

import { imagePlugin, type StartImageOptions } from "../image/plugin/index.ts";
import { DEFAULT_EXTENSIONS, VIRTUAL_MODULES, VITE_ENVIRONMENTS } from "./constants.ts";
import { devServer } from "./dev-server.ts";
import { SolidStartClientFileRouter, SolidStartServerFileRouter } from "./fs-router.ts";
Expand All @@ -21,6 +21,8 @@ export interface SolidStartOptions {
routeDir?: string;
extensions?: string[];
middleware?: string;

image?: StartImageOptions;
}

const absolute = (path: string, root: string) =>
Expand Down Expand Up @@ -176,7 +178,7 @@ export function solidStart(options?: SolidStartOptions): Array<PluginOption> {
envName: VITE_ENVIRONMENTS.client,
getRuntimeCode: () =>
`import { createServerReference } from "${normalizePath(
fileURLToPath(new URL("../server/server-runtime", import.meta.url))
fileURLToPath(new URL("../server/server-runtime", import.meta.url)),
)}"`,
replacer: opts => `createServerReference('${opts.functionId}')`,
},
Expand All @@ -185,7 +187,7 @@ export function solidStart(options?: SolidStartOptions): Array<PluginOption> {
envName: VITE_ENVIRONMENTS.server,
getRuntimeCode: () =>
`import { createServerReference } from '${normalizePath(
fileURLToPath(new URL("../server/server-fns-runtime", import.meta.url))
fileURLToPath(new URL("../server/server-fns-runtime", import.meta.url)),
)}'`,
replacer: opts => `createServerReference(${opts.fn}, '${opts.functionId}')`,
},
Expand All @@ -194,11 +196,12 @@ export function solidStart(options?: SolidStartOptions): Array<PluginOption> {
envName: VITE_ENVIRONMENTS.server,
getRuntimeCode: () =>
`import { createServerReference } from '${normalizePath(
fileURLToPath(new URL("../server/server-fns-runtime", import.meta.url))
fileURLToPath(new URL("../server/server-fns-runtime", import.meta.url)),
)}'`,
replacer: opts => `createServerReference(${opts.fn}, '${opts.functionId}')`,
},
}),
options?.image ? imagePlugin(options.image) : undefined,
{
name: "solid-start:virtual-modules",
async resolveId(id) {
Expand Down
86 changes: 86 additions & 0 deletions packages/start/src/image/aspect-ratio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
function gcd(a: number, b: number): number {
if (b === 0) {
return a;
}
return gcd(b, a % b);
}

export interface AspectRatio {
width: number;
height: number;
}

const HORIZONTAL_ASPECT_RATIO = [
{ width: 4, height: 4 }, // Square
{ width: 4, height: 3 }, // Standard Fullscreen
{ width: 16, height: 10 }, // Standard LCD
{ width: 16, height: 9 }, // HD
// { width: 37, height: 20 }, // Widescreen
{ width: 6, height: 3 }, // Univisium
{ width: 21, height: 9 }, // Anamorphic 2.35:1
// { width: 64, height: 27 }, // Anamorphic 2.39:1 or 2.37:1
{ width: 19, height: 16 }, // Movietone
{ width: 5, height: 4 }, // 17' LCD CRT
// { width: 48, height: 35 }, // 16mm and 35mm
{ width: 11, height: 8 }, // 35mm full sound
// { width: 143, height: 100 }, // IMAX
{ width: 6, height: 4 }, // 35mm photo
{ width: 14, height: 9 }, // commercials
{ width: 5, height: 3 }, // Paramount
{ width: 7, height: 4 }, // early 35mm
{ width: 11, height: 5 }, // 70mm
{ width: 12, height: 5 }, // Bluray
{ width: 8, height: 3 }, // Super 16
{ width: 18, height: 5 }, // IMAX
{ width: 12, height: 3 }, // Polyvision
];

const VERTICAL_ASPECT_RATIO = HORIZONTAL_ASPECT_RATIO.map(item => ({
width: item.height,
height: item.width,
}));

const ASPECT_RATIO = [...HORIZONTAL_ASPECT_RATIO, ...VERTICAL_ASPECT_RATIO];

export function getAspectRatio({ width, height }: AspectRatio): AspectRatio {
const denom = gcd(width, height);

return {
width: width / denom,
height: height / denom,
};
}

export function getNearestAspectRatio(ratio: AspectRatio): AspectRatio {
let nearest = Number.MAX_VALUE;
let id = 0;

const originalRatio = ratio.width / ratio.height;

for (let i = 0; i < ASPECT_RATIO.length; i += 1) {
const target = ASPECT_RATIO[i]!;

const tRatio = target.width / target.height;
const squared = tRatio - originalRatio;
const distance = Math.sqrt(squared * squared);

if (i === 0) {
nearest = distance;
} else if (distance < nearest) {
id = i;
nearest = distance;
}
}

return ASPECT_RATIO[id]!;
}

export function getScaledComponentRatio(ratio: AspectRatio): AspectRatio {
const xScale = 9 / ratio.width;
const yScale = 9 / ratio.height;
const scale = Math.min(xScale, yScale);
return {
width: scale * ratio.width,
height: scale * ratio.height,
};
}
37 changes: 37 additions & 0 deletions packages/start/src/image/client-only.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { JSX } from "solid-js";
import { createSignal, onMount, Show } from "solid-js";
import { isServer } from "solid-js/web";

export const createClientSignal = isServer
? (): (() => boolean) => () => false
: (): (() => boolean) => {
const [flag, setFlag] = createSignal(false);

onMount(() => {
setFlag(true);
});

return flag;
};

export interface ClientOnlyProps {
fallback?: JSX.Element;
children?: JSX.Element;
}

export const ClientOnly = (props: ClientOnlyProps): JSX.Element => {
const isClient = createClientSignal();

return Show({
keyed: false,
get when() {
return isClient();
},
get fallback() {
return props.fallback;
},
get children() {
return props.children;
},
});
};
Loading
Loading