@bleedingdev/modern-js-create 3.2.0-ultramodern.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/LICENSE +21 -0
- package/README.md +112 -0
- package/bin/run.js +73 -0
- package/dist/index.js +2320 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/locale/en.d.ts +55 -0
- package/dist/types/locale/index.d.ts +112 -0
- package/dist/types/locale/zh.d.ts +55 -0
- package/dist/types/ultramodern-workspace.d.ts +20 -0
- package/package.json +56 -0
- package/template/.browserslistrc +4 -0
- package/template/.github/workflows/ultramodern-gates.yml.handlebars +30 -0
- package/template/.gitignore.handlebars +30 -0
- package/template/.nvmrc +2 -0
- package/template/README.md +78 -0
- package/template/api/effect/index.ts.handlebars +61 -0
- package/template/api/lambda/hello.ts.handlebars +6 -0
- package/template/biome.json +41 -0
- package/template/modern.config.ts.handlebars +50 -0
- package/template/package.json.handlebars +47 -0
- package/template/postcss.config.mjs.handlebars +6 -0
- package/template/scripts/validate-ultramodern.mjs.handlebars +102 -0
- package/template/shared/effect/api.ts.handlebars +18 -0
- package/template/src/modern-app-env.d.ts +1 -0
- package/template/src/modern.runtime.ts.handlebars +9 -0
- package/template/src/routes/index.css.handlebars +118 -0
- package/template/src/routes/layout.tsx.handlebars +9 -0
- package/template/src/routes/page.tsx.handlebars +119 -0
- package/template/tailwind.config.ts.handlebars +10 -0
- package/template/tsconfig.json +16 -0
- package/template-workspace/README.md.handlebars +28 -0
- package/template-workspace/pnpm-workspace.yaml +5 -0
- package/template-workspace/scripts/validate-ultramodern-workspace.mjs.handlebars +276 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2320 @@
|
|
|
1
|
+
import node_crypto from "node:crypto";
|
|
2
|
+
import node_fs from "node:fs";
|
|
3
|
+
import node_path from "node:path";
|
|
4
|
+
import node_readline from "node:readline";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import "node:module";
|
|
7
|
+
class I18CLILanguageDetector {
|
|
8
|
+
formatShellLocale(rawLC) {
|
|
9
|
+
if (!rawLC) return '';
|
|
10
|
+
const LCs = rawLC.split(':');
|
|
11
|
+
const LC = LCs[0].split('.')[0].split('_')[0].split('-')[0];
|
|
12
|
+
if ('C' === LC) return '';
|
|
13
|
+
return LC;
|
|
14
|
+
}
|
|
15
|
+
detect() {
|
|
16
|
+
const env = globalThis.process?.env;
|
|
17
|
+
const shellLocale = env?.LC_ALL ?? env?.LC_MESSAGES ?? env?.LANG ?? env?.LANGUAGE ?? Intl.DateTimeFormat().resolvedOptions().locale;
|
|
18
|
+
return this.formatShellLocale(shellLocale);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function getLocaleLanguage() {
|
|
22
|
+
const detector = new I18CLILanguageDetector();
|
|
23
|
+
return detector.detect();
|
|
24
|
+
}
|
|
25
|
+
var isArray = Array.isArray;
|
|
26
|
+
const lodash_es_isArray = isArray;
|
|
27
|
+
var freeGlobal = 'object' == typeof global && global && global.Object === Object && global;
|
|
28
|
+
const _freeGlobal = freeGlobal;
|
|
29
|
+
var freeSelf = 'object' == typeof self && self && self.Object === Object && self;
|
|
30
|
+
var _root_root = _freeGlobal || freeSelf || Function('return this')();
|
|
31
|
+
const _root = _root_root;
|
|
32
|
+
var Symbol = _root.Symbol;
|
|
33
|
+
const _Symbol = Symbol;
|
|
34
|
+
var objectProto = Object.prototype;
|
|
35
|
+
var _getRawTag_hasOwnProperty = objectProto.hasOwnProperty;
|
|
36
|
+
var nativeObjectToString = objectProto.toString;
|
|
37
|
+
var symToStringTag = _Symbol ? _Symbol.toStringTag : void 0;
|
|
38
|
+
function getRawTag(value) {
|
|
39
|
+
var isOwn = _getRawTag_hasOwnProperty.call(value, symToStringTag), tag = value[symToStringTag];
|
|
40
|
+
try {
|
|
41
|
+
value[symToStringTag] = void 0;
|
|
42
|
+
var unmasked = true;
|
|
43
|
+
} catch (e) {}
|
|
44
|
+
var result = nativeObjectToString.call(value);
|
|
45
|
+
if (unmasked) if (isOwn) value[symToStringTag] = tag;
|
|
46
|
+
else delete value[symToStringTag];
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
const _getRawTag = getRawTag;
|
|
50
|
+
var _objectToString_objectProto = Object.prototype;
|
|
51
|
+
var _objectToString_nativeObjectToString = _objectToString_objectProto.toString;
|
|
52
|
+
function objectToString(value) {
|
|
53
|
+
return _objectToString_nativeObjectToString.call(value);
|
|
54
|
+
}
|
|
55
|
+
const _objectToString = objectToString;
|
|
56
|
+
var nullTag = '[object Null]', undefinedTag = '[object Undefined]';
|
|
57
|
+
var _baseGetTag_symToStringTag = _Symbol ? _Symbol.toStringTag : void 0;
|
|
58
|
+
function baseGetTag(value) {
|
|
59
|
+
if (null == value) return void 0 === value ? undefinedTag : nullTag;
|
|
60
|
+
return _baseGetTag_symToStringTag && _baseGetTag_symToStringTag in Object(value) ? _getRawTag(value) : _objectToString(value);
|
|
61
|
+
}
|
|
62
|
+
const _baseGetTag = baseGetTag;
|
|
63
|
+
function isObjectLike(value) {
|
|
64
|
+
return null != value && 'object' == typeof value;
|
|
65
|
+
}
|
|
66
|
+
const lodash_es_isObjectLike = isObjectLike;
|
|
67
|
+
var symbolTag = '[object Symbol]';
|
|
68
|
+
function isSymbol(value) {
|
|
69
|
+
return 'symbol' == typeof value || lodash_es_isObjectLike(value) && _baseGetTag(value) == symbolTag;
|
|
70
|
+
}
|
|
71
|
+
const lodash_es_isSymbol = isSymbol;
|
|
72
|
+
var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, reIsPlainProp = /^\w*$/;
|
|
73
|
+
function isKey(value, object) {
|
|
74
|
+
if (lodash_es_isArray(value)) return false;
|
|
75
|
+
var type = typeof value;
|
|
76
|
+
if ('number' == type || 'symbol' == type || 'boolean' == type || null == value || lodash_es_isSymbol(value)) return true;
|
|
77
|
+
return reIsPlainProp.test(value) || !reIsDeepProp.test(value) || null != object && value in Object(object);
|
|
78
|
+
}
|
|
79
|
+
const _isKey = isKey;
|
|
80
|
+
function isObject(value) {
|
|
81
|
+
var type = typeof value;
|
|
82
|
+
return null != value && ('object' == type || 'function' == type);
|
|
83
|
+
}
|
|
84
|
+
const lodash_es_isObject = isObject;
|
|
85
|
+
var asyncTag = '[object AsyncFunction]', funcTag = '[object Function]', genTag = '[object GeneratorFunction]', proxyTag = '[object Proxy]';
|
|
86
|
+
function isFunction(value) {
|
|
87
|
+
if (!lodash_es_isObject(value)) return false;
|
|
88
|
+
var tag = _baseGetTag(value);
|
|
89
|
+
return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
|
|
90
|
+
}
|
|
91
|
+
const lodash_es_isFunction = isFunction;
|
|
92
|
+
var coreJsData = _root["__core-js_shared__"];
|
|
93
|
+
const _coreJsData = coreJsData;
|
|
94
|
+
var maskSrcKey = function() {
|
|
95
|
+
var uid = /[^.]+$/.exec(_coreJsData && _coreJsData.keys && _coreJsData.keys.IE_PROTO || '');
|
|
96
|
+
return uid ? 'Symbol(src)_1.' + uid : '';
|
|
97
|
+
}();
|
|
98
|
+
function isMasked(func) {
|
|
99
|
+
return !!maskSrcKey && maskSrcKey in func;
|
|
100
|
+
}
|
|
101
|
+
const _isMasked = isMasked;
|
|
102
|
+
var funcProto = Function.prototype;
|
|
103
|
+
var funcToString = funcProto.toString;
|
|
104
|
+
function toSource(func) {
|
|
105
|
+
if (null != func) {
|
|
106
|
+
try {
|
|
107
|
+
return funcToString.call(func);
|
|
108
|
+
} catch (e) {}
|
|
109
|
+
try {
|
|
110
|
+
return func + '';
|
|
111
|
+
} catch (e) {}
|
|
112
|
+
}
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
const _toSource = toSource;
|
|
116
|
+
var reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
|
|
117
|
+
var reIsHostCtor = /^\[object .+?Constructor\]$/;
|
|
118
|
+
var _baseIsNative_funcProto = Function.prototype, _baseIsNative_objectProto = Object.prototype;
|
|
119
|
+
var _baseIsNative_funcToString = _baseIsNative_funcProto.toString;
|
|
120
|
+
var _baseIsNative_hasOwnProperty = _baseIsNative_objectProto.hasOwnProperty;
|
|
121
|
+
var reIsNative = RegExp('^' + _baseIsNative_funcToString.call(_baseIsNative_hasOwnProperty).replace(reRegExpChar, '\\$&').replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$');
|
|
122
|
+
function baseIsNative(value) {
|
|
123
|
+
if (!lodash_es_isObject(value) || _isMasked(value)) return false;
|
|
124
|
+
var pattern = lodash_es_isFunction(value) ? reIsNative : reIsHostCtor;
|
|
125
|
+
return pattern.test(_toSource(value));
|
|
126
|
+
}
|
|
127
|
+
const _baseIsNative = baseIsNative;
|
|
128
|
+
function getValue(object, key) {
|
|
129
|
+
return null == object ? void 0 : object[key];
|
|
130
|
+
}
|
|
131
|
+
const _getValue = getValue;
|
|
132
|
+
function getNative(object, key) {
|
|
133
|
+
var value = _getValue(object, key);
|
|
134
|
+
return _baseIsNative(value) ? value : void 0;
|
|
135
|
+
}
|
|
136
|
+
const _getNative = getNative;
|
|
137
|
+
var nativeCreate = _getNative(Object, 'create');
|
|
138
|
+
const _nativeCreate = nativeCreate;
|
|
139
|
+
function hashClear() {
|
|
140
|
+
this.__data__ = _nativeCreate ? _nativeCreate(null) : {};
|
|
141
|
+
this.size = 0;
|
|
142
|
+
}
|
|
143
|
+
const _hashClear = hashClear;
|
|
144
|
+
function hashDelete(key) {
|
|
145
|
+
var result = this.has(key) && delete this.__data__[key];
|
|
146
|
+
this.size -= result ? 1 : 0;
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
const _hashDelete = hashDelete;
|
|
150
|
+
var HASH_UNDEFINED = '__lodash_hash_undefined__';
|
|
151
|
+
var _hashGet_objectProto = Object.prototype;
|
|
152
|
+
var _hashGet_hasOwnProperty = _hashGet_objectProto.hasOwnProperty;
|
|
153
|
+
function hashGet(key) {
|
|
154
|
+
var data = this.__data__;
|
|
155
|
+
if (_nativeCreate) {
|
|
156
|
+
var result = data[key];
|
|
157
|
+
return result === HASH_UNDEFINED ? void 0 : result;
|
|
158
|
+
}
|
|
159
|
+
return _hashGet_hasOwnProperty.call(data, key) ? data[key] : void 0;
|
|
160
|
+
}
|
|
161
|
+
const _hashGet = hashGet;
|
|
162
|
+
var _hashHas_objectProto = Object.prototype;
|
|
163
|
+
var _hashHas_hasOwnProperty = _hashHas_objectProto.hasOwnProperty;
|
|
164
|
+
function hashHas(key) {
|
|
165
|
+
var data = this.__data__;
|
|
166
|
+
return _nativeCreate ? void 0 !== data[key] : _hashHas_hasOwnProperty.call(data, key);
|
|
167
|
+
}
|
|
168
|
+
const _hashHas = hashHas;
|
|
169
|
+
var _hashSet_HASH_UNDEFINED = '__lodash_hash_undefined__';
|
|
170
|
+
function hashSet(key, value) {
|
|
171
|
+
var data = this.__data__;
|
|
172
|
+
this.size += this.has(key) ? 0 : 1;
|
|
173
|
+
data[key] = _nativeCreate && void 0 === value ? _hashSet_HASH_UNDEFINED : value;
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
const _hashSet = hashSet;
|
|
177
|
+
function Hash(entries) {
|
|
178
|
+
var index = -1, length = null == entries ? 0 : entries.length;
|
|
179
|
+
this.clear();
|
|
180
|
+
while(++index < length){
|
|
181
|
+
var entry = entries[index];
|
|
182
|
+
this.set(entry[0], entry[1]);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
Hash.prototype.clear = _hashClear;
|
|
186
|
+
Hash.prototype['delete'] = _hashDelete;
|
|
187
|
+
Hash.prototype.get = _hashGet;
|
|
188
|
+
Hash.prototype.has = _hashHas;
|
|
189
|
+
Hash.prototype.set = _hashSet;
|
|
190
|
+
const _Hash = Hash;
|
|
191
|
+
function listCacheClear() {
|
|
192
|
+
this.__data__ = [];
|
|
193
|
+
this.size = 0;
|
|
194
|
+
}
|
|
195
|
+
const _listCacheClear = listCacheClear;
|
|
196
|
+
function eq(value, other) {
|
|
197
|
+
return value === other || value !== value && other !== other;
|
|
198
|
+
}
|
|
199
|
+
const lodash_es_eq = eq;
|
|
200
|
+
function assocIndexOf(array, key) {
|
|
201
|
+
var length = array.length;
|
|
202
|
+
while(length--)if (lodash_es_eq(array[length][0], key)) return length;
|
|
203
|
+
return -1;
|
|
204
|
+
}
|
|
205
|
+
const _assocIndexOf = assocIndexOf;
|
|
206
|
+
var arrayProto = Array.prototype;
|
|
207
|
+
var splice = arrayProto.splice;
|
|
208
|
+
function listCacheDelete(key) {
|
|
209
|
+
var data = this.__data__, index = _assocIndexOf(data, key);
|
|
210
|
+
if (index < 0) return false;
|
|
211
|
+
var lastIndex = data.length - 1;
|
|
212
|
+
if (index == lastIndex) data.pop();
|
|
213
|
+
else splice.call(data, index, 1);
|
|
214
|
+
--this.size;
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
const _listCacheDelete = listCacheDelete;
|
|
218
|
+
function listCacheGet(key) {
|
|
219
|
+
var data = this.__data__, index = _assocIndexOf(data, key);
|
|
220
|
+
return index < 0 ? void 0 : data[index][1];
|
|
221
|
+
}
|
|
222
|
+
const _listCacheGet = listCacheGet;
|
|
223
|
+
function listCacheHas(key) {
|
|
224
|
+
return _assocIndexOf(this.__data__, key) > -1;
|
|
225
|
+
}
|
|
226
|
+
const _listCacheHas = listCacheHas;
|
|
227
|
+
function listCacheSet(key, value) {
|
|
228
|
+
var data = this.__data__, index = _assocIndexOf(data, key);
|
|
229
|
+
if (index < 0) {
|
|
230
|
+
++this.size;
|
|
231
|
+
data.push([
|
|
232
|
+
key,
|
|
233
|
+
value
|
|
234
|
+
]);
|
|
235
|
+
} else data[index][1] = value;
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
const _listCacheSet = listCacheSet;
|
|
239
|
+
function ListCache(entries) {
|
|
240
|
+
var index = -1, length = null == entries ? 0 : entries.length;
|
|
241
|
+
this.clear();
|
|
242
|
+
while(++index < length){
|
|
243
|
+
var entry = entries[index];
|
|
244
|
+
this.set(entry[0], entry[1]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
ListCache.prototype.clear = _listCacheClear;
|
|
248
|
+
ListCache.prototype['delete'] = _listCacheDelete;
|
|
249
|
+
ListCache.prototype.get = _listCacheGet;
|
|
250
|
+
ListCache.prototype.has = _listCacheHas;
|
|
251
|
+
ListCache.prototype.set = _listCacheSet;
|
|
252
|
+
const _ListCache = ListCache;
|
|
253
|
+
var Map = _getNative(_root, 'Map');
|
|
254
|
+
const _Map = Map;
|
|
255
|
+
function mapCacheClear() {
|
|
256
|
+
this.size = 0;
|
|
257
|
+
this.__data__ = {
|
|
258
|
+
hash: new _Hash,
|
|
259
|
+
map: new (_Map || _ListCache),
|
|
260
|
+
string: new _Hash
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const _mapCacheClear = mapCacheClear;
|
|
264
|
+
function isKeyable(value) {
|
|
265
|
+
var type = typeof value;
|
|
266
|
+
return 'string' == type || 'number' == type || 'symbol' == type || 'boolean' == type ? '__proto__' !== value : null === value;
|
|
267
|
+
}
|
|
268
|
+
const _isKeyable = isKeyable;
|
|
269
|
+
function getMapData(map, key) {
|
|
270
|
+
var data = map.__data__;
|
|
271
|
+
return _isKeyable(key) ? data['string' == typeof key ? 'string' : 'hash'] : data.map;
|
|
272
|
+
}
|
|
273
|
+
const _getMapData = getMapData;
|
|
274
|
+
function mapCacheDelete(key) {
|
|
275
|
+
var result = _getMapData(this, key)['delete'](key);
|
|
276
|
+
this.size -= result ? 1 : 0;
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
const _mapCacheDelete = mapCacheDelete;
|
|
280
|
+
function mapCacheGet(key) {
|
|
281
|
+
return _getMapData(this, key).get(key);
|
|
282
|
+
}
|
|
283
|
+
const _mapCacheGet = mapCacheGet;
|
|
284
|
+
function mapCacheHas(key) {
|
|
285
|
+
return _getMapData(this, key).has(key);
|
|
286
|
+
}
|
|
287
|
+
const _mapCacheHas = mapCacheHas;
|
|
288
|
+
function mapCacheSet(key, value) {
|
|
289
|
+
var data = _getMapData(this, key), size = data.size;
|
|
290
|
+
data.set(key, value);
|
|
291
|
+
this.size += data.size == size ? 0 : 1;
|
|
292
|
+
return this;
|
|
293
|
+
}
|
|
294
|
+
const _mapCacheSet = mapCacheSet;
|
|
295
|
+
function MapCache(entries) {
|
|
296
|
+
var index = -1, length = null == entries ? 0 : entries.length;
|
|
297
|
+
this.clear();
|
|
298
|
+
while(++index < length){
|
|
299
|
+
var entry = entries[index];
|
|
300
|
+
this.set(entry[0], entry[1]);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
MapCache.prototype.clear = _mapCacheClear;
|
|
304
|
+
MapCache.prototype['delete'] = _mapCacheDelete;
|
|
305
|
+
MapCache.prototype.get = _mapCacheGet;
|
|
306
|
+
MapCache.prototype.has = _mapCacheHas;
|
|
307
|
+
MapCache.prototype.set = _mapCacheSet;
|
|
308
|
+
const _MapCache = MapCache;
|
|
309
|
+
var FUNC_ERROR_TEXT = 'Expected a function';
|
|
310
|
+
function memoize(func, resolver) {
|
|
311
|
+
if ('function' != typeof func || null != resolver && 'function' != typeof resolver) throw new TypeError(FUNC_ERROR_TEXT);
|
|
312
|
+
var memoized = function() {
|
|
313
|
+
var args = arguments, key = resolver ? resolver.apply(this, args) : args[0], cache = memoized.cache;
|
|
314
|
+
if (cache.has(key)) return cache.get(key);
|
|
315
|
+
var result = func.apply(this, args);
|
|
316
|
+
memoized.cache = cache.set(key, result) || cache;
|
|
317
|
+
return result;
|
|
318
|
+
};
|
|
319
|
+
memoized.cache = new (memoize.Cache || _MapCache);
|
|
320
|
+
return memoized;
|
|
321
|
+
}
|
|
322
|
+
memoize.Cache = _MapCache;
|
|
323
|
+
const lodash_es_memoize = memoize;
|
|
324
|
+
var MAX_MEMOIZE_SIZE = 500;
|
|
325
|
+
function memoizeCapped(func) {
|
|
326
|
+
var result = lodash_es_memoize(func, function(key) {
|
|
327
|
+
if (cache.size === MAX_MEMOIZE_SIZE) cache.clear();
|
|
328
|
+
return key;
|
|
329
|
+
});
|
|
330
|
+
var cache = result.cache;
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
const _memoizeCapped = memoizeCapped;
|
|
334
|
+
var rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;
|
|
335
|
+
var reEscapeChar = /\\(\\)?/g;
|
|
336
|
+
var stringToPath = _memoizeCapped(function(string) {
|
|
337
|
+
var result = [];
|
|
338
|
+
if (46 === string.charCodeAt(0)) result.push('');
|
|
339
|
+
string.replace(rePropName, function(match, number, quote, subString) {
|
|
340
|
+
result.push(quote ? subString.replace(reEscapeChar, '$1') : number || match);
|
|
341
|
+
});
|
|
342
|
+
return result;
|
|
343
|
+
});
|
|
344
|
+
const _stringToPath = stringToPath;
|
|
345
|
+
function arrayMap(array, iteratee) {
|
|
346
|
+
var index = -1, length = null == array ? 0 : array.length, result = Array(length);
|
|
347
|
+
while(++index < length)result[index] = iteratee(array[index], index, array);
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
const _arrayMap = arrayMap;
|
|
351
|
+
var INFINITY = 1 / 0;
|
|
352
|
+
var symbolProto = _Symbol ? _Symbol.prototype : void 0, symbolToString = symbolProto ? symbolProto.toString : void 0;
|
|
353
|
+
function baseToString(value) {
|
|
354
|
+
if ('string' == typeof value) return value;
|
|
355
|
+
if (lodash_es_isArray(value)) return _arrayMap(value, baseToString) + '';
|
|
356
|
+
if (lodash_es_isSymbol(value)) return symbolToString ? symbolToString.call(value) : '';
|
|
357
|
+
var result = value + '';
|
|
358
|
+
return '0' == result && 1 / value == -INFINITY ? '-0' : result;
|
|
359
|
+
}
|
|
360
|
+
const _baseToString = baseToString;
|
|
361
|
+
function toString_toString(value) {
|
|
362
|
+
return null == value ? '' : _baseToString(value);
|
|
363
|
+
}
|
|
364
|
+
const lodash_es_toString = toString_toString;
|
|
365
|
+
function castPath(value, object) {
|
|
366
|
+
if (lodash_es_isArray(value)) return value;
|
|
367
|
+
return _isKey(value, object) ? [
|
|
368
|
+
value
|
|
369
|
+
] : _stringToPath(lodash_es_toString(value));
|
|
370
|
+
}
|
|
371
|
+
const _castPath = castPath;
|
|
372
|
+
var _toKey_INFINITY = 1 / 0;
|
|
373
|
+
function toKey(value) {
|
|
374
|
+
if ('string' == typeof value || lodash_es_isSymbol(value)) return value;
|
|
375
|
+
var result = value + '';
|
|
376
|
+
return '0' == result && 1 / value == -_toKey_INFINITY ? '-0' : result;
|
|
377
|
+
}
|
|
378
|
+
const _toKey = toKey;
|
|
379
|
+
function baseGet(object, path) {
|
|
380
|
+
path = _castPath(path, object);
|
|
381
|
+
var index = 0, length = path.length;
|
|
382
|
+
while(null != object && index < length)object = object[_toKey(path[index++])];
|
|
383
|
+
return index && index == length ? object : void 0;
|
|
384
|
+
}
|
|
385
|
+
const _baseGet = baseGet;
|
|
386
|
+
function get(object, path, defaultValue) {
|
|
387
|
+
var result = null == object ? void 0 : _baseGet(object, path);
|
|
388
|
+
return void 0 === result ? defaultValue : result;
|
|
389
|
+
}
|
|
390
|
+
const lodash_es_get = get;
|
|
391
|
+
var stringTag = '[object String]';
|
|
392
|
+
function isString(value) {
|
|
393
|
+
return 'string' == typeof value || !lodash_es_isArray(value) && lodash_es_isObjectLike(value) && _baseGetTag(value) == stringTag;
|
|
394
|
+
}
|
|
395
|
+
const lodash_es_isString = isString;
|
|
396
|
+
function getObjKeyMap(obj, prefix = '') {
|
|
397
|
+
const result = {};
|
|
398
|
+
Object.keys(obj).forEach((key)=>{
|
|
399
|
+
if (lodash_es_isString(obj[key])) result[key] = prefix ? `${prefix}.${key}` : key;
|
|
400
|
+
else if (lodash_es_isObject(obj[key])) result[key] = getObjKeyMap(obj[key], prefix ? `${prefix}.${key}` : key);
|
|
401
|
+
});
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
class I18n {
|
|
405
|
+
format(msg, vars) {
|
|
406
|
+
return msg.replace(/\{(\w+)\}/g, (_match, capture)=>Object.prototype.hasOwnProperty.call(vars, capture) ? vars[capture] : capture);
|
|
407
|
+
}
|
|
408
|
+
getMessage(lang, key, vars, fallbackText) {
|
|
409
|
+
const languages = Object.keys(this.languageMap);
|
|
410
|
+
const resultLang = languages.find((l)=>l === lang);
|
|
411
|
+
if (!resultLang && 0 === languages.length) return fallbackText || key;
|
|
412
|
+
const model = this.languageMap[resultLang || 'en'];
|
|
413
|
+
if (!model) return fallbackText || key;
|
|
414
|
+
const message = lodash_es_get(model, key);
|
|
415
|
+
const value = message || fallbackText || key;
|
|
416
|
+
if ('string' == typeof value) return this.format(value, vars || {});
|
|
417
|
+
throw new Error('key is not a string');
|
|
418
|
+
}
|
|
419
|
+
init(language, languageMap) {
|
|
420
|
+
this.language = language || 'en';
|
|
421
|
+
if (languageMap) this.languageMap = languageMap;
|
|
422
|
+
return getObjKeyMap(this.languageMap[this.language]);
|
|
423
|
+
}
|
|
424
|
+
changeLanguage(config) {
|
|
425
|
+
this.language = config.locale || 'en';
|
|
426
|
+
}
|
|
427
|
+
t(key, vars, fallbackText) {
|
|
428
|
+
return this.getMessage(this.language, key, vars, fallbackText);
|
|
429
|
+
}
|
|
430
|
+
lang(lang) {
|
|
431
|
+
return {
|
|
432
|
+
t: (key, vars, fallbackText)=>this.getMessage(lang, key, vars, fallbackText)
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
constructor(){
|
|
436
|
+
this.language = 'en';
|
|
437
|
+
this.languageMap = {};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const EN_LOCALE = {
|
|
441
|
+
prompt: {
|
|
442
|
+
projectName: 'Please enter project name: '
|
|
443
|
+
},
|
|
444
|
+
error: {
|
|
445
|
+
projectNameEmpty: 'Error: Project name cannot be empty',
|
|
446
|
+
directoryExists: 'Error: Directory "{projectName}" already exists and is not empty',
|
|
447
|
+
invalidRouter: 'Error: Unsupported router "{router}". Use "react-router" or "tanstack".',
|
|
448
|
+
invalidBffRuntime: 'Error: Unsupported BFF runtime "{runtime}". Use "hono" or "effect".',
|
|
449
|
+
createFailed: 'Error creating project:'
|
|
450
|
+
},
|
|
451
|
+
message: {
|
|
452
|
+
welcome: '🚀 Welcome to Modern.js',
|
|
453
|
+
success: '✨ Created successfully!',
|
|
454
|
+
nextSteps: '📋 Next steps:',
|
|
455
|
+
step1: 'cd {projectName}',
|
|
456
|
+
step2: 'pnpm install',
|
|
457
|
+
step3: 'pnpm dev'
|
|
458
|
+
},
|
|
459
|
+
help: {
|
|
460
|
+
title: '🚀 Modern.js Project Creator',
|
|
461
|
+
description: 'Create a new Modern.js project with ease',
|
|
462
|
+
usage: '📖 Usage:',
|
|
463
|
+
usageExample: ' create [project-name] [options]',
|
|
464
|
+
options: '⚙️ Options:',
|
|
465
|
+
optionHelp: ' -h, --help Display this help message',
|
|
466
|
+
optionVersion: ' -v, --version Display version information',
|
|
467
|
+
optionLang: ' -l, --lang Set the language (zh or en)',
|
|
468
|
+
optionRouter: ' -r, --router Select router framework (react-router or tanstack)',
|
|
469
|
+
optionBff: ' --bff Enable BFF scaffold (default runtime: effect)',
|
|
470
|
+
optionBffRuntime: ' --bff-runtime Select BFF runtime (hono or effect)',
|
|
471
|
+
optionTailwind: ' --tailwind Enable Tailwind CSS v4 scaffold (PostCSS + starter styles)',
|
|
472
|
+
optionWorkspace: ' --workspace Use workspace protocol for @modern-js dependencies (for local monorepo testing)',
|
|
473
|
+
optionUltramodernWorkspace: ' --ultramodern-workspace Generate the canonical UltraModern SuperApp workspace',
|
|
474
|
+
optionUltramodernPackageSource: ' --ultramodern-package-source Select UltraModern package source (workspace or install)',
|
|
475
|
+
optionUltramodernPackageScope: ' --ultramodern-package-scope Publish scope for npm alias installs (for example bleedingdev)',
|
|
476
|
+
optionUltramodernPackageNamePrefix: ' --ultramodern-package-name-prefix Prefix for npm alias package names (default: modern-js-)',
|
|
477
|
+
optionSub: ' -s, --sub Mark as a subproject (package in monorepo)',
|
|
478
|
+
examples: '💡 Examples:',
|
|
479
|
+
example1: ' create my-app',
|
|
480
|
+
example2: ' create my-app --lang zh',
|
|
481
|
+
example3: ' create my-app --sub',
|
|
482
|
+
example4: ' create --help',
|
|
483
|
+
example5: ' create my-app --router tanstack',
|
|
484
|
+
example6: ' create my-app --router tanstack --tailwind',
|
|
485
|
+
example7: ' create my-app --bff',
|
|
486
|
+
example8: ' create my-app --router tanstack --bff-runtime effect',
|
|
487
|
+
example9: ' create my-app --router tanstack --bff-runtime effect --workspace',
|
|
488
|
+
example10: ' create my-super-app --ultramodern-workspace --ultramodern-package-source install --ultramodern-package-scope bleedingdev',
|
|
489
|
+
moreInfo: '📚 Learn more: https://modernjs.dev'
|
|
490
|
+
},
|
|
491
|
+
version: {
|
|
492
|
+
message: '@modern-js/create version: {version}'
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
const ZH_LOCALE = {
|
|
496
|
+
prompt: {
|
|
497
|
+
projectName: '请输入项目名称: '
|
|
498
|
+
},
|
|
499
|
+
error: {
|
|
500
|
+
projectNameEmpty: '错误: 项目名称不能为空',
|
|
501
|
+
directoryExists: '错误: 目录 "{projectName}" 已存在且不为空',
|
|
502
|
+
invalidRouter: '错误: 不支持的路由器 "{router}",请使用 "react-router" 或 "tanstack"',
|
|
503
|
+
invalidBffRuntime: '错误: 不支持的 BFF 运行时 "{runtime}",请使用 "hono" 或 "effect"',
|
|
504
|
+
createFailed: '创建项目时出错:'
|
|
505
|
+
},
|
|
506
|
+
message: {
|
|
507
|
+
welcome: '🚀 欢迎使用 Modern.js',
|
|
508
|
+
success: '✨ 创建成功!',
|
|
509
|
+
nextSteps: '📋 下一步:',
|
|
510
|
+
step1: 'cd {projectName}',
|
|
511
|
+
step2: 'pnpm install',
|
|
512
|
+
step3: 'pnpm dev'
|
|
513
|
+
},
|
|
514
|
+
help: {
|
|
515
|
+
title: '🚀 Modern.js 项目创建工具',
|
|
516
|
+
description: '快速创建一个新的 Modern.js 项目',
|
|
517
|
+
usage: '📖 用法:',
|
|
518
|
+
usageExample: ' create [项目名称] [选项]',
|
|
519
|
+
options: '⚙️ 选项:',
|
|
520
|
+
optionHelp: ' -h, --help 显示帮助信息',
|
|
521
|
+
optionVersion: ' -v, --version 显示版本信息',
|
|
522
|
+
optionLang: ' -l, --lang 设置语言 (zh 或 en)',
|
|
523
|
+
optionRouter: ' -r, --router 选择路由框架 (react-router 或 tanstack)',
|
|
524
|
+
optionBff: ' --bff 启用 BFF 模板(默认运行时:effect)',
|
|
525
|
+
optionBffRuntime: ' --bff-runtime 选择 BFF 运行时(hono 或 effect)',
|
|
526
|
+
optionTailwind: ' --tailwind 启用 Tailwind CSS v4 模板(PostCSS + 示例样式)',
|
|
527
|
+
optionWorkspace: ' --workspace 对 @modern-js 依赖使用 workspace 协议(用于本地 monorepo 联调)',
|
|
528
|
+
optionUltramodernWorkspace: ' --ultramodern-workspace 生成标准 UltraModern SuperApp 工作区',
|
|
529
|
+
optionUltramodernPackageSource: ' --ultramodern-package-source 选择 UltraModern 依赖来源(workspace 或 install)',
|
|
530
|
+
optionUltramodernPackageScope: ' --ultramodern-package-scope npm alias 安装使用的发布 scope(例如 bleedingdev)',
|
|
531
|
+
optionUltramodernPackageNamePrefix: ' --ultramodern-package-name-prefix npm alias 包名前缀(默认:modern-js-)',
|
|
532
|
+
optionSub: ' -s, --sub 标记为子项目(monorepo 中的子包)',
|
|
533
|
+
examples: '💡 示例:',
|
|
534
|
+
example1: ' create my-app',
|
|
535
|
+
example2: ' create my-app --lang zh',
|
|
536
|
+
example3: ' create my-app --sub',
|
|
537
|
+
example4: ' create --help',
|
|
538
|
+
example5: ' create my-app --router tanstack',
|
|
539
|
+
example6: ' create my-app --router tanstack --tailwind',
|
|
540
|
+
example7: ' create my-app --bff',
|
|
541
|
+
example8: ' create my-app --router tanstack --bff-runtime effect',
|
|
542
|
+
example9: ' create my-app --router tanstack --bff-runtime effect --workspace',
|
|
543
|
+
example10: ' create my-super-app --ultramodern-workspace --ultramodern-package-source install --ultramodern-package-scope bleedingdev',
|
|
544
|
+
moreInfo: '📚 更多信息: https://modernjs.dev'
|
|
545
|
+
},
|
|
546
|
+
version: {
|
|
547
|
+
message: '@modern-js/create 版本: {version}'
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
const i18n = new I18n();
|
|
551
|
+
const localeKeys = i18n.init('en', {
|
|
552
|
+
zh: ZH_LOCALE,
|
|
553
|
+
en: EN_LOCALE
|
|
554
|
+
});
|
|
555
|
+
const ultramodern_workspace_dirname = node_path.dirname(fileURLToPath(import.meta.url));
|
|
556
|
+
const workspaceTemplateDir = node_path.resolve(ultramodern_workspace_dirname, '..', 'template-workspace');
|
|
557
|
+
const TANSTACK_ROUTER_VERSION = '1.170.1';
|
|
558
|
+
const MODULE_FEDERATION_VERSION = '2.4.0';
|
|
559
|
+
const TYPESCRIPT_VERSION = '6.0.3';
|
|
560
|
+
const REACT_VERSION = '^19.2.6';
|
|
561
|
+
const REACT_DOM_VERSION = '^19.2.6';
|
|
562
|
+
const WORKSPACE_PACKAGE_VERSION = 'workspace:*';
|
|
563
|
+
const modernPackageNames = [
|
|
564
|
+
'@modern-js/app-tools',
|
|
565
|
+
'@modern-js/plugin-bff',
|
|
566
|
+
'@modern-js/plugin-tanstack',
|
|
567
|
+
'@modern-js/runtime'
|
|
568
|
+
];
|
|
569
|
+
const ULTRAMODERN_WORKSPACE_FLAG = '--ultramodern-workspace';
|
|
570
|
+
const shellApp = {
|
|
571
|
+
id: 'shell-super-app',
|
|
572
|
+
directory: 'apps/shell-super-app',
|
|
573
|
+
packageSuffix: 'shell-super-app',
|
|
574
|
+
displayName: 'Shell Super App',
|
|
575
|
+
kind: 'shell',
|
|
576
|
+
portEnv: 'SHELL_SUPER_APP_PORT',
|
|
577
|
+
port: 3020,
|
|
578
|
+
mfName: 'shellSuperApp',
|
|
579
|
+
remoteRefs: [
|
|
580
|
+
'remote-commerce',
|
|
581
|
+
'remote-identity',
|
|
582
|
+
'remote-design-system'
|
|
583
|
+
],
|
|
584
|
+
ownership: {
|
|
585
|
+
team: 'super-app-platform',
|
|
586
|
+
slack: '#super-app-platform',
|
|
587
|
+
pagerDuty: 'pd-super-app-platform',
|
|
588
|
+
runbookRef: 'runbooks/wave2/shell-super-app.md',
|
|
589
|
+
adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#shell-super-app',
|
|
590
|
+
blastRadius: {
|
|
591
|
+
tier: 'tier-0-shell',
|
|
592
|
+
references: [
|
|
593
|
+
'docs/super-app-rfc-adr/wave2/blast-radius.md#shell',
|
|
594
|
+
'docs/super-app-rfc-adr/wave2/rollback.md#shell-lkg'
|
|
595
|
+
]
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
const remoteApps = [
|
|
600
|
+
{
|
|
601
|
+
id: 'remote-commerce',
|
|
602
|
+
directory: 'apps/remotes/remote-commerce',
|
|
603
|
+
packageSuffix: 'remote-commerce',
|
|
604
|
+
displayName: 'Commerce Remote',
|
|
605
|
+
kind: 'vertical',
|
|
606
|
+
domain: 'commerce',
|
|
607
|
+
portEnv: 'REMOTE_COMMERCE_PORT',
|
|
608
|
+
port: 3021,
|
|
609
|
+
mfName: 'remoteCommerce',
|
|
610
|
+
exposes: {
|
|
611
|
+
'./Route': './src/remote-entry.tsx',
|
|
612
|
+
'./Widget': './src/components/CommerceWidget.tsx'
|
|
613
|
+
},
|
|
614
|
+
ownership: {
|
|
615
|
+
team: 'commerce-experience',
|
|
616
|
+
slack: '#commerce-experience',
|
|
617
|
+
pagerDuty: 'pd-commerce-experience',
|
|
618
|
+
runbookRef: 'runbooks/wave2/remote-commerce.md',
|
|
619
|
+
adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#remote-commerce',
|
|
620
|
+
blastRadius: {
|
|
621
|
+
tier: 'tier-1-revenue-path',
|
|
622
|
+
references: [
|
|
623
|
+
'docs/super-app-rfc-adr/wave2/blast-radius.md#commerce',
|
|
624
|
+
'docs/super-app-rfc-adr/wave2/rollback.md#commerce-lkg'
|
|
625
|
+
]
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
id: 'remote-identity',
|
|
631
|
+
directory: 'apps/remotes/remote-identity',
|
|
632
|
+
packageSuffix: 'remote-identity',
|
|
633
|
+
displayName: 'Identity Remote',
|
|
634
|
+
kind: 'vertical',
|
|
635
|
+
domain: 'identity',
|
|
636
|
+
portEnv: 'REMOTE_IDENTITY_PORT',
|
|
637
|
+
port: 3022,
|
|
638
|
+
mfName: 'remoteIdentity',
|
|
639
|
+
exposes: {
|
|
640
|
+
'./Route': './src/remote-entry.tsx',
|
|
641
|
+
'./Widget': './src/components/IdentityWidget.tsx'
|
|
642
|
+
},
|
|
643
|
+
ownership: {
|
|
644
|
+
team: 'identity-platform',
|
|
645
|
+
slack: '#identity-platform',
|
|
646
|
+
pagerDuty: 'pd-identity-platform',
|
|
647
|
+
runbookRef: 'runbooks/wave2/remote-identity.md',
|
|
648
|
+
adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#remote-identity',
|
|
649
|
+
blastRadius: {
|
|
650
|
+
tier: 'tier-0-authentication',
|
|
651
|
+
references: [
|
|
652
|
+
'docs/super-app-rfc-adr/wave2/blast-radius.md#identity',
|
|
653
|
+
'docs/super-app-rfc-adr/wave2/rollback.md#identity-lkg'
|
|
654
|
+
]
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
id: 'remote-design-system',
|
|
660
|
+
directory: 'apps/remotes/remote-design-system',
|
|
661
|
+
packageSuffix: 'remote-design-system',
|
|
662
|
+
displayName: 'Design System Remote',
|
|
663
|
+
kind: 'horizontal-design-system',
|
|
664
|
+
domain: 'design-system',
|
|
665
|
+
portEnv: 'REMOTE_DESIGN_SYSTEM_PORT',
|
|
666
|
+
port: 3023,
|
|
667
|
+
mfName: 'remoteDesignSystem',
|
|
668
|
+
exposes: {
|
|
669
|
+
'./Button': './src/components/Button.tsx',
|
|
670
|
+
'./tokens': './src/tokens.ts'
|
|
671
|
+
},
|
|
672
|
+
ownership: {
|
|
673
|
+
team: 'design-platform',
|
|
674
|
+
slack: '#design-platform',
|
|
675
|
+
pagerDuty: 'pd-design-platform',
|
|
676
|
+
runbookRef: 'runbooks/wave2/remote-design-system.md',
|
|
677
|
+
adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#remote-design-system',
|
|
678
|
+
blastRadius: {
|
|
679
|
+
tier: 'tier-0-shared-ui',
|
|
680
|
+
references: [
|
|
681
|
+
'docs/super-app-rfc-adr/wave2/blast-radius.md#design-system',
|
|
682
|
+
'docs/super-app-rfc-adr/wave2/rollback.md#design-system-pins'
|
|
683
|
+
]
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
];
|
|
688
|
+
const effectService = {
|
|
689
|
+
id: 'service-recommendations-effect',
|
|
690
|
+
directory: 'services/service-recommendations-effect',
|
|
691
|
+
packageSuffix: 'service-recommendations-effect',
|
|
692
|
+
portEnv: 'SERVICE_RECOMMENDATIONS_PORT',
|
|
693
|
+
port: 3030,
|
|
694
|
+
ownership: {
|
|
695
|
+
team: 'personalization-platform',
|
|
696
|
+
slack: '#personalization-platform',
|
|
697
|
+
pagerDuty: 'pd-personalization-platform',
|
|
698
|
+
runbookRef: 'runbooks/wave2/service-recommendations-effect.md',
|
|
699
|
+
adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#service-recommendations-effect',
|
|
700
|
+
blastRadius: {
|
|
701
|
+
tier: 'tier-2-personalization',
|
|
702
|
+
references: [
|
|
703
|
+
'docs/super-app-rfc-adr/wave2/blast-radius.md#recommendations',
|
|
704
|
+
'docs/super-app-rfc-adr/wave2/rollback.md#effect-service-lkg'
|
|
705
|
+
]
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
const sharedPackages = [
|
|
710
|
+
{
|
|
711
|
+
id: 'shared-contracts',
|
|
712
|
+
directory: 'packages/shared-contracts',
|
|
713
|
+
description: 'Route, ownership, and topology contract placeholders.'
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
id: 'shared-design-tokens',
|
|
717
|
+
directory: 'packages/shared-design-tokens',
|
|
718
|
+
description: 'Design token placeholders consumed by shell and remotes.'
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
id: 'shared-effect-api',
|
|
722
|
+
directory: 'packages/shared-effect-api',
|
|
723
|
+
description: 'Shared Effect API type placeholders for services and clients.'
|
|
724
|
+
}
|
|
725
|
+
];
|
|
726
|
+
function normalizePath(filePath) {
|
|
727
|
+
return filePath.split(node_path.sep).join('/');
|
|
728
|
+
}
|
|
729
|
+
function assertSafeRelativePath(relativePath) {
|
|
730
|
+
if (0 === relativePath.length || node_path.isAbsolute(relativePath) || relativePath.split(/[\\/]+/).includes('..')) throw new Error(`Unsafe workspace template path: ${relativePath}`);
|
|
731
|
+
}
|
|
732
|
+
function ensureInsideRoot(root, targetPath) {
|
|
733
|
+
const relativePath = node_path.relative(root, targetPath);
|
|
734
|
+
if (relativePath.startsWith('..') || node_path.isAbsolute(relativePath)) throw new Error(`Refusing to write outside workspace root: ${targetPath}`);
|
|
735
|
+
}
|
|
736
|
+
function writeFile(targetDir, relativePath, content) {
|
|
737
|
+
assertSafeRelativePath(relativePath);
|
|
738
|
+
const filePath = node_path.join(targetDir, relativePath);
|
|
739
|
+
ensureInsideRoot(targetDir, filePath);
|
|
740
|
+
if (node_fs.existsSync(filePath)) throw new Error(`Refusing to overwrite generated workspace file: ${relativePath}`);
|
|
741
|
+
node_fs.mkdirSync(node_path.dirname(filePath), {
|
|
742
|
+
recursive: true
|
|
743
|
+
});
|
|
744
|
+
node_fs.writeFileSync(filePath, content, 'utf-8');
|
|
745
|
+
}
|
|
746
|
+
function writeJson(targetDir, relativePath, value) {
|
|
747
|
+
writeFile(targetDir, relativePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
748
|
+
}
|
|
749
|
+
function renderTemplate(template, data) {
|
|
750
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key)=>data[key] ?? match);
|
|
751
|
+
}
|
|
752
|
+
function collectTemplateFiles(dir) {
|
|
753
|
+
const files = [];
|
|
754
|
+
function collect(currentDir) {
|
|
755
|
+
for (const entry of node_fs.readdirSync(currentDir, {
|
|
756
|
+
withFileTypes: true
|
|
757
|
+
}).sort((a, b)=>a.name.localeCompare(b.name))){
|
|
758
|
+
const entryPath = node_path.join(currentDir, entry.name);
|
|
759
|
+
if (entry.isDirectory()) collect(entryPath);
|
|
760
|
+
else if (entry.isFile()) files.push(normalizePath(node_path.relative(dir, entryPath)));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
collect(dir);
|
|
764
|
+
return files;
|
|
765
|
+
}
|
|
766
|
+
function hashFile(filePath) {
|
|
767
|
+
return node_crypto.createHash('sha256').update(node_fs.readFileSync(filePath)).digest('hex');
|
|
768
|
+
}
|
|
769
|
+
function hashTemplateTree(dir) {
|
|
770
|
+
const hash = node_crypto.createHash('sha256');
|
|
771
|
+
for (const relativePath of collectTemplateFiles(dir)){
|
|
772
|
+
hash.update(relativePath);
|
|
773
|
+
hash.update('\0');
|
|
774
|
+
hash.update(hashFile(node_path.join(dir, relativePath)));
|
|
775
|
+
hash.update('\0');
|
|
776
|
+
}
|
|
777
|
+
return hash.digest('hex');
|
|
778
|
+
}
|
|
779
|
+
function copyRootTemplate(targetDir, data) {
|
|
780
|
+
for (const relativePath of collectTemplateFiles(workspaceTemplateDir)){
|
|
781
|
+
const sourcePath = node_path.join(workspaceTemplateDir, relativePath);
|
|
782
|
+
const outputPath = relativePath.replace(/\.handlebars$/, '');
|
|
783
|
+
const content = relativePath.endsWith('.handlebars') ? renderTemplate(node_fs.readFileSync(sourcePath, 'utf-8'), data) : node_fs.readFileSync(sourcePath, 'utf-8');
|
|
784
|
+
writeFile(targetDir, outputPath, content);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function toPackageScope(packageName) {
|
|
788
|
+
const normalized = packageName.replace(/^@/, '').replace(/[\\/]+/g, '-').toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+|[._-]+$/g, '').replace(/-{2,}/g, '-');
|
|
789
|
+
return normalized || 'ultramodern-superapp';
|
|
790
|
+
}
|
|
791
|
+
function ultramodern_workspace_packageName(scope, suffix) {
|
|
792
|
+
return `@${scope}/${suffix}`;
|
|
793
|
+
}
|
|
794
|
+
function relativeRootFor(packageDir) {
|
|
795
|
+
return normalizePath(node_path.relative(packageDir, '.') || '.');
|
|
796
|
+
}
|
|
797
|
+
function resolvePackageSource(options) {
|
|
798
|
+
const strategy = options.packageSource?.strategy ?? 'workspace';
|
|
799
|
+
return {
|
|
800
|
+
strategy,
|
|
801
|
+
modernPackageVersion: 'install' === strategy ? options.packageSource?.modernPackageVersion ?? options.modernVersion : WORKSPACE_PACKAGE_VERSION,
|
|
802
|
+
registry: options.packageSource?.registry,
|
|
803
|
+
aliasScope: options.packageSource?.aliasScope,
|
|
804
|
+
aliasPackageNamePrefix: options.packageSource?.aliasPackageNamePrefix
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function modernPackageVersion(packageSource) {
|
|
808
|
+
return 'install' === packageSource.strategy ? packageSource.modernPackageVersion : WORKSPACE_PACKAGE_VERSION;
|
|
809
|
+
}
|
|
810
|
+
function modernAliasPackageName(packageName, packageSource) {
|
|
811
|
+
if (!packageSource.aliasScope) return packageName;
|
|
812
|
+
const scope = packageSource.aliasScope.replace(/^@/, '');
|
|
813
|
+
const unscopedName = packageName.split('/').at(-1);
|
|
814
|
+
return `@${scope}/${packageSource.aliasPackageNamePrefix ?? ''}${unscopedName}`;
|
|
815
|
+
}
|
|
816
|
+
function modernPackageSpecifier(packageName, packageSource) {
|
|
817
|
+
if ('install' !== packageSource.strategy) return WORKSPACE_PACKAGE_VERSION;
|
|
818
|
+
if (!packageSource.aliasScope) return packageSource.modernPackageVersion;
|
|
819
|
+
return `npm:${modernAliasPackageName(packageName, packageSource)}@${packageSource.modernPackageVersion}`;
|
|
820
|
+
}
|
|
821
|
+
function appDependencies(scope, packageSource) {
|
|
822
|
+
return {
|
|
823
|
+
'@modern-js/plugin-tanstack': modernPackageSpecifier('@modern-js/plugin-tanstack', packageSource),
|
|
824
|
+
'@modern-js/runtime': modernPackageSpecifier('@modern-js/runtime', packageSource),
|
|
825
|
+
'@module-federation/modern-js-v3': MODULE_FEDERATION_VERSION,
|
|
826
|
+
'@module-federation/runtime': MODULE_FEDERATION_VERSION,
|
|
827
|
+
'@tanstack/react-router': TANSTACK_ROUTER_VERSION,
|
|
828
|
+
[ultramodern_workspace_packageName(scope, 'shared-contracts')]: WORKSPACE_PACKAGE_VERSION,
|
|
829
|
+
[ultramodern_workspace_packageName(scope, 'shared-design-tokens')]: WORKSPACE_PACKAGE_VERSION,
|
|
830
|
+
react: REACT_VERSION,
|
|
831
|
+
'react-dom': REACT_DOM_VERSION
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function appDevDependencies(packageSource) {
|
|
835
|
+
return {
|
|
836
|
+
'@modern-js/app-tools': modernPackageSpecifier('@modern-js/app-tools', packageSource),
|
|
837
|
+
'@types/node': '^20',
|
|
838
|
+
'@types/react': '^19.1.8',
|
|
839
|
+
'@types/react-dom': '^19.1.6',
|
|
840
|
+
typescript: TYPESCRIPT_VERSION
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
function createRootPackageJson(scope, packageSource) {
|
|
844
|
+
return {
|
|
845
|
+
private: true,
|
|
846
|
+
name: scope,
|
|
847
|
+
version: '0.1.0',
|
|
848
|
+
scripts: {
|
|
849
|
+
dev: `pnpm --parallel --filter ${ultramodern_workspace_packageName(scope, shellApp.packageSuffix)} --filter ${ultramodern_workspace_packageName(scope, 'remote-commerce')} --filter ${ultramodern_workspace_packageName(scope, 'remote-identity')} --filter ${ultramodern_workspace_packageName(scope, 'remote-design-system')} dev`,
|
|
850
|
+
'dev:shell': `pnpm --filter ${ultramodern_workspace_packageName(scope, shellApp.packageSuffix)} dev`,
|
|
851
|
+
'dev:commerce': `pnpm --filter ${ultramodern_workspace_packageName(scope, 'remote-commerce')} dev`,
|
|
852
|
+
'dev:identity': `pnpm --filter ${ultramodern_workspace_packageName(scope, 'remote-identity')} dev`,
|
|
853
|
+
'dev:design-system': `pnpm --filter ${ultramodern_workspace_packageName(scope, 'remote-design-system')} dev`,
|
|
854
|
+
'dev:recommendations': `pnpm --filter ${ultramodern_workspace_packageName(scope, effectService.packageSuffix)} dev`,
|
|
855
|
+
build: 'pnpm -r --filter ./apps/** --filter ./services/** build',
|
|
856
|
+
typecheck: 'pnpm -r --filter ./apps/** --filter ./services/** --filter ./packages/** typecheck',
|
|
857
|
+
'ultramodern:check': "node ./scripts/validate-ultramodern-workspace.mjs",
|
|
858
|
+
check: 'pnpm ultramodern:check'
|
|
859
|
+
},
|
|
860
|
+
engines: {
|
|
861
|
+
node: '>=20',
|
|
862
|
+
pnpm: '>=10.0.0'
|
|
863
|
+
},
|
|
864
|
+
workspaces: [
|
|
865
|
+
'apps/*',
|
|
866
|
+
'apps/remotes/*',
|
|
867
|
+
'services/*',
|
|
868
|
+
'packages/*'
|
|
869
|
+
],
|
|
870
|
+
modernjs: {
|
|
871
|
+
preset: 'presetUltramodern',
|
|
872
|
+
workspace: 'ultramodern-superapp',
|
|
873
|
+
topology: './topology/reference-topology.json',
|
|
874
|
+
ownership: './topology/ownership.json',
|
|
875
|
+
packageSource: {
|
|
876
|
+
strategy: packageSource.strategy,
|
|
877
|
+
config: './.modernjs/ultramodern-package-source.json'
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
devDependencies: {
|
|
881
|
+
'@biomejs/biome': '1.9.4',
|
|
882
|
+
typescript: TYPESCRIPT_VERSION
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function createTsConfigBase(scope) {
|
|
887
|
+
return {
|
|
888
|
+
compilerOptions: {
|
|
889
|
+
target: 'ES2022',
|
|
890
|
+
lib: [
|
|
891
|
+
'DOM',
|
|
892
|
+
'DOM.Iterable',
|
|
893
|
+
'ES2022'
|
|
894
|
+
],
|
|
895
|
+
module: 'ESNext',
|
|
896
|
+
moduleResolution: 'Bundler',
|
|
897
|
+
jsx: 'preserve',
|
|
898
|
+
strict: true,
|
|
899
|
+
noEmit: true,
|
|
900
|
+
esModuleInterop: true,
|
|
901
|
+
skipLibCheck: true,
|
|
902
|
+
resolveJsonModule: true,
|
|
903
|
+
baseUrl: '.',
|
|
904
|
+
paths: Object.fromEntries(sharedPackages.map((sharedPackage)=>[
|
|
905
|
+
ultramodern_workspace_packageName(scope, sharedPackage.id),
|
|
906
|
+
[
|
|
907
|
+
`${sharedPackage.directory}/src/index.ts`
|
|
908
|
+
]
|
|
909
|
+
]))
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function createPackageTsConfig(packageDir, includeApi = false) {
|
|
914
|
+
const include = [
|
|
915
|
+
'src',
|
|
916
|
+
'modern.config.ts',
|
|
917
|
+
'module-federation.config.ts'
|
|
918
|
+
];
|
|
919
|
+
if (includeApi) include.push('api', 'shared');
|
|
920
|
+
return {
|
|
921
|
+
extends: `${relativeRootFor(packageDir)}/tsconfig.base.json`,
|
|
922
|
+
compilerOptions: {
|
|
923
|
+
baseUrl: '.',
|
|
924
|
+
paths: {
|
|
925
|
+
'@/*': [
|
|
926
|
+
'./src/*'
|
|
927
|
+
],
|
|
928
|
+
'@api/*': [
|
|
929
|
+
'./api/*'
|
|
930
|
+
],
|
|
931
|
+
'@shared/*': [
|
|
932
|
+
'./shared/*'
|
|
933
|
+
]
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
include
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function createAppPackage(scope, app, packageSource) {
|
|
940
|
+
return {
|
|
941
|
+
private: true,
|
|
942
|
+
name: ultramodern_workspace_packageName(scope, app.packageSuffix),
|
|
943
|
+
version: '0.1.0',
|
|
944
|
+
scripts: {
|
|
945
|
+
dev: 'modern dev',
|
|
946
|
+
build: 'modern build',
|
|
947
|
+
serve: 'modern serve',
|
|
948
|
+
typecheck: 'tsgo --noEmit -p tsconfig.json'
|
|
949
|
+
},
|
|
950
|
+
modernjs: {
|
|
951
|
+
preset: 'presetUltramodern',
|
|
952
|
+
role: 'shell' === app.kind ? 'shell' : 'module-federation-remote',
|
|
953
|
+
appId: app.id,
|
|
954
|
+
topology: `${relativeRootFor(app.directory)}/topology/reference-topology.json`
|
|
955
|
+
},
|
|
956
|
+
dependencies: appDependencies(scope, packageSource),
|
|
957
|
+
devDependencies: appDevDependencies(packageSource)
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function createServicePackage(scope, packageSource) {
|
|
961
|
+
return {
|
|
962
|
+
private: true,
|
|
963
|
+
name: ultramodern_workspace_packageName(scope, effectService.packageSuffix),
|
|
964
|
+
version: '0.1.0',
|
|
965
|
+
scripts: {
|
|
966
|
+
dev: 'modern dev',
|
|
967
|
+
build: 'modern build',
|
|
968
|
+
serve: 'modern serve',
|
|
969
|
+
typecheck: 'tsgo --noEmit -p tsconfig.json'
|
|
970
|
+
},
|
|
971
|
+
modernjs: {
|
|
972
|
+
preset: 'presetUltramodern',
|
|
973
|
+
role: 'effect-service',
|
|
974
|
+
appId: effectService.id,
|
|
975
|
+
topology: `${relativeRootFor(effectService.directory)}/topology/reference-topology.json`
|
|
976
|
+
},
|
|
977
|
+
dependencies: {
|
|
978
|
+
'@modern-js/runtime': modernPackageSpecifier('@modern-js/runtime', packageSource),
|
|
979
|
+
[ultramodern_workspace_packageName(scope, 'shared-effect-api')]: WORKSPACE_PACKAGE_VERSION,
|
|
980
|
+
react: REACT_VERSION,
|
|
981
|
+
'react-dom': REACT_DOM_VERSION
|
|
982
|
+
},
|
|
983
|
+
devDependencies: {
|
|
984
|
+
'@modern-js/app-tools': modernPackageSpecifier('@modern-js/app-tools', packageSource),
|
|
985
|
+
'@modern-js/plugin-bff': modernPackageSpecifier('@modern-js/plugin-bff', packageSource),
|
|
986
|
+
'@types/node': '^20',
|
|
987
|
+
'@types/react': '^19.1.8',
|
|
988
|
+
'@types/react-dom': '^19.1.6',
|
|
989
|
+
typescript: TYPESCRIPT_VERSION
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function createSharedPackage(scope, id, description) {
|
|
994
|
+
return {
|
|
995
|
+
private: true,
|
|
996
|
+
name: ultramodern_workspace_packageName(scope, id),
|
|
997
|
+
version: '0.1.0',
|
|
998
|
+
description,
|
|
999
|
+
type: 'module',
|
|
1000
|
+
exports: {
|
|
1001
|
+
'.': './src/index.ts'
|
|
1002
|
+
},
|
|
1003
|
+
scripts: {
|
|
1004
|
+
typecheck: 'tsgo --noEmit -p tsconfig.json'
|
|
1005
|
+
},
|
|
1006
|
+
devDependencies: {
|
|
1007
|
+
typescript: TYPESCRIPT_VERSION
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
function createAppModernConfig(app) {
|
|
1012
|
+
return `import { appTools, defineConfig, presetUltramodern } from '@modern-js/app-tools';
|
|
1013
|
+
import { tanstackRouterPlugin } from '@modern-js/plugin-tanstack';
|
|
1014
|
+
import { moduleFederationPlugin } from '@module-federation/modern-js-v3';
|
|
1015
|
+
|
|
1016
|
+
const appId = '${app.id}';
|
|
1017
|
+
const port = Number(process.env.${app.portEnv} ?? ${app.port});
|
|
1018
|
+
|
|
1019
|
+
export default defineConfig(
|
|
1020
|
+
presetUltramodern(
|
|
1021
|
+
{
|
|
1022
|
+
server: {
|
|
1023
|
+
port,
|
|
1024
|
+
ssr: {
|
|
1025
|
+
mode: 'string',
|
|
1026
|
+
moduleFederationAppSSR: true,
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
output: {
|
|
1030
|
+
polyfill: 'off',
|
|
1031
|
+
disableTsChecker: true,
|
|
1032
|
+
splitRouteChunks: false,
|
|
1033
|
+
},
|
|
1034
|
+
plugins: [
|
|
1035
|
+
appTools(),
|
|
1036
|
+
tanstackRouterPlugin(),
|
|
1037
|
+
moduleFederationPlugin(),
|
|
1038
|
+
],
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
appId,
|
|
1042
|
+
enableModuleFederationSSR: true,
|
|
1043
|
+
enableBffRequestId: true,
|
|
1044
|
+
enableTelemetryExporters: true,
|
|
1045
|
+
telemetryFailLoudStartup: false,
|
|
1046
|
+
},
|
|
1047
|
+
),
|
|
1048
|
+
);
|
|
1049
|
+
`;
|
|
1050
|
+
}
|
|
1051
|
+
function createShellModuleFederationConfig() {
|
|
1052
|
+
return `import { createRequire } from 'node:module';
|
|
1053
|
+
import { createModuleFederationConfig } from '@module-federation/modern-js-v3';
|
|
1054
|
+
import { dependencies } from './package.json';
|
|
1055
|
+
|
|
1056
|
+
const require = createRequire(import.meta.url);
|
|
1057
|
+
const runtimeVersion = (
|
|
1058
|
+
require('@modern-js/runtime/package.json') as { version: string }
|
|
1059
|
+
).version;
|
|
1060
|
+
const reactVersion = (require('react/package.json') as { version: string })
|
|
1061
|
+
.version;
|
|
1062
|
+
const reactDomVersion = (
|
|
1063
|
+
require('react-dom/package.json') as { version: string }
|
|
1064
|
+
).version;
|
|
1065
|
+
|
|
1066
|
+
export default createModuleFederationConfig({
|
|
1067
|
+
name: '${shellApp.mfName}',
|
|
1068
|
+
dts: false,
|
|
1069
|
+
remotes: {
|
|
1070
|
+
commerce:
|
|
1071
|
+
process.env.REMOTE_COMMERCE_MF_MANIFEST ??
|
|
1072
|
+
'remoteCommerce@http://localhost:3021/mf-manifest.json',
|
|
1073
|
+
identity:
|
|
1074
|
+
process.env.REMOTE_IDENTITY_MF_MANIFEST ??
|
|
1075
|
+
'remoteIdentity@http://localhost:3022/mf-manifest.json',
|
|
1076
|
+
designSystem:
|
|
1077
|
+
process.env.REMOTE_DESIGN_SYSTEM_MF_MANIFEST ??
|
|
1078
|
+
'remoteDesignSystem@http://localhost:3023/mf-manifest.json',
|
|
1079
|
+
},
|
|
1080
|
+
shared: {
|
|
1081
|
+
react: {
|
|
1082
|
+
singleton: true,
|
|
1083
|
+
requiredVersion: reactVersion,
|
|
1084
|
+
treeShaking: false,
|
|
1085
|
+
},
|
|
1086
|
+
'react-dom': {
|
|
1087
|
+
singleton: true,
|
|
1088
|
+
requiredVersion: reactDomVersion,
|
|
1089
|
+
treeShaking: false,
|
|
1090
|
+
},
|
|
1091
|
+
'@tanstack/react-router': {
|
|
1092
|
+
singleton: true,
|
|
1093
|
+
requiredVersion: dependencies['@tanstack/react-router'],
|
|
1094
|
+
treeShaking: false,
|
|
1095
|
+
},
|
|
1096
|
+
'@modern-js/runtime': {
|
|
1097
|
+
singleton: true,
|
|
1098
|
+
requiredVersion: runtimeVersion,
|
|
1099
|
+
treeShaking: false,
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
});
|
|
1103
|
+
`;
|
|
1104
|
+
}
|
|
1105
|
+
function createRemoteModuleFederationConfig(app) {
|
|
1106
|
+
const exposes = JSON.stringify(app.exposes ?? {}, null, 4).replace(/^/gm, ' ');
|
|
1107
|
+
return `import { createRequire } from 'node:module';
|
|
1108
|
+
import { createModuleFederationConfig } from '@module-federation/modern-js-v3';
|
|
1109
|
+
import { dependencies } from './package.json';
|
|
1110
|
+
|
|
1111
|
+
const require = createRequire(import.meta.url);
|
|
1112
|
+
const runtimeVersion = (
|
|
1113
|
+
require('@modern-js/runtime/package.json') as { version: string }
|
|
1114
|
+
).version;
|
|
1115
|
+
const reactVersion = (require('react/package.json') as { version: string })
|
|
1116
|
+
.version;
|
|
1117
|
+
const reactDomVersion = (
|
|
1118
|
+
require('react-dom/package.json') as { version: string }
|
|
1119
|
+
).version;
|
|
1120
|
+
|
|
1121
|
+
export default createModuleFederationConfig({
|
|
1122
|
+
name: '${app.mfName}',
|
|
1123
|
+
dts: false,
|
|
1124
|
+
filename: 'remoteEntry.js',
|
|
1125
|
+
exposes: ${exposes},
|
|
1126
|
+
shared: {
|
|
1127
|
+
react: {
|
|
1128
|
+
singleton: true,
|
|
1129
|
+
requiredVersion: reactVersion,
|
|
1130
|
+
treeShaking: false,
|
|
1131
|
+
},
|
|
1132
|
+
'react-dom': {
|
|
1133
|
+
singleton: true,
|
|
1134
|
+
requiredVersion: reactDomVersion,
|
|
1135
|
+
treeShaking: false,
|
|
1136
|
+
},
|
|
1137
|
+
'@tanstack/react-router': {
|
|
1138
|
+
singleton: true,
|
|
1139
|
+
requiredVersion: dependencies['@tanstack/react-router'],
|
|
1140
|
+
treeShaking: false,
|
|
1141
|
+
},
|
|
1142
|
+
'@modern-js/runtime': {
|
|
1143
|
+
singleton: true,
|
|
1144
|
+
requiredVersion: runtimeVersion,
|
|
1145
|
+
treeShaking: false,
|
|
1146
|
+
},
|
|
1147
|
+
},
|
|
1148
|
+
});
|
|
1149
|
+
`;
|
|
1150
|
+
}
|
|
1151
|
+
function createServiceModernConfig() {
|
|
1152
|
+
return `import { appTools, defineConfig, presetUltramodern } from '@modern-js/app-tools';
|
|
1153
|
+
import { bffPlugin } from '@modern-js/plugin-bff';
|
|
1154
|
+
|
|
1155
|
+
const appId = '${effectService.id}';
|
|
1156
|
+
const port = Number(process.env.${effectService.portEnv} ?? ${effectService.port});
|
|
1157
|
+
|
|
1158
|
+
export default defineConfig(
|
|
1159
|
+
presetUltramodern(
|
|
1160
|
+
{
|
|
1161
|
+
server: {
|
|
1162
|
+
port,
|
|
1163
|
+
},
|
|
1164
|
+
bff: {
|
|
1165
|
+
prefix: '/recommendations-api',
|
|
1166
|
+
runtimeFramework: 'effect',
|
|
1167
|
+
effect: {
|
|
1168
|
+
openapi: {
|
|
1169
|
+
path: '/openapi.json',
|
|
1170
|
+
},
|
|
1171
|
+
},
|
|
1172
|
+
},
|
|
1173
|
+
plugins: [appTools(), bffPlugin()],
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
appId,
|
|
1177
|
+
enableBffRequestId: true,
|
|
1178
|
+
enableTelemetryExporters: true,
|
|
1179
|
+
telemetryFailLoudStartup: false,
|
|
1180
|
+
},
|
|
1181
|
+
),
|
|
1182
|
+
);
|
|
1183
|
+
`;
|
|
1184
|
+
}
|
|
1185
|
+
function createShellPage() {
|
|
1186
|
+
return `const remotes = [
|
|
1187
|
+
'remote-commerce',
|
|
1188
|
+
'remote-identity',
|
|
1189
|
+
'remote-design-system',
|
|
1190
|
+
];
|
|
1191
|
+
|
|
1192
|
+
export default function ShellHome() {
|
|
1193
|
+
return (
|
|
1194
|
+
<main>
|
|
1195
|
+
<h1>UltraModern SuperApp Shell</h1>
|
|
1196
|
+
<p data-testid="ultramodern-preset">presetUltramodern workspace</p>
|
|
1197
|
+
<ul>
|
|
1198
|
+
{remotes.map(remote => (
|
|
1199
|
+
<li key={remote}>{remote}</li>
|
|
1200
|
+
))}
|
|
1201
|
+
</ul>
|
|
1202
|
+
</main>
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
`;
|
|
1206
|
+
}
|
|
1207
|
+
function createRemotePage(app) {
|
|
1208
|
+
return `export default function ${toPascalCase(app.id)}Home() {
|
|
1209
|
+
return (
|
|
1210
|
+
<main>
|
|
1211
|
+
<h1>${app.displayName}</h1>
|
|
1212
|
+
<p data-mf-role="${app.kind}">${app.domain ?? app.kind}</p>
|
|
1213
|
+
</main>
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
`;
|
|
1217
|
+
}
|
|
1218
|
+
function createLayout(appId) {
|
|
1219
|
+
return `import type { ReactNode } from 'react';
|
|
1220
|
+
|
|
1221
|
+
export default function Layout({ children }: { children: ReactNode }) {
|
|
1222
|
+
return <div data-app-id="${appId}">{children}</div>;
|
|
1223
|
+
}
|
|
1224
|
+
`;
|
|
1225
|
+
}
|
|
1226
|
+
function createRemoteEntry(app) {
|
|
1227
|
+
const componentName = 'remote-identity' === app.id ? 'IdentityWidget' : 'CommerceWidget';
|
|
1228
|
+
return `export { default } from './components/${componentName}';
|
|
1229
|
+
`;
|
|
1230
|
+
}
|
|
1231
|
+
function createRemoteWidget(app) {
|
|
1232
|
+
const componentName = 'remote-identity' === app.id ? 'IdentityWidget' : 'CommerceWidget';
|
|
1233
|
+
return `export default function ${componentName}() {
|
|
1234
|
+
return (
|
|
1235
|
+
<section data-mf-remote="${app.id}">
|
|
1236
|
+
<h2>${app.displayName}</h2>
|
|
1237
|
+
<p>Owns the ${app.domain} vertical route surface.</p>
|
|
1238
|
+
</section>
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
`;
|
|
1242
|
+
}
|
|
1243
|
+
function createDesignButton() {
|
|
1244
|
+
return `import { designTokens } from '../tokens';
|
|
1245
|
+
|
|
1246
|
+
export default function Button({ label = 'Design System Button' }: { label?: string }) {
|
|
1247
|
+
return (
|
|
1248
|
+
<button
|
|
1249
|
+
type="button"
|
|
1250
|
+
style={{
|
|
1251
|
+
borderRadius: designTokens.radius.control,
|
|
1252
|
+
color: designTokens.color.foreground,
|
|
1253
|
+
}}
|
|
1254
|
+
>
|
|
1255
|
+
{label}
|
|
1256
|
+
</button>
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
`;
|
|
1260
|
+
}
|
|
1261
|
+
function createDesignTokens() {
|
|
1262
|
+
return `export const designTokens = {
|
|
1263
|
+
color: {
|
|
1264
|
+
foreground: '#133225',
|
|
1265
|
+
accent: '#2f8f68',
|
|
1266
|
+
},
|
|
1267
|
+
radius: {
|
|
1268
|
+
control: '999px',
|
|
1269
|
+
},
|
|
1270
|
+
} as const;
|
|
1271
|
+
`;
|
|
1272
|
+
}
|
|
1273
|
+
function createEffectSharedApi() {
|
|
1274
|
+
return `import {
|
|
1275
|
+
HttpApi,
|
|
1276
|
+
HttpApiEndpoint,
|
|
1277
|
+
HttpApiGroup,
|
|
1278
|
+
Schema,
|
|
1279
|
+
} from '@modern-js/plugin-bff/effect-client';
|
|
1280
|
+
|
|
1281
|
+
const recommendationSchema = Schema.Struct({
|
|
1282
|
+
id: Schema.String,
|
|
1283
|
+
title: Schema.String,
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
export const recommendationsEffectApi = HttpApi.make(
|
|
1287
|
+
'RecommendationsEffectApi',
|
|
1288
|
+
).add(
|
|
1289
|
+
HttpApiGroup.make('recommendations').add(
|
|
1290
|
+
HttpApiEndpoint.get('list', '/effect/recommendations', {
|
|
1291
|
+
success: Schema.Struct({
|
|
1292
|
+
items: Schema.Array(recommendationSchema),
|
|
1293
|
+
}),
|
|
1294
|
+
}),
|
|
1295
|
+
),
|
|
1296
|
+
);
|
|
1297
|
+
`;
|
|
1298
|
+
}
|
|
1299
|
+
function createEffectServiceEntry() {
|
|
1300
|
+
return `import {
|
|
1301
|
+
defineEffectBff,
|
|
1302
|
+
Effect,
|
|
1303
|
+
HttpApiBuilder,
|
|
1304
|
+
Layer,
|
|
1305
|
+
} from '@modern-js/plugin-bff/effect-server';
|
|
1306
|
+
import { recommendationsEffectApi } from '../../shared/effect/api';
|
|
1307
|
+
|
|
1308
|
+
const recommendationsLayer = HttpApiBuilder.group(
|
|
1309
|
+
recommendationsEffectApi,
|
|
1310
|
+
'recommendations',
|
|
1311
|
+
(handlers: any) =>
|
|
1312
|
+
handlers.handle('list', () =>
|
|
1313
|
+
Effect.succeed({
|
|
1314
|
+
items: [
|
|
1315
|
+
{
|
|
1316
|
+
id: 'starter-recommendation',
|
|
1317
|
+
title: 'Wire a real recommendation source here',
|
|
1318
|
+
},
|
|
1319
|
+
],
|
|
1320
|
+
}),
|
|
1321
|
+
),
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1324
|
+
const layer = HttpApiBuilder.layer(recommendationsEffectApi).pipe(
|
|
1325
|
+
Layer.provide(recommendationsLayer),
|
|
1326
|
+
);
|
|
1327
|
+
|
|
1328
|
+
export default defineEffectBff({
|
|
1329
|
+
api: recommendationsEffectApi,
|
|
1330
|
+
layer,
|
|
1331
|
+
});
|
|
1332
|
+
`;
|
|
1333
|
+
}
|
|
1334
|
+
function toPascalCase(value) {
|
|
1335
|
+
return value.split(/[-_]+/).filter(Boolean).map((part)=>`${part.charAt(0).toUpperCase()}${part.slice(1)}`).join('');
|
|
1336
|
+
}
|
|
1337
|
+
function createTopology(scope) {
|
|
1338
|
+
return {
|
|
1339
|
+
schemaVersion: 1,
|
|
1340
|
+
id: 'ultramodern-superapp-workspace-reference-topology',
|
|
1341
|
+
description: 'Generated UltraModern workspace skeleton based on the reference topology shape.',
|
|
1342
|
+
preset: 'presetUltramodern',
|
|
1343
|
+
sourceFixture: "scripts/mv-integration-pilot/__fixtures__/reference-topology.json",
|
|
1344
|
+
shell: {
|
|
1345
|
+
id: shellApp.id,
|
|
1346
|
+
kind: 'shell',
|
|
1347
|
+
package: ultramodern_workspace_packageName(scope, shellApp.packageSuffix),
|
|
1348
|
+
remoteRefs: shellApp.remoteRefs,
|
|
1349
|
+
moduleFederation: {
|
|
1350
|
+
role: 'host',
|
|
1351
|
+
name: shellApp.mfName,
|
|
1352
|
+
remotes: remoteApps.map((remote)=>({
|
|
1353
|
+
id: remote.id,
|
|
1354
|
+
name: remote.mfName,
|
|
1355
|
+
manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`
|
|
1356
|
+
})),
|
|
1357
|
+
ssr: true,
|
|
1358
|
+
sharedContractVersion: 'mf-ssr-contract-v1'
|
|
1359
|
+
},
|
|
1360
|
+
ownership: shellApp.ownership
|
|
1361
|
+
},
|
|
1362
|
+
remotes: remoteApps.map((remote)=>({
|
|
1363
|
+
id: remote.id,
|
|
1364
|
+
kind: remote.kind,
|
|
1365
|
+
domain: remote.domain,
|
|
1366
|
+
package: ultramodern_workspace_packageName(scope, remote.packageSuffix),
|
|
1367
|
+
moduleFederation: {
|
|
1368
|
+
role: 'remote',
|
|
1369
|
+
name: remote.mfName,
|
|
1370
|
+
manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`,
|
|
1371
|
+
exposes: Object.keys(remote.exposes ?? {}),
|
|
1372
|
+
ssr: true,
|
|
1373
|
+
fallbackTelemetryEvent: 'modernjs:mv-runtime-parity',
|
|
1374
|
+
sharedContractVersion: 'mf-ssr-contract-v1'
|
|
1375
|
+
},
|
|
1376
|
+
ownership: remote.ownership
|
|
1377
|
+
})),
|
|
1378
|
+
effectServices: [
|
|
1379
|
+
{
|
|
1380
|
+
id: effectService.id,
|
|
1381
|
+
kind: 'effect-service',
|
|
1382
|
+
runtime: 'effect',
|
|
1383
|
+
package: ultramodern_workspace_packageName(scope, effectService.packageSuffix),
|
|
1384
|
+
consumedBy: [
|
|
1385
|
+
shellApp.id,
|
|
1386
|
+
'remote-commerce'
|
|
1387
|
+
],
|
|
1388
|
+
bff: {
|
|
1389
|
+
prefix: '/recommendations-api',
|
|
1390
|
+
openapi: '/openapi.json'
|
|
1391
|
+
},
|
|
1392
|
+
ownership: effectService.ownership
|
|
1393
|
+
}
|
|
1394
|
+
],
|
|
1395
|
+
sharedPackages: sharedPackages.map((sharedPackage)=>({
|
|
1396
|
+
id: sharedPackage.id,
|
|
1397
|
+
package: ultramodern_workspace_packageName(scope, sharedPackage.id),
|
|
1398
|
+
path: sharedPackage.directory,
|
|
1399
|
+
description: sharedPackage.description
|
|
1400
|
+
})),
|
|
1401
|
+
validation: {
|
|
1402
|
+
script: "scripts/validate-ultramodern-workspace.mjs",
|
|
1403
|
+
commands: [
|
|
1404
|
+
'pnpm ultramodern:check'
|
|
1405
|
+
]
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
function createOwnership(scope) {
|
|
1410
|
+
return {
|
|
1411
|
+
schemaVersion: 1,
|
|
1412
|
+
preset: 'presetUltramodern',
|
|
1413
|
+
owners: [
|
|
1414
|
+
shellApp,
|
|
1415
|
+
...remoteApps,
|
|
1416
|
+
{
|
|
1417
|
+
id: effectService.id,
|
|
1418
|
+
packageSuffix: effectService.packageSuffix,
|
|
1419
|
+
directory: effectService.directory,
|
|
1420
|
+
ownership: effectService.ownership
|
|
1421
|
+
},
|
|
1422
|
+
...sharedPackages.map((sharedPackage)=>({
|
|
1423
|
+
id: sharedPackage.id,
|
|
1424
|
+
packageSuffix: sharedPackage.id,
|
|
1425
|
+
directory: sharedPackage.directory,
|
|
1426
|
+
ownership: {
|
|
1427
|
+
team: 'super-app-platform',
|
|
1428
|
+
slack: '#super-app-platform',
|
|
1429
|
+
pagerDuty: 'pd-super-app-platform',
|
|
1430
|
+
runbookRef: `runbooks/wave2/${sharedPackage.id}.md`,
|
|
1431
|
+
adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#shared-packages',
|
|
1432
|
+
blastRadius: {
|
|
1433
|
+
tier: 'tier-1-shared-contract',
|
|
1434
|
+
references: [
|
|
1435
|
+
'docs/super-app-rfc-adr/wave2/blast-radius.md#shared-packages'
|
|
1436
|
+
]
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}))
|
|
1440
|
+
].map((owner)=>({
|
|
1441
|
+
id: owner.id,
|
|
1442
|
+
package: ultramodern_workspace_packageName(scope, owner.packageSuffix),
|
|
1443
|
+
path: owner.directory,
|
|
1444
|
+
ownership: owner.ownership
|
|
1445
|
+
}))
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
function createDevelopmentOverlay() {
|
|
1449
|
+
return {
|
|
1450
|
+
schemaVersion: 1,
|
|
1451
|
+
environment: 'development',
|
|
1452
|
+
preset: 'presetUltramodern',
|
|
1453
|
+
ports: Object.fromEntries([
|
|
1454
|
+
shellApp,
|
|
1455
|
+
...remoteApps
|
|
1456
|
+
].map((app)=>[
|
|
1457
|
+
app.id,
|
|
1458
|
+
app.port
|
|
1459
|
+
]).concat([
|
|
1460
|
+
[
|
|
1461
|
+
effectService.id,
|
|
1462
|
+
effectService.port
|
|
1463
|
+
]
|
|
1464
|
+
])),
|
|
1465
|
+
manifests: Object.fromEntries(remoteApps.map((remote)=>[
|
|
1466
|
+
remote.id,
|
|
1467
|
+
`http://localhost:${remote.port}/mf-manifest.json`
|
|
1468
|
+
])),
|
|
1469
|
+
services: {
|
|
1470
|
+
[effectService.id]: `http://localhost:${effectService.port}/recommendations-api`
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
function createPackageSourceMetadata(scope, packageSource) {
|
|
1475
|
+
const modernPackages = {
|
|
1476
|
+
packages: modernPackageNames,
|
|
1477
|
+
specifier: modernPackageVersion(packageSource)
|
|
1478
|
+
};
|
|
1479
|
+
if (packageSource.registry) modernPackages.registry = packageSource.registry;
|
|
1480
|
+
if (packageSource.aliasScope) modernPackages.aliases = Object.fromEntries(modernPackageNames.map((packageName)=>[
|
|
1481
|
+
packageName,
|
|
1482
|
+
modernAliasPackageName(packageName, packageSource)
|
|
1483
|
+
]));
|
|
1484
|
+
return {
|
|
1485
|
+
schemaVersion: 1,
|
|
1486
|
+
strategy: packageSource.strategy,
|
|
1487
|
+
modernPackages,
|
|
1488
|
+
generatedWorkspacePackages: {
|
|
1489
|
+
packages: sharedPackages.map((sharedPackage)=>ultramodern_workspace_packageName(scope, sharedPackage.id)),
|
|
1490
|
+
specifier: WORKSPACE_PACKAGE_VERSION
|
|
1491
|
+
},
|
|
1492
|
+
validation: {
|
|
1493
|
+
validator: "scripts/validate-ultramodern-workspace.mjs",
|
|
1494
|
+
strategyAwareChecks: [
|
|
1495
|
+
'generated-validator',
|
|
1496
|
+
'contract-doctor'
|
|
1497
|
+
]
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
function createTemplateManifest(modernVersion, packageSource) {
|
|
1502
|
+
return {
|
|
1503
|
+
schemaVersion: 1,
|
|
1504
|
+
template: {
|
|
1505
|
+
id: 'modernjs-ultramodern-superapp-workspace',
|
|
1506
|
+
version: modernVersion,
|
|
1507
|
+
displayName: 'Modern.js UltraModern SuperApp Workspace',
|
|
1508
|
+
description: 'Canonical shell, remotes, Effect service, shared packages, and topology skeleton.',
|
|
1509
|
+
compatibilityLane: 'ultramodern-mv',
|
|
1510
|
+
minimumModernVersion: modernVersion
|
|
1511
|
+
},
|
|
1512
|
+
source: {
|
|
1513
|
+
type: 'builtin',
|
|
1514
|
+
name: 'modernjs-ultramodern-superapp-workspace',
|
|
1515
|
+
repositoryPath: 'packages/toolkit/create/template-workspace',
|
|
1516
|
+
generator: 'packages/toolkit/create/src/ultramodern-workspace.ts'
|
|
1517
|
+
},
|
|
1518
|
+
integrity: {
|
|
1519
|
+
checksums: [
|
|
1520
|
+
{
|
|
1521
|
+
algorithm: 'sha256',
|
|
1522
|
+
value: hashTemplateTree(workspaceTemplateDir),
|
|
1523
|
+
scope: 'source-tree'
|
|
1524
|
+
}
|
|
1525
|
+
],
|
|
1526
|
+
provenance: {
|
|
1527
|
+
kind: 'repo-local',
|
|
1528
|
+
issuer: '@modern-js/create',
|
|
1529
|
+
subject: 'packages/toolkit/create/template-workspace'
|
|
1530
|
+
}
|
|
1531
|
+
},
|
|
1532
|
+
materialization: {
|
|
1533
|
+
targetRoot: 'generated-project-root',
|
|
1534
|
+
allowedPaths: [
|
|
1535
|
+
'.modernjs/**',
|
|
1536
|
+
'README.md',
|
|
1537
|
+
'apps/**',
|
|
1538
|
+
'packages/**',
|
|
1539
|
+
'package.json',
|
|
1540
|
+
'pnpm-workspace.yaml',
|
|
1541
|
+
"scripts/**",
|
|
1542
|
+
'services/**',
|
|
1543
|
+
'topology/**',
|
|
1544
|
+
'tsconfig.base.json'
|
|
1545
|
+
],
|
|
1546
|
+
deniedPaths: [
|
|
1547
|
+
'.git/**',
|
|
1548
|
+
'.github/**',
|
|
1549
|
+
'.npmrc',
|
|
1550
|
+
'.yarnrc',
|
|
1551
|
+
'.env',
|
|
1552
|
+
'.env.*',
|
|
1553
|
+
'node_modules/**',
|
|
1554
|
+
'dist/**'
|
|
1555
|
+
],
|
|
1556
|
+
overwritePolicy: 'deny-existing'
|
|
1557
|
+
},
|
|
1558
|
+
packageSource: {
|
|
1559
|
+
strategy: packageSource.strategy,
|
|
1560
|
+
config: '.modernjs/ultramodern-package-source.json',
|
|
1561
|
+
modernPackageSpecifier: modernPackageVersion(packageSource),
|
|
1562
|
+
generatedWorkspacePackageSpecifier: WORKSPACE_PACKAGE_VERSION
|
|
1563
|
+
},
|
|
1564
|
+
validation: {
|
|
1565
|
+
schemaValidation: true,
|
|
1566
|
+
sourceValidation: [
|
|
1567
|
+
'source-type-supported',
|
|
1568
|
+
'checksum-verified',
|
|
1569
|
+
'provenance-present'
|
|
1570
|
+
],
|
|
1571
|
+
materializationValidation: [
|
|
1572
|
+
'path-boundary-allowlist',
|
|
1573
|
+
'path-boundary-denylist',
|
|
1574
|
+
'no-path-traversal',
|
|
1575
|
+
'no-absolute-paths',
|
|
1576
|
+
'overwrite-policy-enforced'
|
|
1577
|
+
],
|
|
1578
|
+
postMaterializationValidation: [
|
|
1579
|
+
'ultramodern-workspace-contract-check',
|
|
1580
|
+
'template-manifest-retained'
|
|
1581
|
+
],
|
|
1582
|
+
expectedCommands: [
|
|
1583
|
+
"pnpm install --ignore-scripts",
|
|
1584
|
+
'pnpm run ultramodern:check'
|
|
1585
|
+
]
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function writeApp(targetDir, scope, app, packageSource) {
|
|
1590
|
+
writeJson(targetDir, `${app.directory}/package.json`, createAppPackage(scope, app, packageSource));
|
|
1591
|
+
writeJson(targetDir, `${app.directory}/tsconfig.json`, createPackageTsConfig(app.directory));
|
|
1592
|
+
writeFile(targetDir, `${app.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n");
|
|
1593
|
+
writeFile(targetDir, `${app.directory}/modern.config.ts`, createAppModernConfig(app));
|
|
1594
|
+
writeFile(targetDir, `${app.directory}/module-federation.config.ts`, 'shell' === app.kind ? createShellModuleFederationConfig() : createRemoteModuleFederationConfig(app));
|
|
1595
|
+
writeFile(targetDir, `${app.directory}/src/routes/layout.tsx`, createLayout(app.id));
|
|
1596
|
+
writeFile(targetDir, `${app.directory}/src/routes/page.tsx`, 'shell' === app.kind ? createShellPage() : createRemotePage(app));
|
|
1597
|
+
if ('vertical' === app.kind) {
|
|
1598
|
+
writeFile(targetDir, `${app.directory}/src/remote-entry.tsx`, createRemoteEntry(app));
|
|
1599
|
+
const widgetFile = 'remote-identity' === app.id ? 'IdentityWidget.tsx' : 'CommerceWidget.tsx';
|
|
1600
|
+
writeFile(targetDir, `${app.directory}/src/components/${widgetFile}`, createRemoteWidget(app));
|
|
1601
|
+
}
|
|
1602
|
+
if ('horizontal-design-system' === app.kind) {
|
|
1603
|
+
writeFile(targetDir, `${app.directory}/src/components/Button.tsx`, createDesignButton());
|
|
1604
|
+
writeFile(targetDir, `${app.directory}/src/tokens.ts`, createDesignTokens());
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function writeEffectService(targetDir, scope, packageSource) {
|
|
1608
|
+
writeJson(targetDir, `${effectService.directory}/package.json`, createServicePackage(scope, packageSource));
|
|
1609
|
+
writeJson(targetDir, `${effectService.directory}/tsconfig.json`, createPackageTsConfig(effectService.directory, true));
|
|
1610
|
+
writeFile(targetDir, `${effectService.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n");
|
|
1611
|
+
writeFile(targetDir, `${effectService.directory}/src/routes/page.tsx`, `export default function RecommendationsServiceHome() {
|
|
1612
|
+
return <main>Recommendations Effect service</main>;
|
|
1613
|
+
}
|
|
1614
|
+
`);
|
|
1615
|
+
writeFile(targetDir, `${effectService.directory}/modern.config.ts`, createServiceModernConfig());
|
|
1616
|
+
writeFile(targetDir, `${effectService.directory}/shared/effect/api.ts`, createEffectSharedApi());
|
|
1617
|
+
writeFile(targetDir, `${effectService.directory}/api/effect/index.ts`, createEffectServiceEntry());
|
|
1618
|
+
}
|
|
1619
|
+
function writeSharedPackages(targetDir, scope) {
|
|
1620
|
+
for (const sharedPackage of sharedPackages){
|
|
1621
|
+
writeJson(targetDir, `${sharedPackage.directory}/package.json`, createSharedPackage(scope, sharedPackage.id, sharedPackage.description));
|
|
1622
|
+
writeJson(targetDir, `${sharedPackage.directory}/tsconfig.json`, {
|
|
1623
|
+
extends: `${relativeRootFor(sharedPackage.directory)}/tsconfig.base.json`,
|
|
1624
|
+
include: [
|
|
1625
|
+
'src'
|
|
1626
|
+
]
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
writeFile(targetDir, 'packages/shared-contracts/src/index.ts', `export const ultramodernWorkspaceContract = {
|
|
1630
|
+
preset: 'presetUltramodern',
|
|
1631
|
+
topology: 'topology/reference-topology.json',
|
|
1632
|
+
ownership: 'topology/ownership.json',
|
|
1633
|
+
} as const;
|
|
1634
|
+
`);
|
|
1635
|
+
writeFile(targetDir, 'packages/shared-design-tokens/src/index.ts', `export const sharedDesignTokens = {
|
|
1636
|
+
color: {
|
|
1637
|
+
surface: '#f6fbf7',
|
|
1638
|
+
foreground: '#133225',
|
|
1639
|
+
accent: '#2f8f68',
|
|
1640
|
+
},
|
|
1641
|
+
} as const;
|
|
1642
|
+
`);
|
|
1643
|
+
writeFile(targetDir, 'packages/shared-effect-api/src/index.ts', `export type Recommendation = {
|
|
1644
|
+
id: string;
|
|
1645
|
+
title: string;
|
|
1646
|
+
};
|
|
1647
|
+
|
|
1648
|
+
export const recommendationsApiContract = {
|
|
1649
|
+
serviceId: '${effectService.id}',
|
|
1650
|
+
basePath: '/recommendations-api/effect/recommendations',
|
|
1651
|
+
} as const;
|
|
1652
|
+
`);
|
|
1653
|
+
}
|
|
1654
|
+
function generateUltramodernWorkspace(options) {
|
|
1655
|
+
const scope = toPackageScope(options.packageName);
|
|
1656
|
+
const packageSource = resolvePackageSource(options);
|
|
1657
|
+
node_fs.mkdirSync(options.targetDir, {
|
|
1658
|
+
recursive: true
|
|
1659
|
+
});
|
|
1660
|
+
copyRootTemplate(options.targetDir, {
|
|
1661
|
+
packageName: options.packageName,
|
|
1662
|
+
packageScope: scope
|
|
1663
|
+
});
|
|
1664
|
+
writeJson(options.targetDir, 'package.json', createRootPackageJson(scope, packageSource));
|
|
1665
|
+
writeJson(options.targetDir, 'tsconfig.base.json', createTsConfigBase(scope));
|
|
1666
|
+
writeJson(options.targetDir, 'topology/reference-topology.json', createTopology(scope));
|
|
1667
|
+
writeJson(options.targetDir, 'topology/ownership.json', createOwnership(scope));
|
|
1668
|
+
writeJson(options.targetDir, 'topology/local-overlays/development.json', createDevelopmentOverlay());
|
|
1669
|
+
writeJson(options.targetDir, '.modernjs/ultramodern-workspace-template-manifest.json', createTemplateManifest(options.modernVersion, packageSource));
|
|
1670
|
+
writeJson(options.targetDir, '.modernjs/ultramodern-package-source.json', createPackageSourceMetadata(scope, packageSource));
|
|
1671
|
+
writeApp(options.targetDir, scope, shellApp, packageSource);
|
|
1672
|
+
for (const remote of remoteApps)writeApp(options.targetDir, scope, remote, packageSource);
|
|
1673
|
+
writeEffectService(options.targetDir, scope, packageSource);
|
|
1674
|
+
writeSharedPackages(options.targetDir, scope);
|
|
1675
|
+
}
|
|
1676
|
+
const src_dirname = node_path.dirname(fileURLToPath(import.meta.url));
|
|
1677
|
+
const templateDir = node_path.resolve(src_dirname, '..', 'template');
|
|
1678
|
+
const semverPattern = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
1679
|
+
const semverTagPattern = /^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?$/;
|
|
1680
|
+
const sha1Pattern = /^[0-9a-f]{40}$/;
|
|
1681
|
+
const sha256Pattern = /^[0-9a-f]{64}$/;
|
|
1682
|
+
const templateIdPattern = /^[a-z0-9][a-z0-9._-]*$/;
|
|
1683
|
+
const packageNamePattern = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
|
|
1684
|
+
const requiredDeniedPaths = [
|
|
1685
|
+
'.git/**',
|
|
1686
|
+
'.github/**',
|
|
1687
|
+
'.npmrc',
|
|
1688
|
+
'.yarnrc',
|
|
1689
|
+
'.env',
|
|
1690
|
+
'.env.*',
|
|
1691
|
+
'node_modules/**',
|
|
1692
|
+
'dist/**'
|
|
1693
|
+
];
|
|
1694
|
+
const requiredLifecycleDeniedScripts = [
|
|
1695
|
+
'preinstall',
|
|
1696
|
+
'install',
|
|
1697
|
+
'postinstall',
|
|
1698
|
+
'prepare'
|
|
1699
|
+
];
|
|
1700
|
+
function getOptionValue(args, names) {
|
|
1701
|
+
for (const name of names){
|
|
1702
|
+
const prefix = `${name}=`;
|
|
1703
|
+
const byEquals = args.find((arg)=>arg.startsWith(prefix));
|
|
1704
|
+
if (byEquals) return byEquals.slice(prefix.length);
|
|
1705
|
+
const index = args.findIndex((arg)=>arg === name);
|
|
1706
|
+
if (-1 !== index && args[index + 1] && !args[index + 1].startsWith('-')) return args[index + 1];
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
const detectLanguage = ()=>{
|
|
1710
|
+
const lang = getOptionValue(process.argv.slice(2), [
|
|
1711
|
+
'--lang',
|
|
1712
|
+
'-l'
|
|
1713
|
+
]);
|
|
1714
|
+
if (lang) return 'zh' === lang ? 'zh' : 'en';
|
|
1715
|
+
const detectedLang = getLocaleLanguage();
|
|
1716
|
+
if ('zh' === detectedLang) return 'zh';
|
|
1717
|
+
return 'en';
|
|
1718
|
+
};
|
|
1719
|
+
i18n.changeLanguage({
|
|
1720
|
+
locale: detectLanguage()
|
|
1721
|
+
});
|
|
1722
|
+
function detectRouterFramework() {
|
|
1723
|
+
const args = process.argv.slice(2);
|
|
1724
|
+
if (args.includes('--tanstack')) return 'tanstack';
|
|
1725
|
+
const routerValue = getOptionValue(args, [
|
|
1726
|
+
'--router',
|
|
1727
|
+
'-r'
|
|
1728
|
+
]);
|
|
1729
|
+
if (!routerValue || 'react-router' === routerValue) return 'react-router';
|
|
1730
|
+
if ('tanstack' === routerValue) return 'tanstack';
|
|
1731
|
+
console.error(i18n.t(localeKeys.error.invalidRouter, {
|
|
1732
|
+
router: routerValue
|
|
1733
|
+
}));
|
|
1734
|
+
process.exit(1);
|
|
1735
|
+
}
|
|
1736
|
+
function detectBffRuntime() {
|
|
1737
|
+
const args = process.argv.slice(2);
|
|
1738
|
+
const runtimeValue = getOptionValue(args, [
|
|
1739
|
+
'--bff-runtime'
|
|
1740
|
+
]);
|
|
1741
|
+
if (!runtimeValue) return args.includes('--bff') ? 'effect' : 'none';
|
|
1742
|
+
if ('hono' === runtimeValue || 'effect' === runtimeValue) return runtimeValue;
|
|
1743
|
+
console.error(i18n.t(localeKeys.error.invalidBffRuntime, {
|
|
1744
|
+
runtime: runtimeValue
|
|
1745
|
+
}));
|
|
1746
|
+
process.exit(1);
|
|
1747
|
+
}
|
|
1748
|
+
function src_renderTemplate(template, data) {
|
|
1749
|
+
const tagRegex = /\{\{(#if|#unless|\/if|\/unless)(?:\s+(\w+))?\}\}/g;
|
|
1750
|
+
function renderConditionals(startIndex, expectedClose) {
|
|
1751
|
+
let rendered = '';
|
|
1752
|
+
let cursor = startIndex;
|
|
1753
|
+
tagRegex.lastIndex = startIndex;
|
|
1754
|
+
while(true){
|
|
1755
|
+
const match = tagRegex.exec(template);
|
|
1756
|
+
if (!match) return {
|
|
1757
|
+
rendered: rendered + template.slice(cursor),
|
|
1758
|
+
nextIndex: template.length
|
|
1759
|
+
};
|
|
1760
|
+
const [raw, tag, condition] = match;
|
|
1761
|
+
const tagIndex = match.index;
|
|
1762
|
+
rendered += template.slice(cursor, tagIndex);
|
|
1763
|
+
cursor = tagIndex + raw.length;
|
|
1764
|
+
if ('#if' === tag || '#unless' === tag) {
|
|
1765
|
+
const kind = '#if' === tag ? 'if' : 'unless';
|
|
1766
|
+
const innerResult = renderConditionals(cursor, kind);
|
|
1767
|
+
cursor = innerResult.nextIndex;
|
|
1768
|
+
tagRegex.lastIndex = cursor;
|
|
1769
|
+
const conditionValue = Boolean(data[condition ?? '']);
|
|
1770
|
+
const shouldInclude = 'if' === kind ? conditionValue : !conditionValue;
|
|
1771
|
+
if (shouldInclude) rendered += innerResult.rendered;
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
if ('/if' === tag || '/unless' === tag) {
|
|
1775
|
+
const kind = '/if' === tag ? 'if' : 'unless';
|
|
1776
|
+
if (expectedClose === kind) return {
|
|
1777
|
+
rendered,
|
|
1778
|
+
nextIndex: cursor
|
|
1779
|
+
};
|
|
1780
|
+
rendered += raw;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
let result = renderConditionals(0).rendered;
|
|
1785
|
+
const varRegex = /\{\{(\w+)\}\}/g;
|
|
1786
|
+
result = result.replace(varRegex, (match, key)=>{
|
|
1787
|
+
const value = data[key];
|
|
1788
|
+
return null != value ? String(value) : match;
|
|
1789
|
+
});
|
|
1790
|
+
return result;
|
|
1791
|
+
}
|
|
1792
|
+
function normalizePathForManifest(filePath) {
|
|
1793
|
+
return filePath.split(node_path.sep).join('/');
|
|
1794
|
+
}
|
|
1795
|
+
function isUnsafeRelativePath(filePath) {
|
|
1796
|
+
return 0 === filePath.length || node_path.isAbsolute(filePath) || filePath.startsWith('/') || /^[A-Za-z]:[\\/]/.test(filePath) || filePath.split(/[\\/]+/).includes('..');
|
|
1797
|
+
}
|
|
1798
|
+
function src_hashFile(filePath) {
|
|
1799
|
+
return node_crypto.createHash('sha256').update(node_fs.readFileSync(filePath)).digest('hex');
|
|
1800
|
+
}
|
|
1801
|
+
function getTemplateFiles(dir) {
|
|
1802
|
+
const files = [];
|
|
1803
|
+
function collect(currentDir) {
|
|
1804
|
+
const entries = node_fs.readdirSync(currentDir, {
|
|
1805
|
+
withFileTypes: true
|
|
1806
|
+
}).sort((a, b)=>a.name.localeCompare(b.name));
|
|
1807
|
+
for (const entry of entries){
|
|
1808
|
+
const entryPath = node_path.join(currentDir, entry.name);
|
|
1809
|
+
if (entry.isDirectory()) collect(entryPath);
|
|
1810
|
+
else if (entry.isFile()) files.push(normalizePathForManifest(node_path.relative(dir, entryPath)));
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
collect(dir);
|
|
1814
|
+
return files;
|
|
1815
|
+
}
|
|
1816
|
+
function src_hashTemplateTree(dir) {
|
|
1817
|
+
const hash = node_crypto.createHash('sha256');
|
|
1818
|
+
for (const relativePath of getTemplateFiles(dir)){
|
|
1819
|
+
const fileHash = src_hashFile(node_path.join(dir, relativePath));
|
|
1820
|
+
hash.update(relativePath);
|
|
1821
|
+
hash.update('\0');
|
|
1822
|
+
hash.update(fileHash);
|
|
1823
|
+
hash.update('\0');
|
|
1824
|
+
}
|
|
1825
|
+
return hash.digest('hex');
|
|
1826
|
+
}
|
|
1827
|
+
function createBuiltinTemplateManifest(version) {
|
|
1828
|
+
return {
|
|
1829
|
+
schemaVersion: 1,
|
|
1830
|
+
template: {
|
|
1831
|
+
id: 'modernjs-ultramodern-app',
|
|
1832
|
+
version,
|
|
1833
|
+
displayName: 'Modern.js Ultramodern App',
|
|
1834
|
+
description: 'Repository-owned Modern.js application scaffold with UltraModern preset defaults.',
|
|
1835
|
+
compatibilityLane: 'ultramodern-mv',
|
|
1836
|
+
minimumModernVersion: version
|
|
1837
|
+
},
|
|
1838
|
+
source: {
|
|
1839
|
+
type: 'builtin',
|
|
1840
|
+
name: 'modernjs-ultramodern-app',
|
|
1841
|
+
repositoryPath: 'packages/toolkit/create/template'
|
|
1842
|
+
},
|
|
1843
|
+
integrity: {
|
|
1844
|
+
checksums: [
|
|
1845
|
+
{
|
|
1846
|
+
algorithm: 'sha256',
|
|
1847
|
+
value: src_hashTemplateTree(templateDir),
|
|
1848
|
+
scope: 'source-tree'
|
|
1849
|
+
}
|
|
1850
|
+
],
|
|
1851
|
+
provenance: {
|
|
1852
|
+
kind: 'repo-local',
|
|
1853
|
+
issuer: '@modern-js/create',
|
|
1854
|
+
subject: 'packages/toolkit/create/template'
|
|
1855
|
+
}
|
|
1856
|
+
},
|
|
1857
|
+
materialization: {
|
|
1858
|
+
targetRoot: 'generated-project-root',
|
|
1859
|
+
allowedPaths: [
|
|
1860
|
+
'.browserslistrc',
|
|
1861
|
+
'.gitignore',
|
|
1862
|
+
'.modernjs/**',
|
|
1863
|
+
'.nvmrc',
|
|
1864
|
+
'README.md',
|
|
1865
|
+
'api/**',
|
|
1866
|
+
'biome.json',
|
|
1867
|
+
'modern.config.ts',
|
|
1868
|
+
'package.json',
|
|
1869
|
+
'postcss.config.mjs',
|
|
1870
|
+
"scripts/**",
|
|
1871
|
+
'shared/**',
|
|
1872
|
+
'src/**',
|
|
1873
|
+
'tailwind.config.ts',
|
|
1874
|
+
'tsconfig.json'
|
|
1875
|
+
],
|
|
1876
|
+
deniedPaths: requiredDeniedPaths,
|
|
1877
|
+
overwritePolicy: 'deny-existing'
|
|
1878
|
+
},
|
|
1879
|
+
lifecyclePolicy: {
|
|
1880
|
+
denyByDefault: true,
|
|
1881
|
+
deniedScripts: requiredLifecycleDeniedScripts,
|
|
1882
|
+
allowedScripts: [],
|
|
1883
|
+
requiresExplicitOptIn: true
|
|
1884
|
+
},
|
|
1885
|
+
validation: {
|
|
1886
|
+
schemaValidation: true,
|
|
1887
|
+
sourceValidation: [
|
|
1888
|
+
'source-type-supported',
|
|
1889
|
+
'checksum-verified',
|
|
1890
|
+
'provenance-present'
|
|
1891
|
+
],
|
|
1892
|
+
materializationValidation: [
|
|
1893
|
+
'path-boundary-allowlist',
|
|
1894
|
+
'path-boundary-denylist',
|
|
1895
|
+
'no-path-traversal',
|
|
1896
|
+
'no-absolute-paths',
|
|
1897
|
+
'overwrite-policy-enforced'
|
|
1898
|
+
],
|
|
1899
|
+
postMaterializationValidation: [
|
|
1900
|
+
'ultramodern-contract-check',
|
|
1901
|
+
'dependency-install-with-lifecycle-deny',
|
|
1902
|
+
'template-manifest-retained'
|
|
1903
|
+
],
|
|
1904
|
+
expectedCommands: [
|
|
1905
|
+
"pnpm install --ignore-scripts",
|
|
1906
|
+
'pnpm run ultramodern:check'
|
|
1907
|
+
]
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
function assertTemplateManifest(condition, message) {
|
|
1912
|
+
if (!condition) throw new Error(`Template manifest validation failed: ${message}`);
|
|
1913
|
+
}
|
|
1914
|
+
function assertSafeManifestPath(filePath, label) {
|
|
1915
|
+
assertTemplateManifest(!isUnsafeRelativePath(filePath), `${label} is unsafe`);
|
|
1916
|
+
}
|
|
1917
|
+
function validateTemplateSource(source) {
|
|
1918
|
+
const sourceType = source.type;
|
|
1919
|
+
assertTemplateManifest('builtin' === sourceType || 'npm' === sourceType || 'git' === sourceType || 'local' === sourceType, `unsupported source type "${source.type}"`);
|
|
1920
|
+
if ('builtin' === source.type) {
|
|
1921
|
+
assertTemplateManifest(templateIdPattern.test(source.name), 'builtin source name must be a template id');
|
|
1922
|
+
if (source.repositoryPath) assertSafeManifestPath(source.repositoryPath, 'builtin repositoryPath');
|
|
1923
|
+
}
|
|
1924
|
+
if ('npm' === source.type) {
|
|
1925
|
+
assertTemplateManifest(packageNamePattern.test(source.packageName), 'npm packageName must be exact package metadata');
|
|
1926
|
+
assertTemplateManifest(semverPattern.test(source.version), 'npm source version must be an exact semver');
|
|
1927
|
+
assertTemplateManifest(sha256Pattern.test(source.tarballSha256), 'npm source tarballSha256 must be sha256 hex');
|
|
1928
|
+
}
|
|
1929
|
+
if ('git' === source.type) {
|
|
1930
|
+
assertTemplateManifest(sha1Pattern.test(source.checkoutSha), 'git checkoutSha must pin a commit');
|
|
1931
|
+
if ('sha' === source.ref.kind) assertTemplateManifest(sha1Pattern.test(source.ref.sha), 'git sha ref must be pinned to a commit');
|
|
1932
|
+
else {
|
|
1933
|
+
assertTemplateManifest(semverTagPattern.test(source.ref.tag), 'git tag ref must be a semver tag');
|
|
1934
|
+
assertTemplateManifest(sha1Pattern.test(source.ref.tagSha), 'git tag ref must include the resolved tag sha');
|
|
1935
|
+
}
|
|
1936
|
+
if (source.subdirectory) assertSafeManifestPath(source.subdirectory, 'git subdirectory');
|
|
1937
|
+
}
|
|
1938
|
+
if ('local' === source.type) {
|
|
1939
|
+
assertSafeManifestPath(source.path, 'local source path');
|
|
1940
|
+
assertTemplateManifest(true !== source.allowOutsideWorkspace, 'local source cannot allow outside workspace materialization');
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
function validateTemplateManifest(manifest) {
|
|
1944
|
+
assertTemplateManifest(1 === manifest.schemaVersion, 'schemaVersion must be 1');
|
|
1945
|
+
assertTemplateManifest(templateIdPattern.test(manifest.template.id), 'template.id must be a template id');
|
|
1946
|
+
assertTemplateManifest(semverPattern.test(manifest.template.version), 'template.version must be exact semver');
|
|
1947
|
+
assertTemplateManifest('ultramodern-mv' === manifest.template.compatibilityLane || 'ultramodern-shell' === manifest.template.compatibilityLane || 'ultramodern-remote' === manifest.template.compatibilityLane, 'template.compatibilityLane is unsupported');
|
|
1948
|
+
if (manifest.template.minimumModernVersion) assertTemplateManifest(semverPattern.test(manifest.template.minimumModernVersion), 'template.minimumModernVersion must be exact semver');
|
|
1949
|
+
validateTemplateSource(manifest.source);
|
|
1950
|
+
assertTemplateManifest(manifest.integrity.checksums.length > 0, 'integrity.checksums must not be empty');
|
|
1951
|
+
for (const checksum of manifest.integrity.checksums){
|
|
1952
|
+
assertTemplateManifest('sha256' === checksum.algorithm, 'checksum algorithm must be sha256');
|
|
1953
|
+
assertTemplateManifest(sha256Pattern.test(checksum.value), 'checksum value must be sha256 hex');
|
|
1954
|
+
assertTemplateManifest('manifest' === checksum.scope || 'source-archive' === checksum.scope || 'source-tree' === checksum.scope || 'lockfile' === checksum.scope, 'checksum scope is unsupported');
|
|
1955
|
+
}
|
|
1956
|
+
assertTemplateManifest(manifest.integrity.provenance.kind && manifest.integrity.provenance.issuer && manifest.integrity.provenance.subject, 'provenance kind, issuer, and subject are required');
|
|
1957
|
+
if (manifest.integrity.lockfile) {
|
|
1958
|
+
assertSafeManifestPath(manifest.integrity.lockfile.path, 'lockfile path');
|
|
1959
|
+
assertTemplateManifest(sha256Pattern.test(manifest.integrity.lockfile.sha256), 'lockfile sha256 must be sha256 hex');
|
|
1960
|
+
}
|
|
1961
|
+
assertTemplateManifest('generated-project-root' === manifest.materialization.targetRoot || 'workspace-package-root' === manifest.materialization.targetRoot, 'materialization.targetRoot is unsupported');
|
|
1962
|
+
assertTemplateManifest(manifest.materialization.allowedPaths.length > 0, 'materialization.allowedPaths must not be empty');
|
|
1963
|
+
for (const allowedPath of manifest.materialization.allowedPaths)assertSafeManifestPath(allowedPath.replace(/\/\*\*$/, '/placeholder'), 'allowed path');
|
|
1964
|
+
for (const deniedPath of manifest.materialization.deniedPaths)assertSafeManifestPath(deniedPath.replace(/\/\*\*$/, '/placeholder'), 'denied path');
|
|
1965
|
+
for (const deniedPath of requiredDeniedPaths)assertTemplateManifest(manifest.materialization.deniedPaths.includes(deniedPath), `materialization.deniedPaths must include ${deniedPath}`);
|
|
1966
|
+
assertTemplateManifest(!manifest.materialization.overwritePolicy || 'deny-existing' === manifest.materialization.overwritePolicy || 'allow-generated-only' === manifest.materialization.overwritePolicy, 'materialization.overwritePolicy is unsupported');
|
|
1967
|
+
assertTemplateManifest(true === manifest.lifecyclePolicy.denyByDefault, 'lifecyclePolicy.denyByDefault must be true');
|
|
1968
|
+
for (const scriptName of requiredLifecycleDeniedScripts)assertTemplateManifest(manifest.lifecyclePolicy.deniedScripts.includes(scriptName), `lifecyclePolicy.deniedScripts must include ${scriptName}`);
|
|
1969
|
+
assertTemplateManifest(0 === manifest.lifecyclePolicy.allowedScripts.length, 'lifecyclePolicy.allowedScripts must be empty for builtin materialization');
|
|
1970
|
+
assertTemplateManifest(true === manifest.validation.schemaValidation, 'validation.schemaValidation must be true');
|
|
1971
|
+
for (const token of [
|
|
1972
|
+
'source-type-supported',
|
|
1973
|
+
'checksum-verified',
|
|
1974
|
+
'provenance-present'
|
|
1975
|
+
])assertTemplateManifest(manifest.validation.sourceValidation.includes(token), `validation.sourceValidation must include ${token}`);
|
|
1976
|
+
for (const token of [
|
|
1977
|
+
'path-boundary-allowlist',
|
|
1978
|
+
'path-boundary-denylist',
|
|
1979
|
+
'no-path-traversal',
|
|
1980
|
+
'no-absolute-paths',
|
|
1981
|
+
'overwrite-policy-enforced'
|
|
1982
|
+
])assertTemplateManifest(manifest.validation.materializationValidation.includes(token), `validation.materializationValidation must include ${token}`);
|
|
1983
|
+
assertTemplateManifest(manifest.validation.postMaterializationValidation.includes('template-manifest-retained'), 'validation.postMaterializationValidation must retain manifest evidence');
|
|
1984
|
+
}
|
|
1985
|
+
function matchesManifestPattern(pattern, relativePath) {
|
|
1986
|
+
if (pattern.endsWith('/**')) {
|
|
1987
|
+
const prefix = pattern.slice(0, -3);
|
|
1988
|
+
return relativePath === prefix || relativePath.startsWith(`${prefix}/`);
|
|
1989
|
+
}
|
|
1990
|
+
if (pattern.endsWith('.*')) {
|
|
1991
|
+
const prefix = pattern.slice(0, -1);
|
|
1992
|
+
return relativePath.startsWith(prefix);
|
|
1993
|
+
}
|
|
1994
|
+
return relativePath === pattern;
|
|
1995
|
+
}
|
|
1996
|
+
function canMaterializePath(manifest, relativePath) {
|
|
1997
|
+
if (isUnsafeRelativePath(relativePath)) throw new Error(`Unsafe template path rejected: ${relativePath}`);
|
|
1998
|
+
if (manifest.materialization.deniedPaths.some((pattern)=>matchesManifestPattern(pattern, relativePath))) return false;
|
|
1999
|
+
if (!manifest.materialization.allowedPaths.some((pattern)=>matchesManifestPattern(pattern, relativePath))) throw new Error(`Template path is not allowed by manifest: ${relativePath}`);
|
|
2000
|
+
return true;
|
|
2001
|
+
}
|
|
2002
|
+
function writeTemplateManifestEvidence(targetDir, manifest) {
|
|
2003
|
+
const evidencePath = node_path.join(targetDir, '.modernjs', 'mv-template-manifest.json');
|
|
2004
|
+
const evidenceRelativePath = normalizePathForManifest(node_path.relative(targetDir, evidencePath));
|
|
2005
|
+
if (!canMaterializePath(manifest, evidenceRelativePath)) throw new Error('Template manifest evidence path is denied by manifest');
|
|
2006
|
+
node_fs.mkdirSync(node_path.dirname(evidencePath), {
|
|
2007
|
+
recursive: true
|
|
2008
|
+
});
|
|
2009
|
+
node_fs.writeFileSync(evidencePath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
2010
|
+
}
|
|
2011
|
+
function showVersion() {
|
|
2012
|
+
const createPackageJson = node_path.resolve(src_dirname, '..', 'package.json');
|
|
2013
|
+
const createPackage = JSON.parse(node_fs.readFileSync(createPackageJson, 'utf-8'));
|
|
2014
|
+
const version = createPackage.version || 'unknown';
|
|
2015
|
+
console.log(i18n.t(localeKeys.version.message, {
|
|
2016
|
+
version
|
|
2017
|
+
}));
|
|
2018
|
+
process.exit(0);
|
|
2019
|
+
}
|
|
2020
|
+
function showHelp() {
|
|
2021
|
+
console.log(i18n.t(localeKeys.help.title));
|
|
2022
|
+
console.log(i18n.t(localeKeys.help.description));
|
|
2023
|
+
console.log('');
|
|
2024
|
+
console.log(i18n.t(localeKeys.help.usage));
|
|
2025
|
+
console.log(i18n.t(localeKeys.help.usageExample));
|
|
2026
|
+
console.log('');
|
|
2027
|
+
console.log(i18n.t(localeKeys.help.options));
|
|
2028
|
+
console.log(i18n.t(localeKeys.help.optionHelp));
|
|
2029
|
+
console.log(i18n.t(localeKeys.help.optionVersion));
|
|
2030
|
+
console.log(i18n.t(localeKeys.help.optionLang));
|
|
2031
|
+
console.log(i18n.t(localeKeys.help.optionRouter));
|
|
2032
|
+
if (localeKeys.help.optionBff) console.log(i18n.t(localeKeys.help.optionBff));
|
|
2033
|
+
if (localeKeys.help.optionBffRuntime) console.log(i18n.t(localeKeys.help.optionBffRuntime));
|
|
2034
|
+
if (localeKeys.help.optionTailwind) console.log(i18n.t(localeKeys.help.optionTailwind));
|
|
2035
|
+
if (localeKeys.help.optionWorkspace) console.log(i18n.t(localeKeys.help.optionWorkspace));
|
|
2036
|
+
if (localeKeys.help.optionUltramodernWorkspace) console.log(i18n.t(localeKeys.help.optionUltramodernWorkspace));
|
|
2037
|
+
if (localeKeys.help.optionUltramodernPackageSource) console.log(i18n.t(localeKeys.help.optionUltramodernPackageSource));
|
|
2038
|
+
if (localeKeys.help.optionUltramodernPackageScope) console.log(i18n.t(localeKeys.help.optionUltramodernPackageScope));
|
|
2039
|
+
if (localeKeys.help.optionUltramodernPackageNamePrefix) console.log(i18n.t(localeKeys.help.optionUltramodernPackageNamePrefix));
|
|
2040
|
+
console.log(i18n.t(localeKeys.help.optionSub));
|
|
2041
|
+
console.log('');
|
|
2042
|
+
console.log(i18n.t(localeKeys.help.examples));
|
|
2043
|
+
console.log(i18n.t(localeKeys.help.example1));
|
|
2044
|
+
console.log(i18n.t(localeKeys.help.example2));
|
|
2045
|
+
console.log(i18n.t(localeKeys.help.example3));
|
|
2046
|
+
if (localeKeys.help.example4) console.log(i18n.t(localeKeys.help.example4));
|
|
2047
|
+
if (localeKeys.help.example5) console.log(i18n.t(localeKeys.help.example5));
|
|
2048
|
+
if (localeKeys.help.example6) console.log(i18n.t(localeKeys.help.example6));
|
|
2049
|
+
if (localeKeys.help.example7) console.log(i18n.t(localeKeys.help.example7));
|
|
2050
|
+
if (localeKeys.help.example8) console.log(i18n.t(localeKeys.help.example8));
|
|
2051
|
+
if (localeKeys.help.example9) console.log(i18n.t(localeKeys.help.example9));
|
|
2052
|
+
if (localeKeys.help.example10) console.log(i18n.t(localeKeys.help.example10));
|
|
2053
|
+
console.log('');
|
|
2054
|
+
console.log(i18n.t(localeKeys.help.moreInfo));
|
|
2055
|
+
console.log('');
|
|
2056
|
+
process.exit(0);
|
|
2057
|
+
}
|
|
2058
|
+
function promptInput(question) {
|
|
2059
|
+
const rl = node_readline.createInterface({
|
|
2060
|
+
input: process.stdin,
|
|
2061
|
+
output: process.stdout
|
|
2062
|
+
});
|
|
2063
|
+
return new Promise((resolve)=>{
|
|
2064
|
+
rl.question(question, (answer)=>{
|
|
2065
|
+
rl.close();
|
|
2066
|
+
resolve(answer.trim());
|
|
2067
|
+
});
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
function detectSubprojectFlag() {
|
|
2071
|
+
const args = process.argv.slice(2);
|
|
2072
|
+
if (args.includes('--sub') || args.includes('-s')) return true;
|
|
2073
|
+
if (args.includes('--no-sub')) return false;
|
|
2074
|
+
return null;
|
|
2075
|
+
}
|
|
2076
|
+
function detectTailwindFlag() {
|
|
2077
|
+
const args = process.argv.slice(2);
|
|
2078
|
+
return args.includes('--tailwind');
|
|
2079
|
+
}
|
|
2080
|
+
function detectWorkspaceProtocolFlag() {
|
|
2081
|
+
const args = process.argv.slice(2);
|
|
2082
|
+
return args.includes('--workspace');
|
|
2083
|
+
}
|
|
2084
|
+
function detectUltramodernWorkspaceFlag() {
|
|
2085
|
+
const args = process.argv.slice(2);
|
|
2086
|
+
return args.includes(ULTRAMODERN_WORKSPACE_FLAG);
|
|
2087
|
+
}
|
|
2088
|
+
function detectUltramodernPackageSource(args, modernVersion) {
|
|
2089
|
+
const strategy = getOptionValue(args, [
|
|
2090
|
+
'--ultramodern-package-source'
|
|
2091
|
+
]) ?? 'workspace';
|
|
2092
|
+
if ('workspace' !== strategy && 'install' !== strategy) {
|
|
2093
|
+
console.error('--ultramodern-package-source must be "workspace" or "install"');
|
|
2094
|
+
process.exit(1);
|
|
2095
|
+
}
|
|
2096
|
+
return {
|
|
2097
|
+
strategy,
|
|
2098
|
+
modernPackageVersion: getOptionValue(args, [
|
|
2099
|
+
'--ultramodern-package-version'
|
|
2100
|
+
]) ?? modernVersion,
|
|
2101
|
+
registry: getOptionValue(args, [
|
|
2102
|
+
'--ultramodern-package-registry'
|
|
2103
|
+
]),
|
|
2104
|
+
aliasScope: getOptionValue(args, [
|
|
2105
|
+
'--ultramodern-package-scope'
|
|
2106
|
+
]),
|
|
2107
|
+
aliasPackageNamePrefix: getOptionValue(args, [
|
|
2108
|
+
'--ultramodern-package-name-prefix'
|
|
2109
|
+
]) ?? 'modern-js-'
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
function isDirectoryEmpty(dirPath) {
|
|
2113
|
+
if (!node_fs.existsSync(dirPath)) return false;
|
|
2114
|
+
try {
|
|
2115
|
+
const files = node_fs.readdirSync(dirPath);
|
|
2116
|
+
return 0 === files.length;
|
|
2117
|
+
} catch {
|
|
2118
|
+
return false;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
async function getProjectName() {
|
|
2122
|
+
const args = process.argv.slice(2);
|
|
2123
|
+
const optionWithValue = new Set([
|
|
2124
|
+
'--lang',
|
|
2125
|
+
'-l',
|
|
2126
|
+
'--router',
|
|
2127
|
+
'-r',
|
|
2128
|
+
'--bff-runtime',
|
|
2129
|
+
'--ultramodern-package-source',
|
|
2130
|
+
'--ultramodern-package-version',
|
|
2131
|
+
'--ultramodern-package-registry',
|
|
2132
|
+
'--ultramodern-package-scope',
|
|
2133
|
+
'--ultramodern-package-name-prefix'
|
|
2134
|
+
]);
|
|
2135
|
+
const optionWithoutValue = new Set([
|
|
2136
|
+
'--help',
|
|
2137
|
+
'-h',
|
|
2138
|
+
'--version',
|
|
2139
|
+
'-v',
|
|
2140
|
+
'--sub',
|
|
2141
|
+
'-s',
|
|
2142
|
+
'--no-sub',
|
|
2143
|
+
'--tanstack',
|
|
2144
|
+
'--bff',
|
|
2145
|
+
'--tailwind',
|
|
2146
|
+
'--workspace',
|
|
2147
|
+
ULTRAMODERN_WORKSPACE_FLAG
|
|
2148
|
+
]);
|
|
2149
|
+
const positionalArgs = [];
|
|
2150
|
+
for(let i = 0; i < args.length; i++){
|
|
2151
|
+
const arg = args[i];
|
|
2152
|
+
if (!optionWithoutValue.has(arg)) {
|
|
2153
|
+
if (optionWithValue.has(arg)) {
|
|
2154
|
+
i += 1;
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
if (!(arg.startsWith('--lang=') || arg.startsWith('--router=') || arg.startsWith('--bff-runtime=') || arg.startsWith('--ultramodern-package-source=') || arg.startsWith('--ultramodern-package-version=') || arg.startsWith('--ultramodern-package-registry=') || arg.startsWith('--ultramodern-package-scope=') || arg.startsWith('--ultramodern-package-name-prefix='))) positionalArgs.push(arg);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
const projectNameArg = positionalArgs[0];
|
|
2161
|
+
if (projectNameArg) return {
|
|
2162
|
+
name: projectNameArg,
|
|
2163
|
+
useCurrentDir: false
|
|
2164
|
+
};
|
|
2165
|
+
const currentDir = process.cwd();
|
|
2166
|
+
if (isDirectoryEmpty(currentDir)) return {
|
|
2167
|
+
name: node_path.basename(currentDir),
|
|
2168
|
+
useCurrentDir: true
|
|
2169
|
+
};
|
|
2170
|
+
const projectName = await promptInput(i18n.t(localeKeys.prompt.projectName));
|
|
2171
|
+
if (!projectName) {
|
|
2172
|
+
console.error(i18n.t(localeKeys.error.projectNameEmpty));
|
|
2173
|
+
process.exit(1);
|
|
2174
|
+
}
|
|
2175
|
+
return {
|
|
2176
|
+
name: projectName,
|
|
2177
|
+
useCurrentDir: false
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
async function main() {
|
|
2181
|
+
const args = process.argv.slice(2);
|
|
2182
|
+
if (args.includes('--help') || args.includes('-h')) return void showHelp();
|
|
2183
|
+
if (args.includes('--version') || args.includes('-v')) return void showVersion();
|
|
2184
|
+
console.log(`\n${i18n.t(localeKeys.message.welcome)}\n`);
|
|
2185
|
+
const { name: projectName, useCurrentDir } = await getProjectName();
|
|
2186
|
+
const targetDir = useCurrentDir ? process.cwd() : node_path.isAbsolute(projectName) ? projectName : node_path.resolve(process.cwd(), projectName);
|
|
2187
|
+
const generatedPackageName = useCurrentDir || node_path.isAbsolute(projectName) ? node_path.basename(targetDir) : projectName;
|
|
2188
|
+
if (node_fs.existsSync(targetDir)) {
|
|
2189
|
+
const files = node_fs.readdirSync(targetDir);
|
|
2190
|
+
if (files.length > 0) {
|
|
2191
|
+
console.error(i18n.t(localeKeys.error.directoryExists, {
|
|
2192
|
+
projectName
|
|
2193
|
+
}));
|
|
2194
|
+
process.exit(1);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
const createPackageJson = node_path.resolve(src_dirname, '..', 'package.json');
|
|
2198
|
+
const createPackage = JSON.parse(node_fs.readFileSync(createPackageJson, 'utf-8'));
|
|
2199
|
+
const version = createPackage.version || 'latest';
|
|
2200
|
+
const generateWorkspace = detectUltramodernWorkspaceFlag();
|
|
2201
|
+
if (generateWorkspace) {
|
|
2202
|
+
generateUltramodernWorkspace({
|
|
2203
|
+
targetDir,
|
|
2204
|
+
packageName: generatedPackageName,
|
|
2205
|
+
modernVersion: version,
|
|
2206
|
+
packageSource: detectUltramodernPackageSource(args, version)
|
|
2207
|
+
});
|
|
2208
|
+
const dim = '\x1b[2m\x1b[3m';
|
|
2209
|
+
const reset = '\x1b[0m';
|
|
2210
|
+
console.log(`${i18n.t(localeKeys.message.success)}\n`);
|
|
2211
|
+
console.log(i18n.t(localeKeys.message.nextSteps));
|
|
2212
|
+
if (!useCurrentDir) console.log(`${dim} ${i18n.t(localeKeys.message.step1, {
|
|
2213
|
+
projectName
|
|
2214
|
+
})}${reset}`);
|
|
2215
|
+
console.log(`${dim} ${i18n.t(localeKeys.message.step2)}${reset}`);
|
|
2216
|
+
console.log(`${dim} pnpm ultramodern:check${reset}`);
|
|
2217
|
+
console.log(`${dim} ${i18n.t(localeKeys.message.step3)}${reset}\n`);
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
const subprojectFlag = detectSubprojectFlag();
|
|
2221
|
+
const isSubproject = true === subprojectFlag;
|
|
2222
|
+
const routerFramework = detectRouterFramework();
|
|
2223
|
+
const bffRuntime = detectBffRuntime();
|
|
2224
|
+
const enableTailwind = detectTailwindFlag();
|
|
2225
|
+
const useWorkspaceProtocol = detectWorkspaceProtocolFlag();
|
|
2226
|
+
const dependencyVersion = useWorkspaceProtocol ? 'workspace:*' : version;
|
|
2227
|
+
const templateManifest = createBuiltinTemplateManifest(version);
|
|
2228
|
+
validateTemplateManifest(templateManifest);
|
|
2229
|
+
copyTemplate(templateDir, targetDir, {
|
|
2230
|
+
packageName: generatedPackageName,
|
|
2231
|
+
version: dependencyVersion,
|
|
2232
|
+
isSubproject,
|
|
2233
|
+
routerFramework,
|
|
2234
|
+
bffRuntime,
|
|
2235
|
+
enableTailwind,
|
|
2236
|
+
templateManifest
|
|
2237
|
+
});
|
|
2238
|
+
const targetPackageJson = node_path.join(targetDir, 'package.json');
|
|
2239
|
+
const packageJson = JSON.parse(node_fs.readFileSync(targetPackageJson, 'utf-8'));
|
|
2240
|
+
packageJson.name = generatedPackageName;
|
|
2241
|
+
if (isSubproject) {
|
|
2242
|
+
delete packageJson['lint-staged'];
|
|
2243
|
+
delete packageJson['simple-git-hooks'];
|
|
2244
|
+
if (packageJson.scripts) {
|
|
2245
|
+
delete packageJson.scripts.prepare;
|
|
2246
|
+
delete packageJson.scripts.lint;
|
|
2247
|
+
}
|
|
2248
|
+
if (packageJson.devDependencies) {
|
|
2249
|
+
delete packageJson.devDependencies['lint-staged'];
|
|
2250
|
+
delete packageJson.devDependencies['simple-git-hooks'];
|
|
2251
|
+
delete packageJson.devDependencies['@biomejs/biome'];
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
node_fs.writeFileSync(targetPackageJson, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
2255
|
+
writeTemplateManifestEvidence(targetDir, templateManifest);
|
|
2256
|
+
const dim = '\x1b[2m\x1b[3m';
|
|
2257
|
+
const reset = '\x1b[0m';
|
|
2258
|
+
console.log(`${i18n.t(localeKeys.message.success)}\n`);
|
|
2259
|
+
console.log(i18n.t(localeKeys.message.nextSteps));
|
|
2260
|
+
if (!useCurrentDir) console.log(`${dim} ${i18n.t(localeKeys.message.step1, {
|
|
2261
|
+
projectName
|
|
2262
|
+
})}${reset}`);
|
|
2263
|
+
console.log(`${dim} ${i18n.t(localeKeys.message.step2)}${reset}`);
|
|
2264
|
+
console.log(`${dim} ${i18n.t(localeKeys.message.step3)}${reset}\n`);
|
|
2265
|
+
}
|
|
2266
|
+
function copyTemplate(src, dest, options) {
|
|
2267
|
+
node_fs.mkdirSync(dest, {
|
|
2268
|
+
recursive: true
|
|
2269
|
+
});
|
|
2270
|
+
const excludeInSubproject = [
|
|
2271
|
+
'.gitignore.handlebars',
|
|
2272
|
+
'biome.json',
|
|
2273
|
+
'.npmrc',
|
|
2274
|
+
'.nvmrc'
|
|
2275
|
+
];
|
|
2276
|
+
function copyRecursive(srcDir, destDir) {
|
|
2277
|
+
const entries = node_fs.readdirSync(srcDir, {
|
|
2278
|
+
withFileTypes: true
|
|
2279
|
+
});
|
|
2280
|
+
for (const entry of entries){
|
|
2281
|
+
if (options.isSubproject && excludeInSubproject.includes(entry.name)) continue;
|
|
2282
|
+
const srcPath = node_path.join(srcDir, entry.name);
|
|
2283
|
+
let destPath = node_path.join(destDir, entry.name);
|
|
2284
|
+
const sourceRelativePath = normalizePathForManifest(node_path.relative(src, srcPath));
|
|
2285
|
+
const finalRelativePath = normalizePathForManifest(sourceRelativePath.replace(/\.handlebars$/, ''));
|
|
2286
|
+
if (!(!canMaterializePath(options.templateManifest, finalRelativePath) || entry.isDirectory() && options.templateManifest.materialization.deniedPaths.some((pattern)=>matchesManifestPattern(pattern, finalRelativePath)))) if (entry.isDirectory()) {
|
|
2287
|
+
node_fs.mkdirSync(destPath, {
|
|
2288
|
+
recursive: true
|
|
2289
|
+
});
|
|
2290
|
+
copyRecursive(srcPath, destPath);
|
|
2291
|
+
} else if (entry.name.endsWith('.handlebars')) {
|
|
2292
|
+
const templateContent = node_fs.readFileSync(srcPath, 'utf-8');
|
|
2293
|
+
const rendered = src_renderTemplate(templateContent, {
|
|
2294
|
+
packageName: options.packageName,
|
|
2295
|
+
version: options.version,
|
|
2296
|
+
isSubproject: options.isSubproject,
|
|
2297
|
+
isTanstackRouter: 'tanstack' === options.routerFramework,
|
|
2298
|
+
enableBff: 'none' !== options.bffRuntime,
|
|
2299
|
+
useEffectBff: 'effect' === options.bffRuntime,
|
|
2300
|
+
useHonoBff: 'hono' === options.bffRuntime,
|
|
2301
|
+
bffRuntime: options.bffRuntime,
|
|
2302
|
+
enableTailwind: options.enableTailwind,
|
|
2303
|
+
routerImportPath: 'tanstack' === options.routerFramework ? 'tanstack-router' : 'router'
|
|
2304
|
+
});
|
|
2305
|
+
if (0 === rendered.trim().length) continue;
|
|
2306
|
+
destPath = destPath.replace(/\.handlebars$/, '');
|
|
2307
|
+
if ('deny-existing' === options.templateManifest.materialization.overwritePolicy && node_fs.existsSync(destPath)) throw new Error(`Template refused to overwrite existing file: ${finalRelativePath}`);
|
|
2308
|
+
node_fs.writeFileSync(destPath, rendered, 'utf-8');
|
|
2309
|
+
} else {
|
|
2310
|
+
if ('deny-existing' === options.templateManifest.materialization.overwritePolicy && node_fs.existsSync(destPath)) throw new Error(`Template refused to overwrite existing file: ${finalRelativePath}`);
|
|
2311
|
+
node_fs.copyFileSync(srcPath, destPath);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
copyRecursive(src, dest);
|
|
2316
|
+
}
|
|
2317
|
+
main().catch((error)=>{
|
|
2318
|
+
console.error(i18n.t(localeKeys.error.createFailed), error);
|
|
2319
|
+
process.exit(1);
|
|
2320
|
+
});
|