odac 0.9.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/.editorconfig +21 -0
- package/.github/workflows/auto-pr-description.yml +49 -0
- package/.github/workflows/release.yml +32 -0
- package/.github/workflows/test-coverage.yml +58 -0
- package/.husky/pre-commit +2 -0
- package/.kiro/steering/code-style.md +56 -0
- package/.kiro/steering/product.md +20 -0
- package/.kiro/steering/structure.md +77 -0
- package/.kiro/steering/tech.md +87 -0
- package/.prettierrc +10 -0
- package/.releaserc.js +134 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +181 -0
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +661 -0
- package/README.md +57 -0
- package/SECURITY.md +26 -0
- package/bin/candy +10 -0
- package/bin/candypack +10 -0
- package/cli/index.js +3 -0
- package/cli/src/Cli.js +348 -0
- package/cli/src/Connector.js +93 -0
- package/cli/src/Monitor.js +416 -0
- package/core/Candy.js +87 -0
- package/core/Commands.js +239 -0
- package/core/Config.js +1094 -0
- package/core/Lang.js +52 -0
- package/core/Log.js +43 -0
- package/core/Process.js +26 -0
- package/docs/backend/01-overview/01-whats-in-the-candy-box.md +9 -0
- package/docs/backend/01-overview/02-super-handy-helper-functions.md +9 -0
- package/docs/backend/01-overview/03-development-server.md +79 -0
- package/docs/backend/02-structure/01-typical-project-layout.md +39 -0
- package/docs/backend/03-config/00-configuration-overview.md +214 -0
- package/docs/backend/03-config/01-database-connection.md +60 -0
- package/docs/backend/03-config/02-static-route-mapping-optional.md +20 -0
- package/docs/backend/03-config/03-request-timeout.md +11 -0
- package/docs/backend/03-config/04-environment-variables.md +227 -0
- package/docs/backend/03-config/05-early-hints.md +352 -0
- package/docs/backend/04-routing/01-basic-page-routes.md +28 -0
- package/docs/backend/04-routing/02-controller-less-view-routes.md +43 -0
- package/docs/backend/04-routing/03-api-and-data-routes.md +20 -0
- package/docs/backend/04-routing/04-authentication-aware-routes.md +48 -0
- package/docs/backend/04-routing/05-advanced-routing.md +14 -0
- package/docs/backend/04-routing/06-error-pages.md +101 -0
- package/docs/backend/04-routing/07-cron-jobs.md +149 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +17 -0
- package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +20 -0
- package/docs/backend/05-controllers/03-controller-classes.md +93 -0
- package/docs/backend/05-forms/01-custom-forms.md +395 -0
- package/docs/backend/05-forms/02-automatic-database-insert.md +297 -0
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +96 -0
- package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +40 -0
- package/docs/backend/07-views/01-the-view-directory.md +73 -0
- package/docs/backend/07-views/02-rendering-a-view.md +179 -0
- package/docs/backend/07-views/03-template-syntax.md +181 -0
- package/docs/backend/07-views/03-variables.md +328 -0
- package/docs/backend/07-views/04-request-data.md +231 -0
- package/docs/backend/07-views/05-conditionals.md +290 -0
- package/docs/backend/07-views/06-loops.md +353 -0
- package/docs/backend/07-views/07-translations.md +358 -0
- package/docs/backend/07-views/08-backend-javascript.md +398 -0
- package/docs/backend/07-views/09-comments.md +297 -0
- package/docs/backend/08-database/01-database-connection.md +99 -0
- package/docs/backend/08-database/02-using-mysql.md +322 -0
- package/docs/backend/09-validation/01-the-validator-service.md +424 -0
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +53 -0
- package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +55 -0
- package/docs/backend/10-authentication/03-register.md +134 -0
- package/docs/backend/10-authentication/04-candy-register-forms.md +676 -0
- package/docs/backend/10-authentication/05-session-management.md +159 -0
- package/docs/backend/10-authentication/06-candy-login-forms.md +596 -0
- package/docs/backend/11-mail/01-the-mail-service.md +42 -0
- package/docs/backend/12-streaming/01-streaming-overview.md +300 -0
- package/docs/backend/13-utilities/01-candy-var.md +504 -0
- package/docs/frontend/01-overview/01-introduction.md +146 -0
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +608 -0
- package/docs/frontend/02-ajax-navigation/02-configuration.md +370 -0
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +519 -0
- package/docs/frontend/03-forms/01-form-handling.md +420 -0
- package/docs/frontend/04-api-requests/01-get-post.md +443 -0
- package/docs/frontend/05-streaming/01-client-streaming.md +163 -0
- package/docs/index.json +452 -0
- package/docs/server/01-installation/01-quick-install.md +19 -0
- package/docs/server/01-installation/02-manual-installation-via-npm.md +9 -0
- package/docs/server/02-get-started/01-core-concepts.md +7 -0
- package/docs/server/02-get-started/02-basic-commands.md +57 -0
- package/docs/server/02-get-started/03-cli-reference.md +276 -0
- package/docs/server/02-get-started/04-cli-quick-reference.md +102 -0
- package/docs/server/03-service/01-start-a-new-service.md +57 -0
- package/docs/server/03-service/02-delete-a-service.md +48 -0
- package/docs/server/04-web/01-create-a-website.md +36 -0
- package/docs/server/04-web/02-list-websites.md +9 -0
- package/docs/server/04-web/03-delete-a-website.md +29 -0
- package/docs/server/05-subdomain/01-create-a-subdomain.md +32 -0
- package/docs/server/05-subdomain/02-list-subdomains.md +33 -0
- package/docs/server/05-subdomain/03-delete-a-subdomain.md +41 -0
- package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +34 -0
- package/docs/server/07-mail/01-create-a-mail-account.md +23 -0
- package/docs/server/07-mail/02-delete-a-mail-account.md +20 -0
- package/docs/server/07-mail/03-list-mail-accounts.md +20 -0
- package/docs/server/07-mail/04-change-account-password.md +23 -0
- package/eslint.config.mjs +120 -0
- package/framework/index.js +4 -0
- package/framework/src/Auth.js +309 -0
- package/framework/src/Candy.js +81 -0
- package/framework/src/Config.js +79 -0
- package/framework/src/Env.js +60 -0
- package/framework/src/Lang.js +57 -0
- package/framework/src/Mail.js +83 -0
- package/framework/src/Mysql.js +575 -0
- package/framework/src/Request.js +301 -0
- package/framework/src/Route/Cron.js +128 -0
- package/framework/src/Route/Internal.js +439 -0
- package/framework/src/Route.js +455 -0
- package/framework/src/Server.js +15 -0
- package/framework/src/Stream.js +163 -0
- package/framework/src/Token.js +37 -0
- package/framework/src/Validator.js +271 -0
- package/framework/src/Var.js +211 -0
- package/framework/src/View/EarlyHints.js +190 -0
- package/framework/src/View/Form.js +600 -0
- package/framework/src/View.js +513 -0
- package/framework/web/candy.js +838 -0
- package/jest.config.js +22 -0
- package/locale/de-DE.json +80 -0
- package/locale/en-US.json +79 -0
- package/locale/es-ES.json +80 -0
- package/locale/fr-FR.json +80 -0
- package/locale/pt-BR.json +80 -0
- package/locale/ru-RU.json +80 -0
- package/locale/tr-TR.json +85 -0
- package/locale/zh-CN.json +80 -0
- package/package.json +86 -0
- package/server/index.js +5 -0
- package/server/src/Api.js +88 -0
- package/server/src/DNS.js +940 -0
- package/server/src/Hub.js +535 -0
- package/server/src/Mail.js +571 -0
- package/server/src/SSL.js +180 -0
- package/server/src/Server.js +27 -0
- package/server/src/Service.js +248 -0
- package/server/src/Subdomain.js +64 -0
- package/server/src/Web/Firewall.js +170 -0
- package/server/src/Web/Proxy.js +134 -0
- package/server/src/Web.js +451 -0
- package/server/src/mail/imap.js +1091 -0
- package/server/src/mail/server.js +32 -0
- package/server/src/mail/smtp.js +786 -0
- package/test/cli/Cli.test.js +36 -0
- package/test/core/Candy.test.js +234 -0
- package/test/core/Commands.test.js +538 -0
- package/test/core/Config.test.js +1435 -0
- package/test/core/Lang.test.js +250 -0
- package/test/core/Process.test.js +156 -0
- package/test/framework/Route.test.js +239 -0
- package/test/framework/View/EarlyHints.test.js +282 -0
- package/test/scripts/check-coverage.js +132 -0
- package/test/server/Api.test.js +647 -0
- package/test/server/Client.test.js +338 -0
- package/test/server/DNS.test.js +2050 -0
- package/test/server/DNS.test.js.bak +2084 -0
- package/test/server/Log.test.js +73 -0
- package/test/server/Mail.account.test_.js +460 -0
- package/test/server/Mail.init.test_.js +411 -0
- package/test/server/Mail.test_.js +1340 -0
- package/test/server/SSL.test_.js +1491 -0
- package/test/server/Server.test.js +765 -0
- package/test/server/Service.test_.js +1127 -0
- package/test/server/Subdomain.test.js +440 -0
- package/test/server/Web/Firewall.test.js +175 -0
- package/test/server/Web.test_.js +1562 -0
- package/test/server/__mocks__/acme-client.js +17 -0
- package/test/server/__mocks__/bcrypt.js +50 -0
- package/test/server/__mocks__/child_process.js +389 -0
- package/test/server/__mocks__/crypto.js +432 -0
- package/test/server/__mocks__/fs.js +450 -0
- package/test/server/__mocks__/globalCandy.js +227 -0
- package/test/server/__mocks__/http-proxy.js +105 -0
- package/test/server/__mocks__/http.js +575 -0
- package/test/server/__mocks__/https.js +272 -0
- package/test/server/__mocks__/index.js +249 -0
- package/test/server/__mocks__/mail/server.js +100 -0
- package/test/server/__mocks__/mail/smtp.js +31 -0
- package/test/server/__mocks__/mailparser.js +81 -0
- package/test/server/__mocks__/net.js +369 -0
- package/test/server/__mocks__/node-forge.js +328 -0
- package/test/server/__mocks__/os.js +320 -0
- package/test/server/__mocks__/path.js +291 -0
- package/test/server/__mocks__/selfsigned.js +8 -0
- package/test/server/__mocks__/server/src/mail/server.js +100 -0
- package/test/server/__mocks__/server/src/mail/smtp.js +31 -0
- package/test/server/__mocks__/smtp-server.js +106 -0
- package/test/server/__mocks__/sqlite3.js +394 -0
- package/test/server/__mocks__/testFactories.js +299 -0
- package/test/server/__mocks__/testHelpers.js +363 -0
- package/test/server/__mocks__/tls.js +229 -0
- package/watchdog/index.js +3 -0
- package/watchdog/src/Watchdog.js +156 -0
- package/web/config.json +5 -0
- package/web/controller/page/about.js +27 -0
- package/web/controller/page/index.js +34 -0
- package/web/package.json +18 -0
- package/web/public/assets/css/style.css +1835 -0
- package/web/public/assets/js/app.js +96 -0
- package/web/route/www.js +19 -0
- package/web/skeleton/main.html +22 -0
- package/web/view/content/about.html +65 -0
- package/web/view/content/home.html +205 -0
- package/web/view/footer/main.html +11 -0
- package/web/view/head/main.html +5 -0
- package/web/view/header/main.html +14 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
const EarlyHints = require('../../../framework/src/View/EarlyHints')
|
|
2
|
+
|
|
3
|
+
describe('EarlyHints', () => {
|
|
4
|
+
let earlyHints
|
|
5
|
+
let mockConfig
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockConfig = {
|
|
9
|
+
enabled: true,
|
|
10
|
+
auto: true,
|
|
11
|
+
maxResources: 5
|
|
12
|
+
}
|
|
13
|
+
earlyHints = new EarlyHints(mockConfig)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('initialization', () => {
|
|
17
|
+
it('should create instance with default config', () => {
|
|
18
|
+
const hints = new EarlyHints()
|
|
19
|
+
expect(hints).toBeDefined()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should create instance with custom config', () => {
|
|
23
|
+
const hints = new EarlyHints({enabled: false})
|
|
24
|
+
expect(hints).toBeDefined()
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('extractFromHtml', () => {
|
|
29
|
+
it('should extract CSS resources from head', () => {
|
|
30
|
+
const html = `
|
|
31
|
+
<html>
|
|
32
|
+
<head>
|
|
33
|
+
<link rel="stylesheet" href="/css/main.css">
|
|
34
|
+
<link rel="stylesheet" href="/css/theme.css">
|
|
35
|
+
</head>
|
|
36
|
+
<body></body>
|
|
37
|
+
</html>
|
|
38
|
+
`
|
|
39
|
+
const resources = earlyHints.extractFromHtml(html)
|
|
40
|
+
expect(resources).toHaveLength(2)
|
|
41
|
+
expect(resources[0]).toEqual({href: '/css/main.css', as: 'style'})
|
|
42
|
+
expect(resources[1]).toEqual({href: '/css/theme.css', as: 'style'})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should extract JS resources from head', () => {
|
|
46
|
+
const html = `
|
|
47
|
+
<html>
|
|
48
|
+
<head>
|
|
49
|
+
<script src="/js/app.js"></script>
|
|
50
|
+
</head>
|
|
51
|
+
<body></body>
|
|
52
|
+
</html>
|
|
53
|
+
`
|
|
54
|
+
const resources = earlyHints.extractFromHtml(html)
|
|
55
|
+
expect(resources).toHaveLength(1)
|
|
56
|
+
expect(resources[0]).toEqual({href: '/js/app.js', as: 'script'})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should not extract deferred JS', () => {
|
|
60
|
+
const html = `
|
|
61
|
+
<html>
|
|
62
|
+
<head>
|
|
63
|
+
<script src="/js/app.js" defer></script>
|
|
64
|
+
<script src="/js/async.js" async></script>
|
|
65
|
+
</head>
|
|
66
|
+
<body></body>
|
|
67
|
+
</html>
|
|
68
|
+
`
|
|
69
|
+
const resources = earlyHints.extractFromHtml(html)
|
|
70
|
+
expect(resources).toHaveLength(0)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should extract font resources', () => {
|
|
74
|
+
const html = `
|
|
75
|
+
<html>
|
|
76
|
+
<head>
|
|
77
|
+
<link rel="preload" href="/fonts/main.woff2" as="font">
|
|
78
|
+
</head>
|
|
79
|
+
<body></body>
|
|
80
|
+
</html>
|
|
81
|
+
`
|
|
82
|
+
const resources = earlyHints.extractFromHtml(html)
|
|
83
|
+
expect(resources).toHaveLength(1)
|
|
84
|
+
expect(resources[0]).toEqual({
|
|
85
|
+
href: '/fonts/main.woff2',
|
|
86
|
+
as: 'font',
|
|
87
|
+
crossorigin: 'anonymous'
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should limit resources to maxResources', () => {
|
|
92
|
+
const html = `
|
|
93
|
+
<html>
|
|
94
|
+
<head>
|
|
95
|
+
<link rel="stylesheet" href="/css/1.css">
|
|
96
|
+
<link rel="stylesheet" href="/css/2.css">
|
|
97
|
+
<link rel="stylesheet" href="/css/3.css">
|
|
98
|
+
<link rel="stylesheet" href="/css/4.css">
|
|
99
|
+
<link rel="stylesheet" href="/css/5.css">
|
|
100
|
+
<link rel="stylesheet" href="/css/6.css">
|
|
101
|
+
</head>
|
|
102
|
+
<body></body>
|
|
103
|
+
</html>
|
|
104
|
+
`
|
|
105
|
+
const resources = earlyHints.extractFromHtml(html)
|
|
106
|
+
expect(resources).toHaveLength(5)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should return empty array when no head tag', () => {
|
|
110
|
+
const html = '<html><body></body></html>'
|
|
111
|
+
const resources = earlyHints.extractFromHtml(html)
|
|
112
|
+
expect(resources).toEqual([])
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should return empty array when disabled', () => {
|
|
116
|
+
const hints = new EarlyHints({enabled: false})
|
|
117
|
+
const html = '<html><head><link rel="stylesheet" href="/css/main.css"></head></html>'
|
|
118
|
+
const resources = hints.extractFromHtml(html)
|
|
119
|
+
expect(resources).toEqual([])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should skip resources with defer attribute', () => {
|
|
123
|
+
const html = `
|
|
124
|
+
<html>
|
|
125
|
+
<head>
|
|
126
|
+
<link rel="stylesheet" href="/css/critical.css">
|
|
127
|
+
<link rel="stylesheet" href="/css/non-critical.css" defer>
|
|
128
|
+
<script src="/js/app.js"></script>
|
|
129
|
+
<script src="/js/analytics.js" defer></script>
|
|
130
|
+
</head>
|
|
131
|
+
<body></body>
|
|
132
|
+
</html>
|
|
133
|
+
`
|
|
134
|
+
const resources = earlyHints.extractFromHtml(html)
|
|
135
|
+
expect(resources).toHaveLength(2)
|
|
136
|
+
expect(resources[0]).toEqual({href: '/css/critical.css', as: 'style'})
|
|
137
|
+
expect(resources[1]).toEqual({href: '/js/app.js', as: 'script'})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should only detect stylesheets with rel="stylesheet"', () => {
|
|
141
|
+
const html = `
|
|
142
|
+
<html>
|
|
143
|
+
<head>
|
|
144
|
+
<link rel="stylesheet" href="/css/main.css">
|
|
145
|
+
<link rel="icon" href="/favicon.css">
|
|
146
|
+
<link rel="preload" href="/data.css" as="fetch">
|
|
147
|
+
<link href="/other.css">
|
|
148
|
+
</head>
|
|
149
|
+
<body></body>
|
|
150
|
+
</html>
|
|
151
|
+
`
|
|
152
|
+
const resources = earlyHints.extractFromHtml(html)
|
|
153
|
+
expect(resources).toHaveLength(1)
|
|
154
|
+
expect(resources[0]).toEqual({href: '/css/main.css', as: 'style'})
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('formatLinkHeader', () => {
|
|
159
|
+
it('should format basic resource', () => {
|
|
160
|
+
const resource = {href: '/css/main.css', as: 'style'}
|
|
161
|
+
const header = earlyHints.formatLinkHeader(resource)
|
|
162
|
+
expect(header).toBe('</css/main.css>; rel=preload; as=style')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('should format resource with crossorigin', () => {
|
|
166
|
+
const resource = {href: '/font.woff2', as: 'font', crossorigin: 'anonymous'}
|
|
167
|
+
const header = earlyHints.formatLinkHeader(resource)
|
|
168
|
+
expect(header).toBe('</font.woff2>; rel=preload; as=font; crossorigin')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should format resource with type', () => {
|
|
172
|
+
const resource = {href: '/data.json', as: 'fetch', type: 'application/json'}
|
|
173
|
+
const header = earlyHints.formatLinkHeader(resource)
|
|
174
|
+
expect(header).toBe('</data.json>; rel=preload; as=fetch; type=application/json')
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe('caching', () => {
|
|
179
|
+
it('should cache hints for route', () => {
|
|
180
|
+
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
181
|
+
earlyHints.cacheHints('/home', resources)
|
|
182
|
+
|
|
183
|
+
const cached = earlyHints.getHints(null, '/home')
|
|
184
|
+
expect(cached).toEqual(resources)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should not cache when disabled', () => {
|
|
188
|
+
const hints = new EarlyHints({enabled: false})
|
|
189
|
+
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
190
|
+
hints.cacheHints('/home', resources)
|
|
191
|
+
|
|
192
|
+
const cached = hints.getHints(null, '/home')
|
|
193
|
+
expect(cached).toBeNull()
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('send', () => {
|
|
198
|
+
it('should return false when disabled', () => {
|
|
199
|
+
const hints = new EarlyHints({enabled: false})
|
|
200
|
+
const mockRes = {
|
|
201
|
+
headersSent: false,
|
|
202
|
+
writableEnded: false,
|
|
203
|
+
writeEarlyHints: jest.fn()
|
|
204
|
+
}
|
|
205
|
+
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
206
|
+
|
|
207
|
+
const result = hints.send(mockRes, resources)
|
|
208
|
+
expect(result).toBe(false)
|
|
209
|
+
expect(mockRes.writeEarlyHints).not.toHaveBeenCalled()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should return false when headers already sent', () => {
|
|
213
|
+
const mockRes = {
|
|
214
|
+
headersSent: true,
|
|
215
|
+
writableEnded: false,
|
|
216
|
+
writeEarlyHints: jest.fn()
|
|
217
|
+
}
|
|
218
|
+
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
219
|
+
|
|
220
|
+
const result = earlyHints.send(mockRes, resources)
|
|
221
|
+
expect(result).toBe(false)
|
|
222
|
+
expect(mockRes.writeEarlyHints).not.toHaveBeenCalled()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('should return false when response ended', () => {
|
|
226
|
+
const mockRes = {
|
|
227
|
+
headersSent: false,
|
|
228
|
+
writableEnded: true,
|
|
229
|
+
writeEarlyHints: jest.fn()
|
|
230
|
+
}
|
|
231
|
+
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
232
|
+
|
|
233
|
+
const result = earlyHints.send(mockRes, resources)
|
|
234
|
+
expect(result).toBe(false)
|
|
235
|
+
expect(mockRes.writeEarlyHints).not.toHaveBeenCalled()
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('should return true even when writeEarlyHints not available', () => {
|
|
239
|
+
const mockRes = {
|
|
240
|
+
headersSent: false,
|
|
241
|
+
writableEnded: false,
|
|
242
|
+
setHeader: jest.fn()
|
|
243
|
+
}
|
|
244
|
+
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
245
|
+
|
|
246
|
+
const result = earlyHints.send(mockRes, resources)
|
|
247
|
+
expect(result).toBe(true)
|
|
248
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('X-Candy-Early-Hints', JSON.stringify(['</css/main.css>; rel=preload; as=style']))
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should send early hints successfully', () => {
|
|
252
|
+
const mockRes = {
|
|
253
|
+
headersSent: false,
|
|
254
|
+
writableEnded: false,
|
|
255
|
+
writeEarlyHints: jest.fn(),
|
|
256
|
+
setHeader: jest.fn()
|
|
257
|
+
}
|
|
258
|
+
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
259
|
+
|
|
260
|
+
const result = earlyHints.send(mockRes, resources)
|
|
261
|
+
expect(result).toBe(true)
|
|
262
|
+
expect(mockRes.writeEarlyHints).toHaveBeenCalledWith({
|
|
263
|
+
link: ['</css/main.css>; rel=preload; as=style']
|
|
264
|
+
})
|
|
265
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith('X-Candy-Early-Hints', JSON.stringify(['</css/main.css>; rel=preload; as=style']))
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('should handle writeEarlyHints errors gracefully', () => {
|
|
269
|
+
const mockRes = {
|
|
270
|
+
headersSent: false,
|
|
271
|
+
writableEnded: false,
|
|
272
|
+
writeEarlyHints: jest.fn(() => {
|
|
273
|
+
throw new Error('Write error')
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
277
|
+
|
|
278
|
+
const result = earlyHints.send(mockRes, resources)
|
|
279
|
+
expect(result).toBe(false)
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pre-commit hook to check test coverage for changed files
|
|
5
|
+
* Only runs tests for files that have been modified
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const {execSync} = require('child_process')
|
|
9
|
+
const fs = require('fs')
|
|
10
|
+
|
|
11
|
+
// Get list of staged files
|
|
12
|
+
function getStagedFiles() {
|
|
13
|
+
try {
|
|
14
|
+
const output = execSync('git diff --cached --name-only --diff-filter=ACM', {
|
|
15
|
+
encoding: 'utf8'
|
|
16
|
+
})
|
|
17
|
+
return output
|
|
18
|
+
.split('\n')
|
|
19
|
+
.filter(file => file.endsWith('.js'))
|
|
20
|
+
.filter(file => file.startsWith('core/') || file.startsWith('server/'))
|
|
21
|
+
.filter(file => !file.includes('.test.js') && !file.includes('.spec.js'))
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error('Error getting staged files:', err.message)
|
|
24
|
+
return []
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if there are corresponding test files
|
|
29
|
+
function checkTestFiles(changedFiles) {
|
|
30
|
+
const missingTests = []
|
|
31
|
+
|
|
32
|
+
for (const file of changedFiles) {
|
|
33
|
+
// Skip if file doesn't exist (deleted files)
|
|
34
|
+
if (!fs.existsSync(file)) continue
|
|
35
|
+
|
|
36
|
+
// Determine test file path
|
|
37
|
+
const testFile = file.replace(/^(core|server)\//, 'test/$1/').replace(/\.js$/, '.test.js')
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(testFile)) {
|
|
40
|
+
missingTests.push({
|
|
41
|
+
source: file,
|
|
42
|
+
test: testFile
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return missingTests
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Run tests for specific files
|
|
51
|
+
function runTestsForFiles(files) {
|
|
52
|
+
if (files.length === 0) {
|
|
53
|
+
console.log('ā No testable files changed')
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`\nš Running tests for ${files.length} changed file(s)...\n`)
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Create a pattern to match test files for changed source files
|
|
61
|
+
const testPatterns = files
|
|
62
|
+
.map(file => {
|
|
63
|
+
const testFile = file.replace(/^(core|server)\//, 'test/$1/').replace(/\.js$/, '.test.js')
|
|
64
|
+
return testFile
|
|
65
|
+
})
|
|
66
|
+
.filter(testFile => fs.existsSync(testFile))
|
|
67
|
+
|
|
68
|
+
if (testPatterns.length === 0) {
|
|
69
|
+
console.log('ā ļø No test files found for changed files')
|
|
70
|
+
console.log('Skipping coverage check (no tests available yet)\n')
|
|
71
|
+
return true // Don't block commit if no tests exist yet
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Run Jest for specific test files - no coverage threshold enforcement
|
|
75
|
+
const testFiles = testPatterns.join(' ')
|
|
76
|
+
const command = `npx jest ${testFiles} --passWithNoTests`
|
|
77
|
+
|
|
78
|
+
execSync(command, {
|
|
79
|
+
stdio: 'inherit',
|
|
80
|
+
encoding: 'utf8'
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
console.log('\nā All tests passed\n')
|
|
84
|
+
return true
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error('\nā Tests failed\n')
|
|
87
|
+
console.error('Please ensure:')
|
|
88
|
+
console.error(' 1. All tests pass')
|
|
89
|
+
console.error(' 2. Add tests for new functionality\n')
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Main execution
|
|
95
|
+
function main() {
|
|
96
|
+
console.log('\nš Checking test coverage for changed files...\n')
|
|
97
|
+
|
|
98
|
+
const changedFiles = getStagedFiles()
|
|
99
|
+
|
|
100
|
+
if (changedFiles.length === 0) {
|
|
101
|
+
console.log('ā No core or server files changed\n')
|
|
102
|
+
process.exit(0)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log('Changed files:')
|
|
106
|
+
changedFiles.forEach(file => console.log(` - ${file}`))
|
|
107
|
+
console.log('')
|
|
108
|
+
|
|
109
|
+
// Check for missing test files
|
|
110
|
+
const missingTests = checkTestFiles(changedFiles)
|
|
111
|
+
|
|
112
|
+
if (missingTests.length > 0) {
|
|
113
|
+
console.log('ā ļø Warning: Missing test files for:')
|
|
114
|
+
missingTests.forEach(({source, test}) => {
|
|
115
|
+
console.log(` - ${source}`)
|
|
116
|
+
console.log(` Expected: ${test}`)
|
|
117
|
+
})
|
|
118
|
+
console.log('')
|
|
119
|
+
console.log('Consider adding tests for better coverage.\n')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Run tests
|
|
123
|
+
const success = runTestsForFiles(changedFiles)
|
|
124
|
+
|
|
125
|
+
if (!success) {
|
|
126
|
+
process.exit(1)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
process.exit(0)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main()
|