@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.
Files changed (41) hide show
  1. package/dist/cli/init.d.ts.map +1 -1
  2. package/dist/cli/init.js +33 -33
  3. package/dist/cli/migrate.js +3 -3
  4. package/dist/cli/start.js +3 -3
  5. package/dist/configuration.d.ts +9 -3
  6. package/dist/configuration.d.ts.map +1 -1
  7. package/dist/configuration.js +95 -34
  8. package/dist/index.d.ts +4 -3
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +6 -5
  11. package/dist/launcher.d.ts.map +1 -1
  12. package/dist/launcher.js +6 -5
  13. package/dist/loader.js +5 -5
  14. package/dist/utils/compile.d.ts.map +1 -1
  15. package/dist/utils/compile.js +8 -7
  16. package/dist/utils/{utils.d.ts → fileUtils.d.ts} +1 -5
  17. package/dist/utils/fileUtils.d.ts.map +1 -0
  18. package/dist/utils/fileUtils.js +42 -0
  19. package/dist/utils/sfc.d.ts +24 -24
  20. package/dist/utils/sfc.d.ts.map +1 -1
  21. package/dist/utils/sfc.js +54 -98
  22. package/dist/utils/textUtils.d.ts +17 -0
  23. package/dist/utils/textUtils.d.ts.map +1 -0
  24. package/dist/utils/textUtils.js +105 -0
  25. package/dist/utils/v3.d.ts.map +1 -1
  26. package/dist/utils/v3.js +7 -7
  27. package/package.json +5 -2
  28. package/src/cli/init.ts +32 -31
  29. package/src/cli/migrate.ts +1 -1
  30. package/src/cli/start.ts +1 -1
  31. package/src/configuration.ts +86 -35
  32. package/src/index.ts +4 -3
  33. package/src/launcher.ts +3 -2
  34. package/src/loader.ts +1 -1
  35. package/src/utils/compile.ts +5 -4
  36. package/src/utils/{utils.ts → fileUtils.ts} +0 -18
  37. package/src/utils/sfc.ts +62 -85
  38. package/src/utils/textUtils.ts +82 -0
  39. package/src/utils/v3.ts +6 -7
  40. package/dist/utils/utils.d.ts.map +0 -1
  41. 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 { Configuration } from '../configuration';
6
- import { assert, isDirectory, writeFileMakeDir } from '../utils/utils';
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: projectName,
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: Configuration = {
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: projectName,
95
+ mongoDbName: name.withDashes,
101
96
  azureStorageConnection: '<CONN_STR>',
102
- azureStorageAccountName: projectName,
103
- azureStorageUrl: `https://${projectName}.blob.core.windows.net`,
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(48)).toString('hex'),
102
+ secret: (await randomBytes(32)).toString('hex'),
108
103
  importProjects: [
109
104
  {
110
- name: 'mars-ide',
111
- url: 'https://ide.marscloud.dev',
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
- console.log('Installing dependencies. This might take a couple of minutes.');
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.yellow(`\nMake sure to update parameters in config/default.json`));
135
+ console.log(`\n${chalk.green('Success!')} Created ${projectName} at ${projectDir}`);
138
136
 
139
- console.log(`${chalk.green('Success!')} Created ${projectName} at ${projectDir}`);
140
- console.log(chalk.dim(`\nInside that directory, you can start MarsX with\n\n cd ${projectName}\n npm run start\n`));
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
  }
@@ -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/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
@@ -1,7 +1,7 @@
1
1
  import child_process from 'child_process';
2
2
  import { config } from '../configuration';
3
3
  import { launchBooter } from '../launcher';
4
- import { assert } from '../utils/utils';
4
+ import { assert } from '../utils/textUtils';
5
5
 
6
6
  export const RESTART_EXIT_CODe = 9;
7
7
 
@@ -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 { assert } from './utils/utils';
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
- const ensureType =
14
- <T>(typeName: string) =>
15
- (configName: string): T => {
16
- const value = configModule.get(configName);
17
- assert(typeof value === typeName);
18
- return value as T;
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 ensureString = ensureType<string>('string');
22
- const ensureBoolean = ensureType<boolean>('boolean');
23
- const ensureNumber = ensureType<number>('number');
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 validatedConfig() {
72
+ function validateConfig(): Config {
26
73
  if (configModule.util.getConfigSources().length === 0) {
27
- return new Proxy({} as never, {
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
- return {
35
- production: ensureBoolean('production'),
36
- port: ensureNumber('port'),
37
- blocksDir: path.resolve(ensureString('blocksDir')),
38
- cacheDir: path.resolve(ensureString('cacheDir')),
39
- mongoConn: ensureString('mongoConn'),
40
- mongoDbName: ensureString('mongoDbName'),
41
- azureStorageConnection: ensureString('azureStorageConnection'),
42
- azureStorageAccountName: ensureString('azureStorageAccountName'),
43
- azureStorageUrl: ensureString('azureStorageUrl'),
44
- secret: ensureString('secret'),
45
- webFilesTable: ensureString('webFilesTable'),
46
- webRecentFilesTable: ensureString('webRecentFilesTable'),
47
- webFilesBlob: ensureString('webFilesBlob'),
48
- importProjects: configModule.get<ImportProjectConfig[]>('importProjects'),
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 config = validatedConfig();
53
- export type Configuration = typeof config;
104
+ export const CustomEnvironmentVariables = getEnvVarMapping();
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
- export { config, ImportProjectConfig, Configuration } from './configuration';
1
+ export { config, ImportProjectConfig, Config } from './configuration';
2
2
  export { compileSfcSource, transpileTypescript } from './utils/compile';
3
- export { parseSFC, serializeSfc, SfcBlock, SfcIdentity, SfcSource, parseSfcIdentity, serializeSfcIdentity } from './utils/sfc';
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 { assert, writeFileMakeDir } from './utils/utils';
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.identity.name === booterBlockName);
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/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[]> {
@@ -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 { assert, writeFileMakeDir } from './utils';
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.identity.filePath}`);
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.identity.filePath}.${sourceId}.js`);
53
- const originalFilePath = path.join(config.blocksDir, sfcBlock.identity.filePath);
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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, TextModes } from '@vue/compiler-core';
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 prettier, { Options } from 'prettier';
9
- import { escapeCloseTag, escapeHtmlAttr, unescapeCloseTag } from './utils';
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 SfcIdentity = {
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 function parseSfcIdentity(filePath: string): SfcIdentity {
26
- // Blog/BlogPost.page.vue => {id: "BlogPost.page", folder:"Blog", name: "BlogPost", blockTypeName: "page", ext: "vue", fullName: "Blog.BlogPost"}
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 serializeSfcIdentity(identity: SfcIdentity): string {
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 identity = parseSfcIdentity(filePath);
83
- if (identity.ext !== SFC_EXT) {
84
- return { identity, metadata: {}, jsons: {}, sources: {}, rawContent: content };
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
- identity: parseSfcIdentity(filePath),
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
- ast.children.forEach(node => {
108
- if (node.type === NodeTypes.ELEMENT) {
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
- if (content) {
113
- const sectionId = getNodeAttr(node, 'id');
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
- if (!sectionId) {
116
- errors.push({ name: `missing-id`, message: `Id property is missing`, code: 0, loc: node.loc });
117
- return;
118
- }
119
- if (seenIds.has(sectionId)) {
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
- const source = unescapeCloseTag(content.slice(1, -1), node.tag);
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 = serializeSfcIdentity(block.identity);
157
- if (block.identity.ext !== SFC_EXT) {
158
- if (block.rawContent === null) throw new Error(`SfcBlock must have rawContent if ext!="${SFC_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=="${SFC_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 [[METADATA, block.metadata] as const, ..._.sortBy(Object.entries(block.jsons), e => e[0])]) {
167
- const jsonStr = prettier.format('(' + stringify(source) + ')', PRETTIER_JSON_CONFIG);
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 || ''] || 'script';
174
- content += `<${tag} id="${escapeHtmlAttr(name)}" lang="${source.lang}">\n${escapeCloseTag(source.source, tag)}\n</${tag}>\n\n`;
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') };