webspresso 0.0.110 → 0.0.112

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.
@@ -53,7 +53,30 @@ const SERVER_ONLY_PACKAGES = new Set([
53
53
  ]);
54
54
 
55
55
  const SERVER_ONLY_INTERNAL_PATTERN =
56
- /(?:^|\/)(?:core\/(?:orm|services|auth|secrets|queue|mail|http|metrics|health|rate-limit)|src\/(?:server|file-router|api|pages)|models|services|jobs|migrations|pages\/api)(?:\/|$)/;
56
+ /(?:^(?:src\/)?(?:models|services|jobs|migrations|pages\/api)|(?:^|\/)(?:core\/(?:orm|services|auth|secrets|queue|mail|http|metrics|health|rate-limit)|src\/(?:server|file-router|api|pages)))(?:\/|$)/;
57
+
58
+ function isClientServiceFile(resolvedBase) {
59
+ const candidates = [
60
+ resolvedBase,
61
+ `${resolvedBase}.ts`,
62
+ `${resolvedBase}.js`,
63
+ `${resolvedBase}.mjs`,
64
+ `${resolvedBase}.tsx`,
65
+ `${resolvedBase}.jsx`,
66
+ ];
67
+ for (const cand of candidates) {
68
+ if (cand.includes('.client.')) return true;
69
+ if (fs.existsSync(cand) && fs.statSync(cand).isFile()) {
70
+ try {
71
+ const content = fs.readFileSync(cand, 'utf8');
72
+ if (/\bdefineClientService\b/.test(content)) {
73
+ return true;
74
+ }
75
+ } catch (_) {}
76
+ }
77
+ }
78
+ return false;
79
+ }
57
80
 
58
81
  const DEFAULT_ENTRYPOINTS = [
59
82
  'src/client/main.js',
@@ -290,6 +313,9 @@ function checkServerImportViolation(specifier, importerPath, cwd = process.cwd()
290
313
  const relToRoot = path.relative(cwd, resolved).replace(/\\/g, '/');
291
314
 
292
315
  if (SERVER_ONLY_INTERNAL_PATTERN.test(relToRoot)) {
316
+ if (/^(?:src\/)?services(?:\/|$)/.test(relToRoot) && isClientServiceFile(resolved)) {
317
+ return { forbidden: false };
318
+ }
293
319
  return {
294
320
  forbidden: true,
295
321
  reason: `Server module "${relToRoot}" (imported as "${raw}" in "${path.relative(cwd, importerPath)}") cannot be bundled into browser code.`,
@@ -700,6 +726,8 @@ function createDevBundler(options = {}, devReloadHub = null) {
700
726
  validateClientBundleBoundaries(entryPoints, cwd);
701
727
 
702
728
  const outbase = options.outbase || (fs.existsSync(path.resolve(cwd, 'src/client')) ? path.resolve(cwd, 'src/client') : undefined);
729
+ const splitting = options.splitting !== undefined ? Boolean(options.splitting) : format === 'esm';
730
+ const chunkNames = options.chunkNames || 'chunks/[name]-[hash]';
703
731
  const esbuildConfig = {
704
732
  entryPoints,
705
733
  outdir,
@@ -708,6 +736,12 @@ function createDevBundler(options = {}, devReloadHub = null) {
708
736
  minify,
709
737
  target,
710
738
  format,
739
+ splitting,
740
+ ...(splitting ? { chunkNames } : {}),
741
+ define: {
742
+ 'process.env.NODE_ENV': '"development"',
743
+ ...(options.define || {}),
744
+ },
711
745
  metafile,
712
746
  outbase,
713
747
  plugins: [createServerBoundaryPlugin(cwd), devReloadPlugin, ...userPlugins],
@@ -804,6 +838,7 @@ async function buildBundle(options = {}) {
804
838
  publicDir,
805
839
  manifestPath,
806
840
  hash: options.hash !== false,
841
+ splitting: options.splitting !== false && (options.format || 'esm') === 'esm',
807
842
  });
808
843
  }
809
844
 
@@ -812,8 +847,10 @@ async function buildBundle(options = {}) {
812
847
  const sourcemap = options.sourcemap !== undefined ? options.sourcemap : true;
813
848
  const target = options.target || ['es2020'];
814
849
  const format = options.format || 'esm';
850
+ const splitting = options.splitting !== undefined ? Boolean(options.splitting) : format === 'esm';
815
851
  const outbase = options.outbase || (fs.existsSync(path.resolve(cwd, 'src/client')) ? path.resolve(cwd, 'src/client') : undefined);
816
852
  const entryNames = options.entryNames || (options.hash !== false ? '[dir]/[name].[hash]' : '[dir]/[name]');
853
+ const chunkNames = options.chunkNames || (options.hash !== false ? 'chunks/[name].[hash]' : 'chunks/[name]');
817
854
  const userPlugins = Array.isArray(options.plugins) ? options.plugins : [];
818
855
 
819
856
  const manifestPlugin = createManifestPlugin({
@@ -838,6 +875,8 @@ async function buildBundle(options = {}) {
838
875
  sourcemap,
839
876
  target,
840
877
  format,
878
+ splitting,
879
+ ...(splitting ? { chunkNames } : {}),
841
880
  entryNames,
842
881
  outbase,
843
882
  metafile: true,
@@ -858,12 +897,34 @@ async function buildBundle(options = {}) {
858
897
  };
859
898
  }
860
899
 
900
+ function resolveLocalModuleFile(importerAbsPath, specifier) {
901
+ if (!specifier || !specifier.startsWith('.')) return null;
902
+ const baseResolved = path.resolve(path.dirname(importerAbsPath), specifier);
903
+ const candidates = [
904
+ baseResolved,
905
+ `${baseResolved}.ts`,
906
+ `${baseResolved}.js`,
907
+ `${baseResolved}.mjs`,
908
+ `${baseResolved}.tsx`,
909
+ `${baseResolved}.jsx`,
910
+ `${baseResolved}.css`,
911
+ path.join(baseResolved, 'index.ts'),
912
+ path.join(baseResolved, 'index.js'),
913
+ ];
914
+ for (const cand of candidates) {
915
+ if (fs.existsSync(cand) && fs.statSync(cand).isFile()) {
916
+ return cand;
917
+ }
918
+ }
919
+ return null;
920
+ }
921
+
861
922
  /**
862
923
  * Built-in fallback asset builder when esbuild is not installed in node_modules.
863
- * Compiles `.ts` / `.js` client entrypoints, hashes filenames for cache-busting,
864
- * and writes `manifest.json`.
924
+ * Compiles `.ts` / `.js` client entrypoints, splits dynamic `import()` modules
925
+ * and shared chunks into `chunks/`, hashes filenames for cache-busting, and writes `manifest.json`.
865
926
  */
866
- function buildBundleFallback({ cwd, entryPoints, outdir, publicDir, manifestPath, hash = true }) {
927
+ function buildBundleFallback({ cwd, entryPoints, outdir, publicDir, manifestPath, hash = true, splitting = true }) {
867
928
  const crypto = require('crypto');
868
929
  let ts = null;
869
930
  try {
@@ -880,22 +941,116 @@ function buildBundleFallback({ cwd, entryPoints, outdir, publicDir, manifestPath
880
941
  : entryPoints;
881
942
 
882
943
  const metafile = { inputs: {}, outputs: {} };
944
+ const emittedChunksBySource = new Map(); // absSourcePath -> { absOut, relFromOutdir, relOutput }
883
945
 
884
- for (const [entryKey, rawFilePath] of Object.entries(entriesMap)) {
885
- const absIn = path.resolve(cwd, rawFilePath);
886
- if (!fs.existsSync(absIn)) continue;
887
- const rawCode = fs.readFileSync(absIn, 'utf8');
888
- let outCode = rawCode;
889
-
890
- if ((absIn.endsWith('.ts') || absIn.endsWith('.tsx')) && ts) {
946
+ function transpileSource(absFile, rawCode) {
947
+ if ((absFile.endsWith('.ts') || absFile.endsWith('.tsx')) && ts) {
891
948
  const transpiled = ts.transpileModule(rawCode, {
892
949
  compilerOptions: {
893
950
  module: ts.ModuleKind.ESNext,
894
951
  target: ts.ScriptTarget.ES2020,
895
952
  },
896
- fileName: absIn,
953
+ fileName: absFile,
897
954
  });
898
- outCode = transpiled.outputText;
955
+ return transpiled.outputText;
956
+ }
957
+ return rawCode;
958
+ }
959
+
960
+ function emitChunkModule(absModulePath) {
961
+ if (emittedChunksBySource.has(absModulePath)) {
962
+ return emittedChunksBySource.get(absModulePath);
963
+ }
964
+ if (!fs.existsSync(absModulePath)) return null;
965
+
966
+ const rawCode = fs.readFileSync(absModulePath, 'utf8');
967
+ const isCss = absModulePath.endsWith('.css');
968
+ let outCode = isCss ? rawCode : transpileSource(absModulePath, rawCode);
969
+
970
+ // Reserve placeholder to prevent infinite recursion on circular imports
971
+ const baseName = path.basename(absModulePath, path.extname(absModulePath)).replace(/\.client$/i, '');
972
+ const placeholder = { absOut: '', relFromOutdir: '', relOutput: '' };
973
+ emittedChunksBySource.set(absModulePath, placeholder);
974
+
975
+ const chunkImports = [];
976
+ if (!isCss && splitting) {
977
+ const placeholderOutDir = path.join(outdir, 'chunks');
978
+ outCode = rewriteModuleImports(absModulePath, outCode, placeholderOutDir, chunkImports);
979
+ }
980
+
981
+ const digest = hash
982
+ ? '.' + crypto.createHash('sha256').update(outCode).digest('hex').slice(0, 8).toUpperCase()
983
+ : '';
984
+ const ext = isCss ? '.css' : '.js';
985
+ const relFromOutdir = `chunks/${baseName}${digest}${ext}`;
986
+ const absOut = path.join(outdir, relFromOutdir);
987
+ fs.mkdirSync(path.dirname(absOut), { recursive: true });
988
+ fs.writeFileSync(absOut, outCode, 'utf8');
989
+
990
+ const relInput = path.relative(cwd, absModulePath).replace(/\\/g, '/');
991
+ const relOutput = path.relative(cwd, absOut).replace(/\\/g, '/');
992
+ metafile.inputs[relInput] = { bytes: Buffer.byteLength(rawCode, 'utf8') };
993
+ metafile.outputs[relOutput] = {
994
+ bytes: Buffer.byteLength(outCode, 'utf8'),
995
+ imports: chunkImports,
996
+ inputs: {
997
+ [relInput]: { bytesInOutput: Buffer.byteLength(outCode, 'utf8') },
998
+ },
999
+ };
1000
+
1001
+ placeholder.absOut = absOut;
1002
+ placeholder.relFromOutdir = relFromOutdir;
1003
+ placeholder.relOutput = relOutput;
1004
+ return placeholder;
1005
+ }
1006
+
1007
+ function rewriteModuleImports(importerAbsPath, code, emitterOutDir, collectedImports) {
1008
+ // 1. Rewrite dynamic `import('./...')`
1009
+ let rewritten = code.replace(
1010
+ /\bimport\s*\(\s*(['"])(\.[^'"]+)\1\s*\)/g,
1011
+ (fullMatch, quote, spec) => {
1012
+ const resolved = resolveLocalModuleFile(importerAbsPath, spec);
1013
+ if (!resolved) return fullMatch;
1014
+ const chunkInfo = emitChunkModule(resolved);
1015
+ if (!chunkInfo || !chunkInfo.absOut) return fullMatch;
1016
+ let relSpec = path.relative(emitterOutDir, chunkInfo.absOut).replace(/\\/g, '/');
1017
+ if (!relSpec.startsWith('.')) relSpec = './' + relSpec;
1018
+ collectedImports.push({ path: chunkInfo.relOutput, kind: 'dynamic-import' });
1019
+ return `import(${quote}${relSpec}${quote})`;
1020
+ }
1021
+ );
1022
+
1023
+ // 2. Rewrite static relative `import ... from './...'` / `export ... from './...'`
1024
+ rewritten = rewritten.replace(
1025
+ /(\b(?:import|export)\s+(?:[\s\S]*?\bfrom\s+)?)(['"])(\.[^'"]+)\2/g,
1026
+ (fullMatch, prefix, quote, spec) => {
1027
+ const resolved = resolveLocalModuleFile(importerAbsPath, spec);
1028
+ if (!resolved) return fullMatch;
1029
+ const chunkInfo = emitChunkModule(resolved);
1030
+ if (!chunkInfo || !chunkInfo.absOut) return fullMatch;
1031
+ let relSpec = path.relative(emitterOutDir, chunkInfo.absOut).replace(/\\/g, '/');
1032
+ if (!relSpec.startsWith('.')) relSpec = './' + relSpec;
1033
+ collectedImports.push({ path: chunkInfo.relOutput, kind: 'import-statement' });
1034
+ if (resolved.endsWith('.css')) {
1035
+ return `/* css: ${relSpec} */`;
1036
+ }
1037
+ return `${prefix}${quote}${relSpec}${quote}`;
1038
+ }
1039
+ );
1040
+
1041
+ return rewritten;
1042
+ }
1043
+
1044
+ for (const [entryKey, rawFilePath] of Object.entries(entriesMap)) {
1045
+ const absIn = path.resolve(cwd, rawFilePath);
1046
+ if (!fs.existsSync(absIn)) continue;
1047
+ const rawCode = fs.readFileSync(absIn, 'utf8');
1048
+ let outCode = transpileSource(absIn, rawCode);
1049
+
1050
+ const entryOutDir = path.dirname(path.join(outdir, `${entryKey}.js`));
1051
+ const entryImports = [];
1052
+ if (splitting) {
1053
+ outCode = rewriteModuleImports(absIn, outCode, entryOutDir, entryImports);
899
1054
  }
900
1055
 
901
1056
  const digest = hash
@@ -912,10 +1067,16 @@ function buildBundleFallback({ cwd, entryPoints, outdir, publicDir, manifestPath
912
1067
  metafile.outputs[relOutput] = {
913
1068
  entryPoint: relInput,
914
1069
  bytes: Buffer.byteLength(outCode, 'utf8'),
1070
+ imports: entryImports,
1071
+ inputs: {
1072
+ [relInput]: { bytesInOutput: Buffer.byteLength(outCode, 'utf8') },
1073
+ },
915
1074
  };
916
1075
  }
917
1076
 
918
- const manifest = generateManifest(metafile, { outdir, publicDir });
1077
+ const relOutdir = path.relative(cwd, outdir).replace(/\\/g, '/') || 'public/dist';
1078
+ const relPublicDir = path.relative(cwd, publicDir).replace(/\\/g, '/') || 'public';
1079
+ const manifest = generateManifest(metafile, { outdir: relOutdir, publicDir: relPublicDir });
919
1080
  fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
920
1081
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
921
1082
 
package/src/dev/banner.js CHANGED
@@ -154,7 +154,10 @@ function collectBannerSummary(context = {}) {
154
154
  hasAdmin = Boolean(context.hasAdmin);
155
155
  } else if (pluginManager && pluginManager.plugins instanceof Map) {
156
156
  pluginsList = Array.from(pluginManager.plugins.keys());
157
- hasAdmin = pluginManager.plugins.has('adminPanel') || pluginManager.plugins.has('admin');
157
+ hasAdmin =
158
+ pluginManager.plugins.has('admin-panel') ||
159
+ pluginManager.plugins.has('adminPanel') ||
160
+ pluginManager.plugins.has('admin');
158
161
  } else {
159
162
  hasAdmin = true;
160
163
  }
@@ -315,6 +315,21 @@ function createDevReloadHub(config = {}) {
315
315
 
316
316
  let activeNunjucksEnv = nunjucksEnv;
317
317
  let onTemplateChangeCallback = onTemplateChange;
318
+ let getDevMetaCallback = typeof config.getDevMeta === 'function' ? config.getDevMeta : null;
319
+
320
+ function resolveDevMetaJson() {
321
+ if (typeof getDevMetaCallback !== 'function') return null;
322
+ try {
323
+ const meta = getDevMetaCallback();
324
+ if (!meta || typeof meta !== 'object') return null;
325
+ return JSON.stringify(meta)
326
+ .replace(/</g, '\\u003c')
327
+ .replace(/>/g, '\\u003e')
328
+ .replace(/&/g, '\\u0026');
329
+ } catch (_) {
330
+ return null;
331
+ }
332
+ }
318
333
 
319
334
  function broadcast(type, payload = {}) {
320
335
  if (isClosed) return;
@@ -375,6 +390,9 @@ function createDevReloadHub(config = {}) {
375
390
  setOnTemplateChange: (fn) => {
376
391
  onTemplateChangeCallback = fn;
377
392
  },
393
+ setDevMetaProvider: (fn) => {
394
+ getDevMetaCallback = fn;
395
+ },
378
396
  invalidateTemplateCache,
379
397
  triggerTemplateReload: (filename, reason = 'template') => {
380
398
  onTemplateFileChanged(filename, reason);
@@ -418,6 +436,12 @@ function createDevReloadHub(config = {}) {
418
436
  // Client JS route handler
419
437
  function handleScript(req, res) {
420
438
  res.type('application/javascript');
439
+ const metaJson = resolveDevMetaJson();
440
+ if (metaJson) {
441
+ const metaPrefix = `(function(){if(typeof window!=='undefined'){window.__WEBSPRESSO_DEV_META__=${metaJson};if(window.__WEBSPRESSO_DEVTOOLS__&&typeof window.__WEBSPRESSO_DEVTOOLS__.setMeta==='function'){window.__WEBSPRESSO_DEVTOOLS__.setMeta(window.__WEBSPRESSO_DEV_META__);}}})();\n`;
442
+ res.send(metaPrefix + CLIENT_RUNTIME_SCRIPT);
443
+ return;
444
+ }
421
445
  res.send(CLIENT_RUNTIME_SCRIPT);
422
446
  }
423
447
 
@@ -429,7 +453,11 @@ function createDevReloadHub(config = {}) {
429
453
  const origSend = res.send.bind(res);
430
454
  res.send = function(body) {
431
455
  if (typeof body === 'string' && body.includes('</body>')) {
432
- const scriptTag = `<script defer src="${CLIENT_SCRIPT_PATH}"></script>`;
456
+ const metaJson = resolveDevMetaJson();
457
+ const metaTag = metaJson
458
+ ? `<script>window.__WEBSPRESSO_DEV_META__=${metaJson};</script>\n`
459
+ : '';
460
+ const scriptTag = `${metaTag}<script defer src="${CLIENT_SCRIPT_PATH}"></script>`;
433
461
  if (!body.includes(CLIENT_SCRIPT_PATH)) {
434
462
  body = body.replace('</body>', scriptTag + '\n</body>');
435
463
  }
package/src/server.js CHANGED
@@ -1517,6 +1517,34 @@ function createApp(options = {}) {
1517
1517
  options: typeof devReloadOption === 'object' ? devReloadOption : {},
1518
1518
  enabled: devReloadEnabled,
1519
1519
  logger: logging ? console : null,
1520
+ getDevMeta: () => {
1521
+ const pluginsList =
1522
+ pluginManager && pluginManager.plugins instanceof Map
1523
+ ? Array.from(pluginManager.plugins.keys())
1524
+ : [];
1525
+ const hasDb = Boolean(options.db || core.db || (app.locals && app.locals.db));
1526
+ const hasStudio = Boolean(
1527
+ hasDb ||
1528
+ options.studio ||
1529
+ pluginsList.includes('studio') ||
1530
+ pluginsList.includes('db-studio')
1531
+ );
1532
+ const hasAdmin = Boolean(
1533
+ pluginsList.includes('admin-panel') ||
1534
+ pluginsList.includes('adminPanel') ||
1535
+ pluginsList.includes('admin') ||
1536
+ app.adminApi ||
1537
+ (app.locals && app.locals.adminRegistry)
1538
+ );
1539
+ const adminPath = (app.locals && app.locals.adminBasePath) || '/_admin';
1540
+ return {
1541
+ hasDb,
1542
+ hasStudio,
1543
+ hasAdmin,
1544
+ adminPath,
1545
+ plugins: pluginsList,
1546
+ };
1547
+ },
1520
1548
  });
1521
1549
 
1522
1550
  if (devReloadHub) {