tiendu 0.4.0 → 0.6.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/README.md +56 -13
- package/bin/tiendu.js +43 -16
- package/bin/tiendu.mjs +1 -136
- package/lib/api.mjs +82 -30
- package/lib/archive.mjs +30 -0
- package/lib/assets.mjs +245 -0
- package/lib/build.mjs +299 -41
- package/lib/dev.mjs +234 -144
- package/lib/fs-utils.mjs +35 -0
- package/lib/local-preview.mjs +393 -0
- package/lib/postcss.mjs +166 -0
- package/lib/preview.mjs +279 -73
- package/lib/publish.mjs +32 -17
- package/lib/pull.mjs +37 -12
- package/lib/push.mjs +60 -57
- package/lib/retry.mjs +69 -0
- package/package.json +2 -2
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_PORT = 9292;
|
|
5
|
+
const MAX_PORT_ATTEMPTS = 20;
|
|
6
|
+
const MAX_SSE_CLIENTS = 20;
|
|
7
|
+
const RELOAD_DEBOUNCE_MS = 150;
|
|
8
|
+
const HEARTBEAT_INTERVAL_MS = 15_000;
|
|
9
|
+
const PROXY_TIMEOUT_MS = 30_000;
|
|
10
|
+
const MAX_PROXY_REQUEST_BODY_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
const LIVE_RELOAD_PATH = "/__tiendu__/livereload.js";
|
|
13
|
+
const EVENTS_PATH = "/__tiendu__/events";
|
|
14
|
+
|
|
15
|
+
const LIVE_RELOAD_SCRIPT = `const source = new EventSource(${JSON.stringify(EVENTS_PATH)});
|
|
16
|
+
let reloadTimer = null;
|
|
17
|
+
|
|
18
|
+
source.addEventListener("reload", () => {
|
|
19
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
20
|
+
reloadTimer = setTimeout(() => window.location.reload(), 60);
|
|
21
|
+
});
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
25
|
+
"connection",
|
|
26
|
+
"keep-alive",
|
|
27
|
+
"proxy-authenticate",
|
|
28
|
+
"proxy-authorization",
|
|
29
|
+
"te",
|
|
30
|
+
"trailer",
|
|
31
|
+
"transfer-encoding",
|
|
32
|
+
"upgrade",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const readRequestBody = async (request) => {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
let totalBytes = 0;
|
|
38
|
+
|
|
39
|
+
for await (const chunk of request) {
|
|
40
|
+
totalBytes += chunk.length;
|
|
41
|
+
if (totalBytes > MAX_PROXY_REQUEST_BODY_BYTES) {
|
|
42
|
+
const error = new Error("Local preview request body is too large.");
|
|
43
|
+
error.statusCode = 413;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
chunks.push(chunk);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (chunks.length === 0) return undefined;
|
|
51
|
+
return Buffer.concat(chunks);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const createForwardHeaders = (request, previewHostname) => {
|
|
55
|
+
const headers = new Headers();
|
|
56
|
+
|
|
57
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
58
|
+
if (value == null) continue;
|
|
59
|
+
|
|
60
|
+
const normalizedName = name.toLowerCase();
|
|
61
|
+
if (
|
|
62
|
+
HOP_BY_HOP_HEADERS.has(normalizedName) ||
|
|
63
|
+
normalizedName === "host" ||
|
|
64
|
+
normalizedName === "origin" ||
|
|
65
|
+
normalizedName === "referer" ||
|
|
66
|
+
normalizedName === "content-length"
|
|
67
|
+
) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
for (const entry of value) {
|
|
73
|
+
headers.append(name, entry);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
headers.set(name, value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
headers.set("host", previewHostname);
|
|
82
|
+
headers.set("x-forwarded-host", previewHostname);
|
|
83
|
+
|
|
84
|
+
return headers;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const isHtmlResponse = (headers) =>
|
|
88
|
+
(headers.get("content-type") ?? "").toLowerCase().includes("text/html");
|
|
89
|
+
|
|
90
|
+
const isHtmlDocument = (html) => /<html\b|<!doctype\s+html/i.test(html);
|
|
91
|
+
|
|
92
|
+
const injectLiveReloadScript = (html) => {
|
|
93
|
+
if (html.includes(LIVE_RELOAD_PATH)) return html;
|
|
94
|
+
if (!isHtmlDocument(html)) return html;
|
|
95
|
+
|
|
96
|
+
const scriptTag = `<script type="module" src="${LIVE_RELOAD_PATH}"></script>`;
|
|
97
|
+
|
|
98
|
+
if (html.includes("</head>")) {
|
|
99
|
+
return html.replace("</head>", `${scriptTag}</head>`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (html.includes("</body>")) {
|
|
103
|
+
return html.replace("</body>", `${scriptTag}</body>`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return `${html}${scriptTag}`;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const rewriteSetCookie = (cookieValue) =>
|
|
110
|
+
cookieValue
|
|
111
|
+
.replace(/;\s*Secure/gi, "")
|
|
112
|
+
.replace(/;\s*Domain=[^;]+/gi, "");
|
|
113
|
+
|
|
114
|
+
const rewriteLocationHeader = (locationValue, localOrigin, previewOrigin, upstreamOrigin) => {
|
|
115
|
+
if (!locationValue) return null;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const locationUrl = new URL(locationValue, previewOrigin);
|
|
119
|
+
if (
|
|
120
|
+
locationUrl.origin === previewOrigin.origin ||
|
|
121
|
+
locationUrl.origin === upstreamOrigin.origin
|
|
122
|
+
) {
|
|
123
|
+
return `${localOrigin.origin}${locationUrl.pathname}${locationUrl.search}${locationUrl.hash}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return locationValue;
|
|
127
|
+
} catch {
|
|
128
|
+
return locationValue;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const writeResponseHeaders = (response, serverResponse, context) => {
|
|
133
|
+
const { localOrigin, previewOrigin, upstreamOrigin } = context;
|
|
134
|
+
|
|
135
|
+
for (const [name, value] of response.headers) {
|
|
136
|
+
const normalizedName = name.toLowerCase();
|
|
137
|
+
if (
|
|
138
|
+
HOP_BY_HOP_HEADERS.has(normalizedName) ||
|
|
139
|
+
normalizedName === "content-length" ||
|
|
140
|
+
normalizedName === "content-encoding" ||
|
|
141
|
+
normalizedName === "set-cookie"
|
|
142
|
+
) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (normalizedName === "location") {
|
|
147
|
+
const rewritten = rewriteLocationHeader(
|
|
148
|
+
value,
|
|
149
|
+
localOrigin,
|
|
150
|
+
previewOrigin,
|
|
151
|
+
upstreamOrigin,
|
|
152
|
+
);
|
|
153
|
+
if (rewritten) serverResponse.setHeader(name, rewritten);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
serverResponse.setHeader(name, value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const setCookies = response.headers.getSetCookie?.() ?? [];
|
|
161
|
+
if (setCookies.length > 0) {
|
|
162
|
+
serverResponse.setHeader(
|
|
163
|
+
"set-cookie",
|
|
164
|
+
setCookies.map(rewriteSetCookie),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const listenOnAvailablePort = (server, preferredPort) =>
|
|
170
|
+
new Promise((resolve, reject) => {
|
|
171
|
+
let currentPort = preferredPort;
|
|
172
|
+
|
|
173
|
+
const tryListen = () => {
|
|
174
|
+
const onError = (error) => {
|
|
175
|
+
server.off("listening", onListening);
|
|
176
|
+
|
|
177
|
+
if (error?.code === "EADDRINUSE" && currentPort < preferredPort + MAX_PORT_ATTEMPTS) {
|
|
178
|
+
currentPort += 1;
|
|
179
|
+
tryListen();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
reject(error);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const onListening = () => {
|
|
187
|
+
server.off("error", onError);
|
|
188
|
+
const address = server.address();
|
|
189
|
+
if (!address || typeof address === "string") {
|
|
190
|
+
reject(new Error("Could not determine local preview port."));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
resolve(address.port);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
server.once("error", onError);
|
|
198
|
+
server.once("listening", onListening);
|
|
199
|
+
server.listen(currentPort, "localhost");
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
tryListen();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
export const startLocalPreviewServer = async ({
|
|
206
|
+
apiBaseUrl,
|
|
207
|
+
previewHostname,
|
|
208
|
+
port = DEFAULT_PORT,
|
|
209
|
+
}) => {
|
|
210
|
+
const upstreamOrigin = new URL(apiBaseUrl);
|
|
211
|
+
const previewOrigin = new URL(`${upstreamOrigin.protocol}//${previewHostname}`);
|
|
212
|
+
const sseClients = new Set();
|
|
213
|
+
const sockets = new Set();
|
|
214
|
+
const upstreamRequests = new Set();
|
|
215
|
+
let reloadTimer = null;
|
|
216
|
+
let closed = false;
|
|
217
|
+
let closePromise = null;
|
|
218
|
+
|
|
219
|
+
const server = createServer(async (request, response) => {
|
|
220
|
+
if (!request.url) {
|
|
221
|
+
response.writeHead(400);
|
|
222
|
+
response.end("Missing request URL");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const localOrigin = new URL(`http://${request.headers.host ?? `127.0.0.1:${port}`}`);
|
|
227
|
+
const requestUrl = new URL(request.url, localOrigin);
|
|
228
|
+
|
|
229
|
+
if (requestUrl.pathname === LIVE_RELOAD_PATH) {
|
|
230
|
+
response.writeHead(200, {
|
|
231
|
+
"content-type": "application/javascript; charset=utf-8",
|
|
232
|
+
"cache-control": "no-store",
|
|
233
|
+
});
|
|
234
|
+
response.end(LIVE_RELOAD_SCRIPT);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (requestUrl.pathname === EVENTS_PATH) {
|
|
239
|
+
response.writeHead(200, {
|
|
240
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
241
|
+
"cache-control": "no-store",
|
|
242
|
+
connection: "keep-alive",
|
|
243
|
+
});
|
|
244
|
+
response.write("event: connected\ndata: ok\n\n");
|
|
245
|
+
|
|
246
|
+
if (sseClients.size >= MAX_SSE_CLIENTS) {
|
|
247
|
+
const oldestClient = sseClients.values().next().value;
|
|
248
|
+
oldestClient?.end();
|
|
249
|
+
if (oldestClient) {
|
|
250
|
+
sseClients.delete(oldestClient);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
sseClients.add(response);
|
|
255
|
+
|
|
256
|
+
request.on("close", () => {
|
|
257
|
+
sseClients.delete(response);
|
|
258
|
+
});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const targetUrl = new URL(requestUrl.pathname + requestUrl.search, upstreamOrigin);
|
|
263
|
+
const upstreamRequest = new AbortController();
|
|
264
|
+
upstreamRequests.add(upstreamRequest);
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const body = await readRequestBody(request);
|
|
268
|
+
const upstreamResponse = await fetch(targetUrl, {
|
|
269
|
+
method: request.method,
|
|
270
|
+
headers: createForwardHeaders(request, previewHostname),
|
|
271
|
+
body,
|
|
272
|
+
redirect: "manual",
|
|
273
|
+
signal: AbortSignal.any([
|
|
274
|
+
AbortSignal.timeout(PROXY_TIMEOUT_MS),
|
|
275
|
+
upstreamRequest.signal,
|
|
276
|
+
]),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (isHtmlResponse(upstreamResponse.headers)) {
|
|
280
|
+
if (closed || response.destroyed) return;
|
|
281
|
+
const html = injectLiveReloadScript(await upstreamResponse.text());
|
|
282
|
+
writeResponseHeaders(upstreamResponse, response, {
|
|
283
|
+
localOrigin,
|
|
284
|
+
previewOrigin,
|
|
285
|
+
upstreamOrigin,
|
|
286
|
+
});
|
|
287
|
+
response.statusCode = upstreamResponse.status;
|
|
288
|
+
response.setHeader("cache-control", "no-store");
|
|
289
|
+
response.setHeader("content-length", Buffer.byteLength(html, "utf-8"));
|
|
290
|
+
response.end(html);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
writeResponseHeaders(upstreamResponse, response, {
|
|
295
|
+
localOrigin,
|
|
296
|
+
previewOrigin,
|
|
297
|
+
upstreamOrigin,
|
|
298
|
+
});
|
|
299
|
+
if (closed || response.destroyed) return;
|
|
300
|
+
response.statusCode = upstreamResponse.status;
|
|
301
|
+
|
|
302
|
+
if (!upstreamResponse.body) {
|
|
303
|
+
response.end();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const proxyStream = Readable.fromWeb(upstreamResponse.body);
|
|
308
|
+
proxyStream.on("error", (error) => {
|
|
309
|
+
console.warn(`Local preview proxy stream error: ${error.message}`);
|
|
310
|
+
response.destroy(error);
|
|
311
|
+
});
|
|
312
|
+
proxyStream.pipe(response);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (response.destroyed || response.writableEnded) return;
|
|
315
|
+
|
|
316
|
+
const wasAbort = error?.name === "AbortError" || error?.name === "TimeoutError";
|
|
317
|
+
if (closed && wasAbort) {
|
|
318
|
+
response.destroy();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const statusCode = error.statusCode ?? (error?.name === "TimeoutError" ? 504 : 502);
|
|
323
|
+
response.writeHead(statusCode, { "content-type": "text/plain; charset=utf-8" });
|
|
324
|
+
response.end(`Local preview proxy error: ${error.message}`);
|
|
325
|
+
} finally {
|
|
326
|
+
upstreamRequests.delete(upstreamRequest);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
server.on("connection", (socket) => {
|
|
331
|
+
sockets.add(socket);
|
|
332
|
+
socket.on("close", () => {
|
|
333
|
+
sockets.delete(socket);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const heartbeat = setInterval(() => {
|
|
338
|
+
for (const client of sseClients) {
|
|
339
|
+
client.write(": ping\n\n");
|
|
340
|
+
}
|
|
341
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
342
|
+
|
|
343
|
+
const boundPort = await listenOnAvailablePort(server, port);
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
url: `http://localhost:${boundPort}/`,
|
|
347
|
+
notifyReload() {
|
|
348
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
349
|
+
|
|
350
|
+
reloadTimer = setTimeout(() => {
|
|
351
|
+
reloadTimer = null;
|
|
352
|
+
for (const client of sseClients) {
|
|
353
|
+
client.write("event: reload\ndata: now\n\n");
|
|
354
|
+
}
|
|
355
|
+
}, RELOAD_DEBOUNCE_MS);
|
|
356
|
+
},
|
|
357
|
+
async close() {
|
|
358
|
+
if (closePromise) return closePromise;
|
|
359
|
+
closed = true;
|
|
360
|
+
|
|
361
|
+
closePromise = new Promise((resolve, reject) => {
|
|
362
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
363
|
+
clearInterval(heartbeat);
|
|
364
|
+
|
|
365
|
+
for (const client of sseClients) {
|
|
366
|
+
client.end();
|
|
367
|
+
}
|
|
368
|
+
sseClients.clear();
|
|
369
|
+
|
|
370
|
+
for (const upstreamRequest of upstreamRequests) {
|
|
371
|
+
upstreamRequest.abort();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
server.close((error) => {
|
|
375
|
+
if (error && error.code !== "ERR_SERVER_NOT_RUNNING") {
|
|
376
|
+
reject(error);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
resolve();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
server.closeIdleConnections?.();
|
|
384
|
+
server.closeAllConnections?.();
|
|
385
|
+
for (const socket of sockets) {
|
|
386
|
+
socket.destroy();
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
return closePromise;
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
};
|
package/lib/postcss.mjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { fileExists } from "./fs-utils.mjs";
|
|
6
|
+
import { rewriteCssAssetUrls } from "./assets.mjs";
|
|
7
|
+
|
|
8
|
+
const POSTCSS_CONFIG_FILES = [
|
|
9
|
+
"postcss.config.mjs",
|
|
10
|
+
"postcss.config.js",
|
|
11
|
+
"postcss.config.cjs",
|
|
12
|
+
"postcss.config.json",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const getProjectRequire = (rootDir) =>
|
|
16
|
+
createRequire(path.join(rootDir, "package.json"));
|
|
17
|
+
|
|
18
|
+
const unwrapModule = (moduleNamespace) => moduleNamespace?.default ?? moduleNamespace;
|
|
19
|
+
|
|
20
|
+
const importProjectModule = async (rootDir, specifier) => {
|
|
21
|
+
try {
|
|
22
|
+
const requireFromProject = getProjectRequire(rootDir);
|
|
23
|
+
const resolvedPath = requireFromProject.resolve(specifier);
|
|
24
|
+
return await import(pathToFileURL(resolvedPath).href);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const instantiatePlugin = async (rootDir, pluginEntry, pluginOptions) => {
|
|
31
|
+
if (!pluginEntry) return null;
|
|
32
|
+
|
|
33
|
+
if (typeof pluginEntry === "string") {
|
|
34
|
+
const moduleNamespace = await importProjectModule(rootDir, pluginEntry);
|
|
35
|
+
if (!moduleNamespace) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Could not resolve PostCSS plugin "${pluginEntry}" from the theme project.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const pluginFactory = unwrapModule(moduleNamespace);
|
|
42
|
+
if (typeof pluginFactory === "function") {
|
|
43
|
+
return pluginOptions === undefined || pluginOptions === true
|
|
44
|
+
? pluginFactory()
|
|
45
|
+
: pluginFactory(pluginOptions);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return pluginFactory;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (Array.isArray(pluginEntry)) {
|
|
52
|
+
const [nestedEntry, nestedOptions] = pluginEntry;
|
|
53
|
+
return instantiatePlugin(rootDir, nestedEntry, nestedOptions);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof pluginEntry === "function") {
|
|
57
|
+
return pluginOptions === undefined
|
|
58
|
+
? pluginEntry
|
|
59
|
+
: pluginEntry(pluginOptions);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return pluginEntry;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const normalizePlugins = async (rootDir, plugins) => {
|
|
66
|
+
if (!plugins) return [];
|
|
67
|
+
|
|
68
|
+
if (Array.isArray(plugins)) {
|
|
69
|
+
const resolvedPlugins = [];
|
|
70
|
+
|
|
71
|
+
for (const pluginEntry of plugins) {
|
|
72
|
+
const plugin = await instantiatePlugin(rootDir, pluginEntry);
|
|
73
|
+
if (plugin) resolvedPlugins.push(plugin);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return resolvedPlugins;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof plugins === "object") {
|
|
80
|
+
const resolvedPlugins = [];
|
|
81
|
+
|
|
82
|
+
for (const [pluginName, pluginOptions] of Object.entries(plugins)) {
|
|
83
|
+
if (!pluginOptions) continue;
|
|
84
|
+
const plugin = await instantiatePlugin(rootDir, pluginName, pluginOptions);
|
|
85
|
+
if (plugin) resolvedPlugins.push(plugin);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return resolvedPlugins;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const loadPostcssConfig = async (rootDir) => {
|
|
95
|
+
for (const configFile of POSTCSS_CONFIG_FILES) {
|
|
96
|
+
const configPath = path.join(rootDir, configFile);
|
|
97
|
+
if (!(await fileExists(configPath))) continue;
|
|
98
|
+
|
|
99
|
+
if (configFile.endsWith(".json")) {
|
|
100
|
+
const raw = await readFile(configPath, "utf-8");
|
|
101
|
+
return JSON.parse(raw);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const moduleNamespace = await import(pathToFileURL(configPath).href);
|
|
105
|
+
let config = unwrapModule(moduleNamespace);
|
|
106
|
+
|
|
107
|
+
if (typeof config === "function") {
|
|
108
|
+
config = await config({ env: process.env.NODE_ENV ?? "development" });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return config ?? null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const createCssPostCssPlugin = async (rootDir, { sourceDirs } = {}) => {
|
|
118
|
+
const postcssModule = await importProjectModule(rootDir, "postcss");
|
|
119
|
+
const postcss = postcssModule ? unwrapModule(postcssModule) : null;
|
|
120
|
+
const config = postcssModule ? await loadPostcssConfig(rootDir) : null;
|
|
121
|
+
|
|
122
|
+
let plugins = [];
|
|
123
|
+
if (config?.plugins) {
|
|
124
|
+
plugins = await normalizePlugins(rootDir, config.plugins);
|
|
125
|
+
} else if (postcssModule) {
|
|
126
|
+
const tailwindModule = await importProjectModule(rootDir, "@tailwindcss/postcss");
|
|
127
|
+
if (tailwindModule) {
|
|
128
|
+
const tailwindPluginFactory = unwrapModule(tailwindModule);
|
|
129
|
+
plugins = [
|
|
130
|
+
typeof tailwindPluginFactory === "function"
|
|
131
|
+
? tailwindPluginFactory()
|
|
132
|
+
: tailwindPluginFactory,
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
name: "tiendu-postcss",
|
|
139
|
+
setup(build) {
|
|
140
|
+
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
|
141
|
+
const source = await readFile(args.path, "utf-8");
|
|
142
|
+
const rewrittenSource = await rewriteCssAssetUrls(source, args.path, rootDir, sourceDirs);
|
|
143
|
+
|
|
144
|
+
if (!postcss || plugins.length === 0) {
|
|
145
|
+
return {
|
|
146
|
+
contents: rewrittenSource,
|
|
147
|
+
loader: "css",
|
|
148
|
+
resolveDir: path.dirname(args.path),
|
|
149
|
+
watchFiles: [args.path],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = await postcss(plugins).process(rewrittenSource, {
|
|
154
|
+
from: args.path,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
contents: result.css,
|
|
159
|
+
loader: "css",
|
|
160
|
+
resolveDir: path.dirname(args.path),
|
|
161
|
+
watchFiles: [args.path],
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
};
|