vite-plugin-shopify-easel 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +1363 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1363 @@
|
|
|
1
|
+
// src/build.ts
|
|
2
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync, rmSync as rmSync4 } from "fs";
|
|
3
|
+
import { join as join4 } from "path";
|
|
4
|
+
|
|
5
|
+
// src/validation.ts
|
|
6
|
+
var SAFE_OUTPUT_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
|
+
var SAFE_ASSET_FILENAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
8
|
+
var SAFE_LIQUID_FILENAME = /^[a-z0-9][a-z0-9_-]*\.liquid$/;
|
|
9
|
+
|
|
10
|
+
// src/virtual.ts
|
|
11
|
+
var INTERNAL_PREFIX = "\0easel:";
|
|
12
|
+
var PUBLIC_PREFIX = "/@easel/";
|
|
13
|
+
function virtualBundleId(name) {
|
|
14
|
+
return `${INTERNAL_PREFIX}${name}`;
|
|
15
|
+
}
|
|
16
|
+
function easelBundleModules(getBundles) {
|
|
17
|
+
return {
|
|
18
|
+
name: "easel:bundle-modules",
|
|
19
|
+
enforce: "pre",
|
|
20
|
+
resolveId(id) {
|
|
21
|
+
const bundles = getBundles();
|
|
22
|
+
if (id.startsWith(PUBLIC_PREFIX)) {
|
|
23
|
+
const name = id.slice(PUBLIC_PREFIX.length);
|
|
24
|
+
if (bundles.some((bundle) => bundle.name === name)) {
|
|
25
|
+
return virtualBundleId(name);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (id.startsWith(INTERNAL_PREFIX)) return id;
|
|
29
|
+
const normalizedId = normalizeModulePath(id);
|
|
30
|
+
const isBundleInput = bundles.flatMap(bundleInputs).some((path) => normalizeModulePath(path) === normalizedId);
|
|
31
|
+
return isBundleInput ? { id: normalizedId, moduleSideEffects: true } : void 0;
|
|
32
|
+
},
|
|
33
|
+
load(id) {
|
|
34
|
+
if (!id.startsWith(INTERNAL_PREFIX)) return void 0;
|
|
35
|
+
const name = id.slice(INTERNAL_PREFIX.length);
|
|
36
|
+
const bundle = getBundles().find((candidate) => candidate.name === name);
|
|
37
|
+
if (bundle === void 0) {
|
|
38
|
+
throw new Error(`[easel] unknown virtual bundle "${name}"`);
|
|
39
|
+
}
|
|
40
|
+
return `${bundleInputs(bundle).map((path) => `import ${JSON.stringify(path)};`).join("\n")}
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function bundleInputs(bundle) {
|
|
46
|
+
return [bundle.script, bundle.style].filter(
|
|
47
|
+
(path) => path !== void 0
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
function normalizeModulePath(path) {
|
|
51
|
+
return path.replaceAll("\\", "/");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/liquid.ts
|
|
55
|
+
var MAX_LIQUID_BYTES = 2e5;
|
|
56
|
+
function renderProductionLiquid(manifest) {
|
|
57
|
+
const entries = validatedManifestEntries(manifest);
|
|
58
|
+
const defaultEntry = entries[0]?.[0];
|
|
59
|
+
if (defaultEntry === void 0) {
|
|
60
|
+
throw new Error("[easel] cannot render Liquid without an entry");
|
|
61
|
+
}
|
|
62
|
+
const lines = loaderHeader("Generated asset loader managed by Easel.", defaultEntry);
|
|
63
|
+
for (const [name, entry] of entries) {
|
|
64
|
+
lines.push(` {% when ${liquidEntryLiteral(name)} %}`);
|
|
65
|
+
for (const style of entry.styles) {
|
|
66
|
+
lines.push(
|
|
67
|
+
` {{ ${liquidAssetLiteral(style, "stylesheet", ".css")} | asset_url | stylesheet_tag }}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
for (const imported of entry.imports) {
|
|
71
|
+
lines.push(
|
|
72
|
+
` <link rel="modulepreload" href="{{ ${liquidAssetLiteral(imported, "module import", ".js")} | asset_url }}">`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (entry.script !== void 0) {
|
|
76
|
+
lines.push(
|
|
77
|
+
` <script src="{{ ${liquidAssetLiteral(entry.script, "entry script", ".js")} | asset_url }}" type="module" defer></script>`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return finishLoader(lines);
|
|
82
|
+
}
|
|
83
|
+
function renderDevelopmentLiquid(names, origin) {
|
|
84
|
+
const entries = validatedEntryNames(names);
|
|
85
|
+
const defaultEntry = entries[0];
|
|
86
|
+
if (defaultEntry === void 0) {
|
|
87
|
+
throw new Error("[easel] cannot render Liquid without an entry");
|
|
88
|
+
}
|
|
89
|
+
const safeOrigin = normalizeOrigin(origin);
|
|
90
|
+
const lines = loaderHeader(
|
|
91
|
+
"Generated development asset loader managed by Easel.",
|
|
92
|
+
defaultEntry,
|
|
93
|
+
[`<script src="${safeOrigin}/@vite/client" type="module"></script>`]
|
|
94
|
+
);
|
|
95
|
+
for (const name of entries) {
|
|
96
|
+
lines.push(` {% when ${liquidEntryLiteral(name)} %}`);
|
|
97
|
+
lines.push(
|
|
98
|
+
` <script src="${safeOrigin}${PUBLIC_PREFIX}${encodeURIComponent(name)}" type="module"></script>`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return finishLoader(lines);
|
|
102
|
+
}
|
|
103
|
+
function loaderHeader(description, defaultEntry, preamble = []) {
|
|
104
|
+
return [
|
|
105
|
+
"{% doc %}",
|
|
106
|
+
description,
|
|
107
|
+
"@param {string} [entry] - Named Easel bundle to render.",
|
|
108
|
+
"{% enddoc %}",
|
|
109
|
+
"{% liquid",
|
|
110
|
+
` assign easel_entry = entry | default: ${liquidEntryLiteral(defaultEntry)}`,
|
|
111
|
+
"%}",
|
|
112
|
+
...preamble,
|
|
113
|
+
"{% case easel_entry %}"
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
function finishLoader(lines) {
|
|
117
|
+
lines.push(
|
|
118
|
+
" {% else %}",
|
|
119
|
+
' <!-- [easel] Unknown bundle "{{ easel_entry | escape }}". -->',
|
|
120
|
+
"{% endcase %}",
|
|
121
|
+
""
|
|
122
|
+
);
|
|
123
|
+
return assertLiquidSize(lines.join("\n"));
|
|
124
|
+
}
|
|
125
|
+
function validatedManifestEntries(manifest) {
|
|
126
|
+
if (manifest.schemaVersion !== 1) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`[easel] unsupported manifest schema version: ${JSON.stringify(manifest.schemaVersion)}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const entries = Object.entries(manifest.entries);
|
|
132
|
+
validatedEntryNames(entries.map(([name]) => name));
|
|
133
|
+
return entries;
|
|
134
|
+
}
|
|
135
|
+
function validatedEntryNames(names) {
|
|
136
|
+
const seen = /* @__PURE__ */ new Set();
|
|
137
|
+
for (const name of names) {
|
|
138
|
+
if (!SAFE_OUTPUT_NAME.test(name)) {
|
|
139
|
+
throw new Error(`[easel] cannot render unsafe Liquid bundle name: ${name}`);
|
|
140
|
+
}
|
|
141
|
+
if (seen.has(name)) {
|
|
142
|
+
throw new Error(`[easel] cannot render duplicate Liquid bundle name: ${name}`);
|
|
143
|
+
}
|
|
144
|
+
seen.add(name);
|
|
145
|
+
}
|
|
146
|
+
return names;
|
|
147
|
+
}
|
|
148
|
+
function liquidEntryLiteral(name) {
|
|
149
|
+
if (!SAFE_OUTPUT_NAME.test(name)) {
|
|
150
|
+
throw new Error(`[easel] cannot render unsafe Liquid bundle name: ${name}`);
|
|
151
|
+
}
|
|
152
|
+
return `'${name}'`;
|
|
153
|
+
}
|
|
154
|
+
function liquidAssetLiteral(filename, role, extension) {
|
|
155
|
+
if (!SAFE_ASSET_FILENAME.test(filename) || !filename.endsWith(extension)) {
|
|
156
|
+
throw new Error(`[easel] cannot render unsafe ${role} filename: ${filename}`);
|
|
157
|
+
}
|
|
158
|
+
return `'${filename}'`;
|
|
159
|
+
}
|
|
160
|
+
function normalizeOrigin(origin) {
|
|
161
|
+
let url;
|
|
162
|
+
try {
|
|
163
|
+
url = new URL(origin);
|
|
164
|
+
} catch {
|
|
165
|
+
throw new Error(`[easel] invalid Vite development origin: ${origin}`);
|
|
166
|
+
}
|
|
167
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
168
|
+
throw new Error(`[easel] Vite development origin must use HTTP or HTTPS: ${origin}`);
|
|
169
|
+
}
|
|
170
|
+
if (url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`[easel] Vite development origin cannot contain credentials, a path, a query, or a fragment: ${origin}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return url.origin;
|
|
176
|
+
}
|
|
177
|
+
function assertLiquidSize(content) {
|
|
178
|
+
const size = Buffer.byteLength(content);
|
|
179
|
+
if (size > MAX_LIQUID_BYTES) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`[easel] generated Liquid is ${size} bytes; reduce the number of bundles below ${MAX_LIQUID_BYTES} bytes`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return content;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/manifest.ts
|
|
188
|
+
function createEaselManifest(viteManifest, bundles, emittedFiles = []) {
|
|
189
|
+
const entries = {};
|
|
190
|
+
const generated = new Set(emittedFiles);
|
|
191
|
+
for (const bundle of bundles) {
|
|
192
|
+
const chunk = findEntry(viteManifest, bundle.name);
|
|
193
|
+
const { styles, imports } = collectStaticDependencies(viteManifest, chunk);
|
|
194
|
+
if (bundle.script === void 0) {
|
|
195
|
+
generated.delete(chunk.file);
|
|
196
|
+
} else {
|
|
197
|
+
generated.add(chunk.file);
|
|
198
|
+
}
|
|
199
|
+
for (const style of styles) generated.add(style);
|
|
200
|
+
for (const imported of imports) generated.add(imported);
|
|
201
|
+
collectGeneratedFiles(viteManifest, chunk, generated);
|
|
202
|
+
entries[bundle.name] = {
|
|
203
|
+
...bundle.script === void 0 ? {} : { script: chunk.file },
|
|
204
|
+
styles,
|
|
205
|
+
imports
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
schemaVersion: 1,
|
|
210
|
+
entries,
|
|
211
|
+
generated: [...generated].sort()
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function findEntry(manifest, name) {
|
|
215
|
+
const chunk = Object.values(manifest).find(
|
|
216
|
+
(candidate) => candidate.isEntry === true && candidate.name === name
|
|
217
|
+
);
|
|
218
|
+
if (chunk === void 0) {
|
|
219
|
+
throw new Error(`[easel] Vite manifest does not contain bundle "${name}"`);
|
|
220
|
+
}
|
|
221
|
+
return chunk;
|
|
222
|
+
}
|
|
223
|
+
function collectStaticDependencies(manifest, entry) {
|
|
224
|
+
const styles = /* @__PURE__ */ new Set();
|
|
225
|
+
const imports = /* @__PURE__ */ new Set();
|
|
226
|
+
const visited = /* @__PURE__ */ new Set();
|
|
227
|
+
const visit = (chunk) => {
|
|
228
|
+
for (const key of chunk.imports ?? []) {
|
|
229
|
+
if (visited.has(key)) continue;
|
|
230
|
+
visited.add(key);
|
|
231
|
+
const imported = requiredChunk(manifest, key);
|
|
232
|
+
imports.add(imported.file);
|
|
233
|
+
visit(imported);
|
|
234
|
+
}
|
|
235
|
+
for (const style of chunk.css ?? []) styles.add(style);
|
|
236
|
+
};
|
|
237
|
+
visit(entry);
|
|
238
|
+
return { styles: [...styles], imports: [...imports] };
|
|
239
|
+
}
|
|
240
|
+
function collectGeneratedFiles(manifest, entry, files) {
|
|
241
|
+
const visited = /* @__PURE__ */ new Set();
|
|
242
|
+
const visit = (chunk) => {
|
|
243
|
+
for (const asset of chunk.assets ?? []) files.add(asset);
|
|
244
|
+
for (const style of chunk.css ?? []) files.add(style);
|
|
245
|
+
for (const key of [...chunk.imports ?? [], ...chunk.dynamicImports ?? []]) {
|
|
246
|
+
if (visited.has(key)) continue;
|
|
247
|
+
visited.add(key);
|
|
248
|
+
const imported = requiredChunk(manifest, key);
|
|
249
|
+
files.add(imported.file);
|
|
250
|
+
visit(imported);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
visit(entry);
|
|
254
|
+
}
|
|
255
|
+
function requiredChunk(manifest, key) {
|
|
256
|
+
const chunk = manifest[key];
|
|
257
|
+
if (chunk === void 0) {
|
|
258
|
+
throw new Error(`[easel] Vite manifest import is missing: ${key}`);
|
|
259
|
+
}
|
|
260
|
+
return chunk;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/output.ts
|
|
264
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, rmSync as rmSync3 } from "fs";
|
|
265
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
266
|
+
|
|
267
|
+
// src/lock.ts
|
|
268
|
+
import { randomUUID } from "crypto";
|
|
269
|
+
import {
|
|
270
|
+
lstatSync,
|
|
271
|
+
mkdirSync,
|
|
272
|
+
readFileSync,
|
|
273
|
+
rmSync,
|
|
274
|
+
statSync,
|
|
275
|
+
writeFileSync
|
|
276
|
+
} from "fs";
|
|
277
|
+
import { dirname, join } from "path";
|
|
278
|
+
function acquireCommitLock(lockPath) {
|
|
279
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
280
|
+
const token = randomUUID();
|
|
281
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
282
|
+
try {
|
|
283
|
+
createLock(lockPath, token);
|
|
284
|
+
return () => releaseLock(lockPath, token);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (!isAlreadyExistsError(error)) throw error;
|
|
287
|
+
if (!isStaleLock(lockPath)) {
|
|
288
|
+
throw new Error(`[easel] another build is publishing this theme: ${lockPath}`);
|
|
289
|
+
}
|
|
290
|
+
rmSync(lockPath, { recursive: true });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
throw new Error(`[easel] could not acquire the theme commit lock: ${lockPath}`);
|
|
294
|
+
}
|
|
295
|
+
function createLock(lockPath, token) {
|
|
296
|
+
mkdirSync(lockPath);
|
|
297
|
+
try {
|
|
298
|
+
writeFileSync(
|
|
299
|
+
join(lockPath, "owner.json"),
|
|
300
|
+
`${JSON.stringify({ pid: process.pid, token })}
|
|
301
|
+
`
|
|
302
|
+
);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
rmSync(lockPath, { recursive: true });
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function releaseLock(lockPath, token) {
|
|
309
|
+
if (readLockOwner(lockPath)?.token === token) {
|
|
310
|
+
rmSync(lockPath, { recursive: true });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function isStaleLock(lockPath) {
|
|
314
|
+
const metadata = lstatSync(lockPath);
|
|
315
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
316
|
+
throw new Error(`[easel] commit lock must be a real directory: ${lockPath}`);
|
|
317
|
+
}
|
|
318
|
+
const owner = readLockOwner(lockPath);
|
|
319
|
+
if (owner === void 0) {
|
|
320
|
+
return Date.now() - statSync(lockPath).mtimeMs > 3e4;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
process.kill(owner.pid, 0);
|
|
324
|
+
return false;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
return isNodeError(error) && error.code === "ESRCH";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function readLockOwner(lockPath) {
|
|
330
|
+
try {
|
|
331
|
+
const value = JSON.parse(
|
|
332
|
+
readFileSync(join(lockPath, "owner.json"), "utf8")
|
|
333
|
+
);
|
|
334
|
+
if (isLockOwner(value)) return value;
|
|
335
|
+
} catch {
|
|
336
|
+
return void 0;
|
|
337
|
+
}
|
|
338
|
+
return void 0;
|
|
339
|
+
}
|
|
340
|
+
function isLockOwner(value) {
|
|
341
|
+
return typeof value === "object" && value !== null && "pid" in value && typeof value.pid === "number" && Number.isInteger(value.pid) && value.pid > 0 && "token" in value && typeof value.token === "string" && value.token.length > 0;
|
|
342
|
+
}
|
|
343
|
+
function isAlreadyExistsError(error) {
|
|
344
|
+
return isNodeError(error) && error.code === "EEXIST";
|
|
345
|
+
}
|
|
346
|
+
function isNodeError(error) {
|
|
347
|
+
return error instanceof Error && "code" in error;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/ownership.ts
|
|
351
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
352
|
+
import { sep } from "path";
|
|
353
|
+
function readOwnershipLedger(options) {
|
|
354
|
+
const path = options.ledgerPath;
|
|
355
|
+
if (!existsSync(path)) return void 0;
|
|
356
|
+
let value;
|
|
357
|
+
try {
|
|
358
|
+
value = JSON.parse(readFileSync2(path, "utf8"));
|
|
359
|
+
} catch {
|
|
360
|
+
throw new Error(`[easel] ownership ledger is not valid JSON: ${path}`);
|
|
361
|
+
}
|
|
362
|
+
if (!isRecord(value) || value.themePath !== options.themePath) {
|
|
363
|
+
throw invalidLedger(options);
|
|
364
|
+
}
|
|
365
|
+
if (value.schemaVersion === 1 && isLegacyGeneratedList(value.generated)) {
|
|
366
|
+
return {
|
|
367
|
+
schemaVersion: 2,
|
|
368
|
+
themePath: options.themePath,
|
|
369
|
+
prefix: options.prefix,
|
|
370
|
+
liquidFilename: options.liquidFilename,
|
|
371
|
+
generated: value.generated
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (!isCurrentLedger(value)) throw invalidLedger(options);
|
|
375
|
+
return value;
|
|
376
|
+
}
|
|
377
|
+
function ownedAssetPath(options, file) {
|
|
378
|
+
return namespacedAssetPath(options.prefix, file);
|
|
379
|
+
}
|
|
380
|
+
function ownedLiquidPath(options) {
|
|
381
|
+
return `snippets/${options.liquidFilename}`;
|
|
382
|
+
}
|
|
383
|
+
function assertAssetWritable(destinationPath, relativePath, content, previousFiles) {
|
|
384
|
+
if (existsSync(destinationPath) && !previousFiles.has(relativePath) && !readFileSync2(destinationPath).equals(content)) {
|
|
385
|
+
throw new Error(
|
|
386
|
+
`[easel] refusing to overwrite a file Easel does not own: ${destinationPath}`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function assertLiquidWritable(absolutePath, relativePath, previousFiles) {
|
|
391
|
+
if (!existsSync(absolutePath) || previousFiles.has(relativePath)) return;
|
|
392
|
+
if (isGeneratedLiquid(readFileSync2(absolutePath, "utf8"))) return;
|
|
393
|
+
throw new Error(
|
|
394
|
+
`[easel] refusing to overwrite a Liquid file Easel does not own: ${absolutePath}`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
function isGeneratedLiquid(content) {
|
|
398
|
+
return content.startsWith("{% doc %}\nGenerated asset loader managed by Easel.\n") || isDevelopmentLiquid(content);
|
|
399
|
+
}
|
|
400
|
+
function isDevelopmentLiquid(content) {
|
|
401
|
+
return content.startsWith(
|
|
402
|
+
"{% doc %}\nGenerated development asset loader managed by Easel.\n"
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
function isCurrentLedger(value) {
|
|
406
|
+
if (value.schemaVersion !== 2 || typeof value.themePath !== "string" || typeof value.prefix !== "string" || typeof value.liquidFilename !== "string") {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
const namespace = value.prefix.slice(0, -1);
|
|
410
|
+
return value.prefix.endsWith("-") && SAFE_OUTPUT_NAME.test(namespace) && SAFE_LIQUID_FILENAME.test(value.liquidFilename) && isGeneratedList(value.generated, value.prefix, value.liquidFilename);
|
|
411
|
+
}
|
|
412
|
+
function isLegacyGeneratedList(value) {
|
|
413
|
+
if (!Array.isArray(value) || !value.every((file) => typeof file === "string")) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
const snippets = value.filter((file) => file.startsWith("snippets/"));
|
|
417
|
+
return snippets.length === 1 && SAFE_LIQUID_FILENAME.test(snippets[0]?.slice("snippets/".length) ?? "") && value.every(
|
|
418
|
+
(file) => /^assets\/[a-z0-9][a-z0-9._-]*$/.test(file) || file === snippets[0]
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
function isGeneratedList(value, prefix, liquidFilename) {
|
|
422
|
+
return Array.isArray(value) && value.every(
|
|
423
|
+
(file) => typeof file === "string" && isOwnedThemePath(file, prefix, liquidFilename)
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
function isOwnedThemePath(file, prefix, liquidFilename) {
|
|
427
|
+
if (file === `snippets/${liquidFilename}`) return true;
|
|
428
|
+
if (!file.startsWith("assets/")) return false;
|
|
429
|
+
try {
|
|
430
|
+
return namespacedAssetPath(prefix, file.slice("assets/".length)) === file;
|
|
431
|
+
} catch {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function namespacedAssetPath(prefix, file) {
|
|
436
|
+
const normalized = file.split(sep).join("/");
|
|
437
|
+
if (!SAFE_ASSET_FILENAME.test(normalized) || !normalized.startsWith(prefix) || normalized === prefix) {
|
|
438
|
+
throw new Error(`[easel] generated asset is outside Easel's namespace: ${file}`);
|
|
439
|
+
}
|
|
440
|
+
return `assets/${normalized}`;
|
|
441
|
+
}
|
|
442
|
+
function isRecord(value) {
|
|
443
|
+
return typeof value === "object" && value !== null;
|
|
444
|
+
}
|
|
445
|
+
function invalidLedger(options) {
|
|
446
|
+
return new Error(
|
|
447
|
+
`[easel] invalid ownership ledger for ${options.themePath}: ${options.ledgerPath}`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/transaction.ts
|
|
452
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
453
|
+
import {
|
|
454
|
+
existsSync as existsSync2,
|
|
455
|
+
lstatSync as lstatSync2,
|
|
456
|
+
mkdirSync as mkdirSync2,
|
|
457
|
+
readFileSync as readFileSync3,
|
|
458
|
+
renameSync,
|
|
459
|
+
rmSync as rmSync2,
|
|
460
|
+
writeFileSync as writeFileSync2
|
|
461
|
+
} from "fs";
|
|
462
|
+
import { dirname as dirname2, join as join2, relative, resolve, sep as sep2 } from "path";
|
|
463
|
+
function snapshotFiles(paths) {
|
|
464
|
+
return new Map(
|
|
465
|
+
[...new Set(paths)].map((path) => [
|
|
466
|
+
path,
|
|
467
|
+
existsSync2(path) ? readFileSync3(path) : void 0
|
|
468
|
+
])
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
function restoreSnapshots(snapshots) {
|
|
472
|
+
for (const [path, content] of snapshots) {
|
|
473
|
+
if (content === void 0) {
|
|
474
|
+
if (existsSync2(path)) rmSync2(path);
|
|
475
|
+
} else {
|
|
476
|
+
writeIfChanged(path, content);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function writeTransactionJournal(options, snapshots) {
|
|
481
|
+
const temporary = `${options.transactionPath}-${randomUUID2()}`;
|
|
482
|
+
mkdirSync2(temporary, { recursive: true });
|
|
483
|
+
try {
|
|
484
|
+
const entries = [];
|
|
485
|
+
let index = 0;
|
|
486
|
+
for (const [path, content] of snapshots) {
|
|
487
|
+
const location = journalLocation(options, path);
|
|
488
|
+
if (content === void 0) {
|
|
489
|
+
entries.push({ ...location, existed: false });
|
|
490
|
+
} else {
|
|
491
|
+
const blob = `${index}.bin`;
|
|
492
|
+
writeFileSync2(join2(temporary, blob), content);
|
|
493
|
+
entries.push({ ...location, existed: true, blob });
|
|
494
|
+
index += 1;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const journal = {
|
|
498
|
+
schemaVersion: 1,
|
|
499
|
+
themePath: options.themePath,
|
|
500
|
+
entries
|
|
501
|
+
};
|
|
502
|
+
writeFileSync2(
|
|
503
|
+
join2(temporary, "journal.json"),
|
|
504
|
+
`${JSON.stringify(journal, null, 2)}
|
|
505
|
+
`
|
|
506
|
+
);
|
|
507
|
+
renameSync(temporary, options.transactionPath);
|
|
508
|
+
} finally {
|
|
509
|
+
if (existsSync2(temporary)) rmSync2(temporary, { recursive: true });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function runFileTransaction(options, paths, action) {
|
|
513
|
+
const snapshots = snapshotFiles(paths);
|
|
514
|
+
writeTransactionJournal(options, snapshots);
|
|
515
|
+
try {
|
|
516
|
+
action();
|
|
517
|
+
rmSync2(options.transactionPath, { recursive: true });
|
|
518
|
+
} catch (error) {
|
|
519
|
+
try {
|
|
520
|
+
restoreSnapshots(snapshots);
|
|
521
|
+
rmSync2(options.transactionPath, { recursive: true, force: true });
|
|
522
|
+
} catch (rollbackError) {
|
|
523
|
+
throw new AggregateError(
|
|
524
|
+
[error, rollbackError],
|
|
525
|
+
"[easel] output commit failed and could not be rolled back completely"
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function recoverInterruptedCommit(options) {
|
|
532
|
+
if (!existsSync2(options.transactionPath)) return;
|
|
533
|
+
const metadata = lstatSync2(options.transactionPath);
|
|
534
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
`[easel] transaction journal must be a real directory: ${options.transactionPath}`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
let value;
|
|
540
|
+
try {
|
|
541
|
+
value = JSON.parse(
|
|
542
|
+
readFileSync3(join2(options.transactionPath, "journal.json"), "utf8")
|
|
543
|
+
);
|
|
544
|
+
} catch {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`[easel] interrupted transaction journal is invalid: ${options.transactionPath}`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (!isTransactionJournal(value) || value.themePath !== options.themePath) {
|
|
550
|
+
throw new Error(
|
|
551
|
+
`[easel] interrupted transaction journal is invalid: ${options.transactionPath}`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
555
|
+
for (const entry of value.entries) {
|
|
556
|
+
const path = resolveJournalLocation(options, entry);
|
|
557
|
+
if (!entry.existed) {
|
|
558
|
+
snapshots.set(path, void 0);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (entry.blob === void 0 || !/^\d+\.bin$/.test(entry.blob)) {
|
|
562
|
+
throw new Error(
|
|
563
|
+
`[easel] interrupted transaction journal is invalid: ${options.transactionPath}`
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
const blobPath = join2(options.transactionPath, entry.blob);
|
|
567
|
+
if (!existsSync2(blobPath)) {
|
|
568
|
+
throw new Error(`[easel] interrupted transaction snapshot is missing: ${blobPath}`);
|
|
569
|
+
}
|
|
570
|
+
snapshots.set(path, readFileSync3(blobPath));
|
|
571
|
+
}
|
|
572
|
+
restoreSnapshots(snapshots);
|
|
573
|
+
rmSync2(options.transactionPath, { recursive: true });
|
|
574
|
+
}
|
|
575
|
+
function safeResolve(root, path) {
|
|
576
|
+
const base = resolve(root);
|
|
577
|
+
const result = resolve(base, path);
|
|
578
|
+
const relativePath = relative(base, result);
|
|
579
|
+
if (relativePath === ".." || relativePath.startsWith(`..${sep2}`)) {
|
|
580
|
+
throw new Error(`[easel] output escapes its allowed root: ${path}`);
|
|
581
|
+
}
|
|
582
|
+
return result;
|
|
583
|
+
}
|
|
584
|
+
function writeIfChanged(path, content) {
|
|
585
|
+
const next = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
|
586
|
+
if (existsSync2(path) && readFileSync3(path).equals(next)) return;
|
|
587
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
588
|
+
const temporary = `${path}.easel-${process.pid}-${Date.now()}`;
|
|
589
|
+
writeFileSync2(temporary, next);
|
|
590
|
+
try {
|
|
591
|
+
if (process.platform === "win32" && existsSync2(path)) {
|
|
592
|
+
replaceFileOnWindows(temporary, path);
|
|
593
|
+
} else {
|
|
594
|
+
renameSync(temporary, path);
|
|
595
|
+
}
|
|
596
|
+
} finally {
|
|
597
|
+
if (existsSync2(temporary)) rmSync2(temporary);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function journalLocation(options, path) {
|
|
601
|
+
const statePath = relativeWithin(dirname2(options.ledgerPath), path);
|
|
602
|
+
if (statePath !== void 0) {
|
|
603
|
+
return assertJournalLocation({ scope: "state", path: statePath });
|
|
604
|
+
}
|
|
605
|
+
const themePath = relativeWithin(options.themePath, path);
|
|
606
|
+
if (themePath !== void 0) {
|
|
607
|
+
return assertJournalLocation({ scope: "theme", path: themePath });
|
|
608
|
+
}
|
|
609
|
+
throw new Error(`[easel] cannot journal output outside Easel's roots: ${path}`);
|
|
610
|
+
}
|
|
611
|
+
function resolveJournalLocation(options, entry) {
|
|
612
|
+
assertJournalLocation(entry);
|
|
613
|
+
const root = entry.scope === "theme" ? options.themePath : dirname2(options.ledgerPath);
|
|
614
|
+
const path = safeResolve(root, entry.path);
|
|
615
|
+
if (relativeWithin(root, path) !== entry.path) {
|
|
616
|
+
throw new Error(
|
|
617
|
+
`[easel] interrupted transaction journal contains an invalid path: ${entry.path}`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
return path;
|
|
621
|
+
}
|
|
622
|
+
function assertJournalLocation(location) {
|
|
623
|
+
const valid = location.scope === "theme" ? isJournalThemePath(location.path) : ["outputs.json", "production.txt", "production.liquid", "manifest.json"].includes(
|
|
624
|
+
location.path
|
|
625
|
+
);
|
|
626
|
+
if (!valid) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
`[easel] transaction journal contains an invalid path: ${location.path}`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
return location;
|
|
632
|
+
}
|
|
633
|
+
function isJournalThemePath(path) {
|
|
634
|
+
const [directory, filename, extra] = path.split("/");
|
|
635
|
+
if (filename === void 0 || extra !== void 0) return false;
|
|
636
|
+
if (directory === "assets") return SAFE_ASSET_FILENAME.test(filename);
|
|
637
|
+
return directory === "snippets" && SAFE_LIQUID_FILENAME.test(filename);
|
|
638
|
+
}
|
|
639
|
+
function relativeWithin(root, path) {
|
|
640
|
+
const value = relative(resolve(root), resolve(path));
|
|
641
|
+
if (value === "" || value === ".." || value.startsWith(`..${sep2}`)) {
|
|
642
|
+
return void 0;
|
|
643
|
+
}
|
|
644
|
+
return value.split(sep2).join("/");
|
|
645
|
+
}
|
|
646
|
+
function isTransactionJournal(value) {
|
|
647
|
+
if (typeof value !== "object" || value === null || !("schemaVersion" in value) || value.schemaVersion !== 1 || !("themePath" in value) || typeof value.themePath !== "string" || !("entries" in value) || !Array.isArray(value.entries)) {
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
return value.entries.every((entry) => isTransactionEntry(entry));
|
|
651
|
+
}
|
|
652
|
+
function isTransactionEntry(entry) {
|
|
653
|
+
return typeof entry === "object" && entry !== null && "scope" in entry && (entry.scope === "theme" || entry.scope === "state") && "path" in entry && typeof entry.path === "string" && "existed" in entry && typeof entry.existed === "boolean" && (!("blob" in entry) || entry.blob === void 0 || typeof entry.blob === "string");
|
|
654
|
+
}
|
|
655
|
+
function replaceFileOnWindows(temporary, destination) {
|
|
656
|
+
const backup = `${temporary}.previous`;
|
|
657
|
+
renameSync(destination, backup);
|
|
658
|
+
try {
|
|
659
|
+
renameSync(temporary, destination);
|
|
660
|
+
rmSync2(backup);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
if (existsSync2(destination)) rmSync2(destination);
|
|
663
|
+
renameSync(backup, destination);
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/output.ts
|
|
669
|
+
function commitProductionOutput(options, manifest, liquid) {
|
|
670
|
+
const releaseLock2 = acquireCommitLock(options.commitLockPath);
|
|
671
|
+
try {
|
|
672
|
+
recoverInterruptedCommit(options);
|
|
673
|
+
migrateLegacyProductionBackup(options);
|
|
674
|
+
const previous = readOwnershipLedger(options);
|
|
675
|
+
const previousFiles = new Set(previous?.generated ?? []);
|
|
676
|
+
const assets = prepareAssets(options, manifest.generated);
|
|
677
|
+
const nextFiles = /* @__PURE__ */ new Set([
|
|
678
|
+
...assets.map((asset) => asset.relativePath),
|
|
679
|
+
ownedLiquidPath(options)
|
|
680
|
+
]);
|
|
681
|
+
assertProductionOutputWritable(options, assets, previousFiles);
|
|
682
|
+
const stalePaths = [...previousFiles].filter((file) => !nextFiles.has(file)).map((file) => safeResolve(options.themePath, file));
|
|
683
|
+
runFileTransaction(
|
|
684
|
+
options,
|
|
685
|
+
changedOutputPaths(options, assets, stalePaths),
|
|
686
|
+
() => publishProductionOutput(options, manifest, liquid, assets, stalePaths, nextFiles)
|
|
687
|
+
);
|
|
688
|
+
} finally {
|
|
689
|
+
releaseLock2();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function assertDevelopmentLiquidWritable(options) {
|
|
693
|
+
migrateLegacyProductionBackup(options);
|
|
694
|
+
const previous = readOwnershipLedger(options);
|
|
695
|
+
assertLiquidWritable(
|
|
696
|
+
options.liquidPath,
|
|
697
|
+
ownedLiquidPath(options),
|
|
698
|
+
new Set(previous?.generated ?? [])
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
function writeDevelopmentLiquid(options, content) {
|
|
702
|
+
assertDevelopmentLiquidWritable(options);
|
|
703
|
+
const currentContent = readOptionalFile(options.liquidPath);
|
|
704
|
+
preserveProductionLiquid(options, currentContent);
|
|
705
|
+
const productionContent = currentContent !== void 0 && isDevelopmentLiquid(currentContent.toString()) ? readProductionLiquidBackup(options) : currentContent;
|
|
706
|
+
writeIfChanged(options.liquidPath, content);
|
|
707
|
+
return () => restoreDevelopmentLiquid(options.liquidPath, content, productionContent);
|
|
708
|
+
}
|
|
709
|
+
function prepareAssets(options, generatedFiles) {
|
|
710
|
+
return generatedFiles.map((file) => {
|
|
711
|
+
const sourcePath = safeResolve(options.stagingPath, file);
|
|
712
|
+
if (!existsSync3(sourcePath)) {
|
|
713
|
+
throw new Error(`[easel] generated Vite asset is missing: ${sourcePath}`);
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
relativePath: ownedAssetPath(options, file),
|
|
717
|
+
destinationPath: safeResolve(join3(options.themePath, "assets"), file),
|
|
718
|
+
content: readFileSync4(sourcePath)
|
|
719
|
+
};
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
function assertProductionOutputWritable(options, assets, previousFiles) {
|
|
723
|
+
for (const asset of assets) {
|
|
724
|
+
assertAssetWritable(
|
|
725
|
+
asset.destinationPath,
|
|
726
|
+
asset.relativePath,
|
|
727
|
+
asset.content,
|
|
728
|
+
previousFiles
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
assertLiquidWritable(options.liquidPath, ownedLiquidPath(options), previousFiles);
|
|
732
|
+
}
|
|
733
|
+
function changedOutputPaths(options, assets, stalePaths) {
|
|
734
|
+
return [
|
|
735
|
+
...assets.map((asset) => asset.destinationPath),
|
|
736
|
+
options.liquidPath,
|
|
737
|
+
options.productionLiquidPath,
|
|
738
|
+
options.manifestPath,
|
|
739
|
+
...stalePaths,
|
|
740
|
+
options.ledgerPath
|
|
741
|
+
];
|
|
742
|
+
}
|
|
743
|
+
function publishProductionOutput(options, manifest, liquid, assets, stalePaths, nextFiles) {
|
|
744
|
+
for (const asset of assets) {
|
|
745
|
+
writeIfChanged(asset.destinationPath, asset.content);
|
|
746
|
+
}
|
|
747
|
+
writeIfChanged(options.liquidPath, liquid);
|
|
748
|
+
writeIfChanged(options.productionLiquidPath, liquid);
|
|
749
|
+
writeIfChanged(options.manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
750
|
+
`);
|
|
751
|
+
for (const stale of stalePaths) {
|
|
752
|
+
if (existsSync3(stale)) rmSync3(stale);
|
|
753
|
+
}
|
|
754
|
+
const ledger = {
|
|
755
|
+
schemaVersion: 2,
|
|
756
|
+
themePath: options.themePath,
|
|
757
|
+
prefix: options.prefix,
|
|
758
|
+
liquidFilename: options.liquidFilename,
|
|
759
|
+
generated: [...nextFiles].sort()
|
|
760
|
+
};
|
|
761
|
+
writeIfChanged(options.ledgerPath, `${JSON.stringify(ledger, null, 2)}
|
|
762
|
+
`);
|
|
763
|
+
}
|
|
764
|
+
function preserveProductionLiquid(options, currentContent) {
|
|
765
|
+
if (currentContent !== void 0 && isGeneratedLiquid(currentContent.toString()) && !isDevelopmentLiquid(currentContent.toString())) {
|
|
766
|
+
writeIfChanged(options.productionLiquidPath, currentContent);
|
|
767
|
+
} else if (currentContent === void 0 && existsSync3(options.productionLiquidPath)) {
|
|
768
|
+
rmSync3(options.productionLiquidPath);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function restoreDevelopmentLiquid(liquidPath, developmentContent, productionContent) {
|
|
772
|
+
if (!existsSync3(liquidPath)) return;
|
|
773
|
+
if (readFileSync4(liquidPath, "utf8") !== developmentContent) return;
|
|
774
|
+
if (productionContent === void 0) {
|
|
775
|
+
rmSync3(liquidPath);
|
|
776
|
+
} else {
|
|
777
|
+
writeIfChanged(liquidPath, productionContent);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function migrateLegacyProductionBackup(options) {
|
|
781
|
+
const legacyPath = join3(dirname3(options.productionLiquidPath), "production.liquid");
|
|
782
|
+
if (!existsSync3(legacyPath)) return;
|
|
783
|
+
const legacyContent = readFileSync4(legacyPath);
|
|
784
|
+
if (!isGeneratedLiquid(legacyContent.toString())) {
|
|
785
|
+
throw new Error(
|
|
786
|
+
`[easel] legacy production backup is not generated by Easel: ${legacyPath}`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
if (!existsSync3(options.productionLiquidPath)) {
|
|
790
|
+
writeIfChanged(options.productionLiquidPath, legacyContent);
|
|
791
|
+
}
|
|
792
|
+
rmSync3(legacyPath);
|
|
793
|
+
}
|
|
794
|
+
function readProductionLiquidBackup(options) {
|
|
795
|
+
const content = readOptionalFile(options.productionLiquidPath);
|
|
796
|
+
return content !== void 0 && isGeneratedLiquid(content.toString()) && !isDevelopmentLiquid(content.toString()) ? content : void 0;
|
|
797
|
+
}
|
|
798
|
+
function readOptionalFile(path) {
|
|
799
|
+
return existsSync3(path) ? readFileSync4(path) : void 0;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/build.ts
|
|
803
|
+
function finalizeProductionBuild(options, config, publish) {
|
|
804
|
+
try {
|
|
805
|
+
if (!publish) return;
|
|
806
|
+
const viteManifest = readViteManifest(options.stagingPath);
|
|
807
|
+
const manifest = createEaselManifest(
|
|
808
|
+
viteManifest,
|
|
809
|
+
options.bundles,
|
|
810
|
+
listEmittedFiles(options.stagingPath)
|
|
811
|
+
);
|
|
812
|
+
commitProductionOutput(options, manifest, renderProductionLiquid(manifest));
|
|
813
|
+
} finally {
|
|
814
|
+
removeStagingDirectory(options.stagingPath, config);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function readViteManifest(stagingPath) {
|
|
818
|
+
const path = join4(stagingPath, ".vite", "manifest.json");
|
|
819
|
+
if (!existsSync4(path)) {
|
|
820
|
+
throw new Error(`[easel] Vite did not write its build manifest: ${path}`);
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
824
|
+
} catch (error) {
|
|
825
|
+
throw new Error(`[easel] Vite wrote an invalid build manifest: ${path}`, {
|
|
826
|
+
cause: error
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function removeStagingDirectory(stagingPath, config) {
|
|
831
|
+
try {
|
|
832
|
+
rmSync4(stagingPath, { recursive: true, force: true });
|
|
833
|
+
} catch (error) {
|
|
834
|
+
config.logger.warn(
|
|
835
|
+
`[easel] could not remove staging directory ${stagingPath}: ${errorMessage(error)}`
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function listEmittedFiles(stagingPath) {
|
|
840
|
+
const files = [];
|
|
841
|
+
for (const entry of readdirSync(stagingPath, { withFileTypes: true })) {
|
|
842
|
+
if (entry.name === ".vite" && entry.isDirectory()) continue;
|
|
843
|
+
if (!entry.isFile()) {
|
|
844
|
+
throw new Error(`[easel] unexpected output in staging directory: ${entry.name}`);
|
|
845
|
+
}
|
|
846
|
+
files.push(entry.name);
|
|
847
|
+
}
|
|
848
|
+
return files;
|
|
849
|
+
}
|
|
850
|
+
function errorMessage(error) {
|
|
851
|
+
return error instanceof Error ? error.message : String(error);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/options.ts
|
|
855
|
+
import { createHash, randomUUID as randomUUID3 } from "crypto";
|
|
856
|
+
import { existsSync as existsSync5, lstatSync as lstatSync3, realpathSync, statSync as statSync2 } from "fs";
|
|
857
|
+
import { isAbsolute, join as join5, relative as relative2, resolve as resolve2, sep as sep3 } from "path";
|
|
858
|
+
var DEFAULT_SOURCE = "src";
|
|
859
|
+
var DEFAULT_NAMESPACE = "easel";
|
|
860
|
+
var DEFAULT_REFRESH_SIGNAL = ".easel/shopify-ready";
|
|
861
|
+
var DEFAULT_REFRESH_DELAY = 100;
|
|
862
|
+
function resolveEaselOptions(options, projectRoot) {
|
|
863
|
+
const root = realpathSync(resolve2(projectRoot));
|
|
864
|
+
const configuredThemePath = resolveFrom(root, options.theme ?? ".");
|
|
865
|
+
assertTheme(configuredThemePath);
|
|
866
|
+
const themePath = realpathSync(configuredThemePath);
|
|
867
|
+
const configuredSourcePath = resolveFrom(root, options.source ?? DEFAULT_SOURCE);
|
|
868
|
+
const namespace = options.namespace ?? DEFAULT_NAMESPACE;
|
|
869
|
+
assertSource(configuredSourcePath);
|
|
870
|
+
const sourcePath = realpathSync(configuredSourcePath);
|
|
871
|
+
assertNamespace(namespace);
|
|
872
|
+
const liquidFilename = `${namespace}-assets.liquid`;
|
|
873
|
+
const prefix = `${namespace}-`;
|
|
874
|
+
const refresh = resolveRefresh(options.refresh, root);
|
|
875
|
+
const bundles = resolveBundles(options.bundles, sourcePath);
|
|
876
|
+
const statePaths = resolveEaselStatePaths(root, themePath);
|
|
877
|
+
return {
|
|
878
|
+
projectRoot: root,
|
|
879
|
+
themePath,
|
|
880
|
+
sourcePath,
|
|
881
|
+
...statePaths,
|
|
882
|
+
liquidPath: join5(themePath, "snippets", liquidFilename),
|
|
883
|
+
liquidFilename,
|
|
884
|
+
namespace,
|
|
885
|
+
prefix,
|
|
886
|
+
refresh,
|
|
887
|
+
bundles
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
function resolveRefresh(configured, projectRoot) {
|
|
891
|
+
if (configured === false) {
|
|
892
|
+
return {
|
|
893
|
+
enabled: false,
|
|
894
|
+
signalPath: join5(projectRoot, DEFAULT_REFRESH_SIGNAL),
|
|
895
|
+
delay: DEFAULT_REFRESH_DELAY
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const options = configured === true || configured === void 0 ? {} : configured;
|
|
899
|
+
const delay = options.delay ?? DEFAULT_REFRESH_DELAY;
|
|
900
|
+
if (!Number.isInteger(delay) || delay < 0 || delay > 1e4) {
|
|
901
|
+
throw new Error(
|
|
902
|
+
"[easel] refresh.delay must be an integer from 0 to 10000 milliseconds"
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
enabled: true,
|
|
907
|
+
signalPath: resolveFrom(projectRoot, options.signal ?? DEFAULT_REFRESH_SIGNAL),
|
|
908
|
+
delay
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function resolveBundles(configured, sourcePath) {
|
|
912
|
+
const bundles = configured ?? { theme: resolveDefaultBundle(sourcePath) };
|
|
913
|
+
const entries = Object.entries(bundles);
|
|
914
|
+
if (entries.length === 0) {
|
|
915
|
+
throw new Error("[easel] bundles must contain at least one named bundle");
|
|
916
|
+
}
|
|
917
|
+
const resolved = entries.map(([name, bundle]) => {
|
|
918
|
+
if (!SAFE_OUTPUT_NAME.test(name)) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`[easel] invalid bundle name "${name}"; use lowercase letters, numbers, and hyphens`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
if (bundle.script === void 0 && bundle.style === void 0) {
|
|
924
|
+
throw new Error(`[easel] bundle "${name}" needs a script, a style, or both`);
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
name,
|
|
928
|
+
...resolveBundleFiles(name, bundle, sourcePath)
|
|
929
|
+
};
|
|
930
|
+
});
|
|
931
|
+
const scriptOwners = /* @__PURE__ */ new Map();
|
|
932
|
+
for (const bundle of resolved) {
|
|
933
|
+
if (bundle.script === void 0) continue;
|
|
934
|
+
const owner = scriptOwners.get(bundle.script);
|
|
935
|
+
if (owner !== void 0) {
|
|
936
|
+
throw new Error(
|
|
937
|
+
`[easel] bundles "${owner}" and "${bundle.name}" use the same script; each named bundle needs a distinct script entry`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
scriptOwners.set(bundle.script, bundle.name);
|
|
941
|
+
}
|
|
942
|
+
return resolved;
|
|
943
|
+
}
|
|
944
|
+
function resolveDefaultBundle(sourcePath) {
|
|
945
|
+
const typescriptEntry = join5(sourcePath, "main.ts");
|
|
946
|
+
const javascriptEntry = join5(sourcePath, "main.js");
|
|
947
|
+
const styleEntry = join5(sourcePath, "style.css");
|
|
948
|
+
const hasTypescript = isFile(typescriptEntry);
|
|
949
|
+
const hasJavascript = isFile(javascriptEntry);
|
|
950
|
+
if (hasTypescript && hasJavascript) {
|
|
951
|
+
throw new Error(
|
|
952
|
+
`[easel] both main.ts and main.js exist in ${sourcePath}; configure bundles to choose one explicitly`
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
if (!hasTypescript && !hasJavascript) {
|
|
956
|
+
throw new Error(
|
|
957
|
+
`[easel] expected main.ts or main.js in ${sourcePath}; configure source or bundles for a different layout`
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
if (!isFile(styleEntry)) {
|
|
961
|
+
throw new Error(
|
|
962
|
+
`[easel] expected style.css in ${sourcePath}; configure bundles explicitly to build without a stylesheet`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
return {
|
|
966
|
+
script: hasTypescript ? typescriptEntry : javascriptEntry,
|
|
967
|
+
style: styleEntry
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function resolveBundleFiles(name, bundle, sourcePath) {
|
|
971
|
+
const resolved = {};
|
|
972
|
+
if (bundle.script !== void 0) {
|
|
973
|
+
resolved.script = resolveInput(name, "script", bundle.script, sourcePath);
|
|
974
|
+
}
|
|
975
|
+
if (bundle.style !== void 0) {
|
|
976
|
+
resolved.style = resolveInput(name, "style", bundle.style, sourcePath);
|
|
977
|
+
}
|
|
978
|
+
return resolved;
|
|
979
|
+
}
|
|
980
|
+
function resolveInput(bundleName, kind, input, sourcePath) {
|
|
981
|
+
const path = resolveFrom(sourcePath, input);
|
|
982
|
+
if (!isFile(path)) {
|
|
983
|
+
throw new Error(`[easel] ${kind} for bundle "${bundleName}" does not exist: ${path}`);
|
|
984
|
+
}
|
|
985
|
+
return realpathSync(path);
|
|
986
|
+
}
|
|
987
|
+
function resolveEaselStatePaths(projectRoot, themePath) {
|
|
988
|
+
const easelPath = join5(projectRoot, ".easel");
|
|
989
|
+
const themeId = createHash("sha256").update(themePath).digest("hex").slice(0, 16);
|
|
990
|
+
const stagingParent = join5(easelPath, "build", themeId);
|
|
991
|
+
const themeStatePath = join5(easelPath, "themes", themeId);
|
|
992
|
+
assertEaselStatePaths([
|
|
993
|
+
easelPath,
|
|
994
|
+
join5(easelPath, "build"),
|
|
995
|
+
stagingParent,
|
|
996
|
+
join5(easelPath, "themes"),
|
|
997
|
+
themeStatePath
|
|
998
|
+
]);
|
|
999
|
+
return {
|
|
1000
|
+
stagingPath: join5(stagingParent, randomUUID3()),
|
|
1001
|
+
ledgerPath: join5(themeStatePath, "outputs.json"),
|
|
1002
|
+
commitLockPath: join5(themeStatePath, "commit.lock"),
|
|
1003
|
+
manifestPath: join5(themeStatePath, "manifest.json"),
|
|
1004
|
+
productionLiquidPath: join5(themeStatePath, "production.txt"),
|
|
1005
|
+
transactionPath: join5(themeStatePath, "transaction")
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function assertTheme(themePath) {
|
|
1009
|
+
for (const directory of ["assets", "layout", "snippets"]) {
|
|
1010
|
+
const path = join5(themePath, directory);
|
|
1011
|
+
const metadata = lstatSync3(path, { throwIfNoEntry: false });
|
|
1012
|
+
if (metadata?.isSymbolicLink()) {
|
|
1013
|
+
throw new Error(
|
|
1014
|
+
`[easel] Shopify theme output directory cannot be a symbolic link: ${path}`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
if (metadata === void 0 || !metadata.isDirectory()) {
|
|
1018
|
+
throw new Error(
|
|
1019
|
+
`[easel] invalid Shopify theme at ${themePath}: missing ${directory}/`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
function assertEaselStatePaths(paths) {
|
|
1025
|
+
for (const path of paths) {
|
|
1026
|
+
const metadata = lstatSync3(path, { throwIfNoEntry: false });
|
|
1027
|
+
if (metadata === void 0) continue;
|
|
1028
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
1029
|
+
throw new Error(`[easel] state directory must be a real directory: ${path}`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function assertSource(sourcePath) {
|
|
1034
|
+
if (!existsSync5(sourcePath) || !statSync2(sourcePath).isDirectory()) {
|
|
1035
|
+
throw new Error(`[easel] source directory does not exist: ${sourcePath}`);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
function assertNamespace(namespace) {
|
|
1039
|
+
if (!SAFE_OUTPUT_NAME.test(namespace)) {
|
|
1040
|
+
throw new Error(
|
|
1041
|
+
"[easel] namespace must use lowercase letters, numbers, and single hyphens"
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function resolveFrom(base, path) {
|
|
1046
|
+
return isAbsolute(path) ? resolve2(path) : resolve2(base, path);
|
|
1047
|
+
}
|
|
1048
|
+
function isFile(path) {
|
|
1049
|
+
return existsSync5(path) && statSync2(path).isFile();
|
|
1050
|
+
}
|
|
1051
|
+
function projectRelative(path, projectRoot) {
|
|
1052
|
+
const value = relative2(projectRoot, path);
|
|
1053
|
+
return value.split(sep3).join("/");
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/refresh.ts
|
|
1057
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1058
|
+
import { dirname as dirname4 } from "path";
|
|
1059
|
+
var POLL_INTERVAL = 50;
|
|
1060
|
+
function watchRefreshSignal(signalPath, delay, refresh) {
|
|
1061
|
+
mkdirSync3(dirname4(signalPath), { recursive: true });
|
|
1062
|
+
if (!existsSync6(signalPath)) writeFileSync3(signalPath, "");
|
|
1063
|
+
let previous = readStats(signalPath);
|
|
1064
|
+
let timer;
|
|
1065
|
+
const poller = setInterval(() => {
|
|
1066
|
+
const current = readStats(signalPath);
|
|
1067
|
+
if (sameStats(current, previous)) return;
|
|
1068
|
+
previous = current;
|
|
1069
|
+
clearTimeout(timer);
|
|
1070
|
+
timer = setTimeout(refresh, delay);
|
|
1071
|
+
}, POLL_INTERVAL);
|
|
1072
|
+
poller.unref();
|
|
1073
|
+
return () => {
|
|
1074
|
+
clearTimeout(timer);
|
|
1075
|
+
clearInterval(poller);
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function readStats(path) {
|
|
1079
|
+
try {
|
|
1080
|
+
return statSync3(path);
|
|
1081
|
+
} catch {
|
|
1082
|
+
return void 0;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
function sameStats(current, previous) {
|
|
1086
|
+
if (current === void 0 || previous === void 0) return current === previous;
|
|
1087
|
+
return current.mtimeMs === previous.mtimeMs && current.ctimeMs === previous.ctimeMs && current.size === previous.size;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/development.ts
|
|
1091
|
+
function configureDevelopmentServer(server, options) {
|
|
1092
|
+
if (server.httpServer === null) {
|
|
1093
|
+
throw new Error(
|
|
1094
|
+
"[easel] Vite middleware mode is not supported; Easel requires the development server lifecycle"
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
assertDevelopmentLiquidWritable(options);
|
|
1098
|
+
const stopWatching = configureRefresh(server, options);
|
|
1099
|
+
let restoreDevelopmentLiquid2;
|
|
1100
|
+
server.httpServer.once("listening", () => {
|
|
1101
|
+
restoreDevelopmentLiquid2 = writeDevelopmentLiquid(
|
|
1102
|
+
options,
|
|
1103
|
+
renderDevelopmentLiquid(
|
|
1104
|
+
options.bundles.map((bundle) => bundle.name),
|
|
1105
|
+
developmentOrigin(server)
|
|
1106
|
+
)
|
|
1107
|
+
);
|
|
1108
|
+
});
|
|
1109
|
+
server.httpServer.once("close", () => {
|
|
1110
|
+
stopWatching?.();
|
|
1111
|
+
restoreDevelopmentLiquid2?.();
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
function configureRefresh(server, options) {
|
|
1115
|
+
if (!options.refresh.enabled) return void 0;
|
|
1116
|
+
const stopWatching = watchRefreshSignal(
|
|
1117
|
+
options.refresh.signalPath,
|
|
1118
|
+
options.refresh.delay,
|
|
1119
|
+
() => server.ws.send({ type: "full-reload", path: "*" })
|
|
1120
|
+
);
|
|
1121
|
+
server.config.logger.info(
|
|
1122
|
+
`[easel] Shopify refresh signal: ${projectRelative(
|
|
1123
|
+
options.refresh.signalPath,
|
|
1124
|
+
options.projectRoot
|
|
1125
|
+
)}`
|
|
1126
|
+
);
|
|
1127
|
+
return stopWatching;
|
|
1128
|
+
}
|
|
1129
|
+
function developmentOrigin(server) {
|
|
1130
|
+
const origin = server.config.server.origin ?? server.resolvedUrls?.local[0] ?? server.resolvedUrls?.network[0];
|
|
1131
|
+
if (origin === void 0) {
|
|
1132
|
+
throw new Error(
|
|
1133
|
+
"[easel] Vite did not expose a development URL; set server.origin explicitly"
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
return origin;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/vite-config.ts
|
|
1140
|
+
function prepareViteConfig(easelOptions, userConfig, command) {
|
|
1141
|
+
const options = resolveEaselOptions(easelOptions, userConfig.root ?? process.cwd());
|
|
1142
|
+
switch (command) {
|
|
1143
|
+
case "build":
|
|
1144
|
+
assertCompatibleBuildConfig(userConfig);
|
|
1145
|
+
break;
|
|
1146
|
+
case "serve":
|
|
1147
|
+
break;
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
options,
|
|
1151
|
+
config: createViteConfig(userConfig, options, command)
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function validateResolvedViteConfig(config, options) {
|
|
1155
|
+
switch (config.command) {
|
|
1156
|
+
case "build":
|
|
1157
|
+
assertResolvedBuildConfig(config, options);
|
|
1158
|
+
assertNoLaterPostBuildHooks(config);
|
|
1159
|
+
break;
|
|
1160
|
+
case "serve":
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
function createViteConfig(config, options, command) {
|
|
1165
|
+
const stableCssEntryIds = new Set(
|
|
1166
|
+
options.bundles.map((bundle) => `easel:${bundle.name}`)
|
|
1167
|
+
);
|
|
1168
|
+
const input = Object.fromEntries(
|
|
1169
|
+
options.bundles.map((bundle) => [bundle.name, virtualBundleId(bundle.name)])
|
|
1170
|
+
);
|
|
1171
|
+
return {
|
|
1172
|
+
...command === "build" ? { base: "./" } : {},
|
|
1173
|
+
build: {
|
|
1174
|
+
outDir: options.stagingPath,
|
|
1175
|
+
emptyOutDir: true,
|
|
1176
|
+
assetsDir: "",
|
|
1177
|
+
manifest: true,
|
|
1178
|
+
rollupOptions: {
|
|
1179
|
+
input,
|
|
1180
|
+
output: {
|
|
1181
|
+
entryFileNames: `${options.prefix}[name].js`,
|
|
1182
|
+
chunkFileNames: `${options.prefix}[name]-[hash].js`,
|
|
1183
|
+
assetFileNames(asset) {
|
|
1184
|
+
const original = asset.names[0] ?? asset.name ?? "asset";
|
|
1185
|
+
if (!original.endsWith(".css")) {
|
|
1186
|
+
return `${options.prefix}[name]-[hash][extname]`;
|
|
1187
|
+
}
|
|
1188
|
+
return isEntryStylesheet(asset.originalFileNames, stableCssEntryIds) ? `${options.prefix}[name].css` : `${options.prefix}[name]-[hash].css`;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
server: {
|
|
1194
|
+
...config.server?.cors === void 0 ? { cors: shopifyCorsPolicy() } : {},
|
|
1195
|
+
fs: {
|
|
1196
|
+
allow: easelFileSystemAllowList(config.server?.fs?.allow ?? [], options)
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function isEntryStylesheet(originalFileNames, stableCssEntryIds) {
|
|
1202
|
+
return originalFileNames.some((file) => {
|
|
1203
|
+
const normalized = file.replaceAll("\\", "/");
|
|
1204
|
+
return stableCssEntryIds.has(normalized.slice(normalized.lastIndexOf("/") + 1));
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
function shopifyCorsPolicy() {
|
|
1208
|
+
return {
|
|
1209
|
+
origin: [
|
|
1210
|
+
/^https?:\/\/(?:[^.]+\.)*myshopify\.com$/,
|
|
1211
|
+
/^https:\/\/admin\.shopify\.com$/,
|
|
1212
|
+
/^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/
|
|
1213
|
+
]
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function easelFileSystemAllowList(configured, options) {
|
|
1217
|
+
const existing = new Set(configured);
|
|
1218
|
+
return unique([options.projectRoot, options.sourcePath]).filter(
|
|
1219
|
+
(path) => !existing.has(path)
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
function assertCompatibleBuildConfig(config) {
|
|
1223
|
+
const build = config.build;
|
|
1224
|
+
const conflict = firstViolation([
|
|
1225
|
+
[
|
|
1226
|
+
config.base !== void 0 && config.base !== "./",
|
|
1227
|
+
'Vite base must be "./" for Shopify CDN assets'
|
|
1228
|
+
],
|
|
1229
|
+
[
|
|
1230
|
+
build?.outDir !== void 0,
|
|
1231
|
+
"build.outDir conflicts with Easel staging; configure the Shopify theme with easel({ theme }) instead"
|
|
1232
|
+
],
|
|
1233
|
+
[
|
|
1234
|
+
build?.emptyOutDir !== void 0,
|
|
1235
|
+
"build.emptyOutDir is managed by Easel for safe staging"
|
|
1236
|
+
],
|
|
1237
|
+
[
|
|
1238
|
+
build?.assetsDir !== void 0,
|
|
1239
|
+
"build.assetsDir is managed by Easel for flat Shopify assets"
|
|
1240
|
+
],
|
|
1241
|
+
[build?.manifest !== void 0, "build.manifest is managed by Easel"],
|
|
1242
|
+
[build?.write === false, "build.write must remain enabled"],
|
|
1243
|
+
[
|
|
1244
|
+
build?.rollupOptions?.input !== void 0,
|
|
1245
|
+
"build.rollupOptions.input conflicts with Easel bundles"
|
|
1246
|
+
],
|
|
1247
|
+
[
|
|
1248
|
+
build?.rollupOptions?.output !== void 0,
|
|
1249
|
+
"build.rollupOptions.output is managed by Easel"
|
|
1250
|
+
]
|
|
1251
|
+
]);
|
|
1252
|
+
if (conflict !== void 0) throw new Error(`[easel] ${conflict}`);
|
|
1253
|
+
}
|
|
1254
|
+
function assertResolvedBuildConfig(config, options) {
|
|
1255
|
+
const violation = firstViolation([
|
|
1256
|
+
[config.base !== "./", 'resolved Vite base must be "./"'],
|
|
1257
|
+
[
|
|
1258
|
+
config.build.outDir !== options.stagingPath,
|
|
1259
|
+
"another Vite plugin changed Easel's staging directory"
|
|
1260
|
+
],
|
|
1261
|
+
[
|
|
1262
|
+
config.build.assetsDir !== "",
|
|
1263
|
+
"another Vite plugin changed Easel's flat asset layout"
|
|
1264
|
+
],
|
|
1265
|
+
[
|
|
1266
|
+
config.build.emptyOutDir !== true,
|
|
1267
|
+
"another Vite plugin disabled Easel's staging cleanup"
|
|
1268
|
+
],
|
|
1269
|
+
[
|
|
1270
|
+
config.build.manifest !== true,
|
|
1271
|
+
"another Vite plugin changed Easel's manifest setting"
|
|
1272
|
+
],
|
|
1273
|
+
[config.build.write === false, "build.write must remain enabled"]
|
|
1274
|
+
]);
|
|
1275
|
+
if (violation !== void 0) throw new Error(`[easel] ${violation}`);
|
|
1276
|
+
}
|
|
1277
|
+
function firstViolation(checks) {
|
|
1278
|
+
return checks.find(([violated]) => violated)?.[1];
|
|
1279
|
+
}
|
|
1280
|
+
function assertNoLaterPostBuildHooks(config) {
|
|
1281
|
+
const easelIndex = config.plugins.findIndex(
|
|
1282
|
+
(plugin) => plugin.name === "easel:shopify-theme"
|
|
1283
|
+
);
|
|
1284
|
+
const unsafe = config.plugins.slice(easelIndex + 1).filter(hasPostOutputHook);
|
|
1285
|
+
if (unsafe.length > 0) {
|
|
1286
|
+
throw new Error(
|
|
1287
|
+
`[easel] Easel must run after plugins with post-order output hooks: ${unsafe.map((plugin) => plugin.name).join(", ")}. Move easel() after those plugins.`
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
function hasPostOutputHook(plugin) {
|
|
1292
|
+
return [plugin.writeBundle, plugin.closeBundle].some(
|
|
1293
|
+
(hook) => typeof hook === "object" && hook.order === "post"
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
function unique(values) {
|
|
1297
|
+
return [...new Set(values)];
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// src/plugin.ts
|
|
1301
|
+
function easel(options = {}) {
|
|
1302
|
+
let resolvedOptions;
|
|
1303
|
+
let resolvedConfig;
|
|
1304
|
+
const build = {
|
|
1305
|
+
failed: false,
|
|
1306
|
+
outputHooksCompleted: false
|
|
1307
|
+
};
|
|
1308
|
+
const requireOptions = () => {
|
|
1309
|
+
if (resolvedOptions === void 0) {
|
|
1310
|
+
throw new Error("[easel] plugin options have not been resolved");
|
|
1311
|
+
}
|
|
1312
|
+
return resolvedOptions;
|
|
1313
|
+
};
|
|
1314
|
+
const integration = {
|
|
1315
|
+
name: "easel:shopify-theme",
|
|
1316
|
+
enforce: "post",
|
|
1317
|
+
config(config, environment) {
|
|
1318
|
+
const prepared = prepareViteConfig(options, config, environment.command);
|
|
1319
|
+
resolvedOptions = prepared.options;
|
|
1320
|
+
return prepared.config;
|
|
1321
|
+
},
|
|
1322
|
+
configResolved(config) {
|
|
1323
|
+
resolvedConfig = config;
|
|
1324
|
+
validateResolvedViteConfig(config, requireOptions());
|
|
1325
|
+
},
|
|
1326
|
+
configureServer(server) {
|
|
1327
|
+
configureDevelopmentServer(server, requireOptions());
|
|
1328
|
+
},
|
|
1329
|
+
buildStart() {
|
|
1330
|
+
build.failed = false;
|
|
1331
|
+
build.outputHooksCompleted = false;
|
|
1332
|
+
},
|
|
1333
|
+
buildEnd(error) {
|
|
1334
|
+
if (error !== void 0) build.failed = true;
|
|
1335
|
+
},
|
|
1336
|
+
writeBundle: {
|
|
1337
|
+
order: "post",
|
|
1338
|
+
sequential: true,
|
|
1339
|
+
handler() {
|
|
1340
|
+
build.outputHooksCompleted = true;
|
|
1341
|
+
}
|
|
1342
|
+
},
|
|
1343
|
+
closeBundle: {
|
|
1344
|
+
order: "post",
|
|
1345
|
+
sequential: true,
|
|
1346
|
+
handler() {
|
|
1347
|
+
const config = resolvedConfig;
|
|
1348
|
+
if (config?.command !== "build") return;
|
|
1349
|
+
finalizeProductionBuild(
|
|
1350
|
+
requireOptions(),
|
|
1351
|
+
config,
|
|
1352
|
+
!build.failed && build.outputHooksCompleted
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
return [easelBundleModules(() => requireOptions().bundles), integration];
|
|
1358
|
+
}
|
|
1359
|
+
export {
|
|
1360
|
+
easel as default,
|
|
1361
|
+
easel
|
|
1362
|
+
};
|
|
1363
|
+
//# sourceMappingURL=index.js.map
|