@10up/wp-vite-plugins 1.0.0-alpha.0 → 1.0.0-alpha.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { resolve, join, relative, dirname, extname } from 'path';
2
- import { readdirSync, existsSync, readFileSync } from 'fs';
2
+ import { readdirSync, existsSync, readFileSync, realpathSync } from 'fs';
3
3
  import crypto, { createHash } from 'crypto';
4
4
  import fg from 'fast-glob';
5
5
  import { init, parse } from 'es-module-lexer';
@@ -1586,8 +1586,36 @@ function discoverEntries(blocksDirAbs, buildType, skip) {
1586
1586
  }
1587
1587
  return entries;
1588
1588
  }
1589
+ function normalizePath(filePath) {
1590
+ try {
1591
+ return realpathSync.native(filePath);
1592
+ } catch {
1593
+ return filePath;
1594
+ }
1595
+ }
1596
+ var MARKER_PROPERTY = "--wp-vite-entry";
1597
+ var MARKER_DECLARATION_RE = new RegExp(
1598
+ `${MARKER_PROPERTY}\\s*:\\s*(?:"[^"]*"|'[^']*')\\s*;?`,
1599
+ "g"
1600
+ );
1601
+ var EMPTY_ROOT_RULE_RE = /:root\s*\{\s*\}/g;
1602
+ function markerRule(entryName, isIndentedSass) {
1603
+ if (isIndentedSass) {
1604
+ return `
1605
+ :root
1606
+ ${MARKER_PROPERTY}: "${entryName}"
1607
+ `;
1608
+ }
1609
+ return `
1610
+ :root{${MARKER_PROPERTY}:"${entryName}"}
1611
+ `;
1612
+ }
1613
+ function stripMarker(source) {
1614
+ return source.replace(MARKER_DECLARATION_RE, "").replace(EMPTY_ROOT_RULE_RE, "");
1615
+ }
1589
1616
  function wpBlockStyles(options) {
1590
1617
  let stylesDirAbs;
1618
+ const entryNameByFile = /* @__PURE__ */ new Map();
1591
1619
  return {
1592
1620
  name: "wp-block-styles",
1593
1621
  enforce: "pre",
@@ -1597,10 +1625,13 @@ function wpBlockStyles(options) {
1597
1625
  `${stylesDirAbs.replace(/\\/g, "/")}/**/*.{css,scss,sass}`,
1598
1626
  { absolute: true }
1599
1627
  );
1628
+ entryNameByFile.clear();
1600
1629
  const entries = {};
1601
1630
  for (const filePath of stylesheets) {
1602
1631
  const blockName = relative(stylesDirAbs, filePath).replace(extname(filePath), "").replace(/\\/g, "/");
1603
- entries[`autoenqueue/${blockName}`] = filePath;
1632
+ const entryName = `autoenqueue/${blockName}`;
1633
+ entries[entryName] = filePath;
1634
+ entryNameByFile.set(normalizePath(filePath), entryName);
1604
1635
  }
1605
1636
  return {
1606
1637
  build: {
@@ -1609,6 +1640,38 @@ function wpBlockStyles(options) {
1609
1640
  }
1610
1641
  }
1611
1642
  };
1643
+ },
1644
+ transform(code, id) {
1645
+ const [rawPath] = id.split("?");
1646
+ const filePath = normalizePath(rawPath);
1647
+ const entryName = entryNameByFile.get(filePath);
1648
+ if (!entryName) return null;
1649
+ return code + markerRule(entryName, extname(rawPath) === ".sass");
1650
+ },
1651
+ generateBundle(_options, bundle) {
1652
+ const expected = /* @__PURE__ */ new Map();
1653
+ for (const [filePath, entryName] of entryNameByFile) {
1654
+ expected.set(`${entryName}.css`, filePath);
1655
+ }
1656
+ for (const output of Object.values(bundle)) {
1657
+ if (output.type !== "asset" || !expected.has(output.fileName)) continue;
1658
+ if (typeof output.source === "string") {
1659
+ output.source = stripMarker(output.source);
1660
+ } else {
1661
+ output.source = Buffer.from(
1662
+ stripMarker(Buffer.from(output.source).toString("utf-8"))
1663
+ );
1664
+ }
1665
+ }
1666
+ const missing = [];
1667
+ for (const [fileName, filePath] of expected) {
1668
+ if (!(fileName in bundle)) missing.push(`${fileName} (from ${filePath})`);
1669
+ }
1670
+ if (missing.length > 0) {
1671
+ throw new Error(
1672
+ "wpBlockStyles: expected stylesheet(s) missing from the build output \u2014 each source under blocksStylesDir must produce its own dist file:\n" + missing.map((entry) => ` - ${entry}`).join("\n")
1673
+ );
1674
+ }
1612
1675
  }
1613
1676
  };
1614
1677
  }
@@ -1683,8 +1746,75 @@ function isScriptModule(metadata, subpath) {
1683
1746
  function camelize(kebabCaseName) {
1684
1747
  return kebabCaseName.replace(/-(.)/g, (_match, letter) => letter.toUpperCase());
1685
1748
  }
1749
+ function pickExportsEntry(value) {
1750
+ if (typeof value === "string") return value;
1751
+ if (Array.isArray(value)) {
1752
+ for (const candidate of value) {
1753
+ const picked = pickExportsEntry(candidate);
1754
+ if (picked) return picked;
1755
+ }
1756
+ return null;
1757
+ }
1758
+ if (!value || typeof value !== "object") return null;
1759
+ const record = value;
1760
+ if ("." in record) return pickExportsEntry(record["."]);
1761
+ for (const condition of ["module", "import", "browser", "default"]) {
1762
+ if (!(condition in record)) continue;
1763
+ const picked = pickExportsEntry(record[condition]);
1764
+ if (picked) return picked;
1765
+ }
1766
+ return null;
1767
+ }
1768
+ function resolvePackageEsmEntry(packageName, fromPaths) {
1769
+ for (const from of fromPaths) {
1770
+ const packageJsonPath = findPackageJson(packageName, from);
1771
+ if (!packageJsonPath) continue;
1772
+ let metadata;
1773
+ try {
1774
+ metadata = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1775
+ } catch {
1776
+ continue;
1777
+ }
1778
+ const entry = pickExportsEntry(metadata.exports) ?? (typeof metadata.module === "string" ? metadata.module : null) ?? (typeof metadata.main === "string" ? metadata.main : null) ?? "./index.js";
1779
+ const entryPath = resolve(dirname(packageJsonPath), entry);
1780
+ if (!existsSync(entryPath)) continue;
1781
+ try {
1782
+ return realpathSync(entryPath);
1783
+ } catch {
1784
+ return entryPath;
1785
+ }
1786
+ }
1787
+ return null;
1788
+ }
1789
+ function findPackageJson(packageName, from) {
1790
+ try {
1791
+ return createRequire(from).resolve(`${packageName}/package.json`);
1792
+ } catch {
1793
+ }
1794
+ let dir = dirname(from);
1795
+ for (; ; ) {
1796
+ const candidate = join(dir, "node_modules", packageName, "package.json");
1797
+ if (existsSync(candidate)) return candidate;
1798
+ const parent = dirname(dir);
1799
+ if (parent === dir) return null;
1800
+ dir = parent;
1801
+ }
1802
+ }
1686
1803
 
1687
1804
  // src/wp-externals.ts
1805
+ var DEFAULT_BUNDLED_PACKAGES = ["@wordpress/icons"];
1806
+ var BUNDLED_MARKER_PREFIX = "wp-vite-bundled:";
1807
+ var JS_SOURCE_EXTENSION = /\.[mc]?[jt]sx?$/;
1808
+ function escapeRegExp(value) {
1809
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1810
+ }
1811
+ function isBarePackageName(name) {
1812
+ if (name.startsWith("@")) {
1813
+ const parts = name.split("/");
1814
+ return parts.length === 2 && parts[0].length > 1 && !!parts[1];
1815
+ }
1816
+ return name.length > 0 && !name.includes("/") && !name.startsWith(".");
1817
+ }
1688
1818
  var DEFAULT_VENDOR = {
1689
1819
  react: { global: "React", handle: "react" },
1690
1820
  "react-dom": { global: "ReactDOM", handle: "react-dom" },
@@ -1737,6 +1867,14 @@ function wpExternals(options = {}) {
1737
1867
  const moduleEntryMatchers = options.moduleEntryMatchers ?? [];
1738
1868
  const wpBuildAssetMatchers = options.wpBuildAssetMatchers ?? [];
1739
1869
  const scriptModuleNamespaces = options.scriptModuleNamespaces ?? [];
1870
+ for (const name of options.bundle ?? []) {
1871
+ if (isBarePackageName(name)) continue;
1872
+ const { name: rootName, subpath } = splitImport(name);
1873
+ throw new Error(
1874
+ `wpExternals: \`bundle\` accepts bare package names only, got '${name}'.` + (subpath ? ` Force-bundling is a whole-package verdict \u2014 list '${rootName}' instead of the '${subpath}' subpath.` : "")
1875
+ );
1876
+ }
1877
+ const bundledPackages = /* @__PURE__ */ new Set([...DEFAULT_BUNDLED_PACKAGES, ...options.bundle ?? []]);
1740
1878
  function matchesScriptModuleNamespace(packageName) {
1741
1879
  if (!packageName.startsWith("@")) return false;
1742
1880
  const slashIndex = packageName.indexOf("/");
@@ -1767,6 +1905,7 @@ function wpExternals(options = {}) {
1767
1905
  return false;
1768
1906
  }
1769
1907
  function resolveExternal(specifier) {
1908
+ if (bundledPackages.has(splitImport(specifier).name)) return null;
1770
1909
  if (vendorExternals[specifier]) {
1771
1910
  const vendor = vendorExternals[specifier];
1772
1911
  return {
@@ -1897,9 +2036,15 @@ function wpExternals(options = {}) {
1897
2036
  await init;
1898
2037
  let imports;
1899
2038
  try {
1900
- [imports] = parse(code);
1901
- } catch {
1902
- return null;
2039
+ [imports] = parse(code, id);
2040
+ } catch (error) {
2041
+ const filePath = id.split("?")[0] ?? id;
2042
+ if (!JS_SOURCE_EXTENSION.test(filePath)) return null;
2043
+ return this.error(
2044
+ `wp-externals could not parse the imports of ${filePath}: ${error instanceof Error ? error.message : String(error)}
2045
+ Its @wordpress/* imports would ship un-rewritten (broken window.wp.* reads and an incomplete .asset.php), so this is a hard error.
2046
+ Most likely cause: JSX in a file that isn't .jsx/.tsx. JSX must live in .jsx/.tsx files \u2014 do not use \`rollupOptions.moduleTypes\` to map .js to jsx; rename the files instead. The JSX transform runs *after* plugin transform hooks, so \`moduleTypes\` makes the build pass while this plugin still sees raw JSX and produces broken output.`
2047
+ );
1903
2048
  }
1904
2049
  if (imports.length === 0) return null;
1905
2050
  const magicString = new MagicString(code);
@@ -2062,27 +2207,342 @@ function wpExternals(options = {}) {
2062
2207
  }
2063
2208
  }
2064
2209
  };
2065
- return [resolverPlugin, transformPlugin];
2210
+ const bundlePlugin = {
2211
+ name: `wp-externals-bundle(${buildType})`,
2212
+ enforce: "pre",
2213
+ transform(code, id) {
2214
+ if (bundledPackages.size === 0) return null;
2215
+ if (id.startsWith("\0")) return null;
2216
+ const filePath = (id.split("?")[0] ?? id).replace(/\\/g, "/");
2217
+ const magicString = new MagicString(code);
2218
+ let codeChanged = false;
2219
+ for (const packageName of bundledPackages) {
2220
+ if (!code.includes(packageName)) continue;
2221
+ if (filePath.includes(`/node_modules/${packageName}/`)) continue;
2222
+ const pattern = new RegExp(
2223
+ `(?:\\bfrom|\\bimport|\\brequire)\\s*\\(?\\s*(['"])${escapeRegExp(
2224
+ packageName
2225
+ )}\\1`,
2226
+ "g"
2227
+ );
2228
+ for (const match of code.matchAll(pattern)) {
2229
+ const end = match.index + match[0].length - 1;
2230
+ magicString.overwrite(
2231
+ end - packageName.length,
2232
+ end,
2233
+ BUNDLED_MARKER_PREFIX + packageName
2234
+ );
2235
+ codeChanged = true;
2236
+ }
2237
+ }
2238
+ if (!codeChanged) return null;
2239
+ return {
2240
+ code: magicString.toString(),
2241
+ map: magicString.generateMap({ hires: true })
2242
+ };
2243
+ },
2244
+ resolveId(source, importer) {
2245
+ if (!source.startsWith(BUNDLED_MARKER_PREFIX)) return null;
2246
+ const packageName = source.slice(BUNDLED_MARKER_PREFIX.length);
2247
+ const fromPaths = [
2248
+ ...importer && !importer.startsWith("\0") ? [importer.split("?")[0]] : [],
2249
+ join(projectRoot, "package.json")
2250
+ ];
2251
+ const entry = resolvePackageEsmEntry(packageName, fromPaths);
2252
+ if (!entry) {
2253
+ return this.error(
2254
+ `wp-externals is configured to bundle '${packageName}' (see the \`bundle\` option), but the package is not installed${importer ? ` \u2014 resolved from ${importer} and ` : " under "}${projectRoot}.
2255
+ Force-bundling requires the package in node_modules, since its code has to be compiled into your output. Run \`npm install ${packageName}\`.`
2256
+ );
2257
+ }
2258
+ return entry;
2259
+ }
2260
+ };
2261
+ return [resolverPlugin, transformPlugin, bundlePlugin];
2262
+ }
2263
+ var MARKER_PARAM = "wp-entry";
2264
+ var MARKER_RE = new RegExp(`[?&]${MARKER_PARAM}=([^&]*)`);
2265
+ var PARTITIONABLE_EXTENSION = /\.[cm]?[jt]sx?$/;
2266
+ var INLINABLE_SYNTHETIC_ID = [/^\0?rolldown[:/]runtime(\.[cm]?js)?$/, /^\0wp-external-global:/];
2267
+ function markerOf(id) {
2268
+ return id.match(MARKER_RE)?.[1];
2269
+ }
2270
+ function withMarker(id, marker) {
2271
+ return `${id}${id.includes("?") ? "&" : "?"}${MARKER_PARAM}=${marker}`;
2272
+ }
2273
+ function stripEntryMarker(id) {
2274
+ return id.replace(
2275
+ new RegExp(`([?&])${MARKER_PARAM}=[^&]*&?`),
2276
+ (_match, lead) => lead === "?" ? "?" : "&"
2277
+ ).replace(/[?&]$/, "");
2278
+ }
2279
+ function mintMarker(entryId) {
2280
+ return crypto.createHash("sha1").update(entryId).digest("hex").slice(0, 8);
2281
+ }
2282
+ function isPartitionable(id) {
2283
+ if (id.includes("\0") || id.includes("?")) return false;
2284
+ return PARTITIONABLE_EXTENSION.test(id);
2285
+ }
2286
+ function isInlinableSyntheticChunk(chunk) {
2287
+ const moduleIds = Object.keys(chunk.modules ?? {});
2288
+ if (moduleIds.length === 0) return false;
2289
+ return moduleIds.every((id) => INLINABLE_SYNTHETIC_ID.some((pattern) => pattern.test(id)));
2290
+ }
2291
+ function objectKey(name) {
2292
+ return JSON.stringify(name);
2293
+ }
2294
+ function parseImportClause(statement) {
2295
+ const match = statement.match(/^\s*import\s*(?:([\s\S]*?)\s*from\s*)?['"][^'"]*['"]\s*;?\s*$/);
2296
+ if (!match) return null;
2297
+ const bindings = match[1]?.trim();
2298
+ if (!bindings) return { named: [] };
2299
+ const clause = { named: [] };
2300
+ let rest = bindings;
2301
+ const leadingDefault = rest.match(/^([A-Za-z_$][\w$]*)\s*(?:,\s*([\s\S]*))?$/);
2302
+ if (leadingDefault) {
2303
+ clause.default = leadingDefault[1];
2304
+ rest = leadingDefault[2]?.trim() ?? "";
2305
+ }
2306
+ if (rest.startsWith("*")) {
2307
+ const namespace = rest.match(/^\*\s*as\s+([A-Za-z_$][\w$]*)$/);
2308
+ if (!namespace) return null;
2309
+ clause.namespace = namespace[1];
2310
+ return clause;
2311
+ }
2312
+ if (rest.startsWith("{")) {
2313
+ if (!rest.endsWith("}")) return null;
2314
+ for (const binding of rest.slice(1, -1).split(",")) {
2315
+ const trimmed = binding.trim();
2316
+ if (!trimmed) continue;
2317
+ const aliased = trimmed.match(/^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/);
2318
+ if (aliased) {
2319
+ clause.named.push({ imported: aliased[1], local: aliased[2] });
2320
+ continue;
2321
+ }
2322
+ if (!/^[A-Za-z_$][\w$]*$/.test(trimmed)) return null;
2323
+ clause.named.push({ imported: trimmed, local: trimmed });
2324
+ }
2325
+ return clause;
2326
+ }
2327
+ return rest === "" ? clause : null;
2328
+ }
2329
+ function toInlineExpression(code) {
2330
+ const [imports, exports] = parse(code);
2331
+ if (imports.length > 0) return null;
2332
+ const body = code.replace(/^[ \t]*export\s*\{[^}]*\}\s*;?[ \t]*$/gm, "");
2333
+ if (body === code && exports.length > 0) return null;
2334
+ if (/^\s*(?:import|export)\b/m.test(body)) return null;
2335
+ const returned = exports.map((record) => `${objectKey(record.n)}: ${record.ln ?? record.n}`).join(", ");
2336
+ return `(() => {
2337
+ ${body.trim()}
2338
+ return { ${returned} };
2339
+ })()`;
2340
+ }
2341
+ function resolveChunkSpecifier(hostFileName, specifier) {
2342
+ const segments = hostFileName.split("/").slice(0, -1);
2343
+ for (const segment of specifier.split("/")) {
2344
+ if (segment === "." || segment === "") continue;
2345
+ if (segment === "..") segments.pop();
2346
+ else segments.push(segment);
2347
+ }
2348
+ return segments.join("/");
2349
+ }
2350
+ function shiftMappings(map, atLine, addedLines) {
2351
+ if (!map || typeof map.mappings !== "string" || addedLines <= 0) return;
2352
+ const lines = map.mappings.split(";");
2353
+ if (atLine > lines.length) return;
2354
+ lines.splice(atLine + 1, 0, ...new Array(addedLines).fill(""));
2355
+ map.mappings = lines.join(";");
2356
+ }
2357
+ function wpScriptDedupe() {
2358
+ const inFlight = /* @__PURE__ */ new Set();
2359
+ return {
2360
+ name: "wp-script-dedupe",
2361
+ // Mandatory: normal-bucket `resolveId` hooks are never called during a
2362
+ // Vite 8 build — Rolldown's native resolver answers between the `pre`
2363
+ // and normal buckets.
2364
+ enforce: "pre",
2365
+ apply: "build",
2366
+ async resolveId(source, importer, options) {
2367
+ if (!importer) return null;
2368
+ if (source.includes("\0")) return null;
2369
+ const key = `${importer}
2370
+ ${source}`;
2371
+ if (inFlight.has(key)) return null;
2372
+ const marker = markerOf(importer) ?? mintMarker(importer);
2373
+ inFlight.add(key);
2374
+ let resolved;
2375
+ try {
2376
+ resolved = await this.resolve(
2377
+ stripEntryMarker(source),
2378
+ stripEntryMarker(importer),
2379
+ {
2380
+ // `kind` rides along so plugins keying on `require-call`
2381
+ // (wpExternals' CJS shim) still see the truth.
2382
+ ...options,
2383
+ skipSelf: true
2384
+ }
2385
+ );
2386
+ } finally {
2387
+ inFlight.delete(key);
2388
+ }
2389
+ if (!resolved || resolved.external) return resolved;
2390
+ if (!isPartitionable(resolved.id)) return resolved;
2391
+ return { ...resolved, id: withMarker(resolved.id, marker) };
2392
+ },
2393
+ generateBundle: {
2394
+ // Load-bearing, not cosmetic: wpExternals emits its `.asset.php`
2395
+ // sidecars from `generateBundle`, unioning the dependencies of every
2396
+ // chunk an entry reaches — including the synthetic shim chunks this
2397
+ // hook inlines and deletes. Rolldown hands each plugin a freshly built
2398
+ // `bundle` object and drops `modules` mutations on write-back
2399
+ // (`collectChangedBundle` sends `modules: {}`), so the merge below
2400
+ // cannot be what keeps that union correct under Vite 8 — running after
2401
+ // the sidecars are emitted is.
2402
+ order: "post",
2403
+ async handler(_outputOptions, bundle) {
2404
+ await init;
2405
+ let inlinedSomething = true;
2406
+ while (inlinedSomething) {
2407
+ inlinedSomething = false;
2408
+ for (const [fileName, chunk] of Object.entries(bundle)) {
2409
+ if (chunk.type !== "chunk") continue;
2410
+ if (chunk.isEntry || chunk.isDynamicEntry) continue;
2411
+ if (!isInlinableSyntheticChunk(chunk)) continue;
2412
+ const expression = toInlineExpression(chunk.code);
2413
+ if (!expression) continue;
2414
+ let consumers = 0;
2415
+ let failures = 0;
2416
+ for (const [hostFileName, host] of Object.entries(bundle)) {
2417
+ if (host.type !== "chunk" || hostFileName === fileName) continue;
2418
+ if (!(host.imports ?? []).includes(fileName)) continue;
2419
+ consumers += 1;
2420
+ if (!inlineChunkInto(host, hostFileName, fileName, expression)) {
2421
+ failures += 1;
2422
+ continue;
2423
+ }
2424
+ absorbChunkMetadata(host, chunk, fileName);
2425
+ }
2426
+ if (consumers > 0 && failures === 0) {
2427
+ delete bundle[fileName];
2428
+ inlinedSomething = true;
2429
+ }
2430
+ }
2431
+ }
2432
+ for (const [fileName, chunk] of Object.entries(bundle)) {
2433
+ if (chunk.type !== "chunk" || chunk.isEntry || chunk.isDynamicEntry) continue;
2434
+ const consumers = Object.entries(bundle).filter(
2435
+ ([, host]) => host.type === "chunk" && (host.imports ?? []).includes(fileName)
2436
+ ).map(([hostFileName]) => hostFileName);
2437
+ if (consumers.length === 0) continue;
2438
+ const moduleIds = Object.keys(chunk.modules ?? {});
2439
+ const unpartitionable = moduleIds.filter(
2440
+ (id) => !id.includes("\0") && !PARTITIONABLE_EXTENSION.test(id.split("?")[0])
2441
+ );
2442
+ this.warn(
2443
+ `wp-script-dedupe: shared chunk ${fileName} could not be duplicated into its consumers (${consumers.join(", ")}). It holds: ${moduleIds.join(", ")}. ` + (unpartitionable.length > 0 ? `Only JS/TS modules can be partitioned per entry, so a shared non-JS module (${unpartitionable.join(", ")}) stays in a chunk a classic script cannot import. Re-export it from a per-entry .ts module, give each entry its own copy, or build these entries in module mode.` : `A classic script cannot import, so those entries will not load \u2014 report the module list above upstream.`)
2444
+ );
2445
+ }
2446
+ }
2447
+ }
2448
+ };
2449
+ }
2450
+ function absorbChunkMetadata(host, inlined, inlinedFileName) {
2451
+ host.modules = { ...host.modules, ...inlined.modules };
2452
+ host.moduleIds = [.../* @__PURE__ */ new Set([...host.moduleIds ?? [], ...inlined.moduleIds ?? []])];
2453
+ host.imports = (host.imports ?? []).filter((imported) => imported !== inlinedFileName);
2454
+ const importedBindings = host.importedBindings;
2455
+ if (importedBindings) delete importedBindings[inlinedFileName];
2456
+ mergeViteMetadata(host, inlined);
2457
+ }
2458
+ function mergeViteMetadata(host, inlined) {
2459
+ const hostMetadata = host.viteMetadata;
2460
+ const inlinedMetadata = inlined.viteMetadata;
2461
+ if (!hostMetadata || !inlinedMetadata) return;
2462
+ for (const cssFileName of inlinedMetadata.importedCss ?? []) {
2463
+ hostMetadata.importedCss?.add(cssFileName);
2464
+ }
2465
+ for (const assetFileName of inlinedMetadata.importedAssets ?? []) {
2466
+ hostMetadata.importedAssets?.add(assetFileName);
2467
+ }
2468
+ }
2469
+ function inlineChunkInto(host, hostFileName, targetFileName, expression) {
2470
+ const [imports] = parse(host.code);
2471
+ const replacements = [];
2472
+ let counter = 0;
2473
+ for (const record of imports) {
2474
+ if (record.d !== -1) continue;
2475
+ if (!record.n) continue;
2476
+ if (resolveChunkSpecifier(hostFileName, record.n) !== targetFileName) continue;
2477
+ const clause = parseImportClause(host.code.slice(record.ss, record.se));
2478
+ if (!clause) return false;
2479
+ const named = clause.named.map(({ imported, local }) => `${objectKey(imported)}: ${local}`).join(", ");
2480
+ let code;
2481
+ if (clause.default || clause.namespace && clause.named.length > 0) {
2482
+ const temp = `__wpInlined${counter++}`;
2483
+ const parts = [`const ${temp} = ${expression};`];
2484
+ if (clause.default) parts.push(`const ${clause.default} = ${temp}["default"];`);
2485
+ if (clause.namespace) parts.push(`const ${clause.namespace} = ${temp};`);
2486
+ if (named) parts.push(`const { ${named} } = ${temp};`);
2487
+ code = parts.join("\n");
2488
+ } else if (clause.namespace) {
2489
+ code = `const ${clause.namespace} = ${expression};`;
2490
+ } else if (named) {
2491
+ code = `const { ${named} } = ${expression};`;
2492
+ } else {
2493
+ code = `${expression};`;
2494
+ }
2495
+ replacements.push({ start: record.ss, end: record.se, code });
2496
+ }
2497
+ if (replacements.length === 0) return false;
2498
+ const magicString = new MagicString(host.code);
2499
+ for (const replacement of replacements.reverse()) {
2500
+ magicString.overwrite(replacement.start, replacement.end, replacement.code);
2501
+ const line = host.code.slice(0, replacement.start).split("\n").length - 1;
2502
+ const addedLines = replacement.code.split("\n").length - host.code.slice(replacement.start, replacement.end).split("\n").length;
2503
+ shiftMappings(host.map, line, addedLines);
2504
+ }
2505
+ host.code = magicString.toString();
2506
+ return true;
2066
2507
  }
2067
2508
 
2068
2509
  // src/wp-script-iife.ts
2510
+ var CLASSIC_SCRIPT_DEFINE = {
2511
+ "import.meta.url": "document.currentScript && document.currentScript.src",
2512
+ "import.meta.resolve": "undefined"
2513
+ };
2514
+ var LIVE_MODULE_STATEMENT = /^[ \t]*(?:import\s*(?:[{*"']|[A-Za-z_$][\w$]*\s*(?:,|from\b))|export\s*(?:[{*]|(?:default|const|let|var|function|class|async)\b))[^\n]*/m;
2069
2515
  function wpScriptIife() {
2070
2516
  return {
2071
2517
  name: "wp-script-iife",
2518
+ // Build-only. Vite filters plugins by `apply` before it runs `config`,
2519
+ // which is what keeps the `import.meta` define below out of `vite serve`
2520
+ // — the dev server ships real ESM to the browser, where `import.meta.url`
2521
+ // is both legal and load-bearing.
2522
+ apply: "build",
2523
+ config(userConfig) {
2524
+ const define = {};
2525
+ for (const [key, value] of Object.entries(CLASSIC_SCRIPT_DEFINE)) {
2526
+ if (userConfig.define && key in userConfig.define) continue;
2527
+ define[key] = value;
2528
+ }
2529
+ return Object.keys(define).length > 0 ? { define } : null;
2530
+ },
2072
2531
  async writeBundle(options, bundle) {
2073
2532
  const { readFile, writeFile } = await import('fs/promises');
2074
- const { join: join4 } = await import('path');
2533
+ const { join: join5 } = await import('path');
2075
2534
  const outDir = options.dir ?? ".";
2076
2535
  for (const [fileName, output] of Object.entries(bundle)) {
2077
2536
  if (output.type !== "chunk") continue;
2078
2537
  if (!fileName.endsWith(".js") && !fileName.endsWith(".mjs")) continue;
2079
2538
  if (!output.isEntry && !output.isDynamicEntry) continue;
2080
- const filePath = join4(outDir, fileName);
2539
+ const filePath = join5(outDir, fileName);
2081
2540
  let chunkSource = await readFile(filePath, "utf8");
2082
2541
  const sideEffectImport = /^\s*import\s+["'][^"']+["'];?\s*\n?/;
2083
2542
  while (sideEffectImport.test(chunkSource)) {
2084
2543
  chunkSource = chunkSource.replace(sideEffectImport, "");
2085
2544
  }
2545
+ assertClassicScript(fileName, chunkSource, output.isEntry !== true);
2086
2546
  await writeFile(filePath, `(()=>{
2087
2547
  ${chunkSource}
2088
2548
  })();
@@ -2091,6 +2551,37 @@ ${chunkSource}
2091
2551
  }
2092
2552
  };
2093
2553
  }
2554
+ function assertClassicScript(fileName, code, isDynamicOnly = false) {
2555
+ let parseError;
2556
+ try {
2557
+ new Function(code);
2558
+ return;
2559
+ } catch (error) {
2560
+ if (!(error instanceof SyntaxError)) throw error;
2561
+ parseError = error;
2562
+ }
2563
+ const liveStatement = code.match(LIVE_MODULE_STATEMENT);
2564
+ if (liveStatement) {
2565
+ const specifiers = [...code.matchAll(/^[ \t]*(?:import|export)\b[^\n]*?["']([^"']+)["']/gm)].map((match) => match[1]).filter((specifier, index, all) => all.indexOf(specifier) === index);
2566
+ throw new Error(
2567
+ `wp-script-iife: ${fileName} still contains a top-level module statement, which is a SyntaxError once WordPress enqueues it as a classic script:
2568
+ ${liveStatement[0].trim()}
2569
+ ` + (specifiers.length > 0 ? ` Imported chunk(s): ${specifiers.join(", ")}
2570
+ ` : "") + (isDynamicOnly ? ` This file is a dynamic-import chunk, so it needs its \`export\`s and cannot be an IIFE. A script-mode entry can't lazily load one either (WP has no module context to resolve it in) \u2014 drop the dynamic \`import()\`, or build this entry in module mode.` : ` This is almost always a shared chunk: a module two or more script-mode entries import gets hoisted into its own chunk instead of being duplicated into each entry. Add the wpScriptDedupe() plugin (auto-included by wp() in script mode \u2014 check you haven't set \`scriptDedupe: false\`), or build this entry in module mode.`)
2571
+ );
2572
+ }
2573
+ if (/\bimport\.meta\b/.test(code)) {
2574
+ throw new Error(
2575
+ `wp-script-iife: ${fileName} references \`import.meta\`, which is a SyntaxError once WordPress enqueues it as a classic script.
2576
+ Vite injects its module-preload helper (which reads \`import.meta.url\`) whenever the bundled graph contains a dynamic \`import()\` \u2014 \`build.modulePreload: false\` does not remove it. This plugin's \`config()\` hook defines \`import.meta.url\`/\`import.meta.resolve\` away; a surviving reference means either that define was overridden, or the code touches another \`import.meta\` member. Extend \`define\` in your Vite config, or move this entry to module mode.`
2577
+ );
2578
+ }
2579
+ throw new Error(
2580
+ `wp-script-iife: ${fileName} does not parse as a classic script, so WordPress could not load it:
2581
+ ${parseError.message}
2582
+ Nothing in this plugin's usual suspects (a shared chunk's \`import\`, an \`import.meta\` reference) explains it \u2014 inspect the written file, and report it upstream if the source is valid.`
2583
+ );
2584
+ }
2094
2585
 
2095
2586
  // src/wp-clean-css-chunks.ts
2096
2587
  function wpCleanCssChunks() {
@@ -2107,8 +2598,66 @@ function wpCleanCssChunks() {
2107
2598
  }
2108
2599
  };
2109
2600
  }
2601
+ var STYLESHEET_EXTENSION = /\.(css|scss|sass)$/i;
2602
+ function wpCssAssetInfo() {
2603
+ const cssEntries = /* @__PURE__ */ new Map();
2604
+ return {
2605
+ name: "wp-css-asset-info",
2606
+ configResolved(config) {
2607
+ cssEntries.clear();
2608
+ const input = config.build?.rollupOptions?.input;
2609
+ if (!input || typeof input !== "object" || Array.isArray(input)) return;
2610
+ for (const [entryName, source] of Object.entries(input)) {
2611
+ if (typeof source === "string" && STYLESHEET_EXTENSION.test(source)) {
2612
+ cssEntries.set(entryName, source);
2613
+ }
2614
+ }
2615
+ },
2616
+ generateBundle: {
2617
+ // MUST be post: wpBlockStyles strips its dedup markers from CSS
2618
+ // assets in its own normal-order generateBundle, and the version has
2619
+ // to hash the FINAL shipped bytes, not the pre-strip content.
2620
+ order: "post",
2621
+ handler(_outputOptions, bundle) {
2622
+ for (const [entryName, sourcePath] of cssEntries) {
2623
+ const expectedFileName = `${entryName}.css`;
2624
+ const cssAsset = findCssAsset(bundle, expectedFileName, sourcePath);
2625
+ if (!cssAsset) continue;
2626
+ const assetPhpFileName = cssAsset.fileName.replace(/\.css$/i, ".asset.php");
2627
+ if (bundle[assetPhpFileName]) continue;
2628
+ const version = crypto.createHash("md5").update(cssAsset.source).digest("hex");
2629
+ this.emitFile({
2630
+ type: "asset",
2631
+ fileName: assetPhpFileName,
2632
+ source: `<?php return array( 'dependencies' => array(), 'version' => '${version}' );
2633
+ `
2634
+ });
2635
+ }
2636
+ }
2637
+ }
2638
+ };
2639
+ }
2640
+ function findCssAsset(bundle, expectedFileName, sourcePath) {
2641
+ const direct = bundle[expectedFileName];
2642
+ if (direct && direct.type === "asset") return direct;
2643
+ for (const output of Object.values(bundle)) {
2644
+ if (output.type !== "asset") continue;
2645
+ if (output.names?.includes(expectedFileName)) return output;
2646
+ if (output.originalFileNames?.some((name) => sourcePath.endsWith(name))) return output;
2647
+ }
2648
+ return void 0;
2649
+ }
2110
2650
 
2111
2651
  // src/wp.ts
2652
+ function wpModuleEntrySignatures() {
2653
+ return {
2654
+ name: "wp-module-entry-signatures",
2655
+ config(userConfig) {
2656
+ if (userConfig.build?.rollupOptions?.preserveEntrySignatures !== void 0) return null;
2657
+ return { build: { rollupOptions: { preserveEntrySignatures: "exports-only" } } };
2658
+ }
2659
+ };
2660
+ }
2112
2661
  function wp(options = {}) {
2113
2662
  const buildType = options.buildType ?? "script";
2114
2663
  const plugins = [];
@@ -2180,15 +2729,24 @@ function wp(options = {}) {
2180
2729
  })
2181
2730
  );
2182
2731
  }
2732
+ if (buildType === "script" && options.scriptDedupe !== false) {
2733
+ plugins.push(wpScriptDedupe());
2734
+ }
2183
2735
  if (buildType === "script" && options.iife !== false) {
2184
2736
  plugins.push(wpScriptIife());
2185
2737
  }
2186
2738
  if (options.cleanCssChunks !== false) {
2187
2739
  plugins.push(wpCleanCssChunks());
2188
2740
  }
2741
+ if (options.cssAssetInfo !== false) {
2742
+ plugins.push(wpCssAssetInfo());
2743
+ }
2744
+ if (buildType === "module") {
2745
+ plugins.push(wpModuleEntrySignatures());
2746
+ }
2189
2747
  return plugins;
2190
2748
  }
2191
2749
 
2192
- export { wp, wpAdminPages, wpBlockStyles, wpBlocks, wpCleanCssChunks, wpCopyAssets, wpExternals, wpScriptIife };
2750
+ export { DEFAULT_BUNDLED_PACKAGES, wp, wpAdminPages, wpBlockStyles, wpBlocks, wpCleanCssChunks, wpCopyAssets, wpCssAssetInfo, wpExternals, wpScriptDedupe, wpScriptIife };
2193
2751
  //# sourceMappingURL=index.js.map
2194
2752
  //# sourceMappingURL=index.js.map