@certe/atmos-editor 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +32 -11
- package/vite-plugin.cjs +397 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@certe/atmos-editor",
|
|
3
3
|
"description": "Browser-based Unity-style editor for the Atmos Engine — hierarchy, inspector, gizmos",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/certesolutions-cyber/atmos.git",
|
|
@@ -9,7 +9,15 @@
|
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://github.com/certesolutions-cyber/atmos",
|
|
11
11
|
"license": "GPL-3.0-or-later",
|
|
12
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"editor",
|
|
14
|
+
"game-engine",
|
|
15
|
+
"webgpu",
|
|
16
|
+
"scene-editor",
|
|
17
|
+
"gizmo",
|
|
18
|
+
"inspector",
|
|
19
|
+
"atmos"
|
|
20
|
+
],
|
|
13
21
|
"type": "module",
|
|
14
22
|
"main": "src/index.ts",
|
|
15
23
|
"types": "src/index.ts",
|
|
@@ -18,7 +26,8 @@
|
|
|
18
26
|
"./player": "./src/player-entry.ts",
|
|
19
27
|
"./vite": {
|
|
20
28
|
"types": "./vite-plugin.d.ts",
|
|
21
|
-
"import": "./vite-plugin.mjs"
|
|
29
|
+
"import": "./vite-plugin.mjs",
|
|
30
|
+
"require": "./vite-plugin.cjs"
|
|
22
31
|
}
|
|
23
32
|
},
|
|
24
33
|
"publishConfig": {
|
|
@@ -35,23 +44,35 @@
|
|
|
35
44
|
},
|
|
36
45
|
"./vite": {
|
|
37
46
|
"types": "./vite-plugin.d.ts",
|
|
38
|
-
"import": "./vite-plugin.mjs"
|
|
47
|
+
"import": "./vite-plugin.mjs",
|
|
48
|
+
"require": "./vite-plugin.cjs"
|
|
39
49
|
}
|
|
40
50
|
}
|
|
41
51
|
},
|
|
42
|
-
"files": [
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"!dist/__tests__",
|
|
55
|
+
"vite-plugin.mjs",
|
|
56
|
+
"vite-plugin.cjs",
|
|
57
|
+
"vite-plugin.d.ts",
|
|
58
|
+
"package.json",
|
|
59
|
+
"README.md",
|
|
60
|
+
"LICENCE"
|
|
61
|
+
],
|
|
43
62
|
"peerDependencies": {
|
|
44
63
|
"vite": ">=5.0.0"
|
|
45
64
|
},
|
|
46
65
|
"peerDependenciesMeta": {
|
|
47
|
-
"vite": {
|
|
66
|
+
"vite": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
48
69
|
},
|
|
49
70
|
"dependencies": {
|
|
50
|
-
"@certe/atmos-animation": "^0.
|
|
51
|
-
"@certe/atmos-assets": "^0.
|
|
52
|
-
"@certe/atmos-core": "^0.
|
|
53
|
-
"@certe/atmos-math": "^0.
|
|
54
|
-
"@certe/atmos-renderer": "^0.
|
|
71
|
+
"@certe/atmos-animation": "^0.2.0",
|
|
72
|
+
"@certe/atmos-assets": "^0.2.0",
|
|
73
|
+
"@certe/atmos-core": "^0.2.0",
|
|
74
|
+
"@certe/atmos-math": "^0.2.0",
|
|
75
|
+
"@certe/atmos-renderer": "^0.2.0",
|
|
55
76
|
"react": "^19.0.0",
|
|
56
77
|
"react-dom": "^19.0.0"
|
|
57
78
|
},
|
package/vite-plugin.cjs
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const DEFAULT_EXCLUDE = new Set([
|
|
5
|
+
'node_modules', '.git', 'dist', '.vite', '.turbo', '__pycache__',
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
function scanDirectory(dirPath, relativeTo, exclude) {
|
|
9
|
+
const entries = [];
|
|
10
|
+
let items;
|
|
11
|
+
try {
|
|
12
|
+
items = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
13
|
+
} catch {
|
|
14
|
+
return entries;
|
|
15
|
+
}
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
if (exclude.has(item.name) || item.name.startsWith('.')) continue;
|
|
18
|
+
const fullPath = path.join(dirPath, item.name);
|
|
19
|
+
const relPath = path.relative(relativeTo, fullPath).replace(/\\/g, '/');
|
|
20
|
+
if (item.isDirectory()) {
|
|
21
|
+
entries.push({
|
|
22
|
+
path: relPath,
|
|
23
|
+
name: item.name,
|
|
24
|
+
kind: 'directory',
|
|
25
|
+
extension: '',
|
|
26
|
+
children: scanDirectory(fullPath, relativeTo, exclude),
|
|
27
|
+
});
|
|
28
|
+
} else {
|
|
29
|
+
const ext = path.extname(item.name).slice(1);
|
|
30
|
+
entries.push({
|
|
31
|
+
path: relPath,
|
|
32
|
+
name: item.name,
|
|
33
|
+
kind: 'file',
|
|
34
|
+
extension: ext,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return entries.sort((a, b) => {
|
|
39
|
+
if (a.kind !== b.kind) return a.kind === 'directory' ? -1 : 1;
|
|
40
|
+
return a.name.localeCompare(b.name);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function generateEditorHtml(entry) {
|
|
45
|
+
return `<!DOCTYPE html>
|
|
46
|
+
<html lang="en">
|
|
47
|
+
<head>
|
|
48
|
+
<meta charset="UTF-8" />
|
|
49
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
50
|
+
<title>Atmos Editor</title>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<script type="module" src="/${entry}"></script>
|
|
54
|
+
</body>
|
|
55
|
+
</html>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function generatePlayerHtml(entry) {
|
|
59
|
+
return `<!DOCTYPE html>
|
|
60
|
+
<html lang="en">
|
|
61
|
+
<head>
|
|
62
|
+
<meta charset="UTF-8" />
|
|
63
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
64
|
+
<title>Atmos Game</title>
|
|
65
|
+
<style>
|
|
66
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
67
|
+
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
|
|
68
|
+
#atmos-container { position: relative; width: 100%; height: 100%; }
|
|
69
|
+
#atmos-canvas { width: 100%; height: 100%; display: block; }
|
|
70
|
+
#atmos-ui { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
|
|
71
|
+
#atmos-ui * { pointer-events: auto; }
|
|
72
|
+
</style>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<div id="atmos-container">
|
|
76
|
+
<canvas id="atmos-canvas"></canvas>
|
|
77
|
+
<div id="atmos-ui"></div>
|
|
78
|
+
</div>
|
|
79
|
+
<script type="module" src="/${entry}"></script>
|
|
80
|
+
</body>
|
|
81
|
+
</html>`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const VIRTUAL_BUILD_ENTRY = 'virtual:atmos-build-entry';
|
|
85
|
+
const RESOLVED_BUILD_ENTRY = '\0virtual:atmos-build-entry';
|
|
86
|
+
|
|
87
|
+
function copyDirSync(src, dest) {
|
|
88
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
89
|
+
for (const item of fs.readdirSync(src, { withFileTypes: true })) {
|
|
90
|
+
const srcPath = path.join(src, item.name);
|
|
91
|
+
const destPath = path.join(dest, item.name);
|
|
92
|
+
if (item.isDirectory()) {
|
|
93
|
+
copyDirSync(srcPath, destPath);
|
|
94
|
+
} else {
|
|
95
|
+
fs.copyFileSync(srcPath, destPath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Collect body data from an IncomingMessage. */
|
|
101
|
+
function collectBody(req) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const chunks = [];
|
|
104
|
+
req.on('data', (c) => chunks.push(c));
|
|
105
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
106
|
+
req.on('error', reject);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @param {import('./vite-plugin.d.ts').AtmosPluginOptions} [options] */
|
|
111
|
+
function atmosPlugin(options) {
|
|
112
|
+
const include = options?.include ?? ['src'];
|
|
113
|
+
const exclude = new Set([...DEFAULT_EXCLUDE, ...(options?.exclude ?? [])]);
|
|
114
|
+
const entry = options?.entry ?? 'src/main.ts';
|
|
115
|
+
let root = '';
|
|
116
|
+
let generatedIndex = '';
|
|
117
|
+
let isBuild = false;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
name: 'atmos-editor',
|
|
121
|
+
|
|
122
|
+
config(cfg, { command }) {
|
|
123
|
+
root = cfg.root || process.cwd();
|
|
124
|
+
isBuild = command === 'build';
|
|
125
|
+
|
|
126
|
+
if (isBuild) {
|
|
127
|
+
// In build mode, generate player HTML (not editor)
|
|
128
|
+
const indexPath = path.resolve(root, 'index.html');
|
|
129
|
+
const userHasIndex = fs.existsSync(indexPath);
|
|
130
|
+
if (!userHasIndex) {
|
|
131
|
+
fs.writeFileSync(indexPath, generatePlayerHtml(VIRTUAL_BUILD_ENTRY));
|
|
132
|
+
generatedIndex = indexPath;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
build: {
|
|
136
|
+
target: cfg.build?.target ?? 'esnext',
|
|
137
|
+
rollupOptions: {
|
|
138
|
+
...cfg.build?.rollupOptions,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Dev mode — generate editor HTML if no index.html exists
|
|
145
|
+
const indexPath = path.resolve(root, 'index.html');
|
|
146
|
+
if (!fs.existsSync(indexPath)) {
|
|
147
|
+
fs.writeFileSync(indexPath, generateEditorHtml(entry));
|
|
148
|
+
generatedIndex = indexPath;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
build: {
|
|
152
|
+
target: cfg.build?.target ?? 'esnext',
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
resolveId(id) {
|
|
158
|
+
if (id === VIRTUAL_BUILD_ENTRY || id === '/' + VIRTUAL_BUILD_ENTRY) {
|
|
159
|
+
return RESOLVED_BUILD_ENTRY;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
load(id) {
|
|
164
|
+
if (id !== RESOLVED_BUILD_ENTRY) return;
|
|
165
|
+
let sceneName = 'main';
|
|
166
|
+
try {
|
|
167
|
+
const settingsPath = path.join(root, 'project-settings.json');
|
|
168
|
+
if (fs.existsSync(settingsPath)) {
|
|
169
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
170
|
+
if (settings.defaultScene) sceneName = settings.defaultScene;
|
|
171
|
+
}
|
|
172
|
+
} catch { /* use default */ }
|
|
173
|
+
return `
|
|
174
|
+
import { startPlayer, createEditorPhysics } from '@certe/atmos-editor/player';
|
|
175
|
+
const scriptModules = import.meta.glob('/src/scripts/*.ts', { eager: true });
|
|
176
|
+
try {
|
|
177
|
+
const physics = await createEditorPhysics();
|
|
178
|
+
const app = await startPlayer({
|
|
179
|
+
scene: 'scenes/${sceneName}.scene.json',
|
|
180
|
+
physics,
|
|
181
|
+
scriptModules,
|
|
182
|
+
});
|
|
183
|
+
} catch (err) {
|
|
184
|
+
document.body.style.background = '#111';
|
|
185
|
+
document.body.style.color = '#f88';
|
|
186
|
+
document.body.style.padding = '2em';
|
|
187
|
+
document.body.style.fontFamily = 'monospace';
|
|
188
|
+
document.body.textContent = 'Atmos: ' + (err instanceof Error ? err.message : String(err));
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
transformIndexHtml: {
|
|
194
|
+
order: 'pre',
|
|
195
|
+
handler(html) {
|
|
196
|
+
if (!isBuild) return html;
|
|
197
|
+
// Replace any existing script src with the virtual build entry
|
|
198
|
+
const hasScript = /<script\s+type="module"\s+src="[^"]*"[^>]*><\/script>/.test(html);
|
|
199
|
+
if (hasScript) {
|
|
200
|
+
return html.replace(
|
|
201
|
+
/<script\s+type="module"\s+src="[^"]*"([^>]*)><\/script>/,
|
|
202
|
+
`<script type="module" src="/${VIRTUAL_BUILD_ENTRY}"$1></script>`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
// No script tag — inject one before </body>
|
|
206
|
+
return html.replace('</body>', ` <script type="module" src="/${VIRTUAL_BUILD_ENTRY}"></script>\n</body>`);
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
closeBundle() {
|
|
211
|
+
if (generatedIndex) {
|
|
212
|
+
try { fs.unlinkSync(generatedIndex); } catch { /* ignore */ }
|
|
213
|
+
generatedIndex = '';
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
writeBundle(options) {
|
|
218
|
+
if (!isBuild) return;
|
|
219
|
+
const outDir = options.dir || path.resolve(root, 'dist');
|
|
220
|
+
const assetDirs = ['scenes', 'materials', 'textures', 'models'];
|
|
221
|
+
for (const dir of assetDirs) {
|
|
222
|
+
const src = path.resolve(root, dir);
|
|
223
|
+
if (fs.existsSync(src)) {
|
|
224
|
+
copyDirSync(src, path.join(outDir, dir));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
configureServer(server) {
|
|
230
|
+
root = server.config.root;
|
|
231
|
+
|
|
232
|
+
// Auto-generate index.html when none exists
|
|
233
|
+
const indexPath = path.resolve(root, 'index.html');
|
|
234
|
+
if (!fs.existsSync(indexPath)) {
|
|
235
|
+
server.middlewares.use((req, res, next) => {
|
|
236
|
+
if (req.url === '/' || req.url === '/index.html') {
|
|
237
|
+
const rawHtml = generateEditorHtml(entry);
|
|
238
|
+
server.transformIndexHtml(req.url, rawHtml).then((html) => {
|
|
239
|
+
res.statusCode = 200;
|
|
240
|
+
res.setHeader('Content-Type', 'text/html');
|
|
241
|
+
res.end(html);
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
next();
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Serve asset tree as JSON
|
|
250
|
+
server.middlewares.use('/__atmos_assets', (_req, res) => {
|
|
251
|
+
const allEntries = [];
|
|
252
|
+
for (const dir of include) {
|
|
253
|
+
const absDir = path.resolve(root, dir);
|
|
254
|
+
if (fs.existsSync(absDir)) {
|
|
255
|
+
allEntries.push(...scanDirectory(absDir, root, exclude));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
res.setHeader('Content-Type', 'application/json');
|
|
259
|
+
res.end(JSON.stringify({ root, entries: allEntries }));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ── Project filesystem endpoints ──────────────────
|
|
263
|
+
|
|
264
|
+
server.middlewares.use((req, res, next) => {
|
|
265
|
+
if (!req.url || !req.url.startsWith('/__atmos_fs/')) { next(); return; }
|
|
266
|
+
|
|
267
|
+
const url = new URL(req.url, 'http://localhost');
|
|
268
|
+
const action = url.pathname.slice('/__atmos_fs'.length); // e.g. /read, /info
|
|
269
|
+
const filePath = url.searchParams.get('path') ?? '';
|
|
270
|
+
|
|
271
|
+
// /info and /tree don't need a file path
|
|
272
|
+
if (action === '/info') {
|
|
273
|
+
res.setHeader('Content-Type', 'application/json');
|
|
274
|
+
res.end(JSON.stringify({ name: path.basename(root), root }));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (action === '/tree') {
|
|
278
|
+
const allEntries = scanDirectory(root, root, exclude);
|
|
279
|
+
res.setHeader('Content-Type', 'application/json');
|
|
280
|
+
res.end(JSON.stringify(allEntries));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Prevent path traversal for file operations
|
|
285
|
+
const absPath = path.resolve(root, filePath);
|
|
286
|
+
if (!absPath.startsWith(root)) {
|
|
287
|
+
res.statusCode = 403;
|
|
288
|
+
res.end('Forbidden');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Handle async actions (write needs body collection)
|
|
293
|
+
handleFsAction(action, absPath, filePath, root, exclude, req, res, server).catch((err) => {
|
|
294
|
+
res.statusCode = 500;
|
|
295
|
+
res.end(String(err));
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Watch for file changes and push HMR custom events
|
|
300
|
+
server.watcher.on('all', (event, filePath) => {
|
|
301
|
+
if (event !== 'add' && event !== 'change' && event !== 'unlink') return;
|
|
302
|
+
const relPath = path.relative(root, filePath).replace(/\\/g, '/');
|
|
303
|
+
const inScope = include.some((dir) => relPath.startsWith(dir));
|
|
304
|
+
if (inScope) {
|
|
305
|
+
server.hot.send('atmos:asset-change', { kind: event, path: relPath });
|
|
306
|
+
}
|
|
307
|
+
// Also notify for project files (materials, scenes, etc.)
|
|
308
|
+
if (relPath.startsWith('materials/') || relPath.startsWith('scenes/') || relPath.startsWith('textures/')) {
|
|
309
|
+
server.hot.send('atmos:project-change', { kind, path: relPath });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function handleFsAction(action, absPath, filePath, root, exclude, req, res, server) {
|
|
317
|
+
switch (action) {
|
|
318
|
+
case '/read': {
|
|
319
|
+
const data = fs.readFileSync(absPath);
|
|
320
|
+
res.setHeader('Content-Type', 'application/octet-stream');
|
|
321
|
+
res.end(data);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
case '/read-text': {
|
|
325
|
+
const text = fs.readFileSync(absPath, 'utf-8');
|
|
326
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
327
|
+
res.end(text);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case '/write': {
|
|
331
|
+
const body = await collectBody(req);
|
|
332
|
+
const dir = path.dirname(absPath);
|
|
333
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
334
|
+
fs.writeFileSync(absPath, body);
|
|
335
|
+
res.end('ok');
|
|
336
|
+
notifyFileChange(server, root, filePath, 'change');
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
case '/delete': {
|
|
340
|
+
fs.unlinkSync(absPath);
|
|
341
|
+
res.end('ok');
|
|
342
|
+
notifyFileChange(server, root, filePath, 'unlink');
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case '/exists': {
|
|
346
|
+
res.setHeader('Content-Type', 'application/json');
|
|
347
|
+
res.end(JSON.stringify(fs.existsSync(absPath)));
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case '/mkdir': {
|
|
351
|
+
fs.mkdirSync(absPath, { recursive: true });
|
|
352
|
+
res.end('ok');
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
case '/list': {
|
|
356
|
+
const dir = filePath || '.';
|
|
357
|
+
const absDir = path.resolve(root, dir);
|
|
358
|
+
const files = listRecursive(absDir, filePath ? `${filePath}/` : '');
|
|
359
|
+
res.setHeader('Content-Type', 'application/json');
|
|
360
|
+
res.end(JSON.stringify(files));
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
default:
|
|
364
|
+
res.statusCode = 404;
|
|
365
|
+
res.end('Unknown action');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function notifyFileChange(server, root, filePath, kind) {
|
|
370
|
+
// Send HMR event immediately (don't wait for chokidar watcher)
|
|
371
|
+
server.hot.send('atmos:project-change', { kind, path: filePath });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function listRecursive(dirPath, prefix) {
|
|
375
|
+
const results = [];
|
|
376
|
+
let items;
|
|
377
|
+
try {
|
|
378
|
+
items = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
379
|
+
} catch {
|
|
380
|
+
return results;
|
|
381
|
+
}
|
|
382
|
+
for (const item of items) {
|
|
383
|
+
if (item.name.startsWith('.')) continue;
|
|
384
|
+
const full = path.join(dirPath, item.name);
|
|
385
|
+
if (item.isFile()) {
|
|
386
|
+
results.push(prefix + item.name);
|
|
387
|
+
} else if (item.isDirectory()) {
|
|
388
|
+
results.push(...listRecursive(full, `${prefix}${item.name}/`));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return results.sort();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** @deprecated Use atmosPlugin() instead */
|
|
395
|
+
const atmosAssetsPlugin = atmosPlugin;
|
|
396
|
+
|
|
397
|
+
module.exports = { atmosPlugin, atmosAssetsPlugin };
|