whereto-bike 0.0.1 → 0.0.2
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/eslint-rules/no-hardcoded-city-locale.js +10 -0
- package/package.json +1 -1
- package/src/content-collections.ts +4 -1
- package/src/content.config.ts +2 -1
- package/src/env.d.ts +3 -0
- package/src/integration.ts +25 -0
- package/src/lib/config.ts +22 -1
|
@@ -6,6 +6,7 @@ export default {
|
|
|
6
6
|
messages: {
|
|
7
7
|
hardcodedCity: "Don't hardcode city name '{{value}}'. Import CITY from src/lib/config.ts.",
|
|
8
8
|
hardcodedLocale: "Don't hardcode locale '{{value}}'. Derive from city config locales.",
|
|
9
|
+
cityFallbackDefault: "Don't use '{{value}}' as a fallback default for CITY. A silent default can cause content to be committed to the wrong directory. Make the value required instead.",
|
|
9
10
|
},
|
|
10
11
|
},
|
|
11
12
|
create(context) {
|
|
@@ -28,7 +29,16 @@ export default {
|
|
|
28
29
|
// Skip type annotations and enums
|
|
29
30
|
if (node.parent.type === 'TSLiteralType') return;
|
|
30
31
|
|
|
32
|
+
// Detect `X || 'ottawa'` or `X ?? 'ottawa'` — a city name used as fallback.
|
|
33
|
+
// This is dangerous because the fallback silently applies when the env var
|
|
34
|
+
// is unset (e.g. in Cloudflare Workers at runtime), causing content to be
|
|
35
|
+
// committed to the wrong directory.
|
|
31
36
|
if (CITY_NAMES.includes(val)) {
|
|
37
|
+
const p = node.parent;
|
|
38
|
+
if (p.type === 'LogicalExpression' && (p.operator === '||' || p.operator === '??') && p.right === node) {
|
|
39
|
+
context.report({ node, messageId: 'cityFallbackDefault', data: { value: node.value } });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
32
42
|
context.report({ node, messageId: 'hardcodedCity', data: { value: node.value } });
|
|
33
43
|
}
|
|
34
44
|
// Only flag exact matches for locale codes (not substrings)
|
package/package.json
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
eventSchema, organizerSchema, pageSchema,
|
|
12
12
|
} from './schemas/index';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// This file is exclusively for blog/club consumer repos. Default to 'blog'
|
|
15
|
+
// since that's the primary consumer — clubs override via CITY env var.
|
|
16
|
+
const city = process.env.CITY || 'blog';
|
|
17
|
+
const CITY_DIR = `${process.env.CONTENT_DIR || '.'}/${city}`;
|
|
15
18
|
const mdPattern = ['**/*.md', '!**/*.??.md'];
|
|
16
19
|
|
|
17
20
|
export const collections = {
|
package/src/content.config.ts
CHANGED
|
@@ -4,12 +4,13 @@ import { routeLoader } from './loaders/routes';
|
|
|
4
4
|
import { rideLoader } from './loaders/rides';
|
|
5
5
|
import { pageLoader } from './loaders/pages';
|
|
6
6
|
import { isBlogInstance } from './lib/city-config';
|
|
7
|
+
import { cityDir } from './lib/config';
|
|
7
8
|
import {
|
|
8
9
|
routeSchema, placeSchema, guideSchema,
|
|
9
10
|
eventSchema, organizerSchema, pageSchema,
|
|
10
11
|
} from './schemas/index';
|
|
11
12
|
|
|
12
|
-
const CITY_DIR =
|
|
13
|
+
const CITY_DIR = cityDir;
|
|
13
14
|
|
|
14
15
|
// Exclude translation files (e.g. bike-crash.fr.md) — only load base language
|
|
15
16
|
const mdPattern = ['**/*.md', '!**/*.??.md'];
|
package/src/env.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ declare global {
|
|
|
20
20
|
/** App repo git branch, baked in at build time via vite.define */
|
|
21
21
|
const __APP_BRANCH__: string;
|
|
22
22
|
|
|
23
|
+
/** City slug, baked in at build time via vite.define */
|
|
24
|
+
const __CITY__: string;
|
|
25
|
+
|
|
23
26
|
// -- D1 (SQL database) types used by drizzle-orm/d1 --
|
|
24
27
|
|
|
25
28
|
interface D1Result<T = unknown> {
|
package/src/integration.ts
CHANGED
|
@@ -77,6 +77,19 @@ function patchStaticCspStyles(rootDir: string) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Auto-detect the city slug by scanning a content directory for subdirectories
|
|
82
|
+
* containing config.yml. If exactly one is found, return it. This lets consumer
|
|
83
|
+
* repos (blog, club) skip setting CITY= when the content layout is unambiguous.
|
|
84
|
+
*/
|
|
85
|
+
function detectCity(contentDir: string): string | null {
|
|
86
|
+
if (!fs.existsSync(contentDir)) return null;
|
|
87
|
+
const candidates = fs.readdirSync(contentDir, { withFileTypes: true })
|
|
88
|
+
.filter(e => e.isDirectory() && fs.existsSync(path.join(contentDir, e.name, 'config.yml')))
|
|
89
|
+
.map(e => e.name);
|
|
90
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
function detectBranch(): string {
|
|
81
94
|
try {
|
|
82
95
|
return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
|
|
@@ -98,6 +111,17 @@ export function wheretoBike(options?: WheretoBikeOptions): AstroIntegration[] {
|
|
|
98
111
|
process.env.CITY = options.city;
|
|
99
112
|
}
|
|
100
113
|
|
|
114
|
+
// Auto-detect CITY for consumer repos: scan the content directory for a single
|
|
115
|
+
// city folder containing config.yml. Blog repos always have blog/config.yml,
|
|
116
|
+
// club repos have their club folder — no need to set CITY explicitly.
|
|
117
|
+
if (!process.env.CITY && options?.consumerRoot) {
|
|
118
|
+
const contentDir = process.env.CONTENT_DIR || options.consumerRoot;
|
|
119
|
+
const detected = detectCity(contentDir);
|
|
120
|
+
if (detected) {
|
|
121
|
+
process.env.CITY = detected;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
101
125
|
const consumerRoot = options?.consumerRoot;
|
|
102
126
|
|
|
103
127
|
// Derive i18n config from city config.yml (same logic as previous astro.config.mjs).
|
|
@@ -129,6 +153,7 @@ export function wheretoBike(options?: WheretoBikeOptions): AstroIntegration[] {
|
|
|
129
153
|
},
|
|
130
154
|
define: {
|
|
131
155
|
__APP_BRANCH__: JSON.stringify(detectBranch()),
|
|
156
|
+
__CITY__: JSON.stringify(CITY),
|
|
132
157
|
},
|
|
133
158
|
plugins: [buildDataPlugin({ consumerRoot })],
|
|
134
159
|
build: {
|
package/src/lib/config.ts
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
|
|
3
3
|
export const CONTENT_DIR = process.env.CONTENT_DIR || path.resolve('..', 'bike-routes');
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* City slug for this instance.
|
|
7
|
+
*
|
|
8
|
+
* At build time: read from process.env.CITY (set by shell or wheretoBike() options).
|
|
9
|
+
* In bundled Worker: inlined as a string literal via Vite define (__CITY__).
|
|
10
|
+
*
|
|
11
|
+
* Never defaults silently — an unset CITY would commit content to the wrong
|
|
12
|
+
* directory in the data repo (the bug that prompted this safeguard).
|
|
13
|
+
*/
|
|
14
|
+
function resolveCity(): string {
|
|
15
|
+
// In the Vite-bundled server, __CITY__ is replaced with a string literal at build time.
|
|
16
|
+
// In Node.js (loaders, tests, scripts), it's undefined and we fall back to process.env.
|
|
17
|
+
if (typeof __CITY__ !== 'undefined') return __CITY__;
|
|
18
|
+
const fromEnv = process.env.CITY;
|
|
19
|
+
if (fromEnv) return fromEnv;
|
|
20
|
+
throw new Error(
|
|
21
|
+
'CITY environment variable is required. Set CITY=ottawa (or blog, demo, etc.) before building or running.',
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const CITY: string = resolveCity();
|
|
5
26
|
export const cityDir = path.join(CONTENT_DIR, CITY);
|