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/index.d.cts CHANGED
@@ -85,6 +85,8 @@ interface ProjectConfig {
85
85
  jsEntryPoint?: string;
86
86
  /** Whether the project uses Vite as its build tool. */
87
87
  isVite: boolean;
88
+ /** Whether the project relies on Tailwind CSS (directives or a config file). */
89
+ usesTailwind?: boolean;
88
90
  }
89
91
  type CDNProviderName = 'esm.sh' | 'skypack' | 'unpkg';
90
92
  type CacheStrategy = 'normal' | 'aggressive';
@@ -141,6 +143,11 @@ interface RenderOptions {
141
143
  projectType?: ProjectType;
142
144
  /** Override entry point detection. */
143
145
  entryPoint?: string;
146
+ /**
147
+ * Tailwind CSS handling. `'auto'` (default) loads Tailwind's Play CDN when the
148
+ * project is detected to use Tailwind; `true` forces it on; `false` disables it.
149
+ */
150
+ tailwind?: 'auto' | boolean;
144
151
  /** Glob patterns of files to include. */
145
152
  include?: string[];
146
153
  /** Glob patterns of files to exclude. */
package/dist/index.d.ts CHANGED
@@ -85,6 +85,8 @@ interface ProjectConfig {
85
85
  jsEntryPoint?: string;
86
86
  /** Whether the project uses Vite as its build tool. */
87
87
  isVite: boolean;
88
+ /** Whether the project relies on Tailwind CSS (directives or a config file). */
89
+ usesTailwind?: boolean;
88
90
  }
89
91
  type CDNProviderName = 'esm.sh' | 'skypack' | 'unpkg';
90
92
  type CacheStrategy = 'normal' | 'aggressive';
@@ -141,6 +143,11 @@ interface RenderOptions {
141
143
  projectType?: ProjectType;
142
144
  /** Override entry point detection. */
143
145
  entryPoint?: string;
146
+ /**
147
+ * Tailwind CSS handling. `'auto'` (default) loads Tailwind's Play CDN when the
148
+ * project is detected to use Tailwind; `true` forces it on; `false` disables it.
149
+ */
150
+ tailwind?: 'auto' | boolean;
144
151
  /** Glob patterns of files to include. */
145
152
  include?: string[];
146
153
  /** Glob patterns of files to exclude. */
package/dist/index.js CHANGED
@@ -638,6 +638,7 @@ function analyzeProject(project, overrideType, overrideEntry) {
638
638
  hasBareImports: hasBareImports(files),
639
639
  isVite
640
640
  });
641
+ const usesTailwind = usesTailwindCSS(files);
641
642
  return {
642
643
  type,
643
644
  entryPoint: htmlEntry,
@@ -646,7 +647,8 @@ function analyzeProject(project, overrideType, overrideEntry) {
646
647
  hasJSX,
647
648
  hasTypeScript,
648
649
  jsEntryPoint: jsEntry,
649
- isVite
650
+ isVite,
651
+ usesTailwind
650
652
  };
651
653
  }
652
654
  function detectProjectType(input) {
@@ -668,6 +670,17 @@ function hasBareImports(files) {
668
670
  }
669
671
  return false;
670
672
  }
673
+ function usesTailwindCSS(files) {
674
+ for (const [path, content] of files) {
675
+ if (/(?:^|\/)tailwind\.config\.(?:js|cjs|mjs|ts)$/.test(path)) {
676
+ return true;
677
+ }
678
+ if (path.endsWith(".css") && /@tailwind\b|@apply\b/.test(content)) {
679
+ return true;
680
+ }
681
+ }
682
+ return false;
683
+ }
671
684
 
672
685
  // src/core/sandbox.ts
673
686
  function buildErrorHTML(message) {
@@ -1522,6 +1535,18 @@ function getCDNProvider(name) {
1522
1535
  return esmShProvider;
1523
1536
  }
1524
1537
  }
1538
+ function buildCSSURL(specifier, options = {}) {
1539
+ const { dependencies = {}, cdnProvider = "esm.sh" } = options;
1540
+ const clean = specifier.replace(/[?#].*$/, "");
1541
+ const { packageName, subpath } = parseSpecifier(clean);
1542
+ const version = dependencies[packageName];
1543
+ const ver = version ? `@${version}` : "";
1544
+ const sub = subpath ? `/${subpath}` : "";
1545
+ if (cdnProvider === "unpkg") {
1546
+ return `https://unpkg.com/${packageName}${ver}${sub}`;
1547
+ }
1548
+ return `https://esm.sh/${packageName}${ver}${sub}`;
1549
+ }
1525
1550
  function rewriteImports(source, options = {}) {
1526
1551
  const {
1527
1552
  dependencies = {},
@@ -1535,6 +1560,9 @@ function rewriteImports(source, options = {}) {
1535
1560
  if (raw.startsWith(".") || raw.startsWith("/") || /^https?:\/\//.test(raw)) {
1536
1561
  return raw;
1537
1562
  }
1563
+ if (/\.css(?:[?#]|$)/.test(raw)) {
1564
+ return raw;
1565
+ }
1538
1566
  const { packageName, subpath } = parseSpecifier(raw);
1539
1567
  if (NODE_BUILTINS.has(packageName) || NODE_BUILTINS.has(packageName.replace("node:", ""))) {
1540
1568
  throw new NodeBuiltinError(packageName);
@@ -1737,20 +1765,25 @@ var ASSET_IMPORT_EXTENSIONS = {
1737
1765
  ".otf": "font/otf"
1738
1766
  };
1739
1767
  function assembleHTML(options) {
1740
- const { project, config, transformedFiles } = options;
1768
+ const { project, config, transformedFiles, cdnProvider, tailwind = "auto" } = options;
1741
1769
  const files = transformedFiles ?? project.files;
1770
+ let result;
1742
1771
  switch (config.type) {
1743
1772
  case "static":
1744
- return assembleStatic(files, project, config);
1773
+ result = assembleStatic(files, project, config);
1774
+ break;
1745
1775
  case "static-esm":
1746
1776
  case "jsx":
1747
1777
  case "typescript":
1748
1778
  case "jsx-typescript":
1749
1779
  case "vite":
1750
- return assembleESM(files, project, config);
1780
+ result = assembleESM(files, project, config, cdnProvider);
1781
+ break;
1751
1782
  default:
1752
1783
  throw new AssemblyError(`Unsupported project type: ${config.type}`);
1753
1784
  }
1785
+ result.html = applyTailwindRuntime(result.html, project, config, tailwind);
1786
+ return result;
1754
1787
  }
1755
1788
  function assembleStatic(files, project, config) {
1756
1789
  let html = files.get(config.entryPoint);
@@ -1764,10 +1797,13 @@ function assembleStatic(files, project, config) {
1764
1797
  html = inlineAssets(html, project, config.entryPoint);
1765
1798
  return { html, usesESM: false };
1766
1799
  }
1767
- function assembleESM(files, project, config) {
1800
+ function assembleESM(files, project, config, cdnProvider) {
1768
1801
  const isGenerated = config.entryPoint === "__generated__/index.html";
1769
1802
  rewriteAssetImports(files, project);
1770
- const cssFromJS = extractCSSImports(files);
1803
+ const { css: cssFromJS, links: cssLinks } = extractCSSImports(files, {
1804
+ dependencies: config.dependencies,
1805
+ cdnProvider
1806
+ });
1771
1807
  if (config.isVite) {
1772
1808
  injectImportMetaEnv(files);
1773
1809
  }
@@ -1786,6 +1822,9 @@ function assembleESM(files, project, config) {
1786
1822
  html = rewriteScriptSrcsToImportMap(html, config.entryPoint);
1787
1823
  html = inlineAssets(html, project, config.entryPoint);
1788
1824
  }
1825
+ for (const href of cssLinks) {
1826
+ html = injectIntoHead(html, `<link rel="stylesheet" href="${href}">`);
1827
+ }
1789
1828
  if (cssFromJS) {
1790
1829
  html = injectIntoHead(html, `<style>
1791
1830
  ${cssFromJS}
@@ -2035,20 +2074,32 @@ function rewriteAssetImports(files, project) {
2035
2074
  }
2036
2075
  }
2037
2076
  }
2038
- function extractCSSImports(files) {
2077
+ function extractCSSImports(files, options = {}) {
2039
2078
  const cssChunks = [];
2040
- const cssImportRe = /import\s+['"]([^'"]+\.css)['"]\s*;?/g;
2079
+ const links = [];
2080
+ const seenLinks = /* @__PURE__ */ new Set();
2081
+ const cssImportRe = /import\s+['"]([^'"]+\.css(?:[?#][^'"]*)?)['"]\s*;?/g;
2041
2082
  for (const [path, content] of files) {
2042
2083
  if (!/\.(js|ts|jsx|tsx|mjs)$/.test(path)) continue;
2043
2084
  let modified = content;
2044
2085
  let match;
2086
+ cssImportRe.lastIndex = 0;
2045
2087
  while ((match = cssImportRe.exec(content)) !== null) {
2046
- const cssPath = match[1];
2047
- const resolved = resolvePath(directoryOf(path), cssPath);
2048
- const css = files.get(resolved);
2049
- if (css) {
2050
- cssChunks.push(`/* ${resolved} */
2088
+ const specifier = match[1];
2089
+ const bare = specifier.replace(/[?#].*$/, "");
2090
+ if (bare.startsWith(".") || bare.startsWith("/")) {
2091
+ const resolved = resolvePath(directoryOf(path), bare);
2092
+ const css = files.get(resolved);
2093
+ if (css) {
2094
+ cssChunks.push(`/* ${resolved} */
2051
2095
  ${css}`);
2096
+ }
2097
+ } else {
2098
+ const href = /^https?:\/\//.test(bare) ? bare : buildCSSURL(bare, options);
2099
+ if (!seenLinks.has(href)) {
2100
+ seenLinks.add(href);
2101
+ links.push(href);
2102
+ }
2052
2103
  }
2053
2104
  modified = modified.replace(match[0], "");
2054
2105
  }
@@ -2056,7 +2107,95 @@ ${css}`);
2056
2107
  files.set(path, modified);
2057
2108
  }
2058
2109
  }
2059
- return cssChunks.join("\n\n");
2110
+ return { css: cssChunks.join("\n\n"), links };
2111
+ }
2112
+ var TAILWIND_DIRECTIVE_RE = /@tailwind\b|@apply\b/;
2113
+ var TAILWIND_CDN_URL = "https://cdn.tailwindcss.com";
2114
+ function hasTailwindDirectives(css) {
2115
+ return TAILWIND_DIRECTIVE_RE.test(css);
2116
+ }
2117
+ function applyTailwindRuntime(html, project, config, tailwind) {
2118
+ const enabled = tailwind === true || tailwind === "auto" && (config.usesTailwind ?? usesTailwindCSS(project.files));
2119
+ if (!enabled) return html;
2120
+ html = html.replace(
2121
+ /<style>([\s\S]*?)<\/style>/g,
2122
+ (match, css) => hasTailwindDirectives(css) ? `<style type="text/tailwindcss">${css}</style>` : match
2123
+ );
2124
+ let injection = `<script src="${TAILWIND_CDN_URL}"></script>`;
2125
+ const configObject = extractTailwindConfig(project.files);
2126
+ if (configObject) {
2127
+ injection += `
2128
+ <script>tailwind.config = ${configObject};</script>`;
2129
+ }
2130
+ return injectIntoHead(html, injection);
2131
+ }
2132
+ function extractTailwindConfig(files) {
2133
+ let source;
2134
+ for (const [path, content] of files) {
2135
+ if (/(?:^|\/)tailwind\.config\.(?:js|cjs|mjs|ts)$/.test(path)) {
2136
+ source = content;
2137
+ break;
2138
+ }
2139
+ }
2140
+ if (!source) return null;
2141
+ const object = extractBalancedObject(source);
2142
+ if (!object || !isSafeConfigObject(object)) return null;
2143
+ return object;
2144
+ }
2145
+ function extractBalancedObject(source) {
2146
+ const opener = source.match(/(?:export\s+default|module\.exports\s*=)\s*\{/);
2147
+ if (!opener || opener.index === void 0) return null;
2148
+ const start = opener.index + opener[0].length - 1;
2149
+ let depth = 0;
2150
+ let str = null;
2151
+ let lineComment = false;
2152
+ let blockComment = false;
2153
+ for (let i = start; i < source.length; i++) {
2154
+ const c = source[i];
2155
+ const n = source[i + 1];
2156
+ if (lineComment) {
2157
+ if (c === "\n") lineComment = false;
2158
+ continue;
2159
+ }
2160
+ if (blockComment) {
2161
+ if (c === "*" && n === "/") {
2162
+ blockComment = false;
2163
+ i++;
2164
+ }
2165
+ continue;
2166
+ }
2167
+ if (str) {
2168
+ if (c === "\\") {
2169
+ i++;
2170
+ continue;
2171
+ }
2172
+ if (c === str) str = null;
2173
+ continue;
2174
+ }
2175
+ if (c === "/" && n === "/") {
2176
+ lineComment = true;
2177
+ i++;
2178
+ continue;
2179
+ }
2180
+ if (c === "/" && n === "*") {
2181
+ blockComment = true;
2182
+ i++;
2183
+ continue;
2184
+ }
2185
+ if (c === '"' || c === "'" || c === "`") {
2186
+ str = c;
2187
+ continue;
2188
+ }
2189
+ if (c === "{") depth++;
2190
+ else if (c === "}") {
2191
+ depth--;
2192
+ if (depth === 0) return source.slice(start, i + 1);
2193
+ }
2194
+ }
2195
+ return null;
2196
+ }
2197
+ function isSafeConfigObject(object) {
2198
+ 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));
2060
2199
  }
2061
2200
  function injectImportMetaEnv(files) {
2062
2201
  const shim = `if(!import.meta.env){Object.defineProperty(import.meta,'env',{value:{MODE:'production',BASE_URL:'/',PROD:true,DEV:false,SSR:false}});}`;
@@ -2167,7 +2306,9 @@ var Diorama = class {
2167
2306
  const { html: html2, usesESM: usesESM2 } = assembleHTML({
2168
2307
  project,
2169
2308
  config,
2170
- transformedFiles: files
2309
+ transformedFiles: files,
2310
+ cdnProvider: this.options.cdnProvider,
2311
+ tailwind: options.tailwind ?? "auto"
2171
2312
  });
2172
2313
  return { html: html2, usesESM: usesESM2, repoName: repoName2 };
2173
2314
  };