Skip to content

Commit d538a0f

Browse files
committed
add support for import.meta.env.SSR
1 parent e7671e4 commit d538a0f

File tree

5 files changed

+138
-85
lines changed

5 files changed

+138
-85
lines changed

packages/eslint-plugin-svelte/src/rules/no-top-level-browser-globals.ts

Lines changed: 112 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -26,105 +26,51 @@ export default createRule('no-top-level-browser-globals', {
2626
}
2727
const blowerGlobals = getBrowserGlobals();
2828

29-
const functions: TSESTree.FunctionLike[] = [];
29+
const referenceTracker = new ReferenceTracker(sourceCode.scopeManager.globalScope!, {
30+
// Specifies the global variables that are allowed to prevent `window.window` from being iterated over.
31+
globalObjectNames: ['globalThis']
32+
});
3033

31-
function isTopLevelLocation(node: TSESTree.Node) {
32-
for (const func of functions) {
33-
if (func.range[0] <= node.range[0] && node.range[1] <= func.range[1]) {
34-
return false;
35-
}
36-
}
37-
return true;
38-
}
34+
type MaybeGuard = {
35+
reference?: { node: TSESTree.Node; name: string };
36+
isAvailableLocation: (node: TSESTree.Node) => boolean;
37+
// The guard that checks whether the browser environment is set to true.
38+
browserEnvironment: boolean;
39+
used?: boolean;
40+
};
41+
const maybeGuards: MaybeGuard[] = [];
42+
43+
const functions: TSESTree.FunctionLike[] = [];
3944

4045
function enterFunction(node: TSESTree.FunctionLike) {
4146
if (isTopLevelLocation(node)) {
4247
functions.push(node);
4348
}
4449
}
4550

46-
function verifyGlobalReferences() {
47-
const referenceTracker = new ReferenceTracker(sourceCode.scopeManager.globalScope!, {
48-
// Specifies the global variables that are allowed to prevent `window.window` from being iterated over.
49-
globalObjectNames: ['globalThis']
50-
});
51-
52-
type MaybeGuard = {
53-
reference?: { node: TSESTree.Node; name: string };
54-
isAvailableLocation: (node: TSESTree.Node) => boolean;
55-
// The guard that checks whether the browser environment is set to true.
56-
browserEnvironment: boolean;
57-
used?: boolean;
58-
};
59-
const maybeGuards: MaybeGuard[] = [];
60-
61-
/**
62-
* Checks whether the node is in a location where the expression is available or not.
63-
* @returns `true` if the expression is available.
64-
*/
65-
function isAvailableLocation(ref: { node: TSESTree.Node; name: string }) {
66-
for (const guard of maybeGuards.reverse()) {
67-
if (guard.isAvailableLocation(ref.node)) {
68-
if (guard.browserEnvironment || guard.reference?.name === ref.name) {
69-
guard.used = true;
70-
return true;
71-
}
51+
function enterMetaProperty(node: TSESTree.MetaProperty) {
52+
if (node.meta.name !== 'import' || node.property.name !== 'meta') return;
53+
for (const ref of referenceTracker.iteratePropertyReferences(node, {
54+
env: {
55+
// See https://vite.dev/guide/ssr#conditional-logic
56+
SSR: {
57+
[ReferenceTracker.READ]: true
7258
}
7359
}
74-
return false;
75-
}
76-
77-
/**
78-
* Iterate over the references of modules that can check the browser environment.
79-
*/
80-
function* iterateBrowserCheckerModuleReferences(): Iterable<TSESTree.Expression> {
81-
for (const ref of referenceTracker.iterateEsmReferences({
82-
'esm-env': {
83-
[ReferenceTracker.ESM]: true,
84-
// See https://www.npmjs.com/package/esm-env
85-
BROWSER: {
86-
[ReferenceTracker.READ]: true
87-
}
88-
},
89-
'$app/environment': {
90-
[ReferenceTracker.ESM]: true,
91-
// See https://svelte.dev/docs/kit/$app-environment#browser
92-
browser: {
93-
[ReferenceTracker.READ]: true
94-
}
95-
}
96-
})) {
97-
if (ref.node.type === 'Identifier' || ref.node.type === 'MemberExpression') {
98-
yield ref.node;
99-
} else if (ref.node.type === 'ImportSpecifier') {
100-
const variable = findVariable(context, ref.node.local);
101-
if (variable) {
102-
for (const reference of variable.references) {
103-
if (reference.isRead() && reference.identifier.type === 'Identifier') {
104-
yield reference.identifier;
105-
}
106-
}
107-
}
60+
})) {
61+
if (ref.node.type === 'Identifier' || ref.node.type === 'MemberExpression') {
62+
const guardChecker = getGuardChecker({ node: ref.node, not: true });
63+
if (guardChecker) {
64+
maybeGuards.push({
65+
isAvailableLocation: guardChecker,
66+
browserEnvironment: true
67+
});
10868
}
10969
}
11070
}
71+
}
11172

112-
/**
113-
* Iterate over the used references of global variables.
114-
*/
115-
function* iterateBrowserGlobalReferences(): Iterable<TrackedReferences<unknown>> {
116-
yield* referenceTracker.iterateGlobalReferences(
117-
Object.fromEntries(
118-
blowerGlobals.map((name) => [
119-
name,
120-
{
121-
[ReferenceTracker.READ]: true
122-
}
123-
])
124-
)
125-
);
126-
}
127-
73+
function verifyGlobalReferences() {
12874
// Collects guarded location checkers by checking module references
12975
// that can check the browser environment.
13076
for (const referenceNode of iterateBrowserCheckerModuleReferences()) {
@@ -171,9 +117,90 @@ export default createRule('no-top-level-browser-globals', {
171117

172118
return {
173119
':function': enterFunction,
120+
MetaProperty: enterMetaProperty,
174121
'Program:exit': verifyGlobalReferences
175122
};
176123

124+
/**
125+
* Checks whether the node is in a location where the expression is available or not.
126+
* @returns `true` if the expression is available.
127+
*/
128+
function isAvailableLocation(ref: { node: TSESTree.Node; name: string }) {
129+
for (const guard of maybeGuards.reverse()) {
130+
if (guard.isAvailableLocation(ref.node)) {
131+
if (guard.browserEnvironment || guard.reference?.name === ref.name) {
132+
guard.used = true;
133+
return true;
134+
}
135+
}
136+
}
137+
return false;
138+
}
139+
140+
/**
141+
* Checks whether the node is in a top-level location.
142+
* @returns `true` if the node is in a top-level location.
143+
*/
144+
function isTopLevelLocation(node: TSESTree.Node) {
145+
for (const func of functions) {
146+
if (func.range[0] <= node.range[0] && node.range[1] <= func.range[1]) {
147+
return false;
148+
}
149+
}
150+
return true;
151+
}
152+
153+
/**
154+
* Iterate over the references of modules that can check the browser environment.
155+
*/
156+
function* iterateBrowserCheckerModuleReferences(): Iterable<TSESTree.Expression> {
157+
for (const ref of referenceTracker.iterateEsmReferences({
158+
'esm-env': {
159+
[ReferenceTracker.ESM]: true,
160+
// See https://www.npmjs.com/package/esm-env
161+
BROWSER: {
162+
[ReferenceTracker.READ]: true
163+
}
164+
},
165+
'$app/environment': {
166+
[ReferenceTracker.ESM]: true,
167+
// See https://svelte.dev/docs/kit/$app-environment#browser
168+
browser: {
169+
[ReferenceTracker.READ]: true
170+
}
171+
}
172+
})) {
173+
if (ref.node.type === 'Identifier' || ref.node.type === 'MemberExpression') {
174+
yield ref.node;
175+
} else if (ref.node.type === 'ImportSpecifier') {
176+
const variable = findVariable(context, ref.node.local);
177+
if (variable) {
178+
for (const reference of variable.references) {
179+
if (reference.isRead() && reference.identifier.type === 'Identifier') {
180+
yield reference.identifier;
181+
}
182+
}
183+
}
184+
}
185+
}
186+
}
187+
188+
/**
189+
* Iterate over the used references of global variables.
190+
*/
191+
function* iterateBrowserGlobalReferences(): Iterable<TrackedReferences<unknown>> {
192+
yield* referenceTracker.iterateGlobalReferences(
193+
Object.fromEntries(
194+
blowerGlobals.map((name) => [
195+
name,
196+
{
197+
[ReferenceTracker.READ]: true
198+
}
199+
])
200+
)
201+
);
202+
}
203+
177204
/**
178205
* If the node is a reference used in a guard clause that checks if the node is in a browser environment,
179206
* it returns information about the expression that checks if the browser variable is available.

packages/eslint-plugin-svelte/src/type-defs/@eslint-community/eslint-utils.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ declare module '@eslint-community/eslint-utils' {
9090
public iterateGlobalReferences<T = unknown>(
9191
traceMap: TraceMap<T>
9292
): IterableIterator<TrackedReferences<T>>;
93+
94+
/**
95+
* Iterate the property references for a given expression AST node.
96+
*/
97+
public iteratePropertyReferences<T = unknown>(
98+
node: TSESTree.Expression,
99+
traceMap: TraceMap<T>
100+
): IterableIterator<TrackedReferences<T>>;
93101
}
94102
export function getStaticValue(
95103
node: TSESTree.Node,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Unexpected top-level browser global variable "location".
2+
line: 3
3+
column: 15
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
if (import.meta.env.SSR) {
3+
console.log(location.href); // NG
4+
} else {
5+
console.log(location.href);
6+
}
7+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
if (import.meta.env.SSR) {
3+
// console.log(location.href); // NG
4+
} else {
5+
console.log(location.href);
6+
}
7+
</script>

0 commit comments

Comments
 (0)