diorama-js 0.1.0 → 0.2.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/dist/react.d.cts CHANGED
@@ -66,6 +66,11 @@ interface RenderOptions {
66
66
  projectType?: ProjectType;
67
67
  /** Override entry point detection. */
68
68
  entryPoint?: string;
69
+ /**
70
+ * Tailwind CSS handling. `'auto'` (default) loads Tailwind's Play CDN when the
71
+ * project is detected to use Tailwind; `true` forces it on; `false` disables it.
72
+ */
73
+ tailwind?: 'auto' | boolean;
69
74
  /** Glob patterns of files to include. */
70
75
  include?: string[];
71
76
  /** Glob patterns of files to exclude. */
@@ -125,6 +130,11 @@ interface DioramaProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onLoad' | '
125
130
  frame?: FrameStyle;
126
131
  /** Allow click-to-expand to fill viewport. Default: `false`. */
127
132
  expand?: boolean;
133
+ /**
134
+ * Tailwind CSS handling. `'auto'` (default) loads Tailwind's Play CDN when the
135
+ * project uses Tailwind; `true` forces it on; `false` disables it.
136
+ */
137
+ tailwind?: 'auto' | boolean;
128
138
  /** Called when rendering completes. */
129
139
  onLoad?: () => void;
130
140
  /** Called on error. */
package/dist/react.d.ts CHANGED
@@ -66,6 +66,11 @@ interface RenderOptions {
66
66
  projectType?: ProjectType;
67
67
  /** Override entry point detection. */
68
68
  entryPoint?: string;
69
+ /**
70
+ * Tailwind CSS handling. `'auto'` (default) loads Tailwind's Play CDN when the
71
+ * project is detected to use Tailwind; `true` forces it on; `false` disables it.
72
+ */
73
+ tailwind?: 'auto' | boolean;
69
74
  /** Glob patterns of files to include. */
70
75
  include?: string[];
71
76
  /** Glob patterns of files to exclude. */
@@ -125,6 +130,11 @@ interface DioramaProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onLoad' | '
125
130
  frame?: FrameStyle;
126
131
  /** Allow click-to-expand to fill viewport. Default: `false`. */
127
132
  expand?: boolean;
133
+ /**
134
+ * Tailwind CSS handling. `'auto'` (default) loads Tailwind's Play CDN when the
135
+ * project uses Tailwind; `true` forces it on; `false` disables it.
136
+ */
137
+ tailwind?: 'auto' | boolean;
128
138
  /** Called when rendering completes. */
129
139
  onLoad?: () => void;
130
140
  /** Called on error. */
package/dist/react.js CHANGED
@@ -624,6 +624,7 @@ function analyzeProject(project, overrideType, overrideEntry) {
624
624
  hasBareImports: hasBareImports(files),
625
625
  isVite
626
626
  });
627
+ const usesTailwind = usesTailwindCSS(files);
627
628
  return {
628
629
  type,
629
630
  entryPoint: htmlEntry,
@@ -632,7 +633,8 @@ function analyzeProject(project, overrideType, overrideEntry) {
632
633
  hasJSX,
633
634
  hasTypeScript,
634
635
  jsEntryPoint: jsEntry,
635
- isVite
636
+ isVite,
637
+ usesTailwind
636
638
  };
637
639
  }
638
640
  function detectProjectType(input) {
@@ -654,6 +656,17 @@ function hasBareImports(files) {
654
656
  }
655
657
  return false;
656
658
  }
659
+ function usesTailwindCSS(files) {
660
+ for (const [path, content] of files) {
661
+ if (/(?:^|\/)tailwind\.config\.(?:js|cjs|mjs|ts)$/.test(path)) {
662
+ return true;
663
+ }
664
+ if (path.endsWith(".css") && /@tailwind\b|@apply\b/.test(content)) {
665
+ return true;
666
+ }
667
+ }
668
+ return false;
669
+ }
657
670
 
658
671
  // src/core/sandbox.ts
659
672
  function buildErrorHTML(message) {
@@ -1508,6 +1521,18 @@ function getCDNProvider(name) {
1508
1521
  return esmShProvider;
1509
1522
  }
1510
1523
  }
1524
+ function buildCSSURL(specifier, options = {}) {
1525
+ const { dependencies = {}, cdnProvider = "esm.sh" } = options;
1526
+ const clean = specifier.replace(/[?#].*$/, "");
1527
+ const { packageName, subpath } = parseSpecifier(clean);
1528
+ const version = dependencies[packageName];
1529
+ const ver = version ? `@${version}` : "";
1530
+ const sub = subpath ? `/${subpath}` : "";
1531
+ if (cdnProvider === "unpkg") {
1532
+ return `https://unpkg.com/${packageName}${ver}${sub}`;
1533
+ }
1534
+ return `https://esm.sh/${packageName}${ver}${sub}`;
1535
+ }
1511
1536
  function rewriteImports(source, options = {}) {
1512
1537
  const {
1513
1538
  dependencies = {},
@@ -1521,6 +1546,9 @@ function rewriteImports(source, options = {}) {
1521
1546
  if (raw.startsWith(".") || raw.startsWith("/") || /^https?:\/\//.test(raw)) {
1522
1547
  return raw;
1523
1548
  }
1549
+ if (/\.css(?:[?#]|$)/.test(raw)) {
1550
+ return raw;
1551
+ }
1524
1552
  const { packageName, subpath } = parseSpecifier(raw);
1525
1553
  if (NODE_BUILTINS.has(packageName) || NODE_BUILTINS.has(packageName.replace("node:", ""))) {
1526
1554
  throw new NodeBuiltinError(packageName);
@@ -1723,20 +1751,25 @@ var ASSET_IMPORT_EXTENSIONS = {
1723
1751
  ".otf": "font/otf"
1724
1752
  };
1725
1753
  function assembleHTML(options) {
1726
- const { project, config, transformedFiles } = options;
1754
+ const { project, config, transformedFiles, cdnProvider, tailwind = "auto" } = options;
1727
1755
  const files = transformedFiles ?? project.files;
1756
+ let result;
1728
1757
  switch (config.type) {
1729
1758
  case "static":
1730
- return assembleStatic(files, project, config);
1759
+ result = assembleStatic(files, project, config);
1760
+ break;
1731
1761
  case "static-esm":
1732
1762
  case "jsx":
1733
1763
  case "typescript":
1734
1764
  case "jsx-typescript":
1735
1765
  case "vite":
1736
- return assembleESM(files, project, config);
1766
+ result = assembleESM(files, project, config, cdnProvider);
1767
+ break;
1737
1768
  default:
1738
1769
  throw new AssemblyError(`Unsupported project type: ${config.type}`);
1739
1770
  }
1771
+ result.html = applyTailwindRuntime(result.html, project, config, tailwind);
1772
+ return result;
1740
1773
  }
1741
1774
  function assembleStatic(files, project, config) {
1742
1775
  let html = files.get(config.entryPoint);
@@ -1750,10 +1783,13 @@ function assembleStatic(files, project, config) {
1750
1783
  html = inlineAssets(html, project, config.entryPoint);
1751
1784
  return { html, usesESM: false };
1752
1785
  }
1753
- function assembleESM(files, project, config) {
1786
+ function assembleESM(files, project, config, cdnProvider) {
1754
1787
  const isGenerated = config.entryPoint === "__generated__/index.html";
1755
1788
  rewriteAssetImports(files, project);
1756
- const cssFromJS = extractCSSImports(files);
1789
+ const { css: cssFromJS, links: cssLinks } = extractCSSImports(files, {
1790
+ dependencies: config.dependencies,
1791
+ cdnProvider
1792
+ });
1757
1793
  if (config.isVite) {
1758
1794
  injectImportMetaEnv(files);
1759
1795
  }
@@ -1772,6 +1808,9 @@ function assembleESM(files, project, config) {
1772
1808
  html = rewriteScriptSrcsToImportMap(html, config.entryPoint);
1773
1809
  html = inlineAssets(html, project, config.entryPoint);
1774
1810
  }
1811
+ for (const href of cssLinks) {
1812
+ html = injectIntoHead(html, `<link rel="stylesheet" href="${href}">`);
1813
+ }
1775
1814
  if (cssFromJS) {
1776
1815
  html = injectIntoHead(html, `<style>
1777
1816
  ${cssFromJS}
@@ -2021,20 +2060,32 @@ function rewriteAssetImports(files, project) {
2021
2060
  }
2022
2061
  }
2023
2062
  }
2024
- function extractCSSImports(files) {
2063
+ function extractCSSImports(files, options = {}) {
2025
2064
  const cssChunks = [];
2026
- const cssImportRe = /import\s+['"]([^'"]+\.css)['"]\s*;?/g;
2065
+ const links = [];
2066
+ const seenLinks = /* @__PURE__ */ new Set();
2067
+ const cssImportRe = /import\s+['"]([^'"]+\.css(?:[?#][^'"]*)?)['"]\s*;?/g;
2027
2068
  for (const [path, content] of files) {
2028
2069
  if (!/\.(js|ts|jsx|tsx|mjs)$/.test(path)) continue;
2029
2070
  let modified = content;
2030
2071
  let match;
2072
+ cssImportRe.lastIndex = 0;
2031
2073
  while ((match = cssImportRe.exec(content)) !== null) {
2032
- const cssPath = match[1];
2033
- const resolved = resolvePath(directoryOf(path), cssPath);
2034
- const css = files.get(resolved);
2035
- if (css) {
2036
- cssChunks.push(`/* ${resolved} */
2074
+ const specifier = match[1];
2075
+ const bare = specifier.replace(/[?#].*$/, "");
2076
+ if (bare.startsWith(".") || bare.startsWith("/")) {
2077
+ const resolved = resolvePath(directoryOf(path), bare);
2078
+ const css = files.get(resolved);
2079
+ if (css) {
2080
+ cssChunks.push(`/* ${resolved} */
2037
2081
  ${css}`);
2082
+ }
2083
+ } else {
2084
+ const href = /^https?:\/\//.test(bare) ? bare : buildCSSURL(bare, options);
2085
+ if (!seenLinks.has(href)) {
2086
+ seenLinks.add(href);
2087
+ links.push(href);
2088
+ }
2038
2089
  }
2039
2090
  modified = modified.replace(match[0], "");
2040
2091
  }
@@ -2042,7 +2093,95 @@ ${css}`);
2042
2093
  files.set(path, modified);
2043
2094
  }
2044
2095
  }
2045
- return cssChunks.join("\n\n");
2096
+ return { css: cssChunks.join("\n\n"), links };
2097
+ }
2098
+ var TAILWIND_DIRECTIVE_RE = /@tailwind\b|@apply\b/;
2099
+ var TAILWIND_CDN_URL = "https://cdn.tailwindcss.com";
2100
+ function hasTailwindDirectives(css) {
2101
+ return TAILWIND_DIRECTIVE_RE.test(css);
2102
+ }
2103
+ function applyTailwindRuntime(html, project, config, tailwind) {
2104
+ const enabled = tailwind === true || tailwind === "auto" && (config.usesTailwind ?? usesTailwindCSS(project.files));
2105
+ if (!enabled) return html;
2106
+ html = html.replace(
2107
+ /<style>([\s\S]*?)<\/style>/g,
2108
+ (match, css) => hasTailwindDirectives(css) ? `<style type="text/tailwindcss">${css}</style>` : match
2109
+ );
2110
+ let injection = `<script src="${TAILWIND_CDN_URL}"></script>`;
2111
+ const configObject = extractTailwindConfig(project.files);
2112
+ if (configObject) {
2113
+ injection += `
2114
+ <script>tailwind.config = ${configObject};</script>`;
2115
+ }
2116
+ return injectIntoHead(html, injection);
2117
+ }
2118
+ function extractTailwindConfig(files) {
2119
+ let source;
2120
+ for (const [path, content] of files) {
2121
+ if (/(?:^|\/)tailwind\.config\.(?:js|cjs|mjs|ts)$/.test(path)) {
2122
+ source = content;
2123
+ break;
2124
+ }
2125
+ }
2126
+ if (!source) return null;
2127
+ const object = extractBalancedObject(source);
2128
+ if (!object || !isSafeConfigObject(object)) return null;
2129
+ return object;
2130
+ }
2131
+ function extractBalancedObject(source) {
2132
+ const opener = source.match(/(?:export\s+default|module\.exports\s*=)\s*\{/);
2133
+ if (!opener || opener.index === void 0) return null;
2134
+ const start = opener.index + opener[0].length - 1;
2135
+ let depth = 0;
2136
+ let str = null;
2137
+ let lineComment = false;
2138
+ let blockComment = false;
2139
+ for (let i = start; i < source.length; i++) {
2140
+ const c = source[i];
2141
+ const n = source[i + 1];
2142
+ if (lineComment) {
2143
+ if (c === "\n") lineComment = false;
2144
+ continue;
2145
+ }
2146
+ if (blockComment) {
2147
+ if (c === "*" && n === "/") {
2148
+ blockComment = false;
2149
+ i++;
2150
+ }
2151
+ continue;
2152
+ }
2153
+ if (str) {
2154
+ if (c === "\\") {
2155
+ i++;
2156
+ continue;
2157
+ }
2158
+ if (c === str) str = null;
2159
+ continue;
2160
+ }
2161
+ if (c === "/" && n === "/") {
2162
+ lineComment = true;
2163
+ i++;
2164
+ continue;
2165
+ }
2166
+ if (c === "/" && n === "*") {
2167
+ blockComment = true;
2168
+ i++;
2169
+ continue;
2170
+ }
2171
+ if (c === '"' || c === "'" || c === "`") {
2172
+ str = c;
2173
+ continue;
2174
+ }
2175
+ if (c === "{") depth++;
2176
+ else if (c === "}") {
2177
+ depth--;
2178
+ if (depth === 0) return source.slice(start, i + 1);
2179
+ }
2180
+ }
2181
+ return null;
2182
+ }
2183
+ function isSafeConfigObject(object) {
2184
+ return !(/\brequire\s*\(/.test(object) || /\bimport\b/.test(object) || /=>/.test(object) || /\bfunction\b/.test(object) || /`/.test(object) || /\bprocess\b/.test(object) || /\b__dirname\b|\b__filename\b/.test(object));
2046
2185
  }
2047
2186
  function injectImportMetaEnv(files) {
2048
2187
  const shim = `if(!import.meta.env){Object.defineProperty(import.meta,'env',{value:{MODE:'production',BASE_URL:'/',PROD:true,DEV:false,SSR:false}});}`;
@@ -2153,7 +2292,9 @@ var Diorama = class {
2153
2292
  const { html: html2, usesESM: usesESM2 } = assembleHTML({
2154
2293
  project,
2155
2294
  config,
2156
- transformedFiles: files
2295
+ transformedFiles: files,
2296
+ cdnProvider: this.options.cdnProvider,
2297
+ tailwind: options.tailwind ?? "auto"
2157
2298
  });
2158
2299
  return { html: html2, usesESM: usesESM2, repoName: repoName2 };
2159
2300
  };
@@ -2266,6 +2407,7 @@ var DioramaPreview = forwardRef(
2266
2407
  height = "500px",
2267
2408
  frame,
2268
2409
  expand,
2410
+ tailwind,
2269
2411
  onLoad,
2270
2412
  onError,
2271
2413
  options,
@@ -2292,6 +2434,7 @@ var DioramaPreview = forwardRef(
2292
2434
  height,
2293
2435
  frame,
2294
2436
  expand,
2437
+ tailwind,
2295
2438
  onLoad,
2296
2439
  onError: (err) => {
2297
2440
  onError?.(err);
@@ -2312,7 +2455,7 @@ var DioramaPreview = forwardRef(
2312
2455
  instanceRef.current?.destroy();
2313
2456
  instanceRef.current = null;
2314
2457
  };
2315
- }, [repo, branch, subdirectory, loading, placeholder, height, frame, expand]);
2458
+ }, [repo, branch, subdirectory, loading, placeholder, height, frame, expand, tailwind]);
2316
2459
  const containerStyle = {
2317
2460
  width: "100%",
2318
2461
  minHeight: height,