Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Svelte 5 Runes mode #51

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

<!-- ## Unreleased -->
## Unreleased

**Breaking Changes**

- minimum Svelte version required is 5

**Features**

- support Svelte 5 runes mode
- upgrade `svelte` dependency to 5.9.1
- upgrade `vite` dependency to 5.4.11
- upgrade `@sveltejs/vite-plugin-svelte` to 4.0.1

## [0.9.0](https://github.com/metonym/svelte-time/releases/tag/v0.9.0) - 2024-04-19

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@
},
"devDependencies": {
"@rollup/plugin-commonjs": "^21.1.0",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@sveltejs/vite-plugin-svelte": "^4.0.1",
"@testing-library/svelte": "^5.2.6",
"dlz": "^0.1.3",
"jsdom": "^25.0.1",
"prettier": "^3.4.1",
"prettier-plugin-svelte": "^3.3.2",
"svelte": "^4.2.19",
"svelte": "^5.9.1",
"svelte-readme": "^3.6.3",
"typescript": "^5.7.2",
"vite": "^5.4.11",
"vitest": "^2.1.6"
},
"repository": {
Expand Down
100 changes: 51 additions & 49 deletions src/Time.svelte
Original file line number Diff line number Diff line change
@@ -1,67 +1,69 @@
<script>
// @ts-check

/**
* Original timestamp
* @type {import("dayjs").ConfigType}
*/
export let timestamp = new Date().toISOString();
const {
/**
* Original timestamp
* @type {import("dayjs").ConfigType}
*/
timestamp = new Date().toISOString(),

/**
* Timestamp format for display.
* It's also used as a title in the `relative` mode
* @type {import("dayjs").OptionType}
* @example "YYYY-MM-DD"
*/
export let format = "MMM DD, YYYY";
/**
* Timestamp format for display.
* It's also used as a title in the `relative` mode
* @type {import("dayjs").OptionType}
* @example "YYYY-MM-DD"
*/
format = "MMM DD, YYYY",

/**
* Set to `true` to display the relative time from the provided `timestamp`.
* The value is displayed in a human-readable, relative format (e.g., "4 days ago", "Last week")
* @type {boolean}
*/
export let relative = false;
/**
* Set to `true` to display the relative time from the provided `timestamp`.
* The value is displayed in a human-readable, relative format (e.g., "4 days ago", "Last week")
* @type {boolean}
*/
relative = false,

/**
* Set to `true` to update the relative time at 60 second interval.
* Pass in a number (ms) to specify the interval length
* @type {boolean | number}
*/
export let live = false;

/**
* Formatted timestamp.
* Result of invoking `dayjs().format()`
* @type {string}
*/
export let formatted = "";
/**
* Set to `true` to update the relative time at 60 second interval.
* Pass in a number (ms) to specify the interval length
* @type {boolean | number}
*/
live = false,
...rest
} = $props();

import { dayjs } from "./dayjs";
import { onMount } from "svelte";

/** @type {undefined | NodeJS.Timeout} */
let interval = undefined;

const DEFAULT_INTERVAL = 60 * 1_000;

onMount(() => {
$effect(() => {
/** @type {undefined | NodeJS.Timeout} */
let interval;
if (relative && live !== false) {
interval = setInterval(
() => {
formatted = dayjs(timestamp).from();
},
Math.abs(typeof live === "number" ? live : DEFAULT_INTERVAL),
);
}
return () => clearInterval(interval);
});
Comment on lines +39 to 51
Copy link

@niemyjski niemyjski Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait how does this even work. If this is setting formatted, but formatted is derived. Guessing this one takes over the derived instance.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call. it does work, but i agree, it doesn't makes sense for me to update a derived value. i'm thinking formatted should be a $state instead of a $derived. if that makes sense, let me know and i will update.

but, in this instance, the derived will only be updated if relative, live, or timestamp ever change, which, most likely, they won't change.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either is fine, I think that if we update it the initial state needs to be set in the effect?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i found that in $effect any values that are read asynchronously, for example in a setInterval, will not be tracked.

https://svelte.dev/docs/svelte/$effect#Understanding-dependencies

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, it doesn't look like it is recommended to set the initial state inside of the $effect. my intellij complains with the following:
image

Copy link
Author

@kvangrae kvangrae Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with what i found out above, i'm thinking to either keep it the way it is or change formatted to use $state instead of $derived. let me know your thoughts.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably using state would be good, whatever keeps it simple and easy to understand

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


$: if (relative && live !== false) {
interval = setInterval(
() => {
formatted = dayjs(timestamp).from();
},
Math.abs(typeof live === "number" ? live : DEFAULT_INTERVAL),
);
}
$: formatted = relative
? dayjs(timestamp).from()
: dayjs(timestamp).format(format);
$: title = relative ? dayjs(timestamp).format(format) : undefined;
/**
* Formatted timestamp.
* Result of invoking `dayjs().format()`
* @type {string}
*/
let formatted = $derived(
relative ? dayjs(timestamp).from() : dayjs(timestamp).format(format)
);

const title = $derived(
relative ? dayjs(timestamp).format(format) : undefined,
);
</script>

<time {title} {...$$restProps} datetime={timestamp}>
<time {title} {...rest} datetime={timestamp}>
{formatted}
</time>
39 changes: 31 additions & 8 deletions tests/SvelteTime.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import dayjs from "dayjs";
import { SvelteComponent, tick } from "svelte";
import { flushSync, mount, tick, unmount } from "svelte";
import { dayjs as dayjsExported } from "svelte-time";
import SvelteTime from "./SvelteTime.test.svelte";
import SvelteTimeLive from "./SvelteTimeLive.test.svelte";
import SvelteTimeCustomTitle from "./SvelteTimeCustomTitle.test.svelte";

describe("svelte-time", () => {
let instance: null | SvelteComponent = null;
let instance: null | Record<string, any> = null;

// Use a fixed date for testing to avoid drift.
const FIXED_DATE = new Date("2024-01-01T00:00:00.000Z");
Expand All @@ -18,7 +18,9 @@ describe("svelte-time", () => {

afterEach(() => {
vi.restoreAllMocks();
instance?.$destroy();
if (instance) {
unmount(instance);
}
instance = null;
document.body.innerHTML = "";
});
Expand All @@ -32,7 +34,8 @@ describe("svelte-time", () => {
test("SvelteTime.test.svelte", async () => {
const target = document.body;

instance = new SvelteTime({ target });
instance = mount(SvelteTime, { target });
flushSync();

const date = new Date();
const timestamp = date.toISOString();
Expand Down Expand Up @@ -125,7 +128,8 @@ describe("svelte-time", () => {
test("SvelteTimeLive.test.svelte", async () => {
const target = document.body;

instance = new SvelteTimeLive({ target });
instance = mount(SvelteTimeLive, { target });
flushSync();

const date = new Date();
const timestamp = date.toISOString();
Expand All @@ -140,7 +144,16 @@ describe("svelte-time", () => {
expect(relativeLive.getAttribute("datetime")).toEqual(timestamp);
expect(actionRelativeLive.getAttribute("datetime")).toEqual(timestamp);

vi.runOnlyPendingTimers();
vi.runOnlyPendingTimers(); // 30 seconds
await tick();
expect(relativeLive.title).toEqual(DEFAULT_TIME);
expect(actionRelativeLive.title).toEqual(DEFAULT_TIME);
expect(relativeLive.innerHTML).toEqual("a few seconds ago");
expect(actionRelativeLive.innerText).toEqual("a few seconds ago");
expect(relativeLive.getAttribute("datetime")).toEqual(timestamp);
expect(actionRelativeLive.getAttribute("datetime")).toEqual(timestamp);

vi.runOnlyPendingTimers(); // 60 seconds
await tick();
expect(relativeLive.title).toEqual(DEFAULT_TIME);
expect(actionRelativeLive.title).toEqual(DEFAULT_TIME);
Expand All @@ -149,7 +162,16 @@ describe("svelte-time", () => {
expect(relativeLive.getAttribute("datetime")).toEqual(timestamp);
expect(actionRelativeLive.getAttribute("datetime")).toEqual(timestamp);

vi.runOnlyPendingTimers();
vi.runOnlyPendingTimers(); // 90 seconds
await tick();
expect(relativeLive.title).toEqual(DEFAULT_TIME);
expect(actionRelativeLive.title).toEqual(DEFAULT_TIME);
expect(relativeLive.innerHTML).toEqual("2 minutes ago");
expect(actionRelativeLive.innerText).toEqual("2 minutes ago");
expect(relativeLive.getAttribute("datetime")).toEqual(timestamp);
expect(actionRelativeLive.getAttribute("datetime")).toEqual(timestamp);

vi.runOnlyPendingTimers(); // 120 seconds
await tick();
expect(relativeLive.title).toEqual(DEFAULT_TIME);
expect(actionRelativeLive.title).toEqual(DEFAULT_TIME);
Expand All @@ -162,7 +184,8 @@ describe("svelte-time", () => {
test("SvelteTimeCustomTitle.test.svelte", async () => {
const target = document.body;

instance = new SvelteTimeCustomTitle({ target });
instance = mount(SvelteTimeCustomTitle, { target });
flushSync();

const relativeLive = getElement('[data-test="custom-title"]');
const relativeLiveOmit = getElement('[data-test="custom-title-omit"]');
Expand Down
15 changes: 12 additions & 3 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@ import path from "node:path";
import { defineConfig } from "vite";
import pkg from "./package.json";

export default defineConfig({
plugins: [svelte({ hot: false, preprocess: [vitePreprocess()] })],
export default defineConfig(({ mode }) => ({
plugins: [
svelte({
compilerOptions: {
runes: true,
},
hot: false,
preprocess: [vitePreprocess()],
}),
],
resolve: {
alias: {
[pkg.name]: path.resolve("./src"),
},
conditions: mode === "test" ? ["browser"] : [],
},
test: {
globals: true,
environment: "jsdom",
},
});
}));