Skip to content
Draft
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
34 changes: 34 additions & 0 deletions eslint/ambient-clock-selectors.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

// `no-restricted-syntax` selectors that flag a direct clock read — `Date.now()`
// or a zero-argument `new Date()` — so card code reads the clock through a
// seam a test can pin.
//
// Anything that renders an elapsed time ("3d ago", a countdown, an age, an
// "expires soon" warning) produces different output depending on when it runs,
// so a visual comparison of it differs between two runs over identical data.
// The usual way to quiet that is to stop comparing the element, which trades
// the regression coverage away. Reading through the seam lets a test pin the
// instant instead, and the value stays visible.
//
// The seam falls back to the real clock when nothing has pinned it, so routing
// a call through it changes nothing outside a test.
//
// `new Date(value)` is untouched: parsing or copying a known instant is not a
// clock read. Only the zero-argument form asks "what time is it now".
const AMBIENT_CLOCK_MESSAGE =
'Read the clock through `helpers/clock` (`now()` / `nowDate()`) instead of calling it directly, so a test can pin the instant and a rendered elapsed time stays comparable between runs.';

const AMBIENT_CLOCK_SELECTORS = [
{
selector:
"CallExpression[callee.type='MemberExpression'][callee.object.name='Date'][callee.property.name='now']",
message: AMBIENT_CLOCK_MESSAGE,
},
{
selector: "NewExpression[callee.name='Date'][arguments.length=0]",
message: AMBIENT_CLOCK_MESSAGE,
},
];

module.exports = { AMBIENT_CLOCK_SELECTORS, AMBIENT_CLOCK_MESSAGE };
3 changes: 3 additions & 0 deletions packages/base/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist
declarations
__boxel
45 changes: 45 additions & 0 deletions packages/base/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict';

const {
AMBIENT_CLOCK_SELECTORS,
} = require('../../eslint/ambient-clock-selectors.cjs');

// Deliberately narrow: this package's ESLint run exists to enforce the clock
// seam and nothing else.
//
// `root: true` with no `extends` means no rule is on unless it is listed here.
// Card source in this package has never been ESLint-linted, so switching on a
// recommended set would surface a backlog unrelated to the clock and make this
// guard wait behind it. Turning more on later is additive; see
// `packages/catalog/.eslintrc.cjs` for what a fuller card-source config looks
// like, including the decorator handling the realm pipeline needs.
module.exports = {
root: true,
// Registered so the rule names in this package's existing `eslint-disable`
// comments resolve, without turning their rules on. Those comments were
// written for a lint run that did not reach here, so they have never done
// anything; naming the plugins keeps them meaningful for whenever their
// rules are switched on, rather than deleting intent that was correct all
// along — the WebGL one in `file-formats/model3d-preview.gts` explains
// itself, and would have to be rediscovered.
plugins: ['@typescript-eslint', '@cardstack/boxel'],
overrides: [
{
files: ['**/*.ts'],
parser: '@typescript-eslint/parser',
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
rules: { 'no-restricted-syntax': ['error', ...AMBIENT_CLOCK_SELECTORS] },
},
{
files: ['**/*.gts'],
parser: 'ember-eslint-parser',
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
rules: { 'no-restricted-syntax': ['error', ...AMBIENT_CLOCK_SELECTORS] },
},
{
// The seam is the one place allowed to read the real clock.
files: ['helpers/clock.ts'],
rules: { 'no-restricted-syntax': 'off' },
},
],
};
3 changes: 2 additions & 1 deletion packages/base/date-range-field.gts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import CalendarStatsIcon from '@cardstack/boxel-icons/calendar-stats';
import { eq, formatDateTime } from '@cardstack/boxel-ui/helpers';
import { formatDateRangeForMarkdown } from './markdown-helpers';
import { BusinessDays } from './components/business-days';
import { nowDate } from './helpers/clock';

interface DateRangeFieldConfiguration {
minDate?: 'today' | Date;
Expand All @@ -28,7 +29,7 @@ function resolveConfiguredDate(
): Date | undefined {
if (!value) return undefined;
if (value === 'today') {
let today = new Date();
let today = nowDate();
today.setHours(0, 0, 0, 0);
return today;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/base/date.gts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { formatDateForMarkdown } from './markdown-helpers';
import { Countdown } from './components/countdown';
import { Timeline } from './components/timeline';
import { Age } from './components/age';
import { nowDate } from './helpers/clock';

// The Intl API is supported in all modern browsers. In older ones, we polyfill
// it in the application route at app startup.
Expand Down Expand Up @@ -217,7 +218,7 @@ export default class DateField extends FieldDef {
if (!date?.length) {
return set(null);
}
let parsed = parse(date, dateFormat, new Date());
let parsed = parse(date, dateFormat, nowDate());
if (!isValid(parsed)) {
return;
}
Expand Down
7 changes: 4 additions & 3 deletions packages/base/date/quarter.gts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { action } from '@ember/object';
import { BoxelSelect } from '@cardstack/boxel-ui/components';
import { not } from '@cardstack/boxel-ui/helpers';
import CalendarStatsIcon from '@cardstack/boxel-icons/calendar-stats';
import { nowDate } from '../helpers/clock';

class QuarterFieldEdit extends Component<typeof QuarterField> {
@tracked quarter = 1;
Expand All @@ -18,7 +19,7 @@ class QuarterFieldEdit extends Component<typeof QuarterField> {
constructor(owner: any, args: any) {
super(owner, args);
this.quarter = this.args.model?.quarter || 1;
this.year = this.args.model?.year || new Date().getFullYear();
this.year = this.args.model?.year || nowDate().getFullYear();
}

get quarterOptions() {
Expand All @@ -37,7 +38,7 @@ class QuarterFieldEdit extends Component<typeof QuarterField> {
}

get years() {
const currentYear = new Date().getFullYear();
const currentYear = nowDate().getFullYear();
return Array.from({ length: 10 }, (_, i) => currentYear - 4 + i);
}

Expand All @@ -57,7 +58,7 @@ class QuarterFieldEdit extends Component<typeof QuarterField> {

@action
updateYear(option: { value: number; label: string } | null) {
this.year = option?.value || new Date().getFullYear();
this.year = option?.value || nowDate().getFullYear();
this.args.model.year = this.year;
}

Expand Down
9 changes: 8 additions & 1 deletion packages/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,25 @@
"@babel/core": "catalog:",
"@cardstack/boxel-icons": "workspace:*",
"@cardstack/boxel-ui": "workspace:*",
"@cardstack/eslint-plugin-boxel": "workspace:*",
"@cardstack/runtime-common": "workspace:*",
"@glimmer/component": "catalog:",
"@glimmer/tracking": "^1.0.4",
"@glint/template": "catalog:",
"@types/flat": "catalog:",
"@types/lodash-es": "catalog:",
"@typescript-eslint/eslint-plugin": "catalog:",
"@typescript-eslint/parser": "catalog:",
"awesome-phonenumber": "catalog:",
"concurrently": "catalog:",
"ember-cli-htmlbars": "^6.3.0",
"ember-concurrency": "catalog:",
"ember-css-url": "^1.0.0",
"ember-eslint-parser": "catalog:",
"ember-modifier": "^3.2.1",
"ember-resources": "catalog:",
"ember-template-lint": "catalog:",
"eslint": "catalog:",
"fflate": "^0.8.2",
"matrix-js-sdk": "catalog:",
"super-fast-md5": "catalog:",
Expand All @@ -37,6 +42,8 @@
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"",
"lint:hbs": "ember-template-lint .",
"lint:hbs:fix": "ember-template-lint . --fix",
"lint:no-isused-option": "node scripts/check-no-isused-option.mjs"
"lint:no-isused-option": "node scripts/check-no-isused-option.mjs",
"lint:js": "eslint . --cache",
"lint:js:fix": "eslint . --fix"
}
}
5 changes: 3 additions & 2 deletions packages/base/time.gts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { eq, not, formatDateTime } from '@cardstack/boxel-ui/helpers';
import { BoxelInput } from '@cardstack/boxel-ui/components';
import ClockIcon from '@cardstack/boxel-icons/clock';
import { TimeSlots } from './components/time-slots';
import { nowDate } from './helpers/clock';

interface TimeConfiguration {
presentation?: 'standard' | 'timeSlots';
Expand Down Expand Up @@ -56,7 +57,7 @@ export default class TimeField extends FieldDef {
const [hours, minutes] = time.split(':').map(Number);
if (isNaN(hours) || isNaN(minutes)) return time;

const today = new Date();
const today = nowDate();
today.setHours(hours, minutes, 0, 0);

const hourCycle = this.config?.hourCycle;
Expand Down Expand Up @@ -105,7 +106,7 @@ export default class TimeField extends FieldDef {
const [hours, minutes] = time.split(':').map(Number);
if (isNaN(hours) || isNaN(minutes)) return time;

const today = new Date();
const today = nowDate();
today.setHours(hours, minutes, 0, 0);

const hourCycle = this.args?.configuration?.hourCycle;
Expand Down
7 changes: 4 additions & 3 deletions packages/base/workspace.gts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import {
import { MarkdownDef } from './markdown-file-def'; // realm README
import type { RealmEventContent } from './matrix-event';
import { Spec } from './spec';
import { now as clockNow, nowDate } from './helpers/clock';

// This file is always loaded through the Boxel loader, which supplies
// `import.meta`. When type-checking, tsc sees the file as CommonJS output and
Expand Down Expand Up @@ -124,7 +125,7 @@ export interface EtaJob {

export function etaMinutes(
job: EtaJob,
now: number = Date.now(),
now: number = clockNow(),
): number | undefined {
let done = job.progressDone ?? 0;
let total = job.progressTotal ?? 0;
Expand Down Expand Up @@ -380,7 +381,7 @@ function toMs(value: unknown): number | undefined {
}

function dayLabelFor(ms: number): string {
let now = new Date();
let now = nowDate();
let startOfToday = new Date(
now.getFullYear(),
now.getMonth(),
Expand All @@ -403,7 +404,7 @@ function relativeTime(value: unknown): string | undefined {
if (ms === undefined) {
return undefined;
}
let diff = Math.max(0, Date.now() - ms);
let diff = Math.max(0, clockNow() - ms);
let minutes = Math.floor(diff / 60000);
if (minutes < 1) {
return 'just now';
Expand Down
2 changes: 2 additions & 0 deletions packages/host/tests/integration/realm-indexing-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -4892,6 +4892,7 @@ module(`Integration | realm indexing`, function (hooks) {
'@cardstack/base/file-formats/file-view-model',
'@cardstack/base/file-formats/image-preview',
'@cardstack/base/file-menu-items',
'@cardstack/base/helpers/clock',
'@cardstack/base/helpers/sanitized-html',
'@cardstack/base/helpers/set-background-image',
'@cardstack/base/links-to-editor',
Expand Down Expand Up @@ -5073,6 +5074,7 @@ module(`Integration | realm indexing`, function (hooks) {
'@cardstack/base/file-formats/file-view-model',
'@cardstack/base/file-formats/image-preview',
'@cardstack/base/file-menu-items',
'@cardstack/base/helpers/clock',
'@cardstack/base/helpers/sanitized-html',
'@cardstack/base/helpers/set-background-image',
'@cardstack/base/links-to-editor',
Expand Down
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading