tungsten 0.4.8 → 0.4.10

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 (95) hide show
  1. package/dist/.build-complete +1 -1
  2. package/dist/Framework.d.ts +1 -0
  3. package/dist/Framework.d.ts.map +1 -1
  4. package/dist/Framework.js +30 -20
  5. package/dist/FrameworkConfig.d.ts +4 -0
  6. package/dist/FrameworkConfig.d.ts.map +1 -1
  7. package/dist/Servers/DevServer.d.ts +0 -1
  8. package/dist/Servers/DevServer.d.ts.map +1 -1
  9. package/dist/Servers/DevServer.js +2 -1
  10. package/dist/Servers/hono/TungstenServerHono.d.ts +1 -1
  11. package/dist/Servers/hono/TungstenServerHono.d.ts.map +1 -1
  12. package/dist/Servers/loadEnvsDev.d.ts.map +1 -1
  13. package/dist/Servers/loadEnvsDev.js +13 -20
  14. package/dist/Servers/tungstenCorePrivateEnvSchema.d.ts +6 -6
  15. package/dist/Servers/tungstenCorePrivateEnvSchema.d.ts.map +1 -1
  16. package/dist/Servers/tungstenCorePrivateEnvSchema.js +12 -22
  17. package/dist/Servers/tungstenCorePublicEnvSchema.d.ts +7 -7
  18. package/dist/Servers/tungstenCorePublicEnvSchema.d.ts.map +1 -1
  19. package/dist/Servers/tungstenCorePublicEnvSchema.js +6 -7
  20. package/dist/api/getApiRoutesHono.d.ts +1 -1
  21. package/dist/api/getApiRoutesHono.d.ts.map +1 -1
  22. package/dist/babelPlugins/BabelAddDevCacheBust.d.ts +1 -0
  23. package/dist/babelPlugins/BabelAddDevCacheBust.d.ts.map +1 -1
  24. package/dist/babelPlugins/BabelAddDevCacheBust.js +1 -0
  25. package/dist/babelPlugins/BabelTransformComponentStyles.d.ts.map +1 -1
  26. package/dist/babelPlugins/BabelTransformComponentStyles.js +4 -2
  27. package/dist/babelPlugins/BabelTransformCssImports.d.ts +1 -0
  28. package/dist/babelPlugins/BabelTransformCssImports.d.ts.map +1 -1
  29. package/dist/babelPlugins/BabelTransformCssImports.js +1 -0
  30. package/dist/babelPlugins/BabelTransformJsxExtensionsToJs.d.ts +1 -0
  31. package/dist/babelPlugins/BabelTransformJsxExtensionsToJs.d.ts.map +1 -1
  32. package/dist/babelPlugins/BabelTransformJsxExtensionsToJs.js +1 -0
  33. package/dist/babelPlugins/BabelTransformPackageImports.d.ts +1 -0
  34. package/dist/babelPlugins/BabelTransformPackageImports.d.ts.map +1 -1
  35. package/dist/babelPlugins/BabelTransformPackageImports.js +75 -15
  36. package/dist/bin/args.d.ts +11 -0
  37. package/dist/bin/args.d.ts.map +1 -0
  38. package/dist/bin/args.js +77 -0
  39. package/dist/bin/commands/build.d.ts.map +1 -1
  40. package/dist/bin/commands/build.js +15 -4
  41. package/dist/bin/commands/buildHelp.js +4 -1
  42. package/dist/bin/commands/clean.d.ts.map +1 -1
  43. package/dist/bin/commands/clean.js +2 -0
  44. package/dist/bin/commands/compile.d.ts.map +1 -1
  45. package/dist/bin/commands/compile.js +76 -75
  46. package/dist/bin/commands/dev.d.ts.map +1 -1
  47. package/dist/bin/commands/dev.js +23 -8
  48. package/dist/bin/commands/devmon.d.ts.map +1 -1
  49. package/dist/bin/commands/devmon.js +26 -30
  50. package/dist/bin/commands/help.js +2 -2
  51. package/dist/bin/commands/start.d.ts.map +1 -1
  52. package/dist/bin/commands/start.js +2 -1
  53. package/dist/bin/commands/static.d.ts.map +1 -1
  54. package/dist/bin/commands/static.js +2 -0
  55. package/dist/bin/index.d.ts.map +1 -1
  56. package/dist/bin/index.js +35 -1
  57. package/dist/bin/moduleLoadAnalysis.d.ts +2 -0
  58. package/dist/bin/moduleLoadAnalysis.d.ts.map +1 -0
  59. package/dist/bin/moduleLoadAnalysis.js +243 -0
  60. package/dist/build/compileJavascript.d.ts +2 -0
  61. package/dist/build/compileJavascript.d.ts.map +1 -1
  62. package/dist/build/compileJavascript.js +71 -13
  63. package/dist/build/compileUserApp.d.ts +1 -1
  64. package/dist/build/compileUserApp.d.ts.map +1 -1
  65. package/dist/build/compileUserApp.js +4 -2
  66. package/dist/build/makeBabelUserDevConfig.d.ts +1 -1
  67. package/dist/build/makeBabelUserDevConfig.d.ts.map +1 -1
  68. package/dist/build/makeBabelUserDevConfig.js +7 -3
  69. package/dist/build/sassImportParser.d.ts.map +1 -1
  70. package/dist/build/sassImportParser.js +5 -2
  71. package/dist/build/tallyBabelPlugins.d.ts +3 -0
  72. package/dist/build/tallyBabelPlugins.d.ts.map +1 -0
  73. package/dist/build/tallyBabelPlugins.js +68 -0
  74. package/dist/env/getEnvSchemas.d.ts +3 -3
  75. package/dist/env/getEnvSchemas.d.ts.map +1 -1
  76. package/dist/env/getEnvSchemas.js +9 -15
  77. package/dist/env/setPrivateEnvValidator.d.ts +2 -2
  78. package/dist/env/setPrivateEnvValidator.d.ts.map +1 -1
  79. package/dist/env/setPublicEnvValidator.d.ts +2 -2
  80. package/dist/env/setPublicEnvValidator.d.ts.map +1 -1
  81. package/dist/frameworkConfigSchema.d.ts +61 -192
  82. package/dist/frameworkConfigSchema.d.ts.map +1 -1
  83. package/dist/frameworkConfigSchema.js +49 -62
  84. package/dist/getUserConfig.d.ts.map +1 -1
  85. package/dist/getUserConfig.js +5 -5
  86. package/dist/moduleReloader.d.ts +1 -3
  87. package/dist/moduleReloader.d.ts.map +1 -1
  88. package/dist/moduleReloader.js +17 -15
  89. package/dist/standardSchema.d.ts +37 -0
  90. package/dist/standardSchema.d.ts.map +1 -0
  91. package/dist/standardSchema.js +47 -0
  92. package/dist/timeProfiler.d.ts +6 -0
  93. package/dist/timeProfiler.d.ts.map +1 -1
  94. package/dist/timeProfiler.js +253 -10
  95. package/package.json +3 -5
@@ -1,67 +1,54 @@
1
- import { type as arkType } from 'arktype';
2
- const babelParserOptionsType = arkType({
3
- 'allowImportExportEverywhere?': 'boolean',
4
- 'allowAwaitOutsideFunction?': 'boolean',
5
- 'allowReturnOutsideFunction?': 'boolean',
6
- 'allowNewTargetOutsideFunction?': 'boolean',
7
- 'allowSuperOutsideMethod?': 'boolean',
8
- 'allowUndeclaredExports?': 'boolean',
9
- 'annexB?': 'boolean',
10
- 'attachComment?': 'boolean',
11
- 'errorRecovery?': 'boolean',
12
- 'sourceType?': '"script" | "module" | "unambiguous"',
13
- 'sourceFilename?': 'string',
14
- 'startLine?': 'number',
15
- 'startColumn?': 'number',
16
- //'plugins?': ParserPlugin[]
17
- 'plugins?': 'unknown[]',
18
- 'strictMode?': 'boolean',
19
- 'ranges?': 'boolean',
20
- 'tokens?': ' boolean',
21
- 'createParenthesizedExpressions?': 'boolean',
22
- 'createImportExpressions?': 'boolean'
23
- });
24
- const frameworkConfigBaseValidator = arkType({
25
- 'codeDir?': 'string',
26
- 'copy?': 'string',
27
- 'exclude?': 'string',
28
- 'copyExclude?': 'string',
29
- 'sourceDir?': 'string',
30
- 'publicDir?': 'string',
31
- 'assetsDir?': 'string',
32
- 'styleImportBase?': 'string',
33
- //publicDir?: string
34
- //siteBasePath?: "" | `/${string}`,
35
- 'siteBasePath?': '"" | /^\\/.*$/',
36
- 'clientSideNav?': 'boolean',
37
- 'staticSite?': 'boolean',
1
+ // exactOptional, not optional, throughout: tsconfig has exactOptionalPropertyTypes, and optional()
2
+ // widens the value to include undefined, which FrameworkConfig's consumers then reject
3
+ import * as v from 'valibot';
4
+ // shape only - babel validates its own options, and a hand-copied key list just drifts from theirs
5
+ const babelParserOptionsSchema = v.pipe(v.looseObject({}),
6
+ // eslint-disable-next-line no-type-assertion/no-type-assertion -- see above
7
+ v.transform(options => options));
8
+ const stringOrStrings = v.union([v.string(), v.array(v.string())]);
9
+ const frameworkConfigBaseEntries = {
10
+ codeDir: v.exactOptional(v.string()),
11
+ copy: v.exactOptional(v.string()),
12
+ exclude: v.exactOptional(v.string()),
13
+ copyExclude: v.exactOptional(v.string()),
14
+ sourceDir: v.exactOptional(v.string()),
15
+ publicDir: v.exactOptional(v.string()),
16
+ assetsDir: v.exactOptional(v.string()),
17
+ styleImportBase: v.exactOptional(v.string()),
18
+ // valibot has no template-literal schema, so the regex proves what the cast then states
19
+ siteBasePath: v.exactOptional(v.pipe(v.union([v.literal(''), v.pipe(v.string(), v.regex(/^\/.*$/))]),
20
+ // eslint-disable-next-line no-type-assertion/no-type-assertion -- see above
21
+ v.transform(path => path))),
22
+ clientSideNav: v.exactOptional(v.boolean()),
23
+ staticSite: v.exactOptional(v.boolean()),
38
24
  // not ready // uniqueComponents?: boolean
39
25
  // not ready // bindMode?: BindMode
40
- //appLocation: AppLocation
41
- //useRootComponent: boolean
42
- //rootComponent?: string
43
- 'routesLocation?': ['string', 'string'],
44
- 'envSchemasLocation?': 'string',
45
- 'babelOptions?': babelParserOptionsType,
46
- 'packageSystem?': '"rewrites" | "aliases"',
47
- 'ignoreDirs?': 'string | string[]',
26
+ routesLocation: v.exactOptional(v.tuple([v.string(), v.string()])),
27
+ envSchemasLocation: v.exactOptional(v.string()),
28
+ babelOptions: v.exactOptional(babelParserOptionsSchema),
29
+ packageSystem: v.exactOptional(v.picklist(['rewrites', 'aliases'])),
30
+ ignoreDirs: v.exactOptional(stringOrStrings),
48
31
  // rename to ignoreFolders?
49
- 'ignorePatterns?': 'string | string[]',
50
- 'skipPackageCompilation?': 'string | string[]',
51
- 'serverMiddleware?': ['string', 'string', 'string'],
52
- 'apiRoutes?': 'string',
53
- 'apiPath?': 'string',
54
- 'csr?': 'boolean'
55
- });
56
- const frameworkConfigWithRootComponentValidator = arkType({
57
- '...': frameworkConfigBaseValidator,
58
- // in an intersection, non-overlapping values at isAdmin would result in a ParseError
59
- 'useRootComponent?': 'true',
60
- rootComponent: 'string'
32
+ ignorePatterns: v.exactOptional(stringOrStrings),
33
+ // test files: left out of every compile unless `tungsten compile --tests` asks for them -
34
+ // the same two shapes as ignoreDirs/ignorePatterns
35
+ testDirs: v.exactOptional(stringOrStrings),
36
+ testPatterns: v.exactOptional(stringOrStrings),
37
+ skipPackageCompilation: v.exactOptional(stringOrStrings),
38
+ serverMiddleware: v.exactOptional(v.tuple([v.string(), v.string(), v.string()])),
39
+ apiRoutes: v.exactOptional(v.string()),
40
+ apiPath: v.exactOptional(v.string()),
41
+ csr: v.exactOptional(v.boolean())
42
+ };
43
+ const frameworkConfigWithRootComponentSchema = v.object({
44
+ ...frameworkConfigBaseEntries,
45
+ useRootComponent: v.exactOptional(v.literal(true)),
46
+ rootComponent: v.string()
61
47
  });
62
- const frameworkConfigWithoutRootComponentValidator = arkType({
63
- '...': frameworkConfigBaseValidator,
64
- // in an intersection, non-overlapping values at isAdmin would result in a ParseError
65
- 'useRootComponent': 'false'
48
+ const frameworkConfigWithoutRootComponentSchema = v.object({
49
+ ...frameworkConfigBaseEntries,
50
+ useRootComponent: v.literal(false)
66
51
  });
67
- export const frameworkConfigValidator = frameworkConfigWithRootComponentValidator.or(frameworkConfigWithoutRootComponentValidator);
52
+ // variant, not union: a plain union reports one issue at the union itself ("Expected Object but
53
+ // received Object"), where this picks the branch by useRootComponent and reports its actual fields
54
+ export const frameworkConfigValidator = v.variant('useRootComponent', [frameworkConfigWithRootComponentSchema, frameworkConfigWithoutRootComponentSchema]);
@@ -1 +1 @@
1
- {"version":3,"file":"getUserConfig.d.ts","sourceRoot":"","sources":["../src/getUserConfig.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAA;AAMxF,wBAAsB,aAAa,CAC/B,SAAS,GAAE,wBAAwB,GAAG,IAAW,EACjD,MAAM,GAAE,QAAQ,GAAG,OAAiB,GACrC,OAAO,CAAC,uBAAuB,CAAC,CAyBlC"}
1
+ {"version":3,"file":"getUserConfig.d.ts","sourceRoot":"","sources":["../src/getUserConfig.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAA;AAOxF,wBAAsB,aAAa,CAC/B,SAAS,GAAE,wBAAwB,GAAG,IAAW,EACjD,MAAM,GAAE,QAAQ,GAAG,OAAiB,GACrC,OAAO,CAAC,uBAAuB,CAAC,CAsBlC"}
@@ -1,21 +1,21 @@
1
- import { ArkErrors } from 'arktype';
2
1
  import { defaultConfig } from "./defaultConfig.js";
3
2
  import { findUserConfig } from "./findUserConfig.js";
4
3
  import { frameworkConfigValidator } from "./frameworkConfigSchema.js";
5
4
  import { makeFinalConfig } from "./makeFinalConfig.js";
6
5
  import { throwError } from "./throwError.js";
7
6
  import { cliOut } from "./cliOutput.js";
7
+ import { validate } from "./standardSchema.js";
8
8
  export async function getUserConfig(overrides = null, volume = 'noisy') {
9
9
  const userConfigOrNull = await findUserConfig(process.cwd(), volume);
10
10
  //frameworkCon
11
11
  if (userConfigOrNull !== null) {
12
12
  const userConfig = userConfigOrNull;
13
- const result = frameworkConfigValidator(userConfig.data);
14
- if (result instanceof ArkErrors) {
13
+ const result = await validate(frameworkConfigValidator, userConfig.data);
14
+ if (!result.success) {
15
15
  cliOut('Config', userConfig);
16
- throwError('Your tungsten config did not match the required format', JSON.stringify(result.flatProblemsByPath));
16
+ throwError('Your tungsten config did not match the required format', '\n' + result.problems);
17
17
  }
18
- const finalConfig = makeFinalConfig(result, overrides);
18
+ const finalConfig = makeFinalConfig(result.value, overrides);
19
19
  //console.log('finalConfig', finalConfig)
20
20
  return finalConfig;
21
21
  //return makeFinalConfig(userConfig.data)
@@ -1,4 +1,2 @@
1
- export { resolve };
2
- import type { ResolveHook } from 'node:module';
3
- declare const resolve: ResolveHook;
1
+ export {};
4
2
  //# sourceMappingURL=moduleReloader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"moduleReloader.d.ts","sourceRoot":"","sources":["../src/moduleReloader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,OAAO,EAAC,CAAA;AAGhB,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,aAAa,CAAA;AA2B5C,QAAA,MAAM,OAAO,EAAE,WAmCd,CAAA"}
1
+ {"version":3,"file":"moduleReloader.d.ts","sourceRoot":"","sources":["../src/moduleReloader.ts"],"names":[],"mappings":""}
@@ -1,9 +1,5 @@
1
- export { resolve };
2
- import { register, isBuiltin } from 'node:module';
3
- if (!import.meta.url.endsWith('?loader')) {
4
- register(`${import.meta.url}?loader`);
5
- }
6
- const selfURL = import.meta.url.replace(/\?loader$/, '');
1
+ import { registerHooks, isBuiltin } from 'node:module';
2
+ const selfURL = import.meta.url;
7
3
  const isSelf = url => url === selfURL;
8
4
  const hasUrlScheme = specifier => /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier);
9
5
  const isBareImport = specifier => !specifier.startsWith('.') && !specifier.startsWith('/') && !specifier.startsWith('\\') && !hasUrlScheme(specifier);
@@ -11,21 +7,24 @@ const cachedModulePattern = /\._cached_\.(?:[cm]?js|[cm]?ts|jsx|tsx)$/i;
11
7
  function isCachedModule(url) {
12
8
  return cachedModulePattern.test(url.pathname);
13
9
  }
14
- const isStaticModule = url => {
15
- const pathname = url.pathname.toLowerCase();
16
- return pathname.includes('/node_modules/') || pathname.includes('/.packages/');
10
+ // takes the raw url rather than a parsed one, so the check can run before any URL is constructed
11
+ const isStaticUrl = url => {
12
+ const lower = url.toLowerCase();
13
+ return lower.includes('/node_modules/') || lower.includes('/.packages/');
17
14
  };
18
15
  let id = 0;
19
- const resolve = async (specifier, context, nextResolve) => {
20
- const result = await nextResolve(specifier, context);
16
+ // registerHooks (sync, in-thread) rather than register (a separate hooks thread): the thread costs
17
+ // ~0.54ms per resolution against ~0.02ms here, which over a dev startup's module count is ~800ms
18
+ const resolve = (specifier, context, nextResolve) => {
19
+ const result = nextResolve(specifier, context);
21
20
  if (!isSelf(result.url) && !isBuiltin(result.url) && context.parentURL !== undefined) {
21
+ // string tests before parsing: registerHooks also sees CJS require, where register did not,
22
+ // and a package's own relative requires are the bulk of those - none can need tagging
23
+ if (isBareImport(specifier) || isStaticUrl(result.url)) return result;
22
24
  const url = new URL(result.url);
23
25
  // A *._cached_.js/ts module and its dependency graph use Node's ordinary
24
26
  // process-wide module cache instead of inheriting the test file instance.
25
27
  if (isCachedModule(url)) return result;
26
- if (isBareImport(specifier) || isStaticModule(url)) {
27
- return result;
28
- }
29
28
  const parentUrl = new URL(context.parentURL);
30
29
  const instance = url.searchParams.get('t-reload') === '' ? `esm-reload-${id++}` : parentUrl.searchParams.get('t-instance');
31
30
  if (instance !== null) {
@@ -40,4 +39,7 @@ const resolve = async (specifier, context, nextResolve) => {
40
39
  }
41
40
  }
42
41
  return result;
43
- };
42
+ };
43
+ registerHooks({
44
+ resolve
45
+ });
@@ -0,0 +1,37 @@
1
+ export type StandardSchemaPathSegment = {
2
+ readonly key: PropertyKey;
3
+ };
4
+ export type StandardSchemaIssue = {
5
+ readonly message: string;
6
+ readonly path?: readonly (PropertyKey | StandardSchemaPathSegment)[] | undefined;
7
+ };
8
+ export type StandardSchemaResult<Output> = {
9
+ readonly value: Output;
10
+ readonly issues?: undefined;
11
+ } | {
12
+ readonly issues: readonly StandardSchemaIssue[];
13
+ };
14
+ export type StandardSchema<Output> = {
15
+ readonly '~standard': {
16
+ readonly version: 1;
17
+ readonly vendor: string;
18
+ readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
19
+ readonly types?: {
20
+ readonly input: unknown;
21
+ readonly output: Output;
22
+ } | undefined;
23
+ };
24
+ };
25
+ type Validation<Output> = {
26
+ success: true;
27
+ value: Output;
28
+ } | {
29
+ success: false;
30
+ problems: string;
31
+ };
32
+ export declare function validate<Output>(schema: StandardSchema<Output>, value: unknown): Promise<Validation<Output>>;
33
+ export declare function formatIssues(issues: readonly StandardSchemaIssue[]): string;
34
+ export declare function checkIsObject(location: string, name: string, value: unknown): void;
35
+ export declare function checkIsStandardSchema(location: string, name: string, schema: unknown): void;
36
+ export {};
37
+ //# sourceMappingURL=standardSchema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standardSchema.d.ts","sourceRoot":"","sources":["../src/standardSchema.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,yBAAyB,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAA;CAAE,CAAA;AAErE,MAAM,MAAM,mBAAmB,GAAG;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,yBAAyB,CAAC,EAAE,GAAG,SAAS,CAAA;CACnF,CAAA;AAED,MAAM,MAAM,oBAAoB,CAAC,MAAM,IACnC;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,mBAAmB,EAAE,CAAA;CAAE,CAAA;AAEzD,MAAM,MAAM,cAAc,CAAC,MAAM,IAAI;IACjC,QAAQ,CAAC,WAAW,EAAE;QAClB,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;QACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,QAAQ,EACjB,CAAC,KAAK,EAAE,OAAO,KAAK,oBAAoB,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAA;QACxF,QAAQ,CAAC,KAAK,CAAC,EAAE;YAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;YAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;SAAE,GAAG,SAAS,CAAA;KACpF,CAAA;CACJ,CAAA;AAED,KAAK,UAAU,CAAC,MAAM,IAAI;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAA;AAGjG,wBAAsB,QAAQ,CAAC,MAAM,EACjC,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,GAC/C,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAI7B;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,mBAAmB,EAAE,GAAG,MAAM,CAE3E;AAWD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAKlF;AAGD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAgB3F"}
@@ -0,0 +1,47 @@
1
+ // Standard Schema v1 (https://standardschema.dev), inlined rather than taken from
2
+ // @standard-schema/spec so nothing new lands in tungsten's shipped types. Only the output type is
3
+ // parameterised - the input is always unknown, whether it is process.env or a user's config file.
4
+ import { throwError } from "./throwError.js";
5
+ // safeParse-shaped, but awaited: the spec lets a schema validate asynchronously
6
+ export async function validate(schema, value) {
7
+ const result = await schema['~standard'].validate(value);
8
+ if (result.issues !== undefined) return {
9
+ success: false,
10
+ problems: formatIssues(result.issues)
11
+ };
12
+ return {
13
+ success: true,
14
+ value: result.value
15
+ };
16
+ }
17
+ export function formatIssues(issues) {
18
+ return issues.map(issue => ` ${pathOf(issue)}: ${issue.message}`).join('\n');
19
+ }
20
+ // a segment is either the key itself or an object wrapping it - both spellings are in the spec
21
+ function pathOf(issue) {
22
+ const segments = issue.path?.map(segment => String(typeof segment === 'object' ? segment.key : segment)) ?? [];
23
+ return segments.length === 0 ? '(whole value)' : segments.join('.');
24
+ }
25
+ // Standard Schema exposes nothing about a schema's structure, so "is it an object schema?" cannot
26
+ // be asked up front the way ZodObject allowed - this asks what it actually produced instead
27
+ export function checkIsObject(location, name, value) {
28
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
29
+ throwError(location, `${name} must produce an object,` + ` but it produced ${Array.isArray(value) ? 'an array' : `type '${typeof value}'`}`);
30
+ }
31
+ }
32
+ // throws rather than returning a result, for the load-time check where there is nothing to recover to
33
+ export function checkIsStandardSchema(location, name, schema) {
34
+ if (schema === null || typeof schema !== 'object') {
35
+ throwError(location, `${name} is not a schema - found ${typeof schema}`);
36
+ }
37
+ if (!('~standard' in schema)) {
38
+ throwError(location, `${name} does not implement Standard Schema (https://standardschema.dev).` + ` Use a recent zod, valibot, arktype or other supporting library.`);
39
+ }
40
+ const props = schema['~standard'];
41
+ if (props === null || typeof props !== 'object' || !('version' in props)) {
42
+ throwError(location, `${name} has a malformed '~standard' property`);
43
+ }
44
+ if (props.version !== 1) {
45
+ throwError(location, `${name} implements Standard Schema v${String(props.version)},` + ` and tungsten supports v1`);
46
+ }
47
+ }
@@ -2,5 +2,11 @@ export type TimeProfiler = {
2
2
  timeProfile: <T>(name: string, task: () => T) => T;
3
3
  timeProfileAsync: <T>(name: string, task: () => T | Promise<T>) => Promise<T>;
4
4
  };
5
+ export declare function isProfiling(): boolean;
6
+ export declare function beginStartupProfiling(profiling: boolean): void;
7
+ export declare function reportStartupPhase(name: string): void;
8
+ export declare function tally(name: string, ms: number): void;
9
+ export declare function addEndOfRunReport(report: () => void): void;
10
+ export declare function reportRunTotal(name: string): void;
5
11
  export declare function createTimeProfiler(profiling?: boolean): TimeProfiler;
6
12
  //# sourceMappingURL=timeProfiler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"timeProfiler.d.ts","sourceRoot":"","sources":["../src/timeProfiler.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,YAAY,GAAG;IACvB,WAAW,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;IAClD,gBAAgB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;CAChF,CAAA;AAMD,wBAAgB,kBAAkB,CAAC,SAAS,UAAO,GAAG,YAAY,CAqBjE"}
1
+ {"version":3,"file":"timeProfiler.d.ts","sourceRoot":"","sources":["../src/timeProfiler.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,YAAY,GAAG;IACvB,WAAW,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;IAClD,gBAAgB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;CAChF,CAAA;AA+MD,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAGD,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAG9D;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAKrD;AAOD,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAKpD;AA0BD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG,IAAI,CAE1D;AAGD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CASjD;AAED,wBAAgB,kBAAkB,CAAC,SAAS,UAAO,GAAG,YAAY,CAoCjE"}
@@ -1,11 +1,239 @@
1
1
  // Same shape as sxy-test-runner's createTimeProfiler (node_modules/sxy-test-runner/dist/packages/timeProfiler.js) -
2
2
  // a disabled profiler just runs the task, so callers never need to branch on whether profiling
3
- // is on. Framework.ts holds one instance per run (default disabled); tungsten dev/build enable
4
- // it via --profile and wrap their own top-level phases with it too.
3
+ // is on. Framework.ts holds one instance per run (default disabled); tungsten dev/build/compile
4
+ // enable it via --profile and wrap their own top-level phases with it too.
5
+ import { performance } from 'node:perf_hooks';
6
+ import { AsyncLocalStorage, createHook } from 'node:async_hooks';
5
7
  import chalk from 'chalk-extensions';
6
8
  import { cliOut } from "./cliOutput.js";
7
- function report(name, ms) {
8
- cliOut(chalk.yellow`${name}` + ' - ' + chalk.orange`${ms.toLocaleString()}ms`);
9
+ // Nesting has to be recorded, not inferred: sibling phases started together sit inside each other's
10
+ // time windows, so intervals alone cannot tell a nested call from a concurrent one. Async storage
11
+ // gives each task its own phase even when several run at once.
12
+ const currentPhase = new AsyncLocalStorage();
13
+ const framesInFlight = [];
14
+ const resourceOwner = new Map();
15
+ let accounting = false;
16
+ // Attributes each async callback's synchronous run to the phase that created the resource. Only
17
+ // under --profile: the hooks fire on every async operation and are not free.
18
+ function startCpuAccounting() {
19
+ if (accounting) return;
20
+ accounting = true;
21
+ createHook({
22
+ init(asyncId, kind) {
23
+ const phase = currentPhase.getStore();
24
+ if (phase === undefined) return;
25
+ resourceOwner.set(asyncId, {
26
+ phase,
27
+ created: performance.now(),
28
+ kind
29
+ });
30
+ },
31
+ before(asyncId) {
32
+ const owner = resourceOwner.get(asyncId);
33
+ if (owner === undefined) return;
34
+ const now = performance.now();
35
+ const wait = owner.phase.waits.get(owner.kind) ?? {
36
+ ms: 0,
37
+ count: 0
38
+ };
39
+ wait.ms += now - owner.created;
40
+ wait.count++;
41
+ owner.phase.waits.set(owner.kind, wait);
42
+ framesInFlight.push({
43
+ phase: owner.phase,
44
+ start: now,
45
+ childCpu: 0
46
+ });
47
+ },
48
+ after(asyncId) {
49
+ if (!resourceOwner.has(asyncId)) return;
50
+ const frame = framesInFlight.pop();
51
+ if (frame === undefined) return;
52
+ const total = performance.now() - frame.start;
53
+ const own = total - frame.childCpu;
54
+ frame.phase.cpu += own;
55
+ // deferred work: started here, ran after this phase returned
56
+ if (frame.phase.finished) frame.phase.cpuAfterEnd += own;
57
+ const parent = framesInFlight[framesInFlight.length - 1];
58
+ if (parent !== undefined) parent.childCpu += total;
59
+ },
60
+ destroy(asyncId) {
61
+ resourceOwner.delete(asyncId);
62
+ }
63
+ }).enable();
64
+ }
65
+ // Every phase reported in the run. performance.now() is process-relative, so startup phases and
66
+ // each profiler's phases share one timeline - which is what makes the overlaps legible: several
67
+ // of these phases run concurrently (the compile targets, and the wrappers around them), so their
68
+ // durations deliberately do not sum to the total.
69
+ const phases = [];
70
+ function ms(value) {
71
+ return `${Math.round(value).toLocaleString()}ms`;
72
+ }
73
+ // Created when the phase starts, so async resources made inside it can be charged to it while it
74
+ // runs; end is filled in by finishPhase().
75
+ function startPhase(name, start, parent) {
76
+ const phase = {
77
+ name,
78
+ start,
79
+ end: start,
80
+ depth: parent === undefined ? 0 : parent.depth + 1,
81
+ cpu: 0,
82
+ childSyncCpu: 0,
83
+ cpuAfterEnd: 0,
84
+ finished: false,
85
+ parent,
86
+ waits: new Map()
87
+ };
88
+ phases.push(phase);
89
+ return phase;
90
+ }
91
+ function finishPhase(phase, end) {
92
+ phase.end = end;
93
+ phase.finished = true;
94
+ cliOut(chalk.yellow`${phase.name}` + ' - ' + chalk.orange`${ms(end - phase.start)}`
95
+ // the window, not just the duration: concurrent phases share one, and a flat list of
96
+ // durations reads as if they ran one after another
97
+ + chalk.mediumgrey` (${Math.round(phase.start).toLocaleString()} - ${ms(end)})`);
98
+ }
99
+ // The startup phases - node's boot and the command's module evaluation. They run before any phase
100
+ // context exists, so the hooks never see them; nothing else is on the loop then either, which makes
101
+ // their elapsed time their cpu time and keeps the cpu column summing to the run total.
102
+ function report(name, start, end) {
103
+ const phase = startPhase(name, start, undefined);
104
+ phase.cpu = end - start;
105
+ finishPhase(phase, end);
106
+ }
107
+ // A bar alone could not say whether two phases nest or run alongside each other, and at any width a
108
+ // cell is worth enough milliseconds that short phases get padded and then misreport their duration.
109
+ // So the structure is carried by indentation and the "with" note, and the bar only shows placement -
110
+ // drawn thin where it is padding rather than duration, so it cannot be read as a real span.
111
+ function reportTimeline() {
112
+ if (phases.length === 0) return;
113
+ // depth first from the roots, each level in start order - a flat sort by start puts a phase
114
+ // under whatever happened to run before it, which reads as the wrong parent
115
+ const childrenOf = new Map();
116
+ for (const phase of phases) {
117
+ const siblings = childrenOf.get(phase.parent) ?? [];
118
+ siblings.push(phase);
119
+ childrenOf.set(phase.parent, siblings);
120
+ }
121
+ const ordered = [];
122
+ const addBranch = phase => {
123
+ ordered.push(phase);
124
+ for (const child of [...(childrenOf.get(phase) ?? [])].sort((a, b) => a.start - b.start)) {
125
+ addBranch(child);
126
+ }
127
+ };
128
+ for (const root of [...(childrenOf.get(undefined) ?? [])].sort((a, b) => a.start - b.start)) {
129
+ addBranch(root);
130
+ }
131
+ const labelWidth = Math.max(...ordered.map(phase => phase.name.length + phase.depth * 2));
132
+ const total = Math.max(...ordered.map(phase => phase.end));
133
+ // the terminal's width, less the label, the duration and room for the notes after the bar.
134
+ // columns is typed as a number but is undefined when stdout is not a tty, hence Number()
135
+ const columns = Number(process.stdout.columns) || 100;
136
+ const barWidth = Math.max(20, Math.min(90, columns - labelWidth - 34));
137
+ // everything before the notes: indent, label, elapsed, cpu, their separators, and the bar
138
+ const rowWidth = 2 + labelWidth + 1 + 9 + 1 + 13 + 1 + barWidth;
139
+ cliOut('');
140
+ cliOut(chalk.mediumgrey`phases - indented by nesting. elapsed is wall clock and overlaps; cpu is` + chalk.mediumgrey` work started by this phase, wherever it ran, and sums to the run total`);
141
+ for (const phase of ordered) {
142
+ // cpu is charged to the phase that created the work, so this is already exclusive of child
143
+ // phases once their synchronous starts are removed - unlike elapsed, these figures do sum
144
+ const ownCpu = phase.cpu - phase.childSyncCpu;
145
+ const from = Math.min(Math.round(phase.start / total * barWidth), barWidth - 1);
146
+ const to = Math.min(Math.max(Math.round(phase.end / total * barWidth), from + 1), barWidth);
147
+ const glyph = (phase.end - phase.start) / total * barWidth < 1 ? '▏' : '█';
148
+ const label = ' '.repeat(phase.depth) + phase.name;
149
+ // Solid for the phase's own cpu, hollow for the rest of its span - a phase that is mostly
150
+ // hollow was mostly waiting on the thread rather than using it. A proportion, not a
151
+ // position: the cpu is interleaved across the span, not spent at the front of it.
152
+ const cells = to - from;
153
+ const workCells = Math.min(cells, Math.max(ownCpu > 0 ? 1 : 0, Math.round(ownCpu / total * barWidth)));
154
+ const bar = glyph === '▏' ? glyph : chalk.orange`${'█'.repeat(workCells)}` + chalk.mediumgrey`${'░'.repeat(cells - workCells)}`;
155
+ // Work is charged to the phase that created the resource, wherever it later runs, so a
156
+ // phase that queues work for something else to await can report more cpu than it existed
157
+ // for - the excess ran after it returned, and whatever awaited it will read as ~0.
158
+ // the kind of async work the phase spent longest waiting on, named as node names it
159
+ const [waitedOn] = [...phase.waits].sort((a, b) => b[1].ms - a[1].ms);
160
+ const fullNotes = (phase.cpuAfterEnd > 1 ? ` ${ms(phase.cpuAfterEnd)} of it deferred` : '') + (waitedOn !== undefined && waitedOn[1].ms > 1 ? ` waits ${waitedOn[0]} x${waitedOn[1].count}` : '');
161
+ // trimmed to what is left of the line - a wrapped note breaks the column alignment of
162
+ // every row after it, which is worse than losing the end of one label
163
+ const notes = fullNotes.slice(0, Math.max(0, columns - rowWidth - 1));
164
+ cliOut(' ' + chalk.yellow`${label.padEnd(labelWidth)}` + ' ' + chalk.orange`${ms(phase.end - phase.start).padStart(9)}` + ' ' + chalk.mediumgrey`${`cpu ${ms(ownCpu)}`.padStart(13)}` + ' ' + ' '.repeat(from) + bar
165
+ // only pad out to the full bar when something follows it
166
+ + (notes === '' ? '' : ' '.repeat(barWidth - to) + chalk.mediumgrey`${notes}`));
167
+ }
168
+ }
169
+ // Phases no Framework profiler can see: node's own boot, then evaluating the import tree of the
170
+ // command module bin/index.ts dynamically imports. performance.now() counts from process start,
171
+ // so the first phase reported measures from there; state is module-level because index.ts starts
172
+ // the timing and the command module it imports finishes it.
173
+ let startupProfiling = false;
174
+ let lastPhaseEnd = 0;
175
+ // Whether --profile is on, for the instrumentation deeper in the build to consult directly. A
176
+ // global rather than a threaded parameter: profiling is a diagnostic, and carrying its switches
177
+ // through every signature between the CLI and a babel visitor would touch code that never uses them.
178
+ export function isProfiling() {
179
+ return startupProfiling;
180
+ }
181
+ // call from the CLI entry once the command is known
182
+ export function beginStartupProfiling(profiling) {
183
+ startupProfiling = profiling;
184
+ reportStartupPhase('node boot + cli entry');
185
+ }
186
+ export function reportStartupPhase(name) {
187
+ if (!startupProfiling) return;
188
+ const now = performance.now();
189
+ report(name, lastPhaseEnd, now);
190
+ lastPhaseEnd = now;
191
+ }
192
+ // Per-operation totals, for pricing one call inside a phase - a phase says where time went, this
193
+ // says what it went on. Accumulating is two clock reads, so callers need not check whether
194
+ // profiling is on; only the report is gated.
195
+ const tallies = new Map();
196
+ export function tally(name, ms) {
197
+ const entry = tallies.get(name) ?? {
198
+ calls: 0,
199
+ ms: 0
200
+ };
201
+ entry.calls++;
202
+ entry.ms += ms;
203
+ tallies.set(name, entry);
204
+ }
205
+ // Deliberately no async variant. Concurrent operations overlap, so summing their wall clock counts
206
+ // the same time once per operation - 291 parallel transforms tallied as 689,576ms of an 11s run.
207
+ // Time async work with a phase, whose cpu figure is attributed rather than summed.
208
+ function reportTallies() {
209
+ if (tallies.size === 0) return;
210
+ cliOut('');
211
+ cliOut(chalk.mediumgrey`tallied work - synchronous totals across the run, nested counted in both`);
212
+ const nameWidth = Math.max(...[...tallies.keys()].map(name => name.length));
213
+ for (const [name, {
214
+ calls,
215
+ ms: total
216
+ }] of [...tallies].sort((a, b) => b[1].ms - a[1].ms)) {
217
+ cliOut(' ' + chalk.yellow`${name.padEnd(nameWidth)}` + ' ' + chalk.orange`${ms(total).padStart(10)}` + ' ' + chalk.mediumgrey`${`${calls.toLocaleString()} calls`.padStart(13)}` + ' ' + chalk.mediumgrey`${`${(total / calls).toFixed(2)}ms each`}`);
218
+ }
219
+ }
220
+ // Other reports to print where the run total goes - the module load analysis registers itself here.
221
+ // It cannot wait for process exit: dev and compile --watch keep running and are killed by a signal,
222
+ // which skips exit handlers entirely.
223
+ const endOfRunReports = [];
224
+ export function addEndOfRunReport(report) {
225
+ endOfRunReports.push(report);
226
+ }
227
+ // wall clock for the whole command, node boot and module loading included
228
+ export function reportRunTotal(name) {
229
+ const total = performance.now();
230
+ // --profile-modules works on its own, so the extra reports are not gated on --profile
231
+ if (startupProfiling) {
232
+ reportTimeline();
233
+ reportTallies();
234
+ cliOut(chalk.yellow`${name}` + ' - ' + chalk.orange`${ms(total)}`);
235
+ }
236
+ for (const report of endOfRunReports) report();
9
237
  }
10
238
  export function createTimeProfiler(profiling = true) {
11
239
  if (!profiling) {
@@ -14,17 +242,32 @@ export function createTimeProfiler(profiling = true) {
14
242
  timeProfileAsync: async (_name, task) => task()
15
243
  };
16
244
  }
245
+ startCpuAccounting();
17
246
  return {
18
247
  timeProfile(name, task) {
19
- const start = Date.now();
20
- const result = task();
21
- report(name, Date.now() - start);
248
+ const parent = currentPhase.getStore();
249
+ const start = performance.now();
250
+ const phase = startPhase(name, start, parent);
251
+ const result = currentPhase.run(phase, task);
252
+ const end = performance.now();
253
+ // wholly synchronous, so none of it crosses an async callback for the hooks to charge
254
+ phase.cpu += end - start;
255
+ if (parent !== undefined) parent.childSyncCpu += end - start;
256
+ finishPhase(phase, end);
22
257
  return result;
23
258
  },
24
259
  async timeProfileAsync(name, task) {
25
- const start = Date.now();
26
- const result = await task();
27
- report(name, Date.now() - start);
260
+ const parent = currentPhase.getStore();
261
+ const start = performance.now();
262
+ const phase = startPhase(name, start, parent);
263
+ // the task runs synchronously up to its first await, and that stretch belongs to this
264
+ // phase too - the hooks only see work that arrives through an async callback
265
+ const pending = currentPhase.run(phase, task);
266
+ const prefix = performance.now() - start;
267
+ phase.cpu += prefix;
268
+ if (parent !== undefined) parent.childSyncCpu += prefix;
269
+ const result = await pending;
270
+ finishPhase(phase, performance.now());
28
271
  return result;
29
272
  }
30
273
  };