@foxtware/mineral 0.1.22 → 0.1.24
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/api/peoplevox/peoplevox.utils.js +2 -1
- package/api/shopify/shopifyMetafieldDefinitionDelete.js +127 -0
- package/api/shopify/shopifyOrdersGet.js +16 -52
- package/api/shopify/shopifyShopifyqlQuery.js +109 -0
- package/api/tagalys/docs.md +4 -4
- package/api/utils.js +4 -0
- package/hosting/hostingPreview.js +2 -2
- package/package.json +1 -1
- package/server.js +10 -2
- package/server.utils.js +169 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const csvtojson = require('csvtojson');
|
|
2
2
|
const xml2js = require('xml2js');
|
|
3
|
+
const { HOSTED } = require('../constants');
|
|
3
4
|
const { resolveCreds } = require('../pipelineSteps');
|
|
4
5
|
const {
|
|
5
6
|
FetchClient,
|
|
@@ -69,7 +70,7 @@ const useSoapEnvelope = async (state) => {
|
|
|
69
70
|
sessionId,
|
|
70
71
|
});
|
|
71
72
|
|
|
72
|
-
logDeep({ envelopeXml });
|
|
73
|
+
!HOSTED && logDeep({ envelopeXml });
|
|
73
74
|
|
|
74
75
|
return {
|
|
75
76
|
requestPayload: {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// https://shopify.dev/docs/api/admin-graphql/latest/mutations/metafieldDefinitionDelete
|
|
2
|
+
|
|
3
|
+
const { credsValidator } = require('../validators');
|
|
4
|
+
const { ArgsWarden, valueProvided, actionSingleOrMultiple, everyIfArray } = require('../utils');
|
|
5
|
+
const { shopifyMutationDo } = require('../shopify/shopifyMutationDo');
|
|
6
|
+
|
|
7
|
+
const metafieldDefinitionIdentifierValidator = (metafieldDefinitionIdentifier) => {
|
|
8
|
+
const {
|
|
9
|
+
id,
|
|
10
|
+
key,
|
|
11
|
+
namespace,
|
|
12
|
+
ownerType,
|
|
13
|
+
} = metafieldDefinitionIdentifier;
|
|
14
|
+
return (key && namespace && ownerType)
|
|
15
|
+
|| valueProvided(id);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const argsWarden = new ArgsWarden([
|
|
19
|
+
['credsPayload', (i) => everyIfArray(credsValidator, i)],
|
|
20
|
+
['metafieldDefinitionIdentifier', (i) => everyIfArray(metafieldDefinitionIdentifierValidator, i)],
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const shopifyMetafieldDefinitionDeleteSingle = async (
|
|
24
|
+
credsPayload,
|
|
25
|
+
metafieldDefinitionIdentifier,
|
|
26
|
+
{
|
|
27
|
+
apiVersion,
|
|
28
|
+
returnSchema = 'deletedDefinitionId',
|
|
29
|
+
deleteAllAssociatedMetafields = false,
|
|
30
|
+
} = {},
|
|
31
|
+
) => {
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
id,
|
|
35
|
+
key,
|
|
36
|
+
namespace,
|
|
37
|
+
ownerType,
|
|
38
|
+
} = metafieldDefinitionIdentifier;
|
|
39
|
+
|
|
40
|
+
return shopifyMutationDo(
|
|
41
|
+
credsPayload,
|
|
42
|
+
'metafieldDefinitionDelete',
|
|
43
|
+
{
|
|
44
|
+
mutationVariables: {
|
|
45
|
+
...(id && { id: {
|
|
46
|
+
type: 'ID!',
|
|
47
|
+
value: `gid://shopify/MetafieldDefinition/${ id }`,
|
|
48
|
+
} }),
|
|
49
|
+
...((key && namespace && ownerType) && {
|
|
50
|
+
identifier: {
|
|
51
|
+
type: 'MetafieldDefinitionIdentifierInput!',
|
|
52
|
+
value: {
|
|
53
|
+
key,
|
|
54
|
+
namespace,
|
|
55
|
+
ownerType,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
deleteAllAssociatedMetafields: {
|
|
60
|
+
type: 'Boolean!',
|
|
61
|
+
value: deleteAllAssociatedMetafields,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
returnSchema,
|
|
65
|
+
apiVersion,
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const shopifyMetafieldDefinitionDelete = async (
|
|
71
|
+
credsPayload,
|
|
72
|
+
metafieldDefinitionIdentifier,
|
|
73
|
+
{
|
|
74
|
+
queueRunOptions,
|
|
75
|
+
...options
|
|
76
|
+
} = {},
|
|
77
|
+
) => {
|
|
78
|
+
|
|
79
|
+
const rejectResponse = await argsWarden.responseIfRejectingArgs({
|
|
80
|
+
credsPayload,
|
|
81
|
+
metafieldDefinitionIdentifier,
|
|
82
|
+
});
|
|
83
|
+
if (rejectResponse) {
|
|
84
|
+
return rejectResponse;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return actionSingleOrMultiple(
|
|
88
|
+
[credsPayload, metafieldDefinitionIdentifier],
|
|
89
|
+
shopifyMetafieldDefinitionDeleteSingle,
|
|
90
|
+
(credsPayloadItem, metafieldDefinitionIdentifierItem) => ({
|
|
91
|
+
args: [
|
|
92
|
+
credsPayloadItem,
|
|
93
|
+
metafieldDefinitionIdentifierItem,
|
|
94
|
+
options,
|
|
95
|
+
],
|
|
96
|
+
}),
|
|
97
|
+
{
|
|
98
|
+
...(queueRunOptions ? { queueRunOptions } : {}),
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const funcApiConfig = {
|
|
104
|
+
argsWarden,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
shopifyMetafieldDefinitionDelete,
|
|
109
|
+
funcApiConfig,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
curl -X POST "http://localhost:8000/shopifyMetafieldDefinitionDelete" \
|
|
114
|
+
-H "Content-Type: application/json" \
|
|
115
|
+
-d '{
|
|
116
|
+
"credsPayload": [
|
|
117
|
+
{ "credsPath": "shopify.au" },
|
|
118
|
+
{ "credsPath": "shopify.us" },
|
|
119
|
+
{ "credsPath": "shopify.uk" }
|
|
120
|
+
],
|
|
121
|
+
"metafieldDefinitionIdentifier": {
|
|
122
|
+
"key": "associations",
|
|
123
|
+
"namespace": "merch",
|
|
124
|
+
"ownerType": "PRODUCT"
|
|
125
|
+
}
|
|
126
|
+
}'
|
|
127
|
+
*/
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { ArgsWarden } = require('../utils');
|
|
2
2
|
const { credsValidator } = require('../validators');
|
|
3
|
-
const {
|
|
3
|
+
const { shopifyGet, shopifyGetter } = require('./shopifyGet');
|
|
4
4
|
|
|
5
5
|
const argsWarden = new ArgsWarden([
|
|
6
6
|
['credsPayload', credsValidator],
|
|
7
7
|
]);
|
|
8
8
|
|
|
9
9
|
const shopifyOrdersGet = async (
|
|
10
|
+
returnGetter, // Always bound
|
|
11
|
+
|
|
10
12
|
credsPayload,
|
|
11
13
|
{
|
|
12
|
-
|
|
13
|
-
after,
|
|
14
|
-
query,
|
|
15
|
-
apiVersion,
|
|
14
|
+
...getterOptions // e.g. limit
|
|
16
15
|
} = {},
|
|
17
16
|
) => {
|
|
18
17
|
|
|
@@ -23,53 +22,17 @@ const shopifyOrdersGet = async (
|
|
|
23
22
|
return rejectResponse;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
query OrdersGet(
|
|
32
|
-
$first: Int!,
|
|
33
|
-
$after: String,
|
|
34
|
-
$query: String
|
|
35
|
-
) {
|
|
36
|
-
orders(
|
|
37
|
-
first: $first,
|
|
38
|
-
after: $after,
|
|
39
|
-
query: $query
|
|
40
|
-
) {
|
|
41
|
-
edges {
|
|
42
|
-
node {
|
|
43
|
-
id
|
|
44
|
-
name
|
|
45
|
-
createdAt
|
|
46
|
-
displayFinancialStatus
|
|
47
|
-
displayFulfillmentStatus
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
`,
|
|
53
|
-
variables: {
|
|
54
|
-
first,
|
|
55
|
-
after,
|
|
56
|
-
query,
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
context: {
|
|
61
|
-
credsPayload,
|
|
62
|
-
apiVersion,
|
|
63
|
-
resultPath: 'data.orders',
|
|
25
|
+
const getterArgs = [
|
|
26
|
+
credsPayload,
|
|
27
|
+
'order',
|
|
28
|
+
{
|
|
29
|
+
...getterOptions,
|
|
64
30
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!response.ok) {
|
|
68
|
-
return response;
|
|
69
|
-
}
|
|
31
|
+
];
|
|
70
32
|
|
|
71
|
-
|
|
72
|
-
|
|
33
|
+
return returnGetter
|
|
34
|
+
? shopifyGetter(...getterArgs)
|
|
35
|
+
: shopifyGet(...getterArgs);
|
|
73
36
|
};
|
|
74
37
|
|
|
75
38
|
const funcApiConfig = {
|
|
@@ -77,6 +40,7 @@ const funcApiConfig = {
|
|
|
77
40
|
};
|
|
78
41
|
|
|
79
42
|
module.exports = {
|
|
80
|
-
shopifyOrdersGet,
|
|
43
|
+
shopifyOrdersGet: (...args) => shopifyOrdersGet(false, ...args),
|
|
44
|
+
shopifyOrdersGetter: (...args) => shopifyOrdersGet(true, ...args),
|
|
81
45
|
funcApiConfig,
|
|
82
46
|
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// https://shopify.dev/docs/api/admin-graphql/latest/queries/shopifyqlQuery
|
|
2
|
+
// Requires API version 2025-10+ and the read_reports access scope.
|
|
3
|
+
|
|
4
|
+
const { credsValidator } = require('../validators');
|
|
5
|
+
const { ArgsWarden } = require('../utils');
|
|
6
|
+
const { shopifyClient } = require('./shopify.utils');
|
|
7
|
+
|
|
8
|
+
const defaultAttrs = `
|
|
9
|
+
tableData {
|
|
10
|
+
columns {
|
|
11
|
+
name
|
|
12
|
+
dataType
|
|
13
|
+
displayName
|
|
14
|
+
}
|
|
15
|
+
rows
|
|
16
|
+
}
|
|
17
|
+
parseErrors
|
|
18
|
+
`.trim();
|
|
19
|
+
|
|
20
|
+
const argsWarden = new ArgsWarden([
|
|
21
|
+
['credsPayload', credsValidator],
|
|
22
|
+
['query'],
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const shopifyShopifyqlQuery = async (
|
|
26
|
+
credsPayload,
|
|
27
|
+
query,
|
|
28
|
+
{
|
|
29
|
+
apiVersion,
|
|
30
|
+
attrs = defaultAttrs,
|
|
31
|
+
} = {},
|
|
32
|
+
) => {
|
|
33
|
+
|
|
34
|
+
const rejectResponse = await argsWarden.responseIfRejectingArgs({
|
|
35
|
+
credsPayload,
|
|
36
|
+
query,
|
|
37
|
+
});
|
|
38
|
+
if (rejectResponse) {
|
|
39
|
+
return rejectResponse;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const response = await shopifyClient.fetch({
|
|
43
|
+
requestPayload: {
|
|
44
|
+
method: 'post',
|
|
45
|
+
body: {
|
|
46
|
+
query: `
|
|
47
|
+
query Shopifyql($query: String!) {
|
|
48
|
+
shopifyqlQuery(query: $query) {
|
|
49
|
+
${ attrs }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
`,
|
|
53
|
+
variables: {
|
|
54
|
+
query,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
context: {
|
|
59
|
+
credsPayload,
|
|
60
|
+
apiVersion,
|
|
61
|
+
resultPath: 'data.shopifyqlQuery',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!response?.ok) {
|
|
66
|
+
return response;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const parseErrors = response?.data?.parseErrors;
|
|
70
|
+
if (Array.isArray(parseErrors) && parseErrors.length) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: {
|
|
74
|
+
code: 'PARSE_ERROR',
|
|
75
|
+
message: parseErrors.join('; '),
|
|
76
|
+
details: parseErrors,
|
|
77
|
+
},
|
|
78
|
+
data: response.data,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return response;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const funcApiConfig = {
|
|
86
|
+
argsWarden,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
module.exports = {
|
|
90
|
+
shopifyShopifyqlQuery,
|
|
91
|
+
funcApiConfig,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
curl -X POST "http://localhost:8000/shopifyShopifyqlQuery" \
|
|
96
|
+
-H "Content-Type: application/json" \
|
|
97
|
+
-d '{
|
|
98
|
+
"credsPayload": { "credsPath": "shopify.au" },
|
|
99
|
+
"query": "FROM sales SHOW total_sales, orders TIMESERIES day SINCE -7d ORDER BY day ASC"
|
|
100
|
+
}'
|
|
101
|
+
|
|
102
|
+
Example command to get at last restock date
|
|
103
|
+
curl -X POST "http://localhost:8000/shopifyShopifyqlQuery" \
|
|
104
|
+
-H "Content-Type: application/json" \
|
|
105
|
+
-d '{
|
|
106
|
+
"credsPayload": { "credsPath": "shopify.au" },
|
|
107
|
+
"query": "FROM inventory_adjustment_history SHOW inventory_adjustment_change, product_variant_title, product_variant_sku, day GROUP BY product_variant_title, product_variant_sku, day HAVING inventory_adjustment_change > 10 ORDER BY day DESC LIMIT 10"
|
|
108
|
+
}'
|
|
109
|
+
*/
|
package/api/tagalys/docs.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Tagalys
|
|
2
2
|
|
|
3
|
-
- [
|
|
4
|
-
- [Support: custom platform integration](https://support.tagalys.com/how-can-i-install-tagalys-to-my-custom-built-ecommerce-platform)
|
|
5
|
-
- [Integration overview](https://support.tagalys.com/integration)
|
|
3
|
+
- [Storefront API (v2)](https://tagalys.notion.site/Storefront-API-v2-20eacdd38c2080d58d3fd0cf6f24e435)
|
|
6
4
|
|
|
7
|
-
Region-specific HTTPS origins with
|
|
5
|
+
Region-specific HTTPS origins (`https://api-r{n}.tagalys.com`) with REST-style JSON under `/v2`. Storefront reads use GET; analytics ingest uses `POST /v2/analytics/events` with a JSON body. Browser requests authenticate with the `shop_id` query param (the shop’s `*.myshopify.com` domain). Server-side integrations should also pass `storefront_api_key`.
|
|
6
|
+
|
|
7
|
+
Endpoints: `GET /v2/collections/:collection_id`, `GET /v2/search`, `GET /v2/search_suggestions`, `GET /v2/popular_searches`, `GET /v2/recommendations/:recommendation_id`, `POST /v2/analytics/events`. Collections and search support pagination (`page`, `per_page`, capped at 10,000 products), sorting (`sort`, `include[]=sort_options`), filtering (`filter[...]`, `include[]=filters`), and scope (`scope[...]`). Use `include[]=products` or `include[]=product_ids` to control product payloads. Optional request context on collections, search, and recommendations: `country`, `language`, `segment_tag`. Errors return JSON with `error.type`, `error.code`, and `error.message`.
|
package/api/utils.js
CHANGED
|
@@ -1180,6 +1180,10 @@ class Getter extends EventEmitter {
|
|
|
1180
1180
|
items = ensureArray(response);
|
|
1181
1181
|
}
|
|
1182
1182
|
|
|
1183
|
+
if (!items) {
|
|
1184
|
+
logDeep({ response });
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1183
1187
|
if (this.limit) {
|
|
1184
1188
|
const itemsLeft = this.limit - resultsCount;
|
|
1185
1189
|
items = items.slice(0, itemsLeft);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const http = require('http');
|
|
2
2
|
const { createRequire } = require('module');
|
|
3
|
-
const { respondJson, errorToReadable } = require('../server.utils');
|
|
3
|
+
const { respondJson, errorToReadable, listenOrOfferKillPort } = require('../server.utils');
|
|
4
4
|
const { getWorkspace, setWorkspace, loadWorkspaceEnv, toAbsolutePath } = require('../api/workspace');
|
|
5
5
|
const { getApiDirs, readCliFlag } = require('../cli');
|
|
6
6
|
const { loadHandlers } = require('../server');
|
|
@@ -137,7 +137,7 @@ const startHostingPreview = (options = {}) => {
|
|
|
137
137
|
const handlers = loadHostedRoutes(config);
|
|
138
138
|
const server = createHostedPreviewServer(handlers);
|
|
139
139
|
|
|
140
|
-
server
|
|
140
|
+
listenOrOfferKillPort(server, config.port, () => {
|
|
141
141
|
console.log(`Hosting preview running on port ${ config.port }`);
|
|
142
142
|
console.log(`Workspace: ${ config.workspace }`);
|
|
143
143
|
console.log('Hosted routes:');
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const http = require('http');
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
respondJson,
|
|
5
|
+
errorToReadable,
|
|
6
|
+
getRequestBody,
|
|
7
|
+
argsFromBody,
|
|
8
|
+
funcApi,
|
|
9
|
+
statusCodeFromResult,
|
|
10
|
+
listenOrOfferKillPort,
|
|
11
|
+
} = require('./server.utils');
|
|
4
12
|
const { getWorkspace, setWorkspace, loadWorkspaceEnv, toAbsolutePath } = require('./api/workspace');
|
|
5
13
|
const { getApiDirs, readCliFlag } = require('./cli');
|
|
6
14
|
const { getFuncApiConfig } = require('./hosting/hosting.utils');
|
|
@@ -208,7 +216,7 @@ const startServer = (options = {}) => {
|
|
|
208
216
|
handlers = loadHandlers(config);
|
|
209
217
|
server = createServer(handlers);
|
|
210
218
|
|
|
211
|
-
server
|
|
219
|
+
listenOrOfferKillPort(server, config.port, () => logStartup({
|
|
212
220
|
...config,
|
|
213
221
|
handlers,
|
|
214
222
|
}));
|
package/server.utils.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { execFileSync } = require('child_process');
|
|
2
|
+
const { logDeep, askQuestion } = require('./api/utils');
|
|
2
3
|
const { HOSTED } = require('./api/constants');
|
|
3
4
|
const { StringDecoder } = require('string_decoder');
|
|
4
5
|
|
|
@@ -277,6 +278,171 @@ const funcApi = (func, config = {}) => {
|
|
|
277
278
|
};
|
|
278
279
|
};
|
|
279
280
|
|
|
281
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
282
|
+
|
|
283
|
+
const isWatchMode = () => process.execArgv.some((arg) => arg.startsWith('--watch'));
|
|
284
|
+
|
|
285
|
+
const exitServer = (code = 0) => {
|
|
286
|
+
if (isWatchMode() && process.ppid && process.ppid !== 1) {
|
|
287
|
+
try {
|
|
288
|
+
process.kill(process.ppid, 'SIGTERM');
|
|
289
|
+
} catch (error) {
|
|
290
|
+
// Parent may already be gone.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
process.exit(code);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const findPortListeners = (port) => {
|
|
298
|
+
try {
|
|
299
|
+
const stdout = execFileSync('lsof', [
|
|
300
|
+
'-nP',
|
|
301
|
+
`-iTCP:${ port }`,
|
|
302
|
+
'-sTCP:LISTEN',
|
|
303
|
+
'-Fpc',
|
|
304
|
+
], {
|
|
305
|
+
encoding: 'utf8',
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const listeners = [];
|
|
309
|
+
let pid = null;
|
|
310
|
+
|
|
311
|
+
for (const line of stdout.split('\n')) {
|
|
312
|
+
if (line.startsWith('p')) {
|
|
313
|
+
pid = Number(line.slice(1));
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (line.startsWith('c') && pid) {
|
|
318
|
+
listeners.push({
|
|
319
|
+
pid,
|
|
320
|
+
command: line.slice(1),
|
|
321
|
+
});
|
|
322
|
+
pid = null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return listeners.filter((listener) => listener.pid && listener.pid !== process.pid);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const formatPortListeners = (listeners) => listeners
|
|
333
|
+
.map(({ pid, command }) => `${ command } (PID ${ pid })`)
|
|
334
|
+
.join(', ');
|
|
335
|
+
|
|
336
|
+
const killPortListeners = async (listeners) => {
|
|
337
|
+
for (const { pid } of listeners) {
|
|
338
|
+
try {
|
|
339
|
+
process.kill(pid, 'SIGTERM');
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error.code !== 'ESRCH') {
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
await wait(300);
|
|
348
|
+
|
|
349
|
+
for (const { pid } of listeners) {
|
|
350
|
+
try {
|
|
351
|
+
process.kill(pid, 0);
|
|
352
|
+
process.kill(pid, 'SIGKILL');
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (error.code !== 'ESRCH') {
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
await wait(200);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const listenOrOfferKillPort = (server, port, onListening) => {
|
|
364
|
+
let recovering = false;
|
|
365
|
+
|
|
366
|
+
const tryListen = () => {
|
|
367
|
+
server.listen(port);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const askRecoveryQuestions = async () => {
|
|
371
|
+
const shouldFind = await askQuestion('Look up the process using it? ');
|
|
372
|
+
|
|
373
|
+
if (shouldFind === 'n') {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (shouldFind !== '') {
|
|
378
|
+
console.error('not a valid input');
|
|
379
|
+
exitServer(1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const listeners = findPortListeners(port);
|
|
383
|
+
|
|
384
|
+
if (!listeners.length) {
|
|
385
|
+
console.error(`Could not find a process listening on port ${ port }.`);
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
console.log('Port in use by:', formatPortListeners(listeners));
|
|
390
|
+
|
|
391
|
+
const shouldKill = await askQuestion('Kill it and retry? ');
|
|
392
|
+
|
|
393
|
+
if (shouldKill === 'n') {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (shouldKill !== '') {
|
|
398
|
+
console.error('not a valid input');
|
|
399
|
+
exitServer(1);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
await killPortListeners(listeners);
|
|
403
|
+
return true;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
server.once('listening', onListening);
|
|
407
|
+
|
|
408
|
+
server.on('error', async (error) => {
|
|
409
|
+
if (error.code !== 'EADDRINUSE') {
|
|
410
|
+
console.error(error);
|
|
411
|
+
exitServer(1);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (recovering) {
|
|
415
|
+
console.error(error);
|
|
416
|
+
exitServer(1);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
console.error(error);
|
|
420
|
+
|
|
421
|
+
if (HOSTED) {
|
|
422
|
+
exitServer(1);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
recovering = true;
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
const shouldRetry = await askRecoveryQuestions();
|
|
429
|
+
|
|
430
|
+
if (!shouldRetry) {
|
|
431
|
+
exitServer(0);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
tryListen();
|
|
435
|
+
} catch (recoveryError) {
|
|
436
|
+
console.error(recoveryError);
|
|
437
|
+
exitServer(1);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
tryListen();
|
|
442
|
+
|
|
443
|
+
return server;
|
|
444
|
+
};
|
|
445
|
+
|
|
280
446
|
module.exports = {
|
|
281
447
|
respondJson,
|
|
282
448
|
errorToReadable,
|
|
@@ -285,4 +451,6 @@ module.exports = {
|
|
|
285
451
|
wrapFunction,
|
|
286
452
|
statusCodeFromResult,
|
|
287
453
|
funcApi,
|
|
454
|
+
findPortListeners,
|
|
455
|
+
listenOrOfferKillPort,
|
|
288
456
|
};
|