@server/next 0.20.30 → 0.20.32
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/package.json +3 -3
- package/readme.md +43 -0
- package/src/helpers/config.js +68 -0
- package/src/helpers/handleRequest.js +11 -0
- package/src/helpers/index.js +1 -0
- package/src/index.js +81 -139
- package/src/index.test.js +15 -0
- package/src/polyfill.js +9 -3
- /package/src/{bucket.js → helpers/bucket.js} +0 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.20.
|
|
4
|
-
"description": "
|
|
5
|
-
"homepage": "https://
|
|
3
|
+
"version": "0.20.32",
|
|
4
|
+
"description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
|
|
5
|
+
"homepage": "https://server-js.com/",
|
|
6
6
|
"repository": "https://github.com/franciscop/server-next.git",
|
|
7
7
|
"bugs": "https://github.com/franciscop/server-next/issues",
|
|
8
8
|
"funding": "https://www.paypal.me/franciscopresencia/19",
|
package/readme.md
CHANGED
|
@@ -119,3 +119,46 @@ Context docs here
|
|
|
119
119
|
## Reply
|
|
120
120
|
|
|
121
121
|
Reply docs here
|
|
122
|
+
|
|
123
|
+
## Runtimes
|
|
124
|
+
|
|
125
|
+
There are many runtimes where Server works! We put a lot of work to make sure it works the same way with minimal changes in them, this includes:
|
|
126
|
+
|
|
127
|
+
- Node.js of course, including everywhere that Node is supported (VPS, Heroku, Render, etc).
|
|
128
|
+
- Bun, and it will be even faster!
|
|
129
|
+
- Cloudflare Worker
|
|
130
|
+
- Netlify Function + Edge function
|
|
131
|
+
|
|
132
|
+
## FAQ
|
|
133
|
+
|
|
134
|
+
#### How is it different from Hono?
|
|
135
|
+
|
|
136
|
+
Server.js attempts to run your code unmodified in all runtimes. With Hono, despite the claims in their homepage, you need to change the code for different runtimes:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
// Server.js code for Node.js, Bun and Netlify
|
|
140
|
+
import server from "@server/next";
|
|
141
|
+
export default server().get("/", () => "Hello server!");
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
// Hono code for Node.js
|
|
146
|
+
import { serve } from '@hono/node-server'
|
|
147
|
+
import { Hono } from 'hono'
|
|
148
|
+
const app = new Hono()
|
|
149
|
+
app.get('/', (c) => c.text('Hello Node.js!'))
|
|
150
|
+
serve(app)
|
|
151
|
+
|
|
152
|
+
// Hono code for Bun
|
|
153
|
+
import { Hono } from 'hono'
|
|
154
|
+
const app = new Hono()
|
|
155
|
+
app.get('/', (c) => c.text('Hello Bun!'))
|
|
156
|
+
export default app
|
|
157
|
+
|
|
158
|
+
// Hono code for Netlify
|
|
159
|
+
import { Hono } from 'jsr:@hono/hono'
|
|
160
|
+
import { handle } from 'jsr:@hono/hono/netlify'
|
|
161
|
+
const app = new Hono()
|
|
162
|
+
app.get('/', (c) => c.text('Hello Hono!'))
|
|
163
|
+
export default handle(app)
|
|
164
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import Bucket from "./bucket.js";
|
|
2
|
+
import createId from "./createId.js";
|
|
3
|
+
|
|
4
|
+
// Big mess; parse all of the options for server, which can be at launch time
|
|
5
|
+
// or dynamically per-request for the functions (so have to read ENV inside)
|
|
6
|
+
export default function config(options) {
|
|
7
|
+
const env = globalThis.env;
|
|
8
|
+
|
|
9
|
+
// Basic options
|
|
10
|
+
options.port = options.port || env.PORT || 3000;
|
|
11
|
+
options.secret = options.secret || env.SECRET || "unsafe-" + createId();
|
|
12
|
+
|
|
13
|
+
// CORS
|
|
14
|
+
options.cors = options.cors || env.CORS || null;
|
|
15
|
+
if (options.cors === true) {
|
|
16
|
+
options.cors = { origin: options.domain || "*" };
|
|
17
|
+
}
|
|
18
|
+
if (typeof options.cors === "string") {
|
|
19
|
+
options.cors = { origin: options.cors };
|
|
20
|
+
}
|
|
21
|
+
if (options.cors && !options.cors.methods) {
|
|
22
|
+
options.cors.methods = "GET,HEAD,POST,PUT,PATCH";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Bucket
|
|
26
|
+
options.views = options.views ? Bucket(options.views) : null;
|
|
27
|
+
options.public = options.public ? Bucket(options.public) : null;
|
|
28
|
+
options.uploads = options.uploads ? Bucket(options.uploads) : null;
|
|
29
|
+
|
|
30
|
+
// Stores
|
|
31
|
+
options.store = options.store ?? null;
|
|
32
|
+
options.cookies = options.cookies ?? {};
|
|
33
|
+
if (options.store && options.cookies) {
|
|
34
|
+
options.session = { store: options.store.prefix("session:") };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// AUTH
|
|
38
|
+
options.auth = options.auth || env.AUTH || null;
|
|
39
|
+
if (options.auth) {
|
|
40
|
+
if (typeof options.auth !== "object") {
|
|
41
|
+
const [type, provider] = options.auth.split(":");
|
|
42
|
+
options.auth = { type, provider };
|
|
43
|
+
}
|
|
44
|
+
if (typeof options.auth.provider === "string") {
|
|
45
|
+
options.auth.provider === options.auth.provider.split("|");
|
|
46
|
+
}
|
|
47
|
+
if (!options.auth.type) {
|
|
48
|
+
throw new Error("Auth options needs a type");
|
|
49
|
+
}
|
|
50
|
+
if (!options.auth.provider) {
|
|
51
|
+
throw new Error("Auth options needs a provider");
|
|
52
|
+
}
|
|
53
|
+
if (!options.auth.session && options.store) {
|
|
54
|
+
options.auth.session = options.store.prefix("auth:");
|
|
55
|
+
}
|
|
56
|
+
if (!options.auth.store && options.store) {
|
|
57
|
+
options.auth.store = options.store.prefix("user:");
|
|
58
|
+
}
|
|
59
|
+
if (!options.auth.cleanUser) {
|
|
60
|
+
options.auth.cleanUser = (fullUser) => {
|
|
61
|
+
const { password, ...user } = fullUser;
|
|
62
|
+
return user;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return options;
|
|
68
|
+
}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
import middle from "../middle/index.js";
|
|
1
2
|
import parseResponse from "../parseResponse.js";
|
|
2
3
|
import pathPattern from "../pathPattern.js";
|
|
3
4
|
import define from "./define.js";
|
|
4
5
|
import validate from "./validate.js";
|
|
5
6
|
|
|
7
|
+
const extendWithDefaults = (ctx) => {
|
|
8
|
+
// Only want to execute it once; it needs to happen on a per-request
|
|
9
|
+
// basis since we only have full access to the options there
|
|
10
|
+
if (ctx.app.extended) return;
|
|
11
|
+
middle(ctx);
|
|
12
|
+
ctx.app.extended = true;
|
|
13
|
+
};
|
|
14
|
+
|
|
6
15
|
export default async function handleRequest(handlers, ctx) {
|
|
16
|
+
extendWithDefaults(ctx);
|
|
17
|
+
|
|
7
18
|
for (let [method, matcher, ...cbs] of handlers[ctx.method]) {
|
|
8
19
|
const match = pathPattern(matcher, ctx.url.pathname || "/");
|
|
9
20
|
// Skip this whole middleware if there was no match
|
package/src/helpers/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as createCookies } from "./createCookies.js";
|
|
2
2
|
export { default as createId } from "./createId.js";
|
|
3
|
+
export { default as config } from "./config.js";
|
|
3
4
|
export { default as define } from "./define.js";
|
|
4
5
|
export { default as getMachine } from "./getMachine.js";
|
|
5
6
|
export { default as handleRequest } from "./handleRequest.js";
|
package/src/index.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import "./polyfill.js";
|
|
2
|
-
// Define the errors for ServerError
|
|
3
2
|
import "./errors/index.js";
|
|
4
3
|
|
|
5
|
-
import Bucket from "./bucket.js";
|
|
6
4
|
import createNodeContext from "./context/node.js";
|
|
7
5
|
import createWinterContext from "./context/winter.js";
|
|
8
6
|
import {
|
|
9
|
-
|
|
7
|
+
config,
|
|
10
8
|
getMachine,
|
|
11
9
|
handleRequest,
|
|
12
10
|
iterate,
|
|
13
11
|
parseHeaders,
|
|
14
12
|
} from "./helpers/index.js";
|
|
15
|
-
import middle from "./middle/index.js";
|
|
16
13
|
|
|
17
14
|
// Export the reply helpers
|
|
18
15
|
export * from "./reply.js";
|
|
@@ -22,103 +19,20 @@ export { default as ServerError } from "./ServerError.js";
|
|
|
22
19
|
// Allow to create a sub-router
|
|
23
20
|
export { default as router } from "./router.js";
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
const http = await import("http");
|
|
27
|
-
http
|
|
28
|
-
.createServer(async (request, response) => {
|
|
29
|
-
try {
|
|
30
|
-
const ctx = await createNodeContext(request, options, app);
|
|
31
|
-
extendWithDefaults(ctx);
|
|
32
|
-
const out = await handleRequest(app.handlers, ctx);
|
|
33
|
-
|
|
34
|
-
response.writeHead(out.status || 200, parseHeaders(out.headers));
|
|
35
|
-
if (out.body instanceof ReadableStream) {
|
|
36
|
-
await iterate(out.body, (chunk) => response.write(chunk));
|
|
37
|
-
} else {
|
|
38
|
-
response.write(out.body || "");
|
|
39
|
-
}
|
|
40
|
-
response.end();
|
|
41
|
-
} catch (error) {
|
|
42
|
-
response.writeHead(error.status || 500);
|
|
43
|
-
response.write(error.message || "");
|
|
44
|
-
response.end();
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
.listen(options.port);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const validateOptions = (options, env = {}) => {
|
|
51
|
-
options.port = options.port || env.PORT || 3000;
|
|
52
|
-
options.secret = options.secret || env.SECRET || "unsafe-" + createId();
|
|
53
|
-
options.cors = options.cors || env.CORS || null;
|
|
54
|
-
if (options.cors === true) {
|
|
55
|
-
options.cors = { origin: options.domain || "*" };
|
|
56
|
-
}
|
|
57
|
-
if (typeof options.cors === "string") {
|
|
58
|
-
options.cors = { origin: options.cors };
|
|
59
|
-
}
|
|
60
|
-
if (options.cors && !options.cors.methods) {
|
|
61
|
-
options.cors.methods = "GET,HEAD,POST,PUT,PATCH";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
options.views = options.views ? Bucket(options.views) : null;
|
|
65
|
-
options.public = options.public ? Bucket(options.public) : null;
|
|
66
|
-
options.uploads = options.uploads ? Bucket(options.uploads) : null;
|
|
67
|
-
|
|
68
|
-
options.store = options.store ?? null;
|
|
69
|
-
options.cookies = options.cookies ?? {};
|
|
70
|
-
if (options.store && options.cookies) {
|
|
71
|
-
options.session = { store: options.store.prefix("session:") };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// AUTH
|
|
75
|
-
options.auth = options.auth || env.AUTH || null;
|
|
76
|
-
if (options.auth) {
|
|
77
|
-
if (typeof options.auth !== "object") {
|
|
78
|
-
const [type, provider] = options.auth.split(":");
|
|
79
|
-
options.auth = { type, provider };
|
|
80
|
-
}
|
|
81
|
-
if (typeof options.auth.provider === "string") {
|
|
82
|
-
options.auth.provider === options.auth.provider.split("|");
|
|
83
|
-
}
|
|
84
|
-
if (!options.auth.type) {
|
|
85
|
-
throw new Error("Auth options needs a type");
|
|
86
|
-
}
|
|
87
|
-
if (!options.auth.provider) {
|
|
88
|
-
throw new Error("Auth options needs a provider");
|
|
89
|
-
}
|
|
90
|
-
if (!options.auth.session && options.store) {
|
|
91
|
-
options.auth.session = options.store.prefix("auth:");
|
|
92
|
-
}
|
|
93
|
-
if (!options.auth.store && options.store) {
|
|
94
|
-
options.auth.store = options.store.prefix("user:");
|
|
95
|
-
}
|
|
96
|
-
if (!options.auth.cleanUser) {
|
|
97
|
-
options.auth.cleanUser = (fullUser) => {
|
|
98
|
-
const { password, ...user } = fullUser;
|
|
99
|
-
return user;
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return options;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const extendWithDefaults = (ctx) => {
|
|
108
|
-
// Only want to execute it once; it needs to happen on a per-request
|
|
109
|
-
// basis since we only have full access to the options there
|
|
110
|
-
if (ctx.app.extended) return;
|
|
111
|
-
middle(ctx);
|
|
112
|
-
ctx.app.extended = true;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// Export the main server()
|
|
22
|
+
// #region server()
|
|
116
23
|
export default function server(options = {}) {
|
|
117
24
|
// Make it so that the exported one is a prototype of function()
|
|
118
25
|
if (!(this instanceof server)) {
|
|
119
26
|
return new server(options).self();
|
|
120
27
|
}
|
|
121
28
|
|
|
29
|
+
// Keep a copy of the options in the instance
|
|
30
|
+
this.opts = options;
|
|
31
|
+
this.platform = getMachine();
|
|
32
|
+
|
|
33
|
+
// TODO: find a way to remove this hack
|
|
34
|
+
this.extended = false;
|
|
35
|
+
|
|
122
36
|
// Skip "forbidden methods" https://fetch.spec.whatwg.org/#concept-method
|
|
123
37
|
this.handlers = {
|
|
124
38
|
socket: [],
|
|
@@ -131,67 +45,93 @@ export default function server(options = {}) {
|
|
|
131
45
|
options: [],
|
|
132
46
|
};
|
|
133
47
|
|
|
134
|
-
this.
|
|
135
|
-
|
|
136
|
-
this.platform = getMachine();
|
|
137
|
-
|
|
138
|
-
// WEBSOCKETS stuff
|
|
139
|
-
const sockets = [];
|
|
48
|
+
this.sockets = [];
|
|
49
|
+
// Note: required by Bun
|
|
140
50
|
this.websocket = {
|
|
141
51
|
message: async (socket, body) => {
|
|
142
52
|
this.handlers.socket
|
|
143
53
|
?.filter((s) => s[0] === "message")
|
|
144
|
-
?.map((s) => s[1]({ socket, sockets, body }));
|
|
54
|
+
?.map((s) => s[1]({ socket, sockets: this.sockets, body }));
|
|
145
55
|
},
|
|
146
|
-
open: (ws) => sockets.push(ws),
|
|
147
|
-
close: (ws) => sockets.splice(sockets.indexOf(ws), 1),
|
|
56
|
+
open: (ws) => this.sockets.push(ws),
|
|
57
|
+
close: (ws) => this.sockets.splice(this.sockets.indexOf(ws), 1),
|
|
148
58
|
};
|
|
149
59
|
|
|
150
|
-
//
|
|
60
|
+
// Initialize it right away for Node.js
|
|
151
61
|
if (this.platform.runtime === "node") {
|
|
152
|
-
|
|
62
|
+
this.node();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
153
65
|
|
|
154
|
-
|
|
66
|
+
server.prototype.self = function () {
|
|
67
|
+
const cb = this.callback.bind(this);
|
|
68
|
+
const proto = Object.getPrototypeOf(this);
|
|
69
|
+
for (let key in { ...proto, ...this }) {
|
|
70
|
+
if (typeof this[key] === "function") {
|
|
71
|
+
cb[key] = this[key].bind(this);
|
|
72
|
+
} else {
|
|
73
|
+
cb[key] = this[key];
|
|
74
|
+
}
|
|
155
75
|
}
|
|
76
|
+
return cb;
|
|
77
|
+
};
|
|
156
78
|
|
|
157
|
-
|
|
158
|
-
|
|
79
|
+
// #region Runtimes
|
|
80
|
+
// Node.js
|
|
81
|
+
server.prototype.node = async function () {
|
|
82
|
+
const http = await import("http");
|
|
83
|
+
http
|
|
84
|
+
.createServer(async (request, response) => {
|
|
85
|
+
try {
|
|
86
|
+
const options = config(this.opts);
|
|
87
|
+
const ctx = await createNodeContext(request, options, this);
|
|
88
|
+
const out = await handleRequest(this.handlers, ctx);
|
|
159
89
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
90
|
+
response.writeHead(out.status || 200, parseHeaders(out.headers));
|
|
91
|
+
if (out.body instanceof ReadableStream) {
|
|
92
|
+
await iterate(out.body, (chunk) => response.write(chunk));
|
|
93
|
+
} else {
|
|
94
|
+
response.write(out.body || "");
|
|
95
|
+
}
|
|
96
|
+
response.end();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
response.writeHead(error.status || 500);
|
|
99
|
+
response.write(error.message || "");
|
|
100
|
+
response.end();
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
.listen(this.opts.port);
|
|
104
|
+
};
|
|
169
105
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const ctx = await createWinterContext(request, options, this);
|
|
176
|
-
extendWithDefaults(ctx);
|
|
177
|
-
return await handleRequest(this.handlers, ctx);
|
|
178
|
-
} catch (error) {
|
|
179
|
-
return new Response(error.message, { status: error.status || 500 });
|
|
106
|
+
// Netlify
|
|
107
|
+
server.prototype.callback = async function (request) {
|
|
108
|
+
try {
|
|
109
|
+
if (typeof Netlify === "undefined") {
|
|
110
|
+
throw new Error("Netlify doesn't exist");
|
|
180
111
|
}
|
|
181
|
-
|
|
182
|
-
|
|
112
|
+
const options = config(this.opts);
|
|
113
|
+
const ctx = await createWinterContext(request, options, this);
|
|
114
|
+
return await handleRequest(this.handlers, ctx);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return new Response(error.message, { status: error.status || 500 });
|
|
117
|
+
}
|
|
118
|
+
};
|
|
183
119
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
120
|
+
// WinterCG, Bun, Cloudflare Workers
|
|
121
|
+
server.prototype.fetch = async function (request, env) {
|
|
122
|
+
if (env?.upgrade(request)) return;
|
|
123
|
+
Object.assign(globalThis.env, env); // Extend env with the passed vars
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const options = config(this.opts);
|
|
127
|
+
const ctx = await createWinterContext(request, options, this);
|
|
128
|
+
return await handleRequest(this.handlers, ctx);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return new Response(error.message, { status: error.status || 500 });
|
|
131
|
+
}
|
|
193
132
|
};
|
|
194
133
|
|
|
134
|
+
// #region HTTP methods
|
|
195
135
|
// INTERNAL
|
|
196
136
|
server.prototype.handle = function (method, path, ...middleware) {
|
|
197
137
|
if (method === "*") {
|
|
@@ -244,6 +184,7 @@ server.prototype.use = function (...middleware) {
|
|
|
244
184
|
return this.handle("*", "*", ...middleware);
|
|
245
185
|
};
|
|
246
186
|
|
|
187
|
+
// Unwind the children routers into the main router
|
|
247
188
|
server.prototype.router = function (basePath, router) {
|
|
248
189
|
basePath = ("/" + basePath + "/").replace(/^\/+/, "/").replace(/\/+$/, "/");
|
|
249
190
|
for (const method in router.handlers) {
|
|
@@ -254,6 +195,7 @@ server.prototype.router = function (basePath, router) {
|
|
|
254
195
|
return this.self();
|
|
255
196
|
};
|
|
256
197
|
|
|
198
|
+
// #region Testing helper
|
|
257
199
|
server.prototype.test = function () {
|
|
258
200
|
let cookie = "";
|
|
259
201
|
const fetch = async (path, options = {}) => {
|
package/src/index.test.js
CHANGED
|
@@ -13,6 +13,21 @@ describe("exports", () => {
|
|
|
13
13
|
|
|
14
14
|
it("export has a fetch", () => {
|
|
15
15
|
expect(typeof server().fetch).toBe("function");
|
|
16
|
+
expect(typeof server().get().fetch).toBe("function");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("export has the basic methods", () => {
|
|
20
|
+
expect(typeof server().get).toBe("function");
|
|
21
|
+
expect(typeof server().post).toBe("function");
|
|
22
|
+
expect(typeof server().use).toBe("function");
|
|
23
|
+
expect(typeof server().router).toBe("function");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("export has the basic nested methods", () => {
|
|
27
|
+
expect(typeof server().get().get).toBe("function");
|
|
28
|
+
expect(typeof server().post().post).toBe("function");
|
|
29
|
+
expect(typeof server().use().use).toBe("function");
|
|
30
|
+
expect(typeof server().get().router).toBe("function");
|
|
16
31
|
});
|
|
17
32
|
|
|
18
33
|
it("nested is also a function", () => {
|
package/src/polyfill.js
CHANGED
|
@@ -6,7 +6,13 @@ if (typeof Response === "undefined") {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
// Polyfill Netlify's environment variables
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Object.assign(
|
|
9
|
+
globalThis.env = {};
|
|
10
|
+
if (typeof Netlify !== "undefined") {
|
|
11
|
+
Object.assign(env, Netlify.env.toObject());
|
|
12
|
+
}
|
|
13
|
+
if (typeof process !== "undefined") {
|
|
14
|
+
Object.assign(env, process.env);
|
|
15
|
+
}
|
|
16
|
+
if (typeof import.meta.env !== "undefined") {
|
|
17
|
+
Object.assign(env, import.meta.env);
|
|
12
18
|
}
|
|
File without changes
|