@gallop.software/canon 2.12.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/eslint/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ declare const plugin: {
|
|
|
31
31
|
'no-native-intersection-observer': import("eslint").Rule.RuleModule;
|
|
32
32
|
'no-component-in-blocks': import("eslint").Rule.RuleModule;
|
|
33
33
|
'prefer-list-components': import("eslint").Rule.RuleModule;
|
|
34
|
+
'no-native-date': import("eslint").Rule.RuleModule;
|
|
34
35
|
};
|
|
35
36
|
/**
|
|
36
37
|
* Recommended rule configurations - spread into your ESLint config
|
|
@@ -50,6 +51,7 @@ declare const plugin: {
|
|
|
50
51
|
readonly 'gallop/no-native-intersection-observer': "warn";
|
|
51
52
|
readonly 'gallop/no-component-in-blocks': "warn";
|
|
52
53
|
readonly 'gallop/prefer-list-components': "warn";
|
|
54
|
+
readonly 'gallop/no-native-date': "warn";
|
|
53
55
|
};
|
|
54
56
|
};
|
|
55
57
|
export default plugin;
|
package/dist/eslint/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import noDataImports from './rules/no-data-imports.js';
|
|
|
11
11
|
import noNativeIntersectionObserver from './rules/no-native-intersection-observer.js';
|
|
12
12
|
import noComponentInBlocks from './rules/no-component-in-blocks.js';
|
|
13
13
|
import preferListComponents from './rules/prefer-list-components.js';
|
|
14
|
+
import noNativeDate from './rules/no-native-date.js';
|
|
14
15
|
/**
|
|
15
16
|
* All Canon ESLint rules with recommended severity levels
|
|
16
17
|
*/
|
|
@@ -28,6 +29,7 @@ const recommended = {
|
|
|
28
29
|
'gallop/no-native-intersection-observer': 'warn',
|
|
29
30
|
'gallop/no-component-in-blocks': 'warn',
|
|
30
31
|
'gallop/prefer-list-components': 'warn',
|
|
32
|
+
'gallop/no-native-date': 'warn',
|
|
31
33
|
};
|
|
32
34
|
const plugin = {
|
|
33
35
|
meta: {
|
|
@@ -48,6 +50,7 @@ const plugin = {
|
|
|
48
50
|
'no-native-intersection-observer': noNativeIntersectionObserver,
|
|
49
51
|
'no-component-in-blocks': noComponentInBlocks,
|
|
50
52
|
'prefer-list-components': preferListComponents,
|
|
53
|
+
'no-native-date': noNativeDate,
|
|
51
54
|
},
|
|
52
55
|
/**
|
|
53
56
|
* Recommended rule configurations - spread into your ESLint config
|
|
@@ -56,4 +59,4 @@ const plugin = {
|
|
|
56
59
|
recommended,
|
|
57
60
|
};
|
|
58
61
|
export default plugin;
|
|
59
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
62
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvZXNsaW50L2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sY0FBYyxNQUFNLDZCQUE2QixDQUFBO0FBQ3hELE9BQU8sb0JBQW9CLE1BQU0sb0NBQW9DLENBQUE7QUFDckUsT0FBTyxvQkFBb0IsTUFBTSxtQ0FBbUMsQ0FBQTtBQUNwRSxPQUFPLDBCQUEwQixNQUFNLHlDQUF5QyxDQUFBO0FBQ2hGLE9BQU8sc0JBQXNCLE1BQU0scUNBQXFDLENBQUE7QUFDeEUsT0FBTyxzQkFBc0IsTUFBTSxxQ0FBcUMsQ0FBQTtBQUN4RSxPQUFPLGNBQWMsTUFBTSw2QkFBNkIsQ0FBQTtBQUN4RCxPQUFPLGlCQUFpQixNQUFNLGdDQUFnQyxDQUFBO0FBQzlELE9BQU8sa0JBQWtCLE1BQU0sa0NBQWtDLENBQUE7QUFDakUsT0FBTyxhQUFhLE1BQU0sNEJBQTRCLENBQUE7QUFDdEQsT0FBTyw0QkFBNEIsTUFBTSw0Q0FBNEMsQ0FBQTtBQUNyRixPQUFPLG1CQUFtQixNQUFNLG1DQUFtQyxDQUFBO0FBQ25FLE9BQU8sb0JBQW9CLE1BQU0sbUNBQW1DLENBQUE7QUFDcEUsT0FBTyxZQUFZLE1BQU0sMkJBQTJCLENBQUE7QUFFcEQ7O0dBRUc7QUFDSCxNQUFNLFdBQVcsR0FBRztJQUNsQix5QkFBeUIsRUFBRSxNQUFNO0lBQ2pDLGdDQUFnQyxFQUFFLE1BQU07SUFDeEMsK0JBQStCLEVBQUUsTUFBTTtJQUN2QyxxQ0FBcUMsRUFBRSxNQUFNO0lBQzdDLGlDQUFpQyxFQUFFLE1BQU07SUFDekMsaUNBQWlDLEVBQUUsTUFBTTtJQUN6Qyx5QkFBeUIsRUFBRSxNQUFNO0lBQ2pDLDRCQUE0QixFQUFFLE1BQU07SUFDcEMsOEJBQThCLEVBQUUsTUFBTTtJQUN0Qyx3QkFBd0IsRUFBRSxNQUFNO0lBQ2hDLHdDQUF3QyxFQUFFLE1BQU07SUFDaEQsK0JBQStCLEVBQUUsTUFBTTtJQUN2QywrQkFBK0IsRUFBRSxNQUFNO0lBQ3ZDLHVCQUF1QixFQUFFLE1BQU07Q0FDdkIsQ0FBQTtBQUVWLE1BQU0sTUFBTSxHQUFHO0lBQ2IsSUFBSSxFQUFFO1FBQ0osSUFBSSxFQUFFLHNCQUFzQjtRQUM1QixPQUFPLEVBQUUsUUFBUTtLQUNsQjtJQUNELEtBQUssRUFBRTtRQUNMLGtCQUFrQixFQUFFLGNBQWM7UUFDbEMseUJBQXlCLEVBQUUsb0JBQW9CO1FBQy9DLHdCQUF3QixFQUFFLG9CQUFvQjtRQUM5Qyw4QkFBOEIsRUFBRSwwQkFBMEI7UUFDMUQsMEJBQTBCLEVBQUUsc0JBQXNCO1FBQ2xELDBCQUEwQixFQUFFLHNCQUFzQjtRQUNsRCxrQkFBa0IsRUFBRSxjQUFjO1FBQ2xDLHFCQUFxQixFQUFFLGlCQUFpQjtRQUN4Qyx1QkFBdUIsRUFBRSxrQkFBa0I7UUFDM0MsaUJBQWlCLEVBQUUsYUFBYTtRQUNoQyxpQ0FBaUMsRUFBRSw0QkFBNEI7UUFDL0Qsd0JBQXdCLEVBQUUsbUJBQW1CO1FBQzdDLHdCQUF3QixFQUFFLG9CQUFvQjtRQUM5QyxnQkFBZ0IsRUFBRSxZQUFZO0tBQy9CO0lBQ0Q7OztPQUdHO0lBQ0gsV0FBVztDQUNaLENBQUE7QUFFRCxlQUFlLE1BQU0sQ0FBQSIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBub0NsaWVudEJsb2NrcyBmcm9tICcuL3J1bGVzL25vLWNsaWVudC1ibG9ja3MuanMnXG5pbXBvcnQgbm9Db250YWluZXJJblNlY3Rpb24gZnJvbSAnLi9ydWxlcy9uby1jb250YWluZXItaW4tc2VjdGlvbi5qcydcbmltcG9ydCBwcmVmZXJDb21wb25lbnRQcm9wcyBmcm9tICcuL3J1bGVzL3ByZWZlci1jb21wb25lbnQtcHJvcHMuanMnXG5pbXBvcnQgcHJlZmVyVHlwb2dyYXBoeUNvbXBvbmVudHMgZnJvbSAnLi9ydWxlcy9wcmVmZXItdHlwb2dyYXBoeS1jb21wb25lbnRzLmpzJ1xuaW1wb3J0IHByZWZlckxheW91dENvbXBvbmVudHMgZnJvbSAnLi9ydWxlcy9wcmVmZXItbGF5b3V0LWNvbXBvbmVudHMuanMnXG5pbXBvcnQgYmFja2dyb3VuZEltYWdlUm91bmRlZCBmcm9tICcuL3J1bGVzL2JhY2tncm91bmQtaW1hZ2Utcm91bmRlZC5qcydcbmltcG9ydCBub0lubGluZVN0eWxlcyBmcm9tICcuL3J1bGVzL25vLWlubGluZS1zdHlsZXMuanMnXG5pbXBvcnQgbm9BcmJpdHJhcnlDb2xvcnMgZnJvbSAnLi9ydWxlcy9uby1hcmJpdHJhcnktY29sb3JzLmpzJ1xuaW1wb3J0IG5vQ3Jvc3Nab25lSW1wb3J0cyBmcm9tICcuL3J1bGVzL25vLWNyb3NzLXpvbmUtaW1wb3J0cy5qcydcbmltcG9ydCBub0RhdGFJbXBvcnRzIGZyb20gJy4vcnVsZXMvbm8tZGF0YS1pbXBvcnRzLmpzJ1xuaW1wb3J0IG5vTmF0aXZlSW50ZXJzZWN0aW9uT2JzZXJ2ZXIgZnJvbSAnLi9ydWxlcy9uby1uYXRpdmUtaW50ZXJzZWN0aW9uLW9ic2VydmVyLmpzJ1xuaW1wb3J0IG5vQ29tcG9uZW50SW5CbG9ja3MgZnJvbSAnLi9ydWxlcy9uby1jb21wb25lbnQtaW4tYmxvY2tzLmpzJ1xuaW1wb3J0IHByZWZlckxpc3RDb21wb25lbnRzIGZyb20gJy4vcnVsZXMvcHJlZmVyLWxpc3QtY29tcG9uZW50cy5qcydcbmltcG9ydCBub05hdGl2ZURhdGUgZnJvbSAnLi9ydWxlcy9uby1uYXRpdmUtZGF0ZS5qcydcblxuLyoqXG4gKiBBbGwgQ2Fub24gRVNMaW50IHJ1bGVzIHdpdGggcmVjb21tZW5kZWQgc2V2ZXJpdHkgbGV2ZWxzXG4gKi9cbmNvbnN0IHJlY29tbWVuZGVkID0ge1xuICAnZ2FsbG9wL25vLWNsaWVudC1ibG9ja3MnOiAnd2FybicsXG4gICdnYWxsb3Avbm8tY29udGFpbmVyLWluLXNlY3Rpb24nOiAnd2FybicsXG4gICdnYWxsb3AvcHJlZmVyLWNvbXBvbmVudC1wcm9wcyc6ICd3YXJuJyxcbiAgJ2dhbGxvcC9wcmVmZXItdHlwb2dyYXBoeS1jb21wb25lbnRzJzogJ3dhcm4nLFxuICAnZ2FsbG9wL3ByZWZlci1sYXlvdXQtY29tcG9uZW50cyc6ICd3YXJuJyxcbiAgJ2dhbGxvcC9iYWNrZ3JvdW5kLWltYWdlLXJvdW5kZWQnOiAnd2FybicsXG4gICdnYWxsb3Avbm8taW5saW5lLXN0eWxlcyc6ICd3YXJuJyxcbiAgJ2dhbGxvcC9uby1hcmJpdHJhcnktY29sb3JzJzogJ3dhcm4nLFxuICAnZ2FsbG9wL25vLWNyb3NzLXpvbmUtaW1wb3J0cyc6ICd3YXJuJyxcbiAgJ2dhbGxvcC9uby1kYXRhLWltcG9ydHMnOiAnd2FybicsXG4gICdnYWxsb3Avbm8tbmF0aXZlLWludGVyc2VjdGlvbi1vYnNlcnZlcic6ICd3YXJuJyxcbiAgJ2dhbGxvcC9uby1jb21wb25lbnQtaW4tYmxvY2tzJzogJ3dhcm4nLFxuICAnZ2FsbG9wL3ByZWZlci1saXN0LWNvbXBvbmVudHMnOiAnd2FybicsXG4gICdnYWxsb3Avbm8tbmF0aXZlLWRhdGUnOiAnd2FybicsXG59IGFzIGNvbnN0XG5cbmNvbnN0IHBsdWdpbiA9IHtcbiAgbWV0YToge1xuICAgIG5hbWU6ICdlc2xpbnQtcGx1Z2luLWdhbGxvcCcsXG4gICAgdmVyc2lvbjogJzIuMTIuMCcsXG4gIH0sXG4gIHJ1bGVzOiB7XG4gICAgJ25vLWNsaWVudC1ibG9ja3MnOiBub0NsaWVudEJsb2NrcyxcbiAgICAnbm8tY29udGFpbmVyLWluLXNlY3Rpb24nOiBub0NvbnRhaW5lckluU2VjdGlvbixcbiAgICAncHJlZmVyLWNvbXBvbmVudC1wcm9wcyc6IHByZWZlckNvbXBvbmVudFByb3BzLFxuICAgICdwcmVmZXItdHlwb2dyYXBoeS1jb21wb25lbnRzJzogcHJlZmVyVHlwb2dyYXBoeUNvbXBvbmVudHMsXG4gICAgJ3ByZWZlci1sYXlvdXQtY29tcG9uZW50cyc6IHByZWZlckxheW91dENvbXBvbmVudHMsXG4gICAgJ2JhY2tncm91bmQtaW1hZ2Utcm91bmRlZCc6IGJhY2tncm91bmRJbWFnZVJvdW5kZWQsXG4gICAgJ25vLWlubGluZS1zdHlsZXMnOiBub0lubGluZVN0eWxlcyxcbiAgICAnbm8tYXJiaXRyYXJ5LWNvbG9ycyc6IG5vQXJiaXRyYXJ5Q29sb3JzLFxuICAgICduby1jcm9zcy16b25lLWltcG9ydHMnOiBub0Nyb3NzWm9uZUltcG9ydHMsXG4gICAgJ25vLWRhdGEtaW1wb3J0cyc6IG5vRGF0YUltcG9ydHMsXG4gICAgJ25vLW5hdGl2ZS1pbnRlcnNlY3Rpb24tb2JzZXJ2ZXInOiBub05hdGl2ZUludGVyc2VjdGlvbk9ic2VydmVyLFxuICAgICduby1jb21wb25lbnQtaW4tYmxvY2tzJzogbm9Db21wb25lbnRJbkJsb2NrcyxcbiAgICAncHJlZmVyLWxpc3QtY29tcG9uZW50cyc6IHByZWZlckxpc3RDb21wb25lbnRzLFxuICAgICduby1uYXRpdmUtZGF0ZSc6IG5vTmF0aXZlRGF0ZSxcbiAgfSxcbiAgLyoqXG4gICAqIFJlY29tbWVuZGVkIHJ1bGUgY29uZmlndXJhdGlvbnMgLSBzcHJlYWQgaW50byB5b3VyIEVTTGludCBjb25maWdcbiAgICogQGV4YW1wbGUgcnVsZXM6IHsgLi4uZ2FsbG9wLnJlY29tbWVuZGVkIH1cbiAgICovXG4gIHJlY29tbWVuZGVkLFxufVxuXG5leHBvcnQgZGVmYXVsdCBwbHVnaW5cbiJdfQ==
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getCanonUrl, getCanonPattern } from '../utils/canon.js';
|
|
2
|
+
const RULE_NAME = 'no-native-date';
|
|
3
|
+
const pattern = getCanonPattern(RULE_NAME);
|
|
4
|
+
const rule = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'suggestion',
|
|
7
|
+
docs: {
|
|
8
|
+
description: pattern?.summary || 'Use Luxon DateTime, not native JavaScript Date',
|
|
9
|
+
recommended: true,
|
|
10
|
+
url: getCanonUrl(RULE_NAME),
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
noNewDate: `[Canon ${pattern?.id || '027'}] Use Luxon's DateTime instead of new Date(). Native Date operates in the user's local timezone, causing inconsistencies. Import: import { DateTime } from 'luxon'`,
|
|
14
|
+
noDateNow: `[Canon ${pattern?.id || '027'}] Use Luxon's DateTime.now() instead of Date.now(). Import: import { DateTime } from 'luxon'`,
|
|
15
|
+
noDateParse: `[Canon ${pattern?.id || '027'}] Use Luxon's DateTime.fromISO() or DateTime.fromFormat() instead of Date.parse(). Import: import { DateTime } from 'luxon'`,
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
const filename = context.filename || context.getFilename();
|
|
21
|
+
// Only check files in src/ (blocks, components, hooks, etc.)
|
|
22
|
+
if (!filename.includes('/src/')) {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
// Skip _scripts and _data folders
|
|
26
|
+
if (filename.includes('/_scripts/') || filename.includes('/_data/')) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
// Catch: new Date()
|
|
31
|
+
NewExpression(node) {
|
|
32
|
+
if (node.callee?.name === 'Date') {
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
messageId: 'noNewDate',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
// Catch: Date.now() and Date.parse()
|
|
40
|
+
CallExpression(node) {
|
|
41
|
+
if (node.callee?.type === 'MemberExpression' &&
|
|
42
|
+
node.callee?.object?.name === 'Date') {
|
|
43
|
+
const methodName = node.callee?.property?.name;
|
|
44
|
+
if (methodName === 'now') {
|
|
45
|
+
context.report({
|
|
46
|
+
node,
|
|
47
|
+
messageId: 'noDateNow',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
else if (methodName === 'parse') {
|
|
51
|
+
context.report({
|
|
52
|
+
node,
|
|
53
|
+
messageId: 'noDateParse',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
export default rule;
|
|
62
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibm8tbmF0aXZlLWRhdGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvZXNsaW50L3J1bGVzL25vLW5hdGl2ZS1kYXRlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFBRSxXQUFXLEVBQUUsZUFBZSxFQUFFLE1BQU0sbUJBQW1CLENBQUE7QUFFaEUsTUFBTSxTQUFTLEdBQUcsZ0JBQWdCLENBQUE7QUFDbEMsTUFBTSxPQUFPLEdBQUcsZUFBZSxDQUFDLFNBQVMsQ0FBQyxDQUFBO0FBRTFDLE1BQU0sSUFBSSxHQUFvQjtJQUM1QixJQUFJLEVBQUU7UUFDSixJQUFJLEVBQUUsWUFBWTtRQUNsQixJQUFJLEVBQUU7WUFDSixXQUFXLEVBQ1QsT0FBTyxFQUFFLE9BQU8sSUFBSSxnREFBZ0Q7WUFDdEUsV0FBVyxFQUFFLElBQUk7WUFDakIsR0FBRyxFQUFFLFdBQVcsQ0FBQyxTQUFTLENBQUM7U0FDNUI7UUFDRCxRQUFRLEVBQUU7WUFDUixTQUFTLEVBQUUsVUFBVSxPQUFPLEVBQUUsRUFBRSxJQUFJLEtBQUssb0tBQW9LO1lBQzdNLFNBQVMsRUFBRSxVQUFVLE9BQU8sRUFBRSxFQUFFLElBQUksS0FBSyw4RkFBOEY7WUFDdkksV0FBVyxFQUFFLFVBQVUsT0FBTyxFQUFFLEVBQUUsSUFBSSxLQUFLLDZIQUE2SDtTQUN6SztRQUNELE1BQU0sRUFBRSxFQUFFO0tBQ1g7SUFFRCxNQUFNLENBQUMsT0FBTztRQUNaLE1BQU0sUUFBUSxHQUFHLE9BQU8sQ0FBQyxRQUFRLElBQUksT0FBTyxDQUFDLFdBQVcsRUFBRSxDQUFBO1FBRTFELDZEQUE2RDtRQUM3RCxJQUFJLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDO1lBQ2hDLE9BQU8sRUFBRSxDQUFBO1FBQ1gsQ0FBQztRQUVELGtDQUFrQztRQUNsQyxJQUFJLFFBQVEsQ0FBQyxRQUFRLENBQUMsWUFBWSxDQUFDLElBQUksUUFBUSxDQUFDLFFBQVEsQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFDO1lBQ3BFLE9BQU8sRUFBRSxDQUFBO1FBQ1gsQ0FBQztRQUVELE9BQU87WUFDTCxvQkFBb0I7WUFDcEIsYUFBYSxDQUFDLElBQVM7Z0JBQ3JCLElBQUksSUFBSSxDQUFDLE1BQU0sRUFBRSxJQUFJLEtBQUssTUFBTSxFQUFFLENBQUM7b0JBQ2pDLE9BQU8sQ0FBQyxNQUFNLENBQUM7d0JBQ2IsSUFBSTt3QkFDSixTQUFTLEVBQUUsV0FBVztxQkFDdkIsQ0FBQyxDQUFBO2dCQUNKLENBQUM7WUFDSCxDQUFDO1lBRUQscUNBQXFDO1lBQ3JDLGNBQWMsQ0FBQyxJQUFTO2dCQUN0QixJQUNFLElBQUksQ0FBQyxNQUFNLEVBQUUsSUFBSSxLQUFLLGtCQUFrQjtvQkFDeEMsSUFBSSxDQUFDLE1BQU0sRUFBRSxNQUFNLEVBQUUsSUFBSSxLQUFLLE1BQU0sRUFDcEMsQ0FBQztvQkFDRCxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsTUFBTSxFQUFFLFFBQVEsRUFBRSxJQUFJLENBQUE7b0JBRTlDLElBQUksVUFBVSxLQUFLLEtBQUssRUFBRSxDQUFDO3dCQUN6QixPQUFPLENBQUMsTUFBTSxDQUFDOzRCQUNiLElBQUk7NEJBQ0osU0FBUyxFQUFFLFdBQVc7eUJBQ3ZCLENBQUMsQ0FBQTtvQkFDSixDQUFDO3lCQUFNLElBQUksVUFBVSxLQUFLLE9BQU8sRUFBRSxDQUFDO3dCQUNsQyxPQUFPLENBQUMsTUFBTSxDQUFDOzRCQUNiLElBQUk7NEJBQ0osU0FBUyxFQUFFLGFBQWE7eUJBQ3pCLENBQUMsQ0FBQTtvQkFDSixDQUFDO2dCQUNILENBQUM7WUFDSCxDQUFDO1NBQ0YsQ0FBQTtJQUNILENBQUM7Q0FDRixDQUFBO0FBRUQsZUFBZSxJQUFJLENBQUEiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgdHlwZSB7IFJ1bGUgfSBmcm9tICdlc2xpbnQnXG5pbXBvcnQgeyBnZXRDYW5vblVybCwgZ2V0Q2Fub25QYXR0ZXJuIH0gZnJvbSAnLi4vdXRpbHMvY2Fub24uanMnXG5cbmNvbnN0IFJVTEVfTkFNRSA9ICduby1uYXRpdmUtZGF0ZSdcbmNvbnN0IHBhdHRlcm4gPSBnZXRDYW5vblBhdHRlcm4oUlVMRV9OQU1FKVxuXG5jb25zdCBydWxlOiBSdWxlLlJ1bGVNb2R1bGUgPSB7XG4gIG1ldGE6IHtcbiAgICB0eXBlOiAnc3VnZ2VzdGlvbicsXG4gICAgZG9jczoge1xuICAgICAgZGVzY3JpcHRpb246XG4gICAgICAgIHBhdHRlcm4/LnN1bW1hcnkgfHwgJ1VzZSBMdXhvbiBEYXRlVGltZSwgbm90IG5hdGl2ZSBKYXZhU2NyaXB0IERhdGUnLFxuICAgICAgcmVjb21tZW5kZWQ6IHRydWUsXG4gICAgICB1cmw6IGdldENhbm9uVXJsKFJVTEVfTkFNRSksXG4gICAgfSxcbiAgICBtZXNzYWdlczoge1xuICAgICAgbm9OZXdEYXRlOiBgW0Nhbm9uICR7cGF0dGVybj8uaWQgfHwgJzAyNyd9XSBVc2UgTHV4b24ncyBEYXRlVGltZSBpbnN0ZWFkIG9mIG5ldyBEYXRlKCkuIE5hdGl2ZSBEYXRlIG9wZXJhdGVzIGluIHRoZSB1c2VyJ3MgbG9jYWwgdGltZXpvbmUsIGNhdXNpbmcgaW5jb25zaXN0ZW5jaWVzLiBJbXBvcnQ6IGltcG9ydCB7IERhdGVUaW1lIH0gZnJvbSAnbHV4b24nYCxcbiAgICAgIG5vRGF0ZU5vdzogYFtDYW5vbiAke3BhdHRlcm4/LmlkIHx8ICcwMjcnfV0gVXNlIEx1eG9uJ3MgRGF0ZVRpbWUubm93KCkgaW5zdGVhZCBvZiBEYXRlLm5vdygpLiBJbXBvcnQ6IGltcG9ydCB7IERhdGVUaW1lIH0gZnJvbSAnbHV4b24nYCxcbiAgICAgIG5vRGF0ZVBhcnNlOiBgW0Nhbm9uICR7cGF0dGVybj8uaWQgfHwgJzAyNyd9XSBVc2UgTHV4b24ncyBEYXRlVGltZS5mcm9tSVNPKCkgb3IgRGF0ZVRpbWUuZnJvbUZvcm1hdCgpIGluc3RlYWQgb2YgRGF0ZS5wYXJzZSgpLiBJbXBvcnQ6IGltcG9ydCB7IERhdGVUaW1lIH0gZnJvbSAnbHV4b24nYCxcbiAgICB9LFxuICAgIHNjaGVtYTogW10sXG4gIH0sXG5cbiAgY3JlYXRlKGNvbnRleHQpIHtcbiAgICBjb25zdCBmaWxlbmFtZSA9IGNvbnRleHQuZmlsZW5hbWUgfHwgY29udGV4dC5nZXRGaWxlbmFtZSgpXG5cbiAgICAvLyBPbmx5IGNoZWNrIGZpbGVzIGluIHNyYy8gKGJsb2NrcywgY29tcG9uZW50cywgaG9va3MsIGV0Yy4pXG4gICAgaWYgKCFmaWxlbmFtZS5pbmNsdWRlcygnL3NyYy8nKSkge1xuICAgICAgcmV0dXJuIHt9XG4gICAgfVxuXG4gICAgLy8gU2tpcCBfc2NyaXB0cyBhbmQgX2RhdGEgZm9sZGVyc1xuICAgIGlmIChmaWxlbmFtZS5pbmNsdWRlcygnL19zY3JpcHRzLycpIHx8IGZpbGVuYW1lLmluY2x1ZGVzKCcvX2RhdGEvJykpIHtcbiAgICAgIHJldHVybiB7fVxuICAgIH1cblxuICAgIHJldHVybiB7XG4gICAgICAvLyBDYXRjaDogbmV3IERhdGUoKVxuICAgICAgTmV3RXhwcmVzc2lvbihub2RlOiBhbnkpIHtcbiAgICAgICAgaWYgKG5vZGUuY2FsbGVlPy5uYW1lID09PSAnRGF0ZScpIHtcbiAgICAgICAgICBjb250ZXh0LnJlcG9ydCh7XG4gICAgICAgICAgICBub2RlLFxuICAgICAgICAgICAgbWVzc2FnZUlkOiAnbm9OZXdEYXRlJyxcbiAgICAgICAgICB9KVxuICAgICAgICB9XG4gICAgICB9LFxuXG4gICAgICAvLyBDYXRjaDogRGF0ZS5ub3coKSBhbmQgRGF0ZS5wYXJzZSgpXG4gICAgICBDYWxsRXhwcmVzc2lvbihub2RlOiBhbnkpIHtcbiAgICAgICAgaWYgKFxuICAgICAgICAgIG5vZGUuY2FsbGVlPy50eXBlID09PSAnTWVtYmVyRXhwcmVzc2lvbicgJiZcbiAgICAgICAgICBub2RlLmNhbGxlZT8ub2JqZWN0Py5uYW1lID09PSAnRGF0ZSdcbiAgICAgICAgKSB7XG4gICAgICAgICAgY29uc3QgbWV0aG9kTmFtZSA9IG5vZGUuY2FsbGVlPy5wcm9wZXJ0eT8ubmFtZVxuXG4gICAgICAgICAgaWYgKG1ldGhvZE5hbWUgPT09ICdub3cnKSB7XG4gICAgICAgICAgICBjb250ZXh0LnJlcG9ydCh7XG4gICAgICAgICAgICAgIG5vZGUsXG4gICAgICAgICAgICAgIG1lc3NhZ2VJZDogJ25vRGF0ZU5vdycsXG4gICAgICAgICAgICB9KVxuICAgICAgICAgIH0gZWxzZSBpZiAobWV0aG9kTmFtZSA9PT0gJ3BhcnNlJykge1xuICAgICAgICAgICAgY29udGV4dC5yZXBvcnQoe1xuICAgICAgICAgICAgICBub2RlLFxuICAgICAgICAgICAgICBtZXNzYWdlSWQ6ICdub0RhdGVQYXJzZScsXG4gICAgICAgICAgICB9KVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfSxcbiAgICB9XG4gIH0sXG59XG5cbmV4cG9ydCBkZWZhdWx0IHJ1bGVcbiJdfQ==
|
package/package.json
CHANGED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Pattern 027: Luxon for Dates
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Components
|
|
6
|
+
**Enforcement:** ESLint (`gallop/no-native-date`)
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Use Luxon's `DateTime` for all date and time operations. Do not use the native JavaScript `Date` object.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Timezone consistency** — Native `Date` operates in the user's local timezone, causing inconsistencies when users in different timezones interact with the same dates. A user in California viewing a New York restaurant's reservation system would see incorrect "today" calculations.
|
|
15
|
+
2. **Explicit timezone handling** — Luxon's `.setZone()` method allows explicit timezone specification, ensuring dates are always interpreted in the intended timezone (e.g., the business's location).
|
|
16
|
+
3. **Immutable API** — Luxon DateTime objects are immutable, preventing accidental mutation bugs common with native Date.
|
|
17
|
+
4. **Better API** — Luxon provides clearer methods for formatting, parsing, and date arithmetic.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Import
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { DateTime } from 'luxon'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Get Current Time in Specific Timezone
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// Get "now" in New York, regardless of user's location
|
|
31
|
+
const now = DateTime.now().setZone('America/New_York')
|
|
32
|
+
|
|
33
|
+
// Get start of "today" in business timezone
|
|
34
|
+
const today = DateTime.now().setZone('America/Chicago').startOf('day')
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Compare Dates in Timezone
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// Check if a date is in the past (relative to business timezone)
|
|
41
|
+
function isDatePast(year: number, month: number, day: number, timezone: string) {
|
|
42
|
+
const date = DateTime.fromObject(
|
|
43
|
+
{ year, month: month + 1, day },
|
|
44
|
+
{ zone: timezone }
|
|
45
|
+
).startOf('day')
|
|
46
|
+
const today = DateTime.now().setZone(timezone).startOf('day')
|
|
47
|
+
return date < today
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Format Dates
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// Format for display
|
|
55
|
+
const formatted = DateTime.now()
|
|
56
|
+
.setZone('America/New_York')
|
|
57
|
+
.toFormat('MMMM d, yyyy') // "January 19, 2026"
|
|
58
|
+
|
|
59
|
+
// Format for form submission (timezone-agnostic string)
|
|
60
|
+
const formValue = DateTime.fromObject({ year: 2026, month: 1, day: 19 })
|
|
61
|
+
.toFormat('yyyy-MM-dd') // "2026-01-19"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Parse User Input
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// Parse a date string and interpret in business timezone
|
|
68
|
+
const date = DateTime.fromFormat('2026-01-19', 'yyyy-MM-dd', {
|
|
69
|
+
zone: 'America/New_York'
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
### Good
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { DateTime } from 'luxon'
|
|
79
|
+
|
|
80
|
+
// Business timezone from config/props
|
|
81
|
+
const BUSINESS_TIMEZONE = 'America/Chicago'
|
|
82
|
+
|
|
83
|
+
// Get "today" in business timezone
|
|
84
|
+
const today = DateTime.now().setZone(BUSINESS_TIMEZONE).startOf('day')
|
|
85
|
+
|
|
86
|
+
// Check if user-selected date is valid
|
|
87
|
+
const selectedDate = DateTime.fromObject(
|
|
88
|
+
{ year: 2026, month: 1, day: 19 },
|
|
89
|
+
{ zone: BUSINESS_TIMEZONE }
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if (selectedDate < today) {
|
|
93
|
+
console.log('Cannot select a date in the past')
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Bad
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
// Native Date uses user's local timezone
|
|
101
|
+
const today = new Date()
|
|
102
|
+
today.setHours(0, 0, 0, 0) // Still in user's timezone!
|
|
103
|
+
|
|
104
|
+
// This will be wrong for users in different timezones
|
|
105
|
+
const selectedDate = new Date(2026, 0, 19)
|
|
106
|
+
if (selectedDate < today) {
|
|
107
|
+
console.log('This comparison is timezone-dependent!')
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Common Timezone Identifiers
|
|
112
|
+
|
|
113
|
+
| City | IANA Timezone |
|
|
114
|
+
|------|---------------|
|
|
115
|
+
| New York | `America/New_York` |
|
|
116
|
+
| Chicago / Dallas / Houston | `America/Chicago` |
|
|
117
|
+
| Denver | `America/Denver` |
|
|
118
|
+
| Los Angeles / San Francisco | `America/Los_Angeles` |
|
|
119
|
+
| London | `Europe/London` |
|
|
120
|
+
| Paris | `Europe/Paris` |
|
|
121
|
+
| Tokyo | `Asia/Tokyo` |
|
|
122
|
+
|
|
123
|
+
## Enforcement
|
|
124
|
+
|
|
125
|
+
- **Method:** ESLint rule `gallop/no-native-date`
|
|
126
|
+
- **Detects:** `new Date()`, `Date.now()`, `Date.parse()`
|
|
127
|
+
- **Suggests:** Use Luxon's `DateTime.now()`, `DateTime.fromISO()`, or `DateTime.fromFormat()`
|
|
128
|
+
|
|
129
|
+
## References
|
|
130
|
+
|
|
131
|
+
- Luxon Documentation: https://moment.github.io/luxon/
|
|
132
|
+
- IANA Timezone Database: https://www.iana.org/time-zones
|
|
133
|
+
- Form components use `timezone` prop for business location
|
package/schema.json
CHANGED
|
@@ -300,6 +300,16 @@
|
|
|
300
300
|
"enforcement": "eslint",
|
|
301
301
|
"rule": "gallop/prefer-list-components",
|
|
302
302
|
"summary": "Use List/Li, not raw ul/li tags"
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"id": "027",
|
|
306
|
+
"title": "Luxon for Dates",
|
|
307
|
+
"file": "patterns/027-luxon-dates.md",
|
|
308
|
+
"category": "components",
|
|
309
|
+
"status": "stable",
|
|
310
|
+
"enforcement": "eslint",
|
|
311
|
+
"rule": "gallop/no-native-date",
|
|
312
|
+
"summary": "Use Luxon DateTime, not native JavaScript Date"
|
|
303
313
|
}
|
|
304
314
|
],
|
|
305
315
|
"guarantees": [
|