@marsx-dev/launcher 0.0.4 → 0.0.8
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/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +33 -33
- package/dist/cli/migrate.js +3 -3
- package/dist/cli/start.js +3 -3
- package/dist/configuration.d.ts +9 -3
- package/dist/configuration.d.ts.map +1 -1
- package/dist/configuration.js +95 -34
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/launcher.d.ts.map +1 -1
- package/dist/launcher.js +6 -5
- package/dist/loader.js +5 -5
- package/dist/utils/compile.d.ts.map +1 -1
- package/dist/utils/compile.js +8 -7
- package/dist/utils/{utils.d.ts → fileUtils.d.ts} +1 -5
- package/dist/utils/fileUtils.d.ts.map +1 -0
- package/dist/utils/fileUtils.js +42 -0
- package/dist/utils/sfc.d.ts +24 -24
- package/dist/utils/sfc.d.ts.map +1 -1
- package/dist/utils/sfc.js +54 -98
- package/dist/utils/textUtils.d.ts +17 -0
- package/dist/utils/textUtils.d.ts.map +1 -0
- package/dist/utils/textUtils.js +105 -0
- package/dist/utils/v3.d.ts.map +1 -1
- package/dist/utils/v3.js +7 -7
- package/package.json +5 -2
- package/src/cli/init.ts +32 -31
- package/src/cli/migrate.ts +1 -1
- package/src/cli/start.ts +1 -1
- package/src/configuration.ts +86 -35
- package/src/index.ts +4 -3
- package/src/launcher.ts +3 -2
- package/src/loader.ts +1 -1
- package/src/utils/compile.ts +5 -4
- package/src/utils/{utils.ts → fileUtils.ts} +0 -18
- package/src/utils/sfc.ts +62 -85
- package/src/utils/textUtils.ts +82 -0
- package/src/utils/v3.ts +6 -7
- package/dist/utils/utils.d.ts.map +0 -1
- package/dist/utils/utils.js +0 -60
package/src/cli/init.ts
CHANGED
|
@@ -2,8 +2,9 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { spawnSync } from 'child_process';
|
|
3
3
|
import { randomBytes } from 'crypto';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { Config, CustomEnvironmentVariables } from '../configuration';
|
|
6
|
+
import { isDirectory, writeFileMakeDir } from '../utils/fileUtils';
|
|
7
|
+
import { assert } from '../utils/textUtils';
|
|
7
8
|
import { CliError } from './index';
|
|
8
9
|
|
|
9
10
|
const FASTIFY_DEPS = [
|
|
@@ -53,23 +54,17 @@ const V3_DEPS = [
|
|
|
53
54
|
|
|
54
55
|
const DEFAULT_DEPS = [...FASTIFY_DEPS, ...COMMON_DEPS, ...V3_DEPS];
|
|
55
56
|
|
|
56
|
-
const DEMO_BOOTER = `<json id='metadata'>
|
|
57
|
-
{"version": 4, "restartOnChange": true}
|
|
58
|
-
</json>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<script id='BlockFunction' lang='tsx'>
|
|
62
|
-
export default async () => {
|
|
63
|
-
console.log('MarsX loaded!')
|
|
64
|
-
};
|
|
65
|
-
</script>`;
|
|
66
|
-
|
|
67
57
|
export async function initProject(projectName: string) {
|
|
68
58
|
if (!projectName.match(/^\w+$/) || projectName !== projectName.toLowerCase()) {
|
|
69
59
|
throw new CliError(
|
|
70
60
|
`Project name "${projectName}" may contain only lower case alphanumeric characters and underscores (eg. my_project_name)`,
|
|
71
61
|
);
|
|
72
62
|
}
|
|
63
|
+
const name = {
|
|
64
|
+
snakeCase: projectName,
|
|
65
|
+
withDashes: projectName.replace('_', '-'),
|
|
66
|
+
noSep: projectName.replace('_', ''),
|
|
67
|
+
};
|
|
73
68
|
|
|
74
69
|
const projectDir = path.resolve(projectName);
|
|
75
70
|
|
|
@@ -80,7 +75,7 @@ export async function initProject(projectName: string) {
|
|
|
80
75
|
console.log(`Creating a new MarsX project in ${chalk.green(projectDir)}`);
|
|
81
76
|
|
|
82
77
|
const packageJson = {
|
|
83
|
-
name:
|
|
78
|
+
name: name.withDashes,
|
|
84
79
|
version: '0.0.0',
|
|
85
80
|
private: true,
|
|
86
81
|
scripts: {
|
|
@@ -91,24 +86,24 @@ export async function initProject(projectName: string) {
|
|
|
91
86
|
const packageJsonStr = JSON.stringify(packageJson, null, 2);
|
|
92
87
|
await writeFileMakeDir(path.join(projectDir, 'package.json'), packageJsonStr);
|
|
93
88
|
|
|
94
|
-
const config:
|
|
89
|
+
const config: Config = {
|
|
95
90
|
production: false,
|
|
96
91
|
port: 3000,
|
|
97
92
|
blocksDir: 'blocks',
|
|
98
93
|
cacheDir: '.cache',
|
|
99
94
|
mongoConn: '<CONN_STR>',
|
|
100
|
-
mongoDbName:
|
|
95
|
+
mongoDbName: name.withDashes,
|
|
101
96
|
azureStorageConnection: '<CONN_STR>',
|
|
102
|
-
azureStorageAccountName:
|
|
103
|
-
azureStorageUrl: `https://${
|
|
97
|
+
azureStorageAccountName: name.noSep,
|
|
98
|
+
azureStorageUrl: `https://${name.noSep}.blob.core.windows.net`,
|
|
104
99
|
webFilesTable: 'webFiles',
|
|
105
100
|
webRecentFilesTable: 'webRecentFiles',
|
|
106
101
|
webFilesBlob: 'web-files',
|
|
107
|
-
secret: (await randomBytes(
|
|
102
|
+
secret: (await randomBytes(32)).toString('hex'),
|
|
108
103
|
importProjects: [
|
|
109
104
|
{
|
|
110
|
-
name: '
|
|
111
|
-
url: 'https://
|
|
105
|
+
name: 'marsx-core',
|
|
106
|
+
url: 'https://core.marsx.dev',
|
|
112
107
|
api_key: '<API_KEY>',
|
|
113
108
|
git_commit_ish: 'main',
|
|
114
109
|
},
|
|
@@ -116,26 +111,32 @@ export async function initProject(projectName: string) {
|
|
|
116
111
|
};
|
|
117
112
|
|
|
118
113
|
await writeFileMakeDir(path.join(projectDir, 'config', 'default.json'), JSON.stringify(config, null, 2));
|
|
114
|
+
await writeFileMakeDir(
|
|
115
|
+
path.join(projectDir, 'config', 'custom-environment-variables.json'),
|
|
116
|
+
JSON.stringify(CustomEnvironmentVariables, null, 2),
|
|
117
|
+
);
|
|
119
118
|
await writeFileMakeDir(path.join(projectDir, '.gitignore'), 'node_modules\ndist\n.cache\n');
|
|
120
119
|
|
|
121
|
-
await writeFileMakeDir(path.join(projectDir, 'blocks', 'Booter.service.vue'), DEMO_BOOTER);
|
|
122
|
-
|
|
123
120
|
function run(...args: string[]) {
|
|
124
121
|
const cmd = args[0];
|
|
125
122
|
assert(cmd);
|
|
126
|
-
spawnSync(cmd, args.slice(1), { cwd: projectDir, stdio: 'inherit' });
|
|
123
|
+
spawnSync(cmd, args.slice(1), { cwd: projectDir, stdio: 'inherit', shell: true });
|
|
127
124
|
}
|
|
128
|
-
|
|
125
|
+
|
|
126
|
+
console.log('\nInstalling dependencies. This might take a couple of minutes.');
|
|
129
127
|
run('npm', 'i', ...DEFAULT_DEPS);
|
|
128
|
+
console.log('');
|
|
130
129
|
|
|
131
|
-
run('git', 'init');
|
|
130
|
+
run('git', 'init', '--initial-branch=main');
|
|
132
131
|
run('git', 'add', '.gitignore');
|
|
133
132
|
run('git', 'add', '-A');
|
|
134
|
-
run('git', 'commit', '-m', 'Initial commit');
|
|
135
|
-
console.log('Initialized git repository and created initial commit.');
|
|
133
|
+
run('git', 'commit', '-m', 'Initial MarsX commit');
|
|
136
134
|
|
|
137
|
-
console.log(chalk.
|
|
135
|
+
console.log(`\n${chalk.green('Success!')} Created ${projectName} at ${projectDir}`);
|
|
138
136
|
|
|
139
|
-
console.log(
|
|
140
|
-
console.log(
|
|
137
|
+
console.log(`\nNext steps:`);
|
|
138
|
+
console.log(` - Open new project directory: ${chalk.underline(`cd ${projectName}`)}`);
|
|
139
|
+
console.log(` - Contact MarsX to get config params and update them: ${chalk.underline('code config/default.json')}`);
|
|
140
|
+
console.log(` - (optionally) Migrate V3 blocks using: ${chalk.underline('npx marsx migrate')}`);
|
|
141
|
+
console.log(` - Start local server: ${chalk.underline('npm run start')}`);
|
|
141
142
|
}
|
package/src/cli/migrate.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { MongoClient } from 'mongodb';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { config } from '../configuration';
|
|
5
5
|
import { serializeSfc } from '../utils/sfc';
|
|
6
|
-
import { writeFileMakeDir } from '../utils/
|
|
6
|
+
import { writeFileMakeDir } from '../utils/fileUtils';
|
|
7
7
|
import { convertV3ToSfc, V3MongoBlock } from '../utils/v3';
|
|
8
8
|
|
|
9
9
|
export async function migrateV3ToV4() {
|
package/src/cli/start.ts
CHANGED
package/src/configuration.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
process.env['SUPPRESS_NO_CONFIG_WARNING'] = 'true';
|
|
2
2
|
import configModule from 'config';
|
|
3
|
+
import _ from 'lodash';
|
|
3
4
|
import path from 'path';
|
|
4
|
-
import
|
|
5
|
+
import * as yup from 'yup';
|
|
6
|
+
import { ValidationError } from 'yup';
|
|
5
7
|
|
|
6
8
|
export interface ImportProjectConfig {
|
|
7
9
|
name: string;
|
|
@@ -10,44 +12,93 @@ export interface ImportProjectConfig {
|
|
|
10
12
|
git_commit_ish?: string | undefined;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
export interface Config {
|
|
16
|
+
production: boolean;
|
|
17
|
+
port: number;
|
|
18
|
+
blocksDir: string;
|
|
19
|
+
cacheDir: string;
|
|
20
|
+
mongoConn: string;
|
|
21
|
+
mongoDbName: string;
|
|
22
|
+
azureStorageConnection: string;
|
|
23
|
+
azureStorageAccountName: string;
|
|
24
|
+
azureStorageUrl: string;
|
|
25
|
+
secret: string;
|
|
26
|
+
webFilesTable: string;
|
|
27
|
+
webRecentFilesTable: string;
|
|
28
|
+
webFilesBlob: string;
|
|
29
|
+
importProjects: ImportProjectConfig[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ConfigError extends Error {}
|
|
33
|
+
|
|
34
|
+
const ImportProjectSchema = yup.object({
|
|
35
|
+
name: yup.string().required(),
|
|
36
|
+
url: yup.string().url().required(),
|
|
37
|
+
api_key: yup.string().required(),
|
|
38
|
+
git_commit_ish: yup.string().optional(),
|
|
39
|
+
});
|
|
20
40
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
41
|
+
const ConfigSchema = yup.object().shape({
|
|
42
|
+
production: yup.boolean().required(),
|
|
43
|
+
port: yup.number().required().positive().integer(),
|
|
44
|
+
blocksDir: yup
|
|
45
|
+
.string()
|
|
46
|
+
.required()
|
|
47
|
+
.transform(v => path.resolve(v)),
|
|
48
|
+
cacheDir: yup
|
|
49
|
+
.string()
|
|
50
|
+
.required()
|
|
51
|
+
.transform(v => path.resolve(v)),
|
|
52
|
+
mongoConn: yup.string().required(),
|
|
53
|
+
mongoDbName: yup.string().required(),
|
|
54
|
+
azureStorageConnection: yup.string().required(),
|
|
55
|
+
azureStorageAccountName: yup.string().required(),
|
|
56
|
+
azureStorageUrl: yup.string().required(),
|
|
57
|
+
secret: yup.string().required(),
|
|
58
|
+
webFilesTable: yup.string().required(),
|
|
59
|
+
webRecentFilesTable: yup.string().required(),
|
|
60
|
+
webFilesBlob: yup.string().required(),
|
|
61
|
+
importProjects: yup.array().of(ImportProjectSchema).required(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function ErrorThrowingConfig(errorMessage: string) {
|
|
65
|
+
return new Proxy({} as never, {
|
|
66
|
+
get() {
|
|
67
|
+
throw new ConfigError(errorMessage);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
24
71
|
|
|
25
|
-
function
|
|
72
|
+
function validateConfig(): Config {
|
|
26
73
|
if (configModule.util.getConfigSources().length === 0) {
|
|
27
|
-
return
|
|
28
|
-
get() {
|
|
29
|
-
throw new Error('Config file not found, ensure you have "config/default.json" file.');
|
|
30
|
-
},
|
|
31
|
-
});
|
|
74
|
+
return ErrorThrowingConfig('Config file not found, ensure you have "config/default.json" file.');
|
|
32
75
|
}
|
|
33
76
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
};
|
|
77
|
+
const configObject = configModule.util.toObject();
|
|
78
|
+
try {
|
|
79
|
+
return ConfigSchema.validateSync(configObject, { abortEarly: false, stripUnknown: false });
|
|
80
|
+
} catch (e) {
|
|
81
|
+
if (e instanceof ValidationError) {
|
|
82
|
+
return ErrorThrowingConfig(e.errors.join('\n'));
|
|
83
|
+
} else {
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const config = validateConfig();
|
|
90
|
+
|
|
91
|
+
function getEnvVarMapping() {
|
|
92
|
+
const result: Record<string, string | { __name: string; __format: string }> = {};
|
|
93
|
+
for (const [name, field] of Object.entries(ConfigSchema.fields)) {
|
|
94
|
+
const envVar = `MARSX_${_.snakeCase(name).toUpperCase()}`;
|
|
95
|
+
if ('type' in field && (field.type === 'object' || field.type === 'array')) {
|
|
96
|
+
result[name] = { __name: envVar, __format: 'json' };
|
|
97
|
+
} else {
|
|
98
|
+
result[name] = envVar;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
50
102
|
}
|
|
51
103
|
|
|
52
|
-
export const
|
|
53
|
-
export type Configuration = typeof config;
|
|
104
|
+
export const CustomEnvironmentVariables = getEnvVarMapping();
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { config, ImportProjectConfig,
|
|
1
|
+
export { config, ImportProjectConfig, Config } from './configuration';
|
|
2
2
|
export { compileSfcSource, transpileTypescript } from './utils/compile';
|
|
3
|
-
export { parseSFC, serializeSfc,
|
|
4
|
-
export * as utils from './utils/utils';
|
|
3
|
+
export { parseSFC, serializeSfc, parseSfcPath, serializeSfcPath, SfcBlock, SfcPath, SfcSource } from './utils/sfc';
|
|
5
4
|
export { convertSfcToV3, convertV3ToSfc, V3MongoBlock } from './utils/v3';
|
|
5
|
+
export * as textUtils from './utils/textUtils';
|
|
6
|
+
export * as fileUtils from './utils/fileUtils';
|
package/src/launcher.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { config } from './configuration';
|
|
|
2
2
|
import { loadAllBlocks } from './loader';
|
|
3
3
|
import { compileSfcSource, transpileTypescript } from './utils/compile';
|
|
4
4
|
import { parseSFC, serializeSfc } from './utils/sfc';
|
|
5
|
-
import {
|
|
5
|
+
import { writeFileMakeDir } from './utils/fileUtils';
|
|
6
|
+
import { assert } from './utils/textUtils';
|
|
6
7
|
import { convertSfcToV3, convertV3ToSfc } from './utils/v3';
|
|
7
8
|
|
|
8
9
|
const LAUNCHER_UTILS = {
|
|
@@ -19,7 +20,7 @@ const LAUNCHER_UTILS = {
|
|
|
19
20
|
export async function launchBooter(booterBlockName = 'Booter') {
|
|
20
21
|
const allBlocks = await loadAllBlocks();
|
|
21
22
|
|
|
22
|
-
const booterBlocks = allBlocks.filter(b => b.
|
|
23
|
+
const booterBlocks = allBlocks.filter(b => b.path.name === booterBlockName);
|
|
23
24
|
if (booterBlocks.length === 0)
|
|
24
25
|
throw new Error(`Booter block ${booterBlockName} not found. Ensure you have it locally or it is imported.`);
|
|
25
26
|
|
package/src/loader.ts
CHANGED
|
@@ -6,7 +6,7 @@ import _ from 'lodash';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { config, ImportProjectConfig } from './configuration';
|
|
8
8
|
import { parseSFC, SfcBlock } from './utils/sfc';
|
|
9
|
-
import { isFile, listFilesRecursive, writeFileMakeDir } from './utils/
|
|
9
|
+
import { isFile, listFilesRecursive, writeFileMakeDir } from './utils/fileUtils';
|
|
10
10
|
import { convertV3ToSfc, V3MongoBlock } from './utils/v3';
|
|
11
11
|
|
|
12
12
|
async function downloadFromExternal(externalImport: ImportProjectConfig): Promise<SfcBlock[]> {
|
package/src/utils/compile.ts
CHANGED
|
@@ -3,7 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import ts from 'typescript';
|
|
4
4
|
import { config } from '../configuration';
|
|
5
5
|
import { SfcBlock } from './sfc';
|
|
6
|
-
import {
|
|
6
|
+
import { writeFileMakeDir } from './fileUtils';
|
|
7
|
+
import { assert } from './textUtils';
|
|
7
8
|
|
|
8
9
|
function genSourceMapComment(sourceMap: SourceMapPayload): string {
|
|
9
10
|
return `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(sourceMap)).toString('base64')}`;
|
|
@@ -47,10 +48,10 @@ export function transpileTypescript(
|
|
|
47
48
|
|
|
48
49
|
export async function compileSfcSource(sfcBlock: SfcBlock, sourceId: string): Promise<string> {
|
|
49
50
|
const sourceCode = sfcBlock.sources[sourceId];
|
|
50
|
-
if (!sourceCode) throw new Error(`Source code block ${sourceId} not found in ${sfcBlock.
|
|
51
|
+
if (!sourceCode) throw new Error(`Source code block ${sourceId} not found in ${sfcBlock.path.filePath}`);
|
|
51
52
|
|
|
52
|
-
const compiledFilePath = path.join(config.cacheDir, 'compiled', `${sfcBlock.
|
|
53
|
-
const originalFilePath = path.join(config.blocksDir, sfcBlock.
|
|
53
|
+
const compiledFilePath = path.join(config.cacheDir, 'compiled', `${sfcBlock.path.filePath}.${sourceId}.js`);
|
|
54
|
+
const originalFilePath = path.join(config.blocksDir, sfcBlock.path.filePath);
|
|
54
55
|
const code = transpileTypescript(sourceCode.source, { compiledFilePath, originalFilePath, lineOffset: sourceCode.lineOffset });
|
|
55
56
|
|
|
56
57
|
await writeFileMakeDir(compiledFilePath, code, 'utf-8');
|
|
@@ -5,24 +5,6 @@ import { Mode, ObjectEncodingOptions, OpenMode } from 'node:fs';
|
|
|
5
5
|
import { Stream } from 'node:stream';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
|
|
8
|
-
export function assert(value: unknown, message = 'value must be defined'): asserts value {
|
|
9
|
-
if (!value) {
|
|
10
|
-
throw new Error(message);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function escapeCloseTag(str: string, tag: string): string {
|
|
15
|
-
return str.replaceAll(`</${tag}>`, `</ ${tag}>`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function unescapeCloseTag(str: string, tag: string): string {
|
|
19
|
-
return str.replaceAll(`</ ${tag}>`, `</${tag}>`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function escapeHtmlAttr(str: string): string {
|
|
23
|
-
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
8
|
export async function listFilesRecursive(dir: string): Promise<string[]> {
|
|
27
9
|
const entries = await fs.readdir(dir);
|
|
28
10
|
|
package/src/utils/sfc.ts
CHANGED
|
@@ -1,29 +1,36 @@
|
|
|
1
|
-
import { CompilerError
|
|
2
|
-
import * as CompilerDOM from '@vue/compiler-dom';
|
|
3
|
-
import { NodeTypes } from '@vue/compiler-dom';
|
|
4
|
-
import stringify from 'json-stable-stringify';
|
|
1
|
+
import { CompilerError } from '@vue/compiler-core';
|
|
5
2
|
import JSON5 from 'json5';
|
|
6
3
|
import _ from 'lodash';
|
|
7
4
|
import path from 'path';
|
|
8
|
-
import
|
|
9
|
-
|
|
5
|
+
import { escapeCloseTag, escapeHtmlAttr, parseVueLike, stableHtmlAttributes, stablePrettyJson, unescapeCloseTag } from './textUtils';
|
|
6
|
+
|
|
7
|
+
export const METADATA_SECTION_ID = 'metadata';
|
|
8
|
+
export const MARS_SFC_EXT = 'mars';
|
|
10
9
|
|
|
11
|
-
export const METADATA = 'metadata';
|
|
12
|
-
export const SFC_EXT = 'vue';
|
|
13
10
|
const SAVE_EMPTY_SOURCES = true;
|
|
14
11
|
|
|
15
|
-
export type
|
|
12
|
+
export type SfcPath = {
|
|
16
13
|
folder: string;
|
|
17
14
|
name: string;
|
|
18
15
|
blockTypeName: string;
|
|
19
16
|
ext: string;
|
|
20
|
-
// Derived
|
|
21
|
-
fullName: string;
|
|
22
17
|
filePath: string;
|
|
23
18
|
};
|
|
24
19
|
|
|
25
|
-
export
|
|
26
|
-
|
|
20
|
+
export type SfcSource = {
|
|
21
|
+
source: string;
|
|
22
|
+
lang: keyof typeof LANG_TAG_MAP | string | undefined;
|
|
23
|
+
props?: Record<string, string | undefined>;
|
|
24
|
+
lineOffset?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type SfcBlock = { path: SfcPath } & (
|
|
28
|
+
| { metadata: Record<string, unknown>; jsons: Record<string, unknown>; sources: Record<string, SfcSource>; rawContent: null }
|
|
29
|
+
| { metadata: EmptyObject; jsons: EmptyObject; sources: EmptyObject; rawContent: Buffer }
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export function parseSfcPath(filePath: string): SfcPath {
|
|
33
|
+
// Blog/BlogPost.page.mars => {folder:"Blog", name: "BlogPost", blockTypeName: "page", ext: "mars", fullName: "Blog.BlogPost"}
|
|
27
34
|
const originalParsed = path.parse(filePath);
|
|
28
35
|
const withoutExt = originalParsed.name;
|
|
29
36
|
const ext = originalParsed.ext.slice(1);
|
|
@@ -32,19 +39,16 @@ export function parseSfcIdentity(filePath: string): SfcIdentity {
|
|
|
32
39
|
if (!parsed.name || !parsed.ext) throw new Error(`Invalid block file path: ${filePath}`);
|
|
33
40
|
const blockTypeName = parsed.ext.slice(1);
|
|
34
41
|
|
|
35
|
-
const namespace = originalParsed.dir.replace(/\//g, '.');
|
|
36
|
-
const fullName = namespace.length > 0 ? `${namespace}.${parsed.name}` : parsed.name;
|
|
37
42
|
return {
|
|
38
43
|
folder: originalParsed.dir,
|
|
39
44
|
name: parsed.name,
|
|
40
45
|
blockTypeName,
|
|
41
46
|
ext,
|
|
42
|
-
fullName,
|
|
43
47
|
filePath,
|
|
44
48
|
};
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
export function
|
|
51
|
+
export function serializeSfcPath(identity: SfcPath): string {
|
|
48
52
|
return path.join(identity.folder, `${identity.name}.${identity.blockTypeName}.${identity.ext}`);
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -67,81 +71,52 @@ const LANG_TAG_MAP = {
|
|
|
67
71
|
text: 'text',
|
|
68
72
|
};
|
|
69
73
|
|
|
70
|
-
export type SfcSource = { source: string; lang: keyof typeof LANG_TAG_MAP | string | undefined; lineOffset?: number };
|
|
71
|
-
|
|
72
|
-
export type SfcBlock = { identity: SfcIdentity } & (
|
|
73
|
-
| { metadata: Record<string, unknown>; jsons: Record<string, unknown>; sources: Record<string, SfcSource>; rawContent: null }
|
|
74
|
-
| { metadata: EmptyObject; jsons: EmptyObject; sources: EmptyObject; rawContent: Buffer }
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
function getNodeAttr(node: CompilerDOM.ElementNode, attr: string) {
|
|
78
|
-
return node.props.map(p => (p.name == attr && p.type === NodeTypes.ATTRIBUTE ? p.value?.content : undefined)).find(p => !!p);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
74
|
export function parseSFC(filePath: string, content: Buffer): SfcBlock {
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
return {
|
|
75
|
+
const sfcPath = parseSfcPath(filePath);
|
|
76
|
+
if (sfcPath.ext !== MARS_SFC_EXT) {
|
|
77
|
+
return { path: sfcPath, metadata: {}, jsons: {}, sources: {}, rawContent: content };
|
|
85
78
|
}
|
|
86
79
|
|
|
87
|
-
const errors: (CompilerError | SyntaxError)[] = [];
|
|
88
|
-
|
|
89
|
-
const ast = CompilerDOM.parse(content.toString('utf-8'), {
|
|
90
|
-
// there are no components at SFC parsing level
|
|
91
|
-
isNativeTag: () => true,
|
|
92
|
-
// preserve all whitespaces
|
|
93
|
-
isPreTag: () => true,
|
|
94
|
-
getTextMode: () => TextModes.RAWTEXT,
|
|
95
|
-
onError: e => errors.push(e),
|
|
96
|
-
});
|
|
97
|
-
|
|
98
80
|
const block: SfcBlock = {
|
|
99
|
-
|
|
81
|
+
path: parseSfcPath(filePath),
|
|
100
82
|
metadata: {},
|
|
101
83
|
jsons: {},
|
|
102
84
|
sources: {},
|
|
103
85
|
rawContent: null,
|
|
104
86
|
};
|
|
105
87
|
const seenIds = new Set<string>();
|
|
88
|
+
const errors: (CompilerError | SyntaxError)[] = [];
|
|
106
89
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const child = node.children.length === 1 ? node.children[0] : null;
|
|
110
|
-
const content = child && child.type === NodeTypes.TEXT ? child.content : null;
|
|
90
|
+
for (const node of parseVueLike(content.toString('utf-8'), errors)) {
|
|
91
|
+
const { id: sectionId, lang, ...props } = node.props;
|
|
111
92
|
|
|
112
|
-
|
|
113
|
-
|
|
93
|
+
if (!sectionId) {
|
|
94
|
+
errors.push({ name: `missing-id`, message: `Id property is missing`, code: 0, loc: node.loc });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (seenIds.has(sectionId)) {
|
|
98
|
+
errors.push({ name: `duplicate-id`, message: `Duplicate section id: ${sectionId}`, code: 0, loc: node.loc });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
seenIds.add(sectionId);
|
|
114
102
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
errors.push({ name: `duplicate-id`, message: `Duplicate section id: ${sectionId}`, code: 0, loc: node.loc });
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
seenIds.add(sectionId);
|
|
124
|
-
|
|
125
|
-
if (node.tag === 'json') {
|
|
126
|
-
try {
|
|
127
|
-
const jsonData = JSON5.parse(content);
|
|
128
|
-
if (sectionId === METADATA) {
|
|
129
|
-
block.metadata = jsonData;
|
|
130
|
-
} else {
|
|
131
|
-
block.jsons[sectionId] = jsonData;
|
|
132
|
-
}
|
|
133
|
-
} catch (e) {
|
|
134
|
-
errors.push({ name: 'invalid-json', message: `${e}`, code: 0, loc: node.loc });
|
|
135
|
-
}
|
|
103
|
+
if (node.tag === 'json') {
|
|
104
|
+
try {
|
|
105
|
+
const jsonData = JSON5.parse(node.content);
|
|
106
|
+
if (sectionId === METADATA_SECTION_ID) {
|
|
107
|
+
block.metadata = jsonData;
|
|
136
108
|
} else {
|
|
137
|
-
|
|
138
|
-
const lang = getNodeAttr(node, 'lang');
|
|
139
|
-
const lineOffset = node.loc.start.line;
|
|
140
|
-
block.sources[sectionId] = { source, lang, lineOffset };
|
|
109
|
+
block.jsons[sectionId] = jsonData;
|
|
141
110
|
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
errors.push({ name: 'invalid-json', message: `${e}`, code: 0, loc: node.loc });
|
|
142
113
|
}
|
|
114
|
+
} else {
|
|
115
|
+
const source = unescapeCloseTag(node.content.slice(1, -1), node.tag);
|
|
116
|
+
const lineOffset = node.loc.start.line;
|
|
117
|
+
block.sources[sectionId] = { source, lang, props, lineOffset };
|
|
143
118
|
}
|
|
144
|
-
}
|
|
119
|
+
}
|
|
145
120
|
|
|
146
121
|
if (errors.length) {
|
|
147
122
|
throw new Error(`Parsing SFC ${filePath} failed with ${JSON.stringify(errors)}`);
|
|
@@ -150,28 +125,30 @@ export function parseSFC(filePath: string, content: Buffer): SfcBlock {
|
|
|
150
125
|
return block;
|
|
151
126
|
}
|
|
152
127
|
|
|
153
|
-
const PRETTIER_JSON_CONFIG: Options = { parser: 'json5', printWidth: 120, trailingComma: 'all' };
|
|
154
|
-
|
|
155
128
|
export function serializeSfc(block: SfcBlock): { filePath: string; content: Buffer } {
|
|
156
|
-
const filePath =
|
|
157
|
-
if (block.
|
|
158
|
-
if (block.rawContent === null) throw new Error(`SfcBlock must have rawContent if ext!="${
|
|
129
|
+
const filePath = serializeSfcPath(block.path);
|
|
130
|
+
if (block.path.ext !== MARS_SFC_EXT) {
|
|
131
|
+
if (block.rawContent === null) throw new Error(`SfcBlock must have rawContent if ext!="${MARS_SFC_EXT}"`);
|
|
159
132
|
return { filePath, content: block.rawContent };
|
|
160
133
|
}
|
|
161
134
|
|
|
162
|
-
if (block.rawContent !== null) throw new Error(`SfcBlock cannot have rawContent if ext=="${
|
|
135
|
+
if (block.rawContent !== null) throw new Error(`SfcBlock cannot have rawContent if ext=="${MARS_SFC_EXT}"`);
|
|
163
136
|
|
|
164
137
|
let content = '';
|
|
165
138
|
|
|
166
|
-
for (const [name, source] of [[
|
|
167
|
-
const jsonStr =
|
|
139
|
+
for (const [name, source] of [[METADATA_SECTION_ID, block.metadata] as const, ..._.sortBy(Object.entries(block.jsons), e => e[0])]) {
|
|
140
|
+
const jsonStr = stablePrettyJson(source);
|
|
168
141
|
content += `<json id="${escapeHtmlAttr(name)}">\n${jsonStr}</json>\n\n`;
|
|
169
142
|
}
|
|
170
143
|
|
|
171
144
|
for (const [name, source] of _.sortBy(Object.entries(block.sources), e => e[0])) {
|
|
172
145
|
if (!SAVE_EMPTY_SOURCES && !source.source.trim()) continue;
|
|
173
|
-
const tag = (LANG_TAG_MAP as Record<string, string>)[source.lang || ''] || '
|
|
174
|
-
|
|
146
|
+
const tag = (LANG_TAG_MAP as Record<string, string>)[source.lang || ''] || 'text';
|
|
147
|
+
const id = escapeHtmlAttr(name);
|
|
148
|
+
const lang = escapeHtmlAttr(source.lang || '');
|
|
149
|
+
const props = stableHtmlAttributes(source.props);
|
|
150
|
+
const text = escapeCloseTag(source.source, tag);
|
|
151
|
+
content += `<${tag} id="${id}" lang="${lang}"${props}>\n${text}\n</${tag}>\n\n`;
|
|
175
152
|
}
|
|
176
153
|
|
|
177
154
|
return { filePath, content: Buffer.from(content, 'utf-8') };
|