1// Copyright © 2023 650 Industries.
2// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
3
4import URL from 'url-parse';
5
6class DOMException extends Error {
7  constructor(message: string, name: string) {
8    super(message);
9    this.name = name;
10  }
11}
12
13// The differences between the definitions of `Location` and `WorkerLocation`
14// are because of the `LegacyUnforgeable` attribute only specified upon
15// `Location`'s properties. See:
16// - https://html.spec.whatwg.org/multipage/history.html#the-location-interface
17// - https://heycam.github.io/webidl/#LegacyUnforgeable
18class Location {
19  constructor(href: string | null = null) {
20    const url = new URL(
21      // @ts-expect-error
22      href
23    );
24    // @ts-expect-error
25    url.username = '';
26    // @ts-expect-error
27    url.password = '';
28    Object.defineProperties(this, {
29      hash: {
30        get() {
31          return url.hash;
32        },
33        set() {
34          throw new DOMException(`Cannot set "location.hash".`, 'NotSupportedError');
35        },
36        enumerable: true,
37      },
38      host: {
39        get() {
40          return url.host;
41        },
42        set() {
43          throw new DOMException(`Cannot set "location.host".`, 'NotSupportedError');
44        },
45        enumerable: true,
46      },
47      hostname: {
48        get() {
49          return url.hostname;
50        },
51        set() {
52          throw new DOMException(`Cannot set "location.hostname".`, 'NotSupportedError');
53        },
54        enumerable: true,
55      },
56      href: {
57        get() {
58          return url.href;
59        },
60        set() {
61          throw new DOMException(`Cannot set "location.href".`, 'NotSupportedError');
62        },
63        enumerable: true,
64      },
65      origin: {
66        get() {
67          return url.origin;
68        },
69        enumerable: true,
70      },
71      pathname: {
72        get() {
73          return url.pathname;
74        },
75        set() {
76          throw new DOMException(`Cannot set "location.pathname".`, 'NotSupportedError');
77        },
78        enumerable: true,
79      },
80      port: {
81        get() {
82          return url.port;
83        },
84        set() {
85          throw new DOMException(`Cannot set "location.port".`, 'NotSupportedError');
86        },
87        enumerable: true,
88      },
89      protocol: {
90        get() {
91          return url.protocol;
92        },
93        set() {
94          throw new DOMException(`Cannot set "location.protocol".`, 'NotSupportedError');
95        },
96        enumerable: true,
97      },
98      search: {
99        get() {
100          // @ts-expect-error
101          return url.search;
102        },
103        set() {
104          throw new DOMException(`Cannot set "location.search".`, 'NotSupportedError');
105        },
106        enumerable: true,
107      },
108      ancestorOrigins: {
109        get() {
110          return {
111            length: 0,
112            item: () => null,
113            contains: () => false,
114          };
115        },
116        enumerable: true,
117      },
118      assign: {
119        value: function assign() {
120          throw new DOMException(`Cannot call "location.assign()".`, 'NotSupportedError');
121        },
122        enumerable: true,
123      },
124      reload: {
125        value: function reload() {
126          if (process.env.NODE_ENV !== 'production') {
127            // NOTE: This does change how native fast refresh works. The upstream metro-runtime will check
128            // if `location.reload` exists before falling back on an implementation that is nearly identical to
129            // this. The main difference is that on iOS there is a "reason" message sent, but at the time of writing
130            // this, that message is unused (ref: `RCTTriggerReloadCommandNotification`).
131            const DevSettings = (require('react-native') as typeof import('react-native'))
132              .DevSettings;
133            return DevSettings.reload();
134          } else {
135            throw new DOMException(`Cannot call "location.reload()".`, 'NotSupportedError');
136          }
137        },
138        enumerable: true,
139      },
140      replace: {
141        value: function replace() {
142          throw new DOMException(`Cannot call "location.replace()".`, 'NotSupportedError');
143        },
144        enumerable: true,
145      },
146      toString: {
147        value: function toString() {
148          return url.href;
149        },
150        enumerable: true,
151      },
152      [Symbol.for('Expo.privateCustomInspect')]: {
153        value(inspect: any) {
154          const object = {
155            hash: this.hash,
156            host: this.host,
157            hostname: this.hostname,
158            href: this.href,
159            origin: this.origin,
160            pathname: this.pathname,
161            port: this.port,
162            protocol: this.protocol,
163            search: this.search,
164          };
165          return `${this.constructor.name} ${inspect(object)}`;
166        },
167      },
168    });
169  }
170}
171
172Object.defineProperties(Location.prototype, {
173  [Symbol.toString()]: {
174    value: 'Location',
175    configurable: true,
176  },
177});
178
179let location: Location | undefined = undefined;
180
181export function setLocationHref(href: string) {
182  location = new Location(href);
183}
184
185export function install() {
186  Object.defineProperty(global, 'Location', {
187    value: Location,
188    configurable: true,
189    writable: true,
190  });
191
192  Object.defineProperty(window, 'location', {
193    get() {
194      return location;
195    },
196    set() {
197      throw new DOMException(`Cannot set "location".`, 'NotSupportedError');
198    },
199    enumerable: true,
200  });
201}
202