@astryxdesign/cli 0.7.12-canary.e0821c1 → 0.7.12-canary.f36bd72

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 (48) hide show
  1. package/api/component/__fixtures__/resolutions/modules.txt +3 -3
  2. package/api/render/_adapter.mjs +9 -0
  3. package/api/render/render.doc.mjs +2 -2
  4. package/api/swizzle/_moduleGraph.d.mts +7 -0
  5. package/api/swizzle/_moduleGraph.mjs +191 -83
  6. package/api/swizzle/copy/copy.mjs +28 -6
  7. package/api/template/template.mjs +1 -0
  8. package/assets/discover/official-packages.json +16 -0
  9. package/assets/docs/styling-overview.doc.mjs +3 -3
  10. package/assets/docs/tree/render-pages.doc.mjs +1 -1
  11. package/assets/docs/tree/tailwind.doc.mjs +41 -1
  12. package/assets/docs/tree/type-scale.doc.mjs +19 -2
  13. package/assets/templates/blocks/components/Hooks/useInputStatusIconHookUsage.tsx +1 -0
  14. package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.doc.mjs +14 -0
  15. package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.tsx +56 -0
  16. package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.doc.mjs +2 -2
  17. package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.tsx +66 -10
  18. package/assets/templates/blocks/components/Link/useLinkifyHookUsage.doc.mjs +14 -0
  19. package/assets/templates/blocks/components/Link/useLinkifyHookUsage.tsx +39 -0
  20. package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.doc.mjs +14 -0
  21. package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.tsx +83 -0
  22. package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.doc.mjs +14 -0
  23. package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.tsx +35 -0
  24. package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.doc.mjs +14 -0
  25. package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.tsx +97 -0
  26. package/assets/templates/pages/table-filter/page.tsx +15 -23
  27. package/assets/templates/pages/table-inbox/page.tsx +3 -6
  28. package/assets/templates/themes/butter/butterTheme.ts +1 -1
  29. package/clients/cli/lib/resolve-theme.mjs +16 -7
  30. package/foundation/discovery/hook-discovery.mjs +31 -8
  31. package/foundation/doc-compiler/import.d.mts +5 -1
  32. package/foundation/doc-compiler/import.mjs +40 -6
  33. package/foundation/fs/import-url.d.mts +20 -0
  34. package/foundation/fs/import-url.mjs +22 -0
  35. package/foundation/fs/module-loader.d.mts +24 -3
  36. package/foundation/fs/module-loader.mjs +69 -47
  37. package/foundation/render/embedded-imports.d.mts +7 -7
  38. package/foundation/render/embedded-imports.mjs +63 -54
  39. package/foundation/render/page-worker.mjs +103 -52
  40. package/foundation/render/ts-paths.d.mts +63 -0
  41. package/foundation/render/ts-paths.mjs +138 -0
  42. package/foundation/response/error-codes.doc.mjs +2 -2
  43. package/foundation/response/error-codes.mjs +3 -2
  44. package/foundation/text/levenshtein.d.mts +6 -1
  45. package/foundation/text/levenshtein.mjs +25 -9
  46. package/foundation/text/suggest-order.d.mts +8 -1
  47. package/foundation/text/suggest-order.mjs +72 -6
  48. package/package.json +6 -6
@@ -271,7 +271,7 @@ useIsomorphicLayoutEffect useIsomorphicLayoutEffect hooks/useAnimatedLayout.doc.
271
271
  useLayerDismissal useLayerDismissal Layer/Layer.doc.mjs Layer/useLayerDismissal.ts
272
272
  useLightbox useLightbox Lightbox/useLightbox.doc.mjs Lightbox/useLightbox.tsx
273
273
  useLinkComponent useLinkComponent Link/useLinkComponent.doc.mjs Link/useLinkComponent.ts
274
- useLinkify useLinkify Link/Link.doc.mjs Link/useLinkify.tsx
274
+ useLinkify useLinkify Link/useLinkify.doc.mjs Link/useLinkify.tsx
275
275
  useListFormatter useListFormatter i18n/InternationalizationProvider.doc.mjs i18n/useListFormatter.ts
276
276
  useMenuDrillIn useMenuDrillIn DropdownMenu/DropdownMenu.doc.mjs DropdownMenu/useMenuDrillIn.tsx
277
277
  useMenuHover useMenuHover hooks/useAnimatedLayout.doc.mjs hooks/useMenuHover.ts
@@ -282,7 +282,7 @@ useOverlay useOverlay Overlay/useOverlay.doc.mjs Overlay/useOverlay.tsx
282
282
  useOwnScrollGesture useOwnScrollGesture DateInput/DateInput.doc.mjs DateInput/useOwnScrollGesture.ts
283
283
  usePager usePager hooks/useAnimatedLayout.doc.mjs hooks/usePager.ts
284
284
  usePointerDragScroll usePointerDragScroll DateInput/DateInput.doc.mjs DateInput/usePointerDragScroll.ts
285
- usePowerSearchConfig usePowerSearchConfig PowerSearch/PowerSearch.doc.mjs PowerSearch/usePowerSearchConfig.ts
285
+ usePowerSearchConfig usePowerSearchConfig PowerSearch/usePowerSearchConfig.doc.mjs PowerSearch/usePowerSearchConfig.ts
286
286
  usePowerSearchSource usePowerSearchSource PowerSearch/PowerSearch.doc.mjs PowerSearch/usePowerSearchSource.ts
287
287
  useRenamedProp useRenamedProp hooks/useAnimatedLayout.doc.mjs hooks/useRenamedProp.ts
288
288
  useResolvedRequired useResolvedRequired hooks/useAnimatedLayout.doc.mjs hooks/useResolvedRequired.ts
@@ -298,7 +298,7 @@ useSpeechRecognition useSpeechRecognition Chat/Chat.doc.mjs Chat/useSpeechRecogn
298
298
  useSwipeAction useSwipeAction Item/Item.doc.mjs Item/useSwipeAction.ts
299
299
  useSwitchTouch useSwitchTouch Switch/Switch.doc.mjs Switch/useSwitchTouch.ts
300
300
  useTableCellStyles useTableCellStyles Table/Table.doc.mjs Table/useTableCellStyles.ts
301
- useTableColumnSettingsState useTableColumnSettingsState Table/Table.doc.mjs Table/plugins/columnSettings/useTableColumnSettingsState.tsx
301
+ useTableColumnSettingsState useTableColumnSettingsState Table/plugins/columnSettings/useTableColumnSettingsState.doc.mjs Table/plugins/columnSettings/useTableColumnSettingsState.tsx
302
302
  useTableSortableState useTableSortableState Table/plugins/sortable/useTableSortableState.doc.mjs Table/plugins/sortable/useTableSortableState.tsx
303
303
  useToastGesture useToastGesture Toast/Toast.doc.mjs Toast/useToastGesture.ts
304
304
  useTouchTrigger useTouchTrigger Layer/Layer.doc.mjs Layer/useTouchTrigger.ts
@@ -548,6 +548,15 @@ export function reverseToFile({cwd, input, output, overwrite}) {
548
548
  ...(missing.length > 0 ? {missing} : {}),
549
549
  };
550
550
  }
551
+ // A file a render wrote, whose source block was taken out on the way
552
+ // (sanitizers and editors strip <script> elements), says so.
553
+ if (!isAstryxMarkup(html) && /<meta name="generator" content="Astryx[ "]/.test(html)) {
554
+ throw new AstryxError(
555
+ `${shown(cwd, input)} was rendered by Astryx, but its embedded source was removed (a sanitizer or an editor strips <script> blocks) and it has no Astryx components to rebuild from; nothing was written. Render the page again from its source and send the new file as is.`,
556
+ [],
557
+ ERROR_CODES.ERR_RENDER_SOURCE_DAMAGED,
558
+ );
559
+ }
551
560
  if (!isAstryxMarkup(html)) {
552
561
  throw new AstryxError(
553
562
  `${shown(cwd, input)} is not an Astryx page: it has no embedded source and no Astryx components to rebuild from. To start an Astryx page like it, run astryx build "<what the page is>".`,
@@ -99,9 +99,9 @@ export const doc = {
99
99
  {code: 'ERR_RENDER_FAILED', when: 'the page throws while rendering for any other reason'},
100
100
  {
101
101
  code: 'ERR_RENDER_NOT_ASTRYX',
102
- when: 'reversing a file with no embedded source and no Astryx markup; the suggestion is `astryx build "<what the page is>"`',
102
+ when: 'reversing a file a render did not write, with no embedded source and no Astryx markup; the suggestion is `astryx build "<what the page is>"`',
103
103
  },
104
- {code: 'ERR_RENDER_SOURCE_DAMAGED', when: 'reversing a file whose embedded source is damaged, lists a path twice, carries a module, stylesheet or JSON file the page does not reach through its imports, or an image, font or media file whose bytes are not one, or carries a file a render never embeds (a dotfile or dot-folder, `node_modules`, a package manifest, lockfile or tool configuration, or any kind but a page module, stylesheet, JSON or image)'},
104
+ {code: 'ERR_RENDER_SOURCE_DAMAGED', when: 'reversing a file a render wrote whose embedded source was removed (and that has no Astryx components to rebuild from), a file whose embedded source is damaged, lists a path twice, carries a module, stylesheet or JSON file the page does not reach through its imports, or an image, font or media file whose bytes are not one, or carries a file a render never embeds (a dotfile or dot-folder, `node_modules`, a package manifest, lockfile or tool configuration, or any kind but a page module, stylesheet, JSON or image)'},
105
105
  {code: 'ERR_PATH_TRAVERSAL', when: 'a file would be written outside the current folder (symlinks followed), or through a symbolic link: `out`, or with `reverse` the page or an embedded file; or, with `reverse`, a file other than the page would go into a folder that already exists'},
106
106
  ],
107
107
  examples: [
@@ -16,6 +16,8 @@
16
16
  * file the copy holds (absolute source path), with the edits its imports need
17
17
  * @property {Map<string, Set<string>>} dependsOn file -> carried files its
18
18
  * computed specifiers point at
19
+ * @property {Map<string, string>} otherPackages the source root of each other
20
+ * Astryx package the copy carries modules from, with its package name
19
21
  * @property {Record<string, string>} dependencies each npm package the copied
20
22
  * files import, other than the owner package and Node's built-in modules,
21
23
  * with the range the owner package declares
@@ -162,6 +164,11 @@ export type CopyPlan = {
162
164
  * computed specifiers point at
163
165
  */
164
166
  dependsOn: Map<string, Set<string>>;
167
+ /**
168
+ * the source root of each other
169
+ * Astryx package the copy carries modules from, with its package name
170
+ */
171
+ otherPackages: Map<string, string>;
165
172
  /**
166
173
  * each npm package the copied
167
174
  * files import, other than the owner package and Node's built-in modules,
@@ -14,6 +14,7 @@ import * as fs from 'node:fs';
14
14
  import * as path from 'node:path';
15
15
  import {isBuiltin} from 'node:module';
16
16
  import {packageOf} from '../../foundation/render/served-packages.mjs';
17
+ import {resolveInstalledPackageDir} from '../../foundation/integrations/installed-package.mjs';
17
18
  import {parse} from '@babel/parser';
18
19
  import {
19
20
  inspectPackageExport,
@@ -390,6 +391,8 @@ function publicSurfaceOf(packageDir, srcRoot, originsOf) {
390
391
  * file the copy holds (absolute source path), with the edits its imports need
391
392
  * @property {Map<string, Set<string>>} dependsOn file -> carried files its
392
393
  * computed specifiers point at
394
+ * @property {Map<string, string>} otherPackages the source root of each other
395
+ * Astryx package the copy carries modules from, with its package name
393
396
  * @property {Record<string, string>} dependencies each npm package the copied
394
397
  * files import, other than the owner package and Node's built-in modules,
395
398
  * with the range the owner package declares
@@ -429,19 +432,6 @@ export function planCopy({
429
432
  });
430
433
  /** @param {string} file @param {string} name */
431
434
  const originOf = (file, name) => originsOf(file, 'type').get(name) ?? null;
432
- /**
433
- * The subpaths that export a module's export: as a value when the import
434
- * takes a value, since a type-only export is erased at runtime.
435
- * @param {string} file
436
- * @param {string} name
437
- * @param {boolean} typeOnly
438
- */
439
- const exportersOf = (file, name, typeOnly) => {
440
- const origin = originOf(file, name);
441
- if (!origin) return [];
442
- const kind = typeOnly ? 'type' : 'value';
443
- return surface.byOrigin[kind].get(`${origin.file}#${origin.name}`) ?? [];
444
- };
445
435
  /**
446
436
  * The name a module exports one of its own bindings under, or null.
447
437
  * @param {{file: string, name: string}} origin
@@ -454,7 +444,6 @@ export function planCopy({
454
444
  };
455
445
  /** @type {Map<string, string|null>} */
456
446
  const resolutions = new Map();
457
- const surface = publicSurfaceOf(packageDir, srcRoot, originsOf);
458
447
  /** @type {CopyPlan['files']} */
459
448
  const files = new Map();
460
449
  /** @type {CopyPlan['dependsOn']} */
@@ -472,73 +461,162 @@ export function planCopy({
472
461
  };
473
462
  /** @param {string} file */
474
463
  const inComponent = file => file.startsWith(componentDir + path.sep);
475
- /** @param {string} file */
476
- const inSource = file => file.startsWith(srcRoot + path.sep);
477
464
 
478
465
  /**
479
- * The subpath that exports every name a module exports, with the same
480
- * definition. An exactly exported file is its own subpath.
481
- * @param {string} file
482
- * @returns {string|null}
466
+ * @typedef {ReturnType<typeof makeContext>} PackageContext
483
467
  */
484
- const subpathCoveringModule = file => {
485
- const exact = surface.byFile.get(file);
486
- if (exact) return exact;
487
- const names = [...originsOf(file, 'type').keys()];
488
- if (names.length === 0) return null;
489
- const values = originsOf(file, 'value');
490
- const options = names.map(name =>
491
- exportersOf(file, name, !values.has(name))
492
- .filter(o => o.name === name)
493
- .map(o => o.subpath),
494
- );
495
- const common = options.reduce((acc, list) =>
496
- acc.filter(s => list.includes(s)),
497
- );
498
- return STYLEX_FILE.test(file) ? null : pickSubpath(common, file);
499
- };
500
468
 
501
469
  /**
502
- * The public subpath to take a name from. A StyleX module is imported by
503
- * its own exported path, because the StyleX compiler resolves styles and
504
- * variables from the real module; when that path is not exported, there is
505
- * no public import.
506
- * @param {string[]} subpaths subpaths that export the name
507
- * @param {string} target the module the import names
508
- * @param {string} [originFile] the module that defines the name
509
- * @returns {string|null}
470
+ * How references inside one package's source are planned: its public
471
+ * surface and the subpaths a name or module is imported from.
472
+ * @param {{packageDir: string, srcRoot: string, ownerPackage: string}} pkg
510
473
  */
511
- const choosePublic = (subpaths, target, originFile) => {
512
- const exact = originFile ? surface.byFile.get(originFile) : undefined;
513
- if (exact && subpaths.includes(exact)) return exact;
514
- if (
515
- STYLEX_FILE.test(target) ||
516
- (originFile && STYLEX_FILE.test(originFile))
517
- )
518
- return null;
519
- return pickSubpath(subpaths, target);
474
+ function makeContext({packageDir, srcRoot, ownerPackage}) {
475
+ const surface = publicSurfaceOf(packageDir, srcRoot, originsOf);
476
+ /** @param {string} file */
477
+ const inSource = file => file.startsWith(srcRoot + path.sep);
478
+ /**
479
+ * The subpaths that export a module's export: as a value when the import
480
+ * takes a value, since a type-only export is erased at runtime.
481
+ * @param {string} file
482
+ * @param {string} name
483
+ * @param {boolean} typeOnly
484
+ */
485
+ const exportersOf = (file, name, typeOnly) => {
486
+ const origin = originOf(file, name);
487
+ if (!origin) return [];
488
+ const kind = typeOnly ? 'type' : 'value';
489
+ return surface.byOrigin[kind].get(`${origin.file}#${origin.name}`) ?? [];
490
+ };
491
+ /**
492
+ * The subpath that exports every name a module exports, with the same
493
+ * definition. An exactly exported file is its own subpath.
494
+ * @param {string} file
495
+ * @returns {string|null}
496
+ */
497
+ const subpathCoveringModule = file => {
498
+ const exact = surface.byFile.get(file);
499
+ if (exact) return exact;
500
+ const names = [...originsOf(file, 'type').keys()];
501
+ if (names.length === 0) return null;
502
+ const values = originsOf(file, 'value');
503
+ const options = names.map(name =>
504
+ exportersOf(file, name, !values.has(name))
505
+ .filter(o => o.name === name)
506
+ .map(o => o.subpath),
507
+ );
508
+ const common = options.reduce((acc, list) =>
509
+ acc.filter(s => list.includes(s)),
510
+ );
511
+ return STYLEX_FILE.test(file) ? null : pickSubpath(common, file);
512
+ };
513
+
514
+ /**
515
+ * The public subpath to take a name from. A StyleX module is imported by
516
+ * its own exported path, because the StyleX compiler resolves styles and
517
+ * variables from the real module; when that path is not exported, there is
518
+ * no public import.
519
+ * @param {string[]} subpaths subpaths that export the name
520
+ * @param {string} target the module the import names
521
+ * @param {string} [originFile] the module that defines the name
522
+ * @returns {string|null}
523
+ */
524
+ const choosePublic = (subpaths, target, originFile) => {
525
+ const exact = originFile ? surface.byFile.get(originFile) : undefined;
526
+ if (exact && subpaths.includes(exact)) return exact;
527
+ if (
528
+ STYLEX_FILE.test(target) ||
529
+ (originFile && STYLEX_FILE.test(originFile))
530
+ )
531
+ return null;
532
+ return pickSubpath(subpaths, target);
533
+ };
534
+
535
+ /**
536
+ * Prefer the subpath of the module's top folder, then the package root.
537
+ * @param {string[]} subpaths
538
+ * @param {string} file
539
+ * @returns {string|null}
540
+ */
541
+ const pickSubpath = (subpaths, file) => {
542
+ if (subpaths.length === 0) return null;
543
+ const top = `./${toPosix(path.relative(srcRoot, file)).split('/')[0]}`;
544
+ return (
545
+ subpaths.find(s => s === top) ??
546
+ subpaths.find(s => s === '.') ??
547
+ [...subpaths].sort((a, b) => a.length - b.length)[0]
548
+ );
549
+ };
550
+
551
+ /** @param {string} subpath */
552
+ const publicSpecifier = subpath =>
553
+ subpath === '.' ? ownerPackage : `${ownerPackage}/${subpath.slice(2)}`;
554
+
555
+ return {
556
+ packageDir,
557
+ srcRoot,
558
+ ownerPackage,
559
+ surface,
560
+ inSource,
561
+ exportersOf,
562
+ subpathCoveringModule,
563
+ choosePublic,
564
+ publicSpecifier,
565
+ };
566
+ }
567
+ const owner = makeContext({packageDir, srcRoot, ownerPackage});
568
+ /** @type {Map<string, PackageContext>} other packages' contexts, by source root */
569
+ const others = new Map();
570
+ /** @param {string} file */
571
+ const contextOf = file => {
572
+ if (file.startsWith(srcRoot + path.sep)) return owner;
573
+ for (const [root, context] of others) {
574
+ if (file.startsWith(root + path.sep)) return context;
575
+ }
576
+ return owner;
520
577
  };
521
578
 
522
579
  /**
523
- * Prefer the subpath of the module's top folder, then the package root.
524
- * @param {string[]} subpaths
580
+ * The source module a bare import of another Astryx package's
581
+ * `./internal/*` path names, or null. That module is carried, and its
582
+ * package gets a context of its own.
525
583
  * @param {string} file
584
+ * @param {string} specifier
526
585
  * @returns {string|null}
527
586
  */
528
- const pickSubpath = (subpaths, file) => {
529
- if (subpaths.length === 0) return null;
530
- const top = `./${toPosix(path.relative(srcRoot, file)).split('/')[0]}`;
531
- return (
532
- subpaths.find(s => s === top) ??
533
- subpaths.find(s => s === '.') ??
534
- [...subpaths].sort((a, b) => a.length - b.length)[0]
535
- );
587
+ const internalModuleOf = (file, specifier) => {
588
+ const name = packageOf(specifier);
589
+ const sub = specifier.slice(name.length + 1);
590
+ if (!name.startsWith('@astryxdesign/') || name === ownerPackage)
591
+ return null;
592
+ if (!/^internal(\/|$)/.test(sub)) return null;
593
+ const installed = resolveInstalledPackageDir(name, path.dirname(file));
594
+ if (!installed) return null;
595
+ const dir = fs.realpathSync(installed.packageDir);
596
+ /** @type {{exports?: unknown}} */
597
+ let pkg;
598
+ try {
599
+ pkg = JSON.parse(
600
+ fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'),
601
+ );
602
+ } catch {
603
+ return null;
604
+ }
605
+ const target = sourceExportTarget(pkg, dir, `./${sub}`);
606
+ // A StyleX module's identity is its path, so the copy keeps importing it
607
+ // by its exported path and agrees with the package on it.
608
+ if (!target || STYLEX_FILE.test(target)) return null;
609
+ const src = path.join(dir, 'src');
610
+ const root = target.startsWith(src + path.sep) ? src : dir;
611
+ if (!others.has(root)) {
612
+ others.set(
613
+ root,
614
+ makeContext({packageDir: dir, srcRoot: root, ownerPackage: name}),
615
+ );
616
+ }
617
+ return target;
536
618
  };
537
619
 
538
- /** @param {string} subpath */
539
- const publicSpecifier = subpath =>
540
- subpath === '.' ? ownerPackage : `${ownerPackage}/${subpath.slice(2)}`;
541
-
542
620
  /**
543
621
  * Carry a module in the copy.
544
622
  * @param {string} from
@@ -560,8 +638,14 @@ export function planCopy({
560
638
  const edits = [];
561
639
  files.set(file, {code: parsed.code, edits});
562
640
  if (!CODE_FILE.test(file)) continue;
641
+ const context = contextOf(file);
563
642
  for (const ref of parsed.references) {
564
643
  if (!ref.source.startsWith('.')) {
644
+ const internal = internalModuleOf(file, ref.source);
645
+ if (internal) {
646
+ edits.push(sourceEdit(parsed.code, ref, carry(file, internal)));
647
+ continue;
648
+ }
565
649
  const builtin = ref.source.startsWith('node:') || isBuiltin(ref.source);
566
650
  const name = packageOf(ref.source);
567
651
  if (!builtin && name !== ownerPackage) packages.add(name);
@@ -576,44 +660,61 @@ export function planCopy({
576
660
  else edits.push(sourceEdit(parsed.code, ref, carry(file, target)));
577
661
  continue;
578
662
  }
579
- if (!CODE_FILE.test(target) || !inSource(target)) {
663
+ if (!CODE_FILE.test(target) || !context.inSource(target)) {
580
664
  // An asset, such as `../../locales/en.json` or a stylesheet, that the
581
665
  // package exports by its own path is imported from the package.
582
- const subpath = `./${toPosix(path.relative(packageDir, target))}`;
666
+ const subpath = `./${toPosix(path.relative(context.packageDir, target))}`;
583
667
  if (
584
- subpathDeclaration(surface.exportsField, subpath).state === 'declared'
668
+ subpathDeclaration(context.surface.exportsField, subpath).state ===
669
+ 'declared'
585
670
  ) {
671
+ if (context !== owner) packages.add(context.ownerPackage);
586
672
  edits.push(
587
- sourceEdit(parsed.code, ref, `${ownerPackage}/${subpath.slice(2)}`),
673
+ sourceEdit(
674
+ parsed.code,
675
+ ref,
676
+ `${context.ownerPackage}/${subpath.slice(2)}`,
677
+ ),
588
678
  );
589
679
  continue;
590
680
  }
591
681
  // An unexported asset in the source is carried; one outside it is
592
682
  // left for the build to report.
593
- if (!inSource(target)) continue;
683
+ if (!context.inSource(target)) continue;
594
684
  edits.push(sourceEdit(parsed.code, ref, carry(file, target)));
595
685
  continue;
596
686
  }
597
- edits.push(planReference(file, parsed.code, ref, target));
687
+ edits.push(planReference(context, file, parsed.code, ref, target));
598
688
  }
599
689
  }
600
- return {files, dependsOn, dependencies: declaredRanges(packageDir, packages)};
690
+ return {
691
+ files,
692
+ dependsOn,
693
+ dependencies: declaredRanges(packageDir, packages),
694
+ otherPackages: new Map(
695
+ [...others].map(([root, c]) => [root, c.ownerPackage]),
696
+ ),
697
+ };
601
698
 
602
699
  /**
603
700
  * Decide one reference that leaves the component folder.
701
+ * @param {PackageContext} context the package the importing file is in
604
702
  * @param {string} file
605
703
  * @param {string} code
606
704
  * @param {ModuleReference} ref
607
705
  * @param {string} target
608
706
  * @returns {PlannedEdit}
609
707
  */
610
- function planReference(file, code, ref, target) {
708
+ function planReference(context, file, code, ref, target) {
709
+ // A carried module of another package imports that package's public names
710
+ // from it, so the copy depends on it.
711
+ if (context !== owner) packages.add(context.ownerPackage);
611
712
  if (ref.kind !== 'named' && ref.kind !== 'reexportNamed') {
612
- const covering = subpathCoveringModule(target);
713
+ const covering = context.subpathCoveringModule(target);
613
714
  return sourceEdit(
614
715
  code,
615
716
  ref,
616
- covering ? publicSpecifier(covering) : carry(file, target),
717
+ covering ? context.publicSpecifier(covering) : carry(file, target),
617
718
  );
618
719
  }
619
720
  /** @type {Array<{choice: SpecifierChoice, specs: ImportSpecifier[]}>} */
@@ -630,13 +731,20 @@ export function planCopy({
630
731
  };
631
732
  for (const spec of ref.specifiers) {
632
733
  if (spec.imported === '*') {
633
- const covering = subpathCoveringModule(target);
634
- add(covering ? publicSpecifier(covering) : carry(file, target), spec);
734
+ const covering = context.subpathCoveringModule(target);
735
+ add(
736
+ covering ? context.publicSpecifier(covering) : carry(file, target),
737
+ spec,
738
+ );
635
739
  continue;
636
740
  }
637
741
  const origin = originOf(target, spec.imported);
638
- const publicOptions = exportersOf(target, spec.imported, spec.typeOnly);
639
- const chosen = choosePublic(
742
+ const publicOptions = context.exportersOf(
743
+ target,
744
+ spec.imported,
745
+ spec.typeOnly,
746
+ );
747
+ const chosen = context.choosePublic(
640
748
  publicOptions.map(o => o.subpath),
641
749
  target,
642
750
  origin?.file,
@@ -645,7 +753,7 @@ export function planCopy({
645
753
  const exported = /** @type {{name: string}} */ (
646
754
  publicOptions.find(o => o.subpath === chosen)
647
755
  ).name;
648
- add(publicSpecifier(chosen), {...spec, imported: exported});
756
+ add(context.publicSpecifier(chosen), {...spec, imported: exported});
649
757
  } else {
650
758
  // Take the name from the module that defines it, so the copy carries
651
759
  // that module and not a whole barrel. An origin names a binding, so
@@ -654,7 +762,7 @@ export function planCopy({
654
762
  const definingExport =
655
763
  origin &&
656
764
  origin.file !== target &&
657
- inSource(origin.file) &&
765
+ context.inSource(origin.file) &&
658
766
  !parseModule(target).declares.has(spec.imported)
659
767
  ? exportNameOf(origin)
660
768
  : null;
@@ -361,12 +361,32 @@ export async function swizzleCopy(component, options = {}) {
361
361
  );
362
362
  /** @type {Map<string, string>} */
363
363
  const rootDests = new Map();
364
+ // A module from another Astryx package goes under a folder named after that
365
+ // package, at its path in that package's source.
366
+ /** @param {string} file */
367
+ const otherPackageOf = file => {
368
+ for (const [root, name] of plan.otherPackages) {
369
+ if (file.startsWith(root + path.sep)) return {root, folder: name.split('/').pop() ?? name};
370
+ }
371
+ return null;
372
+ };
373
+ /** @param {string} file the version of the package a helper comes from */
374
+ const versionOf = file => {
375
+ const other = otherPackageOf(file);
376
+ return other
377
+ ? packageVersionOf(findPackageRoot(other.root) ?? other.root)
378
+ : ownerVersion;
379
+ };
364
380
  for (const file of [...allFiles].sort()) {
365
381
  if (layoutDestOf(file) != null) continue;
366
- const packagePath = path.relative(graphRoot, file);
367
- let dest = path.join(outputBase, packagePath);
382
+ const other = otherPackageOf(file);
383
+ const base = other ? other.folder : path.basename(packageDir);
384
+ const packagePath = path.relative(other ? other.root : graphRoot, file);
385
+ let dest = other
386
+ ? path.join(outputBase, base, packagePath)
387
+ : path.join(outputBase, packagePath);
368
388
  for (let n = 1; takenDests.has(dest); n++) {
369
- const folder = n === 1 ? path.basename(packageDir) : `${path.basename(packageDir)}-${n}`;
389
+ const folder = n === 1 && !other ? base : `${base}-${n}`;
370
390
  dest = path.join(outputBase, folder, packagePath);
371
391
  }
372
392
  takenDests.add(dest);
@@ -381,15 +401,17 @@ export async function swizzleCopy(component, options = {}) {
381
401
  // A private attribute owner module and the unexported style modules it
382
402
  // imports travel whole and are replaced only with --overwrite.
383
403
  const ownerGroup = new Set();
404
+ /** @param {string} file the source root a carried module sits under */
405
+ const rootOf = file => otherPackageOf(file)?.root ?? srcRoot;
384
406
  const ownerQueue = helpers.filter(f =>
385
- OWNER_MODULE.test(toModulePath(srcRoot, f)),
407
+ OWNER_MODULE.test(toModulePath(rootOf(f), f)),
386
408
  );
387
409
  while (ownerQueue.length > 0) {
388
410
  const file = /** @type {string} */ (ownerQueue.shift());
389
411
  if (ownerGroup.has(file)) continue;
390
412
  ownerGroup.add(file);
391
413
  for (const dep of plan.dependsOn.get(file) ?? []) {
392
- if (!isComponentFile(dep) && STYLEX_MODULE.test(toModulePath(srcRoot, dep))) {
414
+ if (!isComponentFile(dep) && STYLEX_MODULE.test(toModulePath(rootOf(dep), dep))) {
393
415
  ownerQueue.push(dep);
394
416
  }
395
417
  }
@@ -431,7 +453,7 @@ export async function swizzleCopy(component, options = {}) {
431
453
  attempts.set(file, attempt);
432
454
  redirected.set(
433
455
  file,
434
- versionedPath(baseDestOf(file), ownerVersion, attempt),
456
+ versionedPath(baseDestOf(file), versionOf(file), attempt),
435
457
  );
436
458
  moved = true;
437
459
  }
@@ -193,6 +193,7 @@ export async function template(name, options = {}) {
193
193
  `Unknown template "${name}"`,
194
194
  orderByAffinity(name, templates, t => [t.dirName, t.name], {
195
195
  closest: true,
196
+ typos: true,
196
197
  }).map(t => ({
197
198
  name: t.dirName,
198
199
  reason: `${t.type} template`,
@@ -100,6 +100,10 @@
100
100
  "kind": "component",
101
101
  "name": "ChatUnreadDivider"
102
102
  },
103
+ {
104
+ "kind": "component",
105
+ "name": "ChatVirtualizer"
106
+ },
103
107
  {
104
108
  "kind": "component",
105
109
  "name": "CircleCheckIndicator"
@@ -128,6 +132,10 @@
128
132
  "kind": "component",
129
133
  "name": "CommentThread"
130
134
  },
135
+ {
136
+ "kind": "component",
137
+ "name": "DialogHeroHeader"
138
+ },
131
139
  {
132
140
  "kind": "component",
133
141
  "name": "DiffViewer"
@@ -136,6 +144,10 @@
136
144
  "kind": "component",
137
145
  "name": "EmojiPicker"
138
146
  },
147
+ {
148
+ "kind": "component",
149
+ "name": "LineClamp"
150
+ },
139
151
  {
140
152
  "kind": "component",
141
153
  "name": "ListInput"
@@ -252,6 +264,10 @@
252
264
  "kind": "template",
253
265
  "name": "CommentThreadShowcase"
254
266
  },
267
+ {
268
+ "kind": "template",
269
+ "name": "DialogHeroHeaderShowcase"
270
+ },
255
271
  {
256
272
  "kind": "template",
257
273
  "name": "DiffViewerShowcase"
@@ -90,7 +90,7 @@ export const docs = {
90
90
  {
91
91
  type: 'code',
92
92
  lang: 'bash',
93
- label: 'Install (add to an existing Tailwind v4 app)',
93
+ label: 'Install',
94
94
  code: '{@pm install @astryxdesign/core @stylexjs/stylex @astryxdesign/theme-neutral}',
95
95
  },
96
96
  {
@@ -113,7 +113,7 @@ export const docs = {
113
113
  },
114
114
  {
115
115
  type: 'prose',
116
- text: 'For Tailwind v3, see the layer workaround in {@link generic:migration-setup}.',
116
+ text: 'A project with Tailwind already set up needs a smaller change, which {@link generic:tailwind} shows. For Tailwind v3, see the layer workaround in {@link generic:migration-setup}.',
117
117
  },
118
118
  ],
119
119
  },
@@ -162,7 +162,7 @@ export const docs = {
162
162
  },
163
163
  {
164
164
  type: 'prose',
165
- text: '`{@command swizzle <Component>}` writes an `astryx-swizzle.json` marker into an output folder it owns (one the copy creates, an empty one, or one it marked before); every module under that folder then compiles as Astryx source, so keep copies in a folder of their own, such as the default `components/astryx`. For a folder that already holds other files it writes no marker and says how to add one by hand. With `@astryxdesign/build`\'s Babel, PostCSS and Vite integrations, or `withAstryx()` under webpack, a copy under that folder compiles into `astryx-base`, below `astryx-theme`, so theme component overrides reach it as they reach the package component. Under Turbopack, and with the stock StyleX plugins above, a copy\'s rules land in your own styles, above `astryx-theme`, and theme component overrides do not reach it.',
165
+ text: "`{@command swizzle <Component>}` writes an `astryx-swizzle.json` marker into an output folder it owns (one the copy creates, an empty one, or one it marked before); every module under that folder then compiles as Astryx source, so keep copies in a folder of their own, such as the default `components/astryx`. For a folder that already holds other files it writes no marker and says how to add one by hand. With `@astryxdesign/build`'s Babel, PostCSS and Vite integrations, or `withAstryx()` under webpack, a copy under that folder compiles into `astryx-base`, below `astryx-theme`, so theme component overrides reach it as they reach the package component. Under Turbopack, and with the stock StyleX plugins above, a copy's rules land in your own styles, above `astryx-theme`, and theme component overrides do not reach it.",
166
166
  },
167
167
  ],
168
168
  },
@@ -86,7 +86,7 @@ export const docs = {
86
86
  },
87
87
  {
88
88
  type: 'prose',
89
- text: 'It writes the page, the modules, stylesheets and JSON files the page reaches through its own imports, and the images, fonts and media the render carried (however the page named them, as long as their bytes really are one): each goes where the page\'s relative imports find it from wherever you put the page, and a file the page imports through a path alias (`@/components/Header`) goes where your project\'s `tsconfig.json` or `jsconfig.json` alias points. Every file but the page goes inside the one new folder the restore creates (the topmost folder on the page\'s path that does not exist yet), never into a folder that already exists: a page that imports other files comes back into a new folder (`--out restored/app/page.tsx`), and a file it imports through a path alias comes back only when that alias points inside the new folder. Nor does it write a file that a test, story or benchmark runner would run by its default patterns (Jest, Vitest, Bun, AVA, Playwright, Storybook, Cypress and `node --test`: names such as `*.test.*`, `*.spec.*`, `*_test.*`, `*-test.*`, `*.bench.*`, `*.stories.*`, `*.cy.*`, and anything under `test/`, `tests/` or `__tests__/`), judged from the repository root (every folder above the file up to it), else the topmost folder with a package.json. Restored files are the sender\'s code: read them before you run or import them. Without `--out`, everything comes back into a new folder beside the HTML file, named after it (`page.html` comes back into `page-restored/`). A received file that carries anything else (a file the page never imports, a dotfile, `package.json`, a tool configuration, a path that leaves the folder) is refused with nothing written. An existing file is never replaced unless you pass `--overwrite`, and the refusal names every one; a generated, vendored or ignored file, and any path through a symbolic link, is not written even then. Add `--json` for the result as data: what was written, how big it is, and every warning.',
89
+ text: 'It writes the page, the modules, stylesheets and JSON files the page reaches through its own imports, and the images, fonts and media the render carried (however the page named them, as long as their bytes really are one): each goes where the page\'s relative imports find it from wherever you put the page, and a file the page imports through a path alias (`@/components/Header`) goes where your project\'s `tsconfig.json` or `jsconfig.json` alias points. Every file but the page goes inside the one new folder the restore creates (the topmost folder on the page\'s path that does not exist yet), never into a folder that already exists: a page that imports other files comes back into a new folder (`--out restored/app/page.tsx`), and a file it imports through a path alias comes back only when that alias points inside the new folder. Nor does it write a file that a test, story or benchmark runner would run by its default patterns (Jest, Vitest, Bun, AVA, Playwright, Storybook, Cypress and `node --test`: names such as `*.test.*`, `*.spec.*`, `*_test.*`, `*-test.*`, `*.bench.*`, `*.stories.*`, `*.cy.*`, and anything under `test/`, `tests/` or `__tests__/`), judged from the repository root (every folder above the file up to it), else the topmost folder with a package.json. Restored files are the sender\'s code: read them before you run or import them. Without `--out`, everything comes back into a new folder beside the HTML file, named after it (`page.html` comes back into `page-restored/`). A received file that carries anything else (a file the page never imports, a dotfile, `package.json`, a tool configuration, a path that leaves the folder) is refused with nothing written. A rendered file that lost its embedded source on the way (a sanitizer or an editor strips `<script>` blocks) and has no Astryx components to rebuild from is refused with `ERR_RENDER_SOURCE_DAMAGED`. An existing file is never replaced unless you pass `--overwrite`, and the refusal names every one; a generated, vendored or ignored file, and any path through a symbolic link, is not written even then. Add `--json` for the result as data: what was written, how big it is, and every warning.',
90
90
  },
91
91
  ],
92
92
  },