hightjs 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/hightjs.js +86 -50
- package/dist/builder.js +56 -3
- package/dist/client/entry.client.js +0 -1
- package/dist/loaders.js +92 -0
- package/package.json +1 -1
- package/src/bin/hightjs.js +99 -55
- package/src/builder.js +62 -3
- package/src/client/entry.client.tsx +0 -2
- package/src/loaders.js +107 -0
package/dist/bin/hightjs.js
CHANGED
|
@@ -127,18 +127,54 @@ program
|
|
|
127
127
|
.command('export')
|
|
128
128
|
.description('Exports the application as static HTML to the "exported" folder.')
|
|
129
129
|
.option('-o, --output <path>', 'Specifies the output directory', 'exported')
|
|
130
|
+
.option('--assets-dir <path>', 'Directory (inside output) where the .hight assets will be written', '.hight')
|
|
131
|
+
.option('--no-html', 'Do not generate index.html (assets only)')
|
|
130
132
|
.action(async (options) => {
|
|
131
133
|
const projectDir = process.cwd();
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
+
// Resolve output:
|
|
135
|
+
// - se vier absoluto, usa como está
|
|
136
|
+
// - se vier relativo, resolve a partir do projeto
|
|
137
|
+
const outputInput = typeof options.output === 'string' && options.output.trim().length
|
|
138
|
+
? options.output.trim()
|
|
139
|
+
: 'exported';
|
|
140
|
+
const exportDir = path.isAbsolute(outputInput)
|
|
141
|
+
? path.resolve(outputInput)
|
|
142
|
+
: path.resolve(projectDir, outputInput);
|
|
143
|
+
// Proteções: nunca permitir apagar a raiz do drive (ex: D:\) ou o root do SO (ex: C:\)
|
|
144
|
+
const exportDirResolved = path.resolve(exportDir);
|
|
145
|
+
const exportDirRoot = path.parse(exportDirResolved).root; // ex: 'D:\\'
|
|
146
|
+
const projectDirResolved = path.resolve(projectDir);
|
|
147
|
+
if (exportDirResolved === exportDirRoot) {
|
|
148
|
+
throw new Error(`Refusing to use output directory at drive root: ${exportDirResolved}`);
|
|
149
|
+
}
|
|
150
|
+
// Também evita `--output .` ou `--output ..` que cairia no diretório do projeto (perigoso)
|
|
151
|
+
// Regra: output precisa estar dentro do projeto, ou dentro de uma subpasta do projeto.
|
|
152
|
+
// (mantém comportamento esperado e evita deletar o repo inteiro por acidente)
|
|
153
|
+
const relExportToProject = path.relative(projectDirResolved, exportDirResolved);
|
|
154
|
+
if (relExportToProject === '' || relExportToProject === '.' || relExportToProject.startsWith('..')) {
|
|
155
|
+
throw new Error(`Refusing to export to ${exportDirResolved}. Use a subfolder like "exported" or an explicit path inside the project.`);
|
|
156
|
+
}
|
|
157
|
+
// assetsDir: sempre relativo ao exportDir (evita escrever fora sem querer)
|
|
158
|
+
// Permite usar '.' (raiz do output)
|
|
159
|
+
const assetsDirInputRaw = typeof options.assetsDir === 'string' ? options.assetsDir : '.hight';
|
|
160
|
+
const assetsDirInput = assetsDirInputRaw.trim().length ? assetsDirInputRaw.trim() : '.';
|
|
161
|
+
const assetsDirResolved = path.resolve(exportDirResolved, assetsDirInput);
|
|
162
|
+
const relAssetsToExport = path.relative(exportDirResolved, assetsDirResolved);
|
|
163
|
+
if (relAssetsToExport.startsWith('..') || path.isAbsolute(relAssetsToExport)) {
|
|
164
|
+
throw new Error(`Invalid --assets-dir: must be inside output directory. Received: ${assetsDirInputRaw}`);
|
|
165
|
+
}
|
|
166
|
+
// Normaliza a pasta para uso em URL no HTML
|
|
167
|
+
// Se assets-dir for '.', relAssetsToExport vira '' e assetsBaseHref vira './'
|
|
168
|
+
const assetsDirUrl = relAssetsToExport.split(path.sep).join('/').replace(/^\.?\/?/, '');
|
|
169
|
+
const assetsBaseHref = './' + (assetsDirUrl.length ? assetsDirUrl + '/' : '');
|
|
134
170
|
console.log('🚀 Starting export...\n');
|
|
135
171
|
try {
|
|
136
172
|
// 1. Cria a pasta exported (limpa se já existir)
|
|
137
|
-
if (fs.existsSync(
|
|
173
|
+
if (fs.existsSync(exportDirResolved)) {
|
|
138
174
|
console.log('🗑️ Cleaning existing export folder...');
|
|
139
|
-
fs.rmSync(
|
|
175
|
+
fs.rmSync(exportDirResolved, { recursive: true, force: true });
|
|
140
176
|
}
|
|
141
|
-
fs.mkdirSync(
|
|
177
|
+
fs.mkdirSync(exportDirResolved, { recursive: true });
|
|
142
178
|
console.log('✅ Export folder created\n');
|
|
143
179
|
// 2. Inicializa e prepara o build
|
|
144
180
|
console.log('🔨 Building application...');
|
|
@@ -147,62 +183,62 @@ program
|
|
|
147
183
|
const app = teste.default({ dev: false, port: 3000, hostname: '0.0.0.0', framework: 'native' });
|
|
148
184
|
await app.prepare();
|
|
149
185
|
console.log('✅ Build complete\n');
|
|
150
|
-
// 3. Copia a pasta .hight para
|
|
151
|
-
const distDir = path.join(
|
|
186
|
+
// 3. Copia a pasta .hight para export (respeitando --assets-dir)
|
|
187
|
+
const distDir = path.join(projectDirResolved, '.hight');
|
|
152
188
|
if (fs.existsSync(distDir)) {
|
|
153
189
|
console.log('📦 Copying JavaScript files...');
|
|
154
|
-
const exportDistDir =
|
|
155
|
-
// --- Lógica de cópia substituída ---
|
|
156
|
-
// A função copyDirRecursive agora lida com tudo (arquivos e subpastas)
|
|
190
|
+
const exportDistDir = assetsDirResolved;
|
|
157
191
|
copyDirRecursive(distDir, exportDistDir);
|
|
158
|
-
|
|
159
|
-
console.log('✅ JavaScript files copied\n');
|
|
192
|
+
console.log(`✅ JavaScript files copied to: ${path.relative(exportDirResolved, exportDistDir) || '.'}\n`);
|
|
160
193
|
}
|
|
161
|
-
// 4. Copia a pasta public se existir
|
|
162
|
-
const publicDir = path.join(
|
|
194
|
+
// 4. Copia a pasta public se existir
|
|
195
|
+
const publicDir = path.join(projectDirResolved, 'public');
|
|
163
196
|
if (fs.existsSync(publicDir)) {
|
|
164
197
|
console.log('📁 Copying public files...');
|
|
165
|
-
const exportPublicDir = path.join(
|
|
166
|
-
// --- Lógica de cópia substituída ---
|
|
167
|
-
// Reutilizamos a mesma função corrigida
|
|
198
|
+
const exportPublicDir = path.join(exportDirResolved, 'public');
|
|
168
199
|
copyDirRecursive(publicDir, exportPublicDir);
|
|
169
|
-
// --- Fim da substituição ---
|
|
170
200
|
console.log('✅ Public files copied\n');
|
|
171
201
|
}
|
|
172
|
-
// 5. Gera o index.html
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
// 5. Gera o index.html (opcional)
|
|
203
|
+
if (options.html) {
|
|
204
|
+
console.log('📝 Generating index.html...');
|
|
205
|
+
// ATENÇÃO: Ajuste os caminhos destes 'requires' conforme a estrutura do seu projeto!
|
|
206
|
+
const { render } = require('../renderer');
|
|
207
|
+
const { loadRoutes, loadLayout, loadNotFound } = require('../router');
|
|
208
|
+
// Carrega as rotas para gerar o HTML
|
|
209
|
+
const userWebDir = path.join(projectDirResolved, 'src', 'web');
|
|
210
|
+
const userWebRoutesDir = path.join(userWebDir, 'routes');
|
|
211
|
+
const routes = loadRoutes(userWebRoutesDir);
|
|
212
|
+
loadLayout(userWebDir);
|
|
213
|
+
loadNotFound(userWebDir);
|
|
214
|
+
// Gera HTML para a rota raiz
|
|
215
|
+
const rootRoute = routes.find(r => r.pattern === '/') || routes[0];
|
|
216
|
+
if (rootRoute) {
|
|
217
|
+
const mockReq = {
|
|
218
|
+
url: '/',
|
|
219
|
+
method: 'GET',
|
|
220
|
+
headers: { host: 'localhost' },
|
|
221
|
+
hwebDev: false,
|
|
222
|
+
hotReloadManager: null
|
|
223
|
+
};
|
|
224
|
+
const html = await render({
|
|
225
|
+
req: mockReq,
|
|
226
|
+
route: rootRoute,
|
|
227
|
+
params: {},
|
|
228
|
+
allRoutes: routes
|
|
229
|
+
});
|
|
230
|
+
// Mantém o padrão /_hight/ mas permite trocar o destino no export
|
|
231
|
+
const scriptReplaced = html.replace(/\/_hight\//g, assetsBaseHref);
|
|
232
|
+
const indexPath = path.join(exportDirResolved, 'index.html');
|
|
233
|
+
fs.writeFileSync(indexPath, scriptReplaced, 'utf8');
|
|
234
|
+
console.log('✅ index.html generated\n');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
console.log('⏭️ Skipping index.html generation (--no-html)\n');
|
|
203
239
|
}
|
|
204
240
|
console.log('🎉 Export completed successfully!');
|
|
205
|
-
console.log(`📂 Files exported to: ${
|
|
241
|
+
console.log(`📂 Files exported to: ${exportDirResolved}\n`);
|
|
206
242
|
}
|
|
207
243
|
catch (error) {
|
|
208
244
|
// Logar o erro completo (com stack trace) é mais útil
|
package/dist/builder.js
CHANGED
|
@@ -143,6 +143,58 @@ const npmDependenciesPlugin = {
|
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
145
|
};
|
|
146
|
+
/**
|
|
147
|
+
* Plugin para resolver aliases do tsconfig.json (como @/)
|
|
148
|
+
*/
|
|
149
|
+
const aliasResolvePlugin = {
|
|
150
|
+
name: 'alias-resolve',
|
|
151
|
+
setup(build) {
|
|
152
|
+
const projectDir = process.cwd();
|
|
153
|
+
const tsconfigPath = path.join(projectDir, 'tsconfig.json');
|
|
154
|
+
let aliases = {};
|
|
155
|
+
// Lê o tsconfig.json para pegar os paths
|
|
156
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
157
|
+
try {
|
|
158
|
+
const tsconfigContent = fs.readFileSync(tsconfigPath, 'utf8');
|
|
159
|
+
// Remove comentários do JSON (tsconfig pode ter comentários)
|
|
160
|
+
const jsonContent = tsconfigContent.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
|
|
161
|
+
const tsconfig = JSON.parse(jsonContent);
|
|
162
|
+
if (tsconfig.compilerOptions?.paths) {
|
|
163
|
+
const baseUrl = tsconfig.compilerOptions.baseUrl || '.';
|
|
164
|
+
const basePath = path.resolve(projectDir, baseUrl);
|
|
165
|
+
// Converte os paths do tsconfig para aliases
|
|
166
|
+
for (const [alias, paths] of Object.entries(tsconfig.compilerOptions.paths)) {
|
|
167
|
+
if (Array.isArray(paths) && paths.length > 0) {
|
|
168
|
+
// Remove o /* do final do alias e do path
|
|
169
|
+
const cleanAlias = alias.replace(/\/\*$/, '');
|
|
170
|
+
const cleanPath = paths[0].replace(/\/\*$/, '');
|
|
171
|
+
aliases[cleanAlias] = path.resolve(basePath, cleanPath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
Console.warn('Error reading tsconfig.json for aliases:', error.message);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Se tem aliases configurados, resolve eles
|
|
181
|
+
if (Object.keys(aliases).length > 0) {
|
|
182
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
183
|
+
// Verifica se o import começa com algum alias
|
|
184
|
+
for (const [alias, aliasPath] of Object.entries(aliases)) {
|
|
185
|
+
if (args.path === alias || args.path.startsWith(alias + '/')) {
|
|
186
|
+
const relativePath = args.path.slice(alias.length);
|
|
187
|
+
const resolvedPath = path.join(aliasPath, relativePath);
|
|
188
|
+
return {
|
|
189
|
+
path: resolvedPath
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
146
198
|
/**
|
|
147
199
|
* Plugin para garantir que React seja corretamente resolvido
|
|
148
200
|
*/
|
|
@@ -368,7 +420,7 @@ async function buildWithChunks(entryPoint, outdir, isProduction = false) {
|
|
|
368
420
|
outdir: outdir,
|
|
369
421
|
loader: { '.js': 'js', '.ts': 'tsx' },
|
|
370
422
|
external: nodeBuiltIns,
|
|
371
|
-
plugins: [postcssPlugin, npmDependenciesPlugin, reactResolvePlugin, markdownPlugin, assetsPlugin],
|
|
423
|
+
plugins: [postcssPlugin, npmDependenciesPlugin, aliasResolvePlugin, reactResolvePlugin, markdownPlugin, assetsPlugin],
|
|
372
424
|
format: 'esm', // ESM suporta melhor o code splitting
|
|
373
425
|
jsx: 'automatic',
|
|
374
426
|
define: {
|
|
@@ -429,7 +481,7 @@ async function watchWithChunks(entryPoint, outdir, hotReloadManager = null) {
|
|
|
429
481
|
outdir: outdir,
|
|
430
482
|
loader: { '.js': 'js', '.ts': 'tsx' },
|
|
431
483
|
external: nodeBuiltIns,
|
|
432
|
-
plugins: [postcssPlugin, npmDependenciesPlugin, reactResolvePlugin, hmrPlugin, buildCompletePlugin, markdownPlugin, assetsPlugin],
|
|
484
|
+
plugins: [postcssPlugin, npmDependenciesPlugin, aliasResolvePlugin, reactResolvePlugin, hmrPlugin, buildCompletePlugin, markdownPlugin, assetsPlugin],
|
|
433
485
|
format: 'esm',
|
|
434
486
|
jsx: 'automatic',
|
|
435
487
|
define: {
|
|
@@ -473,7 +525,7 @@ async function build(entryPoint, outfile, isProduction = false) {
|
|
|
473
525
|
outfile: outfile,
|
|
474
526
|
loader: { '.js': 'js', '.ts': 'tsx' },
|
|
475
527
|
external: nodeBuiltIns,
|
|
476
|
-
plugins: [postcssPlugin, npmDependenciesPlugin, reactResolvePlugin, markdownPlugin, assetsPlugin],
|
|
528
|
+
plugins: [postcssPlugin, npmDependenciesPlugin, aliasResolvePlugin, reactResolvePlugin, markdownPlugin, assetsPlugin],
|
|
477
529
|
format: 'iife',
|
|
478
530
|
globalName: 'HwebApp',
|
|
479
531
|
jsx: 'automatic',
|
|
@@ -535,6 +587,7 @@ async function watch(entryPoint, outfile, hotReloadManager = null) {
|
|
|
535
587
|
outfile: outfile,
|
|
536
588
|
loader: { '.js': 'js', '.ts': 'tsx' },
|
|
537
589
|
external: nodeBuiltIns,
|
|
590
|
+
plugins: [postcssPlugin, npmDependenciesPlugin, aliasResolvePlugin, reactResolvePlugin, hmrPlugin, buildCompletePlugin, markdownPlugin, assetsPlugin],
|
|
538
591
|
format: 'iife',
|
|
539
592
|
globalName: 'HwebApp',
|
|
540
593
|
jsx: 'automatic',
|
|
@@ -160,7 +160,6 @@ function App({ componentMap, routes, initialComponentPath, initialParams, layout
|
|
|
160
160
|
if (match) {
|
|
161
161
|
setCurrentPageComponent(() => componentMap[match.componentPath]);
|
|
162
162
|
setParams(match.params);
|
|
163
|
-
// setar o titulo da página se necessário
|
|
164
163
|
}
|
|
165
164
|
else {
|
|
166
165
|
// Se não encontrou rota, define como null para mostrar 404
|
package/dist/loaders.js
CHANGED
|
@@ -16,11 +16,103 @@
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const Module = require('module');
|
|
21
|
+
/**
|
|
22
|
+
* Carrega e processa o tsconfig.json para obter os aliases
|
|
23
|
+
*/
|
|
24
|
+
function loadTsConfigAliases() {
|
|
25
|
+
const projectDir = process.cwd();
|
|
26
|
+
const tsconfigPath = path.join(projectDir, 'tsconfig.json');
|
|
27
|
+
const aliases = {};
|
|
28
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
29
|
+
try {
|
|
30
|
+
const tsconfigContent = fs.readFileSync(tsconfigPath, 'utf8');
|
|
31
|
+
// Remove comentários JSONC de forma mais robusta
|
|
32
|
+
let jsonContent = tsconfigContent
|
|
33
|
+
// Remove comentários de linha // (mas não em strings)
|
|
34
|
+
.replace(/(?:^|\s)\/\/.*$/gm, '')
|
|
35
|
+
// Remove comentários de bloco /* */
|
|
36
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
37
|
+
// Remove vírgulas finais (trailing commas) que JSON não aceita
|
|
38
|
+
.replace(/,(\s*[}\]])/g, '$1');
|
|
39
|
+
const tsconfig = JSON.parse(jsonContent);
|
|
40
|
+
if (tsconfig.compilerOptions?.paths) {
|
|
41
|
+
const baseUrl = tsconfig.compilerOptions.baseUrl || '.';
|
|
42
|
+
const basePath = path.resolve(projectDir, baseUrl);
|
|
43
|
+
// Converte os paths do tsconfig para aliases
|
|
44
|
+
for (const [alias, paths] of Object.entries(tsconfig.compilerOptions.paths)) {
|
|
45
|
+
if (Array.isArray(paths) && paths.length > 0) {
|
|
46
|
+
// Remove o /* do final
|
|
47
|
+
const cleanAlias = alias.replace(/\/\*$/, '');
|
|
48
|
+
const cleanPath = paths[0].replace(/\/\*$/, '');
|
|
49
|
+
aliases[cleanAlias] = path.resolve(basePath, cleanPath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
// Silenciosamente ignora erros de parsing do tsconfig
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return aliases;
|
|
59
|
+
}
|
|
19
60
|
/**
|
|
20
61
|
* Registra loaders customizados para Node.js
|
|
21
62
|
* Permite importar arquivos não-JS diretamente no servidor
|
|
22
63
|
*/
|
|
23
64
|
function registerLoaders() {
|
|
65
|
+
// Registra resolver de aliases do tsconfig.json
|
|
66
|
+
const aliases = loadTsConfigAliases();
|
|
67
|
+
if (Object.keys(aliases).length > 0) {
|
|
68
|
+
// Guarda referência ao _resolveFilename atual (pode já ter sido modificado por source-map-support)
|
|
69
|
+
const originalResolveFilename = Module._resolveFilename;
|
|
70
|
+
Module._resolveFilename = function (request, parent, isMain, options) {
|
|
71
|
+
// Verifica se o request começa com algum alias
|
|
72
|
+
for (const [alias, aliasPath] of Object.entries(aliases)) {
|
|
73
|
+
if (request === alias || request.startsWith(alias + '/')) {
|
|
74
|
+
const relativePath = request.slice(alias.length);
|
|
75
|
+
const resolvedPath = path.join(aliasPath, relativePath);
|
|
76
|
+
// Tenta resolver com diferentes extensões
|
|
77
|
+
const extensions = ['.tsx', '.ts', '.jsx', '.js'];
|
|
78
|
+
// Primeiro tenta o caminho exato
|
|
79
|
+
if (fs.existsSync(resolvedPath)) {
|
|
80
|
+
const stats = fs.statSync(resolvedPath);
|
|
81
|
+
if (stats.isFile()) {
|
|
82
|
+
// Retorna o caminho resolvido diretamente
|
|
83
|
+
request = resolvedPath;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Tenta com extensões
|
|
88
|
+
let resolved = false;
|
|
89
|
+
for (const ext of extensions) {
|
|
90
|
+
const pathWithExt = resolvedPath + ext;
|
|
91
|
+
if (fs.existsSync(pathWithExt)) {
|
|
92
|
+
request = pathWithExt;
|
|
93
|
+
resolved = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (resolved)
|
|
98
|
+
break;
|
|
99
|
+
// Tenta como diretório com index
|
|
100
|
+
for (const ext of extensions) {
|
|
101
|
+
const indexPath = path.join(resolvedPath, 'index' + ext);
|
|
102
|
+
if (fs.existsSync(indexPath)) {
|
|
103
|
+
request = indexPath;
|
|
104
|
+
resolved = true;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (resolved)
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Chama o resolver original (que pode ser do source-map-support)
|
|
113
|
+
return originalResolveFilename.call(this, request, parent, isMain, options);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
24
116
|
// Loader para arquivos Markdown (.md)
|
|
25
117
|
require.extensions['.md'] = function (module, filename) {
|
|
26
118
|
const content = fs.readFileSync(filename, 'utf8');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hightjs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "HightJS is a high-level framework for building web applications with ease and speed. It provides a robust set of tools and features to streamline development and enhance productivity.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/src/bin/hightjs.js
CHANGED
|
@@ -148,20 +148,64 @@ program
|
|
|
148
148
|
.command('export')
|
|
149
149
|
.description('Exports the application as static HTML to the "exported" folder.')
|
|
150
150
|
.option('-o, --output <path>', 'Specifies the output directory', 'exported')
|
|
151
|
+
.option('--assets-dir <path>', 'Directory (inside output) where the .hight assets will be written', '.hight')
|
|
152
|
+
.option('--no-html', 'Do not generate index.html (assets only)')
|
|
151
153
|
.action(async (options) => {
|
|
152
154
|
const projectDir = process.cwd();
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
|
|
156
|
+
// Resolve output:
|
|
157
|
+
// - se vier absoluto, usa como está
|
|
158
|
+
// - se vier relativo, resolve a partir do projeto
|
|
159
|
+
const outputInput = typeof options.output === 'string' && options.output.trim().length
|
|
160
|
+
? options.output.trim()
|
|
161
|
+
: 'exported';
|
|
162
|
+
const exportDir = path.isAbsolute(outputInput)
|
|
163
|
+
? path.resolve(outputInput)
|
|
164
|
+
: path.resolve(projectDir, outputInput);
|
|
165
|
+
|
|
166
|
+
// Proteções: nunca permitir apagar a raiz do drive (ex: D:\) ou o root do SO (ex: C:\)
|
|
167
|
+
const exportDirResolved = path.resolve(exportDir);
|
|
168
|
+
const exportDirRoot = path.parse(exportDirResolved).root; // ex: 'D:\\'
|
|
169
|
+
const projectDirResolved = path.resolve(projectDir);
|
|
170
|
+
|
|
171
|
+
if (exportDirResolved === exportDirRoot) {
|
|
172
|
+
throw new Error(`Refusing to use output directory at drive root: ${exportDirResolved}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Também evita `--output .` ou `--output ..` que cairia no diretório do projeto (perigoso)
|
|
176
|
+
// Regra: output precisa estar dentro do projeto, ou dentro de uma subpasta do projeto.
|
|
177
|
+
// (mantém comportamento esperado e evita deletar o repo inteiro por acidente)
|
|
178
|
+
const relExportToProject = path.relative(projectDirResolved, exportDirResolved);
|
|
179
|
+
if (relExportToProject === '' || relExportToProject === '.' || relExportToProject.startsWith('..')) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Refusing to export to ${exportDirResolved}. Use a subfolder like "exported" or an explicit path inside the project.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// assetsDir: sempre relativo ao exportDir (evita escrever fora sem querer)
|
|
186
|
+
// Permite usar '.' (raiz do output)
|
|
187
|
+
const assetsDirInputRaw = typeof options.assetsDir === 'string' ? options.assetsDir : '.hight';
|
|
188
|
+
const assetsDirInput = assetsDirInputRaw.trim().length ? assetsDirInputRaw.trim() : '.';
|
|
189
|
+
const assetsDirResolved = path.resolve(exportDirResolved, assetsDirInput);
|
|
190
|
+
const relAssetsToExport = path.relative(exportDirResolved, assetsDirResolved);
|
|
191
|
+
if (relAssetsToExport.startsWith('..') || path.isAbsolute(relAssetsToExport)) {
|
|
192
|
+
throw new Error(`Invalid --assets-dir: must be inside output directory. Received: ${assetsDirInputRaw}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Normaliza a pasta para uso em URL no HTML
|
|
196
|
+
// Se assets-dir for '.', relAssetsToExport vira '' e assetsBaseHref vira './'
|
|
197
|
+
const assetsDirUrl = relAssetsToExport.split(path.sep).join('/').replace(/^\.?\/?/, '');
|
|
198
|
+
const assetsBaseHref = './' + (assetsDirUrl.length ? assetsDirUrl + '/' : '');
|
|
155
199
|
|
|
156
200
|
console.log('🚀 Starting export...\n');
|
|
157
201
|
|
|
158
202
|
try {
|
|
159
203
|
// 1. Cria a pasta exported (limpa se já existir)
|
|
160
|
-
if (fs.existsSync(
|
|
204
|
+
if (fs.existsSync(exportDirResolved)) {
|
|
161
205
|
console.log('🗑️ Cleaning existing export folder...');
|
|
162
|
-
fs.rmSync(
|
|
206
|
+
fs.rmSync(exportDirResolved, { recursive: true, force: true });
|
|
163
207
|
}
|
|
164
|
-
fs.mkdirSync(
|
|
208
|
+
fs.mkdirSync(exportDirResolved, { recursive: true });
|
|
165
209
|
console.log('✅ Export folder created\n');
|
|
166
210
|
|
|
167
211
|
// 2. Inicializa e prepara o build
|
|
@@ -172,74 +216,74 @@ program
|
|
|
172
216
|
await app.prepare();
|
|
173
217
|
console.log('✅ Build complete\n');
|
|
174
218
|
|
|
175
|
-
// 3. Copia a pasta .hight para
|
|
176
|
-
const distDir = path.join(
|
|
219
|
+
// 3. Copia a pasta .hight para export (respeitando --assets-dir)
|
|
220
|
+
const distDir = path.join(projectDirResolved, '.hight');
|
|
177
221
|
if (fs.existsSync(distDir)) {
|
|
178
222
|
console.log('📦 Copying JavaScript files...');
|
|
179
|
-
const exportDistDir =
|
|
223
|
+
const exportDistDir = assetsDirResolved;
|
|
180
224
|
|
|
181
|
-
// --- Lógica de cópia substituída ---
|
|
182
|
-
// A função copyDirRecursive agora lida com tudo (arquivos e subpastas)
|
|
183
225
|
copyDirRecursive(distDir, exportDistDir);
|
|
184
|
-
// --- Fim da substituição ---
|
|
185
226
|
|
|
186
|
-
console.log(
|
|
227
|
+
console.log(`✅ JavaScript files copied to: ${path.relative(exportDirResolved, exportDistDir) || '.'}\n`);
|
|
187
228
|
}
|
|
188
229
|
|
|
189
|
-
// 4. Copia a pasta public se existir
|
|
190
|
-
const publicDir = path.join(
|
|
230
|
+
// 4. Copia a pasta public se existir
|
|
231
|
+
const publicDir = path.join(projectDirResolved, 'public');
|
|
191
232
|
if (fs.existsSync(publicDir)) {
|
|
192
233
|
console.log('📁 Copying public files...');
|
|
193
|
-
const exportPublicDir = path.join(
|
|
234
|
+
const exportPublicDir = path.join(exportDirResolved, 'public');
|
|
194
235
|
|
|
195
|
-
// --- Lógica de cópia substituída ---
|
|
196
|
-
// Reutilizamos a mesma função corrigida
|
|
197
236
|
copyDirRecursive(publicDir, exportPublicDir);
|
|
198
|
-
// --- Fim da substituição ---
|
|
199
237
|
|
|
200
238
|
console.log('✅ Public files copied\n');
|
|
201
239
|
}
|
|
202
240
|
|
|
203
|
-
// 5. Gera o index.html
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
241
|
+
// 5. Gera o index.html (opcional)
|
|
242
|
+
if (options.html) {
|
|
243
|
+
console.log('📝 Generating index.html...');
|
|
244
|
+
// ATENÇÃO: Ajuste os caminhos destes 'requires' conforme a estrutura do seu projeto!
|
|
245
|
+
const { render } = require('../renderer');
|
|
246
|
+
const { loadRoutes, loadLayout, loadNotFound } = require('../router');
|
|
247
|
+
|
|
248
|
+
// Carrega as rotas para gerar o HTML
|
|
249
|
+
const userWebDir = path.join(projectDirResolved, 'src', 'web');
|
|
250
|
+
const userWebRoutesDir = path.join(userWebDir, 'routes');
|
|
251
|
+
|
|
252
|
+
const routes = loadRoutes(userWebRoutesDir);
|
|
253
|
+
loadLayout(userWebDir);
|
|
254
|
+
loadNotFound(userWebDir);
|
|
255
|
+
|
|
256
|
+
// Gera HTML para a rota raiz
|
|
257
|
+
const rootRoute = routes.find(r => r.pattern === '/') || routes[0];
|
|
258
|
+
|
|
259
|
+
if (rootRoute) {
|
|
260
|
+
const mockReq = {
|
|
261
|
+
url: '/',
|
|
262
|
+
method: 'GET',
|
|
263
|
+
headers: { host: 'localhost' },
|
|
264
|
+
hwebDev: false,
|
|
265
|
+
hotReloadManager: null
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const html = await render({
|
|
269
|
+
req: mockReq,
|
|
270
|
+
route: rootRoute,
|
|
271
|
+
params: {},
|
|
272
|
+
allRoutes: routes
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Mantém o padrão /_hight/ mas permite trocar o destino no export
|
|
276
|
+
const scriptReplaced = html.replace(/\/_hight\//g, assetsBaseHref);
|
|
277
|
+
const indexPath = path.join(exportDirResolved, 'index.html');
|
|
278
|
+
fs.writeFileSync(indexPath, scriptReplaced, 'utf8');
|
|
279
|
+
console.log('✅ index.html generated\n');
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
console.log('⏭️ Skipping index.html generation (--no-html)\n');
|
|
239
283
|
}
|
|
240
284
|
|
|
241
285
|
console.log('🎉 Export completed successfully!');
|
|
242
|
-
console.log(`📂 Files exported to: ${
|
|
286
|
+
console.log(`📂 Files exported to: ${exportDirResolved}\n`);
|
|
243
287
|
|
|
244
288
|
} catch (error) {
|
|
245
289
|
// Logar o erro completo (com stack trace) é mais útil
|
package/src/builder.js
CHANGED
|
@@ -153,6 +153,64 @@ const npmDependenciesPlugin = {
|
|
|
153
153
|
}
|
|
154
154
|
};
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Plugin para resolver aliases do tsconfig.json (como @/)
|
|
158
|
+
*/
|
|
159
|
+
const aliasResolvePlugin = {
|
|
160
|
+
name: 'alias-resolve',
|
|
161
|
+
setup(build) {
|
|
162
|
+
const projectDir = process.cwd();
|
|
163
|
+
const tsconfigPath = path.join(projectDir, 'tsconfig.json');
|
|
164
|
+
|
|
165
|
+
let aliases = {};
|
|
166
|
+
|
|
167
|
+
// Lê o tsconfig.json para pegar os paths
|
|
168
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
169
|
+
try {
|
|
170
|
+
const tsconfigContent = fs.readFileSync(tsconfigPath, 'utf8');
|
|
171
|
+
// Remove comentários do JSON (tsconfig pode ter comentários)
|
|
172
|
+
const jsonContent = tsconfigContent.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
|
|
173
|
+
const tsconfig = JSON.parse(jsonContent);
|
|
174
|
+
|
|
175
|
+
if (tsconfig.compilerOptions?.paths) {
|
|
176
|
+
const baseUrl = tsconfig.compilerOptions.baseUrl || '.';
|
|
177
|
+
const basePath = path.resolve(projectDir, baseUrl);
|
|
178
|
+
|
|
179
|
+
// Converte os paths do tsconfig para aliases
|
|
180
|
+
for (const [alias, paths] of Object.entries(tsconfig.compilerOptions.paths)) {
|
|
181
|
+
if (Array.isArray(paths) && paths.length > 0) {
|
|
182
|
+
// Remove o /* do final do alias e do path
|
|
183
|
+
const cleanAlias = alias.replace(/\/\*$/, '');
|
|
184
|
+
const cleanPath = paths[0].replace(/\/\*$/, '');
|
|
185
|
+
aliases[cleanAlias] = path.resolve(basePath, cleanPath);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
Console.warn('Error reading tsconfig.json for aliases:', error.message);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Se tem aliases configurados, resolve eles
|
|
195
|
+
if (Object.keys(aliases).length > 0) {
|
|
196
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
197
|
+
// Verifica se o import começa com algum alias
|
|
198
|
+
for (const [alias, aliasPath] of Object.entries(aliases)) {
|
|
199
|
+
if (args.path === alias || args.path.startsWith(alias + '/')) {
|
|
200
|
+
const relativePath = args.path.slice(alias.length);
|
|
201
|
+
const resolvedPath = path.join(aliasPath, relativePath);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
path: resolvedPath
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
156
214
|
/**
|
|
157
215
|
* Plugin para garantir que React seja corretamente resolvido
|
|
158
216
|
*/
|
|
@@ -411,7 +469,7 @@ async function buildWithChunks(entryPoint, outdir, isProduction = false) {
|
|
|
411
469
|
outdir: outdir,
|
|
412
470
|
loader: { '.js': 'js', '.ts': 'tsx' },
|
|
413
471
|
external: nodeBuiltIns,
|
|
414
|
-
plugins: [postcssPlugin, npmDependenciesPlugin, reactResolvePlugin, markdownPlugin, assetsPlugin],
|
|
472
|
+
plugins: [postcssPlugin, npmDependenciesPlugin, aliasResolvePlugin, reactResolvePlugin, markdownPlugin, assetsPlugin],
|
|
415
473
|
format: 'esm', // ESM suporta melhor o code splitting
|
|
416
474
|
jsx: 'automatic',
|
|
417
475
|
define: {
|
|
@@ -473,7 +531,7 @@ async function watchWithChunks(entryPoint, outdir, hotReloadManager = null) {
|
|
|
473
531
|
outdir: outdir,
|
|
474
532
|
loader: { '.js': 'js', '.ts': 'tsx' },
|
|
475
533
|
external: nodeBuiltIns,
|
|
476
|
-
plugins: [postcssPlugin, npmDependenciesPlugin, reactResolvePlugin, hmrPlugin, buildCompletePlugin, markdownPlugin, assetsPlugin],
|
|
534
|
+
plugins: [postcssPlugin, npmDependenciesPlugin, aliasResolvePlugin, reactResolvePlugin, hmrPlugin, buildCompletePlugin, markdownPlugin, assetsPlugin],
|
|
477
535
|
format: 'esm',
|
|
478
536
|
jsx: 'automatic',
|
|
479
537
|
define: {
|
|
@@ -519,7 +577,7 @@ async function build(entryPoint, outfile, isProduction = false) {
|
|
|
519
577
|
outfile: outfile,
|
|
520
578
|
loader: { '.js': 'js', '.ts': 'tsx' },
|
|
521
579
|
external: nodeBuiltIns,
|
|
522
|
-
plugins: [postcssPlugin, npmDependenciesPlugin, reactResolvePlugin, markdownPlugin, assetsPlugin],
|
|
580
|
+
plugins: [postcssPlugin, npmDependenciesPlugin, aliasResolvePlugin, reactResolvePlugin, markdownPlugin, assetsPlugin],
|
|
523
581
|
format: 'iife',
|
|
524
582
|
globalName: 'HwebApp',
|
|
525
583
|
jsx: 'automatic',
|
|
@@ -582,6 +640,7 @@ async function watch(entryPoint, outfile, hotReloadManager = null) {
|
|
|
582
640
|
outfile: outfile,
|
|
583
641
|
loader: { '.js': 'js', '.ts': 'tsx' },
|
|
584
642
|
external: nodeBuiltIns,
|
|
643
|
+
plugins: [postcssPlugin, npmDependenciesPlugin, aliasResolvePlugin, reactResolvePlugin, hmrPlugin, buildCompletePlugin, markdownPlugin, assetsPlugin],
|
|
585
644
|
format: 'iife',
|
|
586
645
|
globalName: 'HwebApp',
|
|
587
646
|
jsx: 'automatic',
|
|
@@ -152,8 +152,6 @@ function App({ componentMap, routes, initialComponentPath, initialParams, layout
|
|
|
152
152
|
if (match) {
|
|
153
153
|
setCurrentPageComponent(() => componentMap[match.componentPath]);
|
|
154
154
|
setParams(match.params);
|
|
155
|
-
// setar o titulo da página se necessário
|
|
156
|
-
|
|
157
155
|
} else {
|
|
158
156
|
// Se não encontrou rota, define como null para mostrar 404
|
|
159
157
|
setCurrentPageComponent(null);
|
package/src/loaders.js
CHANGED
|
@@ -16,12 +16,119 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const Module = require('module');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Carrega e processa o tsconfig.json para obter os aliases
|
|
24
|
+
*/
|
|
25
|
+
function loadTsConfigAliases() {
|
|
26
|
+
const projectDir = process.cwd();
|
|
27
|
+
const tsconfigPath = path.join(projectDir, 'tsconfig.json');
|
|
28
|
+
|
|
29
|
+
const aliases = {};
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const tsconfigContent = fs.readFileSync(tsconfigPath, 'utf8');
|
|
34
|
+
|
|
35
|
+
// Remove comentários JSONC de forma mais robusta
|
|
36
|
+
let jsonContent = tsconfigContent
|
|
37
|
+
// Remove comentários de linha // (mas não em strings)
|
|
38
|
+
.replace(/(?:^|\s)\/\/.*$/gm, '')
|
|
39
|
+
// Remove comentários de bloco /* */
|
|
40
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
41
|
+
// Remove vírgulas finais (trailing commas) que JSON não aceita
|
|
42
|
+
.replace(/,(\s*[}\]])/g, '$1');
|
|
43
|
+
|
|
44
|
+
const tsconfig = JSON.parse(jsonContent);
|
|
45
|
+
|
|
46
|
+
if (tsconfig.compilerOptions?.paths) {
|
|
47
|
+
const baseUrl = tsconfig.compilerOptions.baseUrl || '.';
|
|
48
|
+
const basePath = path.resolve(projectDir, baseUrl);
|
|
49
|
+
|
|
50
|
+
// Converte os paths do tsconfig para aliases
|
|
51
|
+
for (const [alias, paths] of Object.entries(tsconfig.compilerOptions.paths)) {
|
|
52
|
+
if (Array.isArray(paths) && paths.length > 0) {
|
|
53
|
+
// Remove o /* do final
|
|
54
|
+
const cleanAlias = alias.replace(/\/\*$/, '');
|
|
55
|
+
const cleanPath = paths[0].replace(/\/\*$/, '');
|
|
56
|
+
aliases[cleanAlias] = path.resolve(basePath, cleanPath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Silenciosamente ignora erros de parsing do tsconfig
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return aliases;
|
|
66
|
+
}
|
|
19
67
|
|
|
20
68
|
/**
|
|
21
69
|
* Registra loaders customizados para Node.js
|
|
22
70
|
* Permite importar arquivos não-JS diretamente no servidor
|
|
23
71
|
*/
|
|
24
72
|
function registerLoaders() {
|
|
73
|
+
// Registra resolver de aliases do tsconfig.json
|
|
74
|
+
const aliases = loadTsConfigAliases();
|
|
75
|
+
|
|
76
|
+
if (Object.keys(aliases).length > 0) {
|
|
77
|
+
// Guarda referência ao _resolveFilename atual (pode já ter sido modificado por source-map-support)
|
|
78
|
+
const originalResolveFilename = Module._resolveFilename;
|
|
79
|
+
|
|
80
|
+
Module._resolveFilename = function (request, parent, isMain, options) {
|
|
81
|
+
// Verifica se o request começa com algum alias
|
|
82
|
+
for (const [alias, aliasPath] of Object.entries(aliases)) {
|
|
83
|
+
if (request === alias || request.startsWith(alias + '/')) {
|
|
84
|
+
const relativePath = request.slice(alias.length);
|
|
85
|
+
const resolvedPath = path.join(aliasPath, relativePath);
|
|
86
|
+
|
|
87
|
+
// Tenta resolver com diferentes extensões
|
|
88
|
+
const extensions = ['.tsx', '.ts', '.jsx', '.js'];
|
|
89
|
+
|
|
90
|
+
// Primeiro tenta o caminho exato
|
|
91
|
+
if (fs.existsSync(resolvedPath)) {
|
|
92
|
+
const stats = fs.statSync(resolvedPath);
|
|
93
|
+
if (stats.isFile()) {
|
|
94
|
+
// Retorna o caminho resolvido diretamente
|
|
95
|
+
request = resolvedPath;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Tenta com extensões
|
|
101
|
+
let resolved = false;
|
|
102
|
+
for (const ext of extensions) {
|
|
103
|
+
const pathWithExt = resolvedPath + ext;
|
|
104
|
+
if (fs.existsSync(pathWithExt)) {
|
|
105
|
+
request = pathWithExt;
|
|
106
|
+
resolved = true;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (resolved) break;
|
|
112
|
+
|
|
113
|
+
// Tenta como diretório com index
|
|
114
|
+
for (const ext of extensions) {
|
|
115
|
+
const indexPath = path.join(resolvedPath, 'index' + ext);
|
|
116
|
+
if (fs.existsSync(indexPath)) {
|
|
117
|
+
request = indexPath;
|
|
118
|
+
resolved = true;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (resolved) break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Chama o resolver original (que pode ser do source-map-support)
|
|
128
|
+
return originalResolveFilename.call(this, request, parent, isMain, options);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
25
132
|
// Loader para arquivos Markdown (.md)
|
|
26
133
|
require.extensions['.md'] = function (module, filename) {
|
|
27
134
|
const content = fs.readFileSync(filename, 'utf8');
|