epos 1.2.5 → 1.3.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.
@@ -0,0 +1,19 @@
1
+ import globals from 'globals'
2
+ import pluginJs from '@eslint/js'
3
+
4
+ /** @type {import('eslint').Linter.Config[]} */
5
+ export default [
6
+ { languageOptions: { globals: globals.browser } },
7
+ pluginJs.configs.recommended,
8
+ {
9
+ languageOptions: {
10
+ globals: {
11
+ Self: false,
12
+ process: false,
13
+ },
14
+ },
15
+ rules: {
16
+ 'no-unused-labels': 0,
17
+ },
18
+ },
19
+ ]
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "epos",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
4
4
  "author": "imkost",
5
5
  "description": "",
6
6
  "keywords": [],
7
7
  "license": "MIT",
8
8
  "type": "module",
9
9
  "scripts": {
10
- "dev": "node ./src/kit/kit-bin.js"
10
+ "dev": "node ./src/kit/kit-bin.js",
11
+ "lint": "eslint ./src",
12
+ "publish": "npm publish --loglevel=error"
11
13
  },
12
14
  "bin": {
13
15
  "epos": "src/kit/kit-bin.js"
@@ -27,5 +29,10 @@
27
29
  "mime": "^4.0.6",
28
30
  "prettier": "^3.4.2",
29
31
  "ws": "^8.18.0"
32
+ },
33
+ "devDependencies": {
34
+ "@eslint/js": "^9.18.0",
35
+ "eslint": "^9.18.0",
36
+ "globals": "^15.14.0"
30
37
  }
31
38
  }
@@ -6,58 +6,86 @@ import $yaml from 'js-yaml'
6
6
  import $chokidar from 'chokidar'
7
7
  import * as $ws from 'ws'
8
8
 
9
- // TODO: handle 'port in use' error
10
- // TODO: epos-kit as separate package (?) but bin: epos
11
9
  const $server = {
12
- async init(dir = '/Users/imkost/z/epos') {
10
+ async init(dir) {
13
11
  this._dir = dir
14
12
  this._port = 4322
15
13
  this._maxFiles = 10_000
14
+
16
15
  this._pkgs = {} // { [path]: { name, dir, watcher } }
17
- const httpServer = await this._startHttpServer()
18
- await this._startMainWatcher()
19
- this._wss = await this._startWebSocketServer(httpServer)
20
- console.log('⚡ running')
16
+ this._server = null
17
+ this._wss = null
18
+
19
+ await this._initServer()
20
+ await this._initWebSocket()
21
+ await this._startWatcher()
22
+
23
+ console.log('đŸŸĸ ready')
21
24
  },
22
25
 
23
- async _startWebSocketServer(httpServer) {
24
- // TODO: handle port in use error
25
- const wss = new $ws.WebSocketServer({ server: httpServer })
26
- wss.on('error', e => {
27
- console.warn('##############')
26
+ async _initServer() {
27
+ const ready = Promise.withResolvers()
28
+
29
+ this._server = $http.createServer(async (req, res) => {
30
+ try {
31
+ const { data, type } = await this._handleRequest(req)
32
+ res.writeHead(200, { 'Content-Type': type })
33
+ res.end(data)
34
+ } catch (e) {
35
+ if (e === 404) {
36
+ res.writeHead(404, { 'Content-Type': 'text/plain' })
37
+ res.end('404: File Not Found')
38
+ } else {
39
+ console.error(e)
40
+ res.writeHead(500, { 'Content-Type': 'text/plain' })
41
+ res.end('500: Internal Server Error')
42
+ }
43
+ }
44
+ })
45
+
46
+ this._server.listen(this._port, () => {
47
+ ready.resolve()
28
48
  })
29
- wss.on('open', () => {
30
- console.warn('connection')
49
+
50
+ this._server.on('error', e => {
51
+ if (e.code === 'EADDRINUSE') {
52
+ console.log(`🔴 port ${this._port} is already in use`)
53
+ process.exit()
54
+ }
55
+ ready.reject(e)
31
56
  })
32
- return wss
57
+
58
+ await ready.promise
59
+ },
60
+
61
+ async _initWebSocket() {
62
+ this._wss = new $ws.WebSocketServer({ server: this._server })
33
63
  },
34
64
 
35
- async _startMainWatcher() {
36
- const watcherReady = Promise.withResolvers()
65
+ async _startWatcher() {
66
+ const ready = Promise.withResolvers()
37
67
  const watcher = $chokidar.watch(this._dir, { ignored: this._ignored })
38
68
 
39
69
  // initial scan
40
- let isInitialScan = true
41
- let initialFileCount = 0
42
- watcher.on('all', async (event, path) => {
43
- if (!isInitialScan) return
44
- initialFileCount += 1
45
- if (initialFileCount < this._maxFiles) return
46
- console.error('⛔ too many files')
47
- console.error(`More than ${this._maxFiles} files in the directory.`)
48
- console.error(`Please point to a directory with less files.`)
70
+ let done = false
71
+ let files = 0
72
+ watcher.on('add', async () => {
73
+ if (done) return
74
+ files += 1
75
+ if (files < this._maxFiles) return
76
+ console.log(`🔴 too many files in ${this._dir}`)
77
+ console.log('â„šī¸ select directory with fewer files')
49
78
  process.exit()
50
79
  })
51
80
  watcher.on('ready', () => {
52
- isInitialScan = false
53
- watcherReady.resolve()
81
+ done = false
82
+ ready.resolve()
54
83
  })
55
84
 
56
85
  // watch added manifest files
57
86
  const variants = new Set(['epos.json', 'epos.yaml', 'epos.yml'])
58
87
  watcher.on('add', async path => {
59
- const isEposManifest = variants.has($path.basename(path))
60
- if (!isEposManifest) return
88
+ if (!variants.has($path.basename(path))) return
61
89
  const pkg = await this._createPkgWatcher(path)
62
90
  if (!pkg) return
63
91
  this._pkgs[path] = pkg
@@ -70,73 +98,63 @@ const $server = {
70
98
  delete this._pkgs[path]
71
99
  })
72
100
 
73
- await watcherReady.promise
74
- },
75
-
76
- async _startHttpServer() {
77
- const httpServerReady = Promise.withResolvers()
78
- const httpServer = $http.createServer(async (req, res) => {
79
- const pkgName = req.url.split('/')[1]
80
- const pkg = Object.values(this._pkgs).find(p => p.name === pkgName)
81
- if (!pkg) {
82
- res.writeHead(404, { 'Content-Type': 'text/plain' })
83
- res.end('404: File Not Found')
84
- return
85
- }
86
-
87
- const filePath = $path.join(pkg.dir, req.url.split('/').slice(2).join('/'))
88
- const contentType = $mime.getType(filePath) || 'application/octet-stream'
89
- try {
90
- const data = await $fs.readFile(filePath)
91
- res.writeHead(200, { 'Content-Type': contentType })
92
- res.end(data)
93
- } catch (e) {
94
- if (e.code === 'ENOENT') {
95
- res.writeHead(404, { 'Content-Type': 'text/plain' })
96
- res.end('404: File Not Found')
97
- } else {
98
- res.writeHead(500, { 'Content-Type': 'text/plain' })
99
- res.end('500: Internal Server Error')
100
- }
101
- }
102
- })
103
- httpServer.listen(this._port, () => {
104
- httpServerReady.resolve()
105
- })
106
-
107
- httpServer.on('error', e => {
108
- console.warn(e)
109
- })
110
-
111
- await httpServerReady.promise
112
- return httpServer
101
+ await ready.promise
113
102
  },
114
103
 
115
104
  async _createPkgWatcher(manifestPath) {
105
+ // read manifest
116
106
  const isJson = manifestPath.endsWith('.json')
117
107
  const content = await $fs.readFile(manifestPath, 'utf-8')
118
108
  const manifest = isJson ? JSON.parse(content) : $yaml.load(content)
119
- const pkgName = manifest.name
120
- if (!pkgName) return null
121
109
 
110
+ // no name? -> ignore
111
+ const name = manifest.name
112
+ if (!name) return null
113
+
114
+ // create pkg dir watcher
122
115
  const dir = $path.dirname(manifestPath)
123
116
  const watcher = $chokidar.watch(dir, {
124
117
  ignored: this._ignored,
125
118
  ignoreInitial: true,
126
119
  })
127
120
 
121
+ // broadcast changes
128
122
  watcher.on('all', (event, path) => {
129
- const data = JSON.stringify({ name: pkgName, path: $path.relative(dir, path) })
123
+ const data = JSON.stringify({ name, path: $path.relative(dir, path) })
130
124
  for (const client of this._wss.clients) {
131
125
  if (client.readyState !== 1) continue
132
126
  client.send(data)
133
127
  }
134
128
  })
135
129
 
136
- return {
137
- watcher,
138
- name: pkgName,
139
- dir,
130
+ return { name, dir, watcher }
131
+ },
132
+
133
+ async _handleRequest(req) {
134
+ // /<pkgName>/some/path/to/file.jsx
135
+ const [pkgName, ...filePath] = req.url.split('/').slice(1)
136
+
137
+ // pkg not found? -> 404
138
+ const pkg = Object.values(this._pkgs).find(p => p.name === pkgName)
139
+ if (!pkg) throw 404
140
+
141
+ // file not found? -> 404
142
+ const path = $path.join(pkg.dir, ...filePath)
143
+ const exists = await this._fileExists(path)
144
+ if (!exists) throw 404
145
+
146
+ // respond file
147
+ const data = await $fs.readFile(path)
148
+ const type = $mime.getType(path) || 'application/octet-stream'
149
+ return { data, type }
150
+ },
151
+
152
+ async _fileExists(path) {
153
+ try {
154
+ await $fs.access(path, $fs.constants.F_OK)
155
+ return true
156
+ } catch {
157
+ return false
140
158
  }
141
159
  },
142
160